Merge pull request 'build(swagger): normalize and sort generated OpenAPI path keys' (#101) from build/swagger-normalize-paths into main
All checks were successful
sync-project-tree / sync (push) Successful in 12s
Build container images / build (push) Successful in 53s
Build container images / deploy (push) Successful in 37s
SonarQube / analysis (push) Successful in 2m38s

Reviewed-on: #101
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
This commit is contained in:
2026-07-27 21:00:57 +00:00
9 changed files with 8721 additions and 8564 deletions

View File

@@ -0,0 +1,87 @@
// 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

View File

@@ -1,3 +1,14 @@
// Residual /admin routes — the capabilities not yet carved into their own
// router file (docs/website/API_V2_PLAN.md § Phase 2). Mounted at the root of
// /api/v1/admin by admin/index.js, *after* the extracted capability routers and
// behind the shared `noindex, isLoggedIn, staffOnly` gate it owns, so the URLs
// here are unchanged from when this file held all 110 admin routes.
//
// Already extracted: users, account, invites, auth/providers.
// Still here: shard, dashboard, site-mode, posts, uploads, wiki, pages,
// settings, activity, bot-activity, discord-bot, email, moderation, uo-link.
// This file disappears when the last group moves.
const express = require('express')
const path = require('path')
const fs = require('fs')
@@ -6,32 +17,19 @@ const multer = require('multer')
const { body, param } = require('express-validator')
const ctrl = require('./admin.controller')
const account = require('./account.controller')
const botActivity = require('./botActivity.controller')
const authProviders = require('./authProviders.controller')
const discordBot = require('./discordBot.controller')
const emailConfig = require('./emailConfig.controller')
const uoLink = require('./uoLink.controller')
const shardOps = require('./shardOps.controller')
const usersShard = require('./usersShard.controller')
const invites = require('./invites.controller')
const selfShard = require('../player/shard.controller')
const moderation = require('./moderation.controller')
const pagesCtrl = require('./pages.controller')
const { isLoggedIn, requireRole } = require('../../../utils/auth')
const noindex = require('../../../middleware/noindex')
const { requireRole } = require('../../../utils/auth')
const validate = require('../../../middleware/validate')
const adminRouter = express.Router()
// Every admin route requires auth, a STAFF role, and is kept out of search
// indexes. The staff gate matters now that `player` is a logged-in-but-untrusted
// role: without it, the editor-tier routes below (dashboard, posts, wiki,
// uploads) that are only guarded by isLoggedIn would be reachable by players.
// Players get 403 here and use the self-scoped /player group instead.
const staffOnly = requireRole('admin', 'editor', 'moderator')
adminRouter.use(noindex, isLoggedIn, staffOnly)
// Admin-only gate. Editors may manage content (posts/wiki), but user
// management, site mode, and settings are restricted to the admin role.
const adminOnly = requireRole('admin')
@@ -41,79 +39,6 @@ const adminOnly = requireRole('admin')
// admin check inside the controller.
const modAccess = requireRole('admin', 'moderator')
// ── Account security (self-service, any logged-in role) ───────────────
// Not behind adminOnly: an editor manages their own 2FA too.
adminRouter.get(
'/account',
// #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,
)
adminRouter.post(
'/account/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,
)
adminRouter.post(
'/account/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,
)
adminRouter.post(
'/account/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).
adminRouter.get(
'/account/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,
)
adminRouter.delete(
'/account/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,
)
// ── Game account linking (self-service, any staff role) ───────────────
// Staff link their OWN in-game account here, exactly like players do under
// /player/shard. The controller keys off req.user.id, so the same handlers work.
@@ -979,89 +904,6 @@ adminRouter.post(
emailConfig.disconnect,
)
// ── Authentication providers / SSO (admin only) ───────────────────────
adminRouter.get(
'/auth/providers',
// #swagger.tags = ['Admin · Auth Providers']
// #swagger.summary = 'List configured SSO providers (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.responses[200] = { description: 'Providers (secrets stripped)', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ProviderConfig" } } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
authProviders.list,
)
adminRouter.post(
'/auth/providers',
// #swagger.tags = ['Admin · Auth Providers']
// #swagger.summary = 'Create a custom SSO provider (admin only)'
// #swagger.description = 'Built-in providers (google, discord) are configured via PUT, not created here.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderCreateRequest" } } } } */
/* #swagger.responses[201] = { description: 'Created provider', content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderConfig" } } } } */
/* #swagger.responses[400] = { description: 'Validation error, or a built-in/invalid kind', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[409] = { description: 'Provider id already exists', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
body('id').matches(/^[a-z0-9-]+$/),
body('kind').isIn(['oidc', 'oauth2']),
body('name').isString().trim().notEmpty().isLength({ max: 80 }),
body('enabled').optional().isBoolean(),
body('clientId').optional({ values: 'falsy' }).isString(),
body('secret').optional({ values: 'falsy' }).isString(),
body('authorizeUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('tokenUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('userinfoUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('scopes').optional({ values: 'falsy' }).isString().isLength({ max: 500 }),
body('priority').optional().isInt(),
validate,
authProviders.create,
)
adminRouter.put(
'/auth/providers/:id',
// #swagger.tags = ['Admin · Auth Providers']
// #swagger.summary = 'Update an SSO provider (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
/* #swagger.requestBody = { content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderCreateRequest" } } } } */
/* #swagger.responses[200] = { description: 'Updated provider', content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderConfig" } } } } */
/* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Provider not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
param('id').matches(/^[a-z0-9-]+$/),
body('name').optional().isString().trim().notEmpty().isLength({ max: 80 }),
body('enabled').optional().isBoolean(),
body('clientId').optional({ values: 'falsy' }).isString(),
body('secret').optional({ values: 'falsy' }).isString(),
body('authorizeUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('tokenUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('userinfoUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('scopes').optional({ values: 'falsy' }).isString().isLength({ max: 500 }),
body('priority').optional().isInt(),
validate,
authProviders.update,
)
adminRouter.delete(
'/auth/providers/:id',
// #swagger.tags = ['Admin · Auth Providers']
// #swagger.summary = 'Delete a custom SSO provider (admin only)'
// #swagger.description = 'Built-in providers cannot be deleted — disable them instead.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
/* #swagger.responses[200] = { description: 'Deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedFlag" } } } } */
/* #swagger.responses[400] = { description: 'Built-in provider cannot be deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Provider not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
param('id').matches(/^[a-z0-9-]+$/),
validate,
authProviders.remove,
)
// ── Moderation dashboard (admin + moderator) ──────────────────────────
// Read-only views over the bot's mod_actions log, plus staff notes. The whole
// sub-path is gated for the moderator role (admins included).
@@ -1214,270 +1056,6 @@ adminRouter.get(
moderation.getUserAppeals,
)
// ── User management (admin only) ──────────────────────────────────────
adminRouter.use('/users', adminOnly)
adminRouter.get(
'/users',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'List users (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.responses[200] = { description: 'Users', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/User" } } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
ctrl.listUsers,
)
adminRouter.post(
'/users',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Create a user (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/UserCreateRequest" } } } } */
/* #swagger.responses[201] = { description: 'Created user', content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } } } */
/* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[409] = { description: 'Username already taken', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
body('username').isString().trim().isLength({ min: 3, max: 32 }),
body('password').isString().isLength({ min: 8, max: 64 }),
body('role').optional().isIn(['admin', 'editor', 'moderator', 'player']),
body('status').optional().isIn(['active', 'disabled', 'banned', 'pending']),
body('email').optional({ values: 'falsy' }).isEmail().isLength({ max: 255 }),
validate,
ctrl.createUser,
)
adminRouter.put(
'/users/:id',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Update a user (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.requestBody = { content: { "application/json": { schema: { $ref: "#/components/schemas/UserCreateRequest" } } } } */
/* #swagger.responses[200] = { description: 'Updated user', content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } } } */
/* #swagger.responses[400] = { description: 'Validation error, or cannot demote the last admin', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[409] = { description: 'Username already taken', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
body('username').optional().isString().trim().isLength({ min: 3, max: 32 }),
body('password').optional().isString().isLength({ min: 8, max: 64 }),
body('role').optional().isIn(['admin', 'editor', 'moderator', 'player']),
body('status').optional().isIn(['active', 'disabled', 'banned', 'pending']),
body('email').optional({ values: 'null' }).isEmail().isLength({ max: 255 }),
validate,
ctrl.updateUser,
)
adminRouter.delete(
'/users/:id',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Delete a user (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Deleted (echoes the id)', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedId" } } } } */
/* #swagger.responses[400] = { description: 'Cannot delete your own account or the last admin', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
ctrl.deleteUser,
)
// ── A user's trusted devices & MFA (admin only) ───────────────────────
adminRouter.get(
'/users/:id/trusted-devices',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'List a users trusted devices (admin only)'
// #swagger.description = 'Active (unrevoked, unexpired) trusted devices for the target user — the browsers/apps allowed to skip that users TOTP step. Never returns tokens.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Trusted devices', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/TrustedDevice" } } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
ctrl.listUserTrustedDevices,
)
adminRouter.delete(
'/users/:id/trusted-devices',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Revoke all of a users trusted devices (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Revoked count', content: { "application/json": { schema: { type: "object", properties: { revoked: { type: "integer" } } } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
ctrl.revokeAllUserTrustedDevices,
)
adminRouter.delete(
'/users/:id/trusted-devices/:deviceId',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Revoke one of a users trusted devices (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
// #swagger.parameters['deviceId'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Trusted-device id.' }
/* #swagger.responses[200] = { description: 'Revoked (idempotent)', content: { "application/json": { schema: { type: "object", properties: { revoked: { type: "boolean" } } } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
param('deviceId').isInt({ min: 1 }),
validate,
ctrl.revokeUserTrustedDevice,
)
adminRouter.post(
'/users/:id/mfa/reset',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Reset a users MFA (admin only)'
// #swagger.description = 'Recovers a locked-out user: turns TOTP off, revokes every trusted device, and clears their recovery codes. The user can then sign in with their password alone and re-enroll.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'MFA reset', content: { "application/json": { schema: { $ref: "#/components/schemas/OkFlag" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
ctrl.resetUserMfa,
)
// ── User → shard (uo-link) footprint (admin only) ─────────────────────
// Backs the /admin/users/:id detail page: a user's linked game accounts and,
// scoped to those accounts, their vendor sales / houses / online characters.
// Live character rosters are fetched by the client through /admin/shard/* (which
// already grants admins a bypass to any account), so no routes for them here.
adminRouter.get(
'/users/:id',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Get a single user (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'The user', content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.getUser,
)
adminRouter.get(
'/users/:id/shard/accounts',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'A users linked game accounts (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Linked accounts', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardLink" } } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.listAccounts,
)
adminRouter.get(
'/users/:id/shard/sales',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Recent vendor sales on a users accounts (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Vendor sales', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardVendorSale" } } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.getSales,
)
adminRouter.get(
'/users/:id/shard/houses',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Houses owned by a users accounts (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Houses (IDOC first)', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.getHouses,
)
adminRouter.get(
'/users/:id/shard/online',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'A users characters currently online (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Online characters', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.getOnline,
)
adminRouter.get(
'/users/:id/shard/standing',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'A users shard standing — governorships held and guilds led (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Standing { governorOf, guildsLed }', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.getStanding,
)
adminRouter.delete(
'/users/:id/shard/link/:account',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Unlink a game account from this user (admin only)'
// #swagger.description = 'Severs a game accounts tie to the website user from the site side (sidecar DELETE /link/{account}) and drops the local mirror. actor is stamped from the session.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
// #swagger.parameters['account'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Game account to unlink.' }
/* #swagger.responses[200] = { description: 'Unlinked', content: { "application/json": { schema: { type: "object", properties: { account: { type: "string" }, unlinked: { type: "boolean" } } } } } } */
/* #swagger.responses[403] = { description: 'Protected staff account (refused by shard)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not linked', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
param('id').isInt(),
param('account').matches(SHARD_ACCOUNT_RE),
validate,
usersShard.unlinkAccount,
)
// ── Email invites (admin only) ─────────────────────────────────────────────
adminRouter.post(
'/invites',
// #swagger.tags = ['Admin · Invites']
// #swagger.summary = 'Create and email an account invite at a chosen access level'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", required: ["email","role"], properties: { email: { type: "string" }, role: { type: "string" } } } } } */
/* #swagger.responses[201] = { description: 'Invite created', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
/* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
adminOnly,
body('email').isEmail().isLength({ max: 255 }),
body('role').isIn(['admin', 'editor', 'moderator', 'player']),
validate,
invites.create,
)
adminRouter.get(
'/invites',
// #swagger.tags = ['Admin · Invites']
// #swagger.summary = 'List recent invites (no tokens)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.responses[200] = { description: 'Invites, newest first', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */
adminOnly,
invites.list,
)
adminRouter.delete(
'/invites/:id',
// #swagger.tags = ['Admin · Invites']
// #swagger.summary = 'Revoke a pending invite'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Invite id.' }
/* #swagger.responses[200] = { description: 'Revoked', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
/* #swagger.responses[404] = { description: 'No pending invite to revoke', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
param('id').isInt(),
validate,
invites.revoke,
)
// ── uo-link sidecar control (admin only) ──────────────────────────────────
// Connection config (base/ws URL + token + protocol + enabled) and the town
// crier. The token is write-only (SECURITY note in uoLink.controller.js).

View File

@@ -0,0 +1,102 @@
// Admin · Auth Providers — SSO/OAuth2/OIDC provider configuration.
//
// Mounted at /api/v1/admin/auth by admin/index.js, which already applied
// `noindex, isLoggedIn, staffOnly`; the routes below are /providers under that,
// so the emitted URLs stay /api/v1/admin/auth/providers[/:id].
//
// Admin-only: these rows carry client secrets (write-only, AES-GCM at rest via
// utils/secretBox.js) and decide which external identities may sign in at all.
const express = require('express')
const { body, param } = require('express-validator')
const authProviders = require('./authProviders.controller')
const { requireRole } = require('../../../utils/auth')
const validate = require('../../../middleware/validate')
const providersRouter = express.Router()
const adminOnly = requireRole('admin')
providersRouter.get(
'/providers',
// #swagger.tags = ['Admin · Auth Providers']
// #swagger.summary = 'List configured SSO providers (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.responses[200] = { description: 'Providers (secrets stripped)', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ProviderConfig" } } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
authProviders.list,
)
providersRouter.post(
'/providers',
// #swagger.tags = ['Admin · Auth Providers']
// #swagger.summary = 'Create a custom SSO provider (admin only)'
// #swagger.description = 'Built-in providers (google, discord) are configured via PUT, not created here.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderCreateRequest" } } } } */
/* #swagger.responses[201] = { description: 'Created provider', content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderConfig" } } } } */
/* #swagger.responses[400] = { description: 'Validation error, or a built-in/invalid kind', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[409] = { description: 'Provider id already exists', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
body('id').matches(/^[a-z0-9-]+$/),
body('kind').isIn(['oidc', 'oauth2']),
body('name').isString().trim().notEmpty().isLength({ max: 80 }),
body('enabled').optional().isBoolean(),
body('clientId').optional({ values: 'falsy' }).isString(),
body('secret').optional({ values: 'falsy' }).isString(),
body('authorizeUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('tokenUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('userinfoUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('scopes').optional({ values: 'falsy' }).isString().isLength({ max: 500 }),
body('priority').optional().isInt(),
validate,
authProviders.create,
)
providersRouter.put(
'/providers/:id',
// #swagger.tags = ['Admin · Auth Providers']
// #swagger.summary = 'Update an SSO provider (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
/* #swagger.requestBody = { content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderCreateRequest" } } } } */
/* #swagger.responses[200] = { description: 'Updated provider', content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderConfig" } } } } */
/* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Provider not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
param('id').matches(/^[a-z0-9-]+$/),
body('name').optional().isString().trim().notEmpty().isLength({ max: 80 }),
body('enabled').optional().isBoolean(),
body('clientId').optional({ values: 'falsy' }).isString(),
body('secret').optional({ values: 'falsy' }).isString(),
body('authorizeUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('tokenUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('userinfoUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
body('scopes').optional({ values: 'falsy' }).isString().isLength({ max: 500 }),
body('priority').optional().isInt(),
validate,
authProviders.update,
)
providersRouter.delete(
'/providers/:id',
// #swagger.tags = ['Admin · Auth Providers']
// #swagger.summary = 'Delete a custom SSO provider (admin only)'
// #swagger.description = 'Built-in providers cannot be deleted — disable them instead.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
/* #swagger.responses[200] = { description: 'Deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedFlag" } } } } */
/* #swagger.responses[400] = { description: 'Built-in provider cannot be deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Provider not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
param('id').matches(/^[a-z0-9-]+$/),
validate,
authProviders.remove,
)
module.exports = providersRouter

View File

@@ -0,0 +1,46 @@
// /api/v1/admin — the admin surface, assembled from per-capability routers.
//
// This file owns exactly two things: the gate every admin route shares, and the
// mount table. No route is declared here. Each capability router mounts at the
// prefix it already owned inside the old monolithic admin.routes.js, so the
// emitted URL set is byte-identical — proved per PR by a zero-line diff in
// server/routes.manifest.json (`npm run routes:manifest`).
//
// See docs/website/API_V2_PLAN.md § Phase 2 for the split and its remaining PRs.
const express = require('express')
const { isLoggedIn, requireRole } = require('../../../utils/auth')
const noindex = require('../../../middleware/noindex')
const accountRouter = require('./account.router')
const usersRouter = require('./users.router')
const invitesRouter = require('./invites.router')
const authProvidersRouter = require('./authProviders.router')
const residualRouter = require('./admin.routes')
const adminRouter = express.Router()
// Every admin route requires auth, a STAFF role, and is kept out of search
// indexes. The staff gate matters now that `player` is a logged-in-but-untrusted
// role: without it, the editor-tier routes below (dashboard, posts, wiki,
// uploads) that are only guarded by isLoggedIn would be reachable by players.
// Players get 403 here and use the self-scoped /player group instead.
//
// It lives here, ahead of every mount, so a capability router extracted in a
// later PR cannot silently ship without it.
const staffOnly = requireRole('admin', 'editor', 'moderator')
adminRouter.use(noindex, isLoggedIn, staffOnly)
adminRouter.use('/account', accountRouter)
adminRouter.use('/users', usersRouter)
adminRouter.use('/invites', invitesRouter)
// Mounted at /auth, not /auth/providers: /admin/auth is the capability, and the
// routes inside read as /providers[/:id].
adminRouter.use('/auth', authProvidersRouter)
// Everything not yet extracted, at the group root. Mounted last, but none of the
// prefixes above appear in it, so nothing here depends on the ordering.
adminRouter.use('/', residualRouter)
module.exports = adminRouter

View File

@@ -0,0 +1,54 @@
// Admin · Invites — create, list and revoke emailed account invites.
//
// Mounted at /api/v1/admin/invites by admin/index.js, which already applied
// `noindex, isLoggedIn, staffOnly`. Issuing an invite picks the new account's
// role, so it is admin-only — otherwise an editor could mint an admin.
const express = require('express')
const { body, param } = require('express-validator')
const invites = require('./invites.controller')
const { requireRole } = require('../../../utils/auth')
const validate = require('../../../middleware/validate')
const invitesRouter = express.Router()
const adminOnly = requireRole('admin')
invitesRouter.post(
'/',
// #swagger.tags = ['Admin · Invites']
// #swagger.summary = 'Create and email an account invite at a chosen access level'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", required: ["email","role"], properties: { email: { type: "string" }, role: { type: "string" } } } } } */
/* #swagger.responses[201] = { description: 'Invite created', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
/* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
adminOnly,
body('email').isEmail().isLength({ max: 255 }),
body('role').isIn(['admin', 'editor', 'moderator', 'player']),
validate,
invites.create,
)
invitesRouter.get(
'/',
// #swagger.tags = ['Admin · Invites']
// #swagger.summary = 'List recent invites (no tokens)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.responses[200] = { description: 'Invites, newest first', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */
adminOnly,
invites.list,
)
invitesRouter.delete(
'/:id',
// #swagger.tags = ['Admin · Invites']
// #swagger.summary = 'Revoke a pending invite'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Invite id.' }
/* #swagger.responses[200] = { description: 'Revoked', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
/* #swagger.responses[404] = { description: 'No pending invite to revoke', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
param('id').isInt(),
validate,
invites.revoke,
)
module.exports = invitesRouter

View File

@@ -0,0 +1,249 @@
// Admin · Users — user management, MFA recovery, and a user's shard footprint.
//
// Mounted at /api/v1/admin/users by admin/index.js, which already applied
// `noindex, isLoggedIn, staffOnly`. The whole capability is admin-only: editors
// and moderators manage content and reports, never accounts.
//
// Handlers still live in admin.controller.js (users) and usersShard.controller.js
// (uo-link footprint); this PR re-wires routes, not logic.
const express = require('express')
const { body, param } = require('express-validator')
const ctrl = require('./admin.controller')
const usersShard = require('./usersShard.controller')
const { requireRole } = require('../../../utils/auth')
const validate = require('../../../middleware/validate')
// Same shape the shard routes validate account names with.
const SHARD_ACCOUNT_RE = /^[A-Za-z0-9_.-]{1,120}$/
const usersRouter = express.Router()
const adminOnly = requireRole('admin')
usersRouter.use(adminOnly)
usersRouter.get(
'/',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'List users (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.responses[200] = { description: 'Users', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/User" } } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
ctrl.listUsers,
)
usersRouter.post(
'/',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Create a user (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/UserCreateRequest" } } } } */
/* #swagger.responses[201] = { description: 'Created user', content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } } } */
/* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[409] = { description: 'Username already taken', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
body('username').isString().trim().isLength({ min: 3, max: 32 }),
body('password').isString().isLength({ min: 8, max: 64 }),
body('role').optional().isIn(['admin', 'editor', 'moderator', 'player']),
body('status').optional().isIn(['active', 'disabled', 'banned', 'pending']),
body('email').optional({ values: 'falsy' }).isEmail().isLength({ max: 255 }),
validate,
ctrl.createUser,
)
usersRouter.put(
'/:id',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Update a user (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.requestBody = { content: { "application/json": { schema: { $ref: "#/components/schemas/UserCreateRequest" } } } } */
/* #swagger.responses[200] = { description: 'Updated user', content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } } } */
/* #swagger.responses[400] = { description: 'Validation error, or cannot demote the last admin', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[409] = { description: 'Username already taken', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
body('username').optional().isString().trim().isLength({ min: 3, max: 32 }),
body('password').optional().isString().isLength({ min: 8, max: 64 }),
body('role').optional().isIn(['admin', 'editor', 'moderator', 'player']),
body('status').optional().isIn(['active', 'disabled', 'banned', 'pending']),
body('email').optional({ values: 'null' }).isEmail().isLength({ max: 255 }),
validate,
ctrl.updateUser,
)
usersRouter.delete(
'/:id',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Delete a user (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Deleted (echoes the id)', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedId" } } } } */
/* #swagger.responses[400] = { description: 'Cannot delete your own account or the last admin', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
ctrl.deleteUser,
)
// ── A user's trusted devices & MFA (admin only) ───────────────────────
usersRouter.get(
'/:id/trusted-devices',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'List a users trusted devices (admin only)'
// #swagger.description = 'Active (unrevoked, unexpired) trusted devices for the target user — the browsers/apps allowed to skip that users TOTP step. Never returns tokens.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Trusted devices', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/TrustedDevice" } } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
ctrl.listUserTrustedDevices,
)
usersRouter.delete(
'/:id/trusted-devices',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Revoke all of a users trusted devices (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Revoked count', content: { "application/json": { schema: { type: "object", properties: { revoked: { type: "integer" } } } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
ctrl.revokeAllUserTrustedDevices,
)
usersRouter.delete(
'/:id/trusted-devices/:deviceId',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Revoke one of a users trusted devices (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
// #swagger.parameters['deviceId'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Trusted-device id.' }
/* #swagger.responses[200] = { description: 'Revoked (idempotent)', content: { "application/json": { schema: { type: "object", properties: { revoked: { type: "boolean" } } } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
param('deviceId').isInt({ min: 1 }),
validate,
ctrl.revokeUserTrustedDevice,
)
usersRouter.post(
'/:id/mfa/reset',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Reset a users MFA (admin only)'
// #swagger.description = 'Recovers a locked-out user: turns TOTP off, revokes every trusted device, and clears their recovery codes. The user can then sign in with their password alone and re-enroll.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'MFA reset', content: { "application/json": { schema: { $ref: "#/components/schemas/OkFlag" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
ctrl.resetUserMfa,
)
// ── User → shard (uo-link) footprint (admin only) ─────────────────────
// Backs the /admin/users/:id detail page: a user's linked game accounts and,
// scoped to those accounts, their vendor sales / houses / online characters.
// Live character rosters are fetched by the client through /admin/shard/* (which
// already grants admins a bypass to any account), so no routes for them here.
usersRouter.get(
'/:id',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Get a single user (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'The user', content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.getUser,
)
usersRouter.get(
'/:id/shard/accounts',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'A users linked game accounts (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Linked accounts', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardLink" } } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.listAccounts,
)
usersRouter.get(
'/:id/shard/sales',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Recent vendor sales on a users accounts (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Vendor sales', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardVendorSale" } } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.getSales,
)
usersRouter.get(
'/:id/shard/houses',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Houses owned by a users accounts (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Houses (IDOC first)', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.getHouses,
)
usersRouter.get(
'/:id/shard/online',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'A users characters currently online (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Online characters', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.getOnline,
)
usersRouter.get(
'/:id/shard/standing',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'A users shard standing — governorships held and guilds led (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
/* #swagger.responses[200] = { description: 'Standing { governorOf, guildsLed }', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
/* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt(),
validate,
usersShard.getStanding,
)
usersRouter.delete(
'/:id/shard/link/:account',
// #swagger.tags = ['Admin · Users']
// #swagger.summary = 'Unlink a game account from this user (admin only)'
// #swagger.description = 'Severs a game accounts tie to the website user from the site side (sidecar DELETE /link/{account}) and drops the local mirror. actor is stamped from the session.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
// #swagger.parameters['account'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Game account to unlink.' }
/* #swagger.responses[200] = { description: 'Unlinked', content: { "application/json": { schema: { type: "object", properties: { account: { type: "string" }, unlinked: { type: "boolean" } } } } } } */
/* #swagger.responses[403] = { description: 'Protected staff account (refused by shard)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'Not linked', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
param('id').isInt(),
param('account').matches(SHARD_ACCOUNT_RE),
validate,
usersShard.unlinkAccount,
)
module.exports = usersRouter

View File

@@ -4,7 +4,7 @@ const v1Router = express.Router()
const authRouter = require('./auth/auth.routes')
const publicRouter = require('./public/public.routes')
const adminRouter = require('./admin/admin.routes')
const adminRouter = require('./admin')
const playerRouter = require('./player/player.routes')
v1Router.use('/auth', authRouter)

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,8 @@
// Regenerate with: npm run swagger (from the server/ directory)
// The generated JSON is committed so the docs work without a build step.
const fs = require('fs')
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' })
const pkg = require('../package.json')
const brand = require('../src/config/brand')
@@ -916,7 +918,46 @@ const doc = {
},
}
/**
* Normalize `/a/b/` → `/a/b` in the generated path keys.
*
* swagger-autogen builds a path by string-concatenating the mount prefix with the
* route argument, so a capability router mounted at `/users` that declares its
* collection route as `router.get('/')` documents as `/api/v1/admin/users/`.
* Express itself does not care (non-strict routing treats the two as one route,
* and server/routes.manifest.json records the canonical slash-less form), but the
* *spec* would advertise a URL no client uses and stop documenting the one they
* all call. The domain split (docs/website/API_V2_PLAN.md § Phase 2) creates one
* of these per capability router, so it is fixed here once rather than by
* contorting the route declarations in every router file.
*
* The path keys are also **sorted**. swagger-autogen emits them in router-traversal
* order, so moving a route between files rewrites most of this 5k-line committed
* artifact even when the API is provably unchanged — burying the one line a
* reviewer needs to see. OpenAPI attaches no meaning to path order, and
* scripts/routeManifest.js already sorts for the same reason.
*/
function normalizePaths(spec) {
const paths = {}
for (const [p, item] of Object.entries(spec.paths).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
const key = p.length > 1 ? p.replace(/\/+$/, '') : p
if (paths[key]) {
// Two different declarations collapsed onto one path — merging would hide
// whichever lost. Nothing in the tree does this today; fail loudly if it starts.
throw new Error(
`swagger: "${p}" and "${key}" collide after trailing-slash normalization. ` +
'Two routes are documenting the same URL — reconcile them in the router.',
)
}
paths[key] = item
}
spec.paths = paths
return spec
}
swaggerAutogen(outputFile, routes, doc).then(() => {
const written = JSON.parse(fs.readFileSync(outputFile, 'utf8'))
fs.writeFileSync(outputFile, `${JSON.stringify(normalizePaths(written), null, 2)}\n`)
// eslint-disable-next-line no-console
console.log('swagger-output.json generated.')
})