Session F5 : - auth.store: restoreSession() essaie fetchMe() avec le token existant (< 15 min → fonctionne sans cookie), puis tryRefresh() en fallback - router: appelle restoreSession() au premier chargement au lieu de tryRefresh() Paramètres infrastructure : - Champs ssh_password et proxmox_token en write-only (vide = pas de changement) - SettingsHandler: accepte les clés chiffrées, chiffre avant stockage - Permet de corriger le token Proxmox invalide sans réinstallation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
218 lines
6.1 KiB
TypeScript
218 lines
6.1 KiB
TypeScript
// 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<User | null>(null)
|
|
const accessToken = ref<string | null>(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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<Pick<User, 'lang' | 'theme' | 'sidebar_position'>>): Promise<void> {
|
|
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<typeof setTimeout> | 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,
|
|
}
|
|
})
|