feat(theming): settings-store, nav merge util and radius tokens

Phases 0-2 of docs/website/THEMING_AND_NAV.md. Groundwork only: no admin UI,
no consumer wiring, and an instance that never touches the new settings keys
renders exactly as it does today.

Phase 0 - settings store:
- settingsDb.remove() and DELETE /api/v1/admin/settings/:key, the "reset to
  default" primitive. Defaults for these keys live in BRAND_* env, theme.css
  and the hardcoded NAV arrays, so reset has to delete the row rather than
  store a copy of the default. Allowlisted to the five theming/nav keys plus
  hero_layout_draft, admin-only, idempotent.
- GET /api/v1/settings/nav behind requireAuth with no role gate. AdminLayout
  renders for editors and moderators and PlayerPortalLayout for players, and
  none of them can read GET /admin/settings, so without this their nav
  override would silently never apply.
- A fifth router group for it: /public is anonymous, /admin/settings is
  adminOnly, /player is self-scoped data. This is configuration that needs a
  login.
- parseJsonSetting() in utils/settingsJson.js. settings.value is TEXT, so
  every JSON key arrives as a string; malformed or wrong-shaped reads as
  absent, never as an error and never half-applied.
- theme_visual / brand_assets / nav_public join PUBLIC_KEYS; nav_admin and
  nav_player deliberately do not.

Phase 1 - client/src/lib/navOverrides.js, the pure merge util. Presentation
only: it can set label/order/hidden and (grouped navs) group, and nothing
else. It cannot introduce a `to`, cannot touch roles/feature, and hidden:false
cannot un-hide anything - the existing filters run afterward, unchanged, and
remain the boundary.

Phase 2 - promoted 23 border-radius literals in theme.css to four tokens at
today's values (14x8px, 4x999px, 4x10px, 1x12px). The 7px/6px editor chrome
and the two 50% circles stay literal. --shadow-card and --panel-grad were
already tokens.

Tests: 16 new server tests, 20 new client tests. The route-manifest guard now
also asserts /settings/** sits behind requireAuth. Swagger and both route
artifacts regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-07 18:15:29 -05:00
parent d765280e28
commit ec0036ce6d
18 changed files with 1042 additions and 28 deletions

View File

@@ -0,0 +1,33 @@
// /api/v1/settings — settings any *authenticated* account needs to read, whoever
// they are.
//
// A fifth group alongside /auth, /public, /admin and /player, and deliberately
// not folded into any of them:
//
// - /public is anonymous, and the admin nav's labels describe the shape of the
// admin surface — that belongs behind a login.
// - /admin is `staffOnly` + `requireRole('admin')` on settings, but AdminLayout
// renders for editors and moderators too, so they could never read their own
// nav overrides from there (docs/website/THEMING_AND_NAV.md §4.2).
// - /player is self-service data scoped to req.user.id. These rows are
// site-wide configuration that happens to need a login, not anything about
// the caller.
//
// Group gate: authenticated only, no role restriction — staff and players alike
// read their own layout's nav. It lives here, ahead of every mount, so a route
// added later cannot ship ungated.
const express = require('express')
const { requireAuth } = require('../../../auth/session.middleware')
const noindex = require('../../../middleware/noindex')
const navRouter = require('./nav.router')
const settingsRouter = express.Router()
settingsRouter.use(noindex, requireAuth)
settingsRouter.use('/nav', navRouter)
module.exports = settingsRouter

View File

@@ -0,0 +1,17 @@
const settings = require('../../../model/settings/settings.model')
const log = require('../../../utils/logger')
// The nav overrides for the two authenticated layouts. Values are the raw stored
// JSON strings (settings.value is TEXT) or null; the caller parses them with the
// same fail-safe posture as every other JSON setting — malformed reads as
// absent, and absent means the hardcoded NAV array is used unchanged.
async function getNav(req, res) {
try {
return res.json(await settings.getNav())
} catch (err) {
log.error('getNav', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
module.exports = { getNav }

View File

@@ -0,0 +1,28 @@
// Settings · Nav — the admin-sidebar and player-portal nav overrides, readable
// by the accounts those navs are rendered for.
//
// Mounted at /api/v1/settings/nav by settings/index.js, which already applied
// `noindex, requireAuth`. No role gate on purpose: an editor, a moderator and a
// player each need the override for the layout they see, and the payload is
// presentation-only — label/order/hidden/group over items the reader's own
// role/feature filter still gets the final say on
// (docs/website/THEMING_AND_NAV.md §7).
const express = require('express')
const ctrl = require('./nav.controller')
const navRouter = express.Router()
navRouter.get(
'/',
// #swagger.tags = ['Settings']
// #swagger.summary = 'Nav overrides for the admin and player layouts'
// #swagger.description = 'Returns the stored nav_admin and nav_player overrides as raw JSON strings (null when the admin never overrode that nav). Any authenticated account may read them: AdminLayout renders for editors and moderators, PlayerPortalLayout for players, and none of them can read GET /admin/settings. Presentation-only — the role/feature filters in the layouts still decide what is actually shown.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.responses[200] = { description: 'Nav overrides', content: { "application/json": { schema: { $ref: "#/components/schemas/NavSettings" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
ctrl.getNav,
)
module.exports = navRouter