refactor(api): collapse /admin/account and /player/account onto /auth/me/account
Self-service account security had three URL surfaces onto one controller. All
three mounted the same `admin/account.controller.js` handlers; each of the three
router files carried a header comment apologising for the arrangement.
`/auth/me/account` was already a strict superset, which settles which to keep:
/admin/account 6 routes noindex, isLoggedIn, staffOnly
/player/account 8 routes noindex, requireAuth
/auth/me/account 10 routes noindex, requireAuth
Neither of the deleted surfaces carried recovery codes, and /admin/account
carried no username or password change at all — so client.js already called
/auth/me/account/recovery-codes/* for two operations on a screen it otherwise
served from /admin/account. The split was leaking before this change.
Gating is equivalent where it overlapped: /player and /auth/me apply identical
`noindex, requireAuth`, and `staffOnly` on /admin/account was strictly narrower
while buying nothing, since every handler is self-scoped to req.user.id. There
is no CSRF layer to differ.
- 14 routes deleted, 0 added, no handler changed.
- account.controller.js moves router/v1/admin/ -> router/v1/auth/, beside the
one router that still reaches it.
- Web client: 14 call sites move onto a root-level api.myAccount /
api.changeUsername / ... group, matching the /auth/me methods already there.
- Android app: no change. MeApi.kt was already 100% /auth/me/account/*.
- Two swagger tags, `Admin · Account` and `Player`, were declared only by the
deleted routes and go with them. The orphaned `AccountStatus` schema goes
too; `PlayerAccount` is re-described as the any-role /auth/me/account shape
(the name is kept so existing $refs resolve).
Breaking to the published OpenAPI surface, accepted deliberately: both consumers
are in this org, and deprecate-then-delete would leave the next phase deciding
whether to add routes to surfaces already marked for removal.
Verification: routes.manifest.json shows exactly 14 deletions and 0 additions.
The OpenAPI spec loses the same 14 paths with zero surviving path definitions
changed; its large textual diff is pure reordering, because removing the
first-mounted router shifts every later path. 1203 server tests, 288 client
tests, 53 bot tests green; check:modules, check:hosts and routes:manifest
--check all pass.
Design of record: docs/website/ENGAGEMENT.md Phase 1a. This lands ahead of
engagement Phase 1b, which adds a self-service email field — written once here
rather than three times.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,24 @@ export const api = {
|
|||||||
revokeTrustedDevice: (id) =>
|
revokeTrustedDevice: (id) =>
|
||||||
req(`/auth/me/trusted-devices/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
req(`/auth/me/trusted-devices/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||||
revokeAllTrustedDevices: () => req('/auth/me/trusted-devices', { method: 'DELETE' }),
|
revokeAllTrustedDevices: () => req('/auth/me/trusted-devices', { method: 'DELETE' }),
|
||||||
|
// Self-service account security, role-agnostic under /auth/me/account. This is
|
||||||
|
// the ONLY surface for it: the /admin/account/* and /player/account/* copies
|
||||||
|
// were deleted (both were strictly smaller — neither carried recovery codes),
|
||||||
|
// which is why recovery codes below already lived here while the rest did not.
|
||||||
|
// The change endpoints re-issue the session cookie server-side, so the caller
|
||||||
|
// stays signed in.
|
||||||
|
myAccount: () => req('/auth/me/account'),
|
||||||
|
changeUsername: (username) =>
|
||||||
|
req('/auth/me/account/username', { method: 'PATCH', body: { username } }),
|
||||||
|
changePassword: (newPassword, currentPassword) =>
|
||||||
|
req('/auth/me/account/password', { method: 'PATCH', body: { newPassword, currentPassword } }),
|
||||||
|
totpSetup: () => req('/auth/me/account/totp/setup', { method: 'POST' }),
|
||||||
|
totpEnable: (code) => req('/auth/me/account/totp/enable', { method: 'POST', body: { code } }),
|
||||||
|
totpDisable: (code) => req('/auth/me/account/totp/disable', { method: 'POST', body: { code } }),
|
||||||
|
// Linked SSO identities (self-service). Linking starts at /auth/sso/:id/link.
|
||||||
|
myIdentities: () => req('/auth/me/account/identities'),
|
||||||
|
unlinkIdentity: (provider) =>
|
||||||
|
req(`/auth/me/account/identities/${encodeURIComponent(provider)}`, { method: 'DELETE' }),
|
||||||
// Recovery (backup) codes. status → remaining count; generate → a fresh set,
|
// Recovery (backup) codes. status → remaining count; generate → a fresh set,
|
||||||
// returned ONCE (password step-up for accounts that have a password).
|
// returned ONCE (password step-up for accounts that have a password).
|
||||||
recoveryCodesStatus: () => req('/auth/me/account/recovery-codes/status'),
|
recoveryCodesStatus: () => req('/auth/me/account/recovery-codes/status'),
|
||||||
@@ -435,16 +453,6 @@ export const api = {
|
|||||||
req(`/admin/moderation/appeals/${id}/resolve`, { method: 'POST', body: data }),
|
req(`/admin/moderation/appeals/${id}/resolve`, { method: 'POST', body: data }),
|
||||||
getUserAppeals: (discordId) => req(`/admin/moderation/user/${discordId}/appeals`),
|
getUserAppeals: (discordId) => req(`/admin/moderation/user/${discordId}/appeals`),
|
||||||
|
|
||||||
// ----- account security (self-service 2FA) -----
|
|
||||||
getAccount: () => req('/admin/account'),
|
|
||||||
totpSetup: () => req('/admin/account/totp/setup', { method: 'POST' }),
|
|
||||||
totpEnable: (code) => req('/admin/account/totp/enable', { method: 'POST', body: { code } }),
|
|
||||||
totpDisable: (code) => req('/admin/account/totp/disable', { method: 'POST', body: { code } }),
|
|
||||||
|
|
||||||
// ----- linked SSO identities (self-service) -----
|
|
||||||
linkedIdentities: () => req('/admin/account/identities'),
|
|
||||||
unlinkIdentity: (provider) => req(`/admin/account/identities/${provider}`, { method: 'DELETE' }),
|
|
||||||
|
|
||||||
// ----- auth providers / SSO config (admin only) -----
|
// ----- auth providers / SSO config (admin only) -----
|
||||||
listAuthProviders: () => req('/admin/auth/providers'),
|
listAuthProviders: () => req('/admin/auth/providers'),
|
||||||
createAuthProvider: (data) => req('/admin/auth/providers', { method: 'POST', body: data }),
|
createAuthProvider: (data) => req('/admin/auth/providers', { method: 'POST', body: data }),
|
||||||
@@ -465,20 +473,9 @@ export const api = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ----- player self-service (role: 'player') -----
|
// ----- player self-service (role: 'player') -----
|
||||||
// Mirrors the admin account methods but self-scoped under /player. The change
|
// Account security is NOT here — it is role-agnostic and lives at the root of
|
||||||
// endpoints re-issue the session cookie server-side, so the caller stays signed in.
|
// this object, on /auth/me/account. What remains is genuinely player-scoped.
|
||||||
player: {
|
player: {
|
||||||
getAccount: () => req('/player/account'),
|
|
||||||
changeUsername: (username) =>
|
|
||||||
req('/player/account/username', { method: 'PATCH', body: { username } }),
|
|
||||||
changePassword: (newPassword, currentPassword) =>
|
|
||||||
req('/player/account/password', { method: 'PATCH', body: { newPassword, currentPassword } }),
|
|
||||||
totpSetup: () => req('/player/account/totp/setup', { method: 'POST' }),
|
|
||||||
totpEnable: (code) => req('/player/account/totp/enable', { method: 'POST', body: { code } }),
|
|
||||||
totpDisable: (code) => req('/player/account/totp/disable', { method: 'POST', body: { code } }),
|
|
||||||
linkedIdentities: () => req('/player/account/identities'),
|
|
||||||
unlinkIdentity: (provider) => req(`/player/account/identities/${provider}`, { method: 'DELETE' }),
|
|
||||||
|
|
||||||
// ----- moderation appeals (self-service) -----
|
// ----- moderation appeals (self-service) -----
|
||||||
getMyAppeals: () => req('/player/appeals'),
|
getMyAppeals: () => req('/player/appeals'),
|
||||||
getEligibleAppeals: () => req('/player/appeals/eligible'),
|
getEligibleAppeals: () => req('/player/appeals/eligible'),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function LinkedAccounts() {
|
|||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const [ids, avail] = await Promise.all([
|
const [ids, avail] = await Promise.all([
|
||||||
api.admin.linkedIdentities(),
|
api.myIdentities(),
|
||||||
api.authProviders().catch(() => []),
|
api.authProviders().catch(() => []),
|
||||||
])
|
])
|
||||||
setLinked(ids)
|
setLinked(ids)
|
||||||
@@ -44,7 +44,7 @@ function LinkedAccounts() {
|
|||||||
async function unlink(provider) {
|
async function unlink(provider) {
|
||||||
if (!window.confirm(`Unlink ${nameFor(provider)} from your account?`)) return
|
if (!window.confirm(`Unlink ${nameFor(provider)} from your account?`)) return
|
||||||
try {
|
try {
|
||||||
await api.admin.unlinkIdentity(provider)
|
await api.unlinkIdentity(provider)
|
||||||
await load()
|
await load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || 'Could not unlink.')
|
setError(err.message || 'Could not unlink.')
|
||||||
@@ -134,7 +134,7 @@ export default function AccountAdmin() {
|
|||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
setAccount(await api.admin.getAccount())
|
setAccount(await api.myAccount())
|
||||||
} catch {
|
} catch {
|
||||||
setError('Could not load your account.')
|
setError('Could not load your account.')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -154,7 +154,7 @@ export default function AccountAdmin() {
|
|||||||
setMsg('')
|
setMsg('')
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
setSetup(await api.admin.totpSetup())
|
setSetup(await api.totpSetup())
|
||||||
setCode('')
|
setCode('')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || 'Could not start setup.')
|
setError(err.message || 'Could not start setup.')
|
||||||
@@ -168,7 +168,7 @@ export default function AccountAdmin() {
|
|||||||
setMsg('')
|
setMsg('')
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
const res = await api.admin.totpEnable(code.trim())
|
const res = await api.totpEnable(code.trim())
|
||||||
setSetup(null)
|
setSetup(null)
|
||||||
setCode('')
|
setCode('')
|
||||||
setNewCodes(res?.recoveryCodes || null)
|
setNewCodes(res?.recoveryCodes || null)
|
||||||
@@ -186,7 +186,7 @@ export default function AccountAdmin() {
|
|||||||
setMsg('')
|
setMsg('')
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
await api.admin.totpDisable(code.trim())
|
await api.totpDisable(code.trim())
|
||||||
setCode('')
|
setCode('')
|
||||||
setMsg('Two-factor authentication has been disabled.')
|
setMsg('Two-factor authentication has been disabled.')
|
||||||
await load()
|
await load()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function ChangeUsername({ account, onChanged }) {
|
|||||||
if (username.trim().length < 3) return setError('Username must be at least 3 characters.')
|
if (username.trim().length < 3) return setError('Username must be at least 3 characters.')
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
try {
|
try {
|
||||||
const { username: next } = await api.player.changeUsername(username.trim())
|
const { username: next } = await api.changeUsername(username.trim())
|
||||||
setMsg('Username updated.')
|
setMsg('Username updated.')
|
||||||
await onChanged(next)
|
await onChanged(next)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -67,7 +67,7 @@ function ChangePassword({ account }) {
|
|||||||
if (hasPassword && !current) return setError('Enter your current password.')
|
if (hasPassword && !current) return setError('Enter your current password.')
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
try {
|
try {
|
||||||
await api.player.changePassword(next, hasPassword ? current : undefined)
|
await api.changePassword(next, hasPassword ? current : undefined)
|
||||||
setMsg(hasPassword ? 'Password changed.' : 'Password set. You can now sign in with it.')
|
setMsg(hasPassword ? 'Password changed.' : 'Password set. You can now sign in with it.')
|
||||||
setCurrent('')
|
setCurrent('')
|
||||||
setNext('')
|
setNext('')
|
||||||
@@ -124,7 +124,7 @@ function TwoFactor({ account, reload }) {
|
|||||||
async function begin() {
|
async function begin() {
|
||||||
setBusy(true); setMsg(''); setError('')
|
setBusy(true); setMsg(''); setError('')
|
||||||
try {
|
try {
|
||||||
setSetup(await api.player.totpSetup())
|
setSetup(await api.totpSetup())
|
||||||
setCode('')
|
setCode('')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || 'Could not start setup.')
|
setError(err.message || 'Could not start setup.')
|
||||||
@@ -135,7 +135,7 @@ function TwoFactor({ account, reload }) {
|
|||||||
async function confirm() {
|
async function confirm() {
|
||||||
setBusy(true); setMsg(''); setError('')
|
setBusy(true); setMsg(''); setError('')
|
||||||
try {
|
try {
|
||||||
const res = await api.player.totpEnable(code.trim())
|
const res = await api.totpEnable(code.trim())
|
||||||
setSetup(null); setCode(''); setNewCodes(res?.recoveryCodes || null); setMsg('Two-factor is now enabled.')
|
setSetup(null); setCode(''); setNewCodes(res?.recoveryCodes || null); setMsg('Two-factor is now enabled.')
|
||||||
await reload()
|
await reload()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -147,7 +147,7 @@ function TwoFactor({ account, reload }) {
|
|||||||
async function disable() {
|
async function disable() {
|
||||||
setBusy(true); setMsg(''); setError('')
|
setBusy(true); setMsg(''); setError('')
|
||||||
try {
|
try {
|
||||||
await api.player.totpDisable(code.trim())
|
await api.totpDisable(code.trim())
|
||||||
setCode(''); setMsg('Two-factor has been disabled.')
|
setCode(''); setMsg('Two-factor has been disabled.')
|
||||||
await reload()
|
await reload()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -234,7 +234,7 @@ function LinkedAccounts() {
|
|||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const [ids, avail] = await Promise.all([
|
const [ids, avail] = await Promise.all([
|
||||||
api.player.linkedIdentities(),
|
api.myIdentities(),
|
||||||
api.authProviders().catch(() => []),
|
api.authProviders().catch(() => []),
|
||||||
])
|
])
|
||||||
setLinked(ids)
|
setLinked(ids)
|
||||||
@@ -251,7 +251,7 @@ function LinkedAccounts() {
|
|||||||
async function unlink(provider) {
|
async function unlink(provider) {
|
||||||
if (!window.confirm(`Unlink ${nameFor(provider)} from your account?`)) return
|
if (!window.confirm(`Unlink ${nameFor(provider)} from your account?`)) return
|
||||||
try {
|
try {
|
||||||
await api.player.unlinkIdentity(provider)
|
await api.unlinkIdentity(provider)
|
||||||
await load()
|
await load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || 'Could not unlink.')
|
setError(err.message || 'Could not unlink.')
|
||||||
@@ -397,7 +397,7 @@ export default function PlayerAccount() {
|
|||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setAccount(await api.player.getAccount())
|
setAccount(await api.myAccount())
|
||||||
} catch {
|
} catch {
|
||||||
setError('Could not load your account.')
|
setError('Could not load your account.')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -27,66 +27,6 @@
|
|||||||
"handlers": 1,
|
"handlers": 1,
|
||||||
"gates": []
|
"gates": []
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/account",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/account/identities",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/v1/admin/account/identities/:provider",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/account/totp/disable",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/account/totp/enable",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/account/totp/setup",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/api/v1/admin/activity",
|
"path": "/api/v1/admin/activity",
|
||||||
@@ -1640,88 +1580,6 @@
|
|||||||
"validate"
|
"validate"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/account",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/account/identities",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/v1/player/account/identities/:provider",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/v1/player/account/password",
|
|
||||||
"handlers": 5,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/player/account/totp/disable",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/player/account/totp/enable",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/player/account/totp/setup",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/v1/player/account/username",
|
|
||||||
"handlers": 4,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/api/v1/player/appeals",
|
"path": "/api/v1/player/appeals",
|
||||||
|
|||||||
@@ -17,30 +17,6 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/api/health"
|
"path": "/api/health"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/account/identities"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/v1/admin/account/identities/:provider"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/account/totp/disable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/account/totp/enable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/account/totp/setup"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/api/v1/admin/activity"
|
"path": "/api/v1/admin/activity"
|
||||||
@@ -657,38 +633,6 @@
|
|||||||
"method": "POST",
|
"method": "POST",
|
||||||
"path": "/api/v1/auth/sso/totp"
|
"path": "/api/v1/auth/sso/totp"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/account/identities"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/v1/player/account/identities/:provider"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/v1/player/account/password"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/player/account/totp/disable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/player/account/totp/enable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/player/account/totp/setup"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/v1/player/account/username"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/api/v1/player/appeals"
|
"path": "/api/v1/player/appeals"
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ function buildCtx(id, moduleRoot) {
|
|||||||
// one store, and one place a breach is logged.
|
// one store, and one place a breach is logged.
|
||||||
//
|
//
|
||||||
// `accountChangeLimiter` is handed over whole because it is genuinely
|
// `accountChangeLimiter` is handed over whole because it is genuinely
|
||||||
// shared policy: core's `/auth/me`, `/player/account` and
|
// shared policy: core's `/auth/me/account/*` and `/player/appeals` are
|
||||||
// `/player/appeals` are behind the same counter, and a module's
|
// behind the same counter, and a module's account-change route has to land
|
||||||
// account-change route has to land in it rather than beside it.
|
// in it rather than beside it.
|
||||||
rateLimit: makeLimiter,
|
rateLimit: makeLimiter,
|
||||||
accountChangeLimiter,
|
accountChangeLimiter,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
// Admin · Account — self-service account security for staff.
|
|
||||||
//
|
|
||||||
// Mounted at /api/v1/admin/account by admin/index.js, which already applied
|
|
||||||
// `noindex, isLoggedIn, staffOnly`. Deliberately NOT behind adminOnly: an editor
|
|
||||||
// or moderator manages their own 2FA and linked identities here, exactly as a
|
|
||||||
// player does under /player. Every handler keys off req.user.id.
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const { body, param } = require('express-validator')
|
|
||||||
|
|
||||||
const account = require('./account.controller')
|
|
||||||
const validate = require('../../../middleware/validate')
|
|
||||||
|
|
||||||
const accountRouter = express.Router()
|
|
||||||
|
|
||||||
accountRouter.get(
|
|
||||||
'/',
|
|
||||||
// #swagger.tags = ['Admin · Account']
|
|
||||||
// #swagger.summary = 'Get the current account (self)'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.responses[200] = { description: 'The account', content: { "application/json": { schema: { $ref: "#/components/schemas/AccountStatus" } } } } */
|
|
||||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
account.getAccount,
|
|
||||||
)
|
|
||||||
accountRouter.post(
|
|
||||||
'/totp/setup',
|
|
||||||
// #swagger.tags = ['Admin · Account']
|
|
||||||
// #swagger.summary = 'Begin 2FA enrollment (returns secret + QR)'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.responses[200] = { description: 'otpauth URL and QR data to scan', content: { "application/json": { schema: { $ref: "#/components/schemas/TotpSetup" } } } } */
|
|
||||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
/* #swagger.responses[409] = { description: 'Two-factor already enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
account.totpSetup,
|
|
||||||
)
|
|
||||||
accountRouter.post(
|
|
||||||
'/totp/enable',
|
|
||||||
// #swagger.tags = ['Admin · Account']
|
|
||||||
// #swagger.summary = 'Enable 2FA by confirming a code'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TotpCodeRequest" } } } } */
|
|
||||||
/* #swagger.responses[200] = { description: '2FA enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/TotpState" } } } } */
|
|
||||||
/* #swagger.responses[400] = { description: 'Setup not started, or invalid code', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
/* #swagger.responses[409] = { description: 'Two-factor already enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
body('code').isString().trim().isLength({ min: 6, max: 8 }),
|
|
||||||
validate,
|
|
||||||
account.totpEnable,
|
|
||||||
)
|
|
||||||
accountRouter.post(
|
|
||||||
'/totp/disable',
|
|
||||||
// #swagger.tags = ['Admin · Account']
|
|
||||||
// #swagger.summary = 'Disable 2FA by confirming a code'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TotpCodeRequest" } } } } */
|
|
||||||
/* #swagger.responses[200] = { description: '2FA disabled', content: { "application/json": { schema: { $ref: "#/components/schemas/TotpState" } } } } */
|
|
||||||
/* #swagger.responses[400] = { description: 'Not enabled, or invalid code', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
body('code').isString().trim().isLength({ min: 6, max: 8 }),
|
|
||||||
validate,
|
|
||||||
account.totpDisable,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Linked SSO identities (self-service — any logged-in role manages their own).
|
|
||||||
accountRouter.get(
|
|
||||||
'/identities',
|
|
||||||
// #swagger.tags = ['Admin · Account']
|
|
||||||
// #swagger.summary = 'List linked SSO identities (self)'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.responses[200] = { description: 'Linked identities', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/LinkedIdentity" } } } } } */
|
|
||||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
account.listIdentities,
|
|
||||||
)
|
|
||||||
accountRouter.delete(
|
|
||||||
'/identities/:provider',
|
|
||||||
// #swagger.tags = ['Admin · Account']
|
|
||||||
// #swagger.summary = 'Unlink an SSO identity (self)'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
// #swagger.parameters['provider'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
|
|
||||||
/* #swagger.responses[200] = { description: 'Unlinked', content: { "application/json": { schema: { $ref: "#/components/schemas/UnlinkedFlag" } } } } */
|
|
||||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
/* #swagger.responses[404] = { description: 'No linked account for that provider', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
param('provider').matches(/^[a-z0-9-]+$/),
|
|
||||||
validate,
|
|
||||||
account.unlinkIdentity,
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = accountRouter
|
|
||||||
@@ -16,7 +16,6 @@ const express = require('express')
|
|||||||
const { isLoggedIn, requireRole } = require('../../../utils/auth')
|
const { isLoggedIn, requireRole } = require('../../../utils/auth')
|
||||||
const noindex = require('../../../middleware/noindex')
|
const noindex = require('../../../middleware/noindex')
|
||||||
|
|
||||||
const accountRouter = require('./account.router')
|
|
||||||
const usersRouter = require('./users.router')
|
const usersRouter = require('./users.router')
|
||||||
const invitesRouter = require('./invites.router')
|
const invitesRouter = require('./invites.router')
|
||||||
const authProvidersRouter = require('./authProviders.router')
|
const authProvidersRouter = require('./authProviders.router')
|
||||||
@@ -48,7 +47,6 @@ const adminRouter = express.Router()
|
|||||||
const staffOnly = requireRole('admin', 'editor', 'moderator')
|
const staffOnly = requireRole('admin', 'editor', 'moderator')
|
||||||
adminRouter.use(noindex, isLoggedIn, staffOnly)
|
adminRouter.use(noindex, isLoggedIn, staffOnly)
|
||||||
|
|
||||||
adminRouter.use('/account', accountRouter)
|
|
||||||
adminRouter.use('/users', usersRouter)
|
adminRouter.use('/users', usersRouter)
|
||||||
adminRouter.use('/invites', invitesRouter)
|
adminRouter.use('/invites', invitesRouter)
|
||||||
// Mounted at /auth, not /auth/providers: /admin/auth is the capability, and the
|
// Mounted at /auth, not /auth/providers: /admin/auth is the capability, and the
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
// Self-service account security for the logged-in user (any role). Mounted under
|
// Self-service account security for the logged-in user (any role): username,
|
||||||
// the admin router (so isLoggedIn has already run and req.user is the fresh DB
|
// password, TOTP, linked identities, device sessions, trusted devices and
|
||||||
// row), but NOT behind the admin-only gate — editors manage their own 2FA too.
|
// recovery codes.
|
||||||
|
//
|
||||||
|
// Reached through exactly one router — me.routes.js at /auth/me — which applies
|
||||||
|
// `noindex, requireAuth`, so req.user is the fresh DB row and the status +
|
||||||
|
// session-cutoff checks have already run. Every handler keys off req.user.id and
|
||||||
|
// none of them consults a role: this file lived under router/v1/admin/ while it
|
||||||
|
// also served /admin/account/* and /player/account/*, and moved here when those
|
||||||
|
// two surfaces were deleted.
|
||||||
|
|
||||||
const users = require('../../../model/users/users.model')
|
const users = require('../../../model/users/users.model')
|
||||||
const activity = require('../../../model/activity/activity.model')
|
const activity = require('../../../model/activity/activity.model')
|
||||||
@@ -9,7 +16,7 @@ const mobileSessions = require('../../../model/mobileSessions/mobileSessions.mod
|
|||||||
const trustedDevices = require('../../../model/trustedDevices/trustedDevices.model')
|
const trustedDevices = require('../../../model/trustedDevices/trustedDevices.model')
|
||||||
const recoveryCodes = require('../../../model/recoveryCodes/recoveryCodes.model')
|
const recoveryCodes = require('../../../model/recoveryCodes/recoveryCodes.model')
|
||||||
const sessionService = require('../../../auth/session.service')
|
const sessionService = require('../../../auth/session.service')
|
||||||
const { establishTrust } = require('../auth/trustDevice.helper')
|
const { establishTrust } = require('./trustDevice.helper')
|
||||||
const { setAuthCookie, setTrustCookie, clearTrustCookie } = require('../../../auth/token')
|
const { setAuthCookie, setTrustCookie, clearTrustCookie } = require('../../../auth/token')
|
||||||
const usernamePolicy = require('../../../auth/usernamePolicy')
|
const usernamePolicy = require('../../../auth/usernamePolicy')
|
||||||
const loginProtection = require('../../../middleware/loginProtection')
|
const loginProtection = require('../../../middleware/loginProtection')
|
||||||
@@ -38,10 +38,10 @@ authRouter.use('/mobile', mobileRouter)
|
|||||||
// middleware, so passing through it is a no-op for every other route.
|
// middleware, so passing through it is a no-op for every other route.
|
||||||
authRouter.use(ssoRouter)
|
authRouter.use(ssoRouter)
|
||||||
|
|
||||||
// Role-agnostic self-service ("me") — /auth/me/account*, reusing the same
|
// Role-agnostic self-service ("me") — /auth/me/account*, behind requireAuth (any
|
||||||
// account.controller handlers as /player/account/* and /admin/account/* behind
|
// role). The single self surface: /player/account/* and /admin/account/* were
|
||||||
// requireAuth (any role). Additive; gives the app one self surface that never
|
// deleted in favour of it, so the app and the web client share one set of URLs
|
||||||
// touches /admin.
|
// and neither has to touch /admin.
|
||||||
authRouter.use('/me', meRouter)
|
authRouter.use('/me', meRouter)
|
||||||
|
|
||||||
// Push-notification self-service — /auth/me/devices*, /auth/me/notifications/*.
|
// Push-notification self-service — /auth/me/devices*, /auth/me/notifications/*.
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
// ── Role-agnostic self-service ("me") under /auth/me ───────────────────────
|
// ── Role-agnostic self-service ("me") under /auth/me ───────────────────────
|
||||||
//
|
//
|
||||||
// The canonical self surface for EVERY authenticated role (player and staff
|
// The ONLY self surface, for every authenticated role (player and staff alike).
|
||||||
// alike). It reuses the exact same account.controller handlers as
|
// It gates on requireAuth ONLY (any authenticated, active account), never on a
|
||||||
// /player/account/* and /admin/account/* — no logic duplication — but gates on
|
// specific role.
|
||||||
// requireAuth ONLY (any authenticated, active account), never on a specific role.
|
|
||||||
//
|
//
|
||||||
// Why it exists: the Android app wants one self surface it can call regardless of
|
// Why it exists: the Android app wants one self surface it can call regardless of
|
||||||
// role, and it must never touch /admin (docs/android/PLAN.md §6.4). The older
|
// role, and it must never touch /admin (docs/android/PLAN.md §6.4).
|
||||||
// /player/account/* and /admin/account/* routes stay for web back-compat; these
|
//
|
||||||
// /auth/me/* routes are the additive, role-agnostic canonical form.
|
// It used to be the third of three URL surfaces onto account.controller, beside
|
||||||
|
// /player/account/* and /admin/account/*. Those were deleted: both were strictly
|
||||||
|
// smaller than this one (neither carried recovery codes, and /admin/account
|
||||||
|
// carried no username or password change), so the web client already had to reach
|
||||||
|
// in here for part of one screen. New self-service fields go here and only here.
|
||||||
//
|
//
|
||||||
// requireAuth sets req.user to the fresh DB row and enforces the status + session
|
// requireAuth sets req.user to the fresh DB row and enforces the status + session
|
||||||
// cutoff/revocation checks on every request, exactly as the account handlers
|
// cutoff/revocation checks on every request, exactly as the account handlers
|
||||||
@@ -17,7 +20,7 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const { body, param } = require('express-validator')
|
const { body, param } = require('express-validator')
|
||||||
|
|
||||||
const account = require('../admin/account.controller')
|
const account = require('./account.controller')
|
||||||
const { requireAuth } = require('../../../auth/session.middleware')
|
const { requireAuth } = require('../../../auth/session.middleware')
|
||||||
const noindex = require('../../../middleware/noindex')
|
const noindex = require('../../../middleware/noindex')
|
||||||
const validate = require('../../../middleware/validate')
|
const validate = require('../../../middleware/validate')
|
||||||
@@ -76,8 +79,8 @@ meRouter.patch(
|
|||||||
account.changePassword,
|
account.changePassword,
|
||||||
)
|
)
|
||||||
|
|
||||||
// TOTP self-enrollment — identical to the player/admin account flow (disable
|
// TOTP self-enrollment (disable requires a valid current code; it does not take
|
||||||
// requires a valid current code; it does not take a password).
|
// a password).
|
||||||
meRouter.post(
|
meRouter.post(
|
||||||
'/account/totp/setup',
|
'/account/totp/setup',
|
||||||
// #swagger.tags = ['Auth · Me']
|
// #swagger.tags = ['Auth · Me']
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
// limiters below are what stop the endpoints being used as an oracle by volume.
|
// limiters below are what stop the endpoints being used as an oracle by volume.
|
||||||
//
|
//
|
||||||
// Changing a password while signed in is a different route —
|
// Changing a password while signed in is a different route —
|
||||||
// PATCH /player/account/password (and its /auth/me and /admin twins).
|
// PATCH /auth/me/account/password.
|
||||||
|
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const { body, param } = require('express-validator')
|
const { body, param } = require('express-validator')
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
// Player · Account — self-service credentials, 2FA and linked identities for the
|
|
||||||
// signed-in account.
|
|
||||||
//
|
|
||||||
// Mounted at /api/v1/player/account by player/index.js, which already applied
|
|
||||||
// `noindex, requireAuth`. No extra gate: every handler is self-scoped to
|
|
||||||
// req.user.id, and staff are a superset of players (see player/index.js).
|
|
||||||
//
|
|
||||||
// The handlers are admin/account.controller — the same code serving
|
|
||||||
// /admin/account/* and /auth/me/account/*. Three URL surfaces, one implementation;
|
|
||||||
// this file must not grow a fourth copy of the logic.
|
|
||||||
//
|
|
||||||
// The swagger tag stays 'Player', matching the committed spec.
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const { body, param } = require('express-validator')
|
|
||||||
|
|
||||||
const account = require('../admin/account.controller')
|
|
||||||
const validate = require('../../../middleware/validate')
|
|
||||||
const { accountChangeLimiter } = require('../../../middleware/rateLimit')
|
|
||||||
|
|
||||||
const accountRouter = express.Router()
|
|
||||||
|
|
||||||
accountRouter.get(
|
|
||||||
'/',
|
|
||||||
// #swagger.tags = ['Player']
|
|
||||||
// #swagger.summary = 'Get the current player account (self)'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.responses[200] = { description: 'The player account', content: { "application/json": { schema: { $ref: "#/components/schemas/PlayerAccount" } } } } */
|
|
||||||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
/* #swagger.responses[403] = { description: 'Account not active (disabled/banned)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
account.getAccount,
|
|
||||||
)
|
|
||||||
|
|
||||||
accountRouter.patch(
|
|
||||||
'/username',
|
|
||||||
// #swagger.tags = ['Player']
|
|
||||||
// #swagger.summary = 'Change the current player’s username'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ChangeUsernameRequest" } } } } */
|
|
||||||
/* #swagger.responses[200] = { description: 'Updated username (session cookie re-issued)', content: { "application/json": { schema: { type: "object", properties: { username: { type: "string" } } } } } } */
|
|
||||||
/* #swagger.responses[400] = { description: 'Validation error or unavailable username', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
|
|
||||||
/* #swagger.responses[403] = { description: 'Account not active (disabled/banned)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
/* #swagger.responses[409] = { description: 'Username already taken', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
/* #swagger.responses[429] = { description: 'Too many changes (rate limited)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
accountChangeLimiter,
|
|
||||||
body('username').isString().trim().isLength({ min: 3, max: 32 }),
|
|
||||||
validate,
|
|
||||||
account.changeUsername,
|
|
||||||
)
|
|
||||||
|
|
||||||
accountRouter.patch(
|
|
||||||
'/password',
|
|
||||||
// #swagger.tags = ['Player']
|
|
||||||
// #swagger.summary = 'Change or set the current player’s password'
|
|
||||||
// #swagger.description = 'If the account already has a password, currentPassword is required and verified. SSO-provisioned accounts with no password may set an initial one without a current password. On success the caller’s session is re-issued (they stay logged in) while all other sessions are revoked.'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ChangePasswordRequest" } } } } */
|
|
||||||
/* #swagger.responses[200] = { description: 'Password changed', content: { "application/json": { schema: { $ref: "#/components/schemas/OkFlag" } } } } */
|
|
||||||
/* #swagger.responses[400] = { description: 'Validation error or wrong current password', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
/* #swagger.responses[403] = { description: 'Account not active (disabled/banned)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
/* #swagger.responses[429] = { description: 'Too many changes (rate limited)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
accountChangeLimiter,
|
|
||||||
body('newPassword').isString().isLength({ min: 8, max: 64 }),
|
|
||||||
body('currentPassword').optional({ values: 'falsy' }).isString(),
|
|
||||||
validate,
|
|
||||||
account.changePassword,
|
|
||||||
)
|
|
||||||
|
|
||||||
// TOTP self-enrollment — identical to the admin account flow (disable requires a
|
|
||||||
// valid current code; it does not take a password).
|
|
||||||
accountRouter.post(
|
|
||||||
'/totp/setup',
|
|
||||||
// #swagger.tags = ['Player']
|
|
||||||
// #swagger.summary = 'Begin 2FA enrollment (returns secret + QR)'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.responses[200] = { description: 'otpauth URL and QR data to scan', content: { "application/json": { schema: { $ref: "#/components/schemas/TotpSetup" } } } } */
|
|
||||||
/* #swagger.responses[409] = { description: 'Two-factor already enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
account.totpSetup,
|
|
||||||
)
|
|
||||||
accountRouter.post(
|
|
||||||
'/totp/enable',
|
|
||||||
// #swagger.tags = ['Player']
|
|
||||||
// #swagger.summary = 'Enable 2FA by confirming a code'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TotpCodeRequest" } } } } */
|
|
||||||
/* #swagger.responses[200] = { description: '2FA enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/TotpState" } } } } */
|
|
||||||
/* #swagger.responses[400] = { description: 'Setup not started, or invalid code', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
/* #swagger.responses[409] = { description: 'Two-factor already enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
body('code').isString().trim().isLength({ min: 6, max: 8 }),
|
|
||||||
validate,
|
|
||||||
account.totpEnable,
|
|
||||||
)
|
|
||||||
accountRouter.post(
|
|
||||||
'/totp/disable',
|
|
||||||
// #swagger.tags = ['Player']
|
|
||||||
// #swagger.summary = 'Disable 2FA by confirming a code'
|
|
||||||
// #swagger.description = 'Requires a valid current authenticator code (proves control of the authenticator); it does not take a password.'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TotpCodeRequest" } } } } */
|
|
||||||
/* #swagger.responses[200] = { description: '2FA disabled', content: { "application/json": { schema: { $ref: "#/components/schemas/TotpState" } } } } */
|
|
||||||
/* #swagger.responses[400] = { description: 'Not enabled, or invalid code', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
body('code').isString().trim().isLength({ min: 6, max: 8 }),
|
|
||||||
validate,
|
|
||||||
account.totpDisable,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Linked SSO identities (self-service). Linking itself starts at
|
|
||||||
// GET /auth/sso/:provider/link (already behind requireAuth; works for players).
|
|
||||||
accountRouter.get(
|
|
||||||
'/identities',
|
|
||||||
// #swagger.tags = ['Player']
|
|
||||||
// #swagger.summary = 'List linked SSO identities (self)'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
/* #swagger.responses[200] = { description: 'Linked identities', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/LinkedIdentity" } } } } } */
|
|
||||||
account.listIdentities,
|
|
||||||
)
|
|
||||||
accountRouter.delete(
|
|
||||||
'/identities/:provider',
|
|
||||||
// #swagger.tags = ['Player']
|
|
||||||
// #swagger.summary = 'Unlink an SSO identity (self)'
|
|
||||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
||||||
// #swagger.parameters['provider'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
|
|
||||||
/* #swagger.responses[200] = { description: 'Unlinked', content: { "application/json": { schema: { $ref: "#/components/schemas/UnlinkedFlag" } } } } */
|
|
||||||
/* #swagger.responses[404] = { description: 'No linked account for that provider', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
||||||
param('provider').matches(/^[a-z0-9-]+$/),
|
|
||||||
validate,
|
|
||||||
account.unlinkIdentity,
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = accountRouter
|
|
||||||
@@ -3,19 +3,19 @@
|
|||||||
//
|
//
|
||||||
// This file owns exactly two things: the gate every player route shares, and the
|
// This file owns exactly two things: the gate every player route shares, and the
|
||||||
// mount table. No route is declared here. Each capability router mounts at the
|
// mount table. No route is declared here. Each capability router mounts at the
|
||||||
// prefix it already owned inside the old monolithic player.routes.js, so the
|
// prefix it already owned inside the old monolithic player.routes.js.
|
||||||
// emitted URL set is byte-identical — proved by a zero-line diff in
|
//
|
||||||
// server/routes.manifest.json (`npm run routes:manifest`).
|
// Self-service account security (`/player/account/*`) used to be mounted here. It
|
||||||
|
// is gone: `/auth/me/account/*` is the single canonical self surface for every
|
||||||
|
// role, and this group's copy was a strictly smaller duplicate of it.
|
||||||
//
|
//
|
||||||
// **Staff are a superset of players.** This group is open to any authenticated
|
// **Staff are a superset of players.** This group is open to any authenticated
|
||||||
// account, not just role 'player': every read/write is self-scoped to req.user.id,
|
// account, not just role 'player': every read/write is self-scoped to req.user.id,
|
||||||
// and a staff member has every player ability plus their staff tools on top.
|
// and a staff member has every player ability plus their staff tools on top.
|
||||||
// Adding a requireRole('player') here would 403 an admin off their own characters
|
// Adding a requireRole('player') here would 403 an admin off their own characters
|
||||||
// (it happened once — see docs/website/BACKEND_DESIGN.md). Staff also reach the
|
// (it happened once — see docs/website/BACKEND_DESIGN.md). Staff also reach the
|
||||||
// identical self-scoped handlers under /admin/shard and /auth/me/account; those
|
// identical self-scoped handlers under /admin/shard, which lives in module-uo now
|
||||||
// are alternative URLs onto the same controllers, not duplicated logic — and
|
// — that changes where they are defined and nothing about which URLs answer.
|
||||||
// both of those live in module-uo now, which changes where they are defined and
|
|
||||||
// nothing about which URLs answer.
|
|
||||||
//
|
//
|
||||||
// See docs/website/API_V2_PLAN.md § Phase 2 for the split.
|
// See docs/website/API_V2_PLAN.md § Phase 2 for the split.
|
||||||
|
|
||||||
@@ -24,7 +24,6 @@ const express = require('express')
|
|||||||
const { requireAuth } = require('../../../auth/session.middleware')
|
const { requireAuth } = require('../../../auth/session.middleware')
|
||||||
const noindex = require('../../../middleware/noindex')
|
const noindex = require('../../../middleware/noindex')
|
||||||
|
|
||||||
const accountRouter = require('./account.router')
|
|
||||||
const appealsRouter = require('./appeals.router')
|
const appealsRouter = require('./appeals.router')
|
||||||
const teamsRouter = require('./teams.router')
|
const teamsRouter = require('./teams.router')
|
||||||
const teamForumRouter = require('./teamForum.router')
|
const teamForumRouter = require('./teamForum.router')
|
||||||
@@ -39,7 +38,6 @@ const playerRouter = express.Router()
|
|||||||
// silently ship without it.
|
// silently ship without it.
|
||||||
playerRouter.use(noindex, requireAuth)
|
playerRouter.use(noindex, requireAuth)
|
||||||
|
|
||||||
playerRouter.use('/account', accountRouter)
|
|
||||||
playerRouter.use('/appeals', appealsRouter)
|
playerRouter.use('/appeals', appealsRouter)
|
||||||
playerRouter.use('/teams', teamsRouter)
|
playerRouter.use('/teams', teamsRouter)
|
||||||
// Same prefix, second router. The forum and the leader-exercised grant flow are a
|
// Same prefix, second router. The forum and the leader-exercised grant flow are a
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Auth · Me",
|
"name": "Auth · Me",
|
||||||
"description": "The signed-in account: profile, notification streams and devices"
|
"description": "The signed-in account: profile, account security (credentials, 2FA, linked identities, recovery codes), notification streams and devices"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Auth · Mobile",
|
"name": "Auth · Mobile",
|
||||||
@@ -40,14 +40,6 @@
|
|||||||
"name": "Public",
|
"name": "Public",
|
||||||
"description": "Unauthenticated site content (settings, posts, wiki, contact)"
|
"description": "Unauthenticated site content (settings, posts, wiki, contact)"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Admin · Account",
|
|
||||||
"description": "Self-service account security (2FA, linked identities)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Player",
|
|
||||||
"description": "Self-service player accounts (register, credentials, 2FA, linked identities)"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Player · Appeals",
|
"name": "Player · Appeals",
|
||||||
"description": "Player-submitted moderation appeals"
|
"description": "Player-submitted moderation appeals"
|
||||||
@@ -155,345 +147,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/admin/account": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"Admin · Account"
|
|
||||||
],
|
|
||||||
"summary": "Get the current account (self)",
|
|
||||||
"description": "",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "The account",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/AccountStatus"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Not authenticated",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/account/identities": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"Admin · Account"
|
|
||||||
],
|
|
||||||
"summary": "List linked SSO identities (self)",
|
|
||||||
"description": "",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Linked identities",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/LinkedIdentity"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Not authenticated",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/account/identities/{provider}": {
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"Admin · Account"
|
|
||||||
],
|
|
||||||
"summary": "Unlink an SSO identity (self)",
|
|
||||||
"description": "",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "provider",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"description": "Provider id."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Unlinked",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/UnlinkedFlag"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request"
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Not authenticated",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "No linked account for that provider",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/account/totp/disable": {
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"Admin · Account"
|
|
||||||
],
|
|
||||||
"summary": "Disable 2FA by confirming a code",
|
|
||||||
"description": "",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "2FA disabled",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TotpState"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Not enabled, or invalid code",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Not authenticated",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TotpCodeRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/account/totp/enable": {
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"Admin · Account"
|
|
||||||
],
|
|
||||||
"summary": "Enable 2FA by confirming a code",
|
|
||||||
"description": "",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "2FA enabled",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TotpState"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Setup not started, or invalid code",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Not authenticated",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"409": {
|
|
||||||
"description": "Two-factor already enabled",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TotpCodeRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/account/totp/setup": {
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"Admin · Account"
|
|
||||||
],
|
|
||||||
"summary": "Begin 2FA enrollment (returns secret + QR)",
|
|
||||||
"description": "",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "otpauth URL and QR data to scan",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TotpSetup"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Not authenticated",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"409": {
|
|
||||||
"description": "Two-factor already enabled",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/activity": {
|
"/api/v1/admin/activity": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -9922,500 +9575,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/player/account": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"Player"
|
|
||||||
],
|
|
||||||
"summary": "Get the current player account (self)",
|
|
||||||
"description": "",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "The player account",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/PlayerAccount"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Not authenticated",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Account not active (disabled/banned)",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/player/account/identities": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"Player"
|
|
||||||
],
|
|
||||||
"summary": "List linked SSO identities (self)",
|
|
||||||
"description": "",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Linked identities",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/LinkedIdentity"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/player/account/identities/{provider}": {
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"Player"
|
|
||||||
],
|
|
||||||
"summary": "Unlink an SSO identity (self)",
|
|
||||||
"description": "",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "provider",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"description": "Provider id."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Unlinked",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/UnlinkedFlag"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request"
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "No linked account for that provider",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/player/account/password": {
|
|
||||||
"patch": {
|
|
||||||
"tags": [
|
|
||||||
"Player"
|
|
||||||
],
|
|
||||||
"summary": "Change or set the current player’s password",
|
|
||||||
"description": "If the account already has a password, currentPassword is required and verified. SSO-provisioned accounts with no password may set an initial one without a current password. On success the caller’s session is re-issued (they stay logged in) while all other sessions are revoked.",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Password changed",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/OkFlag"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Validation error or wrong current password",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Account not active (disabled/banned)",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"429": {
|
|
||||||
"description": "Too many changes (rate limited)",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ChangePasswordRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/player/account/totp/disable": {
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"Player"
|
|
||||||
],
|
|
||||||
"summary": "Disable 2FA by confirming a code",
|
|
||||||
"description": "Requires a valid current authenticator code (proves control of the authenticator); it does not take a password.",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "2FA disabled",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TotpState"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Not enabled, or invalid code",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TotpCodeRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/player/account/totp/enable": {
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"Player"
|
|
||||||
],
|
|
||||||
"summary": "Enable 2FA by confirming a code",
|
|
||||||
"description": "",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "2FA enabled",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TotpState"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Setup not started, or invalid code",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
},
|
|
||||||
"409": {
|
|
||||||
"description": "Two-factor already enabled",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TotpCodeRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/player/account/totp/setup": {
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"Player"
|
|
||||||
],
|
|
||||||
"summary": "Begin 2FA enrollment (returns secret + QR)",
|
|
||||||
"description": "",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "otpauth URL and QR data to scan",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TotpSetup"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
},
|
|
||||||
"409": {
|
|
||||||
"description": "Two-factor already enabled",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/player/account/username": {
|
|
||||||
"patch": {
|
|
||||||
"tags": [
|
|
||||||
"Player"
|
|
||||||
],
|
|
||||||
"summary": "Change the current player’s username",
|
|
||||||
"description": "",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Updated username (session cookie re-issued)",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"username": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Validation error or unavailable username",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Account not active (disabled/banned)",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"409": {
|
|
||||||
"description": "Username already taken",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"429": {
|
|
||||||
"description": "Too many changes (rate limited)",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"cookieAuth": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ChangeUsernameRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/player/appeals": {
|
"/api/v1/player/appeals": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -15993,7 +15152,7 @@
|
|||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Self-service player account (GET /player/account)."
|
"example": "The signed-in account (GET /auth/me/account). Same shape for every role."
|
||||||
},
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -16034,6 +15193,9 @@
|
|||||||
"enum": {
|
"enum": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"example": [
|
"example": [
|
||||||
|
"admin",
|
||||||
|
"editor",
|
||||||
|
"moderator",
|
||||||
"player"
|
"player"
|
||||||
],
|
],
|
||||||
"items": {
|
"items": {
|
||||||
@@ -18051,86 +17213,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AccountStatus": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "object"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Self-service account security status (GET /admin/account)."
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "integer"
|
|
||||||
},
|
|
||||||
"example": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "string"
|
|
||||||
},
|
|
||||||
"example": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "admin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "string"
|
|
||||||
},
|
|
||||||
"enum": {
|
|
||||||
"type": "array",
|
|
||||||
"example": [
|
|
||||||
"admin",
|
|
||||||
"editor"
|
|
||||||
],
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"example": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "admin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"totp_enabled": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "boolean"
|
|
||||||
},
|
|
||||||
"example": {
|
|
||||||
"type": "boolean",
|
|
||||||
"example": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TotpSetup": {
|
"TotpSetup": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -58,12 +58,10 @@ const doc = {
|
|||||||
tags: [
|
tags: [
|
||||||
{ name: 'Health', description: 'Liveness probe' },
|
{ name: 'Health', description: 'Liveness probe' },
|
||||||
{ name: 'Auth', description: 'Web session login/logout (cookie + TOTP)' },
|
{ name: 'Auth', description: 'Web session login/logout (cookie + TOTP)' },
|
||||||
{ name: 'Auth · Me', description: 'The signed-in account: profile, notification streams and devices' },
|
{ name: 'Auth · Me', description: 'The signed-in account: profile, account security (credentials, 2FA, linked identities, recovery codes), notification streams and devices' },
|
||||||
{ name: 'Auth · Mobile', description: 'Native bearer-token login, refresh and logout' },
|
{ name: 'Auth · Mobile', description: 'Native bearer-token login, refresh and logout' },
|
||||||
{ name: 'Auth · SSO', description: 'OAuth2 / OIDC provider discovery and redirect flow' },
|
{ name: 'Auth · SSO', description: 'OAuth2 / OIDC provider discovery and redirect flow' },
|
||||||
{ name: 'Public', description: 'Unauthenticated site content (settings, posts, wiki, contact)' },
|
{ name: 'Public', description: 'Unauthenticated site content (settings, posts, wiki, contact)' },
|
||||||
{ name: 'Admin · Account', description: 'Self-service account security (2FA, linked identities)' },
|
|
||||||
{ name: 'Player', description: 'Self-service player accounts (register, credentials, 2FA, linked identities)' },
|
|
||||||
{ name: 'Player · Appeals', description: 'Player-submitted moderation appeals' },
|
{ name: 'Player · Appeals', description: 'Player-submitted moderation appeals' },
|
||||||
{ name: 'Settings', description: 'Site-wide settings any authenticated account may read (nav overrides)' },
|
{ name: 'Settings', description: 'Site-wide settings any authenticated account may read (nav overrides)' },
|
||||||
{ name: 'Admin · Dashboard', description: 'Dashboard summary and site mode' },
|
{ name: 'Admin · Dashboard', description: 'Dashboard summary and site mode' },
|
||||||
@@ -490,13 +488,16 @@ const doc = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// The self account as GET /auth/me/account returns it, for EVERY role — the
|
||||||
|
// name predates the collapse of /player/account and /admin/account onto
|
||||||
|
// /auth/me and is kept so existing $refs and generated clients resolve.
|
||||||
PlayerAccount: {
|
PlayerAccount: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: 'Self-service player account (GET /player/account).',
|
description: 'The signed-in account (GET /auth/me/account). Same shape for every role.',
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: 'integer', example: 42 },
|
id: { type: 'integer', example: 42 },
|
||||||
username: { type: 'string', example: 'newplayer' },
|
username: { type: 'string', example: 'newplayer' },
|
||||||
role: { type: 'string', enum: ['player'], example: 'player' },
|
role: { type: 'string', enum: ['admin', 'editor', 'moderator', 'player'], example: 'player' },
|
||||||
email: { type: 'string', format: 'email', nullable: true, example: 'player@example.com' },
|
email: { type: 'string', format: 'email', nullable: true, example: 'player@example.com' },
|
||||||
status: { type: 'string', enum: ['active', 'disabled', 'banned', 'pending'], example: 'active' },
|
status: { type: 'string', enum: ['active', 'disabled', 'banned', 'pending'], example: 'active' },
|
||||||
totp_enabled: { type: 'boolean', example: false },
|
totp_enabled: { type: 'boolean', example: false },
|
||||||
@@ -749,16 +750,6 @@ const doc = {
|
|||||||
// the affected resource id/slug or a boolean flag. Documented here as-is so
|
// the affected resource id/slug or a boolean flag. Documented here as-is so
|
||||||
// the spec matches the controllers. (The shapes are intentionally recorded
|
// the spec matches the controllers. (The shapes are intentionally recorded
|
||||||
// rather than normalized — see the audit note if standardizing later.)
|
// rather than normalized — see the audit note if standardizing later.)
|
||||||
AccountStatus: {
|
|
||||||
type: 'object',
|
|
||||||
description: 'Self-service account security status (GET /admin/account).',
|
|
||||||
properties: {
|
|
||||||
id: { type: 'integer', example: 1 },
|
|
||||||
username: { type: 'string', example: 'admin' },
|
|
||||||
role: { type: 'string', enum: ['admin', 'editor'], example: 'admin' },
|
|
||||||
totp_enabled: { type: 'boolean', example: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
TotpSetup: {
|
TotpSetup: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: 'Enrollment material returned by POST /account/totp/setup.',
|
description: 'Enrollment material returned by POST /account/totp/setup.',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ process.env.DB_PORT = '59999'
|
|||||||
const { test, beforeEach, after } = require('node:test')
|
const { test, beforeEach, after } = require('node:test')
|
||||||
const assert = require('node:assert/strict')
|
const assert = require('node:assert/strict')
|
||||||
|
|
||||||
const account = require('../src/router/v1/admin/account.controller')
|
const account = require('../src/router/v1/auth/account.controller')
|
||||||
const mobileSessions = require('../src/model/mobileSessions/mobileSessions.model')
|
const mobileSessions = require('../src/model/mobileSessions/mobileSessions.model')
|
||||||
const activity = require('../src/model/activity/activity.model')
|
const activity = require('../src/model/activity/activity.model')
|
||||||
const db = require('../src/utils/db')
|
const db = require('../src/utils/db')
|
||||||
|
|||||||
@@ -518,8 +518,8 @@ test('ctx exposes exactly the documented surface, and is frozen', () => {
|
|||||||
// is core's limiter FACTORY, not a limiter: a module states its own
|
// is core's limiter FACTORY, not a limiter: a module states its own
|
||||||
// window and cap and takes the plumbing, so there is one express-rate-limit in
|
// window and cap and takes the plumbing, so there is one express-rate-limit in
|
||||||
// the process and one place a breach is logged. is
|
// the process and one place a breach is logged. is
|
||||||
// handed over whole because it is shared policy — core's /auth/me and
|
// handed over whole because it is shared policy — core's /auth/me/account/*
|
||||||
// /player/account sit behind the same counter.
|
// and /player/appeals sit behind the same counter.
|
||||||
assert.deepEqual(probe.middleware, [
|
assert.deepEqual(probe.middleware, [
|
||||||
'accountChangeLimiter', 'noindex', 'rateLimit', 'requireAuth', 'requireRole', 'siteMode', 'validate',
|
'accountChangeLimiter', 'noindex', 'rateLimit', 'requireAuth', 'requireRole', 'siteMode', 'validate',
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const assert = require('node:assert/strict')
|
|||||||
const bcrypt = require('bcryptjs')
|
const bcrypt = require('bcryptjs')
|
||||||
|
|
||||||
const authCtrl = require('../src/router/v1/auth/auth.controller')
|
const authCtrl = require('../src/router/v1/auth/auth.controller')
|
||||||
const account = require('../src/router/v1/admin/account.controller')
|
const account = require('../src/router/v1/auth/account.controller')
|
||||||
const users = require('../src/model/users/users.model')
|
const users = require('../src/model/users/users.model')
|
||||||
const settings = require('../src/model/settings/settings.model')
|
const settings = require('../src/model/settings/settings.model')
|
||||||
const botScore = require('../src/middleware/botScore')
|
const botScore = require('../src/middleware/botScore')
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const assert = require('node:assert/strict')
|
|||||||
// - trusting the current device is ownership-scoped and honors the cap (409);
|
// - trusting the current device is ownership-scoped and honors the cap (409);
|
||||||
// - self-revoke is scoped to the caller's own id;
|
// - self-revoke is scoped to the caller's own id;
|
||||||
// - regenerating recovery codes is a password step-up (wrong password → 400).
|
// - regenerating recovery codes is a password step-up (wrong password → 400).
|
||||||
const ctrl = require('../src/router/v1/admin/account.controller')
|
const ctrl = require('../src/router/v1/auth/account.controller')
|
||||||
const users = require('../src/model/users/users.model')
|
const users = require('../src/model/users/users.model')
|
||||||
const activity = require('../src/model/activity/activity.model')
|
const activity = require('../src/model/activity/activity.model')
|
||||||
const sessionService = require('../src/auth/session.service')
|
const sessionService = require('../src/auth/session.service')
|
||||||
|
|||||||
Reference in New Issue
Block a user