diff --git a/src/composables/useAuth.ts b/src/composables/useAuth.ts new file mode 100644 index 0000000..41066ee --- /dev/null +++ b/src/composables/useAuth.ts @@ -0,0 +1,112 @@ +import { computed, ref } from 'vue' + +export interface AuthUser { + id: string + username: string + role: 'admin' | 'user' +} + +export interface UserSummary extends AuthUser { + createdAt: string +} + +const accessToken = ref(localStorage.getItem('access_token')) +const refreshToken = ref(localStorage.getItem('refresh_token')) +const user = ref(null) + +export const isLoggedIn = computed(() => !!accessToken.value) + +function clearTokens(): void { + accessToken.value = null + refreshToken.value = null + user.value = null + localStorage.removeItem('access_token') + localStorage.removeItem('refresh_token') +} + +async function doRefresh(): Promise { + if (!refreshToken.value) return false + const res = await fetch('/api/auth/refresh', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: refreshToken.value }), + }) + if (!res.ok) { + clearTokens() + return false + } + const body = (await res.json()) as { accessToken: string } + accessToken.value = body.accessToken + localStorage.setItem('access_token', body.accessToken) + return true +} + +export async function authedFetch(path: string, init?: RequestInit): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + ...((init?.headers as Record) ?? {}), + } + if (accessToken.value) headers['Authorization'] = `Bearer ${accessToken.value}` + + let res = await fetch(path, { ...init, headers }) + + if (res.status === 401) { + const refreshed = await doRefresh() + if (!refreshed) throw new Error('未登录') + if (accessToken.value) headers['Authorization'] = `Bearer ${accessToken.value}` + res = await fetch(path, { ...init, headers }) + } + + if (!res.ok) { + const body = (await res.json().catch(() => null)) as { error?: string } | null + throw new Error(body?.error ?? `请求失败(${res.status})`) + } + + return res.json() as Promise +} + +export function useAuth() { + async function login(username: string, password: string): Promise { + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }) + if (!res.ok) { + const body = (await res.json().catch(() => null)) as { error?: string } | null + throw new Error(body?.error ?? '登录失败') + } + const body = (await res.json()) as { + accessToken: string + refreshToken: string + user: AuthUser + } + accessToken.value = body.accessToken + refreshToken.value = body.refreshToken + user.value = body.user + localStorage.setItem('access_token', body.accessToken) + localStorage.setItem('refresh_token', body.refreshToken) + } + + async function logout(): Promise { + if (refreshToken.value) { + await fetch('/api/auth/logout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: refreshToken.value }), + }).catch(() => {}) + } + clearTokens() + } + + async function fetchMe(): Promise { + if (!accessToken.value) return + try { + user.value = await authedFetch('/api/auth/me') + } catch { + clearTokens() + } + } + + return { isLoggedIn, user, login, logout, fetchMe } +}