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>
221 lines
15 KiB
JavaScript
221 lines
15 KiB
JavaScript
// Admin · Wiki — wiki pages with revision history, plus the category and tag
|
|
// vocabularies they draw on.
|
|
//
|
|
// Mounted at /api/v1/admin/wiki by admin/index.js, which already applied
|
|
// `noindex, isLoggedIn, staffOnly`. No extra gate — editors own the wiki.
|
|
//
|
|
// Handlers still live in admin.controller.js; this PR re-wires routes, not logic.
|
|
//
|
|
// ORDER IS LOAD-BEARING: the static /categories and /tags paths must stay ahead
|
|
// of /:slug, or `GET /admin/wiki/categories` would be dispatched as a page whose
|
|
// slug is "categories". The route manifest sorts its entries, so it cannot catch
|
|
// a reordering here — keep the declaration order below as it is.
|
|
|
|
const express = require('express')
|
|
const { body, param } = require('express-validator')
|
|
|
|
const ctrl = require('./admin.controller')
|
|
const validate = require('../../../middleware/validate')
|
|
|
|
const wikiRouter = express.Router()
|
|
|
|
// ── Wiki categories (static paths registered before /:slug) ────────────
|
|
wikiRouter.get(
|
|
'/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,
|
|
)
|
|
wikiRouter.post(
|
|
'/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,
|
|
)
|
|
wikiRouter.put(
|
|
'/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,
|
|
)
|
|
wikiRouter.delete(
|
|
'/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 ──────────────────────────────────────────────────────────
|
|
wikiRouter.get(
|
|
'/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 ─────────────────────────────────────────────────────────
|
|
wikiRouter.get(
|
|
'/',
|
|
// #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,
|
|
)
|
|
wikiRouter.post(
|
|
'/',
|
|
// #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,
|
|
)
|
|
wikiRouter.get(
|
|
'/: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,
|
|
)
|
|
wikiRouter.put(
|
|
'/: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,
|
|
)
|
|
wikiRouter.patch(
|
|
'/: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,
|
|
)
|
|
wikiRouter.get(
|
|
'/: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,
|
|
)
|
|
wikiRouter.get(
|
|
'/: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,
|
|
)
|
|
wikiRouter.post(
|
|
'/: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,
|
|
)
|
|
wikiRouter.delete(
|
|
'/: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,
|
|
)
|
|
|
|
module.exports = wikiRouter
|