fix: corriger bug multi-sessions (upsertUser wrong ID + schema repair + logs refresh)

- auth.go: upsertUser utilise toujours SELECT explicite au lieu de LastInsertId()
  qui retournait un rowid obsolète pour ON CONFLICT DO UPDATE sur ligne existante
- auth.go: vérifier l'erreur de l'INSERT refresh_tokens (était silencieusement ignorée)
- auth.go: logs détaillés dans Refresh handler pour diagnostiquer les 401
- db.go: repairSchema() ajoute les colonnes manquantes (ip, last_used_at) dans les
  bases où migration 002 était partiellement appliquée (ancien bug multi-statements)
- app.js: tryRefresh et fetchMe affichent le vrai message d'erreur du backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
enzo 2026-03-22 01:32:01 +01:00
parent dc0c67b89c
commit 95757124de
3 changed files with 88 additions and 19 deletions

View file

@ -5,6 +5,7 @@ import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"log"
"net/http"
"time"
@ -107,10 +108,14 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
// Stocker le hash du refresh token en base pour permettre la révocation
tokenHash := hashToken(refreshToken)
expiry := time.Now().Add(auth.RefreshTokenDuration())
h.db.Exec(`
if _, err := h.db.Exec(`
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))
`, userID, tokenHash, expiry, r.UserAgent(), clientIP(r)); err != nil {
log.Printf("[auth/login] ERREUR stockage refresh token — user=%s userID=%d err=%v", body.Username, userID, err)
JSONError(w, "Erreur création session", http.StatusInternalServerError)
return
}
// Mettre à jour la date de dernier login
h.db.Exec(`UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?`, userID)
@ -177,14 +182,18 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
// Refresh renouvelle l'access token via le refresh token (cookie httpOnly).
// POST /api/auth/refresh
func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
ip := clientIP(r)
cookie, err := r.Cookie("pxp_refresh")
if err != nil {
log.Printf("[auth/refresh] cookie absent — ip=%s err=%v", ip, err)
JSONError(w, "Refresh token manquant", http.StatusUnauthorized)
return
}
userID, err := h.jwtManager.ValidateRefreshToken(cookie.Value)
if err != nil {
log.Printf("[auth/refresh] JWT invalide — ip=%s err=%v", ip, err)
JSONError(w, "Refresh token invalide ou expiré", http.StatusUnauthorized)
return
}
@ -194,10 +203,17 @@ func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
var count int
h.db.QueryRow(`SELECT COUNT(*) FROM refresh_tokens WHERE user_id = ? AND token_hash = ? AND expires_at > CURRENT_TIMESTAMP`, userID, tokenHash).Scan(&count)
if count == 0 {
// Diagnostic : vérifier si le token existe mais avec un mauvais user_id
var anyCount int
h.db.QueryRow(`SELECT COUNT(*) FROM refresh_tokens WHERE token_hash = ?`, tokenHash).Scan(&anyCount)
log.Printf("[auth/refresh] token non trouvé en base — userID=%d tokenHash=%s anyMatch=%d ip=%s",
userID, tokenHash[:8], anyCount, ip)
JSONError(w, "Session expirée ou révoquée", http.StatusUnauthorized)
return
}
log.Printf("[auth/refresh] token valide — userID=%d ip=%s", userID, ip)
// Mettre à jour la date de dernière utilisation
h.db.Exec(`UPDATE refresh_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE token_hash = ?`, tokenHash)
@ -206,6 +222,7 @@ func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
var isAdmin int
err = h.db.QueryRow(`SELECT username, is_admin FROM users WHERE id = ?`, userID).Scan(&username, &isAdmin)
if err != nil {
log.Printf("[auth/refresh] utilisateur introuvable — userID=%d ip=%s err=%v", userID, ip, err)
JSONError(w, "Utilisateur introuvable", http.StatusUnauthorized)
return
}
@ -304,21 +321,20 @@ func (h *AuthHandler) upsertUser(info *auth.UserInfo) (int64, error) {
}
// Mise à jour du statut admin à chaque connexion (peut changer côté Linux)
result, err := h.db.Exec(`
if _, err := h.db.Exec(`
INSERT INTO users (username, is_admin) VALUES (?, ?)
ON CONFLICT(username) DO UPDATE SET is_admin = excluded.is_admin
`, info.Username, isAdmin)
if err != nil {
`, info.Username, isAdmin); err != nil {
return 0, err
}
// Tenter de récupérer l'ID (insertions ou update)
id, err := result.LastInsertId()
if err != nil || id == 0 {
// En cas de ON CONFLICT DO UPDATE, LastInsertId peut retourner 0
err = h.db.QueryRow(`SELECT id FROM users WHERE username = ?`, info.Username).Scan(&id)
// Toujours faire un SELECT explicite : avec ON CONFLICT DO UPDATE sur une ligne
// existante, LastInsertId() peut retourner un rowid obsolète (comportement SQLite).
var id int64
if err := h.db.QueryRow(`SELECT id FROM users WHERE username = ?`, info.Username).Scan(&id); err != nil {
return 0, fmt.Errorf("utilisateur introuvable après upsert: %w", err)
}
return id, err
return id, nil
}
// GetSessions retourne les sessions actives de l'utilisateur connecté.