Merge pull request 'refactor(server): split admin posts, uploads, wiki and pages into capability routers (PR 3)' (#104) from refactor/admin-router-split-3 into main
Reviewed-on: #104 Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
This commit is contained in:
@@ -5,16 +5,12 @@
|
||||
// here are unchanged from when this file held all 110 admin routes.
|
||||
//
|
||||
// Already extracted: users, account, invites, auth/providers, moderation,
|
||||
// bot-activity, activity.
|
||||
// Still here: shard, dashboard, site-mode, posts, uploads, wiki, pages,
|
||||
// settings, discord-bot, email, uo-link.
|
||||
// bot-activity, activity, posts, uploads, wiki, pages.
|
||||
// Still here: shard, dashboard, site-mode, settings, discord-bot, email,
|
||||
// uo-link.
|
||||
// This file disappears when the last group moves.
|
||||
|
||||
const express = require('express')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const crypto = require('crypto')
|
||||
const multer = require('multer')
|
||||
const { body, param } = require('express-validator')
|
||||
|
||||
const ctrl = require('./admin.controller')
|
||||
@@ -23,7 +19,6 @@ const emailConfig = require('./emailConfig.controller')
|
||||
const uoLink = require('./uoLink.controller')
|
||||
const shardOps = require('./shardOps.controller')
|
||||
const selfShard = require('../player/shard.controller')
|
||||
const pagesCtrl = require('./pages.controller')
|
||||
const { requireRole } = require('../../../utils/auth')
|
||||
const validate = require('../../../middleware/validate')
|
||||
|
||||
@@ -238,40 +233,6 @@ adminRouter.get(
|
||||
shardOps.listHouses,
|
||||
)
|
||||
|
||||
// ── Image uploads (screenshots/gallery) ───────────────────────────────
|
||||
const UPLOAD_DIR =
|
||||
process.env.UPLOAD_DIR || path.join(__dirname, '..', '..', '..', '..', 'uploads')
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
||||
|
||||
// Whitelisted image mimetypes → the extension we store the file under. The
|
||||
// stored extension is derived from this map (keyed by the accepted mimetype),
|
||||
// never from originalname — so a spoofed `Content-Type: image/png` paired with
|
||||
// `originalname: x.html` can never land an executable .html file in /uploads.
|
||||
const MIME_EXT = {
|
||||
'image/png': '.png',
|
||||
'image/jpeg': '.jpg',
|
||||
'image/gif': '.gif',
|
||||
'image/webp': '.webp',
|
||||
'image/avif': '.avif',
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, UPLOAD_DIR),
|
||||
filename: (req, file, cb) => {
|
||||
const ext = MIME_EXT[file.mimetype] || ''
|
||||
cb(null, `${Date.now()}-${crypto.randomBytes(8).toString('hex')}${ext}`)
|
||||
},
|
||||
})
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 8 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
// Single source of truth: only mimetypes we can map to a safe extension pass.
|
||||
if (MIME_EXT[file.mimetype]) cb(null, true)
|
||||
else cb(new Error('Only image uploads are allowed'))
|
||||
},
|
||||
})
|
||||
|
||||
// ── Dashboard & site mode ─────────────────────────────────────────────
|
||||
adminRouter.get(
|
||||
'/dashboard',
|
||||
@@ -299,431 +260,6 @@ adminRouter.put(
|
||||
ctrl.setSiteMode,
|
||||
)
|
||||
|
||||
// ── Posts (news / five-on-friday / newsletter / screenshots) ──────────
|
||||
adminRouter.get(
|
||||
'/posts',
|
||||
// #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,
|
||||
)
|
||||
adminRouter.post(
|
||||
'/posts',
|
||||
// #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,
|
||||
)
|
||||
adminRouter.post(
|
||||
'/posts/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,
|
||||
)
|
||||
// Generalized upload (rich-text editors). Same multer middleware; returns { url }.
|
||||
adminRouter.post(
|
||||
'/uploads',
|
||||
// #swagger.tags = ['Admin · Posts']
|
||||
// #swagger.summary = 'Upload an image for rich-text editors (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 file URL', content: { "application/json": { schema: { $ref: "#/components/schemas/UploadResponse" } } } } */
|
||||
/* #swagger.responses[400] = { description: 'No file / 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.uploadFile,
|
||||
)
|
||||
adminRouter.get(
|
||||
'/posts/: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,
|
||||
)
|
||||
adminRouter.put(
|
||||
'/posts/: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,
|
||||
)
|
||||
adminRouter.patch(
|
||||
'/posts/: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,
|
||||
)
|
||||
adminRouter.delete(
|
||||
'/posts/: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,
|
||||
)
|
||||
adminRouter.get(
|
||||
'/posts/: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,
|
||||
)
|
||||
adminRouter.post(
|
||||
'/posts/: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,
|
||||
)
|
||||
|
||||
// ── Wiki categories (static paths registered before /wiki/:slug) ───────
|
||||
adminRouter.get(
|
||||
'/wiki/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,
|
||||
)
|
||||
adminRouter.post(
|
||||
'/wiki/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,
|
||||
)
|
||||
adminRouter.put(
|
||||
'/wiki/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,
|
||||
)
|
||||
adminRouter.delete(
|
||||
'/wiki/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 ──────────────────────────────────────────────────────────
|
||||
adminRouter.get(
|
||||
'/wiki/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 ─────────────────────────────────────────────────────────
|
||||
adminRouter.get(
|
||||
'/wiki',
|
||||
// #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,
|
||||
)
|
||||
adminRouter.post(
|
||||
'/wiki',
|
||||
// #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,
|
||||
)
|
||||
adminRouter.get(
|
||||
'/wiki/: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,
|
||||
)
|
||||
adminRouter.put(
|
||||
'/wiki/: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,
|
||||
)
|
||||
adminRouter.patch(
|
||||
'/wiki/: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,
|
||||
)
|
||||
adminRouter.get(
|
||||
'/wiki/: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,
|
||||
)
|
||||
adminRouter.get(
|
||||
'/wiki/: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,
|
||||
)
|
||||
adminRouter.post(
|
||||
'/wiki/: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,
|
||||
)
|
||||
adminRouter.delete(
|
||||
'/wiki/: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,
|
||||
)
|
||||
|
||||
// ── CMS Pages (block-based page builder) ──────────────────────────────
|
||||
adminRouter.get(
|
||||
'/pages',
|
||||
// #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,
|
||||
)
|
||||
adminRouter.post(
|
||||
'/pages',
|
||||
// #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,
|
||||
)
|
||||
adminRouter.get(
|
||||
'/pages/: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,
|
||||
)
|
||||
adminRouter.patch(
|
||||
'/pages/: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,
|
||||
)
|
||||
adminRouter.delete(
|
||||
'/pages/: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,
|
||||
)
|
||||
adminRouter.post(
|
||||
'/pages/: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,
|
||||
)
|
||||
adminRouter.post(
|
||||
'/pages/: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,
|
||||
)
|
||||
|
||||
// ── Settings ──────────────────────────────────────────────────────────
|
||||
adminRouter.get(
|
||||
'/settings',
|
||||
|
||||
49
server/src/router/v1/admin/imageUpload.js
Normal file
49
server/src/router/v1/admin/imageUpload.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// Shared multer middleware for the two admin image-upload routes:
|
||||
// POST /admin/posts/upload (posts.router.js) and POST /admin/uploads
|
||||
// (uploads.router.js). It lived inline in admin.routes.js while both routes did;
|
||||
// the PR 3 split put them in different files, so the config moved here rather
|
||||
// than being duplicated — one upload directory, one mimetype allowlist.
|
||||
//
|
||||
// Kept in this directory on purpose: UPLOAD_DIR is resolved relative to
|
||||
// __dirname, so moving the file to another folder would silently repoint the
|
||||
// upload directory.
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const crypto = require('crypto')
|
||||
const multer = require('multer')
|
||||
|
||||
const UPLOAD_DIR =
|
||||
process.env.UPLOAD_DIR || path.join(__dirname, '..', '..', '..', '..', 'uploads')
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
||||
|
||||
// Whitelisted image mimetypes → the extension we store the file under. The
|
||||
// stored extension is derived from this map (keyed by the accepted mimetype),
|
||||
// never from originalname — so a spoofed `Content-Type: image/png` paired with
|
||||
// `originalname: x.html` can never land an executable .html file in /uploads.
|
||||
const MIME_EXT = {
|
||||
'image/png': '.png',
|
||||
'image/jpeg': '.jpg',
|
||||
'image/gif': '.gif',
|
||||
'image/webp': '.webp',
|
||||
'image/avif': '.avif',
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, UPLOAD_DIR),
|
||||
filename: (req, file, cb) => {
|
||||
const ext = MIME_EXT[file.mimetype] || ''
|
||||
cb(null, `${Date.now()}-${crypto.randomBytes(8).toString('hex')}${ext}`)
|
||||
},
|
||||
})
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 8 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
// Single source of truth: only mimetypes we can map to a safe extension pass.
|
||||
if (MIME_EXT[file.mimetype]) cb(null, true)
|
||||
else cb(new Error('Only image uploads are allowed'))
|
||||
},
|
||||
})
|
||||
|
||||
module.exports = { upload, UPLOAD_DIR, MIME_EXT }
|
||||
@@ -20,6 +20,10 @@ const authProvidersRouter = require('./authProviders.router')
|
||||
const moderationRouter = require('./moderation.router')
|
||||
const botActivityRouter = require('./botActivity.router')
|
||||
const activityRouter = require('./activity.router')
|
||||
const postsRouter = require('./posts.router')
|
||||
const uploadsRouter = require('./uploads.router')
|
||||
const wikiRouter = require('./wiki.router')
|
||||
const pagesRouter = require('./pages.router')
|
||||
const residualRouter = require('./admin.routes')
|
||||
|
||||
const adminRouter = express.Router()
|
||||
@@ -46,6 +50,12 @@ adminRouter.use('/auth', authProvidersRouter)
|
||||
adminRouter.use('/moderation', moderationRouter)
|
||||
adminRouter.use('/bot-activity', botActivityRouter)
|
||||
adminRouter.use('/activity', activityRouter)
|
||||
// Content, all editor-tier (no gate beyond staffOnly above). /uploads is the
|
||||
// rich-text editors' generalized upload; /posts owns its own /posts/upload.
|
||||
adminRouter.use('/posts', postsRouter)
|
||||
adminRouter.use('/uploads', uploadsRouter)
|
||||
adminRouter.use('/wiki', wikiRouter)
|
||||
adminRouter.use('/pages', pagesRouter)
|
||||
|
||||
// Everything not yet extracted, at the group root. Mounted last, but none of the
|
||||
// prefixes above appear in it, so nothing here depends on the ordering.
|
||||
|
||||
114
server/src/router/v1/admin/pages.router.js
Normal file
114
server/src/router/v1/admin/pages.router.js
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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
|
||||
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
|
||||
33
server/src/router/v1/admin/uploads.router.js
Normal file
33
server/src/router/v1/admin/uploads.router.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Admin · Uploads — the generalized image upload used by the rich-text editors
|
||||
// (wiki, CMS pages). Returns { url }, where the posts-specific sibling
|
||||
// POST /admin/posts/upload returns { image_url }; both write to the same
|
||||
// directory through the shared multer config in imageUpload.js.
|
||||
//
|
||||
// Mounted at /api/v1/admin/uploads by admin/index.js, which already applied
|
||||
// `noindex, isLoggedIn, staffOnly`. No extra gate — same editor tier as posts.
|
||||
//
|
||||
// The swagger tag stays 'Admin · Posts', matching the committed spec. Retagging
|
||||
// it would be a real OpenAPI diff, not a route move, so it does not belong in a
|
||||
// split PR whose acceptance criterion is a byte-identical spec.
|
||||
|
||||
const express = require('express')
|
||||
|
||||
const ctrl = require('./admin.controller')
|
||||
const { upload } = require('./imageUpload')
|
||||
|
||||
const uploadsRouter = express.Router()
|
||||
|
||||
uploadsRouter.post(
|
||||
'/',
|
||||
// #swagger.tags = ['Admin · Posts']
|
||||
// #swagger.summary = 'Upload an image for rich-text editors (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 file URL', content: { "application/json": { schema: { $ref: "#/components/schemas/UploadResponse" } } } } */
|
||||
/* #swagger.responses[400] = { description: 'No file / 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.uploadFile,
|
||||
)
|
||||
|
||||
module.exports = uploadsRouter
|
||||
220
server/src/router/v1/admin/wiki.router.js
Normal file
220
server/src/router/v1/admin/wiki.router.js
Normal file
@@ -0,0 +1,220 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user