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:
@@ -11,6 +11,27 @@ const PUBLIC_KEYS = [
|
||||
'hero_layout', // portal hero composition (JSON). Draft key stays admin-only.
|
||||
]
|
||||
|
||||
// Player self-registration mode. Stored under the 'player_registration' key.
|
||||
// NOTE: the raw value is never exposed publicly — getPublic() derives boolean
|
||||
// availability flags from it instead (see below).
|
||||
const REGISTRATION_KEY = 'player_registration'
|
||||
const REGISTRATION_MODES = ['disabled', 'password', 'sso', 'both']
|
||||
|
||||
// Resolve the registration mode, defaulting to 'disabled' (and coercing any
|
||||
// unexpected stored value back to 'disabled' so a bad row can't open sign-up).
|
||||
async function getRegistrationMode() {
|
||||
const value = await settingsDb.get(REGISTRATION_KEY)
|
||||
return REGISTRATION_MODES.includes(value) ? value : 'disabled'
|
||||
}
|
||||
|
||||
// Derived, public-safe availability flags for the register page.
|
||||
function registrationFlags(mode) {
|
||||
return {
|
||||
password: mode === 'password' || mode === 'both',
|
||||
sso: mode === 'sso' || mode === 'both',
|
||||
}
|
||||
}
|
||||
|
||||
async function get(key) {
|
||||
return settingsDb.get(key)
|
||||
}
|
||||
@@ -35,10 +56,26 @@ async function getAll() {
|
||||
|
||||
async function getPublic() {
|
||||
const all = await getAll()
|
||||
return PUBLIC_KEYS.reduce((acc, key) => {
|
||||
const out = PUBLIC_KEYS.reduce((acc, key) => {
|
||||
if (all[key] !== undefined) acc[key] = all[key]
|
||||
return acc
|
||||
}, {})
|
||||
// Derived registration availability (never the raw mode). Lets the register
|
||||
// page show/hide the password form and SSO buttons.
|
||||
const mode = REGISTRATION_MODES.includes(all[REGISTRATION_KEY]) ? all[REGISTRATION_KEY] : 'disabled'
|
||||
out.registration = registrationFlags(mode)
|
||||
return out
|
||||
}
|
||||
|
||||
module.exports = { get, set, setMany, getAll, getPublic, PUBLIC_KEYS }
|
||||
module.exports = {
|
||||
get,
|
||||
set,
|
||||
setMany,
|
||||
getAll,
|
||||
getPublic,
|
||||
PUBLIC_KEYS,
|
||||
REGISTRATION_KEY,
|
||||
REGISTRATION_MODES,
|
||||
getRegistrationMode,
|
||||
registrationFlags,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user