feat: sessions management, web manifest, square icon-only buttons, remove lang select

- Backend: migration 002 adds user_agent/ip/last_used_at to refresh_tokens
- Backend: GET /api/auth/sessions + DELETE /api/auth/sessions/{id} endpoints
- Frontend: profile page — sessions section (browser, IP, datetime, revoke)
- Frontend: web manifest + SVG icon for PWA support
- Frontend: remove language selector from all navbars (moved to profile page)
- Frontend: neu-btn--icon-sm class for square icon-only buttons (theme/logout/edit)
- Frontend: manifest link added to all 9 HTML pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
enzo 2026-03-21 20:14:11 +01:00
parent b851dc61af
commit 97212b7ffa
18 changed files with 280 additions and 42 deletions

View file

@ -12,6 +12,7 @@ import (
"git.geronzi.fr/proxmoxPanel/core/backend/internal/audit"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/auth"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/db"
"github.com/go-chi/chi/v5"
)
// AuthHandler contient les handlers d'authentification.
@ -107,8 +108,9 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
tokenHash := hashToken(refreshToken)
expiry := time.Now().Add(auth.RefreshTokenDuration())
h.db.Exec(`
INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?)
`, userID, tokenHash, expiry)
INSERT INTO refresh_tokens (user_id, token_hash, expires_at, user_agent, ip, last_used_at)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`, userID, tokenHash, expiry, r.UserAgent(), clientIP(r))
// Mettre à jour la date de dernier login
h.db.Exec(`UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?`, userID)
@ -185,6 +187,9 @@ func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
return
}
// Mettre à jour la date de dernière utilisation
h.db.Exec(`UPDATE refresh_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE token_hash = ?`, tokenHash)
// Récupérer les infos utilisateur
var username string
var isAdmin int
@ -305,6 +310,77 @@ func (h *AuthHandler) upsertUser(info *auth.UserInfo) (int64, error) {
return id, err
}
// GetSessions retourne les sessions actives de l'utilisateur connecté.
// GET /api/auth/sessions
func (h *AuthHandler) GetSessions(w http.ResponseWriter, r *http.Request) {
claims := GetClaims(r)
if claims == nil {
JSONError(w, "Non authentifié", http.StatusUnauthorized)
return
}
rows, err := h.db.Query(`
SELECT id, user_agent, ip, created_at, last_used_at, expires_at
FROM refresh_tokens
WHERE user_id = ? AND expires_at > CURRENT_TIMESTAMP
ORDER BY COALESCE(last_used_at, created_at) DESC
`, claims.UserID)
if err != nil {
JSONError(w, "Erreur récupération sessions", http.StatusInternalServerError)
return
}
defer rows.Close()
type Session struct {
ID int64 `json:"id"`
UserAgent string `json:"user_agent"`
IP string `json:"ip"`
CreatedAt time.Time `json:"created_at"`
LastUsedAt *time.Time `json:"last_used_at"`
ExpiresAt time.Time `json:"expires_at"`
}
sessions := []Session{}
for rows.Next() {
var s Session
var lastUsed sql.NullTime
if err := rows.Scan(&s.ID, &s.UserAgent, &s.IP, &s.CreatedAt, &lastUsed, &s.ExpiresAt); err != nil {
continue
}
if lastUsed.Valid {
s.LastUsedAt = &lastUsed.Time
}
sessions = append(sessions, s)
}
JSONResponse(w, http.StatusOK, sessions)
}
// RevokeSession révoque une session (refresh token) de l'utilisateur connecté.
// DELETE /api/auth/sessions/{id}
func (h *AuthHandler) RevokeSession(w http.ResponseWriter, r *http.Request) {
claims := GetClaims(r)
if claims == nil {
JSONError(w, "Non authentifié", http.StatusUnauthorized)
return
}
sessionID := chi.URLParam(r, "id")
res, err := h.db.Exec(`DELETE FROM refresh_tokens WHERE id = ? AND user_id = ?`, sessionID, claims.UserID)
if err != nil {
JSONError(w, "Erreur révocation session", http.StatusInternalServerError)
return
}
n, _ := res.RowsAffected()
if n == 0 {
JSONError(w, "Session introuvable", http.StatusNotFound)
return
}
h.auditLogger.Log(&claims.UserID, claims.Username, "session_revoked", sessionID, nil, clientIP(r))
JSONResponse(w, http.StatusOK, map[string]string{"message": "Session révoquée"})
}
// hashToken crée un hash SHA-256 d'un token pour le stockage en base.
func hashToken(token string) string {
h := sha256.Sum256([]byte(token))

View file

@ -0,0 +1,6 @@
-- Migration 002 : Infos de session dans refresh_tokens
-- Ajout user_agent, ip, last_used_at pour la gestion des sessions
ALTER TABLE refresh_tokens ADD COLUMN user_agent TEXT NOT NULL DEFAULT '';
ALTER TABLE refresh_tokens ADD COLUMN ip TEXT NOT NULL DEFAULT '';
ALTER TABLE refresh_tokens ADD COLUMN last_used_at DATETIME;

View file

@ -130,6 +130,8 @@ func main() {
r.Post("/api/auth/logout", authHandler.Logout)
r.Get("/api/auth/me", authHandler.Me)
r.Patch("/api/auth/preferences", authHandler.UpdatePreferences)
r.Get("/api/auth/sessions", authHandler.GetSessions)
r.Delete("/api/auth/sessions/{id}", authHandler.RevokeSession)
// Proxmox
r.Get("/api/proxmox/resources", proxmoxHandler.GetResources)