package handlers import ( "log/slog" "net/http" "time" "golang.org/x/oauth2" "github.com/leolionad58/ticketbot/internal/db" panelauth "github.com/leolionad58/ticketbot/internal/panel/auth" ) // LoginHandler handles the Discord OAuth2 login flow. type LoginHandler struct { Admins *db.PanelAdminRepo Auth *panelauth.Service OAuthCfg *oauth2.Config Renderer *Renderer } // HandleLogin serves GET /login. func (h *LoginHandler) HandleLogin(w http.ResponseWriter, r *http.Request) { state, err := panelauth.GenerateState() if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } secure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" http.SetCookie(w, &http.Cookie{ Name: "oauth_state", Value: state, Path: "/oauth", HttpOnly: true, Secure: secure, SameSite: http.SameSiteLaxMode, // Lax to survive the Discord redirect MaxAge: 60, }) type loginData struct { OAuthURL string Error string } h.Renderer.Auth(w, "login", loginData{ OAuthURL: panelauth.AuthURL(h.OAuthCfg, state), }) } // HandleCallback serves GET /oauth/callback. func (h *LoginHandler) HandleCallback(w http.ResponseWriter, r *http.Request) { // Validate state stateCookie, err := r.Cookie("oauth_state") if err != nil || stateCookie.Value != r.URL.Query().Get("state") { type loginData struct{ OAuthURL, Error string } h.Renderer.Auth(w, "login", loginData{Error: "Session expirée, réessaie."}) return } // Clear state cookie http.SetCookie(w, &http.Cookie{Name: "oauth_state", Path: "/oauth", MaxAge: -1, Expires: time.Unix(0, 0)}) code := r.URL.Query().Get("code") if code == "" { http.Redirect(w, r, "/login", http.StatusSeeOther) return } ctx := r.Context() discordUser, err := panelauth.FetchDiscordUser(ctx, h.OAuthCfg, code) if err != nil { slog.Warn("oauth callback: fetch discord user", "err", err) type loginData struct{ OAuthURL, Error string } h.Renderer.Auth(w, "login", loginData{ OAuthURL: panelauth.AuthURL(h.OAuthCfg, ""), Error: "Erreur lors de la connexion Discord. Réessaie.", }) return } // Check admin whitelist admin, err := h.Admins.GetByDiscordID(ctx, discordUser.ID) if err != nil { slog.Error("oauth callback: get admin", "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return } if admin == nil { // Not in whitelist — show denied page h.Renderer.Auth(w, "denied", nil) return } // Update Discord profile info _ = h.Admins.UpdateDiscordProfile(ctx, admin.ID, discordUser.Username, discordUser.Avatar) admin.DiscordUsername = discordUser.Username admin.DiscordAvatar = discordUser.Avatar // Store pending auth cookie panelauth.SetPendingAuthCookie(w, r, admin.DiscordID) if admin.PasswordHash == "" { http.Redirect(w, r, "/auth/password-setup", http.StatusSeeOther) return } if !admin.TOTPEnabled { http.Redirect(w, r, "/auth/totp-setup", http.StatusSeeOther) return } http.Redirect(w, r, "/auth/verify", http.StatusSeeOther) }