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:
@@ -8,6 +8,8 @@ export default function UserEditor({ user, onClose, onSaved }) {
|
||||
username: user?.username || '',
|
||||
password: '',
|
||||
role: user?.role || 'admin',
|
||||
status: user?.status || 'active',
|
||||
email: user?.email || '',
|
||||
})
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
@@ -21,12 +23,19 @@ export default function UserEditor({ user, onClose, onSaved }) {
|
||||
setBusy(true)
|
||||
setError('')
|
||||
try {
|
||||
const email = form.email.trim() || null
|
||||
if (isEdit) {
|
||||
const payload = { username: form.username.trim(), role: form.role }
|
||||
const payload = { username: form.username.trim(), role: form.role, status: form.status, email }
|
||||
if (form.password) payload.password = form.password
|
||||
await api.admin.updateUser(user.id, payload)
|
||||
} else {
|
||||
await api.admin.createUser({ username: form.username.trim(), password: form.password, role: form.role })
|
||||
await api.admin.createUser({
|
||||
username: form.username.trim(),
|
||||
password: form.password,
|
||||
role: form.role,
|
||||
status: form.status,
|
||||
email,
|
||||
})
|
||||
}
|
||||
onSaved()
|
||||
} catch (err) {
|
||||
@@ -75,17 +84,41 @@ export default function UserEditor({ user, onClose, onSaved }) {
|
||||
<input type="text" value={form.username} onChange={set('username')} className="input" autoComplete="off" />
|
||||
</label>
|
||||
<label>
|
||||
<span className="field-label">{isEdit ? 'New password (leave blank to keep)' : 'Password'}</span>
|
||||
<span className="field-label">
|
||||
{isEdit ? 'Reset password (leave blank to keep)' : 'Password'}
|
||||
</span>
|
||||
<input type="password" value={form.password} onChange={set('password')} className="input" autoComplete="new-password" />
|
||||
{isEdit && (
|
||||
<span className="sans dim" style={{ display: 'block', marginTop: 6, fontSize: '0.76rem' }}>
|
||||
Setting a new password here is the supported reset for a player who is locked out. It logs
|
||||
their other sessions out.
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<label>
|
||||
<span className="field-label">Role</span>
|
||||
<select value={form.role} onChange={set('role')} className="select">
|
||||
<option value="admin">admin</option>
|
||||
<option value="editor">editor</option>
|
||||
<option value="moderator">moderator</option>
|
||||
</select>
|
||||
<span className="field-label">Email (optional)</span>
|
||||
<input type="email" value={form.email} onChange={set('email')} className="input" autoComplete="off" placeholder="player@example.com" />
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<label style={{ flex: 1 }}>
|
||||
<span className="field-label">Role</span>
|
||||
<select value={form.role} onChange={set('role')} className="select">
|
||||
<option value="admin">admin</option>
|
||||
<option value="editor">editor</option>
|
||||
<option value="moderator">moderator</option>
|
||||
<option value="player">player</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style={{ flex: 1 }}>
|
||||
<span className="field-label">Status</span>
|
||||
<select value={form.status} onChange={set('status')} className="select">
|
||||
<option value="active">active</option>
|
||||
<option value="disabled">disabled</option>
|
||||
<option value="banned">banned</option>
|
||||
<option value="pending">pending</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user