snipe-auth-rbac 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,6 +37,7 @@ __export(admin_exports, {
37
37
  useDeleteRole: () => useDeleteRole,
38
38
  useInviteCompanyMember: () => useInviteCompanyMember,
39
39
  useRolePermissionGrid: () => useRolePermissionGrid,
40
+ useRolePermissionOverrides: () => useRolePermissionOverrides,
40
41
  useSetRolePermissionCell: () => useSetRolePermissionCell,
41
42
  useUpdateRole: () => useUpdateRole
42
43
  });
@@ -225,6 +226,32 @@ function createSupabaseAdminClient(opts) {
225
226
  }
226
227
  return data ?? [];
227
228
  },
229
+ async listRolePermissionOverrides(roleId) {
230
+ const { data, error } = await rbac.from("role_permission_overrides").select("role_id, resource, action").eq("role_id", roleId);
231
+ if (error) {
232
+ if (/role_permission_overrides/i.test(error.message) && /does not exist/i.test(error.message)) {
233
+ return [];
234
+ }
235
+ throw new Error(`listRolePermissionOverrides: ${error.message}`);
236
+ }
237
+ return data ?? [];
238
+ },
239
+ async setRolePermissionOverride({ role_id, resource, action, suppress }) {
240
+ if (suppress) {
241
+ const { error: error2 } = await rbac.from("role_permission_overrides").upsert(
242
+ { role_id, resource, action },
243
+ { onConflict: "role_id,resource,action" }
244
+ );
245
+ if (error2) {
246
+ throw new Error(`setRolePermissionOverride(insert): ${error2.message}`);
247
+ }
248
+ return;
249
+ }
250
+ const { error } = await rbac.from("role_permission_overrides").delete().eq("role_id", role_id).eq("resource", resource).eq("action", action);
251
+ if (error) {
252
+ throw new Error(`setRolePermissionOverride(delete): ${error.message}`);
253
+ }
254
+ },
228
255
  async applyTemplateDefaults({ role_id, only_missing = true }) {
229
256
  const { data, error } = await rbac.rpc("apply_template_defaults", {
230
257
  p_role_id: role_id,
@@ -408,6 +435,57 @@ function useAdminResourceDependencies() {
408
435
  [transport]
409
436
  );
410
437
  }
438
+ function useRolePermissionOverrides(roleId) {
439
+ const transport = useAdminTransport();
440
+ const [overrides, setOverrides] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
441
+ const [error, setError] = (0, import_react.useState)(null);
442
+ const fetchOverrides = (0, import_react.useCallback)(async () => {
443
+ if (!roleId) {
444
+ setOverrides(/* @__PURE__ */ new Set());
445
+ return;
446
+ }
447
+ try {
448
+ const rows = await transport.listRolePermissionOverrides(roleId);
449
+ setOverrides(new Set(rows.map((r) => `${r.resource}:${r.action}`)));
450
+ setError(null);
451
+ } catch (e) {
452
+ setError(e instanceof Error ? e : new Error(String(e)));
453
+ }
454
+ }, [transport, roleId]);
455
+ (0, import_react.useEffect)(() => {
456
+ void fetchOverrides();
457
+ }, [fetchOverrides]);
458
+ const setOverride = (0, import_react.useCallback)(
459
+ async (resource, action, suppress) => {
460
+ if (!roleId) {
461
+ return;
462
+ }
463
+ const key = `${resource}:${action}`;
464
+ setOverrides((prev) => {
465
+ const next = new Set(prev);
466
+ if (suppress) {
467
+ next.add(key);
468
+ } else {
469
+ next.delete(key);
470
+ }
471
+ return next;
472
+ });
473
+ try {
474
+ await transport.setRolePermissionOverride({
475
+ role_id: roleId,
476
+ resource,
477
+ action,
478
+ suppress
479
+ });
480
+ } catch (e) {
481
+ setError(e instanceof Error ? e : new Error(String(e)));
482
+ }
483
+ void fetchOverrides();
484
+ },
485
+ [transport, roleId, fetchOverrides]
486
+ );
487
+ return { overrides, setOverride, error, refresh: fetchOverrides };
488
+ }
411
489
  function useCreateCompany() {
412
490
  const transport = useAdminTransport();
413
491
  return useMutation(transport.createCompany);
@@ -419,10 +497,9 @@ function useInviteCompanyMember() {
419
497
  function useRolePermissionGrid(roleId) {
420
498
  const { data, isLoading, error, refresh } = useAdminRolePermissions(roleId);
421
499
  const dependencies = useAdminResourceDependencies();
500
+ const overridesHook = useRolePermissionOverrides(roleId);
422
501
  const setCell = useSetRolePermissionCell();
423
502
  const transport = useAdminTransport();
424
- const [isCascading, setCascading] = (0, import_react.useState)(false);
425
- const [cascadeError, setCascadeError] = (0, import_react.useState)(null);
426
503
  const grid = (0, import_react.useMemo)(() => {
427
504
  const out = {};
428
505
  for (const row of data ?? []) {
@@ -435,100 +512,90 @@ function useRolePermissionGrid(roleId) {
435
512
  }
436
513
  return out;
437
514
  }, [data]);
438
- const originGrid = (0, import_react.useMemo)(() => {
439
- const out = {};
440
- for (const row of data ?? []) {
441
- out[row.resource] = {
442
- read: row.read_granted_via ?? null,
443
- write: row.write_granted_via ?? null,
444
- update: row.update_granted_via ?? null,
445
- delete: row.delete_granted_via ?? null
446
- };
447
- }
448
- return out;
449
- }, [data]);
450
- const edgesByParent = (0, import_react.useMemo)(() => {
515
+ const parentsByChild = (0, import_react.useMemo)(() => {
451
516
  const map = /* @__PURE__ */ new Map();
452
517
  for (const edge of dependencies.data ?? []) {
453
- const list = map.get(edge.parent_resource) ?? [];
454
- map.set(edge.parent_resource, [
518
+ const list = map.get(edge.child_resource) ?? [];
519
+ map.set(edge.child_resource, [
455
520
  ...list,
456
- { child: edge.child_resource, action: edge.action }
521
+ { parent: edge.parent_resource, action: edge.action }
457
522
  ]);
458
523
  }
459
524
  return map;
460
525
  }, [dependencies.data]);
526
+ const originGrid = (0, import_react.useMemo)(() => {
527
+ const out = {};
528
+ const resources = new Set(Object.keys(grid));
529
+ for (const child of parentsByChild.keys()) {
530
+ resources.add(child);
531
+ }
532
+ const overrides = overridesHook.overrides;
533
+ for (const resource of resources) {
534
+ const directs = grid[resource];
535
+ const cellOrigins = {
536
+ read: null,
537
+ write: null,
538
+ update: null,
539
+ delete: null
540
+ };
541
+ for (const action of ACTIONS) {
542
+ if (overrides.has(`${resource}:${action}`)) {
543
+ cellOrigins[action] = "override";
544
+ continue;
545
+ }
546
+ if (directs?.[action]) {
547
+ cellOrigins[action] = "direct";
548
+ continue;
549
+ }
550
+ const parents = parentsByChild.get(resource) ?? [];
551
+ const impliedFrom = parents.find(
552
+ (p) => p.action === action && grid[p.parent]?.[action] === true
553
+ );
554
+ cellOrigins[action] = impliedFrom ? impliedFrom.parent : null;
555
+ }
556
+ out[resource] = cellOrigins;
557
+ }
558
+ return out;
559
+ }, [grid, parentsByChild, overridesHook.overrides]);
461
560
  const updateCell = (0, import_react.useCallback)(
462
561
  async (resource, action, value) => {
463
562
  if (!roleId) {
464
563
  return;
465
564
  }
466
- const writes = [
467
- { role_id: roleId, resource, action, value, grantedVia: null }
468
- ];
469
- if (value) {
470
- const edges = edgesByParent.get(resource) ?? [];
471
- for (const edge of edges) {
472
- if (edge.action !== action) {
473
- continue;
474
- }
475
- const childRow = (data ?? []).find((r) => r.resource === edge.child);
476
- const childValue = childRow?.[ACTION_FIELD[action]] === true;
477
- const childOrigin = childRow?.[ORIGIN_FIELD[action]] ?? null;
478
- if (childValue && childOrigin == null) {
479
- continue;
480
- }
481
- writes.push({
482
- role_id: roleId,
483
- resource: edge.child,
484
- action,
485
- value: true,
486
- grantedVia: resource
487
- });
488
- }
489
- }
490
- setCascading(true);
491
- setCascadeError(null);
492
- try {
493
- const [first, ...rest] = writes;
494
- if (first && rest.length === 0) {
495
- await setCell.mutate(first);
496
- } else {
497
- await transport.batchSetRolePermissionCells(writes);
498
- }
499
- void refresh();
500
- } catch (e) {
501
- setCascadeError(e instanceof Error ? e : new Error(String(e)));
502
- throw e;
503
- } finally {
504
- setCascading(false);
505
- }
565
+ await setCell.mutate({
566
+ role_id: roleId,
567
+ resource,
568
+ action,
569
+ value,
570
+ grantedVia: null
571
+ });
572
+ void refresh();
506
573
  },
507
- [roleId, setCell, refresh, edgesByParent, data, transport]
574
+ [roleId, setCell, refresh]
508
575
  );
576
+ void transport;
509
577
  return {
510
578
  grid,
511
579
  originGrid,
580
+ parentsByChild,
581
+ /** 0.6.0+. Set of `"<resource>:<action>"` for this role's overrides. */
582
+ overrides: overridesHook.overrides,
583
+ /**
584
+ * 0.6.0+. Suppress (`suppress=true`) or restore (`suppress=false`)
585
+ * an implied permission for this role. The grid + originGrid
586
+ * re-render with `'override'` state on the cell as soon as the
587
+ * optimistic flip lands.
588
+ */
589
+ setOverride: overridesHook.setOverride,
512
590
  isLoading: isLoading || dependencies.isLoading,
513
- error: error ?? dependencies.error,
591
+ error: error ?? dependencies.error ?? overridesHook.error,
514
592
  refresh,
515
593
  updateCell,
516
- isUpdating: setCell.isPending || isCascading,
517
- updateError: setCell.error ?? cascadeError
594
+ isUpdating: setCell.isPending,
595
+ updateError: setCell.error
518
596
  };
519
597
  }
520
- var ACTION_FIELD = {
521
- read: "can_read",
522
- write: "can_write",
523
- update: "can_update",
524
- delete: "can_delete"
525
- };
526
- var ORIGIN_FIELD = {
527
- read: "read_granted_via",
528
- write: "write_granted_via",
529
- update: "update_granted_via",
530
- delete: "delete_granted_via"
531
- };
598
+ var ACTIONS = ["read", "write", "update", "delete"];
532
599
 
533
600
  // src/admin/PermissionsMatrix.tsx
534
601
  var import_react2 = require("react");
@@ -550,15 +617,27 @@ function groupResources(registry) {
550
617
 
551
618
  // src/admin/PermissionsMatrix.tsx
552
619
  var import_jsx_runtime2 = require("react/jsx-runtime");
553
- var ACTIONS = ["read", "write", "update", "delete"];
620
+ var ACTIONS2 = ["read", "write", "update", "delete"];
554
621
  function PermissionsMatrix(props) {
555
- const { grid, originGrid, isLoading, error, updateCell, isUpdating } = useRolePermissionGrid(props.roleId);
622
+ const {
623
+ grid,
624
+ originGrid,
625
+ isLoading,
626
+ error,
627
+ updateCell,
628
+ isUpdating,
629
+ setOverride: gridSetOverride
630
+ } = useRolePermissionGrid(props.roleId);
556
631
  const groups = (0, import_react2.useMemo)(
557
632
  () => groupResources(props.resources),
558
633
  [props.resources]
559
634
  );
560
635
  const isCellEnabled = (resource, action) => {
561
- return grid[resource]?.[action] ?? false;
636
+ const origin = originGrid[resource]?.[action];
637
+ if (origin == null || origin === "override") {
638
+ return false;
639
+ }
640
+ return true;
562
641
  };
563
642
  const cellOrigin = (resource, action) => {
564
643
  const origin = originGrid[resource]?.[action];
@@ -567,15 +646,19 @@ function PermissionsMatrix(props) {
567
646
  const setCell = async (resource, action, value) => {
568
647
  await updateCell(resource, action, value);
569
648
  };
649
+ const setOverride = async (resource, action, suppress) => {
650
+ await gridSetOverride(resource, action, suppress);
651
+ };
570
652
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: props.children({
571
653
  groups,
572
654
  isCellEnabled,
573
655
  cellOrigin,
574
656
  setCell,
657
+ setOverride,
575
658
  isLoading,
576
659
  isUpdating,
577
660
  error,
578
- actions: ACTIONS
661
+ actions: ACTIONS2
579
662
  }) });
580
663
  }
581
664
 
@@ -717,6 +800,7 @@ function InviteMemberForm(props) {
717
800
  useDeleteRole,
718
801
  useInviteCompanyMember,
719
802
  useRolePermissionGrid,
803
+ useRolePermissionOverrides,
720
804
  useSetRolePermissionCell,
721
805
  useUpdateRole
722
806
  });