package handlers import ( "errors" "strconv" "strings" "polar/internal/auth" "polar/internal/db" "polar/internal/i18n" "polar/internal/user" "polar/internal/web/middleware" "github.com/gofiber/fiber/v2" ) func (h *Handler) LoginPage(c *fiber.Ctx) error { if middleware.CurrentUser(c) != nil { return c.Redirect("/", fiber.StatusSeeOther) } lang, _ := c.Locals("lang").(i18n.Lang) data := h.baseData(c, i18n.T(lang, "nav.login")) data["Next"] = safeNextPath(c.Query("next")) return h.render(c, fiber.StatusOK, "login", data) } func (h *Handler) LoginSubmit(c *fiber.Ctx) error { lang, _ := c.Locals("lang").(i18n.Lang) login := strings.TrimSpace(c.FormValue("login")) password := c.FormValue("password") u, err := h.userSvc.Authenticate(c.UserContext(), login, password) if err != nil { data := h.baseData(c, i18n.T(lang, "nav.login")) data["FormLogin"] = login data["Next"] = safeNextPath(c.FormValue("next")) if errors.Is(err, user.ErrPendingApproval) { data["Error"] = i18n.T(lang, "auth.pending_approval") } else { data["Error"] = i18n.T(lang, "auth.invalid_creds") } return h.render(c, fiber.StatusUnauthorized, "login", data) } sess, err := h.authSvc.CreateSession(c.UserContext(), u.ID) if err != nil { h.logger.Error("create session failed", "err", err) return h.renderErrorT(c, fiber.StatusInternalServerError, "err.session_error", "err.session_create") } c.Cookie(&fiber.Cookie{ Name: middleware.SessionCookieName, Value: sess.ID, Path: "/", Expires: sess.ExpiresAt, HTTPOnly: true, SameSite: "Lax", }) if next := safeNextPath(c.FormValue("next")); next != "" { return c.Redirect(next, fiber.StatusSeeOther) } return c.Redirect("/", fiber.StatusSeeOther) } // safeNextPath returns a same-site path for post-login redirects. func safeNextPath(next string) string { next = strings.TrimSpace(next) if next == "" || !strings.HasPrefix(next, "/") || strings.HasPrefix(next, "//") || strings.HasPrefix(next, "/\\") { return "" } return next } func (h *Handler) RegisterPage(c *fiber.Ctx) error { if middleware.CurrentUser(c) != nil { return c.Redirect("/", fiber.StatusSeeOther) } lang, _ := c.Locals("lang").(i18n.Lang) data := h.baseData(c, i18n.T(lang, "nav.register")) return h.render(c, fiber.StatusOK, "register", data) } func (h *Handler) RegisterSubmit(c *fiber.Ctx) error { lang, _ := c.Locals("lang").(i18n.Lang) username := strings.TrimSpace(c.FormValue("username")) email := strings.TrimSpace(c.FormValue("email")) password := c.FormValue("password") mode := h.db.GetRegistrationMode(c.UserContext(), h.cfg.Auth.RegistrationEnabled) u, err := h.userSvc.RegisterWithMode(c.UserContext(), username, email, password, mode) if err != nil { data := h.baseData(c, i18n.T(lang, "nav.register")) data["FormUsername"] = username data["FormEmail"] = email if errors.Is(err, user.ErrRegistrationDisabled) { data["Error"] = i18n.T(lang, "auth.register.disabled") } else { data["Error"] = err.Error() } return h.render(c, fiber.StatusBadRequest, "register", data) } if !u.IsApproved { h.setFlashT(c, "auth.pending_approval") return c.Redirect("/login", fiber.StatusSeeOther) } sess, err := h.authSvc.CreateSession(c.UserContext(), u.ID) if err != nil { h.logger.Error("create session after register failed", "err", err) return c.Redirect("/login", fiber.StatusSeeOther) } c.Cookie(&fiber.Cookie{ Name: middleware.SessionCookieName, Value: sess.ID, Path: "/", Expires: sess.ExpiresAt, HTTPOnly: true, SameSite: "Lax", }) h.setFlashT(c, "flash.welcome", u.Username) return c.Redirect("/", fiber.StatusSeeOther) } func (h *Handler) LogoutSubmit(c *fiber.Ctx) error { sessID := c.Cookies(middleware.SessionCookieName) if sessID != "" { _ = h.authSvc.DeleteSession(c.UserContext(), sessID) } c.ClearCookie(middleware.SessionCookieName) return c.Redirect("/", fiber.StatusSeeOther) } func (h *Handler) SettingsPage(c *fiber.Ctx) error { u := middleware.CurrentUser(c) lang, _ := c.Locals("lang").(i18n.Lang) data := h.baseData(c, i18n.T(lang, "settings.title")) tokens, err := h.authSvc.ListTokens(c.UserContext(), u.ID) if err != nil { h.logger.Error("list tokens failed", "err", err) } data["Tokens"] = tokens newToken := c.Cookies("polar_new_token") if newToken != "" { c.ClearCookie("polar_new_token") data["NewRawToken"] = newToken } return h.render(c, fiber.StatusOK, "settings", data) } func (h *Handler) UpdateProfileSubmit(c *fiber.Ctx) error { u := middleware.CurrentUser(c) email := c.FormValue("email") fullName := c.FormValue("full_name") bio := c.FormValue("bio") location := c.FormValue("location") website := c.FormValue("website") if err := h.userSvc.UpdateProfileDetails(c.UserContext(), u.ID, email, fullName, bio, location, website); err != nil { lang, _ := c.Locals("lang").(i18n.Lang) data := h.baseData(c, i18n.T(lang, "settings.title")) tokens, _ := h.authSvc.ListTokens(c.UserContext(), u.ID) data["Tokens"] = tokens data["Error"] = err.Error() return h.render(c, fiber.StatusBadRequest, "settings", data) } if c.FormValue("remove_avatar") == "true" { if err := h.userSvc.UpdateAvatar(c.UserContext(), u.ID, ""); err != nil { h.logger.Error("remove profile avatar failed", "user_id", u.ID, "err", err) h.setFlashT(c, "flash.image_save_failed") return c.Redirect("/settings", fiber.StatusSeeOther) } } else if avatarData, err := readImageField(c, "avatar_data", "avatar_file", maxAvatarImageBytes); err != nil { h.setFlashT(c, "flash.image_invalid") return c.Redirect("/settings", fiber.StatusSeeOther) } else if avatarData != "" { if err := h.userSvc.UpdateAvatar(c.UserContext(), u.ID, avatarData); err != nil { h.logger.Error("update profile avatar failed", "user_id", u.ID, "err", err) h.setFlashT(c, "flash.image_save_failed") return c.Redirect("/settings", fiber.StatusSeeOther) } } if c.FormValue("remove_background") == "true" { if err := h.userSvc.UpdateBackground(c.UserContext(), u.ID, ""); err != nil { h.logger.Error("remove profile background failed", "user_id", u.ID, "err", err) h.setFlashT(c, "flash.image_save_failed") return c.Redirect("/settings", fiber.StatusSeeOther) } } else if backgroundData, err := readImageField(c, "background_data", "background_file", maxBackgroundImageBytes); err != nil { h.setFlashT(c, "flash.image_invalid") return c.Redirect("/settings", fiber.StatusSeeOther) } else if backgroundData != "" { if err := h.userSvc.UpdateBackground(c.UserContext(), u.ID, backgroundData); err != nil { h.logger.Error("update profile background failed", "user_id", u.ID, "err", err) h.setFlashT(c, "flash.image_save_failed") return c.Redirect("/settings", fiber.StatusSeeOther) } } h.setFlashT(c, "flash.profile_updated") return c.Redirect("/settings", fiber.StatusSeeOther) } func (h *Handler) UpdatePasswordSubmit(c *fiber.Ctx) error { u := middleware.CurrentUser(c) currentPwd := c.FormValue("current_password") newPwd := c.FormValue("new_password") if err := h.userSvc.UpdatePassword(c.UserContext(), u.ID, currentPwd, newPwd); err != nil { lang, _ := c.Locals("lang").(i18n.Lang) data := h.baseData(c, i18n.T(lang, "settings.title")) tokens, _ := h.authSvc.ListTokens(c.UserContext(), u.ID) data["Tokens"] = tokens data["Error"] = err.Error() return h.render(c, fiber.StatusBadRequest, "settings", data) } h.setFlashT(c, "flash.password_updated") return c.Redirect("/settings", fiber.StatusSeeOther) } func (h *Handler) CreateTokenSubmit(c *fiber.Ctx) error { u := middleware.CurrentUser(c) name := c.FormValue("name") rawToken, _, err := h.authSvc.CreateToken(c.UserContext(), u.ID, name) if err != nil { lang, _ := c.Locals("lang").(i18n.Lang) data := h.baseData(c, i18n.T(lang, "settings.title")) tokens, _ := h.authSvc.ListTokens(c.UserContext(), u.ID) data["Tokens"] = tokens data["Error"] = err.Error() return h.render(c, fiber.StatusBadRequest, "settings", data) } c.Cookie(&fiber.Cookie{ Name: "polar_new_token", Value: rawToken, Path: "/settings", MaxAge: 60, HTTPOnly: true, SameSite: "Lax", }) return c.Redirect("/settings#tokens", fiber.StatusSeeOther) } func (h *Handler) RevokeTokenSubmit(c *fiber.Ctx) error { u := middleware.CurrentUser(c) tokenID, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil { return c.Redirect("/settings#tokens", fiber.StatusSeeOther) } _ = h.authSvc.RevokeToken(c.UserContext(), u.ID, tokenID) h.setFlashT(c, "flash.token_revoked") return c.Redirect("/settings#tokens", fiber.StatusSeeOther) } func (h *Handler) AdminPage(c *fiber.Ctx) error { lang, _ := c.Locals("lang").(i18n.Lang) data := h.baseData(c, i18n.T(lang, "admin.title")) users, err := h.userSvc.ListAll(c.UserContext()) if err != nil { h.logger.Error("list users failed", "err", err) } repos, err := h.repoSvc.ListAccessible(c.UserContext(), middleware.CurrentUser(c), 200) if err != nil { h.logger.Error("list repositories failed", "err", err) } data["Users"] = users data["Repos"] = repos return h.render(c, fiber.StatusOK, "admin", data) } func (h *Handler) AdminSettingsSubmit(c *fiber.Ctx) error { if mode := strings.TrimSpace(c.FormValue("registration_mode")); mode != "" { if !db.ValidRegistrationMode(mode) { h.setFlashT(c, "flash.admin_registration_invalid") return c.Redirect("/admin", fiber.StatusSeeOther) } if err := h.db.SetRegistrationMode(c.UserContext(), mode); err != nil { h.logger.Error("update registration mode failed", "mode", mode, "err", err) return h.renderErrorT(c, fiber.StatusInternalServerError, "err.settings_error", "err.settings_save") } h.setFlashT(c, "flash.settings_updated") return c.Redirect("/admin", fiber.StatusSeeOther) } key := c.FormValue("setting_key") val := c.FormValue("setting_val") // Fallback for legacy registration_enabled direct form if key == "" { if rVal := c.FormValue("registration_enabled"); rVal != "" { key = "registration_enabled" val = rVal } } if key == "" { return c.Redirect("/admin", fiber.StatusSeeOther) } enabled := val == "true" || val == "1" || val == "on" var err error if key == "registration_enabled" { err = h.db.SetRegistrationEnabled(c.UserContext(), enabled) } else { err = h.db.SetBoolSetting(c.UserContext(), key, enabled) } if err != nil { h.logger.Error("update admin setting failed", "key", key, "err", err) return h.renderErrorT(c, fiber.StatusInternalServerError, "err.settings_error", "err.settings_save") } h.setFlashT(c, "flash.settings_updated") return c.Redirect("/admin", fiber.StatusSeeOther) } var _ = auth.ErrInvalidCredentials