diff --git a/artifacts/api-server/src/lib/sanitize.ts b/artifacts/api-server/src/lib/sanitize.ts index 0329cd2d..e2938f38 100644 --- a/artifacts/api-server/src/lib/sanitize.ts +++ b/artifacts/api-server/src/lib/sanitize.ts @@ -17,8 +17,12 @@ const FONT_SIZE_RE = /^(?:[8-9]|[1-9]\d)px$/; const COLOR_RE = /^(?:#[0-9a-f]{3}|#[0-9a-f]{6}|rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\))$/i; export const RICH_TEXT_OPTIONS: sanitizeHtml.IOptions = { - allowedTags: ["span", "strong", "b", "em", "i", "u", "br"], + //

is included so Tiptap's TextAlign extension (applied to paragraph + // nodes) can persist text-align: left/center/right via the inline style + // allowlist below. Block tags beyond

stay disallowed. + allowedTags: ["p", "span", "strong", "b", "em", "i", "u", "br"], allowedAttributes: { + p: ["style"], span: ["style"], strong: ["style"], b: ["style"], diff --git a/artifacts/api-server/tests/executive-meetings.test.mjs b/artifacts/api-server/tests/executive-meetings.test.mjs index d4856323..8909daf5 100644 --- a/artifacts/api-server/tests/executive-meetings.test.mjs +++ b/artifacts/api-server/tests/executive-meetings.test.mjs @@ -580,6 +580,29 @@ test("Sanitization: rich text strips disallowed tags but keeps inline formatting assert.ok(/]*>One<\/em>/i.test(att.name), "em must survive in attendee"); }); +test("Sanitization: text-align on

survives a round-trip via PATCH/GET", async () => { + const m = await api(adminCookie, "POST", "/api/executive-meetings", { + titleAr: "محاذاة", titleEn: "Align", meetingDate: today, + }); + const M = await m.json(); created.meetingIds.push(M.id); + // Tiptap's TextAlign extension emits paragraphs like + //

. The sanitizer must allow that + // structure so Word-like alignment persists. + const html = '

يمين

center

'; + const patch = await api(adminCookie, "PATCH", + `/api/executive-meetings/${M.id}`, { titleAr: html }); + assert.equal(patch.status, 200); + const day = await api(adminCookie, "GET", + `/api/executive-meetings?date=${today}`); + const body = await day.json(); + const got = body.meetings.find((x) => x.id === M.id); + assert.ok(got, "meeting must come back"); + assert.match(got.titleAr, /]*text-align:\s*right[^>]*>يمين<\/p>/i, + "right-aligned

survives sanitization"); + assert.match(got.titleAr, /]*text-align:\s*center[^>]*>center<\/p>/i, + "center-aligned

survives sanitization"); +}); + test("Reorder: POST /reorder renumbers a full day to 1..N and inherits slot times", async () => { // Use a fresh future date to guarantee the reorder is full-day. const reorderDate = "2050-01-15";