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