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>
70 lines
3.1 KiB
JavaScript
70 lines
3.1 KiB
JavaScript
// The route manifest is the freeze that proves the domain split (docs/website/
|
|
// API_V2_PLAN.md § Phase 2) moves no URL. CI runs `npm run routes:manifest -- --check`,
|
|
// but that only fires on a pull request — this test makes the same drift visible on
|
|
// `npm test`, and adds the two structural invariants the manifest alone can't state.
|
|
//
|
|
// Point the pool at a closed port BEFORE requiring anything: the generator loads the
|
|
// real app, which pulls in every model and builds a mariadb pool at require time. No
|
|
// query is ever run here (the Express stack is introspected, not called).
|
|
process.env.DB_HOST = '127.0.0.1'
|
|
process.env.DB_PORT = '59999'
|
|
|
|
const { test, after } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const manifestTool = require('../scripts/routeManifest')
|
|
const db = require('../src/utils/db')
|
|
|
|
after(() => db.close())
|
|
|
|
const SERVER_ROOT = path.join(__dirname, '..')
|
|
const read = (file) => fs.readFileSync(path.join(SERVER_ROOT, file), 'utf8').replace(/\r\n/g, '\n')
|
|
|
|
const collected = manifestTool.collect()
|
|
|
|
test('routes.manifest.json is in sync with the live Express stack', () => {
|
|
const generated = manifestTool.serialize(manifestTool.buildManifest(collected))
|
|
assert.equal(
|
|
read('routes.manifest.json'),
|
|
generated,
|
|
'The URL surface changed. If that was deliberate, run `npm run routes:manifest` and ' +
|
|
'commit the result so the change is reviewed — do not smuggle a URL change into a ' +
|
|
'"mechanical" refactor PR.',
|
|
)
|
|
})
|
|
|
|
test('routes.guards.json is in sync with the live Express stack', () => {
|
|
const generated = manifestTool.serialize(manifestTool.buildGuards(collected))
|
|
assert.equal(read('routes.guards.json'), generated, 'Run `npm run routes:manifest`.')
|
|
})
|
|
|
|
test('the manifest only inventories API surface, never static mounts', () => {
|
|
// The SPA catch-all, /uploads and /brand are filesystem-conditional, so including
|
|
// them would make the manifest depend on whether the client had been built.
|
|
for (const route of collected.public) {
|
|
assert.ok(
|
|
route.path.startsWith('/api/') || route.path.startsWith('/.well-known/'),
|
|
`unexpected non-API path in the manifest: ${route.method} ${route.path}`,
|
|
)
|
|
}
|
|
})
|
|
|
|
test('every /admin, /player and /settings route still sits behind the shared auth gate', () => {
|
|
// Router-level `use()` gates do not appear in an individual route's own stack, so a
|
|
// capability router extracted from admin.routes.js without re-applying the gate would
|
|
// silently publish authenticated endpoints. Names are only a hint — `requireRole(...)`
|
|
// returns an anonymous arrow and cannot be seen here — but a *missing* requireAuth is
|
|
// unambiguous.
|
|
const AUTHENTICATED_GROUPS = ['/api/v1/admin/', '/api/v1/player/', '/api/v1/settings/']
|
|
const gated = collected.public.filter((r) => AUTHENTICATED_GROUPS.some((p) => r.path.startsWith(p)))
|
|
assert.ok(gated.length > 100, 'expected the gated surface to be found')
|
|
for (const route of gated) {
|
|
assert.ok(
|
|
route.gates.includes('requireAuth'),
|
|
`${route.method} ${route.path} is missing requireAuth`,
|
|
)
|
|
}
|
|
})
|