Backend for the CMS page builder, all under the existing /api/v1: - pages.model: authoritative save gate — validates blocks against the registry and sanitizes them on every create/update; maps rows to/from the grouped API shape (metadata / settings); slug validated + reserved-checked at create and immutable after; `protected` can be set true via PATCH but only cleared via the unprotect path; published_at stamped on first publish. - sanitizeBlocks: post-validation normalizer (applies each block's sanitize, stamps version, defaults visible, recurses container slots). - reservedSlugs: guards page slugs from shadowing named routes/API namespaces. - Admin routes (staff-gated): GET/POST /pages, GET/PATCH/DELETE /pages/:id, POST /pages/:id/unprotect (password step-up, verified against the caller's own hash, never logged), POST /pages/:id/preview (1h token). Audit-logs create/publish/unpublish/protect/unprotect/delete. - Public routes: GET /public/pages/:slug (published; staff see drafts; site- mode gated) and GET /public/pages/:id/preview/:token (ungated, token is the access control). Preview token primitives added to auth/token.js. - Swagger annotations for all new endpoints. Verified end-to-end: model integration test against the dev DB (sanitize, invalid-block rejection, slug immutability, protected/unprotect, dup/reserved slug, published_at) + authenticated HTTP smoke (201 create, 400 invalid blocks, publish, public slug fetch, preview mint+fetch, 403 delete-protected, 401 wrong-password unprotect). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
48 lines
1.5 KiB
JavaScript
48 lines
1.5 KiB
JavaScript
// Normalize + sanitize a validated blocks array before persisting. Runs AFTER
|
|
// validateBlocks (which guarantees the envelope/prop shape), so this can assume
|
|
// well-formed input and focus on: applying each block's registry `sanitize`
|
|
// normalizer (e.g. rich_text runs its html through the allowlist), stamping the
|
|
// registry `version`, defaulting `visible` to true, and recursing one level into
|
|
// container slots. Returns a new array; never mutates the input.
|
|
|
|
const { getBlock } = require('./registry')
|
|
|
|
function sanitizeBlocks(blocks) {
|
|
if (!Array.isArray(blocks)) return []
|
|
return blocks.map(sanitizeOne)
|
|
}
|
|
|
|
function sanitizeOne(block) {
|
|
const def = getBlock(block.type)
|
|
if (!def) return block // unreachable after validation, but stay defensive
|
|
|
|
let props = block.props && typeof block.props === 'object' ? { ...block.props } : {}
|
|
|
|
// Recurse into container slots first (leaf sub-blocks get sanitized too).
|
|
if (def.container) {
|
|
for (const slot of def.containerSlots) {
|
|
if (Array.isArray(props[slot])) props[slot] = props[slot].map(sanitizeOne)
|
|
}
|
|
}
|
|
|
|
// Apply the block's own normalizer last (operates on its scalar props).
|
|
if (def.sanitize) {
|
|
try {
|
|
props = def.sanitize(props)
|
|
} catch {
|
|
// Leave props as-is; validation already passed, a sanitize throw shouldn't
|
|
// block the save.
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: block.id,
|
|
type: block.type,
|
|
version: Number.isInteger(block.version) ? block.version : def.version,
|
|
visible: block.visible !== false,
|
|
props,
|
|
}
|
|
}
|
|
|
|
module.exports = { sanitizeBlocks }
|