tenseleyflow/shithub / 29c7714

Browse files

Accept email login identifiers

Authored by espadonne
SHA
29c771476165dbae240baae69184dbef83d552cf
Parents
e4f8a82
Tree
13c9835

3 changed files

StatusFile+-
M internal/web/handlers/auth/auth.go 15 4
M internal/web/handlers/auth/auth_test.go 26 0
M internal/web/templates/auth/login.html 1 1
internal/web/handlers/auth/auth.gomodified
@@ -337,6 +337,17 @@ type loginForm struct {
337337
 	Username string
338338
 }
339339
 
340
+func (h *Handlers) loginUser(ctx context.Context, identifier string) (usersdb.User, error) {
341
+	if strings.Contains(identifier, "@") {
342
+		em, err := h.q.GetUserEmailByAddress(ctx, h.d.Pool, identifier)
343
+		if err != nil {
344
+			return usersdb.User{}, err
345
+		}
346
+		return h.q.GetUserIncludingDeleted(ctx, h.d.Pool, em.UserID)
347
+	}
348
+	return h.q.GetUserByUsernameIncludingDeleted(ctx, h.d.Pool, identifier)
349
+}
350
+
340351
 func (h *Handlers) loginForm(w http.ResponseWriter, r *http.Request) {
341352
 	notice := ""
342353
 	switch r.URL.Query().Get("notice") {
@@ -363,7 +374,7 @@ func (h *Handlers) loginSubmit(w http.ResponseWriter, r *http.Request) {
363374
 		h.d.Render.HTTPError(w, r, http.StatusBadRequest, "form parse")
364375
 		return
365376
 	}
366
-	username := strings.ToLower(strings.TrimSpace(r.PostFormValue("username")))
377
+	identifier := strings.ToLower(strings.TrimSpace(r.PostFormValue("username")))
367378
 	pw := r.PostFormValue("password")
368379
 	next := r.PostFormValue("next")
369380
 
@@ -371,13 +382,13 @@ func (h *Handlers) loginSubmit(w http.ResponseWriter, r *http.Request) {
371382
 		h.renderPage(w, r, "auth/login", map[string]any{
372383
 			"Title":     "Sign in",
373384
 			"CSRFToken": middleware.CSRFTokenForRequest(r),
374
-			"Form":      loginForm{Username: username},
385
+			"Form":      loginForm{Username: identifier},
375386
 			"Error":     msg,
376387
 			"Next":      next,
377388
 		})
378389
 	}
379390
 
380
-	throttleKey := fmt.Sprintf("ip:%s|%s", clientIP(r), username)
391
+	throttleKey := fmt.Sprintf("ip:%s|%s", clientIP(r), identifier)
381392
 	if err := h.d.Limiter.Hit(r.Context(), h.d.Pool, throttle.Limit{
382393
 		Scope: "login", Identifier: throttleKey,
383394
 		Max: 6, Window: 15 * time.Minute,
@@ -390,7 +401,7 @@ func (h *Handlers) loginSubmit(w http.ResponseWriter, r *http.Request) {
390401
 	// IncludingDeleted lets us spot soft-deleted users so we can restore
391402
 	// them on login during the grace window. Past the window they look
392403
 	// indistinguishable from "doesn't exist" — same response, same timing.
393
-	user, err := h.q.GetUserByUsernameIncludingDeleted(r.Context(), h.d.Pool, username)
404
+	user, err := h.loginUser(r.Context(), identifier)
394405
 	if err != nil {
395406
 		// User doesn't exist — still hash to keep timing constant.
396407
 		password.VerifyAgainstDummy(pw)
internal/web/handlers/auth/auth_test.gomodified
@@ -403,6 +403,32 @@ func TestSignup_Verify_Login_Logout(t *testing.T) {
403403
 	_ = resp.Body.Close()
404404
 }
405405
 
406
+func TestLogin_AcceptsEmailAddress(t *testing.T) {
407
+	t.Parallel()
408
+	srv, sender := newTestServer(t, true)
409
+	cli := newClient(t, srv)
410
+
411
+	mustSignup(t, cli, "emailuser", "emailuser@example.com", "correct horse battery staple")
412
+	tok := extractTokenFromMessage(t, sender.all()[0], "/verify-email")
413
+	resp := cli.get(t, "/verify-email/"+tok)
414
+	_ = resp.Body.Close()
415
+
416
+	csrf := cli.extractCSRF(t, "/login")
417
+	resp = cli.post(t, "/login", url.Values{
418
+		"csrf_token": {csrf},
419
+		"username":   {"EmailUser@Example.com"},
420
+		"password":   {"correct horse battery staple"},
421
+	})
422
+	if resp.StatusCode != http.StatusSeeOther {
423
+		body, _ := io.ReadAll(resp.Body)
424
+		t.Fatalf("email login: status %d body=%s", resp.StatusCode, body)
425
+	}
426
+	if loc := resp.Header.Get("Location"); loc != "/" {
427
+		t.Fatalf("email login redirect: %q, want /", loc)
428
+	}
429
+	_ = resp.Body.Close()
430
+}
431
+
406432
 func TestPasswordReset_EndToEnd(t *testing.T) {
407433
 	t.Parallel()
408434
 	srv, sender := newTestServer(t, false)
internal/web/templates/auth/login.htmlmodified
@@ -7,7 +7,7 @@
77
     <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
88
     <input type="hidden" name="next" value="{{ .Next }}">
99
     <label>
10
-      <span>Username</span>
10
+      <span>Username or email address</span>
1111
       <input type="text" name="username" required autocomplete="username" autofocus
1212
              value="{{ .Form.Username }}">
1313
     </label>