fix: multi-sessions + système de toasts pour erreurs visibles
Sessions:
- Cookie pxp_refresh path: /api/auth/refresh → /api/auth/ pour être envoyé au logout
- Logout supprime uniquement le token de la session courante (via cookie hash)
si pas de cookie = fallback suppression globale (rétro-compat)
Toasts:
- Store Alpine.store('toasts') avec error/success/warn/info + auto-dismiss
- Conteneur #pxp-toasts injecté dans <body>, persiste à travers les navigations Swup
- fetchMe(): HTTP non-401 → toast explicite (ex: HTTP 404 backend down)
- tryRefresh(): session expirée → sessionStorage → toast orange sur la page login
- logout(): message "Déconnexion réussie" sur la page login
- proxmoxPage.action(): toast succès/erreur pour start/stop LXC
- profilePage.revokeSession(): toast confirmation révocation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
21e1e0ed1e
commit
dc0c67b89c
3 changed files with 151 additions and 11 deletions
|
|
@ -97,6 +97,47 @@ function apiFetch(path, opts = {}) {
|
|||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
|
||||
// ── Store toasts ────────────────────────────────────────────────────────
|
||||
Alpine.store('toasts', {
|
||||
items: [],
|
||||
_id: 0,
|
||||
add(message, type = 'info', duration = 5000) {
|
||||
const id = ++this._id
|
||||
this.items.push({ id, message, type })
|
||||
if (duration > 0) setTimeout(() => this.remove(id), duration)
|
||||
return id
|
||||
},
|
||||
remove(id) { this.items = this.items.filter(t => t.id !== id) },
|
||||
error(msg) { return this.add(msg, 'error', 8000) },
|
||||
success(msg) { return this.add(msg, 'success', 4000) },
|
||||
warn(msg) { return this.add(msg, 'warn', 6000) },
|
||||
info(msg) { return this.add(msg, 'info', 4000) },
|
||||
})
|
||||
|
||||
// Injecter le conteneur de toasts dans le body (persiste à travers Swup)
|
||||
;(function() {
|
||||
const tc = document.createElement('div')
|
||||
tc.id = 'pxp-toasts'
|
||||
tc.setAttribute('x-data', '')
|
||||
tc.innerHTML = `
|
||||
<template x-for="t in $store.toasts.items" :key="t.id">
|
||||
<div class="toast" :class="'toast--' + t.type" x-transition.opacity>
|
||||
<i :class="{
|
||||
'lnid-cross-circle': t.type === 'error',
|
||||
'lnid-check-circle-1': t.type === 'success',
|
||||
'lnid-warning-triangle-1':t.type === 'warn',
|
||||
'lnid-information-circle':t.type === 'info'
|
||||
}"></i>
|
||||
<span class="toast-msg" x-text="t.message"></span>
|
||||
<button class="toast-close" @click="$store.toasts.remove(t.id)">
|
||||
<i class="lnid-cross"></i>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
`
|
||||
document.body.appendChild(tc)
|
||||
})()
|
||||
|
||||
// ── Store auth ─────────────────────────────────────────────────────────
|
||||
Alpine.store('auth', {
|
||||
token: null,
|
||||
|
|
@ -131,8 +172,15 @@ document.addEventListener('alpine:init', () => {
|
|||
if (u.lang && u.lang !== Alpine.store('i18n').lang) {
|
||||
Alpine.store('i18n').setLang(u.lang)
|
||||
}
|
||||
} else if (res.status === 401) {
|
||||
// Token expiré → tenter un refresh silencieusement
|
||||
await this.tryRefresh()
|
||||
} else {
|
||||
// Token expiré, invalide, ou toute autre erreur → tenter un refresh
|
||||
// Erreur inattendue (404, 500…) — signaler + tenter quand même
|
||||
console.error(`[auth/me] HTTP ${res.status}`)
|
||||
Alpine.store('toasts').error(
|
||||
`Erreur serveur (${res.status}) sur /api/auth/me — le backend est-il démarré ?`
|
||||
)
|
||||
await this.tryRefresh()
|
||||
}
|
||||
},
|
||||
|
|
@ -141,11 +189,19 @@ document.addEventListener('alpine:init', () => {
|
|||
const res = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' })
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
// Le backend retourne "access_token" (pas "token")
|
||||
this.token = data.access_token
|
||||
localStorage.setItem('pxp_token', data.access_token)
|
||||
await this.fetchMe()
|
||||
} else {
|
||||
// Session expirée ou révoquée → notifier via sessionStorage (visible sur la page login)
|
||||
const page = window.location.pathname.replace(/^\/|\.html$/g, '')
|
||||
if (page !== 'login' && page !== 'install' && page !== 'index' && page !== '') {
|
||||
sessionStorage.setItem('pxp_auth_notice',
|
||||
res.status === 401
|
||||
? 'Session expirée ou révoquée — veuillez vous reconnecter'
|
||||
: `Erreur ${res.status} lors du renouvellement de session`
|
||||
)
|
||||
}
|
||||
this.clear()
|
||||
}
|
||||
},
|
||||
|
|
@ -171,6 +227,7 @@ document.addEventListener('alpine:init', () => {
|
|||
async logout() {
|
||||
await apiFetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {})
|
||||
this.clear()
|
||||
sessionStorage.setItem('pxp_auth_notice', 'Déconnexion réussie')
|
||||
window.location.href = '/login.html'
|
||||
},
|
||||
|
||||
|
|
@ -315,6 +372,15 @@ document.addEventListener('alpine:init', () => {
|
|||
error: '',
|
||||
loading: false,
|
||||
|
||||
init() {
|
||||
// Afficher le message si la session a expiré avant la redirection
|
||||
const msg = sessionStorage.getItem('pxp_auth_notice')
|
||||
if (msg) {
|
||||
sessionStorage.removeItem('pxp_auth_notice')
|
||||
setTimeout(() => Alpine.store('toasts').warn(msg), 200)
|
||||
}
|
||||
},
|
||||
|
||||
async submit() {
|
||||
this.error = ''
|
||||
this.loading = true
|
||||
|
|
@ -607,8 +673,16 @@ document.addEventListener('alpine:init', () => {
|
|||
this.revoking = { ...this.revoking, [id]: true }
|
||||
try {
|
||||
const res = await apiFetch(`/api/auth/sessions/${id}`, { method: 'DELETE' })
|
||||
if (res.ok) this.sessions = this.sessions.filter(s => s.id !== id)
|
||||
} catch (e) { /* ignore */ }
|
||||
if (res.ok) {
|
||||
this.sessions = this.sessions.filter(s => s.id !== id)
|
||||
Alpine.store('toasts').success('Session révoquée')
|
||||
} else {
|
||||
const d = await res.json().catch(() => ({}))
|
||||
Alpine.store('toasts').error(d.error || 'Erreur révocation session')
|
||||
}
|
||||
} catch (e) {
|
||||
Alpine.store('toasts').error(`Erreur réseau — ${e.message}`)
|
||||
}
|
||||
this.revoking = { ...this.revoking, [id]: false }
|
||||
},
|
||||
|
||||
|
|
@ -673,9 +747,15 @@ document.addEventListener('alpine:init', () => {
|
|||
const key = `${vmid}-${action}`
|
||||
this.actionLoading[key] = true
|
||||
try {
|
||||
await apiFetch(`/api/proxmox/${type}/${vmid}/${action}`, { method: 'POST' })
|
||||
const res = await apiFetch(`/api/proxmox/${type}/${vmid}/${action}`, { method: 'POST' })
|
||||
if (res.ok) {
|
||||
Alpine.store('toasts').success(`Action « ${action} » envoyée sur ${type} ${vmid}`)
|
||||
} else {
|
||||
const d = await res.json().catch(() => ({}))
|
||||
Alpine.store('toasts').error(d.error || `Erreur action ${action} (HTTP ${res.status})`)
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
Alpine.store('toasts').error(`Erreur réseau — ${e.message}`)
|
||||
} finally {
|
||||
this.actionLoading[key] = false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue