Final parity push (web-tier only): - REST API write + token auth: POST /api/tokens (issue a personal_access_token for the session user), Bearer auth via src/lib/api-auth.ts, POST /api/articles/[slug]/comment, GET/DELETE /api/me/tokens, full tickets API (/api/tickets +[id] +[id]/reply), radio current-dj/points/points-leaderboard/ embed-config + POST shouts, and a real-time /api/radio/stream (SSE). 31 public API routes total. - Pages: /draw-badge (buy a custom profile badge → credits + RCON), /me dashboard (stats + online friends + referral claim). Wired into the nav. - HTML sanitisation (sanitize-html) — the HTMLPurifier equivalent — applied to writeable boxes + article bodies before dangerouslySetInnerHTML. - "Dusk" dark theme preset + a default-dark site option honoured by the no-flash boot script. Verified live (prod, amx_test): token issue → Bearer endpoint 200, no-token 401; /api/me/tokens lists it; current-dj/leaderboard JSON; /me + /draw-badge 200; reverted the test user + tokens. tsc 0, vitest 49/49, next build 0.
46 lines
1.5 KiB
TypeScript
46 lines
1.5 KiB
TypeScript
import sanitizeHtml from "sanitize-html";
|
|
|
|
/**
|
|
* Server-side HTML sanitiser for user/staff-authored rich content before it is
|
|
* injected via dangerouslySetInnerHTML — the AtomCMS HTMLPurifier equivalent.
|
|
* Allows a safe formatting subset (no <script>/<style>/<iframe>, no on* event
|
|
* handlers, no javascript: URLs); images/links are permitted with safe schemes.
|
|
*/
|
|
const OPTIONS: sanitizeHtml.IOptions = {
|
|
allowedTags: [
|
|
"a", "b", "i", "em", "strong", "u", "s", "p", "br", "hr", "span", "div",
|
|
"ul", "ol", "li", "blockquote", "code", "pre",
|
|
"h1", "h2", "h3", "h4", "h5", "h6",
|
|
"img", "figure", "figcaption", "table", "thead", "tbody", "tr", "th", "td",
|
|
],
|
|
allowedAttributes: {
|
|
a: ["href", "title", "target", "rel"],
|
|
img: ["src", "alt", "title", "width", "height"],
|
|
"*": ["style", "class"],
|
|
},
|
|
allowedSchemes: ["http", "https", "mailto"],
|
|
allowedSchemesByTag: { img: ["http", "https", "data"] },
|
|
// Drop any style declarations that aren't simple, safe properties.
|
|
allowedStyles: {
|
|
"*": {
|
|
color: [/.*/],
|
|
"background-color": [/.*/],
|
|
"text-align": [/^left$|^right$|^center$|^justify$/],
|
|
"font-weight": [/.*/],
|
|
"font-style": [/.*/],
|
|
"text-decoration": [/.*/],
|
|
"font-size": [/.*/],
|
|
margin: [/.*/],
|
|
padding: [/.*/],
|
|
},
|
|
},
|
|
transformTags: {
|
|
a: sanitizeHtml.simpleTransform("a", { rel: "noopener noreferrer nofollow" }),
|
|
},
|
|
};
|
|
|
|
export function sanitize(html: string | null | undefined): string {
|
|
if (!html) return "";
|
|
return sanitizeHtml(html, OPTIONS);
|
|
}
|