From 08adb4aeea83e33b8ef1c4d6813fad4597c39f42 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 17 Jun 2026 17:59:32 +0200 Subject: [PATCH] feat(permissions): add inquiries resource (view/manage) + idempotent role backfill Co-Authored-By: Claude Fable 5 --- src/components/admin/roles/role-form.tsx | 5 ++++ src/lib/auth/permissions.ts | 1 + .../migrations/0092_inquiries_permission.sql | 25 +++++++++++++++++++ src/lib/db/schema/users.ts | 4 +++ src/lib/db/seed-permissions.ts | 24 ++++++++++++++++++ tests/helpers/factories.ts | 4 +++ 6 files changed, 63 insertions(+) create mode 100644 src/lib/db/migrations/0092_inquiries_permission.sql diff --git a/src/components/admin/roles/role-form.tsx b/src/components/admin/roles/role-form.tsx index 361f1422..3d2bb5c1 100644 --- a/src/components/admin/roles/role-form.tsx +++ b/src/components/admin/roles/role-form.tsx @@ -103,6 +103,10 @@ const DEFAULT_PERMISSIONS: Record> = { delete: false, change_stage: false, }, + inquiries: { + view: false, + manage: false, + }, }; const GROUP_LABELS: Record = { @@ -126,6 +130,7 @@ const GROUP_LABELS: Record = { admin: 'Administration', residential_clients: 'Residential Clients', residential_interests: 'Residential Interests', + inquiries: 'Inquiries', }; function formatAction(action: string): string { diff --git a/src/lib/auth/permissions.ts b/src/lib/auth/permissions.ts index 35aee7d1..1e042d98 100644 --- a/src/lib/auth/permissions.ts +++ b/src/lib/auth/permissions.ts @@ -69,6 +69,7 @@ export const PERMISSION_CATALOG = { ], residential_clients: ['view', 'create', 'edit', 'delete'], residential_interests: ['view', 'create', 'edit', 'delete', 'change_stage'], + inquiries: ['view', 'manage'], } as const satisfies { [R in PermissionResource]: ReadonlyArray & string>; }; diff --git a/src/lib/db/migrations/0092_inquiries_permission.sql b/src/lib/db/migrations/0092_inquiries_permission.sql new file mode 100644 index 00000000..5d063e9c --- /dev/null +++ b/src/lib/db/migrations/0092_inquiries_permission.sql @@ -0,0 +1,25 @@ +-- 0092_inquiries_permission.sql +-- ---------------------------------------------------------------------------- +-- New `inquiries` permission resource (view/manage) backing the top-level +-- Inquiries workbench (previously the inbox lived under /admin and was gated on +-- admin.view_audit_log, which sales roles don't have). +-- +-- Existing role rows are backfilled so the resource defaults to whatever the +-- role's `clients` access is: view ⟵ clients.view, manage ⟵ clients.create. +-- This lights up the right roles (anyone who can see/create clients) without a +-- manual per-role edit, and defaults to deny for read-only roles. +-- +-- New-key only and idempotent via the `? 'inquiries'` guard, so re-running is a +-- no-op. Per-user / port-role override tables are intentionally left untouched: +-- the deep-merge resolver fills missing leaves from the base role (same +-- reasoning as 0041). + +UPDATE roles +SET permissions = permissions || jsonb_build_object( + 'inquiries', jsonb_build_object( + 'view', COALESCE((permissions->'clients'->>'view')::boolean, false), + 'manage', COALESCE((permissions->'clients'->>'create')::boolean, false) + ) +) +WHERE permissions IS NOT NULL + AND NOT (permissions ? 'inquiries'); diff --git a/src/lib/db/schema/users.ts b/src/lib/db/schema/users.ts index 197365fa..10f4b4e2 100644 --- a/src/lib/db/schema/users.ts +++ b/src/lib/db/schema/users.ts @@ -162,6 +162,10 @@ export type RolePermissions = { delete: boolean; change_stage: boolean; }; + inquiries: { + view: boolean; + manage: boolean; + }; }; /** diff --git a/src/lib/db/seed-permissions.ts b/src/lib/db/seed-permissions.ts index b7354ae8..5018373a 100644 --- a/src/lib/db/seed-permissions.ts +++ b/src/lib/db/seed-permissions.ts @@ -88,6 +88,10 @@ export const ALL_PERMISSIONS: RolePermissions = { delete: true, change_stage: true, }, + inquiries: { + view: true, + manage: true, + }, }; export const DIRECTOR_PERMISSIONS: RolePermissions = { @@ -167,6 +171,10 @@ export const DIRECTOR_PERMISSIONS: RolePermissions = { delete: true, change_stage: true, }, + inquiries: { + view: true, + manage: true, + }, }; export const SALES_MANAGER_PERMISSIONS: RolePermissions = { @@ -246,6 +254,10 @@ export const SALES_MANAGER_PERMISSIONS: RolePermissions = { delete: false, change_stage: false, }, + inquiries: { + view: true, + manage: true, + }, }; export const SALES_AGENT_PERMISSIONS: RolePermissions = { @@ -325,6 +337,10 @@ export const SALES_AGENT_PERMISSIONS: RolePermissions = { delete: false, change_stage: false, }, + inquiries: { + view: true, + manage: true, + }, }; export const VIEWER_PERMISSIONS: RolePermissions = { @@ -410,6 +426,10 @@ export const VIEWER_PERMISSIONS: RolePermissions = { delete: false, change_stage: false, }, + inquiries: { + view: true, + manage: false, + }, }; // Residential Partner - for an outside party who handles residential @@ -498,4 +518,8 @@ export const RESIDENTIAL_PARTNER_PERMISSIONS: RolePermissions = { delete: false, change_stage: true, }, + inquiries: { + view: false, + manage: false, + }, }; diff --git a/tests/helpers/factories.ts b/tests/helpers/factories.ts index fd4da278..fd8a6310 100644 --- a/tests/helpers/factories.ts +++ b/tests/helpers/factories.ts @@ -384,6 +384,7 @@ export function makeFullPermissions(): RolePermissions { delete: true, change_stage: true, }, + inquiries: { view: true, manage: true }, }; } @@ -472,6 +473,7 @@ export function makeViewerPermissions(): RolePermissions { delete: false, change_stage: false, }, + inquiries: { view: true, manage: false }, }; } @@ -560,6 +562,7 @@ export function makeSalesAgentPermissions(): RolePermissions { delete: false, change_stage: false, }, + inquiries: { view: true, manage: true }, }; } @@ -648,6 +651,7 @@ export function makeSalesManagerPermissions(): RolePermissions { delete: true, change_stage: true, }, + inquiries: { view: true, manage: true }, }; }