rin
Public Open-source lightweight git hosting.
auth.go · 327 lines · 10.5 KB
Raw
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