mirror of
https://github.com/EasyTierMC/ETMC.Web.git
synced 2025-12-09 05:45:49 +08:00
feat(!draft):oauth
This commit is contained in:
83
src/App.vue
83
src/App.vue
@@ -1,19 +1,51 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterLink, RouterView } from 'vue-router'
|
import { RouterLink, RouterView } from 'vue-router'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { isAuthenticated } from '@/utils/request/api'
|
import { isAuthenticated, getMe, logout } from '@/utils/request/api'
|
||||||
|
|
||||||
const year = new Date().getFullYear()
|
const year = new Date().getFullYear()
|
||||||
const isLoggedIn = ref(false)
|
const isLoggedIn = ref(false)
|
||||||
|
const userInfo = ref<{ username?: string; avatar?: string } | null>(null)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
isLoggedIn.value = isAuthenticated()
|
isLoggedIn.value = await isAuthenticated()
|
||||||
|
|
||||||
|
if (isLoggedIn.value) {
|
||||||
|
try {
|
||||||
|
const userData = getMe();
|
||||||
|
if(!userData) return;
|
||||||
|
userInfo.value = userData
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch user info:', error)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
logout(); //防止cookie没了用户数据还在
|
||||||
|
}
|
||||||
|
|
||||||
// Re-check auth status on storage changes
|
// Re-check auth status on storage changes
|
||||||
window.addEventListener('storage', () => {
|
window.addEventListener('storage', async () => {
|
||||||
isLoggedIn.value = isAuthenticated()
|
isLoggedIn.value = await isAuthenticated()
|
||||||
|
if (isLoggedIn.value) {
|
||||||
|
try {
|
||||||
|
const userData = getMe()
|
||||||
|
if(!userData) return;
|
||||||
|
userInfo.value = userData
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch user info:', error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logout(); //防止cookie没了用户数据还在
|
||||||
|
userInfo.value = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout()
|
||||||
|
isLoggedIn.value = false
|
||||||
|
userInfo.value = null
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -24,13 +56,50 @@ onMounted(() => {
|
|||||||
<h1 class="btn btn-ghost normal-case text-2xl font-bold">EasyTierMC Uptime</h1>
|
<h1 class="btn btn-ghost normal-case text-2xl font-bold">EasyTierMC Uptime</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户信息显示在正中心 -->
|
||||||
|
<div class="navbar-center" v-if="isLoggedIn && userInfo">
|
||||||
|
<div class="flex items-center gap-3 px-4 py-2">
|
||||||
|
<div class="avatar">
|
||||||
|
<div class="w-8 h-8 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2">
|
||||||
|
<img
|
||||||
|
:src="userInfo.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(userInfo.username || 'User')}&background=random`"
|
||||||
|
:alt="userInfo.username || 'User'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium text-sm">{{ userInfo.username || '用户' }}</span>
|
||||||
|
<div class="dropdown dropdown-end">
|
||||||
|
<label tabindex="0" class="btn btn-ghost btn-circle btn-xs">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||||
|
<li><RouterLink to="/dashboard" class="flex items-center gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
管理控制台
|
||||||
|
</RouterLink></li>
|
||||||
|
<li><button @click="handleLogout" class="flex items-center gap-2 text-error">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
|
</svg>
|
||||||
|
退出登录
|
||||||
|
</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="navbar-end lg:flex">
|
<div class="navbar-end lg:flex">
|
||||||
<ul class="menu menu-horizontal px-1 gap-2">
|
<ul class="menu menu-horizontal px-1 gap-2">
|
||||||
<li><RouterLink to="/" class="btn btn-ghost">主页</RouterLink></li>
|
<li><RouterLink to="/" class="btn btn-ghost">主页</RouterLink></li>
|
||||||
<li><RouterLink to="/monitor" class="btn btn-ghost">节点监控</RouterLink></li>
|
<li><RouterLink to="/monitor" class="btn btn-ghost">节点监控</RouterLink></li>
|
||||||
<li><RouterLink to="/submit" class="btn btn-ghost">提交节点</RouterLink></li>
|
<li><RouterLink to="/submit" class="btn btn-ghost">提交节点</RouterLink></li>
|
||||||
<li v-if="isLoggedIn"><RouterLink to="/dashboard" class="btn btn-ghost">管理控制台</RouterLink></li>
|
<li v-if="!isLoggedIn"><RouterLink to="/login" class="btn btn-ghost">登录</RouterLink></li>
|
||||||
<li v-else><RouterLink to="/login" class="btn btn-ghost">登录</RouterLink></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import HomeView from '../views/HomeView.vue'
|
import HomeView from '../views/HomeView.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
|
|||||||
@@ -6,6 +6,23 @@ export interface HealthResponse {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IMe{
|
||||||
|
username:string,
|
||||||
|
avatar:string,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ILogin{
|
||||||
|
info:{
|
||||||
|
login:string,
|
||||||
|
avatar_url:string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function login(code:string){
|
||||||
|
const res = await Api.post('http://localhost:3000/auth/oauth?code='+code);
|
||||||
|
return res as unknown as ILogin
|
||||||
|
}
|
||||||
|
|
||||||
export function getHealth() {
|
export function getHealth() {
|
||||||
const path = import.meta.env.VITE_HEALTH_PATH || '/health'
|
const path = import.meta.env.VITE_HEALTH_PATH || '/health'
|
||||||
return Api.get<HealthResponse>(path)
|
return Api.get<HealthResponse>(path)
|
||||||
@@ -20,25 +37,34 @@ export interface LoginResponse {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(username: string, password: string) {
|
// export async function login(username: string, password: string) {
|
||||||
const res = (await Api.post<LoginResponse>('/auth/login', { username, password })) as unknown as LoginResponse
|
// const res = (await Api.post<LoginResponse>('/auth/login', { username, password })) as unknown as LoginResponse
|
||||||
const token = (res as any)?.data.token
|
// const token = (res as any)?.data.token
|
||||||
if (token) {
|
// if (token) {
|
||||||
localStorage.setItem('auth_token', token)
|
// localStorage.setItem('auth_token', token)
|
||||||
}
|
// }
|
||||||
return res
|
// return res
|
||||||
}
|
// }
|
||||||
|
|
||||||
export function logout() {
|
export function logout() {
|
||||||
localStorage.removeItem('auth_token')
|
sessionStorage.removeItem('me')
|
||||||
|
cookieStore.delete('auth_token')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAuthenticated() {
|
export async function isAuthenticated() {
|
||||||
return !!localStorage.getItem('auth_token')
|
const s = sessionStorage.getItem('me');
|
||||||
|
const c = await cookieStore.get('auth_token')
|
||||||
|
return !!(s&&c)
|
||||||
|
//return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMe() {
|
export function getMe() {
|
||||||
return (Api.get('/auth/me') as Promise<any>)
|
let result:IMe|undefined = undefined;
|
||||||
|
const localme = sessionStorage.getItem('me')
|
||||||
|
if(localme){
|
||||||
|
result = JSON.parse(localme)
|
||||||
|
}
|
||||||
|
return /*(Api.get('/auth/me') as Promise<any>)*/result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin list (requires admin role)
|
// Admin list (requires admin role)
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ const Api = axios.create({
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
withCredentials: false,
|
withCredentials: true,
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
})
|
})
|
||||||
|
|
||||||
Api.interceptors.request.use(
|
Api.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const token = localStorage.getItem('auth_token')
|
const token = cookieStore.get('auth_token')
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers = config.headers || {}
|
config.headers = config.headers || {}
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
|||||||
@@ -1,13 +1,35 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { isAuthenticated, login } from '@/utils/request/api'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
onMounted(async ()=>{
|
||||||
|
const isLoggedIn = await isAuthenticated()
|
||||||
|
if(route.query.code&&typeof route.query.code === 'string'&&!isLoggedIn){
|
||||||
|
loading.value = true
|
||||||
|
login(route.query.code).then(res=>{
|
||||||
|
sessionStorage.setItem('me',
|
||||||
|
JSON.stringify({
|
||||||
|
username: res.info.login,
|
||||||
|
avatar: res.info.avatar_url
|
||||||
|
})
|
||||||
|
)
|
||||||
|
window.location.reload(); //为什么不直接跳转,问就是单页路由的锅
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if(isLoggedIn){
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
})
|
||||||
// GitHub登录 - 直接跳转到后端处理的OAuth端点
|
// GitHub登录 - 直接跳转到后端处理的OAuth端点
|
||||||
function loginWithGitHub() {
|
function loginWithGitHub() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
// 跳转到后端的GitHub OAuth登录端点
|
const thisUrl = window.location.href;
|
||||||
window.location.href = '/auth/github'
|
window.location.href = `https://github.com/login/oauth/authorize?response_type=code&redirect_uri=${encodeURIComponent(thisUrl)}&client_id=Iv23liuQomBDjQIlOaYL`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user