From 60a18e1c7c4ee8641e9e2538a263221d09011102 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Thu, 30 Apr 2026 18:21:21 +0000 Subject: [PATCH] Task #162: Let admins pre-set required permissions while creating an app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Required permissions" section was previously edit-only because `POST /api/apps/:id/permissions` needs an app id, leaving a brief window where a freshly created app was visible to everyone before the admin could re-open the dialog and gate it. The Add app dialog now lets the admin pick required permissions up front and the new app + its `app_permissions` rows are written in a single transaction. Changes: - `lib/api-spec/openapi.yaml`: extended `CreateAppBody` with an optional `permissionIds: integer[]` field. Ran `pnpm --filter @workspace/api-spec run codegen` so `lib/api-zod` and `lib/api-client-react` reflect it. - `artifacts/api-server/src/routes/apps.ts`: `POST /apps` now de-dupes and pre-validates `permissionIds`, returns 404 if any id is unknown (without creating the app), and inside one transaction inserts the app, the `app_permissions` rows (with `.onConflictDoNothing()` against the composite primary key), and a single `permission_audit` row (`previousIds: []`, `newIds: requestedIds`). After the transaction it also writes one `app.permission.add` audit_logs entry per inserted permission so the admin log mirrors the post-create flow. - `artifacts/tx-os/src/pages/admin.tsx`: added `permissionIds: number[]` to `AppForm`, a new `NewAppPermissionsPicker` component (rendered only in create mode — edit mode keeps the existing `AppPermissionsEditor` with its impact preview) that lets admins add/remove permissions locally before submit, and wired `handleSaveApp` to forward the selected ids when creating. Existing edit path strips the field so the update payload remains unchanged. - `replit.md`: documented the new picker and POST /api/apps behavior. No impact preview is shown in the create-mode picker because a brand new app starts with zero users seeing it, so adding permissions cannot hide it from anyone. Code-review follow-up: tightened input validation so non-integer or non-positive `permissionIds` now return 400 with a clear error instead of being silently dropped by the previous filter. The legacy single-add endpoint already used this exact 400 message, so behavior stays consistent across both create and update paths. Verification: - `pnpm --filter @workspace/api-spec run codegen` passes. - `pnpm --filter @workspace/api-server` typechecks with no new errors (executive-meetings.ts errors are pre-existing and unrelated). - Ran the existing app-permission test suites (`app-permission-audit.test.mjs`, `app-permissions-crud.test.mjs`, `app-permissions-impact.test.mjs`) directly — all 16 tests pass. - Ran an e2e Playwright test (login as admin → Add app → pick a permission → save → verify the row shows 1 restriction → reopen and confirm the assigned permission). All steps passed. Follow-up proposed: automated tests for the new create-with-permissions endpoint behavior (#231). Replit-Task-Id: c229777c-4036-4a6a-b4cb-05ccc18f6c9b --- artifacts/api-server/src/routes/apps.ts | 95 +++++++++++- artifacts/tx-os/public/opengraph.jpg | Bin 25953 -> 24636 bytes artifacts/tx-os/src/pages/admin.tsx | 142 +++++++++++++++++- .../src/generated/api.schemas.ts | 2 + lib/api-spec/openapi.yaml | 5 + lib/api-zod/src/generated/api.ts | 6 + replit.md | 2 +- 7 files changed, 246 insertions(+), 6 deletions(-) diff --git a/artifacts/api-server/src/routes/apps.ts b/artifacts/api-server/src/routes/apps.ts index b441dfc2..b5ce451c 100644 --- a/artifacts/api-server/src/routes/apps.ts +++ b/artifacts/api-server/src/routes/apps.ts @@ -232,7 +232,78 @@ router.post("/apps", requireAdmin, async (req, res): Promise => { return; } - const [app] = await db.insert(appsTable).values(parsed.data).returning(); + // Accept an optional permissionIds[] alongside the app fields so admins + // can gate the app at creation time. Pulling it out before the insert + // keeps appsTable.values strictly typed against the Drizzle schema. + const { permissionIds: rawPermissionIds, ...appValues } = parsed.data; + // The zod schema already restricts permissionIds to numbers, but we + // explicitly reject non-integer / non-positive values with 400 here so + // a malformed request never silently drops ids — the admin should know + // their gate request was wrong instead of getting a partially gated app. + if ( + rawPermissionIds !== undefined && + rawPermissionIds.some((n) => !Number.isInteger(n) || n <= 0) + ) { + res.status(400).json({ + error: "permissionIds must be an array of positive integers", + }); + return; + } + const requestedPermissionIds = Array.from(new Set(rawPermissionIds ?? [])); + + // If permission ids were supplied, look them up upfront so we can 404 + // before creating the app row instead of leaving an orphaned app behind. + // We also need the names later for the per-permission audit entries. + const requestedPermissions = requestedPermissionIds.length > 0 + ? await db + .select({ id: permissionsTable.id, name: permissionsTable.name }) + .from(permissionsTable) + .where(inArray(permissionsTable.id, requestedPermissionIds)) + : []; + if (requestedPermissions.length !== requestedPermissionIds.length) { + const foundIds = new Set(requestedPermissions.map((p) => p.id)); + const missing = requestedPermissionIds.filter((id) => !foundIds.has(id)); + res.status(404).json({ + error: `Permission(s) not found: ${missing.join(", ")}`, + }); + return; + } + + // Create the app + insert its permission rows in a single transaction so + // the app never exists in an "unrestricted" state when the admin asked + // for permission gating. onConflictDoNothing keeps it idempotent against + // the (app_id, permission_id) primary key. + const { app, insertedPermissionIds } = await db.transaction(async (tx) => { + const [created] = await tx.insert(appsTable).values(appValues).returning(); + let inserted: number[] = []; + if (requestedPermissionIds.length > 0) { + const ins = await tx + .insert(appPermissionsTable) + .values( + requestedPermissionIds.map((permissionId) => ({ + appId: created.id, + permissionId, + })), + ) + .onConflictDoNothing() + .returning({ permissionId: appPermissionsTable.permissionId }); + inserted = ins.map((r) => r.permissionId); + // Brand-new app starts with no permissions, so previousIds=[] and the + // newIds is the sorted set of what we just attached. This matches the + // single-add endpoint's audit semantics so the history view renders + // a normal "added X" entry instead of a synthetic create marker. + await recordPermissionAudit(tx, { + targetKind: "app", + targetId: created.id, + changeKind: "app.permissions", + actorUserId: req.session.userId ?? null, + previousIds: [], + newIds: requestedPermissionIds, + }); + } + return { app: created, insertedPermissionIds: inserted }; + }); + await db.insert(auditLogsTable).values({ actorUserId: req.session.userId ?? null, action: "app.create", @@ -244,8 +315,30 @@ router.post("/apps", requireAdmin, async (req, res): Promise => { nameEn: app.nameEn, route: app.route, isActive: app.isActive, + permissionIds: requestedPermissionIds, }, }); + // Mirror the per-permission audit rows POST /apps/:id/permissions writes + // so the admin log shows the same "permission added" entries whether the + // gate was set at create time or in a follow-up dialog. We only emit + // entries for rows the transaction actually inserted. + if (insertedPermissionIds.length > 0) { + const nameById = new Map(requestedPermissions.map((p) => [p.id, p.name])); + await db.insert(auditLogsTable).values( + insertedPermissionIds.map((permissionId) => ({ + actorUserId: req.session.userId ?? null, + action: "app.permission.add", + targetType: "app" as const, + targetId: app.id, + metadata: { + slug: app.slug, + nameEn: app.nameEn, + permissionId, + permissionName: nameById.get(permissionId) ?? null, + }, + })), + ); + } res.status(201).json(app); }); diff --git a/artifacts/tx-os/public/opengraph.jpg b/artifacts/tx-os/public/opengraph.jpg index d9336aa63b8973dc19fec85be14095bf504753cd..efdba1fc3f9e2c00092a324d9a6d09bb87375044 100644 GIT binary patch literal 24636 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 void; +}) { + const { t } = useTranslation(); + const { data: allPermissions } = useListPermissions({ + query: { queryKey: getListPermissionsQueryKey() }, + }); + const [pendingId, setPendingId] = useState(""); + + const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]); + const selected = useMemo( + () => (allPermissions ?? []).filter((p) => selectedSet.has(p.id)), + [allPermissions, selectedSet], + ); + const available = useMemo( + () => (allPermissions ?? []).filter((p) => !selectedSet.has(p.id)), + [allPermissions, selectedSet], + ); + + const onAdd = () => { + if (pendingId === "" || typeof pendingId !== "number") return; + if (!selectedSet.has(pendingId)) onChange([...selectedIds, pendingId]); + setPendingId(""); + }; + const onRemove = (id: number) => { + onChange(selectedIds.filter((x) => x !== id)); + }; + + return ( +
+ +

+ {t("admin.appPermissions.help")} +

+ {selected.length === 0 ? ( +

+ {t("admin.appPermissions.none")} +

+ ) : ( +
    + {selected.map((p) => ( +
  • + {p.name} + +
  • + ))} +
+ )} +
+ + +
+
+ ); +} + function AppPermissionsEditor({ appId }: { appId: number }) { const { t } = useTranslation(); const queryClient = useQueryClient(); @@ -723,6 +831,7 @@ export default function AdminPage() { route: app.route ?? "/", color: app.color ?? "#6366f1", sortOrder: app.sortOrder ?? 0, + permissionIds: [], }, }); } @@ -863,14 +972,19 @@ export default function AdminPage() { ); }; - const emptyAppForm: AppForm = { nameAr: "", nameEn: "", slug: "", iconName: "Grid2X2", route: "/", color: "#6366f1", sortOrder: 0 }; + const emptyAppForm: AppForm = { nameAr: "", nameEn: "", slug: "", iconName: "Grid2X2", route: "/", color: "#6366f1", sortOrder: 0, permissionIds: [] }; const emptyServiceForm: ServiceForm = { nameAr: "", nameEn: "", descriptionAr: "", descriptionEn: "", price: "0.00", imageUrl: "", isAvailable: true }; const handleSaveApp = () => { if (!editingApp) return; if (editingApp.id) { + // Edit path: permissionIds aren't part of the update payload — the + // existing AppPermissionsEditor manages them with its own impact + // preview + audit, so strip the field before sending. + const { permissionIds: _ignored, ...updateData } = editingApp.form; + void _ignored; updateApp.mutate( - { id: editingApp.id, data: editingApp.form }, + { id: editingApp.id, data: updateData }, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ADMIN_APPS_KEY }); @@ -880,8 +994,17 @@ export default function AdminPage() { }, ); } else { + const { permissionIds, ...rest } = editingApp.form; createApp.mutate( - { data: editingApp.form }, + { + // Only include permissionIds if the admin actually picked some + // so the request body stays minimal and the server's "no + // restriction" path is unchanged. + data: + permissionIds.length > 0 + ? { ...rest, permissionIds } + : rest, + }, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ADMIN_APPS_KEY }); @@ -995,8 +1118,18 @@ export default function AdminPage() { /> ))} - {editingApp.id !== undefined && ( + {editingApp.id !== undefined ? ( + ) : ( + + setEditingApp({ + ...editingApp, + form: { ...editingApp.form, permissionIds: next }, + }) + } + /> )} {editingApp.id !== undefined && (