Player accounts backend: schema, registration, self-service, SSO provision
- 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
This commit is contained in:
@@ -454,6 +454,13 @@ async function updateSettings(req, res) {
|
||||
if (!updates || typeof updates !== 'object' || Array.isArray(updates)) {
|
||||
return res.status(400).json({ message: 'Expected an object of key/value settings' })
|
||||
}
|
||||
// Enum-constrained keys are validated here (the store itself is schemaless).
|
||||
if (
|
||||
settings.REGISTRATION_KEY in updates &&
|
||||
!settings.REGISTRATION_MODES.includes(updates[settings.REGISTRATION_KEY])
|
||||
) {
|
||||
return res.status(400).json({ message: 'Invalid player_registration value' })
|
||||
}
|
||||
try {
|
||||
await settings.setMany(updates, req.user.id)
|
||||
await activity.log({ req, action: 'settings.update', detail: { keys: Object.keys(updates) } })
|
||||
@@ -493,8 +500,14 @@ async function createUser(req, res) {
|
||||
username: req.body.username,
|
||||
password: req.body.password,
|
||||
role: req.body.role || 'admin',
|
||||
email: req.body.email || null,
|
||||
status: req.body.status || 'active',
|
||||
})
|
||||
await activity.log({
|
||||
req,
|
||||
action: 'user.create',
|
||||
detail: { id: user.id, username: user.username, role: user.role },
|
||||
})
|
||||
await activity.log({ req, action: 'user.create', detail: { id: user.id, username: user.username } })
|
||||
return res.status(201).json(user)
|
||||
} catch (err) {
|
||||
log.error('createUser', err)
|
||||
@@ -525,8 +538,26 @@ async function updateUser(req, res) {
|
||||
username: req.body.username,
|
||||
password: req.body.password,
|
||||
role: req.body.role,
|
||||
email: req.body.email,
|
||||
status: req.body.status,
|
||||
})
|
||||
await activity.log({ req, action: 'user.update', detail: { id } })
|
||||
// Distinct audit trail for the security-sensitive fields (role & status),
|
||||
// so a promotion/ban is greppable beyond the generic user.update entry.
|
||||
if (req.body.role && req.body.role !== target.role) {
|
||||
await activity.log({
|
||||
req,
|
||||
action: 'admin.user.role_change',
|
||||
detail: { id, from: target.role, to: req.body.role },
|
||||
})
|
||||
}
|
||||
if (req.body.status && req.body.status !== target.status) {
|
||||
await activity.log({
|
||||
req,
|
||||
action: 'admin.user.status_change',
|
||||
detail: { id, from: target.status, to: req.body.status },
|
||||
})
|
||||
}
|
||||
return res.json(user)
|
||||
} catch (err) {
|
||||
log.error('updateUser', err)
|
||||
|
||||
Reference in New Issue
Block a user