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:
parent
b851dc61af
commit
97212b7ffa
18 changed files with 280 additions and 42 deletions
|
|
@ -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))
|
||||
|
|
|
|||
6
backend/internal/db/migrations/002_sessions.sql
Normal file
6
backend/internal/db/migrations/002_sessions.sql
Normal 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;
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue