refactor(server): split admin posts, uploads, wiki and pages into capability routers
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>
This commit is contained in:
139
server/src/router/v1/admin/posts.router.js
Normal file
139
server/src/router/v1/admin/posts.router.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// Admin · Posts — news, five-on-friday, newsletter and screenshot posts, plus
|
||||
// the announcement pipeline (town crier + Discord) status and retry.
|
||||
//
|
||||
// Mounted at /api/v1/admin/posts by admin/index.js, which already applied
|
||||
// `noindex, isLoggedIn, staffOnly`. No extra gate: managing content is the
|
||||
// editor tier's whole job, so admin, editor and moderator all reach these.
|
||||
//
|
||||
// Handlers still live in admin.controller.js; this PR re-wires routes, not logic.
|
||||
|
||||
const express = require('express')
|
||||
const { body, param } = require('express-validator')
|
||||
|
||||
const ctrl = require('./admin.controller')
|
||||
const { upload } = require('./imageUpload')
|
||||
const validate = require('../../../middleware/validate')
|
||||
|
||||
const postsRouter = express.Router()
|
||||
|
||||
postsRouter.get(
|
||||
'/',
|
||||
// #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,
|
||||
)
|
||||
postsRouter.post(
|
||||
'/',
|
||||
// #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,
|
||||
)
|
||||
postsRouter.post(
|
||||
'/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,
|
||||
)
|
||||
postsRouter.get(
|
||||
'/: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,
|
||||
)
|
||||
postsRouter.put(
|
||||
'/: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,
|
||||
)
|
||||
postsRouter.patch(
|
||||
'/: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,
|
||||
)
|
||||
postsRouter.delete(
|
||||
'/: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,
|
||||
)
|
||||
postsRouter.get(
|
||||
'/: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,
|
||||
)
|
||||
postsRouter.post(
|
||||
'/: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,
|
||||
)
|
||||
|
||||
module.exports = postsRouter
|
||||
Reference in New Issue
Block a user