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>
36 lines
1.4 KiB
JavaScript
36 lines
1.4 KiB
JavaScript
// Parse a JSON-valued settings row.
|
|
//
|
|
// `settings.value` is TEXT (db/schema.sql), so every JSON-shaped key —
|
|
// hero_layout, and now theme_visual / brand_assets / nav_* — is stored
|
|
// stringified and arrives as a string. Consumers must parse it, and the parse
|
|
// has to be fail-safe: a malformed or wrong-shaped value is treated as
|
|
// **absent** (the surface falls back to its BRAND_* env / theme.css / NAV
|
|
// default), never as an error and never as a half-applied object. That is the
|
|
// same posture parseLayout already takes on the client
|
|
// (client/src/lib/heroLayout.js).
|
|
//
|
|
// See docs/website/THEMING_AND_NAV.md §4.4.
|
|
|
|
/**
|
|
* @param {string|null|undefined} str the raw stored value
|
|
* @param {(value: unknown) => boolean} [validator] shape check; anything it
|
|
* rejects is treated as absent
|
|
* @returns {object|null} the parsed object, or null when absent/malformed
|
|
*/
|
|
function parseJsonSetting(str, validator) {
|
|
if (typeof str !== 'string' || str === '') return null
|
|
let parsed
|
|
try {
|
|
parsed = JSON.parse(str)
|
|
} catch {
|
|
return null
|
|
}
|
|
// Only plain objects. A stored `null`, `4`, `"x"` or array is as unusable to
|
|
// every consumer of these keys as a syntax error is.
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
|
|
if (validator && !validator(parsed)) return null
|
|
return parsed
|
|
}
|
|
|
|
module.exports = { parseJsonSetting }
|