// Admin · Settings — the site-wide key/value settings store. // // Mounted at /api/v1/admin/settings by admin/index.js, which already applied // `noindex, isLoggedIn, staffOnly`. Editors may manage content, but settings // are admin-only: this store gates registration, game-account signup, the // contact form and the rest of the site's behaviour switches. // // Admin-only, and kept as a per-route gate rather than a router-level `use` so // the middleware chain each route carries is unchanged by the move. Handlers // still live in admin.controller.js; this re-wires routes, not logic. const express = require('express') const ctrl = require('./admin.controller') const { upload } = require('./imageUpload') const { requireRole } = require('../../../utils/auth') const settingsRouter = express.Router() const adminOnly = requireRole('admin') settingsRouter.get( '/', // #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, ) settingsRouter.put( '/', // #swagger.tags = ['Admin · Settings'] // #swagger.summary = 'Update site settings (admin only)' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.description = 'Writes the given keys. The JSON-valued theming keys (theme_visual, brand_assets, nav_public, nav_admin, nav_player) accept an object or its stringified form, are validated strictly with the offending field named in the 400, and are stored stringified with unusable fields dropped. Nav overrides key coded entries by their existing route and carry only label/order/hidden/group/section; whether a key names a route the nav declares is settled client-side at merge time. nav_public may additionally carry admin-created dropdown `sections` and admin-authored `links` — the only place an arbitrary path may be named, and therefore restricted to same-origin paths (no scheme, no protocol-relative host). Sections and links are dropped for the other two navs, which cannot render them.' /* #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, ) // Upload one brand asset (logo/hero/favicon) and point brand_assets at it in the // same call — see the controller for why it is one call and not "upload, then // PUT". Uses the shared multer config (one upload directory, one mimetype // allowlist); the per-slot PNG rule and size caps are applied in the handler. settingsRouter.post( '/brand-asset/:slot', // #swagger.tags = ['Admin · Settings'] // #swagger.summary = 'Upload a brand asset and set it as the override (admin only)' // #swagger.description = 'Stores the image and writes the brand_assets settings row in one call, so an upload never leaves an unreferenced file. Favicons must be PNG (max 512 KB); logos max 1 MB; heroes max 8 MB. Absent slots keep falling back to the BRAND_* env defaults — uploading a logo does not clear a hero.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.parameters['slot'] = { in: 'path', required: true, description: 'Which asset to replace', schema: { type: 'string', enum: ['logo', 'hero', 'favicon'] } } */ /* #swagger.requestBody = { required: true, content: { "multipart/form-data": { schema: { type: "object", properties: { image: { type: "string", format: "binary" } } } } } } */ /* #swagger.responses[201] = { description: 'Stored file URL and the updated overrides', content: { "application/json": { schema: { type: "object", properties: { url: { type: "string", example: "/uploads/1712345678901-ab12cd34.png" }, brand_assets: { type: "object", properties: { logo: { type: "string" }, hero: { type: "string" }, favicon: { type: "string" } } } } } } } } */ /* #swagger.responses[400] = { description: 'No file, unknown slot, disallowed type, or over the slot size cap', 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, upload.single('image'), ctrl.uploadBrandAsset, ) // Reset one setting to its default by deleting the row. Only the keys whose // default lives outside the store (theming, nav, hero draft) are deletable — // the controller holds the allowlist. settingsRouter.delete( '/:key', // #swagger.tags = ['Admin · Settings'] // #swagger.summary = 'Reset one setting to its default (admin only)' // #swagger.description = 'Deletes the settings row so the surface falls back to its BRAND_* env / theme.css / hardcoded default. Restricted to the resettable keys (theme_visual, brand_assets, nav_public, nav_admin, nav_player, hero_layout_draft). Idempotent: resetting a key that was never set succeeds.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.parameters['key'] = { in: 'path', required: true, description: 'Settings key to reset', schema: { type: 'string' } } */ /* #swagger.responses[200] = { description: 'Setting reset', content: { "application/json": { schema: { $ref: "#/components/schemas/Message" } } } } */ /* #swagger.responses[400] = { description: 'Setting is not resettable', 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.deleteSetting, ) module.exports = settingsRouter