Add account management page

Add a new /web/account page, reachable by clicking the username in
the nav, with two forms: updating display name/email, and changing
the password (requires the current password, min 8 chars, confirm
match). Add db.SetProfile to persist display name/email.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-08-21 06:32:18 +02:00
co-authored by Copilot
parent 1c039e6396
commit 8b10386c91
9 changed files with 527 additions and 5 deletions
+94
View File
@@ -223,3 +223,97 @@ type emptyStaticFS struct{}
func (emptyStaticFS) Open(name string) (http.File, error) {
return nil, os.ErrNotExist
}
func TestAccountRequiresLogin(t *testing.T) {
s := newTestServer(t)
handler := s.Handler(emptyStaticFS{})
req := httptest.NewRequest(http.MethodGet, "/account", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusSeeOther {
t.Fatalf("expected redirect to login, got %d", rr.Code)
}
}
func TestAccountShowsOwnProfile(t *testing.T) {
s := newTestServer(t)
handler := s.Handler(emptyStaticFS{})
cookie := loginAs(t, handler, "alice", "password")
req := httptest.NewRequest(http.MethodGet, "/account", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "alice") {
t.Fatalf("expected account page to show username, got: %s", rr.Body.String())
}
}
func TestAccountUpdateProfile(t *testing.T) {
s := newTestServer(t)
handler := s.Handler(emptyStaticFS{})
cookie := loginAs(t, handler, "alice", "password")
form := url.Values{"display_name": {"Alice Example"}, "email": {"alice@example.com"}}
req := httptest.NewRequest(http.MethodPost, "/account/profile", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "Profile updated.") {
t.Fatalf("expected success message, got: %s", rr.Body.String())
}
u, err := s.dbase.GetUser("alice")
if err != nil {
t.Fatalf("GetUser: %v", err)
}
if u.DisplayName != "Alice Example" || u.Email != "alice@example.com" {
t.Fatalf("expected profile to be persisted, got %+v", u)
}
}
func TestAccountChangePassword(t *testing.T) {
s := newTestServer(t)
handler := s.Handler(emptyStaticFS{})
cookie := loginAs(t, handler, "alice", "password")
// Wrong current password is rejected.
form := url.Values{"current_password": {"wrong"}, "new_password": {"newpassword123"}, "confirm_password": {"newpassword123"}}
req := httptest.NewRequest(http.MethodPost, "/account/password", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "current password is incorrect") {
t.Fatalf("expected error for wrong current password, got: %s", rr.Body.String())
}
if s.dbase.VerifyPassword("alice", "newpassword123") {
t.Fatal("password should not have changed")
}
// Correct current password succeeds.
form = url.Values{"current_password": {"password"}, "new_password": {"newpassword123"}, "confirm_password": {"newpassword123"}}
req = httptest.NewRequest(http.MethodPost, "/account/password", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "Password changed.") {
t.Fatalf("expected success message, got: %s", rr.Body.String())
}
if !s.dbase.VerifyPassword("alice", "newpassword123") {
t.Fatal("expected password to have changed")
}
}