feat(!draft):oauth

This commit is contained in:
Tianpao
2025-12-07 12:53:45 +08:00
parent cf0327cd04
commit b3b01fa892
5 changed files with 141 additions and 25 deletions

View File

@@ -1,19 +1,51 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
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 isLoggedIn = ref(false)
const userInfo = ref<{ username?: string; avatar?: string } | null>(null)
onMounted(() => {
isLoggedIn.value = isAuthenticated()
onMounted(async () => {
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
window.addEventListener('storage', () => {
isLoggedIn.value = isAuthenticated()
window.addEventListener('storage', async () => {
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>
<template>
@@ -24,13 +56,50 @@ onMounted(() => {
<h1 class="btn btn-ghost normal-case text-2xl font-bold">EasyTierMC Uptime</h1>
</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">
<ul class="menu menu-horizontal px-1 gap-2">
<li><RouterLink to="/" 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 v-if="isLoggedIn"><RouterLink to="/dashboard" class="btn btn-ghost">管理控制台</RouterLink></li>
<li v-else><RouterLink to="/login" class="btn btn-ghost">登录</RouterLink></li>
<li v-if="!isLoggedIn"><RouterLink to="/login" class="btn btn-ghost">登录</RouterLink></li>
</ul>
</div>
</header>

View File

@@ -1,6 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [

View File

@@ -6,6 +6,23 @@ export interface HealthResponse {
[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() {
const path = import.meta.env.VITE_HEALTH_PATH || '/health'
return Api.get<HealthResponse>(path)
@@ -20,25 +37,34 @@ export interface LoginResponse {
[key: string]: any
}
export async function login(username: string, password: string) {
const res = (await Api.post<LoginResponse>('/auth/login', { username, password })) as unknown as LoginResponse
const token = (res as any)?.data.token
if (token) {
localStorage.setItem('auth_token', token)
}
return res
}
// export async function login(username: string, password: string) {
// const res = (await Api.post<LoginResponse>('/auth/login', { username, password })) as unknown as LoginResponse
// const token = (res as any)?.data.token
// if (token) {
// localStorage.setItem('auth_token', token)
// }
// return res
// }
export function logout() {
localStorage.removeItem('auth_token')
sessionStorage.removeItem('me')
cookieStore.delete('auth_token')
}
export function isAuthenticated() {
return !!localStorage.getItem('auth_token')
export async function isAuthenticated() {
const s = sessionStorage.getItem('me');
const c = await cookieStore.get('auth_token')
return !!(s&&c)
//return true
}
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)

View File

@@ -5,13 +5,13 @@ const Api = axios.create({
headers: {
'Content-Type': 'application/json'
},
withCredentials: false,
withCredentials: true,
timeout: 30000
})
Api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth_token')
const token = cookieStore.get('auth_token')
if (token) {
config.headers = config.headers || {}
config.headers.Authorization = `Bearer ${token}`

View File

@@ -1,13 +1,35 @@
<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)
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端点
function loginWithGitHub() {
loading.value = true
// 跳转到后端的GitHub OAuth登录端点
window.location.href = '/auth/github'
const thisUrl = window.location.href;
window.location.href = `https://github.com/login/oauth/authorize?response_type=code&redirect_uri=${encodeURIComponent(thisUrl)}&client_id=Iv23liuQomBDjQIlOaYL`
}
</script>