import { defineStore } from 'pinia' import { ref, computed, onMounted, watch } from 'vue' interface User { id: number username: string email?: string } export const useAuthStore = defineStore('auth', () => { const user = ref(null) const loading = ref(false) const error = ref(null) let refreshInterval: ReturnType | null = null /** * Fetch the current logged-in user from the server. * Tries refresh if access token expired. */ async function fetchUser() { try { loading.value = true const res = await fetch('/api/v1/user/info', { method: 'GET', credentials: 'include', }) if (res.ok) { const data = await res.json() user.value = data.user error.value = null startAutoRefresh() return true } if (res.status === 401) { const refreshed = await refreshToken() if (refreshed) return await fetchUser() user.value = null } else { throw new Error('Failed to fetch user') } } catch (err: any) { console.warn('fetchUser failed:', err) error.value = err.message user.value = null } finally { loading.value = false } } /** * Perform login and set cookies. */ async function login(username: string, password: string, remember: boolean) { try { loading.value = true error.value = null const res = await fetch('/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ username, password, remember }), }) if (!res.ok) throw new Error('Invalid credentials') const data = await res.json() if (data.ok && data.user) { user.value = data.user startAutoRefresh() return true } throw new Error('Login failed') } catch (err: any) { error.value = err.message return false } finally { loading.value = false } } /** * Refresh the access token using refresh cookie. */ async function refreshToken() { if (!document.cookie.includes('refresh_token')) return false try { const res = await fetch('/api/v1/auth/refresh', { method: 'POST', credentials: 'include', }) if (res.ok) { console.info('[Auth] Token refreshed') return true } console.warn('[Auth] Token refresh failed with status', res.status) return false } catch (err) { console.error('[Auth] Refresh error', err) return false } } /** * Automatically refresh tokens before expiry. */ function startAutoRefresh() { if (refreshInterval) clearInterval(refreshInterval) refreshInterval = setInterval(async () => { if (!user.value) return const success = await refreshToken() if (!success) { console.warn('[Auth] Auto-refresh failed, trying fetchUser') const ok = await fetchUser() if (!ok) { console.warn('[Auth] Session expired, logging out') await logout() } } }, 7.5 * 60 * 1000) document.addEventListener('visibilitychange', async () => { if (document.visibilityState === 'visible' && user.value) { const success = await refreshToken() if (!success) await logout() } }) } /** * Stop the refresh timer and logout from backend. */ async function logout() { try { await fetch('/api/v1/auth/logout', { method: 'POST', credentials: 'include', }) } catch (err) { console.warn('Logout error:', err) } finally { user.value = null if (refreshInterval) clearInterval(refreshInterval) refreshInterval = null } } const isAuthenticated = computed(() => !!user.value) return { user, loading, error, isAuthenticated, login, logout, fetchUser, refreshToken, } })