mirror of
https://github.com/EasyTierMC/ETMC.Web.git
synced 2025-12-07 21:15:48 +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">
|
||||
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>
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user