feat(server): expose password_set on /auth/me + TS User type (#819)

The delete-account UI must branch between a password prompt (self-host or
any user with a password) and a confirm-only flow (OAuth-only users with
no password). The client had no signal for this: oauth_providers is not a
valid proxy since a user can have both a password and linked OAuth.

Add "password_set": user.HasPassword() to the /auth/me response map and
password_set?: boolean to the TS User interface. A handler test asserts
the field for both a password user (true, via bootstrap) and an
OAuth-only user (false, via CreateOAuthUser).

Closes TASK-1957.

Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST
This commit is contained in:
xarmian
2026-07-05 18:44:26 -04:00
committed by GitHub
parent 0faa481e82
commit 1d8b04e279
3 changed files with 41 additions and 0 deletions
+1
View File
@@ -904,6 +904,7 @@ func (s *Server) handleGetCurrentUser(w http.ResponseWriter, r *http.Request) {
"role": user.Role,
"avatar_url": user.AvatarURL,
"totp_enabled": user.TOTPEnabled,
"password_set": user.HasPassword(),
"created_at": user.CreatedAt,
"updated_at": user.UpdatedAt,
}
+39
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/PerpetualSoftware/pad/internal/models"
)
@@ -410,6 +411,44 @@ func TestAuthMeEndpoint(t *testing.T) {
if user["email"] != "me@test.com" {
t.Errorf("expected email 'me@test.com', got %v", user["email"])
}
// A bootstrapped user set a password, so password_set must be true. The
// delete-account UI keys off this to show a password prompt vs a
// confirm-only flow (TASK-1957).
if set, ok := user["password_set"].(bool); !ok || !set {
t.Errorf("expected password_set=true for a password user, got %v", user["password_set"])
}
}
// TestAuthMeReportsPasswordSetForOAuthUser pins the OAuth-only branch of
// TASK-1957: a user created via OAuth (no password) must report
// password_set=false so the client can offer confirm-only account deletion.
func TestAuthMeReportsPasswordSetForOAuthUser(t *testing.T) {
srv := testServer(t)
// Bootstrap a first user so the instance is initialized and auth is enforced.
bootstrapFirstUser(t, srv, "admin@test.com", "Admin")
oauthUser, err := srv.store.CreateOAuthUser("oauth@test.com", "OAuth Only", "")
if err != nil {
t.Fatalf("CreateOAuthUser: %v", err)
}
if oauthUser.HasPassword() {
t.Fatal("expected freshly created OAuth user to have no password")
}
token, err := srv.store.CreateSession(oauthUser.ID, "go-test", "192.0.2.1", "", 24*time.Hour)
if err != nil {
t.Fatalf("CreateSession: %v", err)
}
rr := doRequestWithCookie(srv, "GET", "/api/v1/auth/me", nil, token)
if rr.Code != http.StatusOK {
t.Fatalf("me: expected 200, got %d: %s", rr.Code, rr.Body.String())
}
var user map[string]interface{}
parseJSON(t, rr, &user)
if set, ok := user["password_set"].(bool); !ok || set {
t.Errorf("expected password_set=false for an OAuth-only user, got %v", user["password_set"])
}
}
func TestDuplicateRegistration(t *testing.T) {
+1
View File
@@ -9,6 +9,7 @@ export interface User {
avatar_url?: string;
oauth_providers?: string[];
totp_enabled?: boolean;
password_set?: boolean;
created_at: string;
updated_at: string;
}