package handlers import ( "encoding/base64" "strconv" "strings" "polar/internal/auth" "polar/internal/i18n" "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("/repos", 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")) 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("/repos", 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("/repos", 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") regAllowed := h.isRegistrationAllowed(c) u, err := h.userSvc.Register(c.UserContext(), username, email, password, regAllowed) if err != nil { data := h.baseData(c, i18n.T(lang, "nav.register")) data["FormUsername"] = username data["FormEmail"] = email data["Error"] = err.Error() return h.render(c, fiber.StatusBadRequest, "register", data) } 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("/repos", 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" { _ = h.userSvc.UpdateAvatar(c.UserContext(), u.ID, "") } else if avatarData := strings.TrimSpace(c.FormValue("avatar_data")); avatarData != "" && strings.HasPrefix(avatarData, "data:image/") { _ = h.userSvc.UpdateAvatar(c.UserContext(), u.ID, avatarData) } else if file, err := c.FormFile("avatar_file"); err == nil && file != nil && file.Size > 0 && file.Size <= 2*1024*1024 { if f, err := file.Open(); err == nil { defer f.Close() buf := make([]byte, file.Size) if _, err := f.Read(buf); err == nil { mime := file.Header.Get("Content-Type") if mime == "" { mime = "image/png" } dataURI := "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(buf) _ = h.userSvc.UpdateAvatar(c.UserContext(), u.ID, dataURI) } } } 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) } data["Users"] = users return h.render(c, fiber.StatusOK, "admin", data) } func (h *Handler) AdminSettingsSubmit(c *fiber.Ctx) error { 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" if err := h.db.SetBoolSetting(c.UserContext(), key, enabled); 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