const express = require('express') const path = require('path') const fs = require('fs') const crypto = require('crypto') 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 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') // Moderation-dashboard gate. Moderators get the moderation views; admins can do // everything a moderator can. Sensitive writes (admin_only notes) add an extra // 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. const SHARD_ACCOUNT_RE = /^[A-Za-z0-9_.-]{1,120}$/ adminRouter.post( '/shard/link', // #swagger.tags = ['Admin · Account'] // #swagger.summary = 'Link an in-game account with a one-time code (self)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ShardLinkRequest" } } } } */ /* #swagger.responses[200] = { description: 'Linked', content: { "application/json": { schema: { $ref: "#/components/schemas/ShardLinkResult" } } } } */ /* #swagger.responses[400] = { description: 'Unknown or expired code', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ body('code').isString().trim().isLength({ min: 4, max: 32 }), validate, selfShard.link, ) adminRouter.get( '/shard/accounts', // #swagger.tags = ['Admin · Account'] // #swagger.summary = 'List the caller’s linked game accounts (self)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Linked accounts', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardLink" } } } } } */ selfShard.listAccounts, ) adminRouter.get( '/shard/roster/:account', // #swagger.tags = ['Admin · Account'] // #swagger.summary = 'Character roster for an account (self; admins: any account)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['account'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'A game account linked to the caller.' } /* #swagger.responses[200] = { description: 'Account roster', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[403] = { description: 'Account not linked to the caller', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('account').matches(SHARD_ACCOUNT_RE), validate, selfShard.roster, ) adminRouter.get( '/shard/vendors/:account', // #swagger.tags = ['Admin · Account'] // #swagger.summary = 'Player vendors for an account (self; admins: any account)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['account'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'A game account linked to the caller.' } /* #swagger.responses[200] = { description: 'Vendor snapshot', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[403] = { description: 'Account not linked to the caller', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('account').matches(SHARD_ACCOUNT_RE), validate, selfShard.vendors, ) adminRouter.get( '/shard/char/:serial', // #swagger.tags = ['Admin · Account'] // #swagger.summary = 'Character sheet (self-linked characters; admins: any character)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['serial'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Mobile serial, e.g. 0x24C.' } /* #swagger.responses[200] = { description: 'Character profile', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[403] = { description: 'Character not on an account linked to the caller', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('serial').matches(/^0x[0-9a-fA-F]+$/), validate, selfShard.getChar, ) adminRouter.get( '/shard/sales', // #swagger.tags = ['Admin · Account'] // #swagger.summary = 'Recent player-vendor sales for the caller’s linked accounts (self)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Vendor sales', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardVendorSale" } } } } } */ selfShard.getSales, ) // ── In-game staff operations (uo-link write plane + support queue) ───── // Privileged live-shard actions and the help-page queue, open to moderators as // well as admins (modAccess). `actor` is stamped server-side from the session in // the controller — the body never carries it. See shardOps.controller.js. adminRouter.post( '/shard/kick', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Kick every live session of an account (admin/moderator)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { account: { type: "string" }, serial: { type: "string" } } } } } } */ /* #swagger.responses[200] = { description: 'Kicked', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[403] = { description: 'Protected target or write plane disabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ modAccess, body('account').optional({ values: 'falsy' }).matches(SHARD_ACCOUNT_RE), body('serial').optional({ values: 'falsy' }).matches(/^0x[0-9a-fA-F]+$/), validate, shardOps.kick, ) adminRouter.post( '/shard/ban', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Ban an account, timed or indefinite (admin/moderator)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { account: { type: "string" }, serial: { type: "string" }, durationSec: { type: "integer" }, reason: { type: "string" } } } } } } */ /* #swagger.responses[200] = { description: 'Banned', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[403] = { description: 'Protected target or write plane disabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ modAccess, body('account').optional({ values: 'falsy' }).matches(SHARD_ACCOUNT_RE), body('serial').optional({ values: 'falsy' }).matches(/^0x[0-9a-fA-F]+$/), body('durationSec').optional().isInt({ min: 0, max: 315360000 }), body('reason').optional({ values: 'falsy' }).isString().trim().isLength({ max: 500 }), validate, shardOps.ban, ) adminRouter.post( '/shard/unban', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Clear an account ban (admin/moderator)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { account: { type: "string" } }, required: ["account"] } } } } */ /* #swagger.responses[200] = { description: 'Unbanned', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ modAccess, body('account').matches(SHARD_ACCOUNT_RE), validate, shardOps.unban, ) adminRouter.post( '/shard/broadcast', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Broadcast a system message to everyone online (admin/moderator)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { text: { type: "string" }, hue: { type: "integer" } }, required: ["text"] } } } } */ /* #swagger.responses[200] = { description: 'Broadcast', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ modAccess, body('text').isString().trim().isLength({ min: 1, max: 300 }), body('hue').optional().isInt({ min: 0, max: 3000 }), validate, shardOps.broadcast, ) adminRouter.get( '/shard/pages', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Open help-page (support) queue (admin/moderator)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Open pages', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */ modAccess, shardOps.listPages, ) adminRouter.post( '/shard/pages/:id/respond', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Reply to a help page, optionally closing it (admin/moderator)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Page id (sender serial).' } /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { message: { type: "string" }, close: { type: "boolean" } }, required: ["message"] } } } } */ /* #swagger.responses[200] = { description: 'Responded', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[404] = { description: 'Unknown page', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ modAccess, param('id').matches(/^0x[0-9a-fA-F]+$/), body('message').isString().trim().isLength({ min: 1, max: 500 }), body('close').optional().isBoolean(), validate, shardOps.respondPage, ) adminRouter.post( '/shard/pages/:id/close', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Resolve a help page without a reply (admin/moderator)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Page id (sender serial).' } /* #swagger.responses[200] = { description: 'Closed', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ modAccess, param('id').matches(/^0x[0-9a-fA-F]+$/), validate, shardOps.closePage, ) adminRouter.get( '/shard/audit', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Recent in-game moderation audit events (admin/moderator)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'admin.audit events, newest first', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardEvent" } } } } } */ modAccess, shardOps.listAudit, ) adminRouter.get( '/shard/houses', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Full house registry — owner, price, decay (admin/moderator)' // #swagger.description = 'The complete house registry. The public endpoint shows only IDOC houses with location; this staff view carries owner/price/co-owner/decay detail.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Houses, ordered by name', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardHouse" } } } } } */ modAccess, shardOps.listHouses, ) // ── Image uploads (screenshots/gallery) ─────────────────────────────── const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, '..', '..', '..', '..', 'uploads') fs.mkdirSync(UPLOAD_DIR, { recursive: true }) // Whitelisted image mimetypes → the extension we store the file under. The // stored extension is derived from this map (keyed by the accepted mimetype), // never from originalname — so a spoofed `Content-Type: image/png` paired with // `originalname: x.html` can never land an executable .html file in /uploads. const MIME_EXT = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/gif': '.gif', 'image/webp': '.webp', 'image/avif': '.avif', } const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, UPLOAD_DIR), filename: (req, file, cb) => { const ext = MIME_EXT[file.mimetype] || '' cb(null, `${Date.now()}-${crypto.randomBytes(8).toString('hex')}${ext}`) }, }) const upload = multer({ storage, limits: { fileSize: 8 * 1024 * 1024 }, fileFilter: (req, file, cb) => { // Single source of truth: only mimetypes we can map to a safe extension pass. if (MIME_EXT[file.mimetype]) cb(null, true) else cb(new Error('Only image uploads are allowed')) }, }) // ── Dashboard & site mode ───────────────────────────────────────────── adminRouter.get( '/dashboard', // #swagger.tags = ['Admin · Dashboard'] // #swagger.summary = 'Dashboard summary counts' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Summary: site mode, last change, post/user counts and recent activity', content: { "application/json": { schema: { type: "object", properties: { site_mode: { type: "string", example: "live" }, last_change: { type: "object", properties: { at: { type: "string", nullable: true }, by: { type: "string", nullable: true } } }, counts: { type: "object", properties: { posts: { type: "object", additionalProperties: true }, users: { type: "integer" } } }, recent_activity: { type: "array", items: { type: "object", additionalProperties: true } } } } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ ctrl.dashboard, ) adminRouter.put( '/site-mode', // #swagger.tags = ['Admin · Dashboard'] // #swagger.summary = 'Set site mode (admin only)' // #swagger.description = 'Switch the site between live and maintenance.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/SiteModeRequest" } } } } */ /* #swagger.responses[200] = { description: 'Updated site mode', content: { "application/json": { schema: { $ref: "#/components/schemas/SiteModeState" } } } } */ /* #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" } } } } */ adminOnly, body('mode').isIn(['live', 'maintenance']), validate, ctrl.setSiteMode, ) // ── Posts (news / five-on-friday / newsletter / screenshots) ────────── adminRouter.get( '/posts', // #swagger.tags = ['Admin · Posts'] // #swagger.summary = 'List all posts (including unpublished)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['category'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Optional category filter.' } /* #swagger.responses[200] = { description: 'Posts', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Post" } } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ ctrl.listPosts, ) adminRouter.post( '/posts', // #swagger.tags = ['Admin · Posts'] // #swagger.summary = 'Create a post' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/PostCreateRequest" } } } } */ /* #swagger.responses[201] = { description: 'Created post', content: { "application/json": { schema: { $ref: "#/components/schemas/Post" } } } } */ /* #swagger.responses[400] = { description: 'Validation error or unknown category', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ body('category').isString().notEmpty(), body('title').isString().trim().notEmpty().isLength({ max: 200 }), validate, ctrl.createPost, ) adminRouter.post( '/posts/upload', // #swagger.tags = ['Admin · Posts'] // #swagger.summary = 'Upload a post image (multipart)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "multipart/form-data": { schema: { type: "object", properties: { image: { type: "string", format: "binary" } } } } } } */ /* #swagger.responses[201] = { description: 'Stored image URL', content: { "application/json": { schema: { type: "object", properties: { image_url: { type: "string", example: "/uploads/1700000000-abcd.png" } } } } } } */ /* #swagger.responses[400] = { description: 'No image / disallowed type', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ upload.single('image'), ctrl.uploadImage, ) // Generalized upload (rich-text editors). Same multer middleware; returns { url }. adminRouter.post( '/uploads', // #swagger.tags = ['Admin · Posts'] // #swagger.summary = 'Upload an image for rich-text editors (multipart)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "multipart/form-data": { schema: { type: "object", properties: { image: { type: "string", format: "binary" } } } } } } */ /* #swagger.responses[201] = { description: 'Stored file URL', content: { "application/json": { schema: { $ref: "#/components/schemas/UploadResponse" } } } } */ /* #swagger.responses[400] = { description: 'No file / disallowed type', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ upload.single('image'), ctrl.uploadFile, ) adminRouter.get( '/posts/:id', // #swagger.tags = ['Admin · Posts'] // #swagger.summary = 'Get a post by id' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Post id.' } /* #swagger.responses[200] = { description: 'The post', content: { "application/json": { schema: { $ref: "#/components/schemas/Post" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', 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.getPost, ) adminRouter.put( '/posts/:id', // #swagger.tags = ['Admin · Posts'] // #swagger.summary = 'Update a post' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Post id.' } /* #swagger.requestBody = { content: { "application/json": { schema: { $ref: "#/components/schemas/PostCreateRequest" } } } } */ /* #swagger.responses[200] = { description: 'Updated post', content: { "application/json": { schema: { $ref: "#/components/schemas/Post" } } } } */ /* #swagger.responses[400] = { description: 'Validation error or unknown category', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', 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.updatePost, ) adminRouter.patch( '/posts/:id/publish', // #swagger.tags = ['Admin · Posts'] // #swagger.summary = 'Publish / unpublish a post' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Post id.' } /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/PublishRequest" } } } } */ /* #swagger.responses[200] = { description: 'Updated post', content: { "application/json": { schema: { $ref: "#/components/schemas/Post" } } } } */ /* #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[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('id').isInt(), body('published').isBoolean(), validate, ctrl.publishPost, ) adminRouter.delete( '/posts/:id', // #swagger.tags = ['Admin · Posts'] // #swagger.summary = 'Delete a post' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Post id.' } /* #swagger.responses[200] = { description: 'Deleted (echoes the id)', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedId" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', 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.deletePost, ) adminRouter.get( '/posts/:id/announce', // #swagger.tags = ['Admin · Posts'] // #swagger.summary = 'Get the announcement pipeline status for a post' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Post id.' } /* #swagger.responses[200] = { description: 'The announce job for the post, or null if never announced', content: { "application/json": { schema: { type: "object", nullable: true, additionalProperties: true } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('id').isInt(), validate, ctrl.getAnnounceStatus, ) adminRouter.post( '/posts/:id/announce/retry', // #swagger.tags = ['Admin · Posts'] // #swagger.summary = 'Retry one announcement delivery leg (town crier or Discord)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Post id.' } /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { leg: { type: "string", enum: ["towncrier", "discord"] } }, required: ["leg"] } } } } */ /* #swagger.responses[200] = { description: 'Updated announce job', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[404] = { description: 'No announcement job for this post', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('id').isInt(), body('leg').isIn(['towncrier', 'discord']), validate, ctrl.retryAnnounceLeg, ) // ── Wiki categories (static paths registered before /wiki/:slug) ─────── adminRouter.get( '/wiki/categories', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'List wiki categories' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Wiki categories', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/WikiCategory" } } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ ctrl.listWikiCategories, ) adminRouter.post( '/wiki/categories', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'Create a wiki category' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/WikiCategoryCreateRequest" } } } } */ /* #swagger.responses[201] = { description: 'Created category', content: { "application/json": { schema: { $ref: "#/components/schemas/WikiCategory" } } } } */ /* #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[409] = { description: 'Slug already exists', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ body('slug').matches(/^[a-z0-9-]+$/), body('title').isString().trim().notEmpty().isLength({ max: 200 }), body('description').optional({ values: 'falsy' }).isString().isLength({ max: 400 }), body('sort_order').optional().isInt(), validate, ctrl.createWikiCategory, ) adminRouter.put( '/wiki/categories/:id', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'Update a wiki category' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Category id.' } /* #swagger.requestBody = { content: { "application/json": { schema: { $ref: "#/components/schemas/WikiCategoryCreateRequest" } } } } */ /* #swagger.responses[200] = { description: 'Updated category', content: { "application/json": { schema: { $ref: "#/components/schemas/WikiCategory" } } } } */ /* #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[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[409] = { description: 'Slug already exists', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('id').isInt(), body('slug').optional().matches(/^[a-z0-9-]+$/), body('title').optional().isString().trim().notEmpty().isLength({ max: 200 }), body('description').optional({ values: 'falsy' }).isString().isLength({ max: 400 }), body('sort_order').optional().isInt(), validate, ctrl.updateWikiCategory, ) adminRouter.delete( '/wiki/categories/:id', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'Delete a wiki category' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Category id.' } /* #swagger.responses[200] = { description: 'Deleted (echoes the id)', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedId" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', 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.deleteWikiCategory, ) // ── Wiki tags ────────────────────────────────────────────────────────── adminRouter.get( '/wiki/tags', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'List wiki tags' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Wiki tags', content: { "application/json": { schema: { type: "array", items: { type: "string" } } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ ctrl.listWikiTags, ) // ── Wiki pages ───────────────────────────────────────────────────────── adminRouter.get( '/wiki', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'List all wiki pages (including unpublished)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Wiki pages', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/WikiPage" } } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ ctrl.listWiki, ) adminRouter.post( '/wiki', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'Create a wiki page' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/WikiPageCreateRequest" } } } } */ /* #swagger.responses[201] = { description: 'Created wiki page', content: { "application/json": { schema: { $ref: "#/components/schemas/WikiPage" } } } } */ /* #swagger.responses[400] = { description: 'Validation error or unknown category', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[409] = { description: 'Slug already exists', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ body('slug').matches(/^[a-z0-9-]+$/), body('title').isString().trim().notEmpty().isLength({ max: 200 }), body('excerpt').optional({ values: 'falsy' }).isString().isLength({ max: 400 }), body('category_id').optional({ values: 'null' }).isInt(), body('published').optional().isBoolean(), body('tags').optional().isArray(), validate, ctrl.createWiki, ) adminRouter.get( '/wiki/:slug', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'Get a wiki page by slug' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['slug'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Wiki page slug.' } /* #swagger.responses[200] = { description: 'The wiki page', content: { "application/json": { schema: { $ref: "#/components/schemas/WikiPage" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ ctrl.getWiki, ) adminRouter.put( '/wiki/:slug', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'Update a wiki page (creates a revision)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['slug'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Wiki page slug.' } /* #swagger.requestBody = { content: { "application/json": { schema: { allOf: [ { $ref: "#/components/schemas/WikiPageCreateRequest" }, { type: "object", properties: { change_note: { type: "string", maxLength: 280 } } } ] } } } } */ /* #swagger.responses[200] = { description: 'Updated wiki page', content: { "application/json": { schema: { $ref: "#/components/schemas/WikiPage" } } } } */ /* #swagger.responses[400] = { description: 'Validation error or unknown category', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ body('title').optional().isString().trim().notEmpty().isLength({ max: 200 }), body('excerpt').optional({ values: 'falsy' }).isString().isLength({ max: 400 }), body('category_id').optional({ values: 'null' }).isInt(), body('published').optional().isBoolean(), body('tags').optional().isArray(), body('change_note').optional({ values: 'falsy' }).isString().isLength({ max: 280 }), validate, ctrl.updateWiki, ) adminRouter.patch( '/wiki/:slug/publish', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'Publish / unpublish a wiki page' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['slug'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Wiki page slug.' } /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/PublishRequest" } } } } */ /* #swagger.responses[200] = { description: 'Updated wiki page', content: { "application/json": { schema: { $ref: "#/components/schemas/WikiPage" } } } } */ /* #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[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ body('published').isBoolean(), validate, ctrl.publishWiki, ) adminRouter.get( '/wiki/:slug/revisions', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'List revisions of a wiki page' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['slug'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Wiki page slug.' } /* #swagger.responses[200] = { description: 'Revisions', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ ctrl.listWikiRevisions, ) adminRouter.get( '/wiki/:slug/revisions/:id', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'Get a single wiki revision' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['slug'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Wiki page slug.' } // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Revision id.' } /* #swagger.responses[200] = { description: 'The revision', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', 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.getWikiRevision, ) adminRouter.post( '/wiki/:slug/revisions/:id/restore', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'Restore a wiki page to a revision' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['slug'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Wiki page slug.' } // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Revision id to restore.' } /* #swagger.responses[200] = { description: 'Restored wiki page', content: { "application/json": { schema: { $ref: "#/components/schemas/WikiPage" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', 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.restoreWikiRevision, ) adminRouter.delete( '/wiki/:slug', // #swagger.tags = ['Admin · Wiki'] // #swagger.summary = 'Delete a wiki page' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['slug'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Wiki page slug.' } /* #swagger.responses[200] = { description: 'Deleted (echoes the slug)', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedSlug" } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ ctrl.deleteWiki, ) // ── CMS Pages (block-based page builder) ────────────────────────────── adminRouter.get( '/pages', // #swagger.tags = ['Admin · Pages'] // #swagger.summary = 'List all CMS pages (summaries)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Page summaries', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */ pagesCtrl.listPages, ) adminRouter.post( '/pages', // #swagger.tags = ['Admin · Pages'] // #swagger.summary = 'Create a CMS page' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { slug: { type: "string" }, title: { type: "string" }, status: { type: "string", enum: ["draft","published"] }, blocks: { type: "array", items: { type: "object" } }, metadata: { type: "object" }, settings: { type: "object" } } } } } } */ /* #swagger.responses[201] = { description: 'Created page', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[400] = { description: 'Invalid slug / title / blocks / metadata / settings', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[409] = { description: 'Slug already exists', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ body('slug').isString().trim().notEmpty(), body('title').isString().trim().notEmpty().isLength({ max: 200 }), validate, pagesCtrl.createPage, ) adminRouter.get( '/pages/:id', // #swagger.tags = ['Admin · Pages'] // #swagger.summary = 'Get a CMS page by id (full, incl. blocks)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Page id.' } /* #swagger.responses[200] = { description: 'The page', 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, pagesCtrl.getPage, ) adminRouter.patch( '/pages/:id', // #swagger.tags = ['Admin · Pages'] // #swagger.summary = 'Update a CMS page (title, status, blocks, metadata, settings)' // #swagger.description = 'slug is immutable; disabling protection is rejected here (use /unprotect).' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Page id.' } /* #swagger.requestBody = { content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[200] = { description: 'Updated page', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[400] = { description: 'Validation error (slug immutable, invalid blocks, etc.)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[403] = { description: 'Disabling protection requires /unprotect', 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, pagesCtrl.updatePage, ) adminRouter.delete( '/pages/:id', // #swagger.tags = ['Admin · Pages'] // #swagger.summary = 'Delete a CMS page (blocked if protected)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Page id.' } /* #swagger.responses[200] = { description: 'Deleted (echoes the id)', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedId" } } } } */ /* #swagger.responses[403] = { description: 'Page is protected', 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, pagesCtrl.deletePage, ) adminRouter.post( '/pages/:id/unprotect', // #swagger.tags = ['Admin · Pages'] // #swagger.summary = 'Disable page protection (password step-up re-auth)' // #swagger.description = 'Verifies the current admin password server-side, then flips protected → false.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Page id.' } /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { password: { type: "string" } }, required: ["password"] } } } } */ /* #swagger.responses[200] = { description: 'Updated page (protected=false)', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[401] = { description: 'Password incorrect', 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(), body('password').isString().notEmpty(), validate, pagesCtrl.unprotectPage, ) adminRouter.post( '/pages/:id/preview', // #swagger.tags = ['Admin · Pages'] // #swagger.summary = 'Mint a 1h draft-preview link for a page' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Page id.' } /* #swagger.responses[200] = { description: 'Preview token + path', content: { "application/json": { schema: { type: "object", properties: { token: { type: "string" }, expiresInSeconds: { type: "integer" }, path: { type: "string" } } } } } } */ /* #swagger.responses[404] = { description: 'Not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('id').isInt(), validate, pagesCtrl.createPreview, ) // ── Settings ────────────────────────────────────────────────────────── adminRouter.get( '/settings', // #swagger.tags = ['Admin · Settings'] // #swagger.summary = 'Get all site settings (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'All settings', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #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, ctrl.getSettings, ) adminRouter.put( '/settings', // #swagger.tags = ['Admin · Settings'] // #swagger.summary = 'Update site settings (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", additionalProperties: true, description: "An object of key/value settings." } } } } */ /* #swagger.responses[200] = { description: 'Updated settings', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[400] = { description: 'Body must be an object of key/value settings', 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" } } } } */ adminOnly, ctrl.updateSettings, ) // ── Activity log ────────────────────────────────────────────────────── adminRouter.get( '/activity', // #swagger.tags = ['Admin · Activity'] // #swagger.summary = 'List recent admin activity' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['limit'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Max rows to return.' } /* #swagger.responses[200] = { description: 'Activity entries', content: { "application/json": { schema: { type: "array", items: { type: "object", additionalProperties: true } } } } } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ ctrl.listActivity, ) // ── Bot activity (admin only) ───────────────────────────────────────── // Read-only view of the botScore middleware's in-memory scoring/ban state and // recent events, plus an emergency unban for false positives. adminRouter.get( '/bot-activity', // #swagger.tags = ['Admin · Bot Activity'] // #swagger.summary = 'Bot-scoring / ban state and recent events (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Banned IPs, scores and recent events', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #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, botActivity.getBotActivity, ) adminRouter.post( '/bot-activity/unban', // #swagger.tags = ['Admin · Bot Activity'] // #swagger.summary = 'Emergency unban an IP (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/UnbanRequest" } } } } */ /* #swagger.responses[200] = { description: 'Unbanned (echoes the ip and whether an entry was cleared)', content: { "application/json": { schema: { $ref: "#/components/schemas/UnbanResult" } } } } */ /* #swagger.responses[400] = { description: 'Invalid IP', 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" } } } } */ adminOnly, body('ip').isIP(), validate, botActivity.unbanIp, ) // ── Discord bot control (admin only) ────────────────────────────────── // Phase 1: entering/enabling the bot token here — never an env var. The token // is write-only over this API (SECURITY note in discordBot.controller.js). adminRouter.get( '/discord-bot/config', // #swagger.tags = ['Admin · Discord Bot'] // #swagger.summary = 'Get Discord bot config + live status (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Masked config + live status', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #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, discordBot.getConfig, ) adminRouter.put( '/discord-bot/config', // #swagger.tags = ['Admin · Discord Bot'] // #swagger.summary = 'Save Discord bot config (admin only)' // #swagger.description = 'token is write-only — omit/blank it to keep the existing one unchanged.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { guildId: { type: "string" }, token: { type: "string" }, enabled: { type: "boolean" } } } } } } */ /* #swagger.responses[200] = { description: 'Updated config + live status', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[400] = { description: 'Validation error, invalid token, or missing token while enabling', 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" } } } } */ adminOnly, body('guildId').optional({ values: 'falsy' }).isString().trim(), body('token').optional({ values: 'falsy' }).isString().trim(), body('enabled').optional().isBoolean(), validate, discordBot.saveConfig, ) // ── Email delivery (Gmail OAuth2, admin only) ───────────────────────── // Modern replacement for env SMTP: the refresh token is captured by the connect // flow and is write-only over this API (stored encrypted, never returned). adminRouter.get( '/email/config', // #swagger.tags = ['Admin · Email'] // #swagger.summary = 'Get email delivery config + status (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Config (refresh token stripped) + status', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #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, emailConfig.getConfig, ) adminRouter.put( '/email/config', // #swagger.tags = ['Admin · Email'] // #swagger.summary = 'Update email delivery config (admin only)' // #swagger.description = 'Set the From display name and enabled toggle. Enabling requires a connected Gmail account.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { content: { "application/json": { schema: { type: "object", properties: { senderName: { type: "string" }, enabled: { type: "boolean" } } } } } } */ /* #swagger.responses[200] = { description: 'Updated config', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[400] = { description: 'Cannot enable before connecting a mailbox', 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" } } } } */ adminOnly, body('senderName').optional({ values: 'null' }).isString().trim().isLength({ max: 120 }), body('enabled').optional().isBoolean(), validate, emailConfig.saveConfig, ) adminRouter.get( '/email/connect/start', // #swagger.tags = ['Admin · Email'] // #swagger.summary = 'Begin the Gmail OAuth2 connect flow (admin only)' // #swagger.description = 'Returns { url } to redirect the browser to Google. Reuses the google SSO OAuth client.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Authorization URL', content: { "application/json": { schema: { type: "object", properties: { url: { type: "string" } } } } } } */ /* #swagger.responses[400] = { description: 'Google OAuth client not configured', 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" } } } } */ adminOnly, emailConfig.connectStart, ) adminRouter.get( '/email/connect/callback', // #swagger.tags = ['Admin · Email'] // #swagger.summary = 'OAuth2 callback — stores the refresh token, redirects to Settings' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[302] = { description: 'Redirect back to /admin/settings' } */ adminOnly, emailConfig.connectCallback, ) adminRouter.post( '/email/test', // #swagger.tags = ['Admin · Email'] // #swagger.summary = 'Send a test email (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { content: { "application/json": { schema: { type: "object", properties: { to: { type: "string", format: "email" } } } } } } */ /* #swagger.responses[200] = { description: 'Sent', content: { "application/json": { schema: { type: "object", properties: { sent: { type: "boolean" }, to: { type: "string" } } } } } } */ /* #swagger.responses[502] = { description: 'Send failed / not configured', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ adminOnly, body('to').optional({ values: 'falsy' }).isEmail().isLength({ max: 255 }), validate, emailConfig.testSend, ) adminRouter.post( '/email/disconnect', // #swagger.tags = ['Admin · Email'] // #swagger.summary = 'Disconnect Gmail and disable email (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Disconnected config', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #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, 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). adminRouter.use('/moderation', modAccess) adminRouter.get( '/moderation/stats/summary', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Moderation action counts for 24h/7d/30d (admin or moderator)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.getSummary, ) adminRouter.get( '/moderation/recent', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Recent moderation actions, optionally filtered by type' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.getRecent, ) adminRouter.get( '/moderation/search', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Look up moderated users by Discord id or username snapshot' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.search, ) adminRouter.get( '/moderation/members', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Recent member join/leave events (optionally filtered by type)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.getMembers, ) adminRouter.get( '/moderation/filter-hits', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Recent automated content-filter hits' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.getFilterHits, ) adminRouter.get( '/moderation/spam-hits', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Recent automated spam-detection hits' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.getSpamHits, ) adminRouter.get( '/moderation/user/:discordId', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Per-user moderation summary (counts, latest tag, linked account)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] param('discordId').matches(/^[0-9]{1,32}$/), validate, moderation.getUser, ) adminRouter.get( '/moderation/user/:discordId/actions', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Full moderation action history for a user' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] param('discordId').matches(/^[0-9]{1,32}$/), validate, moderation.getUserActions, ) adminRouter.get( '/moderation/user/:discordId/notes', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Staff notes for a user (admin_only notes hidden from moderators)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] param('discordId').matches(/^[0-9]{1,32}$/), validate, moderation.getUserNotes, ) adminRouter.post( '/moderation/user/:discordId/notes', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Add a staff note (admin_only visibility requires the admin role)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] param('discordId').matches(/^[0-9]{1,32}$/), body('body').isString().trim().isLength({ min: 1, max: 4000 }), body('visibility').optional().isIn(['staff_only', 'admin_only']), validate, moderation.addUserNote, ) // ── 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, ) // ── 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 user’s 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 user’s 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 user’s 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 user’s 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 user’s 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 account’s 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). adminRouter.get( '/uo-link/config', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Get uo-link config + live status + ingestion stats (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Masked config, health and ingestion stats', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ adminOnly, uoLink.getConfig, ) adminRouter.put( '/uo-link/config', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Save uo-link connection config (admin only)' // #swagger.description = 'token is write-only — omit/blank it to keep the existing one. Saving (re)starts the WS ingest client.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { baseUrl: { type: "string" }, wsUrl: { type: "string" }, token: { type: "string" }, protocol: { type: "integer" }, enabled: { type: "boolean" } } } } } } */ /* #swagger.responses[200] = { description: 'Updated config + live status', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[400] = { description: 'Validation error, or missing token while enabling', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ adminOnly, body('baseUrl').optional({ values: 'falsy' }).isString().trim().isURL({ require_tld: false, protocols: ['http', 'https'] }), body('wsUrl').optional({ values: 'falsy' }).isString().trim().isURL({ require_tld: false, protocols: ['ws', 'wss'] }), body('token').optional({ values: 'falsy' }).isString().trim(), body('protocol').optional().isInt({ min: 1, max: 99 }), body('enabled').optional().isBoolean(), validate, uoLink.saveConfig, ) adminRouter.post( '/uo-link/towncrier', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Publish / replace a town-crier message (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TownCrierRequest" } } } } */ /* #swagger.responses[200] = { description: 'Posted', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[400] = { description: 'Rejected (over caps)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[503] = { description: 'Shard unavailable', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ adminOnly, body('id').isString().trim().isLength({ min: 1, max: 64 }), body('lines').isArray({ min: 1, max: 8 }), body('lines.*').isString().isLength({ max: 200 }), body('durationSec').optional().isInt({ min: 1, max: 86400 }), validate, uoLink.postTownCrier, ) adminRouter.delete( '/uo-link/towncrier/:id', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Remove a town-crier message (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Town-crier message id.' } /* #swagger.responses[200] = { description: 'Removed', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[404] = { description: 'Unknown id', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ adminOnly, param('id').isString().trim().isLength({ min: 1, max: 64 }), validate, uoLink.deleteTownCrier, ) adminRouter.get( '/uo-link/stream', // #swagger.tags = ['Admin · Shard'] // #swagger.summary = 'Full live shard event stream incl. audit/cheat (SSE, admin only)' /* #swagger.responses[200] = { description: 'An SSE stream (Content-Type: text/event-stream).' } */ adminOnly, uoLink.stream, ) module.exports = adminRouter