// Store d'authentification — gère la session JWT, le profil utilisateur et l'état d'installation. import { defineStore } from 'pinia' import { ref, computed } from 'vue' export interface User { id: number username: string is_admin: boolean lang: string theme: string sidebar_position: string } export const useAuthStore = defineStore('auth', () => { // État const user = ref(null) const accessToken = ref(localStorage.getItem('pxp_token')) const isInstalled = ref(false) const installChecked = ref(false) // Computed const isAuthenticated = computed(() => !!accessToken.value && !!user.value) // ── Actions ────────────────────────────────────────────────────────────── /** * Vérifie si l'application est installée via l'API. * Appelé une seule fois au démarrage par le router guard. */ async function checkInstallation(): Promise { try { const res = await fetch('/api/install/check') if (res.ok) { const data = await res.json() isInstalled.value = data.installed } } catch { // En cas d'erreur réseau, on suppose installé pour éviter une boucle isInstalled.value = true } finally { installChecked.value = true } } /** * Authentifie l'utilisateur avec ses credentials Linux. */ 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 contentType = res.headers.get('content-type') || '' if (contentType.includes('application/json')) { const err = await res.json() throw new Error(err.error || 'Erreur d\'authentification') } throw new Error(`Erreur ${res.status} — réponse inattendue du serveur`) } const data = await res.json() accessToken.value = data.access_token localStorage.setItem('pxp_token', data.access_token) user.value = data.user // Planifier le renouvellement automatique avant expiration (14 min) scheduleRefresh(14 * 60 * 1000) } /** * Restaure la session au démarrage de l'application (après F5). * 1. Essaie fetchMe() avec le token existant (marche si < 15 min) * 2. Si le token est expiré, tente le refresh via le cookie httpOnly */ async function restoreSession(): Promise { if (!accessToken.value) return // Le token est peut-être encore valide : évite d'avoir besoin du cookie await fetchMe() if (user.value) { scheduleRefresh(14 * 60 * 1000) return } // Token expiré — tenter le refresh via le cookie httpOnly try { const res = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include', }) if (res.ok) { const data = await res.json() accessToken.value = data.access_token localStorage.setItem('pxp_token', data.access_token) await fetchMe() if (user.value) scheduleRefresh(14 * 60 * 1000) } else { // Le refresh a explicitement échoué (cookie absent ou expiré) clearSession() } } catch { // Erreur réseau transitoire — ne pas effacer le token, laisser le guard rediriger } } /** * Tente de renouveler le token via le cookie httpOnly (pxp_refresh). * Utilisé par le timer automatique (14 min après login). */ async function tryRefresh(): Promise { const token = localStorage.getItem('pxp_token') if (!token) return try { const res = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include', }) if (res.ok) { const data = await res.json() accessToken.value = data.access_token localStorage.setItem('pxp_token', data.access_token) await fetchMe() scheduleRefresh(14 * 60 * 1000) } else { clearSession() } } catch { clearSession() } } /** * Charge le profil de l'utilisateur connecté. */ async function fetchMe(): Promise { if (!accessToken.value) return const res = await fetch('/api/auth/me', { headers: { Authorization: `Bearer ${accessToken.value}` }, }) if (res.ok) { user.value = await res.json() } } /** * Déconnecte l'utilisateur. */ async function logout(): Promise { try { await fetch('/api/auth/logout', { method: 'POST', headers: { Authorization: `Bearer ${accessToken.value}` }, credentials: 'include', }) } finally { clearSession() } } /** * Met à jour les préférences de l'utilisateur (thème, langue, sidebar). */ async function updatePreferences(prefs: Partial>): Promise { if (!accessToken.value) return await fetch('/api/auth/preferences', { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken.value}`, }, body: JSON.stringify(prefs), }) // Mettre à jour localement if (user.value) { Object.assign(user.value, prefs) } } // ── Helpers privés ──────────────────────────────────────────────────────── let refreshTimer: ReturnType | null = null function scheduleRefresh(delayMs: number): void { if (refreshTimer) clearTimeout(refreshTimer) refreshTimer = setTimeout(() => tryRefresh(), delayMs) } function clearSession(): void { user.value = null accessToken.value = null localStorage.removeItem('pxp_token') if (refreshTimer) clearTimeout(refreshTimer) } return { user, accessToken, isInstalled, installChecked, isAuthenticated, checkInstallation, login, logout, restoreSession, tryRefresh, fetchMe, updatePreferences, } })