From 780e5ec81d2cddcaa0f755b3f3524855d3927eee Mon Sep 17 00:00:00 2001 From: enzo Date: Sat, 21 Mar 2026 22:29:22 +0100 Subject: [PATCH] fix: auth redirect bug + cookie Secure + migration multi-statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetchMe: handle ALL non-ok responses (not just 401) by calling tryRefresh → avoids user=null when backend returns 404/500/any error - DOMContentLoaded guard: check isAuthenticated instead of localStorage token → immediate redirect if fetchMe+tryRefresh both fail, no more flash of dashboard - Cookie Secure flag: check X-Forwarded-Proto header for Traefik/proxy setup → cookie gets Secure=true when behind TLS-terminating reverse proxy - db.go migrate(): split SQL by ; and exec each statement separately → fixes SQLite multi-statement limitation (only first stmt was executed) Co-Authored-By: Claude Sonnet 4.6 --- backend/internal/api/auth.go | 4 +++- backend/internal/db/db.go | 14 +++++++++++--- frontend/js/app.js | 7 ++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/backend/internal/api/auth.go b/backend/internal/api/auth.go index 625e0e6..2508678 100644 --- a/backend/internal/api/auth.go +++ b/backend/internal/api/auth.go @@ -116,12 +116,14 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { h.db.Exec(`UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?`, userID) // Cookie httpOnly pour le refresh token + // Secure=true si TLS direct ou si derrière un proxy (Traefik) qui a terminé TLS + isHTTPS := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" http.SetCookie(w, &http.Cookie{ Name: "pxp_refresh", Value: refreshToken, Path: "/api/auth/refresh", HttpOnly: true, - Secure: r.TLS != nil, + Secure: isHTTPS, SameSite: http.SameSiteStrictMode, Expires: expiry, }) diff --git a/backend/internal/db/db.go b/backend/internal/db/db.go index 76f67ed..d38fbf8 100644 --- a/backend/internal/db/db.go +++ b/backend/internal/db/db.go @@ -126,9 +126,17 @@ func (db *DB) migrate() error { return fmt.Errorf("transaction migration %s : %w", m.name, err) } - if _, err := tx.Exec(string(content)); err != nil { - tx.Rollback() - return fmt.Errorf("exécution migration %s : %w", m.name, err) + // Splitter par ";" pour exécuter chaque statement séparément + // (SQLite / database/sql n'exécute qu'un seul statement par Exec) + for _, stmt := range strings.Split(string(content), ";") { + stmt = strings.TrimSpace(stmt) + if stmt == "" { + continue + } + if _, err := tx.Exec(stmt); err != nil { + tx.Rollback() + return fmt.Errorf("exécution migration %s : %w", m.name, err) + } } // Mettre à jour la version (la migration 001 l'insère elle-même, pas besoin de le refaire) diff --git a/frontend/js/app.js b/frontend/js/app.js index 31ae31d..19ca1d2 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -118,7 +118,8 @@ document.addEventListener('alpine:init', () => { const res = await apiFetch('/api/auth/me') if (res.ok) { this.user = await res.json() - } else if (res.status === 401) { + } else { + // Token expiré, invalide, ou toute autre erreur → tenter un refresh await this.tryRefresh() } }, @@ -918,8 +919,8 @@ document.addEventListener('DOMContentLoaded', async () => { const publicPages = ['login', 'install', 'index', ''] const currentPage = window.location.pathname.replace(/^\/|\.html$/g, '') || 'index' - // Guard rapide (synchrone) : si pas de token du tout, redirect immédiat - if (!publicPages.includes(currentPage) && !localStorage.getItem('pxp_token')) { + // Guard auth : si pas authentifié (token absent ou invalid/expiré), redirect login + if (!publicPages.includes(currentPage) && !Alpine.store('auth').isAuthenticated) { window.location.href = '/login.html' return }