diff --git a/backend/internal/api/auth.go b/backend/internal/api/auth.go index f6ad350..7fdfa46 100644 --- a/backend/internal/api/auth.go +++ b/backend/internal/api/auth.go @@ -347,7 +347,7 @@ func (h *AuthHandler) GetSessions(w http.ResponseWriter, r *http.Request) { } rows, err := h.db.Query(` - SELECT id, user_agent, ip, created_at, last_used_at, expires_at + SELECT id, user_agent, ip, created_at, last_used_at, expires_at, token_hash FROM refresh_tokens WHERE user_id = ? AND expires_at > CURRENT_TIMESTAMP ORDER BY COALESCE(last_used_at, created_at) DESC @@ -358,21 +358,29 @@ func (h *AuthHandler) GetSessions(w http.ResponseWriter, r *http.Request) { } defer rows.Close() + // Hash du cookie courant pour marquer "session actuelle" + currentHash := "" + if cookie, err := r.Cookie("pxp_refresh"); err == nil { + currentHash = hashToken(cookie.Value) + } + type Session struct { - ID int64 `json:"id"` - UserAgent string `json:"user_agent"` - IP string `json:"ip"` - CreatedAt string `json:"created_at"` + ID int64 `json:"id"` + UserAgent string `json:"user_agent"` + IP string `json:"ip"` + CreatedAt string `json:"created_at"` LastUsedAt *string `json:"last_used_at"` - ExpiresAt string `json:"expires_at"` + ExpiresAt string `json:"expires_at"` + IsCurrent bool `json:"is_current"` } sessions := []Session{} for rows.Next() { var s Session + var tokenHash string var createdAt, expiresAt sql.NullString var lastUsedAt sql.NullString - if err := rows.Scan(&s.ID, &s.UserAgent, &s.IP, &createdAt, &lastUsedAt, &expiresAt); err != nil { + if err := rows.Scan(&s.ID, &s.UserAgent, &s.IP, &createdAt, &lastUsedAt, &expiresAt, &tokenHash); err != nil { log.Printf("[GetSessions] scan error userID=%d: %v", claims.UserID, err) continue } @@ -381,6 +389,7 @@ func (h *AuthHandler) GetSessions(w http.ResponseWriter, r *http.Request) { if lastUsedAt.Valid && lastUsedAt.String != "" { s.LastUsedAt = &lastUsedAt.String } + s.IsCurrent = currentHash != "" && tokenHash == currentHash sessions = append(sessions, s) } diff --git a/frontend/css/pages.css b/frontend/css/pages.css index 80ec9fa..f1aae25 100644 --- a/frontend/css/pages.css +++ b/frontend/css/pages.css @@ -725,6 +725,18 @@ .session-sep { opacity: .4; } .session-id { font-family: monospace; opacity: .6; } +.session-current { background: rgba(var(--neu-primary-rgb, 108,142,244), .05); border-radius: var(--neu-radius); padding-left: .5rem; padding-right: .5rem; } + +.badge-current { + font-size: .65rem; + padding: .15rem .45rem; + border-radius: 99px; + background: rgba(34,197,94,.15); + color: var(--neu-success); + font-weight: 600; + letter-spacing: .02em; +} + /* ── Logo auth LineIcons ─────────────────────────────────────────────────────── */ .logo-icon { font-size: 2.5rem; diff --git a/frontend/js/app.js b/frontend/js/app.js index dd903e7..86e7df1 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -670,7 +670,7 @@ document.addEventListener('alpine:init', () => { }, async revokeSession(id) { - this.revoking = { ...this.revoking, [id]: true } + this.revoking[id] = true try { const res = await apiFetch(`/api/auth/sessions/${id}`, { method: 'DELETE' }) if (res.ok) { @@ -682,10 +682,13 @@ document.addEventListener('alpine:init', () => { } } catch (e) { Alpine.store('toasts').error(`Erreur réseau — ${e.message}`) + } finally { + this.revoking[id] = false } - this.revoking = { ...this.revoking, [id]: false } }, + isRevoking(id) { return this.revoking[id] === true }, + formatDate(raw) { if (!raw) return '—' // SQLite retourne "YYYY-MM-DD HH:MM:SS" ou RFC3339 selon le driver diff --git a/frontend/profile.html b/frontend/profile.html index 8483b7b..cb98fa0 100644 --- a/frontend/profile.html +++ b/frontend/profile.html @@ -155,14 +155,15 @@
Aucune session active
-