// Admin · Shard — everything under /api/v1/admin/shard, in two tiers. // // Mounted at /api/v1/admin/shard by admin/index.js, which already applied // `noindex, isLoggedIn, staffOnly`. Two capabilities share this prefix, and // prefix ownership is the invariant the split preserves — so they share a file: // // 1. Self-service game-account linking (no extra gate). A staff member links // and inspects their OWN in-game account exactly as a player does under // /player/shard; the handlers are the very same `player/shard.controller` // ones, keyed off req.user.id. These keep their `Admin · Account` swagger // tag, which is why the tag disagrees with this filename. // 2. Privileged live-shard operations and the help-page queue (`modAccess` — // admin or moderator). `actor` is stamped server-side from the session in // shardOps.controller.js; the request body never carries it. // // `modAccess` stays a per-route gate rather than a router-level `use`: it was // per-route in admin.routes.js, and half the routes here must NOT have it. // // NOTE: /admin/shard/pages is the in-game help-page (support) queue. It is // unrelated to /admin/pages, the CMS page builder. const express = require('express') const { body, param } = require('express-validator') const shardOps = require('./shardOps.controller') const selfShard = require('../player/shard.controller') const { requireRole } = require('../../../utils/auth') const validate = require('../../../middleware/validate') const shardRouter = express.Router() // Moderator gate. Admins can do everything a moderator can. const modAccess = requireRole('admin', 'moderator') // ── 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}$/ shardRouter.post( '/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, ) shardRouter.get( '/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, ) shardRouter.get( '/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, ) shardRouter.get( '/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, ) shardRouter.get( '/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, ) shardRouter.get( '/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, ) shardRouter.post( '/account', // #swagger.tags = ['Admin · Account'] // #swagger.summary = 'Create a game account and link it to the caller (staff self-service)' // #swagger.description = 'Same as POST /player/shard/account but for a signed-in staff user — provisions a game account (own username + password) and links it. Gated by game_account_signup + the shard’s mode; the password is never stored or logged.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", required: ["account","password"], properties: { account: { type: "string" }, password: { type: "string" } } } } } */ /* #swagger.responses[201] = { description: 'Account created and linked', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[403] = { description: 'Game-account signup unavailable (site or shard)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[409] = { description: 'Account name already taken', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ body('account').matches(/^[A-Za-z0-9][A-Za-z0-9_.-]{2,29}$/), body('password').isString().isLength({ min: 8, max: 64 }), validate, selfShard.createGameAccount, ) // ── 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. shardRouter.post( '/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, ) shardRouter.post( '/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, ) shardRouter.post( '/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, ) shardRouter.post( '/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, ) shardRouter.get( '/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, ) shardRouter.post( '/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, ) shardRouter.post( '/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, ) shardRouter.get( '/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, ) shardRouter.get( '/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, ) module.exports = shardRouter