auth.go
·
327 lines
·
10.5 KB
| 1 | package handlers |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "strconv" |
| 6 | "strings" |
| 7 | |
| 8 | "polar/internal/auth" |
| 9 | "polar/internal/db" |
| 10 | "polar/internal/i18n" |
| 11 | "polar/internal/user" |
| 12 | "polar/internal/web/middleware" |
| 13 | |
| 14 | "github.com/gofiber/fiber/v2" |
| 15 | ) |
| 16 | |
| 17 | func (h *Handler) LoginPage(c *fiber.Ctx) error { |
| 18 | if middleware.CurrentUser(c) != nil { |
| 19 | return c.Redirect("/", fiber.StatusSeeOther) |
| 20 | } |
| 21 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 22 | data := h.baseData(c, i18n.T(lang, "nav.login")) |
| 23 | data["Next"] = safeNextPath(c.Query("next")) |
| 24 | return h.render(c, fiber.StatusOK, "login", data) |
| 25 | } |
| 26 | |
| 27 | func (h *Handler) LoginSubmit(c *fiber.Ctx) error { |
| 28 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 29 | login := strings.TrimSpace(c.FormValue("login")) |
| 30 | password := c.FormValue("password") |
| 31 | |
| 32 | u, err := h.userSvc.Authenticate(c.UserContext(), login, password) |
| 33 | if err != nil { |
| 34 | data := h.baseData(c, i18n.T(lang, "nav.login")) |
| 35 | data["FormLogin"] = login |
| 36 | data["Next"] = safeNextPath(c.FormValue("next")) |
| 37 | if errors.Is(err, user.ErrPendingApproval) { |
| 38 | data["Error"] = i18n.T(lang, "auth.pending_approval") |
| 39 | } else { |
| 40 | data["Error"] = i18n.T(lang, "auth.invalid_creds") |
| 41 | } |
| 42 | return h.render(c, fiber.StatusUnauthorized, "login", data) |
| 43 | } |
| 44 | |
| 45 | sess, err := h.authSvc.CreateSession(c.UserContext(), u.ID) |
| 46 | if err != nil { |
| 47 | h.logger.Error("create session failed", "err", err) |
| 48 | return h.renderErrorT(c, fiber.StatusInternalServerError, "err.session_error", "err.session_create") |
| 49 | } |
| 50 | |
| 51 | c.Cookie(&fiber.Cookie{ |
| 52 | Name: middleware.SessionCookieName, |
| 53 | Value: sess.ID, |
| 54 | Path: "/", |
| 55 | Expires: sess.ExpiresAt, |
| 56 | HTTPOnly: true, |
| 57 | SameSite: "Lax", |
| 58 | }) |
| 59 | |
| 60 | if next := safeNextPath(c.FormValue("next")); next != "" { |
| 61 | return c.Redirect(next, fiber.StatusSeeOther) |
| 62 | } |
| 63 | return c.Redirect("/", fiber.StatusSeeOther) |
| 64 | } |
| 65 | |
| 66 | // safeNextPath returns a same-site path for post-login redirects. |
| 67 | func safeNextPath(next string) string { |
| 68 | next = strings.TrimSpace(next) |
| 69 | if next == "" || !strings.HasPrefix(next, "/") || strings.HasPrefix(next, "//") || strings.HasPrefix(next, "/\\") { |
| 70 | return "" |
| 71 | } |
| 72 | return next |
| 73 | } |
| 74 | |
| 75 | func (h *Handler) RegisterPage(c *fiber.Ctx) error { |
| 76 | if middleware.CurrentUser(c) != nil { |
| 77 | return c.Redirect("/", fiber.StatusSeeOther) |
| 78 | } |
| 79 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 80 | data := h.baseData(c, i18n.T(lang, "nav.register")) |
| 81 | return h.render(c, fiber.StatusOK, "register", data) |
| 82 | } |
| 83 | |
| 84 | func (h *Handler) RegisterSubmit(c *fiber.Ctx) error { |
| 85 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 86 | username := strings.TrimSpace(c.FormValue("username")) |
| 87 | email := strings.TrimSpace(c.FormValue("email")) |
| 88 | password := c.FormValue("password") |
| 89 | |
| 90 | mode := h.db.GetRegistrationMode(c.UserContext(), h.cfg.Auth.RegistrationEnabled) |
| 91 | u, err := h.userSvc.RegisterWithMode(c.UserContext(), username, email, password, mode) |
| 92 | if err != nil { |
| 93 | data := h.baseData(c, i18n.T(lang, "nav.register")) |
| 94 | data["FormUsername"] = username |
| 95 | data["FormEmail"] = email |
| 96 | if errors.Is(err, user.ErrRegistrationDisabled) { |
| 97 | data["Error"] = i18n.T(lang, "auth.register.disabled") |
| 98 | } else { |
| 99 | data["Error"] = err.Error() |
| 100 | } |
| 101 | return h.render(c, fiber.StatusBadRequest, "register", data) |
| 102 | } |
| 103 | |
| 104 | if !u.IsApproved { |
| 105 | h.setFlashT(c, "auth.pending_approval") |
| 106 | return c.Redirect("/login", fiber.StatusSeeOther) |
| 107 | } |
| 108 | |
| 109 | sess, err := h.authSvc.CreateSession(c.UserContext(), u.ID) |
| 110 | if err != nil { |
| 111 | h.logger.Error("create session after register failed", "err", err) |
| 112 | return c.Redirect("/login", fiber.StatusSeeOther) |
| 113 | } |
| 114 | |
| 115 | c.Cookie(&fiber.Cookie{ |
| 116 | Name: middleware.SessionCookieName, |
| 117 | Value: sess.ID, |
| 118 | Path: "/", |
| 119 | Expires: sess.ExpiresAt, |
| 120 | HTTPOnly: true, |
| 121 | SameSite: "Lax", |
| 122 | }) |
| 123 | |
| 124 | h.setFlashT(c, "flash.welcome", u.Username) |
| 125 | return c.Redirect("/", fiber.StatusSeeOther) |
| 126 | } |
| 127 | |
| 128 | func (h *Handler) LogoutSubmit(c *fiber.Ctx) error { |
| 129 | sessID := c.Cookies(middleware.SessionCookieName) |
| 130 | if sessID != "" { |
| 131 | _ = h.authSvc.DeleteSession(c.UserContext(), sessID) |
| 132 | } |
| 133 | c.ClearCookie(middleware.SessionCookieName) |
| 134 | return c.Redirect("/", fiber.StatusSeeOther) |
| 135 | } |
| 136 | |
| 137 | func (h *Handler) SettingsPage(c *fiber.Ctx) error { |
| 138 | u := middleware.CurrentUser(c) |
| 139 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 140 | data := h.baseData(c, i18n.T(lang, "settings.title")) |
| 141 | |
| 142 | tokens, err := h.authSvc.ListTokens(c.UserContext(), u.ID) |
| 143 | if err != nil { |
| 144 | h.logger.Error("list tokens failed", "err", err) |
| 145 | } |
| 146 | data["Tokens"] = tokens |
| 147 | |
| 148 | newToken := c.Cookies("polar_new_token") |
| 149 | if newToken != "" { |
| 150 | c.ClearCookie("polar_new_token") |
| 151 | data["NewRawToken"] = newToken |
| 152 | } |
| 153 | |
| 154 | return h.render(c, fiber.StatusOK, "settings", data) |
| 155 | } |
| 156 | |
| 157 | func (h *Handler) UpdateProfileSubmit(c *fiber.Ctx) error { |
| 158 | u := middleware.CurrentUser(c) |
| 159 | email := c.FormValue("email") |
| 160 | fullName := c.FormValue("full_name") |
| 161 | bio := c.FormValue("bio") |
| 162 | location := c.FormValue("location") |
| 163 | website := c.FormValue("website") |
| 164 | |
| 165 | if err := h.userSvc.UpdateProfileDetails(c.UserContext(), u.ID, email, fullName, bio, location, website); err != nil { |
| 166 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 167 | data := h.baseData(c, i18n.T(lang, "settings.title")) |
| 168 | tokens, _ := h.authSvc.ListTokens(c.UserContext(), u.ID) |
| 169 | data["Tokens"] = tokens |
| 170 | data["Error"] = err.Error() |
| 171 | return h.render(c, fiber.StatusBadRequest, "settings", data) |
| 172 | } |
| 173 | |
| 174 | if c.FormValue("remove_avatar") == "true" { |
| 175 | if err := h.userSvc.UpdateAvatar(c.UserContext(), u.ID, ""); err != nil { |
| 176 | h.logger.Error("remove profile avatar failed", "user_id", u.ID, "err", err) |
| 177 | h.setFlashT(c, "flash.image_save_failed") |
| 178 | return c.Redirect("/settings", fiber.StatusSeeOther) |
| 179 | } |
| 180 | } else if avatarData, err := readImageField(c, "avatar_data", "avatar_file", maxAvatarImageBytes); err != nil { |
| 181 | h.setFlashT(c, "flash.image_invalid") |
| 182 | return c.Redirect("/settings", fiber.StatusSeeOther) |
| 183 | } else if avatarData != "" { |
| 184 | if err := h.userSvc.UpdateAvatar(c.UserContext(), u.ID, avatarData); err != nil { |
| 185 | h.logger.Error("update profile avatar failed", "user_id", u.ID, "err", err) |
| 186 | h.setFlashT(c, "flash.image_save_failed") |
| 187 | return c.Redirect("/settings", fiber.StatusSeeOther) |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | if c.FormValue("remove_background") == "true" { |
| 192 | if err := h.userSvc.UpdateBackground(c.UserContext(), u.ID, ""); err != nil { |
| 193 | h.logger.Error("remove profile background failed", "user_id", u.ID, "err", err) |
| 194 | h.setFlashT(c, "flash.image_save_failed") |
| 195 | return c.Redirect("/settings", fiber.StatusSeeOther) |
| 196 | } |
| 197 | } else if backgroundData, err := readImageField(c, "background_data", "background_file", maxBackgroundImageBytes); err != nil { |
| 198 | h.setFlashT(c, "flash.image_invalid") |
| 199 | return c.Redirect("/settings", fiber.StatusSeeOther) |
| 200 | } else if backgroundData != "" { |
| 201 | if err := h.userSvc.UpdateBackground(c.UserContext(), u.ID, backgroundData); err != nil { |
| 202 | h.logger.Error("update profile background failed", "user_id", u.ID, "err", err) |
| 203 | h.setFlashT(c, "flash.image_save_failed") |
| 204 | return c.Redirect("/settings", fiber.StatusSeeOther) |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | h.setFlashT(c, "flash.profile_updated") |
| 209 | return c.Redirect("/settings", fiber.StatusSeeOther) |
| 210 | } |
| 211 | |
| 212 | func (h *Handler) UpdatePasswordSubmit(c *fiber.Ctx) error { |
| 213 | u := middleware.CurrentUser(c) |
| 214 | currentPwd := c.FormValue("current_password") |
| 215 | newPwd := c.FormValue("new_password") |
| 216 | |
| 217 | if err := h.userSvc.UpdatePassword(c.UserContext(), u.ID, currentPwd, newPwd); err != nil { |
| 218 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 219 | data := h.baseData(c, i18n.T(lang, "settings.title")) |
| 220 | tokens, _ := h.authSvc.ListTokens(c.UserContext(), u.ID) |
| 221 | data["Tokens"] = tokens |
| 222 | data["Error"] = err.Error() |
| 223 | return h.render(c, fiber.StatusBadRequest, "settings", data) |
| 224 | } |
| 225 | |
| 226 | h.setFlashT(c, "flash.password_updated") |
| 227 | return c.Redirect("/settings", fiber.StatusSeeOther) |
| 228 | } |
| 229 | |
| 230 | func (h *Handler) CreateTokenSubmit(c *fiber.Ctx) error { |
| 231 | u := middleware.CurrentUser(c) |
| 232 | name := c.FormValue("name") |
| 233 | |
| 234 | rawToken, _, err := h.authSvc.CreateToken(c.UserContext(), u.ID, name) |
| 235 | if err != nil { |
| 236 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 237 | data := h.baseData(c, i18n.T(lang, "settings.title")) |
| 238 | tokens, _ := h.authSvc.ListTokens(c.UserContext(), u.ID) |
| 239 | data["Tokens"] = tokens |
| 240 | data["Error"] = err.Error() |
| 241 | return h.render(c, fiber.StatusBadRequest, "settings", data) |
| 242 | } |
| 243 | |
| 244 | c.Cookie(&fiber.Cookie{ |
| 245 | Name: "polar_new_token", |
| 246 | Value: rawToken, |
| 247 | Path: "/settings", |
| 248 | MaxAge: 60, |
| 249 | HTTPOnly: true, |
| 250 | SameSite: "Lax", |
| 251 | }) |
| 252 | return c.Redirect("/settings#tokens", fiber.StatusSeeOther) |
| 253 | } |
| 254 | |
| 255 | func (h *Handler) RevokeTokenSubmit(c *fiber.Ctx) error { |
| 256 | u := middleware.CurrentUser(c) |
| 257 | tokenID, err := strconv.ParseInt(c.Params("id"), 10, 64) |
| 258 | if err != nil { |
| 259 | return c.Redirect("/settings#tokens", fiber.StatusSeeOther) |
| 260 | } |
| 261 | _ = h.authSvc.RevokeToken(c.UserContext(), u.ID, tokenID) |
| 262 | h.setFlashT(c, "flash.token_revoked") |
| 263 | return c.Redirect("/settings#tokens", fiber.StatusSeeOther) |
| 264 | } |
| 265 | |
| 266 | func (h *Handler) AdminPage(c *fiber.Ctx) error { |
| 267 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 268 | data := h.baseData(c, i18n.T(lang, "admin.title")) |
| 269 | users, err := h.userSvc.ListAll(c.UserContext()) |
| 270 | if err != nil { |
| 271 | h.logger.Error("list users failed", "err", err) |
| 272 | } |
| 273 | repos, err := h.repoSvc.ListAccessible(c.UserContext(), middleware.CurrentUser(c), 200) |
| 274 | if err != nil { |
| 275 | h.logger.Error("list repositories failed", "err", err) |
| 276 | } |
| 277 | data["Users"] = users |
| 278 | data["Repos"] = repos |
| 279 | return h.render(c, fiber.StatusOK, "admin", data) |
| 280 | } |
| 281 | |
| 282 | func (h *Handler) AdminSettingsSubmit(c *fiber.Ctx) error { |
| 283 | if mode := strings.TrimSpace(c.FormValue("registration_mode")); mode != "" { |
| 284 | if !db.ValidRegistrationMode(mode) { |
| 285 | h.setFlashT(c, "flash.admin_registration_invalid") |
| 286 | return c.Redirect("/admin", fiber.StatusSeeOther) |
| 287 | } |
| 288 | if err := h.db.SetRegistrationMode(c.UserContext(), mode); err != nil { |
| 289 | h.logger.Error("update registration mode failed", "mode", mode, "err", err) |
| 290 | return h.renderErrorT(c, fiber.StatusInternalServerError, "err.settings_error", "err.settings_save") |
| 291 | } |
| 292 | h.setFlashT(c, "flash.settings_updated") |
| 293 | return c.Redirect("/admin", fiber.StatusSeeOther) |
| 294 | } |
| 295 | |
| 296 | key := c.FormValue("setting_key") |
| 297 | val := c.FormValue("setting_val") |
| 298 | |
| 299 | // Fallback for legacy registration_enabled direct form |
| 300 | if key == "" { |
| 301 | if rVal := c.FormValue("registration_enabled"); rVal != "" { |
| 302 | key = "registration_enabled" |
| 303 | val = rVal |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | if key == "" { |
| 308 | return c.Redirect("/admin", fiber.StatusSeeOther) |
| 309 | } |
| 310 | |
| 311 | enabled := val == "true" || val == "1" || val == "on" |
| 312 | var err error |
| 313 | if key == "registration_enabled" { |
| 314 | err = h.db.SetRegistrationEnabled(c.UserContext(), enabled) |
| 315 | } else { |
| 316 | err = h.db.SetBoolSetting(c.UserContext(), key, enabled) |
| 317 | } |
| 318 | if err != nil { |
| 319 | h.logger.Error("update admin setting failed", "key", key, "err", err) |
| 320 | return h.renderErrorT(c, fiber.StatusInternalServerError, "err.settings_error", "err.settings_save") |
| 321 | } |
| 322 | |
| 323 | h.setFlashT(c, "flash.settings_updated") |
| 324 | return c.Redirect("/admin", fiber.StatusSeeOther) |
| 325 | } |
| 326 | |
| 327 | var _ = auth.ErrInvalidCredentials |
| 328 |