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:
@@ -28,15 +28,34 @@ CREATE TABLE IF NOT EXISTS posts (
|
||||
INDEX idx_posts_feed (category, published, published_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Wiki categories / sections. Defined before wiki_pages so the FK resolves on a
|
||||
-- fresh install. Pages reference a category (nullable = "Uncategorized").
|
||||
CREATE TABLE IF NOT EXISTS wiki_categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
slug VARCHAR(120) NOT NULL UNIQUE,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
description VARCHAR(400) NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wiki_pages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
slug VARCHAR(120) NOT NULL UNIQUE,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
body MEDIUMTEXT NULL,
|
||||
updated_by INT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_wiki_user FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
slug VARCHAR(120) NOT NULL UNIQUE,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
body MEDIUMTEXT NULL,
|
||||
excerpt VARCHAR(400) NULL,
|
||||
category_id INT NULL,
|
||||
published TINYINT(1) NOT NULL DEFAULT 1,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
updated_by INT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
published_at DATETIME NULL,
|
||||
CONSTRAINT fk_wiki_user FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_wiki_category FOREIGN KEY (category_id) REFERENCES wiki_categories(id) ON DELETE SET NULL,
|
||||
FULLTEXT INDEX idx_wiki_search (title, body)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
@@ -57,3 +76,15 @@ CREATE TABLE IF NOT EXISTS activity_log (
|
||||
CONSTRAINT fk_activity_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
INDEX idx_activity_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migrations for databases created before the wiki upgrade. Each statement uses
|
||||
-- IF NOT EXISTS so re-running on every boot is a harmless no-op. New installs get
|
||||
-- these columns from the CREATE TABLE above; existing installs get them here.
|
||||
-- (The category foreign key is only added on fresh installs; on upgraded databases
|
||||
-- referential integrity for category_id is enforced in application code.)
|
||||
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS excerpt VARCHAR(400) NULL;
|
||||
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS category_id INT NULL;
|
||||
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS published TINYINT(1) NOT NULL DEFAULT 1;
|
||||
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS sort_order INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS published_at DATETIME NULL;
|
||||
ALTER TABLE wiki_pages ADD FULLTEXT INDEX IF NOT EXISTS idx_wiki_search (title, body);
|
||||
|
||||
@@ -22,24 +22,39 @@ const DEFAULT_SETTINGS = {
|
||||
site_title: 'UOMysticmoon',
|
||||
}
|
||||
|
||||
// The 8 starter wiki categories (editable later via the admin panel).
|
||||
// Starter wiki sections (editable later via the admin panel).
|
||||
// [slug, title, description, sort_order]
|
||||
const WIKI_CATEGORIES = [
|
||||
['guides', 'Guides', 'Getting started and how-to guides.', 10],
|
||||
['world', 'World & Lore', 'Regions, maps, and the story of Mysticmoon.', 20],
|
||||
['gameplay', 'Systems & Gameplay', 'Mechanics, items, monsters, and crafting.', 30],
|
||||
['community', 'Community & Rules', 'Player conduct and shard policies.', 40],
|
||||
]
|
||||
|
||||
// The 8 starter pages, each mapped to a section. [slug, title, body, categorySlug]
|
||||
const WIKI_PAGES = [
|
||||
['new-player-guide', 'New Player Guide', 'First steps, basic survival, and early goals.'],
|
||||
['maps-atlas', 'Maps & Atlas', 'Regions, towns, routes, and travel notes.'],
|
||||
['systems', 'Server Systems', 'Shard mechanics and custom features.'],
|
||||
['items', 'Items & Rewards', 'Equipment, treasures, rewards, and curiosities.'],
|
||||
['monsters', 'Monsters & Encounters', 'Creatures, bosses, spawns, and dangers.'],
|
||||
['crafting', 'Crafting', 'Professions, materials, recipes, and tools.'],
|
||||
['lore', 'Lore', 'Stories, places, factions, and mysteries.'],
|
||||
['rules', 'Rules', 'Player conduct, shard expectations, and policies.'],
|
||||
['new-player-guide', 'New Player Guide', 'First steps, basic survival, and early goals.', 'guides'],
|
||||
['maps-atlas', 'Maps & Atlas', 'Regions, towns, routes, and travel notes.', 'world'],
|
||||
['lore', 'Lore', 'Stories, places, factions, and mysteries.', 'world'],
|
||||
['systems', 'Server Systems', 'Shard mechanics and custom features.', 'gameplay'],
|
||||
['items', 'Items & Rewards', 'Equipment, treasures, rewards, and curiosities.', 'gameplay'],
|
||||
['monsters', 'Monsters & Encounters', 'Creatures, bosses, spawns, and dangers.', 'gameplay'],
|
||||
['crafting', 'Crafting', 'Professions, materials, recipes, and tools.', 'gameplay'],
|
||||
['rules', 'Rules', 'Player conduct, shard expectations, and policies.', 'community'],
|
||||
]
|
||||
|
||||
async function seedDefaults() {
|
||||
for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) {
|
||||
await settingsDb.seedDefault(key, value)
|
||||
}
|
||||
for (const [slug, title, body] of WIKI_PAGES) {
|
||||
for (const [slug, title, description, sortOrder] of WIKI_CATEGORIES) {
|
||||
await wikiDb.seedDefaultCategory(slug, title, description, sortOrder)
|
||||
}
|
||||
for (const [slug, title, body, categorySlug] of WIKI_PAGES) {
|
||||
await wikiDb.seedDefault(slug, title, body)
|
||||
// Attach to its section (only if not already categorized — safe re-run /
|
||||
// migration of pages seeded before the wiki upgrade).
|
||||
await wikiDb.assignCategoryBySlug(slug, categorySlug)
|
||||
}
|
||||
log.info('settings and wiki defaults ensured')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user