Wiki Phase 1: categories, drafts/publish, HTML sanitization
Foundation & safety phase of the wiki upgrade (see WIKI_UPGRADE.md). Schema (additive, idempotent via ensureSchema): - new wiki_categories table; wiki_pages gains category_id, excerpt, published, published_at, sort_order, and a FULLTEXT index - migration ALTERs guarded with IF NOT EXISTS for existing databases - seed reworked into 4 sections with the 8 starter pages assigned Security: - new utils/sanitizeHtml.js (sanitize-html allowlist); wiki bodies are sanitized on every save, and the article renders through DOMPurify - strips <script>, event handlers (onerror), and javascript: URLs Backend: - public: published-only list with ?category filter + /wiki/categories - admin: extended page CRUD, PATCH publish toggle, category CRUD; drafts visible to admin, hidden from public - all writes logged to activity_log Frontend: - data-driven public wiki index (sections + real descriptions; removed hardcoded blurbs/Roman numerals) with ?category filtering - article: category breadcrumb + sanitized render - admin: Section/Status columns, draft/publish + section + excerpt in the editor, and a Manage sections modal Verified end-to-end against MariaDB 11: migration clean, XSS neutralized, drafts hidden, client builds, server boots. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -65,22 +65,57 @@ adminRouter.patch(
|
||||
)
|
||||
adminRouter.delete('/posts/:id', param('id').isInt(), validate, ctrl.deletePost)
|
||||
|
||||
// ── Wiki ──────────────────────────────────────────────────────────────
|
||||
// ── Wiki categories (static paths registered before /wiki/:slug) ───────
|
||||
adminRouter.get('/wiki/categories', ctrl.listWikiCategories)
|
||||
adminRouter.post(
|
||||
'/wiki/categories',
|
||||
body('slug').matches(/^[a-z0-9-]+$/),
|
||||
body('title').isString().trim().notEmpty().isLength({ max: 200 }),
|
||||
body('description').optional({ values: 'falsy' }).isString().isLength({ max: 400 }),
|
||||
body('sort_order').optional().isInt(),
|
||||
validate,
|
||||
ctrl.createWikiCategory,
|
||||
)
|
||||
adminRouter.put(
|
||||
'/wiki/categories/:id',
|
||||
param('id').isInt(),
|
||||
body('slug').optional().matches(/^[a-z0-9-]+$/),
|
||||
body('title').optional().isString().trim().notEmpty().isLength({ max: 200 }),
|
||||
body('description').optional({ values: 'falsy' }).isString().isLength({ max: 400 }),
|
||||
body('sort_order').optional().isInt(),
|
||||
validate,
|
||||
ctrl.updateWikiCategory,
|
||||
)
|
||||
adminRouter.delete('/wiki/categories/:id', param('id').isInt(), validate, ctrl.deleteWikiCategory)
|
||||
|
||||
// ── Wiki pages ─────────────────────────────────────────────────────────
|
||||
adminRouter.get('/wiki', ctrl.listWiki)
|
||||
adminRouter.post(
|
||||
'/wiki',
|
||||
body('slug').matches(/^[a-z0-9-]+$/),
|
||||
body('title').isString().trim().notEmpty(),
|
||||
body('title').isString().trim().notEmpty().isLength({ max: 200 }),
|
||||
body('excerpt').optional({ values: 'falsy' }).isString().isLength({ max: 400 }),
|
||||
body('category_id').optional({ values: 'null' }).isInt(),
|
||||
body('published').optional().isBoolean(),
|
||||
validate,
|
||||
ctrl.createWiki,
|
||||
)
|
||||
adminRouter.get('/wiki/:slug', ctrl.getWiki)
|
||||
adminRouter.put(
|
||||
'/wiki/:slug',
|
||||
body('title').isString().trim().notEmpty(),
|
||||
body('title').optional().isString().trim().notEmpty().isLength({ max: 200 }),
|
||||
body('excerpt').optional({ values: 'falsy' }).isString().isLength({ max: 400 }),
|
||||
body('category_id').optional({ values: 'null' }).isInt(),
|
||||
body('published').optional().isBoolean(),
|
||||
validate,
|
||||
ctrl.updateWiki,
|
||||
)
|
||||
adminRouter.patch(
|
||||
'/wiki/:slug/publish',
|
||||
body('published').isBoolean(),
|
||||
validate,
|
||||
ctrl.publishWiki,
|
||||
)
|
||||
adminRouter.delete('/wiki/:slug', ctrl.deleteWiki)
|
||||
|
||||
// ── Settings ──────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user