|
|
|
|
@@ -24,25 +24,72 @@ const adminOnly = requireRole('admin')
|
|
|
|
|
|
|
|
|
|
// ── Account security (self-service, any logged-in role) ───────────────
|
|
|
|
|
// Not behind adminOnly: an editor manages their own 2FA too.
|
|
|
|
|
adminRouter.get('/account', account.getAccount)
|
|
|
|
|
adminRouter.post('/account/totp/setup', account.totpSetup)
|
|
|
|
|
adminRouter.get(
|
|
|
|
|
'/account',
|
|
|
|
|
// #swagger.tags = ['Admin · Account']
|
|
|
|
|
// #swagger.summary = 'Get the current account (self)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.responses[200] = { description: 'The account', content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
account.getAccount,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.post(
|
|
|
|
|
'/account/totp/setup',
|
|
|
|
|
// #swagger.tags = ['Admin · Account']
|
|
|
|
|
// #swagger.summary = 'Begin 2FA enrollment (returns secret + QR)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.responses[200] = { description: 'otpauth URL and QR data to scan', content: { "application/json": { schema: { type: "object", properties: { otpauth_url: { type: "string" }, qr: { type: "string" } } } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[409] = { description: 'Two-factor already enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
account.totpSetup,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.post(
|
|
|
|
|
'/account/totp/enable',
|
|
|
|
|
// #swagger.tags = ['Admin · Account']
|
|
|
|
|
// #swagger.summary = 'Enable 2FA by confirming a code'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TotpCodeRequest" } } } } */
|
|
|
|
|
/* #swagger.responses[200] = { description: '2FA enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
|
|
|
|
/* #swagger.responses[400] = { description: 'Setup not started, or invalid code', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[409] = { description: 'Two-factor already enabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
body('code').isString().trim().isLength({ min: 6, max: 8 }),
|
|
|
|
|
validate,
|
|
|
|
|
account.totpEnable,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.post(
|
|
|
|
|
'/account/totp/disable',
|
|
|
|
|
// #swagger.tags = ['Admin · Account']
|
|
|
|
|
// #swagger.summary = 'Disable 2FA by confirming a code'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TotpCodeRequest" } } } } */
|
|
|
|
|
/* #swagger.responses[200] = { description: '2FA disabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
|
|
|
|
/* #swagger.responses[400] = { description: 'Not enabled, or invalid code', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
body('code').isString().trim().isLength({ min: 6, max: 8 }),
|
|
|
|
|
validate,
|
|
|
|
|
account.totpDisable,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Linked SSO identities (self-service — any logged-in role manages their own).
|
|
|
|
|
adminRouter.get('/account/identities', account.listIdentities)
|
|
|
|
|
adminRouter.get(
|
|
|
|
|
'/account/identities',
|
|
|
|
|
// #swagger.tags = ['Admin · Account']
|
|
|
|
|
// #swagger.summary = 'List linked SSO identities (self)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Linked identities', content: { "application/json": { schema: { type: "array", items: { type: "object", properties: { provider: { type: "string" }, email: { type: "string" } } } } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
account.listIdentities,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.delete(
|
|
|
|
|
'/account/identities/:provider',
|
|
|
|
|
// #swagger.tags = ['Admin · Account']
|
|
|
|
|
// #swagger.summary = 'Unlink an SSO identity (self)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
// #swagger.parameters['provider'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Unlinked', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[404] = { description: 'No linked account for that provider', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
param('provider').matches(/^[a-z0-9-]+$/),
|
|
|
|
|
validate,
|
|
|
|
|
account.unlinkIdentity,
|
|
|
|
|
@@ -83,9 +130,26 @@ const upload = multer({
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// ── Dashboard & site mode ─────────────────────────────────────────────
|
|
|
|
|
adminRouter.get('/dashboard', ctrl.dashboard)
|
|
|
|
|
adminRouter.get(
|
|
|
|
|
'/dashboard',
|
|
|
|
|
// #swagger.tags = ['Admin · Dashboard']
|
|
|
|
|
// #swagger.summary = 'Dashboard summary counts'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Summary counts (posts, wiki, users, site mode)', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
ctrl.dashboard,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.put(
|
|
|
|
|
'/site-mode',
|
|
|
|
|
// #swagger.tags = ['Admin · Dashboard']
|
|
|
|
|
// #swagger.summary = 'Set site mode (admin only)'
|
|
|
|
|
// #swagger.description = 'Switch the site between live and maintenance.'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/SiteModeRequest" } } } } */
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Updated site mode', content: { "application/json": { schema: { type: "object", properties: { mode: { type: "string", example: "maintenance" } } } } } } */
|
|
|
|
|
/* #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[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
adminOnly,
|
|
|
|
|
body('mode').isIn(['live', 'maintenance']),
|
|
|
|
|
validate,
|
|
|
|
|
@@ -93,32 +157,133 @@ adminRouter.put(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ── Posts (news / five-on-friday / newsletter / screenshots) ──────────
|
|
|
|
|
adminRouter.get('/posts', ctrl.listPosts)
|
|
|
|
|
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', upload.single('image'), ctrl.uploadImage)
|
|
|
|
|
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', upload.single('image'), ctrl.uploadFile)
|
|
|
|
|
adminRouter.get('/posts/:id', param('id').isInt(), validate, ctrl.getPost)
|
|
|
|
|
adminRouter.put('/posts/:id', param('id').isInt(), validate, ctrl.updatePost)
|
|
|
|
|
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', param('id').isInt(), validate, ctrl.deletePost)
|
|
|
|
|
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', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
|
|
|
|
/* #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,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ── Wiki categories (static paths registered before /wiki/:slug) ───────
|
|
|
|
|
adminRouter.get('/wiki/categories', ctrl.listWikiCategories)
|
|
|
|
|
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 }),
|
|
|
|
|
@@ -128,6 +293,16 @@ adminRouter.post(
|
|
|
|
|
)
|
|
|
|
|
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 }),
|
|
|
|
|
@@ -136,15 +311,51 @@ adminRouter.put(
|
|
|
|
|
validate,
|
|
|
|
|
ctrl.updateWikiCategory,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.delete('/wiki/categories/:id', param('id').isInt(), validate, ctrl.deleteWikiCategory)
|
|
|
|
|
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', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
|
|
|
|
/* #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', ctrl.listWikiTags)
|
|
|
|
|
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', ctrl.listWiki)
|
|
|
|
|
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 }),
|
|
|
|
|
@@ -154,9 +365,28 @@ adminRouter.post(
|
|
|
|
|
validate,
|
|
|
|
|
ctrl.createWiki,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.get('/wiki/:slug', ctrl.getWiki)
|
|
|
|
|
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(),
|
|
|
|
|
@@ -168,33 +398,132 @@ adminRouter.put(
|
|
|
|
|
)
|
|
|
|
|
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', ctrl.listWikiRevisions)
|
|
|
|
|
adminRouter.get('/wiki/:slug/revisions/:id', param('id').isInt(), validate, ctrl.getWikiRevision)
|
|
|
|
|
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', ctrl.deleteWiki)
|
|
|
|
|
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', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
|
|
|
|
/* #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,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ── Settings ──────────────────────────────────────────────────────────
|
|
|
|
|
adminRouter.get('/settings', adminOnly, ctrl.getSettings)
|
|
|
|
|
adminRouter.put('/settings', adminOnly, ctrl.updateSettings)
|
|
|
|
|
adminRouter.get(
|
|
|
|
|
'/settings',
|
|
|
|
|
// #swagger.tags = ['Admin · Settings']
|
|
|
|
|
// #swagger.summary = 'Get all site settings (admin only)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.responses[200] = { description: 'All settings', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
adminOnly,
|
|
|
|
|
ctrl.getSettings,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.put(
|
|
|
|
|
'/settings',
|
|
|
|
|
// #swagger.tags = ['Admin · Settings']
|
|
|
|
|
// #swagger.summary = 'Update site settings (admin only)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", additionalProperties: true, description: "An object of key/value settings." } } } } */
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Updated settings', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
|
|
|
|
|
/* #swagger.responses[400] = { description: 'Body must be an object of key/value settings', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
adminOnly,
|
|
|
|
|
ctrl.updateSettings,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ── Activity log ──────────────────────────────────────────────────────
|
|
|
|
|
adminRouter.get('/activity', ctrl.listActivity)
|
|
|
|
|
adminRouter.get(
|
|
|
|
|
'/activity',
|
|
|
|
|
// #swagger.tags = ['Admin · Activity']
|
|
|
|
|
// #swagger.summary = 'List recent admin activity'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
// #swagger.parameters['limit'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Max rows to return.' }
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Activity entries', 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" } } } } */
|
|
|
|
|
ctrl.listActivity,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ── Bot activity (admin only) ─────────────────────────────────────────
|
|
|
|
|
// Read-only view of the botScore middleware's in-memory scoring/ban state and
|
|
|
|
|
// recent events, plus an emergency unban for false positives.
|
|
|
|
|
adminRouter.get('/bot-activity', adminOnly, botActivity.getBotActivity)
|
|
|
|
|
adminRouter.get(
|
|
|
|
|
'/bot-activity',
|
|
|
|
|
// #swagger.tags = ['Admin · Bot Activity']
|
|
|
|
|
// #swagger.summary = 'Bot-scoring / ban state and recent events (admin only)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Banned IPs, scores and recent events', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
adminOnly,
|
|
|
|
|
botActivity.getBotActivity,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.post(
|
|
|
|
|
'/bot-activity/unban',
|
|
|
|
|
// #swagger.tags = ['Admin · Bot Activity']
|
|
|
|
|
// #swagger.summary = 'Emergency unban an IP (admin only)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/UnbanRequest" } } } } */
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Unbanned', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
|
|
|
|
/* #swagger.responses[400] = { description: 'Invalid IP', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
adminOnly,
|
|
|
|
|
body('ip').isIP(),
|
|
|
|
|
validate,
|
|
|
|
|
@@ -202,9 +531,29 @@ adminRouter.post(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ── Authentication providers / SSO (admin only) ───────────────────────
|
|
|
|
|
adminRouter.get('/auth/providers', adminOnly, authProviders.list)
|
|
|
|
|
adminRouter.get(
|
|
|
|
|
'/auth/providers',
|
|
|
|
|
// #swagger.tags = ['Admin · Auth Providers']
|
|
|
|
|
// #swagger.summary = 'List configured SSO providers (admin only)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Providers (secrets stripped)', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ProviderConfig" } } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
adminOnly,
|
|
|
|
|
authProviders.list,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.post(
|
|
|
|
|
'/auth/providers',
|
|
|
|
|
// #swagger.tags = ['Admin · Auth Providers']
|
|
|
|
|
// #swagger.summary = 'Create a custom SSO provider (admin only)'
|
|
|
|
|
// #swagger.description = 'Built-in providers (google, discord) are configured via PUT, not created here.'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderCreateRequest" } } } } */
|
|
|
|
|
/* #swagger.responses[201] = { description: 'Created provider', content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderConfig" } } } } */
|
|
|
|
|
/* #swagger.responses[400] = { description: 'Validation error, or a built-in/invalid kind', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[409] = { description: 'Provider id already exists', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
adminOnly,
|
|
|
|
|
body('id').matches(/^[a-z0-9-]+$/),
|
|
|
|
|
body('kind').isIn(['oidc', 'oauth2']),
|
|
|
|
|
@@ -222,6 +571,16 @@ adminRouter.post(
|
|
|
|
|
)
|
|
|
|
|
adminRouter.put(
|
|
|
|
|
'/auth/providers/:id',
|
|
|
|
|
// #swagger.tags = ['Admin · Auth Providers']
|
|
|
|
|
// #swagger.summary = 'Update an SSO provider (admin only)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
|
|
|
|
|
/* #swagger.requestBody = { content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderCreateRequest" } } } } */
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Updated provider', content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderConfig" } } } } */
|
|
|
|
|
/* #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[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[404] = { description: 'Provider not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
adminOnly,
|
|
|
|
|
param('id').matches(/^[a-z0-9-]+$/),
|
|
|
|
|
body('name').optional().isString().trim().notEmpty().isLength({ max: 80 }),
|
|
|
|
|
@@ -238,6 +597,16 @@ adminRouter.put(
|
|
|
|
|
)
|
|
|
|
|
adminRouter.delete(
|
|
|
|
|
'/auth/providers/:id',
|
|
|
|
|
// #swagger.tags = ['Admin · Auth Providers']
|
|
|
|
|
// #swagger.summary = 'Delete a custom SSO provider (admin only)'
|
|
|
|
|
// #swagger.description = 'Built-in providers cannot be deleted — disable them instead.'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
|
|
|
|
/* #swagger.responses[400] = { description: 'Built-in provider cannot be deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[404] = { description: 'Provider not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
adminOnly,
|
|
|
|
|
param('id').matches(/^[a-z0-9-]+$/),
|
|
|
|
|
validate,
|
|
|
|
|
@@ -246,9 +615,27 @@ adminRouter.delete(
|
|
|
|
|
|
|
|
|
|
// ── User management (admin only) ──────────────────────────────────────
|
|
|
|
|
adminRouter.use('/users', adminOnly)
|
|
|
|
|
adminRouter.get('/users', ctrl.listUsers)
|
|
|
|
|
adminRouter.get(
|
|
|
|
|
'/users',
|
|
|
|
|
// #swagger.tags = ['Admin · Users']
|
|
|
|
|
// #swagger.summary = 'List users (admin only)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Users', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/User" } } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
ctrl.listUsers,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.post(
|
|
|
|
|
'/users',
|
|
|
|
|
// #swagger.tags = ['Admin · Users']
|
|
|
|
|
// #swagger.summary = 'Create a user (admin only)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/UserCreateRequest" } } } } */
|
|
|
|
|
/* #swagger.responses[201] = { description: 'Created user', content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } } } */
|
|
|
|
|
/* #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[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[409] = { description: 'Username already taken', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
body('username').isString().trim().isLength({ min: 3, max: 32 }),
|
|
|
|
|
body('password').isString().isLength({ min: 8, max: 64 }),
|
|
|
|
|
body('role').optional().isIn(['admin', 'editor']),
|
|
|
|
|
@@ -257,6 +644,17 @@ adminRouter.post(
|
|
|
|
|
)
|
|
|
|
|
adminRouter.put(
|
|
|
|
|
'/users/:id',
|
|
|
|
|
// #swagger.tags = ['Admin · Users']
|
|
|
|
|
// #swagger.summary = 'Update a user (admin only)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
|
|
|
|
|
/* #swagger.requestBody = { content: { "application/json": { schema: { $ref: "#/components/schemas/UserCreateRequest" } } } } */
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Updated user', content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } } } */
|
|
|
|
|
/* #swagger.responses[400] = { description: 'Validation error, or cannot demote the last admin', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[403] = { description: 'Admin role required', 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: 'Username already taken', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
param('id').isInt(),
|
|
|
|
|
body('username').optional().isString().trim().isLength({ min: 3, max: 32 }),
|
|
|
|
|
body('password').optional().isString().isLength({ min: 8, max: 64 }),
|
|
|
|
|
@@ -264,6 +662,20 @@ adminRouter.put(
|
|
|
|
|
validate,
|
|
|
|
|
ctrl.updateUser,
|
|
|
|
|
)
|
|
|
|
|
adminRouter.delete('/users/:id', param('id').isInt(), validate, ctrl.deleteUser)
|
|
|
|
|
adminRouter.delete(
|
|
|
|
|
'/users/:id',
|
|
|
|
|
// #swagger.tags = ['Admin · Users']
|
|
|
|
|
// #swagger.summary = 'Delete a user (admin only)'
|
|
|
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
|
|
|
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'integer' }, description: 'User id.' }
|
|
|
|
|
/* #swagger.responses[200] = { description: 'Deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */
|
|
|
|
|
/* #swagger.responses[400] = { description: 'Cannot delete your own account or the last admin', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
|
|
|
/* #swagger.responses[403] = { description: 'Admin role required', 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.deleteUser,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
module.exports = adminRouter
|
|
|
|
|
|