RTE Posts upgrade: TipTap editor + sanitization for posts
Extend the wiki's RichTextEditor to the Posts editor and close the stored-XSS gap on public post bodies. - RichTextEditor: add `variant` prop — `full` (wiki), `post` (no internal wiki-page link picker), `minimal` (image-only, for Screenshots captions). Toolbar sections rendered conditionally. - PostEditor: replace the body textarea with a lazy-loaded RichTextEditor in Suspense; variant chosen by category (minimal for screenshots, post otherwise). - posts.model: sanitize body via shared cleanBody on create/update, treat an empty TipTap `<p></p>` as null, and auto-derive the excerpt from the body (max 280 chars) when left blank. - sanitizeHtml util: add deriveExcerpt() helper. - FiveOnFriday / NewsletterIssue: wrap dangerouslySetInnerHTML with DOMPurify.sanitize() as defense-in-depth on render. No schema or dependency changes. Verified end-to-end against the local stack: 24/24 API assertions and a full UI round-trip across all four post categories. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { useState } from 'react'
|
||||
import { lazy, Suspense, useState } from 'react'
|
||||
import Modal from '../../../components/Modal.jsx'
|
||||
import { api } from '../../../api/client.js'
|
||||
|
||||
const RichTextEditor = lazy(() => import('../../../components/RichTextEditor.jsx'))
|
||||
|
||||
const CATEGORIES = [
|
||||
{ v: 'news', l: 'News' },
|
||||
{ v: 'five-on-friday', l: 'Five on Friday' },
|
||||
@@ -146,10 +148,16 @@ export default function PostEditor({ post, onClose, onSaved }) {
|
||||
)}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span className="field-label">Body (HTML or text)</span>
|
||||
<textarea value={form.body} onChange={set('body')} className="textarea" />
|
||||
</label>
|
||||
<div>
|
||||
<span className="field-label">Body</span>
|
||||
<Suspense fallback={<span className="spin" />}>
|
||||
<RichTextEditor
|
||||
value={form.body}
|
||||
onChange={(html) => setForm((f) => ({ ...f, body: html }))}
|
||||
variant={isScreenshot ? 'minimal' : 'post'}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user