| |
| |
| |
| |
|
|
| 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 |
| const TOKEN_REFRESH_BUFFER = 120 * 1000 |
|
|
| export const useAuthStore = defineStore('auth', () => { |
| |
|
|
| const user = ref<User | null>(null) |
| const token = ref<string | null>(null) |
| const refreshTokenValue = ref<string | null>(null) |
| const tokenExpiresAt = ref<number | null>(null) |
| const runMode = ref<'standard' | 'simple'>('standard') |
| let refreshIntervalId: ReturnType<typeof setInterval> | null = null |
| let tokenRefreshTimeoutId: ReturnType<typeof setTimeout> | null = null |
|
|
| |
|
|
| const isAuthenticated = computed(() => { |
| return !!token.value && !!user.value |
| }) |
|
|
| const isAdmin = computed(() => { |
| return user.value?.role === 'admin' |
| }) |
|
|
| const isSimpleMode = computed(() => runMode.value === 'simple') |
|
|
| |
|
|
| |
| |
| |
| |
| |
| 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 |
|
|
| |
| refreshUser().catch((error) => { |
| console.error('Failed to refresh user on init:', error) |
| }) |
|
|
| |
| startAutoRefresh() |
|
|
| |
| |
| if (savedRefreshToken && tokenExpiresAt.value !== null) { |
| scheduleTokenRefreshAt(tokenExpiresAt.value) |
| } |
| } catch (error) { |
| console.error('Failed to parse saved user data:', error) |
| clearAuth() |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| function startAutoRefresh(): void { |
| |
| stopAutoRefresh() |
|
|
| refreshIntervalId = setInterval(() => { |
| if (token.value) { |
| refreshUser().catch((error) => { |
| console.error('Auto-refresh user failed:', error) |
| }) |
| } |
| }, AUTO_REFRESH_INTERVAL) |
| } |
|
|
| |
| |
| |
| function stopAutoRefresh(): void { |
| if (refreshIntervalId) { |
| clearInterval(refreshIntervalId) |
| refreshIntervalId = null |
| } |
| } |
|
|
| |
| |
| |
| |
| function scheduleTokenRefreshAt(expiresAtMs: number): void { |
| |
| if (tokenRefreshTimeoutId) { |
| clearTimeout(tokenRefreshTimeoutId) |
| tokenRefreshTimeoutId = null |
| } |
|
|
| |
| const now = Date.now() |
| const refreshInMs = Math.max(0, expiresAtMs - now - TOKEN_REFRESH_BUFFER) |
|
|
| if (refreshInMs <= 0) { |
| |
| performTokenRefresh() |
| return |
| } |
|
|
| tokenRefreshTimeoutId = setTimeout(() => { |
| performTokenRefresh() |
| }, refreshInMs) |
| } |
|
|
| |
| |
| |
| |
| function scheduleTokenRefresh(expiresInSeconds: number): void { |
| const expiresAtMs = Date.now() + expiresInSeconds * 1000 |
| tokenExpiresAt.value = expiresAtMs |
| localStorage.setItem(TOKEN_EXPIRES_AT_KEY, String(expiresAtMs)) |
| scheduleTokenRefreshAt(expiresAtMs) |
| } |
|
|
| |
| |
| |
| async function performTokenRefresh(): Promise<void> { |
| if (!refreshTokenValue.value) { |
| return |
| } |
|
|
| try { |
| const response = await authAPI.refreshToken() |
|
|
| |
| token.value = response.access_token |
| refreshTokenValue.value = response.refresh_token |
|
|
| |
| scheduleTokenRefresh(response.expires_in) |
| } catch (error) { |
| console.error('Token refresh failed:', error) |
| |
| } |
| } |
|
|
| |
| |
| |
| function stopTokenRefresh(): void { |
| if (tokenRefreshTimeoutId) { |
| clearTimeout(tokenRefreshTimeoutId) |
| tokenRefreshTimeoutId = null |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function login(credentials: LoginRequest): Promise<LoginResponse> { |
| try { |
| const response = await authAPI.login(credentials) |
|
|
| |
| if (isTotp2FARequired(response)) { |
| return response |
| } |
|
|
| |
| setAuthFromResponse(response) |
|
|
| return response |
| } catch (error) { |
| |
| clearAuth() |
| throw error |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async function login2FA(tempToken: string, totpCode: string): Promise<User> { |
| try { |
| const response = await authAPI.login2FA({ temp_token: tempToken, totp_code: totpCode }) |
| setAuthFromResponse(response) |
| return user.value! |
| } catch (error) { |
| clearAuth() |
| throw error |
| } |
| } |
|
|
| |
| |
| |
| |
| function setAuthFromResponse(response: AuthResponse): void { |
| |
| token.value = response.access_token |
|
|
| |
| if (response.refresh_token) { |
| refreshTokenValue.value = response.refresh_token |
| localStorage.setItem(REFRESH_TOKEN_KEY, response.refresh_token) |
| } |
|
|
| |
| if (response.user.run_mode) { |
| runMode.value = response.user.run_mode |
| } |
| const { run_mode: _run_mode, ...userData } = response.user |
| user.value = userData |
|
|
| |
| localStorage.setItem(AUTH_TOKEN_KEY, response.access_token) |
| localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userData)) |
|
|
| |
| startAutoRefresh() |
|
|
| |
| |
| if (response.refresh_token && response.expires_in) { |
| scheduleTokenRefresh(response.expires_in) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function register(userData: RegisterRequest): Promise<User> { |
| try { |
| const response = await authAPI.register(userData) |
|
|
| |
| setAuthFromResponse(response) |
|
|
| return user.value! |
| } catch (error) { |
| |
| clearAuth() |
| throw error |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async function setToken(newToken: string): Promise<User> { |
| |
| |
| stopAutoRefresh() |
| stopTokenRefresh() |
| token.value = null |
| user.value = null |
|
|
| token.value = newToken |
| localStorage.setItem(AUTH_TOKEN_KEY, newToken) |
|
|
| |
| 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() |
|
|
| |
| |
| if (savedRefreshToken && tokenExpiresAt.value !== null) { |
| scheduleTokenRefreshAt(tokenExpiresAt.value) |
| } |
|
|
| return userData |
| } catch (error) { |
| clearAuth() |
| throw error |
| } |
| } |
|
|
| |
| |
| |
| |
| async function logout(): Promise<void> { |
| |
| await authAPI.logout() |
|
|
| |
| clearAuth() |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function refreshUser(): Promise<User> { |
| 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 |
|
|
| |
| localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userData)) |
|
|
| return userData |
| } catch (error) { |
| |
| if ((error as { status?: number }).status === 401) { |
| clearAuth() |
| } |
| throw error |
| } |
| } |
|
|
| |
| |
| |
| |
| function clearAuth(): void { |
| |
| stopAutoRefresh() |
| |
| 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 { |
| |
| user, |
| token, |
| runMode: readonly(runMode), |
|
|
| |
| isAuthenticated, |
| isAdmin, |
| isSimpleMode, |
|
|
| |
| login, |
| login2FA, |
| register, |
| setToken, |
| logout, |
| checkAuth, |
| refreshUser |
| } |
| }) |
|
|