// Admin · Moderation — the moderation dashboard and the appeals queue. // // Mounted at /api/v1/admin/moderation by admin/index.js, which already applied // `noindex, isLoggedIn, staffOnly`. Read-only views over the Discord bot's // mod_actions log, plus staff notes and staff triage of player-submitted // ban/mute appeals. // // The whole capability is gated for the moderator role (admins included), so the // gate is a router-level `use` — exactly equivalent to the old // `adminRouter.use('/moderation', modAccess)` now that this router is mounted at // a prefix. Editors get 403 here. // // Handlers still live in moderation.controller.js; this re-wires routes, not logic. const express = require('express') const { body, param } = require('express-validator') const moderation = require('./moderation.controller') const { requireRole } = require('../../../utils/auth') const validate = require('../../../middleware/validate') const moderationRouter = express.Router() const modAccess = requireRole('admin', 'moderator') moderationRouter.use(modAccess) moderationRouter.get( '/stats/summary', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Moderation action counts for 24h/7d/30d (admin or moderator)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.getSummary, ) moderationRouter.get( '/recent', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Recent moderation actions, optionally filtered by type' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.getRecent, ) moderationRouter.get( '/search', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Look up moderated users by Discord id or username snapshot' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.search, ) moderationRouter.get( '/members', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Recent member join/leave events (optionally filtered by type)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.getMembers, ) moderationRouter.get( '/filter-hits', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Recent automated content-filter hits' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.getFilterHits, ) moderationRouter.get( '/spam-hits', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Recent automated spam-detection hits' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] moderation.getSpamHits, ) moderationRouter.get( '/user/:discordId', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Per-user moderation summary (counts, latest tag, linked account)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] param('discordId').matches(/^\d{1,32}$/), validate, moderation.getUser, ) moderationRouter.get( '/user/:discordId/actions', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Full moderation action history for a user' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] param('discordId').matches(/^\d{1,32}$/), validate, moderation.getUserActions, ) moderationRouter.get( '/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(/^\d{1,32}$/), validate, moderation.getUserNotes, ) moderationRouter.post( '/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(/^\d{1,32}$/), body('body').isString().trim().isLength({ min: 1, max: 4000 }), body('visibility').optional().isIn(['staff_only', 'admin_only']), validate, moderation.addUserNote, ) // ── Appeals queue (Phase 6c) ────────────────────────────────────────── // Approving an appeal can trigger an automatic Discord reversal (Phase 6d) — // see resolveAppeal. moderationRouter.get( '/appeals', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'List moderation appeals (default: pending + under_review)' // #swagger.description = 'Filter with ?status= or ?status=all. Paginated with ?limit&offset.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Appeals queue', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/AppealQueueItem" } } } } } */ moderation.getAppeals, ) moderationRouter.get( '/appeals/:id', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Get a single moderation appeal' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Appeal id.' } /* #swagger.responses[200] = { description: 'The appeal', content: { "application/json": { schema: { $ref: "#/components/schemas/AppealQueueItem" } } } } */ /* #swagger.responses[404] = { description: 'Appeal not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('id').isInt({ min: 1 }), validate, moderation.getAppeal, ) moderationRouter.post( '/appeals/:id/claim', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Claim a pending appeal (→ under_review)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Appeal id.' } /* #swagger.responses[200] = { description: 'The claimed appeal', content: { "application/json": { schema: { $ref: "#/components/schemas/AppealQueueItem" } } } } */ /* #swagger.responses[404] = { description: 'Appeal not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[409] = { description: 'Appeal is not open for claiming', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('id').isInt({ min: 1 }), validate, moderation.claimAppeal, ) moderationRouter.post( '/appeals/:id/resolve', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Resolve an appeal (approved | denied); approval may auto-reverse the Discord action' // #swagger.description = 'Approving a ban/mute appeal best-effort asks the bot to reverse the Discord action (unban / clear timeout). The bot being down never fails the resolution — reversal_status is recorded as failed. The response echoes the updated appeal plus a `reversal` object.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'Appeal id.' } /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ResolveAppealRequest" } } } } */ /* #swagger.responses[200] = { description: 'The resolved appeal (with reversal outcome)', content: { "application/json": { schema: { $ref: "#/components/schemas/AppealResolveResult" } } } } */ /* #swagger.responses[400] = { description: 'Validation error (status must be approved or denied)', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */ /* #swagger.responses[404] = { description: 'Appeal not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[409] = { description: 'Appeal is already resolved', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('id').isInt({ min: 1 }), body('status').isIn(['approved', 'denied']), body('staff_response').optional({ values: 'falsy' }).isString().trim().isLength({ max: 4000 }), validate, moderation.resolveAppeal, ) moderationRouter.get( '/user/:discordId/appeals', // #swagger.tags = ['Admin · Moderation'] // #swagger.summary = 'Appeals submitted for a Discord user' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['discordId'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Discord snowflake.' } /* #swagger.responses[200] = { description: 'Appeals for the user', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/AppealQueueItem" } } } } } */ param('discordId').matches(/^\d{1,32}$/), validate, moderation.getUserAppeals, ) module.exports = moderationRouter