@burdenoff/microfe-billing 2026.514.2 → 2026.514.3
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.
- package/dist/billing/hooks/useBillingPermissions.d.ts +13 -1
- package/dist/billing/hooks/useBillingPermissions.d.ts.map +1 -1
- package/dist/billing/hooks/useBillingPermissions.js +49 -49
- package/dist/billing/hooks/useBillingPermissions.js.map +1 -1
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.d.ts.map +1 -1
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js +10 -10
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -17,7 +17,19 @@
|
|
|
17
17
|
* billingAccount.all → action: "manage" (admin bypass)
|
|
18
18
|
*
|
|
19
19
|
*/
|
|
20
|
-
export
|
|
20
|
+
export interface UseBillingPermissionsOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Caller-resolved billing account ID. When provided, this overrides the
|
|
23
|
+
* hook's internal `useBillingAccountSelection` resolution. Use this when
|
|
24
|
+
* the calling page already resolves a billing account (e.g. via the
|
|
25
|
+
* `useBillingAccounts` + `useBillingAccountSelection` pair) so the
|
|
26
|
+
* billing-scope permission fetch observes the *same* account ID and
|
|
27
|
+
* doesn't lag behind for first-time users whose default account only
|
|
28
|
+
* resolves once the accounts list loads.
|
|
29
|
+
*/
|
|
30
|
+
billingAccountIdOverride?: string | null;
|
|
31
|
+
}
|
|
32
|
+
export declare function useBillingPermissions(options?: UseBillingPermissionsOptions): {
|
|
21
33
|
/** @rbac(action: "read", resource: "billing") */
|
|
22
34
|
canViewBillingAccount: boolean;
|
|
23
35
|
/** @rbac(action: "create", resource: "billing") */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBillingPermissions.d.ts","sourceRoot":"","sources":["../../../src/billing/hooks/useBillingPermissions.ts"],"names":[],"mappings":"AA6CA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB;
|
|
1
|
+
{"version":3,"file":"useBillingPermissions.d.ts","sourceRoot":"","sources":["../../../src/billing/hooks/useBillingPermissions.ts"],"names":[],"mappings":"AA6CA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,4BAA4B;IAC3C;;;;;;;;OAQG;IACH,wBAAwB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1C;AAED,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,4BAA4B;IAyExE,iDAAiD;;IAEjD,mDAAmD;;IAEnD,mDAAmD;;IAEnD,2EAA2E;;IAI3E,iDAAiD;;IAEjD,+EAA+E;;IAE/E,mDAAmD;;IAEnD,wEAAwE;;IAIxE,iDAAiD;;IAEjD,wFAAwF;;IAIxF,iDAAiD;;IAEjD,mGAAmG;;IAInG,iDAAiD;;IAEjD,8EAA8E;;IAI9E,iDAAiD;;IAEjD,gEAAgE;;IAEhE,kFAAkF;;IAIlF,iDAAiD;;IAEjD,qEAAqE;;IAIrE,iDAAiD;;IAEjD,2FAA2F;;IAI3F,iDAAiD;;IAEjD,2EAA2E;;IAI3E,iDAAiD;;IAEjD,kEAAkE;;IAElE,6EAA6E;;IAE7E,kEAAkE;;IAIlE,0EAA0E;;IAE1E,oGAAoG;;IAIpG,iDAAiD;;IAIjD,4FAA4F;;IAI5F,mDAAmD;;IAInD,6GAA6G;;IAI7G,iGAAiG;;IAIjG,0CAA0C;;IAI1C,mEAAmE;;EAGtE"}
|
|
@@ -13,63 +13,63 @@ function p(e) {
|
|
|
13
13
|
return e.level === "ADMIN" && t.push(`${e.resourceType}:*`), t.push(...e.allowedActions.map((t) => `${e.resourceType}:${t}`)), t;
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
-
function m() {
|
|
17
|
-
let { isAuthenticated:
|
|
18
|
-
orgId:
|
|
16
|
+
function m(m) {
|
|
17
|
+
let { isAuthenticated: h } = i(), { hasPermission: g, isLoading: _ } = a(), { apiGatewayUrl: v, authToken: y, orgId: b } = e(), { selectedAccountId: x } = t({
|
|
18
|
+
orgId: b,
|
|
19
19
|
syncFromUrl: !0,
|
|
20
20
|
urlParamName: "billingAccountId"
|
|
21
|
-
}),
|
|
22
|
-
queryKey: ["billing-scope-permissions",
|
|
23
|
-
enabled:
|
|
21
|
+
}), S = m?.billingAccountIdOverride ?? x ?? void 0, C = o({
|
|
22
|
+
queryKey: ["billing-scope-permissions", S],
|
|
23
|
+
enabled: h && !!S,
|
|
24
24
|
staleTime: 300 * 1e3,
|
|
25
25
|
queryFn: async () => p((await n({
|
|
26
|
-
apiGatewayUrl:
|
|
27
|
-
authToken:
|
|
28
|
-
orgId:
|
|
26
|
+
apiGatewayUrl: v,
|
|
27
|
+
authToken: y,
|
|
28
|
+
orgId: b,
|
|
29
29
|
query: f,
|
|
30
|
-
variables: { scopeId:
|
|
30
|
+
variables: { scopeId: S }
|
|
31
31
|
}))?.myPermissions?.permissions ?? [])
|
|
32
|
-
}),
|
|
32
|
+
}), w = r(() => C.data ?? [], [C.data]), T = (e) => {
|
|
33
33
|
let [t] = e.split(/[:.]/);
|
|
34
|
-
return
|
|
35
|
-
},
|
|
34
|
+
return w.includes(e) || w.includes(`${t}:*`);
|
|
35
|
+
}, E = (e) => e.some((e) => g(e) || T(e)), D = h && E(s), O = h && E(c), k = h && E(l), A = h && E(u), j = h && E(d);
|
|
36
36
|
return {
|
|
37
|
-
canViewBillingAccount:
|
|
38
|
-
canCreateBillingAccount:
|
|
39
|
-
canManageBillingAccount:
|
|
40
|
-
canArchiveBillingAccount:
|
|
41
|
-
canViewSubscriptions:
|
|
42
|
-
canCreateSubscription:
|
|
43
|
-
canUpdateSubscription:
|
|
44
|
-
canCancelSubscription:
|
|
45
|
-
canViewPlans:
|
|
46
|
-
canManagePlans:
|
|
47
|
-
canViewAddons:
|
|
48
|
-
canManageAddons:
|
|
49
|
-
canViewQuotas:
|
|
50
|
-
canManageQuotas:
|
|
51
|
-
canViewInvoices:
|
|
52
|
-
canPayInvoice:
|
|
53
|
-
canVoidInvoice:
|
|
54
|
-
canViewPayments:
|
|
55
|
-
canProcessPayment:
|
|
56
|
-
canViewPaymentMethods:
|
|
57
|
-
canManagePaymentMethods:
|
|
58
|
-
canViewUsage:
|
|
59
|
-
canRecordUsage:
|
|
60
|
-
canViewCredits:
|
|
61
|
-
canIssueCredit:
|
|
62
|
-
canWithdrawCredits:
|
|
63
|
-
canGrantCredits:
|
|
64
|
-
canApplyDiscount:
|
|
65
|
-
canManageDiscounts:
|
|
66
|
-
canViewAnalytics:
|
|
67
|
-
canProcessRefund:
|
|
68
|
-
canManageRecurringBilling:
|
|
69
|
-
canManageEntitlements:
|
|
70
|
-
canManageTaxRates:
|
|
71
|
-
isBillingAdmin:
|
|
72
|
-
isLoading:
|
|
37
|
+
canViewBillingAccount: D,
|
|
38
|
+
canCreateBillingAccount: O,
|
|
39
|
+
canManageBillingAccount: k,
|
|
40
|
+
canArchiveBillingAccount: A,
|
|
41
|
+
canViewSubscriptions: D,
|
|
42
|
+
canCreateSubscription: O,
|
|
43
|
+
canUpdateSubscription: k,
|
|
44
|
+
canCancelSubscription: A,
|
|
45
|
+
canViewPlans: D,
|
|
46
|
+
canManagePlans: j,
|
|
47
|
+
canViewAddons: D,
|
|
48
|
+
canManageAddons: j,
|
|
49
|
+
canViewQuotas: D,
|
|
50
|
+
canManageQuotas: j,
|
|
51
|
+
canViewInvoices: D,
|
|
52
|
+
canPayInvoice: k,
|
|
53
|
+
canVoidInvoice: j,
|
|
54
|
+
canViewPayments: D,
|
|
55
|
+
canProcessPayment: O,
|
|
56
|
+
canViewPaymentMethods: D,
|
|
57
|
+
canManagePaymentMethods: k,
|
|
58
|
+
canViewUsage: D,
|
|
59
|
+
canRecordUsage: k,
|
|
60
|
+
canViewCredits: D,
|
|
61
|
+
canIssueCredit: k,
|
|
62
|
+
canWithdrawCredits: k,
|
|
63
|
+
canGrantCredits: j,
|
|
64
|
+
canApplyDiscount: k,
|
|
65
|
+
canManageDiscounts: j,
|
|
66
|
+
canViewAnalytics: D,
|
|
67
|
+
canProcessRefund: j,
|
|
68
|
+
canManageRecurringBilling: j,
|
|
69
|
+
canManageEntitlements: j,
|
|
70
|
+
canManageTaxRates: j,
|
|
71
|
+
isBillingAdmin: j,
|
|
72
|
+
isLoading: _ || !!S && C.isLoading
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBillingPermissions.js","names":[],"sources":["../../../src/billing/hooks/useBillingPermissions.ts"],"sourcesContent":["import { useQuery } from '@tanstack/react-query';\nimport { useMemo } from 'react';\nimport { useAuth, usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useBilling } from '../providers/BillingProvider';\nimport { useBillingAccountSelection } from './useBillingAccountSelection';\nimport { directBillingGraphqlRequest } from '../shared/utils';\n\n// Permission strings emitted by global-billing-svc @rbac directives.\n// We accept both `:` and `.` separators because the RBAC service has\n// historically returned both shapes in `myPermissions.permissions[]`.\nconst BILLING_READ = ['billing:read', 'billing.read'];\nconst BILLING_CREATE = ['billing:create', 'billing.create'];\nconst BILLING_UPDATE = ['billing:update', 'billing.update'];\nconst BILLING_DELETE = ['billing:delete', 'billing.delete'];\nconst BILLING_MANAGE = ['billing:manage', 'billing.manage'];\n\nconst BILLING_SCOPE_PERMISSIONS_QUERY = `\n query BillingScopeMyPermissions($scopeId: String!) {\n myPermissions(scopeId: $scopeId) {\n actorId\n scopeId\n permissions {\n resourceType\n level\n allowedActions\n }\n }\n }\n`;\n\ninterface RawPerm {\n resourceType: string;\n level: string;\n allowedActions: string[];\n}\n\nfunction permsToStrings(perms: RawPerm[]): string[] {\n return perms.flatMap((p) => {\n const out: string[] = [];\n if (p.level === 'ADMIN') out.push(`${p.resourceType}:*`);\n out.push(...p.allowedActions.map((a) => `${p.resourceType}:${a}`));\n return out;\n });\n}\n\n/**\n * Hook for billing-specific RBAC permissions.\n *\n * Permission model aligns with the `@rbac` directives on global-billing-svc:\n * resource: \"billing\"\n * actions: \"read\" – view billing data\n * \"create\" – create billing accounts, initiate payments\n * \"update\" – modify billing accounts, payment methods, usage\n * \"delete\" – remove payment methods, cancel subscriptions\n * \"manage\" – admin-only (plans, addons, quotas, discounts, refunds, tax rates)\n *\n * The RBAC service resolves these via `billingAccount.*` permission strings:\n * billingAccount.read → action: \"read\"\n * billingAccount.create → action: \"create\"\n * billingAccount.update → action: \"update\"\n * billingAccount.delete → action: \"delete\"\n * billingAccount.all → action: \"manage\" (admin bypass)\n *\n */\nexport function useBillingPermissions() {\n const { isAuthenticated } = useAuth();\n // Org-scope permissions from the top-level PermissionProvider. Covers\n // Org Owner / Org Admin permissions on the `billing` resource.\n const { hasPermission: hasOrgPermission, isLoading: orgIsLoading } = usePermissions();\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n\n // Billing-account-scope permissions. Covers Billing Admin / Billing Viewer\n // role assignments on the currently-active billing account. Required\n // because PermissionProvider only fetches a single scope (org), so a\n // user who is *only* a Billing Admin (not Org Owner) would otherwise see\n // empty perms on the billing pages.\n // Resolve account scope from the current URL when present, otherwise fall\n // back to the last selected billing account stored for this org. That keeps\n // route guards aligned with SPA navigation *and* with list/detail flows that\n // rely on persisted account selection.\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const billingAccountId = selectedAccountId ?? undefined;\n const billingScopeQuery = useQuery({\n queryKey: ['billing-scope-permissions', billingAccountId],\n enabled: isAuthenticated && Boolean(billingAccountId),\n staleTime: 5 * 60 * 1000,\n queryFn: async () => {\n const data = await directBillingGraphqlRequest<{\n myPermissions: { permissions: RawPerm[] };\n }>({\n apiGatewayUrl,\n authToken,\n orgId,\n query: BILLING_SCOPE_PERMISSIONS_QUERY,\n variables: { scopeId: billingAccountId },\n });\n return permsToStrings(data?.myPermissions?.permissions ?? []);\n },\n });\n\n const billingScopeStrings = useMemo<string[]>(\n () => billingScopeQuery.data ?? [],\n [billingScopeQuery.data]\n );\n\n const matchesBillingScope = (permission: string): boolean => {\n // Wildcard match: e.g. `billing:read` matches stored `billing:*`.\n const [resource] = permission.split(/[:.]/);\n return (\n billingScopeStrings.includes(permission) || billingScopeStrings.includes(`${resource}:*`)\n );\n };\n\n // Pure RBAC: every check goes through the gateway-evaluated permission\n // set returned by `myPermissions`. No email-domain or role-name fallback —\n // those legacy escape hatches caused divergence between UI and gateway\n // enforcement and have been removed now that billing-scope roles\n // (Billing Admin / Billing Viewer) are auto-assigned on account creation.\n const hasAnyPermission = (permissions: string[]): boolean =>\n permissions.some((p) => hasOrgPermission(p) || matchesBillingScope(p));\n\n const canRead = isAuthenticated && hasAnyPermission(BILLING_READ);\n const canCreate = isAuthenticated && hasAnyPermission(BILLING_CREATE);\n const canUpdate = isAuthenticated && hasAnyPermission(BILLING_UPDATE);\n const canDelete = isAuthenticated && hasAnyPermission(BILLING_DELETE);\n const canManage = isAuthenticated && hasAnyPermission(BILLING_MANAGE);\n\n const isLoading = orgIsLoading || (Boolean(billingAccountId) && billingScopeQuery.isLoading);\n\n return {\n // ── Billing account ───────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewBillingAccount: canRead,\n /** @rbac(action: \"create\", resource: \"billing\") */\n canCreateBillingAccount: canCreate,\n /** @rbac(action: \"update\", resource: \"billing\") */\n canManageBillingAccount: canUpdate,\n /** @rbac(action: \"delete\", resource: \"billing\") — archiveBillingAccount */\n canArchiveBillingAccount: canDelete,\n\n // ── Subscriptions ─────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewSubscriptions: canRead,\n /** @rbac(action: \"create\", resource: \"billing\") — initiatePayment for plans */\n canCreateSubscription: canCreate,\n /** @rbac(action: \"update\", resource: \"billing\") */\n canUpdateSubscription: canUpdate,\n /** @rbac(action: \"delete\", resource: \"billing\") — cancelSubscription */\n canCancelSubscription: canDelete,\n\n // ── Plans (admin-only management) ─────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewPlans: canRead,\n /** @rbac(action: \"manage\", resource: \"billing\") — createPlan, updatePlan, togglePlan */\n canManagePlans: canManage,\n\n // ── Addons (admin-only management) ────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewAddons: canRead,\n /** @rbac(action: \"manage\", resource: \"billing\") — createAddOns, updateAddOnDetails, toggleAddOn */\n canManageAddons: canManage,\n\n // ── Quotas (admin-only management) ────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewQuotas: canRead,\n /** @rbac(action: \"manage\", resource: \"billing\") — createQuota, updateQuota */\n canManageQuotas: canManage,\n\n // ── Invoices ──────────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewInvoices: canRead,\n /** @rbac(action: \"update\", resource: \"billing\") — payInvoice */\n canPayInvoice: canUpdate,\n /** @rbac(action: \"manage\", resource: \"billing\") — voidInvoice, generateInvoice */\n canVoidInvoice: canManage,\n\n // ── Payments ──────────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewPayments: canRead,\n /** @rbac(action: \"create\", resource: \"billing\") — initiatePayment */\n canProcessPayment: canCreate,\n\n // ── Payment Methods ───────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewPaymentMethods: canRead,\n /** @rbac(action: \"update\", resource: \"billing\") — addPaymentMethod, updatePaymentMethod */\n canManagePaymentMethods: canUpdate,\n\n // ── Usage ─────────────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewUsage: canRead,\n /** @rbac(action: \"update\", resource: \"billing\") — addUsage, freeUpUsage */\n canRecordUsage: canUpdate,\n\n // ── Credits ───────────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewCredits: canRead,\n /** @rbac(action: \"update\", resource: \"billing\") — spendCredits */\n canIssueCredit: canUpdate,\n /** @rbac(action: \"update\", resource: \"billing\") — requestCreditWithdrawal */\n canWithdrawCredits: canUpdate,\n /** @rbac(action: \"manage\", resource: \"billing\") — grantCredits */\n canGrantCredits: canManage,\n\n // ── Discounts ─────────────────────────────────────────────────────\n /** @rbac(action: \"update\", resource: \"billing\") — applyBillingDiscount */\n canApplyDiscount: canUpdate,\n /** @rbac(action: \"manage\", resource: \"billing\") — createDiscount, updateDiscount, deleteDiscount */\n canManageDiscounts: canManage,\n\n // ── Analytics ─────────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewAnalytics: canRead,\n\n // ── Refunds (admin-only) ──────────────────────────────────────────\n /** @rbac(action: \"manage\", resource: \"billing\") — processRefund, skipRefund, retryRefund */\n canProcessRefund: canManage,\n\n // ── Recurring Billing (admin/service-only) ────────────────────────\n /** @rbac(action: \"manage\", resource: \"billing\") */\n canManageRecurringBilling: canManage,\n\n // ── Entitlements (admin-only) ─────────────────────────────────────\n /** @rbac(action: \"manage\", resource: \"billing\") — createEntitlement, updateEntitlement, deleteEntitlement */\n canManageEntitlements: canManage,\n\n // ── Tax Rates (admin-only) ────────────────────────────────────────\n /** @rbac(action: \"manage\", resource: \"billing\") — createTaxRate, updateTaxRate, deleteTaxRate */\n canManageTaxRates: canManage,\n\n // ── Admin check ───────────────────────────────────────────────────\n /** Full billing admin (billing:manage) */\n isBillingAdmin: canManage,\n\n // ── Loading state ─────────────────────────────────────────────────\n /** Whether permissions are still being loaded from RBAC service */\n isLoading,\n };\n}\n"],"mappings":";;;;;;;;AAUA,IAAM,IAAe,CAAC,gBAAgB,eAAe,EAC/C,IAAiB,CAAC,kBAAkB,iBAAiB,EACrD,IAAiB,CAAC,kBAAkB,iBAAiB,EACrD,IAAiB,CAAC,kBAAkB,iBAAiB,EACrD,IAAiB,CAAC,kBAAkB,iBAAiB,EAErD,IAAkC;AAoBxC,SAAS,EAAe,GAA4B;AAClD,QAAO,EAAM,SAAS,MAAM;EAC1B,IAAM,IAAgB,EAAE;AAGxB,SAFI,EAAE,UAAU,WAAS,EAAI,KAAK,GAAG,EAAE,aAAa,IAAI,EACxD,EAAI,KAAK,GAAG,EAAE,eAAe,KAAK,MAAM,GAAG,EAAE,aAAa,GAAG,IAAI,CAAC,EAC3D;GACP;;AAsBJ,SAAgB,IAAwB;CACtC,IAAM,EAAE,uBAAoB,GAAS,EAG/B,EAAE,eAAe,GAAkB,WAAW,MAAiB,GAAgB,EAC/E,EAAE,kBAAe,cAAW,aAAU,GAAY,EAWlD,EAAE,yBAAsB,EAA2B;EACvD;EACA,aAAa;EACb,cAAc;EACf,CAAC,EACI,IAAmB,KAAqB,KAAA,GACxC,IAAoB,EAAS;EACjC,UAAU,CAAC,6BAA6B,EAAiB;EACzD,SAAS,KAAmB,EAAQ;EACpC,WAAW,MAAS;EACpB,SAAS,YAUA,GATM,MAAM,EAEhB;GACD;GACA;GACA;GACA,OAAO;GACP,WAAW,EAAE,SAAS,GAAkB;GACzC,CAAC,GAC0B,eAAe,eAAe,EAAE,CAAC;EAEhE,CAAC,EAEI,IAAsB,QACpB,EAAkB,QAAQ,EAAE,EAClC,CAAC,EAAkB,KAAK,CACzB,EAEK,KAAuB,MAAgC;EAE3D,IAAM,CAAC,KAAY,EAAW,MAAM,OAAO;AAC3C,SACE,EAAoB,SAAS,EAAW,IAAI,EAAoB,SAAS,GAAG,EAAS,IAAI;IASvF,KAAoB,MACxB,EAAY,MAAM,MAAM,EAAiB,EAAE,IAAI,EAAoB,EAAE,CAAC,EAElE,IAAU,KAAmB,EAAiB,EAAa,EAC3D,IAAY,KAAmB,EAAiB,EAAe,EAC/D,IAAY,KAAmB,EAAiB,EAAe,EAC/D,IAAY,KAAmB,EAAiB,EAAe,EAC/D,IAAY,KAAmB,EAAiB,EAAe;AAIrE,QAAO;EAGL,uBAAuB;EAEvB,yBAAyB;EAEzB,yBAAyB;EAEzB,0BAA0B;EAI1B,sBAAsB;EAEtB,uBAAuB;EAEvB,uBAAuB;EAEvB,uBAAuB;EAIvB,cAAc;EAEd,gBAAgB;EAIhB,eAAe;EAEf,iBAAiB;EAIjB,eAAe;EAEf,iBAAiB;EAIjB,iBAAiB;EAEjB,eAAe;EAEf,gBAAgB;EAIhB,iBAAiB;EAEjB,mBAAmB;EAInB,uBAAuB;EAEvB,yBAAyB;EAIzB,cAAc;EAEd,gBAAgB;EAIhB,gBAAgB;EAEhB,gBAAgB;EAEhB,oBAAoB;EAEpB,iBAAiB;EAIjB,kBAAkB;EAElB,oBAAoB;EAIpB,kBAAkB;EAIlB,kBAAkB;EAIlB,2BAA2B;EAI3B,uBAAuB;EAIvB,mBAAmB;EAInB,gBAAgB;EAIhB,WA7GgB,KAAiB,EAAQ,KAAqB,EAAkB;EA8GjF"}
|
|
1
|
+
{"version":3,"file":"useBillingPermissions.js","names":[],"sources":["../../../src/billing/hooks/useBillingPermissions.ts"],"sourcesContent":["import { useQuery } from '@tanstack/react-query';\nimport { useMemo } from 'react';\nimport { useAuth, usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useBilling } from '../providers/BillingProvider';\nimport { useBillingAccountSelection } from './useBillingAccountSelection';\nimport { directBillingGraphqlRequest } from '../shared/utils';\n\n// Permission strings emitted by global-billing-svc @rbac directives.\n// We accept both `:` and `.` separators because the RBAC service has\n// historically returned both shapes in `myPermissions.permissions[]`.\nconst BILLING_READ = ['billing:read', 'billing.read'];\nconst BILLING_CREATE = ['billing:create', 'billing.create'];\nconst BILLING_UPDATE = ['billing:update', 'billing.update'];\nconst BILLING_DELETE = ['billing:delete', 'billing.delete'];\nconst BILLING_MANAGE = ['billing:manage', 'billing.manage'];\n\nconst BILLING_SCOPE_PERMISSIONS_QUERY = `\n query BillingScopeMyPermissions($scopeId: String!) {\n myPermissions(scopeId: $scopeId) {\n actorId\n scopeId\n permissions {\n resourceType\n level\n allowedActions\n }\n }\n }\n`;\n\ninterface RawPerm {\n resourceType: string;\n level: string;\n allowedActions: string[];\n}\n\nfunction permsToStrings(perms: RawPerm[]): string[] {\n return perms.flatMap((p) => {\n const out: string[] = [];\n if (p.level === 'ADMIN') out.push(`${p.resourceType}:*`);\n out.push(...p.allowedActions.map((a) => `${p.resourceType}:${a}`));\n return out;\n });\n}\n\n/**\n * Hook for billing-specific RBAC permissions.\n *\n * Permission model aligns with the `@rbac` directives on global-billing-svc:\n * resource: \"billing\"\n * actions: \"read\" – view billing data\n * \"create\" – create billing accounts, initiate payments\n * \"update\" – modify billing accounts, payment methods, usage\n * \"delete\" – remove payment methods, cancel subscriptions\n * \"manage\" – admin-only (plans, addons, quotas, discounts, refunds, tax rates)\n *\n * The RBAC service resolves these via `billingAccount.*` permission strings:\n * billingAccount.read → action: \"read\"\n * billingAccount.create → action: \"create\"\n * billingAccount.update → action: \"update\"\n * billingAccount.delete → action: \"delete\"\n * billingAccount.all → action: \"manage\" (admin bypass)\n *\n */\nexport interface UseBillingPermissionsOptions {\n /**\n * Caller-resolved billing account ID. When provided, this overrides the\n * hook's internal `useBillingAccountSelection` resolution. Use this when\n * the calling page already resolves a billing account (e.g. via the\n * `useBillingAccounts` + `useBillingAccountSelection` pair) so the\n * billing-scope permission fetch observes the *same* account ID and\n * doesn't lag behind for first-time users whose default account only\n * resolves once the accounts list loads.\n */\n billingAccountIdOverride?: string | null;\n}\n\nexport function useBillingPermissions(options?: UseBillingPermissionsOptions) {\n const { isAuthenticated } = useAuth();\n // Org-scope permissions from the top-level PermissionProvider. Covers\n // Org Owner / Org Admin permissions on the `billing` resource.\n const { hasPermission: hasOrgPermission, isLoading: orgIsLoading } = usePermissions();\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n\n // Billing-account-scope permissions. Covers Billing Admin / Billing Viewer\n // role assignments on the currently-active billing account. Required\n // because PermissionProvider only fetches a single scope (org), so a\n // user who is *only* a Billing Admin (not Org Owner) would otherwise see\n // empty perms on the billing pages.\n // Resolve account scope from the current URL when present, otherwise fall\n // back to the last selected billing account stored for this org. That keeps\n // route guards aligned with SPA navigation *and* with list/detail flows that\n // rely on persisted account selection. Callers that already resolve the\n // account (e.g. with a `useBillingAccounts`-backed `useBillingAccountSelection`\n // instance) can pass `billingAccountIdOverride` to keep both observers in sync.\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const billingAccountId = options?.billingAccountIdOverride ?? selectedAccountId ?? undefined;\n const billingScopeQuery = useQuery({\n queryKey: ['billing-scope-permissions', billingAccountId],\n enabled: isAuthenticated && Boolean(billingAccountId),\n staleTime: 5 * 60 * 1000,\n queryFn: async () => {\n const data = await directBillingGraphqlRequest<{\n myPermissions: { permissions: RawPerm[] };\n }>({\n apiGatewayUrl,\n authToken,\n orgId,\n query: BILLING_SCOPE_PERMISSIONS_QUERY,\n variables: { scopeId: billingAccountId },\n });\n return permsToStrings(data?.myPermissions?.permissions ?? []);\n },\n });\n\n const billingScopeStrings = useMemo<string[]>(\n () => billingScopeQuery.data ?? [],\n [billingScopeQuery.data]\n );\n\n const matchesBillingScope = (permission: string): boolean => {\n // Wildcard match: e.g. `billing:read` matches stored `billing:*`.\n const [resource] = permission.split(/[:.]/);\n return (\n billingScopeStrings.includes(permission) || billingScopeStrings.includes(`${resource}:*`)\n );\n };\n\n // Pure RBAC: every check goes through the gateway-evaluated permission\n // set returned by `myPermissions`. No email-domain or role-name fallback —\n // those legacy escape hatches caused divergence between UI and gateway\n // enforcement and have been removed now that billing-scope roles\n // (Billing Admin / Billing Viewer) are auto-assigned on account creation.\n const hasAnyPermission = (permissions: string[]): boolean =>\n permissions.some((p) => hasOrgPermission(p) || matchesBillingScope(p));\n\n const canRead = isAuthenticated && hasAnyPermission(BILLING_READ);\n const canCreate = isAuthenticated && hasAnyPermission(BILLING_CREATE);\n const canUpdate = isAuthenticated && hasAnyPermission(BILLING_UPDATE);\n const canDelete = isAuthenticated && hasAnyPermission(BILLING_DELETE);\n const canManage = isAuthenticated && hasAnyPermission(BILLING_MANAGE);\n\n const isLoading = orgIsLoading || (Boolean(billingAccountId) && billingScopeQuery.isLoading);\n\n return {\n // ── Billing account ───────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewBillingAccount: canRead,\n /** @rbac(action: \"create\", resource: \"billing\") */\n canCreateBillingAccount: canCreate,\n /** @rbac(action: \"update\", resource: \"billing\") */\n canManageBillingAccount: canUpdate,\n /** @rbac(action: \"delete\", resource: \"billing\") — archiveBillingAccount */\n canArchiveBillingAccount: canDelete,\n\n // ── Subscriptions ─────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewSubscriptions: canRead,\n /** @rbac(action: \"create\", resource: \"billing\") — initiatePayment for plans */\n canCreateSubscription: canCreate,\n /** @rbac(action: \"update\", resource: \"billing\") */\n canUpdateSubscription: canUpdate,\n /** @rbac(action: \"delete\", resource: \"billing\") — cancelSubscription */\n canCancelSubscription: canDelete,\n\n // ── Plans (admin-only management) ─────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewPlans: canRead,\n /** @rbac(action: \"manage\", resource: \"billing\") — createPlan, updatePlan, togglePlan */\n canManagePlans: canManage,\n\n // ── Addons (admin-only management) ────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewAddons: canRead,\n /** @rbac(action: \"manage\", resource: \"billing\") — createAddOns, updateAddOnDetails, toggleAddOn */\n canManageAddons: canManage,\n\n // ── Quotas (admin-only management) ────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewQuotas: canRead,\n /** @rbac(action: \"manage\", resource: \"billing\") — createQuota, updateQuota */\n canManageQuotas: canManage,\n\n // ── Invoices ──────────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewInvoices: canRead,\n /** @rbac(action: \"update\", resource: \"billing\") — payInvoice */\n canPayInvoice: canUpdate,\n /** @rbac(action: \"manage\", resource: \"billing\") — voidInvoice, generateInvoice */\n canVoidInvoice: canManage,\n\n // ── Payments ──────────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewPayments: canRead,\n /** @rbac(action: \"create\", resource: \"billing\") — initiatePayment */\n canProcessPayment: canCreate,\n\n // ── Payment Methods ───────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewPaymentMethods: canRead,\n /** @rbac(action: \"update\", resource: \"billing\") — addPaymentMethod, updatePaymentMethod */\n canManagePaymentMethods: canUpdate,\n\n // ── Usage ─────────────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewUsage: canRead,\n /** @rbac(action: \"update\", resource: \"billing\") — addUsage, freeUpUsage */\n canRecordUsage: canUpdate,\n\n // ── Credits ───────────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewCredits: canRead,\n /** @rbac(action: \"update\", resource: \"billing\") — spendCredits */\n canIssueCredit: canUpdate,\n /** @rbac(action: \"update\", resource: \"billing\") — requestCreditWithdrawal */\n canWithdrawCredits: canUpdate,\n /** @rbac(action: \"manage\", resource: \"billing\") — grantCredits */\n canGrantCredits: canManage,\n\n // ── Discounts ─────────────────────────────────────────────────────\n /** @rbac(action: \"update\", resource: \"billing\") — applyBillingDiscount */\n canApplyDiscount: canUpdate,\n /** @rbac(action: \"manage\", resource: \"billing\") — createDiscount, updateDiscount, deleteDiscount */\n canManageDiscounts: canManage,\n\n // ── Analytics ─────────────────────────────────────────────────────\n /** @rbac(action: \"read\", resource: \"billing\") */\n canViewAnalytics: canRead,\n\n // ── Refunds (admin-only) ──────────────────────────────────────────\n /** @rbac(action: \"manage\", resource: \"billing\") — processRefund, skipRefund, retryRefund */\n canProcessRefund: canManage,\n\n // ── Recurring Billing (admin/service-only) ────────────────────────\n /** @rbac(action: \"manage\", resource: \"billing\") */\n canManageRecurringBilling: canManage,\n\n // ── Entitlements (admin-only) ─────────────────────────────────────\n /** @rbac(action: \"manage\", resource: \"billing\") — createEntitlement, updateEntitlement, deleteEntitlement */\n canManageEntitlements: canManage,\n\n // ── Tax Rates (admin-only) ────────────────────────────────────────\n /** @rbac(action: \"manage\", resource: \"billing\") — createTaxRate, updateTaxRate, deleteTaxRate */\n canManageTaxRates: canManage,\n\n // ── Admin check ───────────────────────────────────────────────────\n /** Full billing admin (billing:manage) */\n isBillingAdmin: canManage,\n\n // ── Loading state ─────────────────────────────────────────────────\n /** Whether permissions are still being loaded from RBAC service */\n isLoading,\n };\n}\n"],"mappings":";;;;;;;;AAUA,IAAM,IAAe,CAAC,gBAAgB,eAAe,EAC/C,IAAiB,CAAC,kBAAkB,iBAAiB,EACrD,IAAiB,CAAC,kBAAkB,iBAAiB,EACrD,IAAiB,CAAC,kBAAkB,iBAAiB,EACrD,IAAiB,CAAC,kBAAkB,iBAAiB,EAErD,IAAkC;AAoBxC,SAAS,EAAe,GAA4B;AAClD,QAAO,EAAM,SAAS,MAAM;EAC1B,IAAM,IAAgB,EAAE;AAGxB,SAFI,EAAE,UAAU,WAAS,EAAI,KAAK,GAAG,EAAE,aAAa,IAAI,EACxD,EAAI,KAAK,GAAG,EAAE,eAAe,KAAK,MAAM,GAAG,EAAE,aAAa,GAAG,IAAI,CAAC,EAC3D;GACP;;AAmCJ,SAAgB,EAAsB,GAAwC;CAC5E,IAAM,EAAE,uBAAoB,GAAS,EAG/B,EAAE,eAAe,GAAkB,WAAW,MAAiB,GAAgB,EAC/E,EAAE,kBAAe,cAAW,aAAU,GAAY,EAalD,EAAE,yBAAsB,EAA2B;EACvD;EACA,aAAa;EACb,cAAc;EACf,CAAC,EACI,IAAmB,GAAS,4BAA4B,KAAqB,KAAA,GAC7E,IAAoB,EAAS;EACjC,UAAU,CAAC,6BAA6B,EAAiB;EACzD,SAAS,KAAmB,EAAQ;EACpC,WAAW,MAAS;EACpB,SAAS,YAUA,GATM,MAAM,EAEhB;GACD;GACA;GACA;GACA,OAAO;GACP,WAAW,EAAE,SAAS,GAAkB;GACzC,CAAC,GAC0B,eAAe,eAAe,EAAE,CAAC;EAEhE,CAAC,EAEI,IAAsB,QACpB,EAAkB,QAAQ,EAAE,EAClC,CAAC,EAAkB,KAAK,CACzB,EAEK,KAAuB,MAAgC;EAE3D,IAAM,CAAC,KAAY,EAAW,MAAM,OAAO;AAC3C,SACE,EAAoB,SAAS,EAAW,IAAI,EAAoB,SAAS,GAAG,EAAS,IAAI;IASvF,KAAoB,MACxB,EAAY,MAAM,MAAM,EAAiB,EAAE,IAAI,EAAoB,EAAE,CAAC,EAElE,IAAU,KAAmB,EAAiB,EAAa,EAC3D,IAAY,KAAmB,EAAiB,EAAe,EAC/D,IAAY,KAAmB,EAAiB,EAAe,EAC/D,IAAY,KAAmB,EAAiB,EAAe,EAC/D,IAAY,KAAmB,EAAiB,EAAe;AAIrE,QAAO;EAGL,uBAAuB;EAEvB,yBAAyB;EAEzB,yBAAyB;EAEzB,0BAA0B;EAI1B,sBAAsB;EAEtB,uBAAuB;EAEvB,uBAAuB;EAEvB,uBAAuB;EAIvB,cAAc;EAEd,gBAAgB;EAIhB,eAAe;EAEf,iBAAiB;EAIjB,eAAe;EAEf,iBAAiB;EAIjB,iBAAiB;EAEjB,eAAe;EAEf,gBAAgB;EAIhB,iBAAiB;EAEjB,mBAAmB;EAInB,uBAAuB;EAEvB,yBAAyB;EAIzB,cAAc;EAEd,gBAAgB;EAIhB,gBAAgB;EAEhB,gBAAgB;EAEhB,oBAAoB;EAEpB,iBAAiB;EAIjB,kBAAkB;EAElB,oBAAoB;EAIpB,kBAAkB;EAIlB,kBAAkB;EAIlB,2BAA2B;EAI3B,uBAAuB;EAIvB,mBAAmB;EAInB,gBAAgB;EAIhB,WA7GgB,KAAiB,EAAQ,KAAqB,EAAkB;EA8GjF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamPermissionsPage.d.ts","sourceRoot":"","sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAA6C,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAuF3E,eAAO,MAAM,mBAAmB,EAAE,
|
|
1
|
+
{"version":3,"file":"TeamPermissionsPage.d.ts","sourceRoot":"","sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAA6C,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAuF3E,eAAO,MAAM,mBAAmB,EAAE,EA0SjC,CAAC"}
|
|
@@ -11,12 +11,12 @@ import { useCallback as s, useEffect as c, useMemo as l, useState as u } from "r
|
|
|
11
11
|
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
12
12
|
//#region src/billing/modules/settings/pages/TeamPermissionsPage.tsx
|
|
13
13
|
var p = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, m = "\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n", h = "\n query BillingScopeActors($scopeId: String!) {\n actors(scopeId: $scopeId) {\n actorId\n actorType\n roleAssignments {\n id\n actorId\n actorType\n roleId\n scopeId\n isActive\n expiresAt\n role { id name priority }\n }\n }\n }\n", g = "\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n", _ = "\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n", v = () => {
|
|
14
|
-
let { apiGatewayUrl: v, authToken: y, orgId: b } = e(),
|
|
14
|
+
let { apiGatewayUrl: v, authToken: y, orgId: b } = e(), { billingAccounts: x } = o(), { selectedAccountId: S } = r({
|
|
15
15
|
orgId: b,
|
|
16
|
-
billingAccounts:
|
|
16
|
+
billingAccounts: x,
|
|
17
17
|
syncFromUrl: !0,
|
|
18
18
|
urlParamName: "billingAccountId"
|
|
19
|
-
}),
|
|
19
|
+
}), C = S ?? void 0, w = a({ billingAccountIdOverride: C }), [T, E] = u([]), [D, O] = u([]), [k, A] = u(!1), [j, M] = u(null), [N, P] = u(""), [F, I] = u(""), [L, R] = u(!1), z = s(async (e, t) => i({
|
|
20
20
|
apiGatewayUrl: v,
|
|
21
21
|
authToken: y,
|
|
22
22
|
orgId: b,
|
|
@@ -27,10 +27,10 @@ var p = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, m = "
|
|
|
27
27
|
y,
|
|
28
28
|
b
|
|
29
29
|
]), B = s(async () => {
|
|
30
|
-
if (
|
|
30
|
+
if (C) {
|
|
31
31
|
A(!0), M(null);
|
|
32
32
|
try {
|
|
33
|
-
let [e, t] = await Promise.all([z(m, { scopeId:
|
|
33
|
+
let [e, t] = await Promise.all([z(m, { scopeId: C }), z(h, { scopeId: C })]);
|
|
34
34
|
E(e.roles ?? []), O(t.actors ?? []), I((e.roles ?? []).find((e) => /viewer/i.test(e.name))?.id ?? e.roles?.[0]?.id ?? "");
|
|
35
35
|
} catch (e) {
|
|
36
36
|
M(e instanceof Error ? e.message : "Failed to load team data");
|
|
@@ -38,7 +38,7 @@ var p = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, m = "
|
|
|
38
38
|
A(!1);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
}, [
|
|
41
|
+
}, [C, z]);
|
|
42
42
|
c(() => {
|
|
43
43
|
B();
|
|
44
44
|
}, [B]);
|
|
@@ -46,7 +46,7 @@ var p = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, m = "
|
|
|
46
46
|
let n = Math.max(...e.roleAssignments.map((e) => e.role?.priority ?? 0), 0);
|
|
47
47
|
return Math.max(...t.roleAssignments.map((e) => e.role?.priority ?? 0), 0) - n;
|
|
48
48
|
}), [D]), H = async (e) => {
|
|
49
|
-
if (e.preventDefault(), !
|
|
49
|
+
if (e.preventDefault(), !C || !N.trim() || !F) return;
|
|
50
50
|
let t = N.trim();
|
|
51
51
|
if (!p.test(t)) {
|
|
52
52
|
M("User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.");
|
|
@@ -58,7 +58,7 @@ var p = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, m = "
|
|
|
58
58
|
actorId: t,
|
|
59
59
|
actorType: "USER",
|
|
60
60
|
roleId: F,
|
|
61
|
-
scopeId:
|
|
61
|
+
scopeId: C
|
|
62
62
|
} }), P(""), await B();
|
|
63
63
|
} catch (e) {
|
|
64
64
|
M(e instanceof Error ? e.message : "Failed to assign role");
|
|
@@ -75,7 +75,7 @@ var p = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, m = "
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
|
-
return
|
|
78
|
+
return w.canManageBillingAccount ? C ? /* @__PURE__ */ f("div", {
|
|
79
79
|
className: "space-y-6",
|
|
80
80
|
children: [
|
|
81
81
|
/* @__PURE__ */ d(n, {
|
|
@@ -92,7 +92,7 @@ var p = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, m = "
|
|
|
92
92
|
" ",
|
|
93
93
|
/* @__PURE__ */ d("span", {
|
|
94
94
|
className: "font-mono",
|
|
95
|
-
children:
|
|
95
|
+
children: C
|
|
96
96
|
})
|
|
97
97
|
]
|
|
98
98
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamPermissionsPage.js","names":[],"sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"sourcesContent":["/**\n * Settings Module - Team & Permissions Page\n *\n * Lets a Billing Admin manage billing-scope role assignments for a specific\n * billing account. Calls global-rbac-svc directly via the same gateway URL\n * used by the rest of the billing module.\n *\n * Scope: `scopeType: \"billing\"`, `scopeId: <billingAccountId>`\n * Roles cloned per billing account at creation time:\n * - Billing Admin (full access to this billing account)\n * - Billing Viewer (read-only access to this billing account)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { useBillingAccounts } from '../../dashboard/hooks/useDashboard';\nimport { directBillingGraphqlRequest } from '../../../shared/utils';\n\n// Basic UUID-v1-through-v5 shape — accept any 8-4-4-4-12 hex pattern.\n// We intentionally don't strictly validate the version nibble; the rbac\n// service is the authoritative ID validator.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Role {\n id: string;\n name: string;\n description?: string | null;\n priority: number;\n isSystem: boolean;\n}\n\ninterface ActorRoleAssignment {\n id: string;\n actorId: string;\n actorType: string;\n roleId: string;\n scopeId: string;\n isActive: boolean;\n expiresAt?: string | null;\n role?: Role;\n}\n\ninterface ActorRbacSummary {\n actorId: string;\n actorType: string;\n roleAssignments: ActorRoleAssignment[];\n effectivePermissionCount?: number;\n}\n\nconst ROLES_QUERY = `\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n`;\n\nconst ACTORS_QUERY = `\n query BillingScopeActors($scopeId: String!) {\n actors(scopeId: $scopeId) {\n actorId\n actorType\n roleAssignments {\n id\n actorId\n actorType\n roleId\n scopeId\n isActive\n expiresAt\n role { id name priority }\n }\n }\n }\n`;\n\nconst ASSIGN_ROLE_MUTATION = `\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n`;\n\nconst REMOVE_ROLE_MUTATION = `\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n`;\n\nexport const TeamPermissionsPage: FC = () => {\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n const permissions = useBillingPermissions();\n\n // Resolution priority: URL `?billingAccountId=` → persisted localStorage →\n // default/first available account. Pass `billingAccounts` so first-time\n // users with empty localStorage still land on the default account\n // (without it the hook's auto-select effect can't fire and the page\n // dead-ends on \"No billing account selected\").\n const { billingAccounts } = useBillingAccounts();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n billingAccounts,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const billingAccountId = selectedAccountId ?? undefined;\n\n const [roles, setRoles] = useState<Role[]>([]);\n const [actors, setActors] = useState<ActorRbacSummary[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Add-member form\n const [newActorId, setNewActorId] = useState('');\n const [newRoleId, setNewRoleId] = useState('');\n const [isAdding, setIsAdding] = useState(false);\n\n const gqlPost = useCallback(\n async <T,>(query: string, variables: Record<string, unknown>): Promise<T> => {\n return directBillingGraphqlRequest<T>({\n apiGatewayUrl,\n authToken,\n orgId,\n query,\n variables,\n });\n },\n [apiGatewayUrl, authToken, orgId]\n );\n\n const loadData = useCallback(async () => {\n if (!billingAccountId) return;\n setIsLoading(true);\n setError(null);\n try {\n const [rolesResp, actorsResp] = await Promise.all([\n gqlPost<{ roles: Role[] }>(ROLES_QUERY, { scopeId: billingAccountId }),\n gqlPost<{ actors: ActorRbacSummary[] }>(ACTORS_QUERY, {\n scopeId: billingAccountId,\n }),\n ]);\n setRoles(rolesResp.roles ?? []);\n setActors(actorsResp.actors ?? []);\n // Default the role-picker to \"Billing Viewer\" if available, else first role\n const viewer = (rolesResp.roles ?? []).find((r) => /viewer/i.test(r.name));\n setNewRoleId(viewer?.id ?? rolesResp.roles?.[0]?.id ?? '');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load team data');\n } finally {\n setIsLoading(false);\n }\n }, [billingAccountId, gqlPost]);\n\n useEffect(() => {\n void loadData();\n }, [loadData]);\n\n const sortedActors = useMemo(\n () =>\n [...actors].sort((a, b) => {\n const priA = Math.max(...a.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n const priB = Math.max(...b.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n return priB - priA;\n }),\n [actors]\n );\n\n const handleAddMember = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!billingAccountId || !newActorId.trim() || !newRoleId) return;\n const trimmedActorId = newActorId.trim();\n if (!UUID_RE.test(trimmedActorId)) {\n setError('User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.');\n return;\n }\n setIsAdding(true);\n setError(null);\n try {\n await gqlPost(ASSIGN_ROLE_MUTATION, {\n input: {\n actorId: trimmedActorId,\n actorType: 'USER',\n roleId: newRoleId,\n scopeId: billingAccountId,\n },\n });\n setNewActorId('');\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to assign role');\n } finally {\n setIsAdding(false);\n }\n };\n\n const handleRemoveAssignment = async (assignmentId: string) => {\n if (!confirm('Remove this role assignment?')) return;\n setError(null);\n try {\n await gqlPost(REMOVE_ROLE_MUTATION, { assignmentId });\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove role');\n }\n };\n\n if (!permissions.canManageBillingAccount) {\n return (\n <AccessDenied message=\"You don't have permission to manage team and permissions for this billing account.\" />\n );\n }\n\n if (!billingAccountId) {\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Select a billing account to manage who can access it.\"\n />\n <div className=\"rounded-lg border border-border bg-card p-6 text-sm text-muted-foreground\">\n No billing account selected. Pick one from the billing account selector to manage its\n members and roles.\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Control who can manage this billing account and what they can do.\"\n />\n\n <div className=\"rounded-lg border border-border bg-card p-4 text-xs text-muted-foreground\">\n <span className=\"font-medium text-foreground\">Billing account:</span>{' '}\n <span className=\"font-mono\">{billingAccountId}</span>\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n {/* Add member form */}\n <form\n onSubmit={(e) => void handleAddMember(e)}\n className=\"rounded-lg border border-border bg-card p-4 space-y-3\"\n >\n <h3 className=\"text-sm font-semibold text-foreground\">Add member</h3>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-3\">\n <div className=\"md:col-span-2\">\n <label htmlFor=\"newActorId\" className=\"block text-xs font-medium text-muted-foreground\">\n User ID (actorId)\n </label>\n <input\n id=\"newActorId\"\n type=\"text\"\n value={newActorId}\n onChange={(e) => setNewActorId(e.target.value)}\n placeholder=\"UUID of the user to grant access\"\n className=\"mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm\"\n required\n />\n </div>\n <div>\n <label htmlFor=\"newRoleId\" className=\"block text-xs font-medium text-muted-foreground\">\n Role\n </label>\n <select\n id=\"newRoleId\"\n value={newRoleId}\n onChange={(e) => setNewRoleId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm\"\n required\n >\n {roles.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n </div>\n </div>\n <button\n type=\"submit\"\n disabled={isAdding || !newActorId.trim() || !newRoleId}\n className=\"rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50\"\n >\n {isAdding ? 'Adding…' : 'Add member'}\n </button>\n </form>\n\n {/* Members table */}\n <div className=\"rounded-lg border border-border bg-card\">\n <div className=\"border-b border-border px-4 py-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">Members</h3>\n <p className=\"text-xs text-muted-foreground\">\n Each member's role applies only to this billing account.\n </p>\n </div>\n {isLoading ? (\n <div className=\"p-6 text-sm text-muted-foreground\">Loading…</div>\n ) : sortedActors.length === 0 ? (\n <div className=\"p-6 text-sm text-muted-foreground\">No members assigned yet.</div>\n ) : (\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border text-left text-xs text-muted-foreground\">\n <th className=\"px-4 py-2\">Actor ID</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Roles</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {sortedActors.map((actor) => (\n <tr key={actor.actorId} className=\"border-b border-border last:border-0\">\n <td className=\"px-4 py-3 font-mono text-xs\">{actor.actorId}</td>\n <td className=\"px-4 py-3 text-xs\">{actor.actorType}</td>\n <td className=\"px-4 py-3\">\n <div className=\"flex flex-wrap gap-1\">\n {actor.roleAssignments\n .filter((ra) => ra.isActive)\n .map((ra) => (\n <span\n key={ra.id}\n className=\"inline-flex items-center gap-1 rounded-full border border-border bg-muted px-2 py-0.5 text-xs\"\n >\n {ra.role?.name ?? ra.roleId}\n <button\n type=\"button\"\n onClick={() => void handleRemoveAssignment(ra.id)}\n className=\"text-muted-foreground hover:text-destructive\"\n aria-label={`Remove ${ra.role?.name ?? 'role'}`}\n >\n ×\n </button>\n </span>\n ))}\n </div>\n </td>\n <td className=\"px-4 py-3 text-right\">\n <span className=\"text-xs text-muted-foreground\">\n {actor.effectivePermissionCount ?? '—'} permissions\n </span>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n\n {/* Roles reference */}\n <div className=\"rounded-lg border border-border bg-card p-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">Available roles</h3>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n Roles are cloned from the system templates when a billing account is created.\n </p>\n <ul className=\"mt-3 space-y-2\">\n {roles.map((r) => (\n <li\n key={r.id}\n className=\"flex items-start gap-3 rounded-md border border-border bg-background p-3\"\n >\n <div className=\"flex-1\">\n <div className=\"text-sm font-medium text-foreground\">{r.name}</div>\n {r.description && (\n <p className=\"mt-1 text-xs text-muted-foreground\">{r.description}</p>\n )}\n </div>\n <span className=\"text-xs text-muted-foreground\">priority {r.priority}</span>\n </li>\n ))}\n </ul>\n </div>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;AAuBA,IAAM,IAAU,mEA4BV,IAAc,2KAYd,IAAe,+TAmBf,IAAuB,0KAWvB,IAAuB,yHAMhB,UAAgC;CAC3C,IAAM,EAAE,kBAAe,cAAW,aAAU,GAAY,EAClD,IAAc,GAAuB,EAOrC,EAAE,uBAAoB,GAAoB,EAC1C,EAAE,yBAAsB,EAA2B;EACvD;EACA;EACA,aAAa;EACb,cAAc;EACf,CAAC,EACI,IAAmB,KAAqB,KAAA,GAExC,CAAC,GAAO,KAAY,EAAiB,EAAE,CAAC,EACxC,CAAC,GAAQ,KAAa,EAA6B,EAAE,CAAC,EACtD,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAO,KAAY,EAAwB,KAAK,EAGjD,CAAC,GAAY,KAAiB,EAAS,GAAG,EAC1C,CAAC,GAAW,KAAgB,EAAS,GAAG,EACxC,CAAC,GAAU,KAAe,EAAS,GAAM,EAEzC,IAAU,EACd,OAAW,GAAe,MACjB,EAA+B;EACpC;EACA;EACA;EACA;EACA;EACD,CAAC,EAEJ;EAAC;EAAe;EAAW;EAAM,CAClC,EAEK,IAAW,EAAY,YAAY;AAClC,SAEL;GADA,EAAa,GAAK,EAClB,EAAS,KAAK;AACd,OAAI;IACF,IAAM,CAAC,GAAW,KAAc,MAAM,QAAQ,IAAI,CAChD,EAA2B,GAAa,EAAE,SAAS,GAAkB,CAAC,EACtE,EAAwC,GAAc,EACpD,SAAS,GACV,CAAC,CACH,CAAC;AAKF,IAJA,EAAS,EAAU,SAAS,EAAE,CAAC,EAC/B,EAAU,EAAW,UAAU,EAAE,CAAC,EAGlC,GADgB,EAAU,SAAS,EAAE,EAAE,MAAM,MAAM,UAAU,KAAK,EAAE,KAAK,CAAC,EACrD,MAAM,EAAU,QAAQ,IAAI,MAAM,GAAG;YACnD,GAAK;AACZ,MAAS,aAAe,QAAQ,EAAI,UAAU,2BAA2B;aACjE;AACR,MAAa,GAAM;;;IAEpB,CAAC,GAAkB,EAAQ,CAAC;AAE/B,SAAgB;AACT,KAAU;IACd,CAAC,EAAS,CAAC;CAEd,IAAM,IAAe,QAEjB,CAAC,GAAG,EAAO,CAAC,MAAM,GAAG,MAAM;EACzB,IAAM,IAAO,KAAK,IAAI,GAAG,EAAE,gBAAgB,KAAK,MAAO,EAAG,MAAM,YAAY,EAAE,EAAE,EAAE;AAElF,SADa,KAAK,IAAI,GAAG,EAAE,gBAAgB,KAAK,MAAO,EAAG,MAAM,YAAY,EAAE,EAAE,EAAE,GACpE;GACd,EACJ,CAAC,EAAO,CACT,EAEK,IAAkB,OAAO,MAAuB;AAEpD,MADA,EAAE,gBAAgB,EACd,CAAC,KAAoB,CAAC,EAAW,MAAM,IAAI,CAAC,EAAW;EAC3D,IAAM,IAAiB,EAAW,MAAM;AACxC,MAAI,CAAC,EAAQ,KAAK,EAAe,EAAE;AACjC,KAAS,iFAAiF;AAC1F;;AAGF,EADA,EAAY,GAAK,EACjB,EAAS,KAAK;AACd,MAAI;AAUF,GATA,MAAM,EAAQ,GAAsB,EAClC,OAAO;IACL,SAAS;IACT,WAAW;IACX,QAAQ;IACR,SAAS;IACV,EACF,CAAC,EACF,EAAc,GAAG,EACjB,MAAM,GAAU;WACT,GAAK;AACZ,KAAS,aAAe,QAAQ,EAAI,UAAU,wBAAwB;YAC9D;AACR,KAAY,GAAM;;IAIhB,IAAyB,OAAO,MAAyB;AACxD,cAAQ,+BAA+B,EAC5C;KAAS,KAAK;AACd,OAAI;AAEF,IADA,MAAM,EAAQ,GAAsB,EAAE,iBAAc,CAAC,EACrD,MAAM,GAAU;YACT,GAAK;AACZ,MAAS,aAAe,QAAQ,EAAI,UAAU,wBAAwB;;;;AAyB1E,QArBK,EAAY,0BAMZ,IAgBH,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,OAAM;IACN,aAAY;IACZ,CAAA;GAEF,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD;MAAM,WAAU;gBAA8B;MAAuB,CAAA;KAAC;KACtE,kBAAC,QAAD;MAAM,WAAU;gBAAa;MAAwB,CAAA;KACjD;;GAEL,KACC,kBAAC,OAAD;IAAK,WAAU;cACZ;IACG,CAAA;GAIR,kBAAC,QAAD;IACE,WAAW,MAAM,KAAK,EAAgB,EAAE;IACxC,WAAU;cAFZ;KAIE,kBAAC,MAAD;MAAI,WAAU;gBAAwC;MAAe,CAAA;KACrE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,SAAD;QAAO,SAAQ;QAAa,WAAU;kBAAkD;QAEhF,CAAA,EACR,kBAAC,SAAD;QACE,IAAG;QACH,MAAK;QACL,OAAO;QACP,WAAW,MAAM,EAAc,EAAE,OAAO,MAAM;QAC9C,aAAY;QACZ,WAAU;QACV,UAAA;QACA,CAAA,CACE;UACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAQ;OAAY,WAAU;iBAAkD;OAE/E,CAAA,EACR,kBAAC,UAAD;OACE,IAAG;OACH,OAAO;OACP,WAAW,MAAM,EAAa,EAAE,OAAO,MAAM;OAC7C,WAAU;OACV,UAAA;iBAEC,EAAM,KAAK,MACV,kBAAC,UAAD;QAAmB,OAAO,EAAE;kBACzB,EAAE;QACI,EAFI,EAAE,GAEN,CACT;OACK,CAAA,CACL,EAAA,CAAA,CACF;;KACN,kBAAC,UAAD;MACE,MAAK;MACL,UAAU,KAAY,CAAC,EAAW,MAAM,IAAI,CAAC;MAC7C,WAAU;gBAET,IAAW,YAAY;MACjB,CAAA;KACJ;;GAGP,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBAAwC;MAAY,CAAA,EAClE,kBAAC,KAAD;MAAG,WAAU;gBAAgC;MAEzC,CAAA,CACA;QACL,IACC,kBAAC,OAAD;KAAK,WAAU;eAAoC;KAAc,CAAA,GAC/D,EAAa,WAAW,IAC1B,kBAAC,OAAD;KAAK,WAAU;eAAoC;KAA8B,CAAA,GAEjF,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAY;QAAa,CAAA;OACvC,kBAAC,MAAD;QAAI,WAAU;kBAAY;QAAS,CAAA;OACnC,kBAAC,MAAD;QAAI,WAAU;kBAAY;QAAU,CAAA;OACpC,kBAAC,MAAD;QAAI,WAAU;kBAAuB;QAAY,CAAA;OAC9C;SACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAa,KAAK,MACjB,kBAAC,MAAD;MAAwB,WAAU;gBAAlC;OACE,kBAAC,MAAD;QAAI,WAAU;kBAA+B,EAAM;QAAa,CAAA;OAChE,kBAAC,MAAD;QAAI,WAAU;kBAAqB,EAAM;QAAe,CAAA;OACxD,kBAAC,MAAD;QAAI,WAAU;kBACZ,kBAAC,OAAD;SAAK,WAAU;mBACZ,EAAM,gBACJ,QAAQ,MAAO,EAAG,SAAS,CAC3B,KAAK,MACJ,kBAAC,QAAD;UAEE,WAAU;oBAFZ,CAIG,EAAG,MAAM,QAAQ,EAAG,QACrB,kBAAC,UAAD;WACE,MAAK;WACL,eAAe,KAAK,EAAuB,EAAG,GAAG;WACjD,WAAU;WACV,cAAY,UAAU,EAAG,MAAM,QAAQ;qBACxC;WAEQ,CAAA,CACJ;YAZA,EAAG,GAYH,CACP;SACA,CAAA;QACH,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBACZ,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CACG,EAAM,4BAA4B,KAAI,eAClC;;QACJ,CAAA;OACF;QA9BI,EAAM,QA8BV,CACL,EACI,CAAA,CACF;OAEN;;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,MAAD;MAAI,WAAU;gBAAwC;MAAoB,CAAA;KAC1E,kBAAC,KAAD;MAAG,WAAU;gBAAqC;MAE9C,CAAA;KACJ,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAM,KAAK,MACV,kBAAC,MAAD;OAEE,WAAU;iBAFZ,CAIE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAuC,EAAE;SAAW,CAAA,EAClE,EAAE,eACD,kBAAC,KAAD;SAAG,WAAU;mBAAsC,EAAE;SAAgB,CAAA,CAEnE;WACN,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CAAgD,aAAU,EAAE,SAAgB;UACzE;SAVE,EAAE,GAUJ,CACL;MACC,CAAA;KACD;;GACF;MApKJ,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GACE,OAAM;GACN,aAAY;GACZ,CAAA,EACF,kBAAC,OAAD;GAAK,WAAU;aAA4E;GAGrF,CAAA,CACF;MAfN,kBAAC,GAAD,EAAc,SAAQ,sFAAuF,CAAA"}
|
|
1
|
+
{"version":3,"file":"TeamPermissionsPage.js","names":[],"sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"sourcesContent":["/**\n * Settings Module - Team & Permissions Page\n *\n * Lets a Billing Admin manage billing-scope role assignments for a specific\n * billing account. Calls global-rbac-svc directly via the same gateway URL\n * used by the rest of the billing module.\n *\n * Scope: `scopeType: \"billing\"`, `scopeId: <billingAccountId>`\n * Roles cloned per billing account at creation time:\n * - Billing Admin (full access to this billing account)\n * - Billing Viewer (read-only access to this billing account)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { useBillingAccounts } from '../../dashboard/hooks/useDashboard';\nimport { directBillingGraphqlRequest } from '../../../shared/utils';\n\n// Basic UUID-v1-through-v5 shape — accept any 8-4-4-4-12 hex pattern.\n// We intentionally don't strictly validate the version nibble; the rbac\n// service is the authoritative ID validator.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Role {\n id: string;\n name: string;\n description?: string | null;\n priority: number;\n isSystem: boolean;\n}\n\ninterface ActorRoleAssignment {\n id: string;\n actorId: string;\n actorType: string;\n roleId: string;\n scopeId: string;\n isActive: boolean;\n expiresAt?: string | null;\n role?: Role;\n}\n\ninterface ActorRbacSummary {\n actorId: string;\n actorType: string;\n roleAssignments: ActorRoleAssignment[];\n effectivePermissionCount?: number;\n}\n\nconst ROLES_QUERY = `\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n`;\n\nconst ACTORS_QUERY = `\n query BillingScopeActors($scopeId: String!) {\n actors(scopeId: $scopeId) {\n actorId\n actorType\n roleAssignments {\n id\n actorId\n actorType\n roleId\n scopeId\n isActive\n expiresAt\n role { id name priority }\n }\n }\n }\n`;\n\nconst ASSIGN_ROLE_MUTATION = `\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n`;\n\nconst REMOVE_ROLE_MUTATION = `\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n`;\n\nexport const TeamPermissionsPage: FC = () => {\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n\n // Resolution priority: URL `?billingAccountId=` → persisted localStorage →\n // default/first available account. Pass `billingAccounts` so first-time\n // users with empty localStorage still land on the default account\n // (without it the hook's auto-select effect can't fire and the page\n // dead-ends on \"No billing account selected\").\n const { billingAccounts } = useBillingAccounts();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n billingAccounts,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const billingAccountId = selectedAccountId ?? undefined;\n\n // Share the resolved account with `useBillingPermissions` so its\n // billing-scope permissions fetch observes the same ID. Without this,\n // a first-time user (no URL param, empty localStorage) would see the\n // page resolve to the default account here while the permissions hook\n // still sat at null — billing-scope-only admins would see AccessDenied\n // until a navigation/reload propagated the localStorage write.\n const permissions = useBillingPermissions({ billingAccountIdOverride: billingAccountId });\n\n const [roles, setRoles] = useState<Role[]>([]);\n const [actors, setActors] = useState<ActorRbacSummary[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Add-member form\n const [newActorId, setNewActorId] = useState('');\n const [newRoleId, setNewRoleId] = useState('');\n const [isAdding, setIsAdding] = useState(false);\n\n const gqlPost = useCallback(\n async <T,>(query: string, variables: Record<string, unknown>): Promise<T> => {\n return directBillingGraphqlRequest<T>({\n apiGatewayUrl,\n authToken,\n orgId,\n query,\n variables,\n });\n },\n [apiGatewayUrl, authToken, orgId]\n );\n\n const loadData = useCallback(async () => {\n if (!billingAccountId) return;\n setIsLoading(true);\n setError(null);\n try {\n const [rolesResp, actorsResp] = await Promise.all([\n gqlPost<{ roles: Role[] }>(ROLES_QUERY, { scopeId: billingAccountId }),\n gqlPost<{ actors: ActorRbacSummary[] }>(ACTORS_QUERY, {\n scopeId: billingAccountId,\n }),\n ]);\n setRoles(rolesResp.roles ?? []);\n setActors(actorsResp.actors ?? []);\n // Default the role-picker to \"Billing Viewer\" if available, else first role\n const viewer = (rolesResp.roles ?? []).find((r) => /viewer/i.test(r.name));\n setNewRoleId(viewer?.id ?? rolesResp.roles?.[0]?.id ?? '');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load team data');\n } finally {\n setIsLoading(false);\n }\n }, [billingAccountId, gqlPost]);\n\n useEffect(() => {\n void loadData();\n }, [loadData]);\n\n const sortedActors = useMemo(\n () =>\n [...actors].sort((a, b) => {\n const priA = Math.max(...a.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n const priB = Math.max(...b.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n return priB - priA;\n }),\n [actors]\n );\n\n const handleAddMember = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!billingAccountId || !newActorId.trim() || !newRoleId) return;\n const trimmedActorId = newActorId.trim();\n if (!UUID_RE.test(trimmedActorId)) {\n setError('User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.');\n return;\n }\n setIsAdding(true);\n setError(null);\n try {\n await gqlPost(ASSIGN_ROLE_MUTATION, {\n input: {\n actorId: trimmedActorId,\n actorType: 'USER',\n roleId: newRoleId,\n scopeId: billingAccountId,\n },\n });\n setNewActorId('');\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to assign role');\n } finally {\n setIsAdding(false);\n }\n };\n\n const handleRemoveAssignment = async (assignmentId: string) => {\n if (!confirm('Remove this role assignment?')) return;\n setError(null);\n try {\n await gqlPost(REMOVE_ROLE_MUTATION, { assignmentId });\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove role');\n }\n };\n\n if (!permissions.canManageBillingAccount) {\n return (\n <AccessDenied message=\"You don't have permission to manage team and permissions for this billing account.\" />\n );\n }\n\n if (!billingAccountId) {\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Select a billing account to manage who can access it.\"\n />\n <div className=\"rounded-lg border border-border bg-card p-6 text-sm text-muted-foreground\">\n No billing account selected. Pick one from the billing account selector to manage its\n members and roles.\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Control who can manage this billing account and what they can do.\"\n />\n\n <div className=\"rounded-lg border border-border bg-card p-4 text-xs text-muted-foreground\">\n <span className=\"font-medium text-foreground\">Billing account:</span>{' '}\n <span className=\"font-mono\">{billingAccountId}</span>\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n {/* Add member form */}\n <form\n onSubmit={(e) => void handleAddMember(e)}\n className=\"rounded-lg border border-border bg-card p-4 space-y-3\"\n >\n <h3 className=\"text-sm font-semibold text-foreground\">Add member</h3>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-3\">\n <div className=\"md:col-span-2\">\n <label htmlFor=\"newActorId\" className=\"block text-xs font-medium text-muted-foreground\">\n User ID (actorId)\n </label>\n <input\n id=\"newActorId\"\n type=\"text\"\n value={newActorId}\n onChange={(e) => setNewActorId(e.target.value)}\n placeholder=\"UUID of the user to grant access\"\n className=\"mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm\"\n required\n />\n </div>\n <div>\n <label htmlFor=\"newRoleId\" className=\"block text-xs font-medium text-muted-foreground\">\n Role\n </label>\n <select\n id=\"newRoleId\"\n value={newRoleId}\n onChange={(e) => setNewRoleId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm\"\n required\n >\n {roles.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n </div>\n </div>\n <button\n type=\"submit\"\n disabled={isAdding || !newActorId.trim() || !newRoleId}\n className=\"rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50\"\n >\n {isAdding ? 'Adding…' : 'Add member'}\n </button>\n </form>\n\n {/* Members table */}\n <div className=\"rounded-lg border border-border bg-card\">\n <div className=\"border-b border-border px-4 py-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">Members</h3>\n <p className=\"text-xs text-muted-foreground\">\n Each member's role applies only to this billing account.\n </p>\n </div>\n {isLoading ? (\n <div className=\"p-6 text-sm text-muted-foreground\">Loading…</div>\n ) : sortedActors.length === 0 ? (\n <div className=\"p-6 text-sm text-muted-foreground\">No members assigned yet.</div>\n ) : (\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border text-left text-xs text-muted-foreground\">\n <th className=\"px-4 py-2\">Actor ID</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Roles</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {sortedActors.map((actor) => (\n <tr key={actor.actorId} className=\"border-b border-border last:border-0\">\n <td className=\"px-4 py-3 font-mono text-xs\">{actor.actorId}</td>\n <td className=\"px-4 py-3 text-xs\">{actor.actorType}</td>\n <td className=\"px-4 py-3\">\n <div className=\"flex flex-wrap gap-1\">\n {actor.roleAssignments\n .filter((ra) => ra.isActive)\n .map((ra) => (\n <span\n key={ra.id}\n className=\"inline-flex items-center gap-1 rounded-full border border-border bg-muted px-2 py-0.5 text-xs\"\n >\n {ra.role?.name ?? ra.roleId}\n <button\n type=\"button\"\n onClick={() => void handleRemoveAssignment(ra.id)}\n className=\"text-muted-foreground hover:text-destructive\"\n aria-label={`Remove ${ra.role?.name ?? 'role'}`}\n >\n ×\n </button>\n </span>\n ))}\n </div>\n </td>\n <td className=\"px-4 py-3 text-right\">\n <span className=\"text-xs text-muted-foreground\">\n {actor.effectivePermissionCount ?? '—'} permissions\n </span>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n\n {/* Roles reference */}\n <div className=\"rounded-lg border border-border bg-card p-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">Available roles</h3>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n Roles are cloned from the system templates when a billing account is created.\n </p>\n <ul className=\"mt-3 space-y-2\">\n {roles.map((r) => (\n <li\n key={r.id}\n className=\"flex items-start gap-3 rounded-md border border-border bg-background p-3\"\n >\n <div className=\"flex-1\">\n <div className=\"text-sm font-medium text-foreground\">{r.name}</div>\n {r.description && (\n <p className=\"mt-1 text-xs text-muted-foreground\">{r.description}</p>\n )}\n </div>\n <span className=\"text-xs text-muted-foreground\">priority {r.priority}</span>\n </li>\n ))}\n </ul>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|