package auth import ( "context" "net/http" "time" "github.com/leolionad58/ticketbot/internal/db" ) type ctxKey int const ( ctxAdmin ctxKey = iota ctxSession ctxKey = iota ) // WithAdmin injects an admin into the request context. func WithAdmin(r *http.Request, admin *db.PanelAdmin) *http.Request { return r.WithContext(context.WithValue(r.Context(), ctxAdmin, admin)) } // AdminFromContext retrieves the admin from the request context (nil if not set). func AdminFromContext(ctx context.Context) *db.PanelAdmin { v, _ := ctx.Value(ctxAdmin).(*db.PanelAdmin) return v } // SessionFromContext retrieves the session from the request context. func SessionFromContext(ctx context.Context) *db.PanelSession { v, _ := ctx.Value(ctxSession).(*db.PanelSession) return v } // Middleware wires authentication and CSRF checking onto protected routes. type Middleware struct { svc *Service admins *db.PanelAdminRepo } func NewMiddleware(svc *Service, admins *db.PanelAdminRepo) *Middleware { return &Middleware{svc: svc, admins: admins} } // RequireAuth is an HTTP middleware that validates the session cookie and injects // the admin into the request context. Redirects to /login if not authenticated. func (m *Middleware) RequireAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("panel_session") if err != nil { http.Redirect(w, r, "/login", http.StatusSeeOther) return } ctx := r.Context() sess, err := m.svc.ValidateSession(ctx, cookie.Value) if err != nil || sess == nil { clearSessionCookie(w, r) http.Redirect(w, r, "/login", http.StatusSeeOther) return } // CSRF check for state-changing methods if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete { csrfHeader := r.Header.Get("X-CSRF-Token") if csrfHeader == "" { // Also accept form field for non-HTMX posts csrfHeader = r.FormValue("csrf_token") } if csrfHeader != sess.CSRFToken { http.Error(w, "CSRF token invalid", http.StatusForbidden) return } } // Touch session (update last_activity) _ = m.svc.TouchSession(ctx, cookie.Value) // Load admin admin, err := m.admins.GetByID(ctx, sess.AdminID) if err != nil || admin == nil { clearSessionCookie(w, r) http.Redirect(w, r, "/login", http.StatusSeeOther) return } ctx = context.WithValue(ctx, ctxAdmin, admin) ctx = context.WithValue(ctx, ctxSession, sess) next.ServeHTTP(w, r.WithContext(ctx)) }) } // RequireSuperadmin wraps RequireAuth and additionally checks is_superadmin. func (m *Middleware) RequireSuperadmin(next http.Handler) http.Handler { return m.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { admin := AdminFromContext(r.Context()) if admin == nil || !admin.IsSuperadmin { http.Error(w, "Accès refusé", http.StatusForbidden) return } next.ServeHTTP(w, r) })) } // isSecure returns true if the request arrived over HTTPS (direct TLS or behind a proxy). func isSecure(r *http.Request) bool { return r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" } // SetSessionCookie writes the main session cookie. func SetSessionCookie(w http.ResponseWriter, r *http.Request, token string) { http.SetCookie(w, &http.Cookie{ Name: "panel_session", Value: token, Path: "/", HttpOnly: true, Secure: isSecure(r), SameSite: http.SameSiteStrictMode, }) } // SetPendingAuthCookie stores the Discord ID between OAuth callback and password verification. func SetPendingAuthCookie(w http.ResponseWriter, r *http.Request, discordID string) { http.SetCookie(w, &http.Cookie{ Name: "panel_pending_auth", Value: discordID, Path: "/auth", HttpOnly: true, Secure: isSecure(r), SameSite: http.SameSiteLaxMode, // Lax: survives the redirect from /oauth/callback to /auth/* MaxAge: 300, // 5 minutes }) } // PendingAuthDiscordID reads the pending auth cookie. func PendingAuthDiscordID(r *http.Request) string { c, err := r.Cookie("panel_pending_auth") if err != nil { return "" } return c.Value } func clearSessionCookie(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, &http.Cookie{ Name: "panel_session", Value: "", Path: "/", HttpOnly: true, Secure: isSecure(r), SameSite: http.SameSiteStrictMode, MaxAge: -1, Expires: time.Unix(0, 0), }) } func clearPendingAuthCookie(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, &http.Cookie{ Name: "panel_pending_auth", Value: "", Path: "/auth", HttpOnly: true, Secure: isSecure(r), SameSite: http.SameSiteLaxMode, MaxAge: -1, Expires: time.Unix(0, 0), }) } // ClearPendingAuthCookie is exported for use by handlers. func ClearPendingAuthCookie(w http.ResponseWriter, r *http.Request) { clearPendingAuthCookie(w, r) }