/** * Authentication Store * Manages user authentication state, login/logout, token refresh, and token persistence */ import { defineStore } from 'pinia' import { ref, computed, readonly } from 'vue' import { authAPI, isTotp2FARequired, type LoginResponse } from '@/api' import type { User, LoginRequest, RegisterRequest, AuthResponse } from '@/types' const AUTH_TOKEN_KEY = 'auth_token' const AUTH_USER_KEY = 'auth_user' const REFRESH_TOKEN_KEY = 'refresh_token' const TOKEN_EXPIRES_AT_KEY = 'token_expires_at' // 存储过期时间戳而非有效期 const AUTO_REFRESH_INTERVAL = 60 * 1000 // 60 seconds for user data refresh const TOKEN_REFRESH_BUFFER = 120 * 1000 // 120 seconds before expiry to refresh token export const useAuthStore = defineStore('auth', () => { // ==================== State ==================== const user = ref(null) const token = ref(null) const refreshTokenValue = ref(null) const tokenExpiresAt = ref(null) // 过期时间戳(毫秒) const runMode = ref<'standard' | 'simple'>('standard') let refreshIntervalId: ReturnType | null = null let tokenRefreshTimeoutId: ReturnType | null = null // ==================== Computed ==================== const isAuthenticated = computed(() => { return !!token.value && !!user.value }) const isAdmin = computed(() => { return user.value?.role === 'admin' }) const isSimpleMode = computed(() => runMode.value === 'simple') // ==================== Actions ==================== /** * Initialize auth state from localStorage * Call this on app startup to restore session * Also starts auto-refresh and immediately fetches latest user data */ function checkAuth(): void { const savedToken = localStorage.getItem(AUTH_TOKEN_KEY) const savedUser = localStorage.getItem(AUTH_USER_KEY) const savedRefreshToken = localStorage.getItem(REFRESH_TOKEN_KEY) const savedExpiresAt = localStorage.getItem(TOKEN_EXPIRES_AT_KEY) if (savedToken && savedUser) { try { token.value = savedToken user.value = JSON.parse(savedUser) refreshTokenValue.value = savedRefreshToken tokenExpiresAt.value = savedExpiresAt ? parseInt(savedExpiresAt, 10) : null // Immediately refresh user data from backend (async, don't block) refreshUser().catch((error) => { console.error('Failed to refresh user on init:', error) }) // Start auto-refresh interval for user data startAutoRefresh() // Start proactive token refresh if we have refresh token and expiry info // Note: use !== null to handle case when tokenExpiresAt.value is 0 (expired) if (savedRefreshToken && tokenExpiresAt.value !== null) { scheduleTokenRefreshAt(tokenExpiresAt.value) } } catch (error) { console.error('Failed to parse saved user data:', error) clearAuth() } } } /** * Start auto-refresh interval for user data * Refreshes user data every 60 seconds */ function startAutoRefresh(): void { // Clear existing interval if any stopAutoRefresh() refreshIntervalId = setInterval(() => { if (token.value) { refreshUser().catch((error) => { console.error('Auto-refresh user failed:', error) }) } }, AUTO_REFRESH_INTERVAL) } /** * Stop auto-refresh interval */ function stopAutoRefresh(): void { if (refreshIntervalId) { clearInterval(refreshIntervalId) refreshIntervalId = null } } /** * Schedule proactive token refresh before expiry (based on expiry timestamp) * @param expiresAtMs - Token expiry timestamp in milliseconds */ function scheduleTokenRefreshAt(expiresAtMs: number): void { // Clear any existing timeout if (tokenRefreshTimeoutId) { clearTimeout(tokenRefreshTimeoutId) tokenRefreshTimeoutId = null } // Calculate remaining time until refresh (buffer time before expiry) const now = Date.now() const refreshInMs = Math.max(0, expiresAtMs - now - TOKEN_REFRESH_BUFFER) if (refreshInMs <= 0) { // Token is about to expire or already expired, refresh immediately performTokenRefresh() return } tokenRefreshTimeoutId = setTimeout(() => { performTokenRefresh() }, refreshInMs) } /** * Schedule proactive token refresh before expiry (based on expires_in seconds) * @param expiresInSeconds - Token expiry time in seconds from now */ function scheduleTokenRefresh(expiresInSeconds: number): void { const expiresAtMs = Date.now() + expiresInSeconds * 1000 tokenExpiresAt.value = expiresAtMs localStorage.setItem(TOKEN_EXPIRES_AT_KEY, String(expiresAtMs)) scheduleTokenRefreshAt(expiresAtMs) } /** * Perform the actual token refresh */ async function performTokenRefresh(): Promise { if (!refreshTokenValue.value) { return } try { const response = await authAPI.refreshToken() // Update state token.value = response.access_token refreshTokenValue.value = response.refresh_token // Schedule next refresh (this also updates tokenExpiresAt and localStorage) scheduleTokenRefresh(response.expires_in) } catch (error) { console.error('Token refresh failed:', error) // Don't clear auth here - the interceptor will handle 401 errors } } /** * Stop token refresh timeout */ function stopTokenRefresh(): void { if (tokenRefreshTimeoutId) { clearTimeout(tokenRefreshTimeoutId) tokenRefreshTimeoutId = null } } /** * User login * @param credentials - Login credentials (email and password) * @returns Promise resolving to the login response (may require 2FA) * @throws Error if login fails */ async function login(credentials: LoginRequest): Promise { try { const response = await authAPI.login(credentials) // If 2FA is required, return the response without setting auth state if (isTotp2FARequired(response)) { return response } // Set auth state from the response setAuthFromResponse(response) return response } catch (error) { // Clear any partial state on error clearAuth() throw error } } /** * Complete login with 2FA code * @param tempToken - Temporary token from initial login * @param totpCode - 6-digit TOTP code * @returns Promise resolving to the authenticated user * @throws Error if 2FA verification fails */ async function login2FA(tempToken: string, totpCode: string): Promise { try { const response = await authAPI.login2FA({ temp_token: tempToken, totp_code: totpCode }) setAuthFromResponse(response) return user.value! } catch (error) { clearAuth() throw error } } /** * Set auth state from an AuthResponse * Internal helper function */ function setAuthFromResponse(response: AuthResponse): void { // Store token and user token.value = response.access_token // Store refresh token if present if (response.refresh_token) { refreshTokenValue.value = response.refresh_token localStorage.setItem(REFRESH_TOKEN_KEY, response.refresh_token) } // Extract run_mode if present if (response.user.run_mode) { runMode.value = response.user.run_mode } const { run_mode: _run_mode, ...userData } = response.user user.value = userData // Persist to localStorage localStorage.setItem(AUTH_TOKEN_KEY, response.access_token) localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userData)) // Start auto-refresh interval for user data startAutoRefresh() // Start proactive token refresh if we have refresh token and expiry info // scheduleTokenRefresh will also store the expiry timestamp if (response.refresh_token && response.expires_in) { scheduleTokenRefresh(response.expires_in) } } /** * User registration * @param userData - Registration data (username, email, password) * @returns Promise resolving to the newly registered and authenticated user * @throws Error if registration fails */ async function register(userData: RegisterRequest): Promise { try { const response = await authAPI.register(userData) // Use the common helper to set auth state setAuthFromResponse(response) return user.value! } catch (error) { // Clear any partial state on error clearAuth() throw error } } /** * 直接设置 token(用于 OAuth/SSO 回调),并加载当前用户信息。 * 会自动读取 localStorage 中已设置的 refresh_token 和 token_expires_in * @param newToken - 后端签发的 JWT access token */ async function setToken(newToken: string): Promise { // Clear any previous state first (avoid mixing sessions) // Note: Don't clear localStorage here as OAuth callback may have set refresh_token stopAutoRefresh() stopTokenRefresh() token.value = null user.value = null token.value = newToken localStorage.setItem(AUTH_TOKEN_KEY, newToken) // Read refresh token and expires_at from localStorage if set by OAuth callback const savedRefreshToken = localStorage.getItem(REFRESH_TOKEN_KEY) const savedExpiresAt = localStorage.getItem(TOKEN_EXPIRES_AT_KEY) if (savedRefreshToken) { refreshTokenValue.value = savedRefreshToken } if (savedExpiresAt) { tokenExpiresAt.value = parseInt(savedExpiresAt, 10) } try { const userData = await refreshUser() startAutoRefresh() // Start proactive token refresh if we have refresh token and expiry info // Note: use !== null to handle case when tokenExpiresAt.value is 0 (expired) if (savedRefreshToken && tokenExpiresAt.value !== null) { scheduleTokenRefreshAt(tokenExpiresAt.value) } return userData } catch (error) { clearAuth() throw error } } /** * User logout * Clears all authentication state and persisted data */ async function logout(): Promise { // Call API logout (revokes refresh token on server) await authAPI.logout() // Clear state clearAuth() } /** * Refresh current user data * Fetches latest user info from the server * @returns Promise resolving to the updated user * @throws Error if not authenticated or request fails */ async function refreshUser(): Promise { if (!token.value) { throw new Error('Not authenticated') } try { const response = await authAPI.getCurrentUser() if (response.data.run_mode) { runMode.value = response.data.run_mode } const { run_mode: _run_mode, ...userData } = response.data user.value = userData // Update localStorage localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userData)) return userData } catch (error) { // If refresh fails with 401, clear auth state if ((error as { status?: number }).status === 401) { clearAuth() } throw error } } /** * Clear all authentication state * Internal helper function */ function clearAuth(): void { // Stop auto-refresh stopAutoRefresh() // Stop token refresh stopTokenRefresh() token.value = null refreshTokenValue.value = null tokenExpiresAt.value = null user.value = null localStorage.removeItem(AUTH_TOKEN_KEY) localStorage.removeItem(AUTH_USER_KEY) localStorage.removeItem(REFRESH_TOKEN_KEY) localStorage.removeItem(TOKEN_EXPIRES_AT_KEY) } // ==================== Return Store API ==================== return { // State user, token, runMode: readonly(runMode), // Computed isAuthenticated, isAdmin, isSimpleMode, // Actions login, login2FA, register, setToken, logout, checkAuth, refreshUser } })