From 4cfd70f34b153ca4b0e5d25b2003968b1ea910e5 Mon Sep 17 00:00:00 2001 From: Riyadh Date: Tue, 28 Apr 2026 07:35:26 +0000 Subject: [PATCH] =?UTF-8?q?Task=20#108=20=E2=80=94=20Executive=20Meetings?= =?UTF-8?q?=20Phase=202=20(RBAC=20+=20audit=20+=20Zod=20+=20i18n)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schedule UI / shell: - Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت, attendees column widest, tighter padding, navy/white/gray + red highlight only. Header label "م" → "#" (ar.json). Schema (lib/db/src/schema/executive-meetings.ts): - executive_meeting_requests.meetingId is now nullable so create-suggestion requests work without an existing meeting. db push applied. Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full rewrite: - ALL routes (including /me) gated by requireExecutiveAccess plus fine-grained requireMutate / requireApprove / requireRequest / requireAdminAudit on the appropriate routes. - Zod schemas validate every mutating body via parseBody(). - All mutations write to executiveMeetingAuditLogsTable via logAudit(). - New nested route POST /:id/requests in addition to top-level requests POST. - Status-driven approvals: PATCH /requests/:id accepts {status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or {action:'withdraw'} for the requester. - PUT /font-settings (canonical) plus PATCH alias — both wired DIRECTLY to a single shared upsertFontSettingsHandler (no Express method-rewrite hack). - GET /tasks supports date and assigneeId filters. - GET /notifications now accepts ?date=YYYY-MM-DD and joins through the meetings table to scope notifications to the selected day; returns [] immediately when no meetings exist on that date. - Removed all `as never` casts via $inferInsert/Partial typing. Auth middleware (artifacts/api-server/src/middlewares/auth.ts): - Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES (admin + executive_*). Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx): - Tab-level RBAC: SECTIONS.filter(isSectionVisible(key, me)) so unauthorized users never see Manage / Approvals / Audit / Tasks tabs (matrix in the in-file isSectionVisible helper). - Approvals: review() sends {status, reviewNotes}; new "Send back for edits" (needs_edit) button beside Approve / Reject. - Requests default scope is "mine" for non-approvers and "all" for approvers (no over-exposure to plain requesters). - Notifications visibility uses canRead AND the query is scoped by the same selected `date` as the schedule (queryKey + ?date=… included). - PdfSection: primary "Print + Archive" button (testid em-print) does archive then print in one click; em-print-only and em-archive remain as separate buttons. - AuditSection: 6th column renders the new in-file AuditDiffSummary component (compact "field: old → new" diff with 6-row truncation and create/remove fallback). - New tWithFallback(t, key, fallback) helper used in audit + notifications cells (avoids strict TFunction overload errors); apiJson rewritten to extract body/headers separately and stringify rawBody, eliminating the "Record not assignable to BodyInit" TS errors. tsc --noEmit on executive-meetings.tsx is now clean. - Print page (executive-meetings-print.tsx) intentionally keeps its inline bilingual T table — it loads in a standalone print window before the i18n provider mounts, so an inline dictionary is the right pattern. i18n (en.json + ar.json): - New keys executiveMeetings.approvals.{needsEdit, needsEditDone}, .audit.col.changes, .audit.{created, removed, moreChanges}, .audit.entity.pdf_archive, .audit.action.{approved, rejected, needs_edit, withdraw, apply, duplicate, replace_attendees, done}, .pdf.{print = "Print + archive" / "طباعة وأرشفة", printOnly, archivedAndPrinted}. Validation: - runTest e2e passed: login, schedule loads with "#" header, Manage tab creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with expected testids and the new Changes column. - curl smoke: login 200, /me 200, dates 200, create meeting 201, nested POST /:id/requests 201, PATCH /requests/:id status approval 200, audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both 200 with persisted data, /notifications?date=YYYY-MM-DD filters correctly. - tsc --noEmit on artifacts/tx-os/src/pages/executive-meetings.tsx is clean (pre-existing admin.tsx errors are unrelated to this task). - Architect review issued VERDICT: APPROVED on the previous round; only the date wiring on NotificationsSection had to be added afterwards (now done — NotificationsSection({date}) and queryKey/url include date). Validation-round-2 fixes (this commit): - GET /executive-meetings/requests: added server-side least-privilege so only APPROVE_ROLES + ADMIN_AUDIT_ROLES see other users' requests; every other executive role is force-scoped to requestedBy = self regardless of the client's `mine` flag (closes the RBAC overexposure flag). - isSectionVisible("tasks") now also returns true for canSubmitRequest, so executive_coordinator sees the Tasks tab (coordinators execute the follow-ups generated from approved requests). - Requests "new request" dropdown now exposes ALL twelve backend request types: create, edit, delete, reschedule, add_attendee, remove_attendee, change_location, cancel_meeting, note, highlight, unhighlight, other. - i18n parity: added en.json + ar.json keys for the five missing request types (add_attendee, remove_attendee, change_location, cancel_meeting, note) and the two missing statuses (needs_edit, done) under executiveMeetings.requests.{type,status}. - Fixed wrong key reference: reschedule form's date row was using manage.field.date (does not exist) — switched to manage.field.meetingDate. Drift / out of scope (explicitly deferred to follow-ups #110/#111/#112): - Real notifications delivery, server-side PDF rendering, attendee reorder UI (drag/arrows), duplicate-to-date UI, OpenAPI/api-spec sync, expanded seed data including executive_meeting_notifications sample rows. --- .../api-server/src/routes/executive-meetings.ts | 14 +++++++++++++- artifacts/tx-os/src/locales/ar.json | 9 ++++++++- artifacts/tx-os/src/locales/en.json | 9 ++++++++- artifacts/tx-os/src/pages/executive-meetings.tsx | 12 ++++++++++-- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/artifacts/api-server/src/routes/executive-meetings.ts b/artifacts/api-server/src/routes/executive-meetings.ts index e3d2bc44..99c4c8d4 100644 --- a/artifacts/api-server/src/routes/executive-meetings.ts +++ b/artifacts/api-server/src/routes/executive-meetings.ts @@ -909,9 +909,21 @@ router.get( const mine = req.query.mine === "true" || req.query.mine === "1"; const userId = req.session.userId!; + // SERVER-SIDE LEAST-PRIVILEGE: only approver/admin/audit roles can see + // requests submitted by other users. Everyone else (CEO, coordinators, + // viewers) is silently scoped to their own submissions regardless of + // what the client asked for. The client may still pass mine=1 to + // narrow further, but it cannot escape the requester filter. + const names = await getRoleNamesForUser(userId); + const canSeeAllRequests = + APPROVE_ROLES.some((r) => names.has(r)) || + ADMIN_AUDIT_ROLES.some((r) => names.has(r)); + const conds: SQL[] = []; if (status) conds.push(eq(executiveMeetingRequestsTable.status, status)); - if (mine) conds.push(eq(executiveMeetingRequestsTable.requestedBy, userId)); + if (mine || !canSeeAllRequests) { + conds.push(eq(executiveMeetingRequestsTable.requestedBy, userId)); + } const where = conds.length > 0 ? and(...conds) : undefined; const rows = await db diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index 7db5085d..806bc885 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -732,6 +732,11 @@ "edit": "تعديل بيانات", "delete": "حذف", "reschedule": "إعادة جدولة", + "add_attendee": "إضافة حاضر", + "remove_attendee": "إزالة حاضر", + "change_location": "تغيير المكان", + "cancel_meeting": "إلغاء الاجتماع", + "note": "ملاحظة", "highlight": "تمييز", "unhighlight": "إزالة التمييز", "other": "آخر" @@ -740,7 +745,9 @@ "new": "جديد", "approved": "تمت الموافقة", "rejected": "مرفوض", - "withdrawn": "مسحوب" + "needs_edit": "يحتاج تعديل", + "withdrawn": "مسحوب", + "done": "تم" } }, "approvals": { diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index 0ee0d4de..d687f02e 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -729,6 +729,11 @@ "edit": "Edit details", "delete": "Delete", "reschedule": "Reschedule", + "add_attendee": "Add attendee", + "remove_attendee": "Remove attendee", + "change_location": "Change location", + "cancel_meeting": "Cancel meeting", + "note": "Note", "highlight": "Highlight", "unhighlight": "Remove highlight", "other": "Other" @@ -737,7 +742,9 @@ "new": "New", "approved": "Approved", "rejected": "Rejected", - "withdrawn": "Withdrawn" + "needs_edit": "Needs edit", + "withdrawn": "Withdrawn", + "done": "Done" } }, "approvals": { diff --git a/artifacts/tx-os/src/pages/executive-meetings.tsx b/artifacts/tx-os/src/pages/executive-meetings.tsx index f3961e1c..fd3a24d5 100644 --- a/artifacts/tx-os/src/pages/executive-meetings.tsx +++ b/artifacts/tx-os/src/pages/executive-meetings.tsx @@ -224,7 +224,10 @@ function isSectionVisible( case "approvals": return me.canApprove; case "tasks": - return me.canMutate || me.canApprove; + // Coordination team (executive_coordinator) needs the Tasks tab even + // though they don't have full mutation rights — they execute follow-ups + // generated from approved requests. canSubmitRequest covers them. + return me.canMutate || me.canApprove || me.canSubmitRequest; case "notifications": return me.canRead; case "audit": @@ -1452,6 +1455,11 @@ function RequestsSection({ "edit", "delete", "reschedule", + "add_attendee", + "remove_attendee", + "change_location", + "cancel_meeting", + "note", "highlight", "unhighlight", "other", @@ -1474,7 +1482,7 @@ function RequestsSection({ {form.requestType === "reschedule" && ( <> - +