209 lines
5.4 KiB
TypeScript
209 lines
5.4 KiB
TypeScript
import { navigateTo, useState } from '#app'
|
|
import type { User } from '~~/types'
|
|
|
|
interface LoginResponse {
|
|
success: boolean
|
|
user?: User
|
|
requires2FA?: boolean
|
|
userId?: number
|
|
methods?: string[]
|
|
tempToken?: string // 预认证临时令牌
|
|
}
|
|
|
|
export const useAuth = () => {
|
|
const user = useState<User | null>('user', () => null)
|
|
const token = useState<string | null>('token', () => null)
|
|
const isAuthenticated = useState<boolean>('isAuthenticated', () => false)
|
|
const isAdmin = useState<boolean>('isAdmin', () => false)
|
|
const loading = useState<boolean>('loading', () => false)
|
|
|
|
const clearAuthState = () => {
|
|
user.value = null
|
|
token.value = null
|
|
isAuthenticated.value = false
|
|
isAdmin.value = false
|
|
}
|
|
|
|
const setAuthState = (loggedInUser: User) => {
|
|
token.value = 'cookie-based'
|
|
user.value = loggedInUser
|
|
isAuthenticated.value = true
|
|
isAdmin.value = ['ADMIN', 'SUPER_ADMIN', 'SONG_ADMIN'].includes(loggedInUser.role)
|
|
}
|
|
|
|
const initAuth = async () => {
|
|
// 客户端执行
|
|
if (typeof window === 'undefined' || import.meta.server) {
|
|
return null
|
|
}
|
|
|
|
// 如果已认证,直接返回缓存的用户信息
|
|
if (isAuthenticated.value && user.value) {
|
|
return user.value
|
|
}
|
|
|
|
try {
|
|
const data = await $fetch<{ user: User }>('/api/auth/verify', {
|
|
headers: {
|
|
'Cache-Control': 'no-cache'
|
|
}
|
|
})
|
|
|
|
if (data && data.user) {
|
|
user.value = data.user
|
|
isAuthenticated.value = true
|
|
isAdmin.value = ['ADMIN', 'SUPER_ADMIN', 'SONG_ADMIN'].includes(data.user.role)
|
|
token.value = 'cookie-based'
|
|
return data.user
|
|
} else {
|
|
clearAuthState()
|
|
return null
|
|
}
|
|
} catch (error: any) {
|
|
const hadAuth = isAuthenticated.value
|
|
|
|
// 只有当之前是已认证状态,且接口明确返回401(Token无效/过期),才进行清理和跳转
|
|
if (hadAuth && error.statusCode === 401) {
|
|
clearAuthState()
|
|
// Token失效,重定向到登录页
|
|
await navigateTo('/login?error=SessionExpired')
|
|
} else if (!hadAuth && error.statusCode === 401) {
|
|
// 未登录状态下的 401,仅确保状态清理,不跳转
|
|
clearAuthState()
|
|
}
|
|
return null
|
|
}
|
|
}
|
|
|
|
const login = async (username: string, password: string) => {
|
|
const response = await $fetch<LoginResponse>('/api/auth/login', {
|
|
method: 'POST',
|
|
body: { username, password }
|
|
})
|
|
|
|
if (response.success) {
|
|
if (response.requires2FA) {
|
|
return response
|
|
}
|
|
|
|
if (response.user) {
|
|
setAuthState(response.user)
|
|
return response
|
|
}
|
|
}
|
|
|
|
throw new Error('登录响应格式错误')
|
|
}
|
|
|
|
const verify2FA = async (userId: number, code: string, type: 'totp' | 'email', tempToken?: string) => {
|
|
const response = await $fetch<{ success: boolean; user: User }>('/api/auth/2fa/verify', {
|
|
method: 'POST',
|
|
body: { userId, code, type, token: tempToken }
|
|
})
|
|
|
|
if (response.success && response.user) {
|
|
setAuthState(response.user)
|
|
return response
|
|
}
|
|
throw new Error('验证失败')
|
|
}
|
|
|
|
const changePassword = async (currentPassword: string, newPassword: string) => {
|
|
loading.value = true
|
|
try {
|
|
await $fetch('/api/auth/change-password', {
|
|
method: 'POST',
|
|
body: { currentPassword, newPassword }
|
|
})
|
|
} catch (error: any) {
|
|
// 处理FetchError,提取错误信息
|
|
if (error.data && error.data.statusMessage) {
|
|
throw new Error(error.data.statusMessage)
|
|
} else if (error.data && error.data.message) {
|
|
throw new Error(error.data.message)
|
|
} else if (error.statusMessage) {
|
|
throw new Error(error.statusMessage)
|
|
} else if (error.message) {
|
|
throw new Error(error.message)
|
|
} else {
|
|
throw new Error('密码修改失败,请重试')
|
|
}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const setInitialPassword = async (newPassword: string) => {
|
|
loading.value = true
|
|
try {
|
|
await $fetch('/api/auth/set-initial-password', {
|
|
method: 'POST',
|
|
body: { newPassword }
|
|
})
|
|
if (user.value) {
|
|
user.value.needsPasswordChange = false
|
|
}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const refreshUser = async () => {
|
|
const data = await $fetch<{ user: User }>('/api/auth/verify')
|
|
if (data && data.user) {
|
|
user.value = data.user
|
|
isAuthenticated.value = true
|
|
isAdmin.value = ['ADMIN', 'SUPER_ADMIN', 'SONG_ADMIN'].includes(data.user.role)
|
|
}
|
|
}
|
|
|
|
const register = async (data: Record<string, any>) => {
|
|
const response = await $fetch<LoginResponse>('/api/auth/register', {
|
|
method: 'POST',
|
|
body: data
|
|
})
|
|
if (response.success && response.user) {
|
|
setAuthState(response.user)
|
|
}
|
|
return response
|
|
}
|
|
|
|
const logout = async (redirect = true) => {
|
|
try {
|
|
await $fetch('/api/auth/logout', { method: 'POST' })
|
|
} catch (error) {
|
|
// 忽略登出错误
|
|
}
|
|
|
|
clearAuthState()
|
|
|
|
if (import.meta.client && redirect) {
|
|
await navigateTo('/')
|
|
}
|
|
}
|
|
|
|
const getAuthConfig = () => {
|
|
// 返回认证配置
|
|
return {
|
|
credentials: 'include' as RequestCredentials
|
|
}
|
|
}
|
|
|
|
return {
|
|
user,
|
|
token,
|
|
isAuthenticated,
|
|
isAdmin,
|
|
loading,
|
|
login,
|
|
verify2FA,
|
|
register,
|
|
logout,
|
|
changePassword,
|
|
setInitialPassword,
|
|
refreshUser,
|
|
initAuth,
|
|
getAuthConfig
|
|
}
|
|
}
|