diff --git a/artifacts/api-server/src/lib/sanitize.ts b/artifacts/api-server/src/lib/sanitize.ts index e2938f38..8cd958ac 100644 --- a/artifacts/api-server/src/lib/sanitize.ts +++ b/artifacts/api-server/src/lib/sanitize.ts @@ -1,13 +1,7 @@ import sanitizeHtml from "sanitize-html"; -// font-family is validated by a strict regex (no custom functions, no -// type-bypass casts) so sanitize-html can run it through its native -// `regex.test(value)` style validator. -// -// Allowed families: Cairo, Tajawal, Amiri, Noto Naskh Arabic, +// Allowed font families: Cairo, Tajawal, Amiri, Noto Naskh Arabic, // IBM Plex Sans Arabic, system-ui, sans-serif, serif, monospace. -// A value may be a single name (optionally quoted) or a comma-separated -// stack of allowed names. const FONT_NAME_PART = String.raw`(?:["']?(?:Cairo|Tajawal|Amiri|Noto Naskh Arabic|IBM Plex Sans Arabic|system-ui|sans-serif|serif|monospace)["']?)`; const FONT_FAMILY_RE = new RegExp( `^${FONT_NAME_PART}(?:\\s*,\\s*${FONT_NAME_PART})*$`, @@ -17,9 +11,6 @@ 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 = { - //
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"], @@ -45,8 +36,6 @@ export const RICH_TEXT_OPTIONS: sanitizeHtml.IOptions = { allowedSchemes: [], disallowedTagsMode: "discard", enforceHtmlBoundary: false, - // Keep text content of disallowed tags rather than dropping it entirely; - // this matters when a user pastes from Word and we strip the wrapper. exclusiveFilter: () => false, }; diff --git a/artifacts/api-server/src/routes/executive-meetings.ts b/artifacts/api-server/src/routes/executive-meetings.ts index d22ec4d7..739e76bd 100644 --- a/artifacts/api-server/src/routes/executive-meetings.ts +++ b/artifacts/api-server/src/routes/executive-meetings.ts @@ -931,7 +931,6 @@ router.post( } try { await db.transaction(async (tx) => { - // Load *all* meetings on this date — full-day reorder is required. const allRows = await tx .select() .from(executiveMeetingsTable) @@ -951,10 +950,6 @@ router.post( }); } } - // Snapshot the day's existing slots (start_time, end_time) sorted - // by `start_time NULLS LAST, daily_number` — this is the canonical - // schedule order. The first row of the new order takes the earliest - // existing slot, the second takes the next, and so on. const slots = allRows .slice() .sort((a, b) => { @@ -965,14 +960,11 @@ router.post( if (a.startTime! > b.startTime!) return 1; return a.dailyNumber - b.dailyNumber; } - if (aHasTime) return -1; // NULLS LAST + if (aHasTime) return -1; if (bHasTime) return 1; return a.dailyNumber - b.dailyNumber; }) .map((r) => ({ startTime: r.startTime, endTime: r.endTime })); - // Two-phase update to avoid (date, daily_number) unique conflicts - // *during* the renumber. - // Phase 1: park into negative daily_numbers. for (let i = 0; i < data.orderedIds.length; i++) { const id = data.orderedIds[i]!; await tx @@ -980,8 +972,6 @@ router.post( .set({ dailyNumber: -(i + 1), updatedBy: userId }) .where(eq(executiveMeetingsTable.id, id)); } - // Phase 2: assign final daily_number = i+1 with the matching slot's - // start/end time. for (let i = 0; i < data.orderedIds.length; i++) { const id = data.orderedIds[i]!; const slot = slots[i]!; @@ -996,8 +986,6 @@ router.post( .where(eq(executiveMeetingsTable.id, id)); } await logAudit(tx, { - // Specific reorder action namespace so audit consumers can filter - // schedule reorders separately from per-row meeting writes. action: "executive_meeting.reorder", entityType: "meeting", entityId: data.orderedIds[0]!, @@ -1014,7 +1002,6 @@ router.post( }, performedBy: userId, }); - // Defensive post-check: dailyNumbers must be exactly 1..N. const updated = await tx .select() .from(executiveMeetingsTable) diff --git a/artifacts/api-server/tests/executive-meetings.test.mjs b/artifacts/api-server/tests/executive-meetings.test.mjs index 54819262..372bb9c7 100644 --- a/artifacts/api-server/tests/executive-meetings.test.mjs +++ b/artifacts/api-server/tests/executive-meetings.test.mjs @@ -585,9 +585,6 @@ test("Sanitization: text-align on
survives a round-trip via PATCH/GET", asyn 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 }); @@ -604,7 +601,6 @@ test("Sanitization: text-align on survives a round-trip via PATCH/GET", asyn
});
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";
const a = await api(adminCookie, "POST", "/api/executive-meetings", {
titleAr: "أول", titleEn: "First", meetingDate: reorderDate,
@@ -638,16 +634,12 @@ test("Reorder: POST /reorder renumbers a full day to 1..N and inherits slot time
const cAfter = byId.get(C.id);
const bAfter = byId.get(B.id);
const aAfter = byId.get(A.id);
- // Full-day reorder => dailyNumber recomputed as 1..N in the new order;
- // each row inherits the corresponding slot's start/end time.
assert.equal(cAfter.dailyNumber, 1);
assert.equal(bAfter.dailyNumber, 2);
assert.equal(aAfter.dailyNumber, 3);
assert.equal(cAfter.startTime.slice(0, 5), "09:00");
assert.equal(bAfter.startTime.slice(0, 5), "10:00");
assert.equal(aAfter.startTime.slice(0, 5), "11:00");
- // The reorder route must write an audit row with the namespaced action
- // `executive_meeting.reorder` so downstream filters can target it.
const auditRows = await pool.query(
`SELECT action, entity_type, entity_id, new_value
FROM executive_meeting_audit_logs
@@ -664,9 +656,6 @@ test("Reorder: POST /reorder renumbers a full day to 1..N and inherits slot time
});
test("Reorder: rejects orderedIds containing duplicates with code duplicate_ids", async () => {
- // Even though full-day membership would otherwise be satisfied, sending the
- // same id twice must be refused so we never assign the same dailyNumber to
- // two distinct rows during the two-phase renumber.
const reorderDate = "2050-04-04";
const a = await api(adminCookie, "POST", "/api/executive-meetings", {
titleAr: "أ", titleEn: "A", meetingDate: reorderDate,
diff --git a/artifacts/tx-os/src/components/editable-cell.tsx b/artifacts/tx-os/src/components/editable-cell.tsx
index 6ff1c32c..aba2d99a 100644
--- a/artifacts/tx-os/src/components/editable-cell.tsx
+++ b/artifacts/tx-os/src/components/editable-cell.tsx
@@ -57,12 +57,6 @@ const FONT_SIZE_OPTIONS = [
{ value: "28px", label: "28" },
];
-// Tiptap doesn't ship a font-size mark in v3.22 — we add one as a textStyle
-// attribute so it round-trips through `` (which
-// the server allowlists in sanitize.ts).
-//
-// Module augmentation: register setFontSize/unsetFontSize on Tiptap's
-// Commands map so call-sites stay strongly typed (no `as never` casts).
declare module "@tiptap/core" {
interface Commands