auth.go
·
273 строки
·
8.4 KB
| 1 | package handlers |
| 2 | |
| 3 | import ( |
| 4 | "encoding/base64" |
| 5 | "strconv" |
| 6 | "strings" |
| 7 | |
| 8 | "polar/internal/auth" |
| 9 | "polar/internal/i18n" |
| 10 | "polar/internal/web/middleware" |
| 11 | |
| 12 | "github.com/gofiber/fiber/v2" |
| 13 | ) |
| 14 | |
| 15 | func (h *Handler) LoginPage(c *fiber.Ctx) error { |
| 16 | if middleware.CurrentUser(c) != nil { |
| 17 | return c.Redirect("/", fiber.StatusSeeOther) |
| 18 | } |
| 19 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 20 | data := h.baseData(c, i18n.T(lang, "nav.login")) |
| 21 | data["Next"] = safeNextPath(c.Query("next")) |
| 22 | return h.render(c, fiber.StatusOK, "login", data) |
| 23 | } |
| 24 | |
| 25 | func (h *Handler) LoginSubmit(c *fiber.Ctx) error { |
| 26 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 27 | login := strings.TrimSpace(c.FormValue("login")) |
| 28 | password := c.FormValue("password") |
| 29 | |
| 30 | u, err := h.userSvc.Authenticate(c.UserContext(), login, password) |
| 31 | if err != nil { |
| 32 | data := h.baseData(c, i18n.T(lang, "nav.login")) |
| 33 | data["FormLogin"] = login |
| 34 | data["Next"] = safeNextPath(c.FormValue("next")) |
| 35 | data["Error"] = i18n.T(lang, "auth.invalid_creds") |
| 36 | return h.render(c, fiber.StatusUnauthorized, "login", data) |
| 37 | } |
| 38 | |
| 39 | sess, err := h.authSvc.CreateSession(c.UserContext(), u.ID) |
| 40 | if err != nil { |
| 41 | h.logger.Error("create session failed", "err", err) |
| 42 | return h.renderErrorT(c, fiber.StatusInternalServerError, "err.session_error", "err.session_create") |
| 43 | } |
| 44 | |
| 45 | c.Cookie(&fiber.Cookie{ |
| 46 | Name: middleware.SessionCookieName, |
| 47 | Value: sess.ID, |
| 48 | Path: "/", |
| 49 | Expires: sess.ExpiresAt, |
| 50 | HTTPOnly: true, |
| 51 | SameSite: "Lax", |
| 52 | }) |
| 53 | |
| 54 | if next := safeNextPath(c.FormValue("next")); next != "" { |
| 55 | return c.Redirect(next, fiber.StatusSeeOther) |
| 56 | } |
| 57 | return c.Redirect("/", fiber.StatusSeeOther) |
| 58 | } |
| 59 | |
| 60 | // safeNextPath returns a same-site path for post-login redirects. |
| 61 | func safeNextPath(next string) string { |
| 62 | next = strings.TrimSpace(next) |
| 63 | if next == "" || !strings.HasPrefix(next, "/") || strings.HasPrefix(next, "//") || strings.HasPrefix(next, "/\\") { |
| 64 | return "" |
| 65 | } |
| 66 | return next |
| 67 | } |
| 68 | |
| 69 | func (h *Handler) RegisterPage(c *fiber.Ctx) error { |
| 70 | if middleware.CurrentUser(c) != nil { |
| 71 | return c.Redirect("/", fiber.StatusSeeOther) |
| 72 | } |
| 73 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 74 | data := h.baseData(c, i18n.T(lang, "nav.register")) |
| 75 | return h.render(c, fiber.StatusOK, "register", data) |
| 76 | } |
| 77 | |
| 78 | func (h *Handler) RegisterSubmit(c *fiber.Ctx) error { |
| 79 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 80 | username := strings.TrimSpace(c.FormValue("username")) |
| 81 | email := strings.TrimSpace(c.FormValue("email")) |
| 82 | password := c.FormValue("password") |
| 83 | |
| 84 | regAllowed := h.isRegistrationAllowed(c) |
| 85 | u, err := h.userSvc.Register(c.UserContext(), username, email, password, regAllowed) |
| 86 | if err != nil { |
| 87 | data := h.baseData(c, i18n.T(lang, "nav.register")) |
| 88 | data["FormUsername"] = username |
| 89 | data["FormEmail"] = email |
| 90 | data["Error"] = err.Error() |
| 91 | return h.render(c, fiber.StatusBadRequest, "register", data) |
| 92 | } |
| 93 | |
| 94 | sess, err := h.authSvc.CreateSession(c.UserContext(), u.ID) |
| 95 | if err != nil { |
| 96 | h.logger.Error("create session after register failed", "err", err) |
| 97 | return c.Redirect("/login", fiber.StatusSeeOther) |
| 98 | } |
| 99 | |
| 100 | c.Cookie(&fiber.Cookie{ |
| 101 | Name: middleware.SessionCookieName, |
| 102 | Value: sess.ID, |
| 103 | Path: "/", |
| 104 | Expires: sess.ExpiresAt, |
| 105 | HTTPOnly: true, |
| 106 | SameSite: "Lax", |
| 107 | }) |
| 108 | |
| 109 | h.setFlashT(c, "flash.welcome", u.Username) |
| 110 | return c.Redirect("/", fiber.StatusSeeOther) |
| 111 | } |
| 112 | |
| 113 | func (h *Handler) LogoutSubmit(c *fiber.Ctx) error { |
| 114 | sessID := c.Cookies(middleware.SessionCookieName) |
| 115 | if sessID != "" { |
| 116 | _ = h.authSvc.DeleteSession(c.UserContext(), sessID) |
| 117 | } |
| 118 | c.ClearCookie(middleware.SessionCookieName) |
| 119 | return c.Redirect("/", fiber.StatusSeeOther) |
| 120 | } |
| 121 | |
| 122 | func (h *Handler) SettingsPage(c *fiber.Ctx) error { |
| 123 | u := middleware.CurrentUser(c) |
| 124 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 125 | data := h.baseData(c, i18n.T(lang, "settings.title")) |
| 126 | |
| 127 | tokens, err := h.authSvc.ListTokens(c.UserContext(), u.ID) |
| 128 | if err != nil { |
| 129 | h.logger.Error("list tokens failed", "err", err) |
| 130 | } |
| 131 | data["Tokens"] = tokens |
| 132 | |
| 133 | newToken := c.Cookies("polar_new_token") |
| 134 | if newToken != "" { |
| 135 | c.ClearCookie("polar_new_token") |
| 136 | data["NewRawToken"] = newToken |
| 137 | } |
| 138 | |
| 139 | return h.render(c, fiber.StatusOK, "settings", data) |
| 140 | } |
| 141 | |
| 142 | func (h *Handler) UpdateProfileSubmit(c *fiber.Ctx) error { |
| 143 | u := middleware.CurrentUser(c) |
| 144 | email := c.FormValue("email") |
| 145 | fullName := c.FormValue("full_name") |
| 146 | bio := c.FormValue("bio") |
| 147 | location := c.FormValue("location") |
| 148 | website := c.FormValue("website") |
| 149 | |
| 150 | if err := h.userSvc.UpdateProfileDetails(c.UserContext(), u.ID, email, fullName, bio, location, website); err != nil { |
| 151 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 152 | data := h.baseData(c, i18n.T(lang, "settings.title")) |
| 153 | tokens, _ := h.authSvc.ListTokens(c.UserContext(), u.ID) |
| 154 | data["Tokens"] = tokens |
| 155 | data["Error"] = err.Error() |
| 156 | return h.render(c, fiber.StatusBadRequest, "settings", data) |
| 157 | } |
| 158 | |
| 159 | if c.FormValue("remove_avatar") == "true" { |
| 160 | _ = h.userSvc.UpdateAvatar(c.UserContext(), u.ID, "") |
| 161 | } else if avatarData := strings.TrimSpace(c.FormValue("avatar_data")); avatarData != "" && strings.HasPrefix(avatarData, "data:image/") { |
| 162 | _ = h.userSvc.UpdateAvatar(c.UserContext(), u.ID, avatarData) |
| 163 | } else if file, err := c.FormFile("avatar_file"); err == nil && file != nil && file.Size > 0 && file.Size <= 2*1024*1024 { |
| 164 | if f, err := file.Open(); err == nil { |
| 165 | defer f.Close() |
| 166 | buf := make([]byte, file.Size) |
| 167 | if _, err := f.Read(buf); err == nil { |
| 168 | mime := file.Header.Get("Content-Type") |
| 169 | if mime == "" { |
| 170 | mime = "image/png" |
| 171 | } |
| 172 | dataURI := "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(buf) |
| 173 | _ = h.userSvc.UpdateAvatar(c.UserContext(), u.ID, dataURI) |
| 174 | } |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | h.setFlashT(c, "flash.profile_updated") |
| 179 | return c.Redirect("/settings", fiber.StatusSeeOther) |
| 180 | } |
| 181 | |
| 182 | func (h *Handler) UpdatePasswordSubmit(c *fiber.Ctx) error { |
| 183 | u := middleware.CurrentUser(c) |
| 184 | currentPwd := c.FormValue("current_password") |
| 185 | newPwd := c.FormValue("new_password") |
| 186 | |
| 187 | if err := h.userSvc.UpdatePassword(c.UserContext(), u.ID, currentPwd, newPwd); err != nil { |
| 188 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 189 | data := h.baseData(c, i18n.T(lang, "settings.title")) |
| 190 | tokens, _ := h.authSvc.ListTokens(c.UserContext(), u.ID) |
| 191 | data["Tokens"] = tokens |
| 192 | data["Error"] = err.Error() |
| 193 | return h.render(c, fiber.StatusBadRequest, "settings", data) |
| 194 | } |
| 195 | |
| 196 | h.setFlashT(c, "flash.password_updated") |
| 197 | return c.Redirect("/settings", fiber.StatusSeeOther) |
| 198 | } |
| 199 | |
| 200 | func (h *Handler) CreateTokenSubmit(c *fiber.Ctx) error { |
| 201 | u := middleware.CurrentUser(c) |
| 202 | name := c.FormValue("name") |
| 203 | |
| 204 | rawToken, _, err := h.authSvc.CreateToken(c.UserContext(), u.ID, name) |
| 205 | if err != nil { |
| 206 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 207 | data := h.baseData(c, i18n.T(lang, "settings.title")) |
| 208 | tokens, _ := h.authSvc.ListTokens(c.UserContext(), u.ID) |
| 209 | data["Tokens"] = tokens |
| 210 | data["Error"] = err.Error() |
| 211 | return h.render(c, fiber.StatusBadRequest, "settings", data) |
| 212 | } |
| 213 | |
| 214 | c.Cookie(&fiber.Cookie{ |
| 215 | Name: "polar_new_token", |
| 216 | Value: rawToken, |
| 217 | Path: "/settings", |
| 218 | MaxAge: 60, |
| 219 | HTTPOnly: true, |
| 220 | SameSite: "Lax", |
| 221 | }) |
| 222 | return c.Redirect("/settings#tokens", fiber.StatusSeeOther) |
| 223 | } |
| 224 | |
| 225 | func (h *Handler) RevokeTokenSubmit(c *fiber.Ctx) error { |
| 226 | u := middleware.CurrentUser(c) |
| 227 | tokenID, err := strconv.ParseInt(c.Params("id"), 10, 64) |
| 228 | if err != nil { |
| 229 | return c.Redirect("/settings#tokens", fiber.StatusSeeOther) |
| 230 | } |
| 231 | _ = h.authSvc.RevokeToken(c.UserContext(), u.ID, tokenID) |
| 232 | h.setFlashT(c, "flash.token_revoked") |
| 233 | return c.Redirect("/settings#tokens", fiber.StatusSeeOther) |
| 234 | } |
| 235 | |
| 236 | func (h *Handler) AdminPage(c *fiber.Ctx) error { |
| 237 | lang, _ := c.Locals("lang").(i18n.Lang) |
| 238 | data := h.baseData(c, i18n.T(lang, "admin.title")) |
| 239 | users, err := h.userSvc.ListAll(c.UserContext()) |
| 240 | if err != nil { |
| 241 | h.logger.Error("list users failed", "err", err) |
| 242 | } |
| 243 | data["Users"] = users |
| 244 | return h.render(c, fiber.StatusOK, "admin", data) |
| 245 | } |
| 246 | |
| 247 | func (h *Handler) AdminSettingsSubmit(c *fiber.Ctx) error { |
| 248 | key := c.FormValue("setting_key") |
| 249 | val := c.FormValue("setting_val") |
| 250 | |
| 251 | // Fallback for legacy registration_enabled direct form |
| 252 | if key == "" { |
| 253 | if rVal := c.FormValue("registration_enabled"); rVal != "" { |
| 254 | key = "registration_enabled" |
| 255 | val = rVal |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | if key == "" { |
| 260 | return c.Redirect("/admin", fiber.StatusSeeOther) |
| 261 | } |
| 262 | |
| 263 | enabled := val == "true" || val == "1" || val == "on" |
| 264 | if err := h.db.SetBoolSetting(c.UserContext(), key, enabled); err != nil { |
| 265 | h.logger.Error("update admin setting failed", "key", key, "err", err) |
| 266 | return h.renderErrorT(c, fiber.StatusInternalServerError, "err.settings_error", "err.settings_save") |
| 267 | } |
| 268 | |
| 269 | h.setFlashT(c, "flash.settings_updated") |
| 270 | return c.Redirect("/admin", fiber.StatusSeeOther) |
| 271 | } |
| 272 | |
| 273 | var _ = auth.ErrInvalidCredentials |
| 274 |