// SPDX-License-Identifier: AGPL-3.0-or-later package handler import ( "errors" "log/slog" "net/http" "regexp" "strings" "kode.naiv.no/olemd/favoritter/internal/middleware" "kode.naiv.no/olemd/favoritter/internal/render" "kode.naiv.no/olemd/favoritter/internal/store" ) var usernameRegexp = regexp.MustCompile(`^[a-zA-Z0-9_-]{2,30}$`) func (h *Handler) handleLoginGet(w http.ResponseWriter, r *http.Request) { // If already logged in, redirect to home. if middleware.UserFromContext(r.Context()) != nil { http.Redirect(w, r, h.deps.Config.BasePath+"/", http.StatusSeeOther) return } h.deps.Renderer.Page(w, r, "login", render.PageData{ Title: "Logg inn", }) } func (h *Handler) handleLoginPost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { h.flash(w, r, "login", "Ugyldig forespørsel.", "error", nil) return } username := strings.TrimSpace(r.FormValue("username")) password := r.FormValue("password") user, err := h.deps.Users.Authenticate(username, password) if err != nil { if errors.Is(err, store.ErrInvalidCredentials) || errors.Is(err, store.ErrUserDisabled) { h.flash(w, r, "login", "Feil brukernavn eller passord.", "error", nil) return } slog.Error("login authentication error", "error", err) h.flash(w, r, "login", "Noe gikk galt. Prøv igjen.", "error", nil) return } token, err := h.deps.Sessions.Create(user.ID) if err != nil { slog.Error("session create error", "error", err) h.flash(w, r, "login", "Noe gikk galt. Prøv igjen.", "error", nil) return } h.setSessionCookie(w, r, token) // If user must reset password, redirect there. if user.MustResetPassword { http.Redirect(w, r, h.deps.Config.BasePath+"/reset-password", http.StatusSeeOther) return } http.Redirect(w, r, h.deps.Config.BasePath+"/", http.StatusSeeOther) } func (h *Handler) handleSignupGet(w http.ResponseWriter, r *http.Request) { if middleware.UserFromContext(r.Context()) != nil { http.Redirect(w, r, h.deps.Config.BasePath+"/", http.StatusSeeOther) return } settings, err := h.deps.Settings.Get() if err != nil { slog.Error("get settings error", "error", err) http.Error(w, "Internal error", http.StatusInternalServerError) return } h.deps.Renderer.Page(w, r, "signup", render.PageData{ Title: "Registrer", Data: map[string]string{"Mode": settings.SignupMode}, }) } func (h *Handler) handleSignupPost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { h.flash(w, r, "signup", "Ugyldig forespørsel.", "error", signupData("open")) return } settings, err := h.deps.Settings.Get() if err != nil { slog.Error("get settings error", "error", err) http.Error(w, "Internal error", http.StatusInternalServerError) return } if settings.SignupClosed() { h.flash(w, r, "signup", "Registrering er stengt.", "error", signupData("closed")) return } username := strings.TrimSpace(r.FormValue("username")) password := r.FormValue("password") passwordConfirm := r.FormValue("password_confirm") // Validate input. if !usernameRegexp.MatchString(username) { h.flash(w, r, "signup", "Ugyldig brukernavn. Bruk 2-30 tegn: bokstaver, tall, bindestrek, understrek.", "error", signupData(settings.SignupMode)) return } if len(password) < 8 { h.flash(w, r, "signup", "Passordet må være minst 8 tegn.", "error", signupData(settings.SignupMode)) return } if password != passwordConfirm { h.flash(w, r, "signup", "Passordene er ikke like.", "error", signupData(settings.SignupMode)) return } if settings.SignupRequests() { // Create a signup request for admin approval. err := h.createSignupRequest(username, password) if err != nil { if errors.Is(err, store.ErrUserExists) { h.flash(w, r, "signup", "Brukernavnet er allerede i bruk.", "error", signupData(settings.SignupMode)) return } slog.Error("signup request error", "error", err) h.flash(w, r, "signup", "Noe gikk galt. Prøv igjen.", "error", signupData(settings.SignupMode)) return } h.flash(w, r, "login", "Forespørselen din er sendt. En administrator vil gjennomgå den.", "info", nil) return } // Open signup — create user directly. user, err := h.deps.Users.Create(username, password, "user") if err != nil { if errors.Is(err, store.ErrUserExists) { h.flash(w, r, "signup", "Brukernavnet er allerede i bruk.", "error", signupData(settings.SignupMode)) return } slog.Error("user create error", "error", err) h.flash(w, r, "signup", "Noe gikk galt. Prøv igjen.", "error", signupData(settings.SignupMode)) return } // Auto-login after signup. token, err := h.deps.Sessions.Create(user.ID) if err != nil { slog.Error("session create error", "error", err) http.Redirect(w, r, h.deps.Config.BasePath+"/login", http.StatusSeeOther) return } h.setSessionCookie(w, r, token) http.Redirect(w, r, h.deps.Config.BasePath+"/", http.StatusSeeOther) } func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(middleware.SessionCookieName) if err == nil { if delErr := h.deps.Sessions.Delete(cookie.Value); delErr != nil { slog.Error("session delete error", "error", delErr) } } middleware.ClearSessionCookie(w) http.Redirect(w, r, h.deps.Config.BasePath+"/login", http.StatusSeeOther) } func (h *Handler) handleResetPasswordGet(w http.ResponseWriter, r *http.Request) { user := middleware.UserFromContext(r.Context()) if user == nil { http.Redirect(w, r, h.deps.Config.BasePath+"/login", http.StatusSeeOther) return } if !user.MustResetPassword { http.Redirect(w, r, h.deps.Config.BasePath+"/", http.StatusSeeOther) return } h.deps.Renderer.Page(w, r, "reset_password", render.PageData{ Title: "Endre passord", }) } func (h *Handler) handleResetPasswordPost(w http.ResponseWriter, r *http.Request) { user := middleware.UserFromContext(r.Context()) if user == nil { http.Redirect(w, r, h.deps.Config.BasePath+"/login", http.StatusSeeOther) return } if err := r.ParseForm(); err != nil { h.flash(w, r, "reset_password", "Ugyldig forespørsel.", "error", nil) return } password := r.FormValue("password") passwordConfirm := r.FormValue("password_confirm") if len(password) < 8 { h.flash(w, r, "reset_password", "Passordet må være minst 8 tegn.", "error", nil) return } if password != passwordConfirm { h.flash(w, r, "reset_password", "Passordene er ikke like.", "error", nil) return } if err := h.deps.Users.UpdatePassword(user.ID, password); err != nil { slog.Error("update password error", "error", err) h.flash(w, r, "reset_password", "Noe gikk galt. Prøv igjen.", "error", nil) return } if delErr := h.deps.Sessions.DeleteAllForUser(user.ID); delErr != nil { slog.Error("session cleanup error", "error", delErr) } // Create a fresh session. token, err := h.deps.Sessions.Create(user.ID) if err != nil { slog.Error("session create error", "error", err) http.Redirect(w, r, h.deps.Config.BasePath+"/login", http.StatusSeeOther) return } h.setSessionCookie(w, r, token) http.Redirect(w, r, h.deps.Config.BasePath+"/", http.StatusSeeOther) } func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) { h.deps.Renderer.Page(w, r, "home", render.PageData{}) } func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"ok"}`)) } // setSessionCookie sets the session cookie with appropriate flags for // both local and remote proxy deployments. func (h *Handler) setSessionCookie(w http.ResponseWriter, r *http.Request, token string) { cookie := &http.Cookie{ Name: "session", Value: token, Path: "/", MaxAge: int(h.deps.Config.SessionLifetime.Seconds()), HttpOnly: true, Secure: middleware.IsSecureRequest(r, h.deps.Config), SameSite: http.SameSiteLaxMode, } // Set domain from external URL if configured. if hostname := h.deps.Config.ExternalHostname(); hostname != "" { cookie.Domain = hostname } http.SetCookie(w, cookie) } // flash renders a page with a flash message. func (h *Handler) flash(w http.ResponseWriter, r *http.Request, page, message, flashType string, data any) { h.deps.Renderer.Page(w, r, page, render.PageData{ Flash: message, FlashType: flashType, Data: data, }) } func signupData(mode string) map[string]string { return map[string]string{"Mode": mode} } func (h *Handler) createSignupRequest(username, password string) error { // Check if username is already taken by an existing user. _, err := h.deps.Users.GetByUsername(username) if err == nil { return store.ErrUserExists } err = h.deps.SignupRequests.Create(username, password) if errors.Is(err, store.ErrSignupRequestExists) { return store.ErrUserExists } return err }