Player accounts: self-service player role, registration, and portal #43

Merged
whitlocktech merged 4 commits from feature/player-accounts into main 2026-07-06 20:29:45 +00:00
Member

Implements the full player-accounts plan — a public player account type living in the existing users table, reusing the current auth stack, plus a minimal React player portal.

What's included

Data model (§1)

  • Widen users.role enum to include player; make password_hash nullable (SSO-only players); add email, email_verified, status (active/pending/disabled/banned), last_login_ip; pin the username _ci collation (case-insensitive uniqueness backstop). Idempotent migrations; seeds player_registration='disabled'.

Registration (§2)

  • POST /auth/register — honeypot + registerLimiter + botScore, reserved-name blocklist, duplicate→409, auto-login. Gated by the player_registration setting.
  • SSO auto-provision in finishLogin — the only, setting-gated relaxation of the link-only policy (default off = behaves exactly as today).

Login / status (§3)

  • requireAuth and both login paths refuse non-active accounts (403), effective on the very next request. validatePassword guards a null hash; last_login_ip recorded on login.

Player self-service (§4)

  • New /api/v1/player/* group gated by requireRole('player'), reusing account.controller (TOTP, identities) plus new self-scoped changeUsername / changePassword (accountChangeLimiter). Password change re-issues the caller's session so they aren't logged out, while revoking other sessions.

SSO redirects (§5) — portal/returnTo-aware: player flows land in /account*, staff in /admin*.

Admin (§6)player role + status/email on user create/update, role/status audit entries, player_registration enum validation, derived public registration flags. UI: role/status/reset-password in UserEditor, status column + badge-player, registration toggle in Settings.

Player portal (§7)RequirePlayer guard, /account/{login,register} + settings pages, api.register + api.player.*, AuthContext.register.

Swagger (§8)Player tag + new schemas; regenerated served spec.

Verification

  • 133 server tests green — added usernamePolicy.test.js, playerAccounts.test.js; extended ssoCallback.test.js (auto-provision + portal redirect).
  • DB-backed controller smoke (21 checks) against the live dev DB: register gating, dup/reserved → 409/400, null-hash rules, self change username/password with the re-issued session surviving the tokens_valid_after cutoff, SSO-only initial password, banned-login refusal.
  • Case-insensitive username uniqueness confirmed; /public/settings exposes only derived registration flags (never the raw key).
  • Client builds clean.

Not yet done (needs a live/interactive environment)

  • Real OAuth SSO round-trip and a browser click-through of the /account portal.

🤖 Generated with Claude Code

https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV

Implements the full player-accounts plan — a public **`player`** account type living in the existing `users` table, reusing the current auth stack, plus a minimal React player portal. ## What's included **Data model (§1)** - Widen `users.role` enum to include `player`; make `password_hash` nullable (SSO-only players); add `email`, `email_verified`, `status` (`active`/`pending`/`disabled`/`banned`), `last_login_ip`; pin the username `_ci` collation (case-insensitive uniqueness backstop). Idempotent migrations; seeds `player_registration='disabled'`. **Registration (§2)** - `POST /auth/register` — honeypot + `registerLimiter` + botScore, reserved-name blocklist, duplicate→409, auto-login. Gated by the `player_registration` setting. - SSO auto-provision in `finishLogin` — the only, setting-gated relaxation of the link-only policy (default off = behaves exactly as today). **Login / status (§3)** - `requireAuth` and both login paths refuse non-`active` accounts (403), effective on the very next request. `validatePassword` guards a null hash; `last_login_ip` recorded on login. **Player self-service (§4)** - New `/api/v1/player/*` group gated by `requireRole('player')`, reusing `account.controller` (TOTP, identities) plus new self-scoped `changeUsername` / `changePassword` (`accountChangeLimiter`). Password change re-issues the caller's session so they aren't logged out, while revoking other sessions. **SSO redirects (§5)** — portal/`returnTo`-aware: player flows land in `/account*`, staff in `/admin*`. **Admin (§6)** — `player` role + `status`/`email` on user create/update, role/status audit entries, `player_registration` enum validation, derived public `registration` flags. UI: role/status/reset-password in `UserEditor`, status column + `badge-player`, registration toggle in Settings. **Player portal (§7)** — `RequirePlayer` guard, `/account/{login,register}` + settings pages, `api.register` + `api.player.*`, `AuthContext.register`. **Swagger (§8)** — `Player` tag + new schemas; regenerated served spec. ## Verification - **133 server tests green** — added `usernamePolicy.test.js`, `playerAccounts.test.js`; extended `ssoCallback.test.js` (auto-provision + portal redirect). - **DB-backed controller smoke (21 checks)** against the live dev DB: register gating, dup/reserved → 409/400, null-hash rules, self change username/password with the re-issued session surviving the `tokens_valid_after` cutoff, SSO-only initial password, banned-login refusal. - Case-insensitive username uniqueness confirmed; `/public/settings` exposes only derived `registration` flags (never the raw key). - Client builds clean. ## Not yet done (needs a live/interactive environment) - Real OAuth SSO round-trip and a browser click-through of the `/account` portal. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
wtclaude added 3 commits 2026-07-06 06:57:51 +00:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
- Widen users.role enum to include 'player'; make password_hash nullable;
  add email/email_verified/status/last_login_ip; pin username _ci collation.
- POST /auth/register (honeypot + registerLimiter + botScore, reserved-name
  blocklist, duplicate->409, auto-login). player_registration setting gates it.
- SSO auto-provision in finishLogin (setting-gated); return/portal-aware SSO
  redirects for the player portal; status refusal on login + requireAuth.
- New /player self-service group (account, change username/password, TOTP,
  identities), reusing account.controller; accountChangeLimiter.
- Admin: 'player' role + status/email on user create/update, role/status audit,
  player_registration enum validation, derived public registration flags.
- usernamePolicy module (reserved, sanitize, derive, dedup) + unit tests;
  extend SSO callback tests. 133 server tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
- Player portal: RequirePlayer guard, /account routes (login, register,
  settings) with shared PlayerShell; register reads /public/settings derived
  flags; AuthContext.register; api.register + api.player.* namespace.
- Admin UI: player role + status/email + reset-password hint in UserEditor,
  status column + badge-player in UsersAdmin, player_registration select in
  SettingsAdmin; 'disabled' SSO error copy.
- Swagger: Player tag + RegisterRequest/ChangeUsername/ChangePassword/
  PlayerAccount/OkFlag schemas; regenerated swagger-output.json.
- Fix: remove a semicolon from a schema.sql inline comment that broke the
  statement splitter in ensureSchema.

Verified against the live dev DB: schema migrations apply (player enum,
nullable password_hash, email/status/last_login_ip, seeded setting); 21-check
controller smoke (register gating, dup/reserved, null-hash rules, self change
username/password with session re-issue surviving the cutoff, SSO-only initial
password, banned-login refusal); case-insensitive uniqueness; public settings
expose only derived registration flags. Client builds; 133 server tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
wtclaude added 1 commit 2026-07-06 10:11:42 +00:00
req.user comes from getById which strips password_hash, so has_password was
always false — the account page mis-rendered a real password account as the
SSO-only 'set a password' variant (and the change-password form omitted the
required current-password field). Read the raw row for that one flag.

Caught by a browser click-through of the /account portal. Adds a regression
test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
whitlocktech approved these changes 2026-07-06 20:29:34 +00:00
whitlocktech merged commit d72deff2cc into main 2026-07-06 20:29:45 +00:00
whitlocktech deleted branch feature/player-accounts 2026-07-06 20:29:46 +00:00
Sign in to join this conversation.
No description provided.