PR 3 of the in-place admin router split (docs/website/API_V2_PLAN.md § Phase 2). Moves the content tier out of the residual admin.routes.js into one router file per capability, each mounted at the prefix it already owned. No URL, gate or handler changes. posts.router.js ( 9) /admin/posts uploads.router.js ( 1) /admin/uploads wiki.router.js (14) /admin/wiki pages.router.js ( 7) /admin/pages admin.routes.js (33) residual, was 64 All four capabilities are editor tier, so no gate moved: the shared `noindex, isLoggedIn, staffOnly` in admin/index.js is their whole gate. The multer config moved to admin/imageUpload.js because the two routes that share it (POST /posts/upload and POST /uploads) now live in different files; duplicating a mimetype allowlist is how the two copies drift. It stays in admin/ because UPLOAD_DIR is resolved relative to __dirname. Acceptance — all four gates zero-diff: routes.manifest.json unchanged (200 public + 2 internal) routes.guards.json unchanged (no route lost or gained a gate) swagger-output.json unchanged (198 operations) api-route-inventory.json already in sync plus 434 server tests green. Verified separately, because no gate can catch it: the wiki router's literal /categories and /tags paths still precede /:slug in declaration order. The manifest sorts its entries, so a reordering there would be invisible. Co-Authored-By: Claude <noreply@anthropic.com>
115 lines
7.2 KiB
JavaScript
115 lines
7.2 KiB
JavaScript
// Admin · Pages — the block-based CMS page builder: drafts, protection, and
|
|
// short-lived preview links.
|
|
//
|
|
// Mounted at /api/v1/admin/pages by admin/index.js, which already applied
|
|
// `noindex, isLoggedIn, staffOnly`. No extra gate — editors build pages. Note
|
|
// that protection is *not* a role gate: POST /:id/unprotect re-verifies the
|
|
// caller's password server-side (see pages.controller.js).
|
|
//
|
|
// Unrelated to /admin/shard/pages, which is the in-game help-page (support)
|
|
// queue and stays with the shard capability.
|
|
//
|
|
// Handlers live in pages.controller.js; this PR re-wires routes, not logic.
|
|
|
|
const express = require('express')
|
|
const { body, param } = require('express-validator')
|
|
|
|
const pagesCtrl = require('./pages.controller')
|
|
const validate = require('../../../middleware/validate')
|
|
|
|
const pagesRouter = express.Router()
|
|
|
|
pagesRouter.get(
|
|
'/',
|
|
// #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,
|
|
)
|
|
pagesRouter.post(
|
|
'/',
|
|
// #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,
|
|
)
|
|
pagesRouter.get(
|
|
'/: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,
|
|
)
|
|
pagesRouter.patch(
|
|
'/: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,
|
|
)
|
|
pagesRouter.delete(
|
|
'/: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,
|
|
)
|
|
pagesRouter.post(
|
|
'/: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,
|
|
)
|
|
pagesRouter.post(
|
|
'/: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,
|
|
)
|
|
|
|
module.exports = pagesRouter
|