Player accounts frontend + Swagger + schema comment fix

- 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
This commit is contained in:
2026-07-06 01:49:12 -05:00
parent f8bcc7f6a3
commit 5daf260db9
16 changed files with 2102 additions and 19 deletions

View File

@@ -10,6 +10,18 @@ const FIELDS = [
{ key: 'maintenance_message', label: 'Maintenance message', long: true },
{ key: 'status_message', label: 'Status message' },
{ key: 'contact_email', label: 'Contact email' },
{
key: 'player_registration',
label: 'Player registration',
help: 'Who can create a player account, and how. Off by default.',
options: [
{ value: 'disabled', label: 'Disabled — no self-registration' },
{ value: 'password', label: 'Password — username + password sign-up' },
{ value: 'sso', label: 'SSO — sign up with a linked provider' },
{ value: 'both', label: 'Both — password and SSO' },
],
fallback: 'disabled',
},
]
export default function SettingsAdmin() {
@@ -28,7 +40,7 @@ export default function SettingsAdmin() {
.then((all) => {
if (!active) return
const v = {}
FIELDS.forEach((f) => (v[f.key] = all[f.key] ?? ''))
FIELDS.forEach((f) => (v[f.key] = all[f.key] ?? f.fallback ?? ''))
setValues(v)
setInitial(v)
})
@@ -68,11 +80,24 @@ export default function SettingsAdmin() {
{FIELDS.map((f) => (
<label key={f.key} style={{ display: 'block' }}>
<span className="field-label">{f.label}</span>
{f.long ? (
{f.options ? (
<select value={values[f.key]} onChange={set(f.key)} className="select">
{f.options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
) : f.long ? (
<textarea value={values[f.key]} onChange={set(f.key)} className="textarea" style={{ minHeight: 90 }} />
) : (
<input type="text" value={values[f.key]} onChange={set(f.key)} className="input" />
)}
{f.help && (
<span className="sans dim" style={{ display: 'block', marginTop: 6, fontSize: '0.76rem' }}>
{f.help}
</span>
)}
</label>
))}
<div style={{ display: 'flex', gap: 10, marginTop: 6, alignItems: 'center' }}>