@burdenoff/microfe-admin 2026.805.1 → 2026.829.2
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/admin/AdminRoot.js +77 -72
- package/dist/admin/AdminRoot.js.map +1 -1
- package/dist/admin/AdminRoutes.js +28 -24
- package/dist/admin/AdminRoutes.js.map +1 -1
- package/dist/admin/components/TenantMembersPanel.js +105 -106
- package/dist/admin/components/TenantMembersPanel.js.map +1 -1
- package/dist/admin/hooks/useAdminPermissions.js +1 -0
- package/dist/admin/hooks/useAdminPermissions.js.map +1 -1
- package/dist/admin/hooks/useTr.js +14 -0
- package/dist/admin/hooks/useTr.js.map +1 -0
- package/dist/admin/pages/DashboardPage.js +71 -72
- package/dist/admin/pages/DashboardPage.js.map +1 -1
- package/dist/admin/pages/ExploreConversationsPage.js +419 -0
- package/dist/admin/pages/ExploreConversationsPage.js.map +1 -0
- package/dist/admin/pages/OrganizationsListPage.js +63 -62
- package/dist/admin/pages/OrganizationsListPage.js.map +1 -1
- package/dist/admin/pages/TenantDetailPage.js +148 -149
- package/dist/admin/pages/TenantDetailPage.js.map +1 -1
- package/dist/admin/pages/TenantsListPage.js +106 -107
- package/dist/admin/pages/TenantsListPage.js.map +1 -1
- package/dist/admin/pages/UsersListPage.js +87 -88
- package/dist/admin/pages/UsersListPage.js.map +1 -1
- package/dist/admin/pages/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,215 +1,214 @@
|
|
|
1
1
|
import { useToast as e } from "./Toast.js";
|
|
2
2
|
import { Button as t, Card as n, CardContent as r, CardHeader as i, CardTitle as a, Input as o, Separator as s } from "./ui.js";
|
|
3
3
|
import { formatDate as c } from "../utils/formatters.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { ConfirmDialog as l } from "./ConfirmDialog.js";
|
|
5
|
+
import { useAddTenantMemberMutation as u, useListTenantMembersQuery as d, useRemoveTenantMemberMutation as f, useUpdateTenantMemberRoleMutation as p } from "../../generated/global-operations.js";
|
|
6
|
+
import { useTr as m } from "../hooks/useTr.js";
|
|
7
7
|
import { TenantMemberRole as h } from "../../generated/global-types.js";
|
|
8
8
|
import { useState as g } from "react";
|
|
9
|
-
import { Loader2 as _, Trash2 as v, UserPlus as
|
|
10
|
-
import { Fragment as
|
|
11
|
-
import { useI18n as C } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
9
|
+
import { Loader2 as _, Trash2 as v, UserPlus as y, Users as b } from "lucide-react";
|
|
10
|
+
import { Fragment as x, jsx as S, jsxs as C } from "react/jsx-runtime";
|
|
12
11
|
//#region src/admin/components/TenantMembersPanel.tsx
|
|
13
12
|
var w = {
|
|
14
13
|
userId: "",
|
|
15
14
|
role: h.Member
|
|
16
15
|
}, T = 100;
|
|
17
16
|
function E({ tenantId: E, tenantType: D, canManage: O }) {
|
|
18
|
-
let
|
|
17
|
+
let k = m(), A = e(), [j, M] = g(w), [N, P] = g(null), [F, I] = g(T), L = D?.toUpperCase() === "SHARED", { data: R, loading: z, error: B, refetch: V } = d({
|
|
19
18
|
variables: {
|
|
20
19
|
tenantId: E,
|
|
21
|
-
limit:
|
|
20
|
+
limit: F,
|
|
22
21
|
offset: 0
|
|
23
22
|
},
|
|
24
|
-
skip: !E ||
|
|
23
|
+
skip: !E || L,
|
|
25
24
|
fetchPolicy: "cache-and-network"
|
|
26
|
-
}), [
|
|
27
|
-
if (
|
|
25
|
+
}), [H, { loading: U }] = u(), [W, { loading: G }] = f(), [K] = p();
|
|
26
|
+
if (L) return /* @__PURE__ */ C(n, { children: [/* @__PURE__ */ S(i, { children: /* @__PURE__ */ C(a, {
|
|
28
27
|
className: "flex items-center gap-2 text-lg",
|
|
29
|
-
children: [/* @__PURE__ */
|
|
30
|
-
}) }), /* @__PURE__ */
|
|
28
|
+
children: [/* @__PURE__ */ S(b, { className: "h-4 w-4" }), k("admin.tenantMembers.title", "Tenant Members")]
|
|
29
|
+
}) }), /* @__PURE__ */ S(r, { children: /* @__PURE__ */ S("p", {
|
|
31
30
|
className: "text-sm text-text-secondary",
|
|
32
|
-
children:
|
|
31
|
+
children: k("admin.tenantMembers.sharedImplicit", "This is the shared tenant. Every authenticated user has implicit access — no explicit membership rows are stored.")
|
|
33
32
|
}) })] });
|
|
34
|
-
let
|
|
33
|
+
let q = async (e) => {
|
|
35
34
|
e.preventDefault();
|
|
36
|
-
let t =
|
|
35
|
+
let t = j.userId.trim();
|
|
37
36
|
if (t) try {
|
|
38
|
-
await
|
|
37
|
+
await H({ variables: { input: {
|
|
39
38
|
tenantId: E,
|
|
40
39
|
userId: t,
|
|
41
|
-
role:
|
|
42
|
-
} } }),
|
|
40
|
+
role: j.role
|
|
41
|
+
} } }), A.success(k("admin.tenantMembers.toast.addedTitle", "Member added"), k("admin.tenantMembers.toast.addedDescription", "User {{userId}} can now access this tenant.", { userId: t })), M(w), await V();
|
|
43
42
|
} catch (e) {
|
|
44
|
-
|
|
43
|
+
A.error(k("admin.tenantMembers.toast.errorTitle", "Error"), e instanceof Error ? e.message : k("admin.tenantMembers.toast.addFailed", "Failed to add tenant member."));
|
|
45
44
|
}
|
|
46
|
-
},
|
|
47
|
-
if (!
|
|
48
|
-
let { userId: e } =
|
|
45
|
+
}, J = async () => {
|
|
46
|
+
if (!N) return;
|
|
47
|
+
let { userId: e } = N;
|
|
49
48
|
try {
|
|
50
|
-
await
|
|
49
|
+
await W({ variables: {
|
|
51
50
|
tenantId: E,
|
|
52
51
|
userId: e
|
|
53
|
-
} }),
|
|
52
|
+
} }), A.success(k("admin.tenantMembers.toast.removedTitle", "Member removed"), k("admin.tenantMembers.toast.removedDescription", "User {{userId}} no longer has access.", { userId: e })), P(null), await V();
|
|
54
53
|
} catch (e) {
|
|
55
|
-
|
|
54
|
+
A.error(k("admin.tenantMembers.toast.errorTitle", "Error"), e instanceof Error ? e.message : k("admin.tenantMembers.toast.removeFailed", "Failed to remove tenant member."));
|
|
56
55
|
}
|
|
57
|
-
},
|
|
56
|
+
}, Y = async (e, t) => {
|
|
58
57
|
try {
|
|
59
|
-
await
|
|
58
|
+
await K({ variables: {
|
|
60
59
|
tenantId: E,
|
|
61
60
|
userId: e,
|
|
62
61
|
role: t
|
|
63
|
-
} }), await
|
|
62
|
+
} }), await V();
|
|
64
63
|
} catch (e) {
|
|
65
|
-
|
|
64
|
+
A.error(k("admin.tenantMembers.toast.errorTitle", "Error"), e instanceof Error ? e.message : k("admin.tenantMembers.toast.updateRoleFailed", "Failed to update role."));
|
|
66
65
|
}
|
|
67
|
-
},
|
|
68
|
-
return /* @__PURE__ */
|
|
66
|
+
}, X = R?.tenantMembers?.items ?? [], Z = R?.tenantMembers?.total ?? 0, Q = R?.tenantMembers?.hasMore ?? !1, $ = z && X.length > 0;
|
|
67
|
+
return /* @__PURE__ */ C(x, { children: [/* @__PURE__ */ C(n, { children: [/* @__PURE__ */ S(i, { children: /* @__PURE__ */ C(a, {
|
|
69
68
|
className: "flex items-center gap-2 text-lg",
|
|
70
69
|
children: [
|
|
71
|
-
/* @__PURE__ */
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
/* @__PURE__ */ S(b, { className: "h-4 w-4" }),
|
|
71
|
+
k("admin.tenantMembers.title", "Tenant Members"),
|
|
72
|
+
Z > 0 && /* @__PURE__ */ C("span", {
|
|
74
73
|
className: "text-sm text-text-secondary",
|
|
75
74
|
children: [
|
|
76
75
|
"(",
|
|
77
|
-
|
|
76
|
+
Z,
|
|
78
77
|
")"
|
|
79
78
|
]
|
|
80
79
|
})
|
|
81
80
|
]
|
|
82
|
-
}) }), /* @__PURE__ */
|
|
81
|
+
}) }), /* @__PURE__ */ C(r, {
|
|
83
82
|
className: "space-y-4",
|
|
84
83
|
children: [
|
|
85
|
-
/* @__PURE__ */
|
|
84
|
+
/* @__PURE__ */ S("p", {
|
|
86
85
|
className: "text-sm text-text-secondary",
|
|
87
|
-
children:
|
|
86
|
+
children: k("admin.tenantMembers.description", "Grant a user explicit access to this dedicated tenant. They will be able to create organizations under it and see it in their tenant switcher.")
|
|
88
87
|
}),
|
|
89
|
-
O && /* @__PURE__ */
|
|
90
|
-
onSubmit:
|
|
88
|
+
O && /* @__PURE__ */ C(x, { children: [/* @__PURE__ */ C("form", {
|
|
89
|
+
onSubmit: q,
|
|
91
90
|
className: "flex flex-wrap items-end gap-2",
|
|
92
91
|
children: [
|
|
93
|
-
/* @__PURE__ */
|
|
92
|
+
/* @__PURE__ */ C("div", {
|
|
94
93
|
className: "flex-1 min-w-[240px]",
|
|
95
|
-
children: [/* @__PURE__ */
|
|
94
|
+
children: [/* @__PURE__ */ S("label", {
|
|
96
95
|
className: "mb-1 block text-xs font-medium text-text-secondary",
|
|
97
|
-
children:
|
|
98
|
-
}), /* @__PURE__ */
|
|
99
|
-
value:
|
|
100
|
-
onChange: (e) =>
|
|
101
|
-
...
|
|
96
|
+
children: k("admin.tenantMembers.form.userIdLabel", "User ID")
|
|
97
|
+
}), /* @__PURE__ */ S(o, {
|
|
98
|
+
value: j.userId,
|
|
99
|
+
onChange: (e) => M({
|
|
100
|
+
...j,
|
|
102
101
|
userId: e.target.value
|
|
103
102
|
}),
|
|
104
|
-
placeholder:
|
|
103
|
+
placeholder: k("admin.tenantMembers.form.userIdPlaceholder", "UUID of the user to grant access"),
|
|
105
104
|
required: !0,
|
|
106
|
-
disabled:
|
|
105
|
+
disabled: U
|
|
107
106
|
})]
|
|
108
107
|
}),
|
|
109
|
-
/* @__PURE__ */
|
|
108
|
+
/* @__PURE__ */ C("div", {
|
|
110
109
|
className: "min-w-[140px]",
|
|
111
|
-
children: [/* @__PURE__ */
|
|
110
|
+
children: [/* @__PURE__ */ S("label", {
|
|
112
111
|
className: "mb-1 block text-xs font-medium text-text-secondary",
|
|
113
|
-
children:
|
|
114
|
-
}), /* @__PURE__ */
|
|
115
|
-
value:
|
|
116
|
-
onChange: (e) =>
|
|
117
|
-
...
|
|
112
|
+
children: k("admin.tenantMembers.form.roleLabel", "Role")
|
|
113
|
+
}), /* @__PURE__ */ C("select", {
|
|
114
|
+
value: j.role,
|
|
115
|
+
onChange: (e) => M({
|
|
116
|
+
...j,
|
|
118
117
|
role: e.target.value
|
|
119
118
|
}),
|
|
120
|
-
disabled:
|
|
119
|
+
disabled: U,
|
|
121
120
|
className: "h-9 w-full rounded-md border border-border-default bg-bg-surface px-3 text-sm",
|
|
122
|
-
children: [/* @__PURE__ */
|
|
121
|
+
children: [/* @__PURE__ */ S("option", {
|
|
123
122
|
value: h.Member,
|
|
124
|
-
children:
|
|
125
|
-
}), /* @__PURE__ */
|
|
123
|
+
children: k("admin.tenantMembers.role.member", "Member")
|
|
124
|
+
}), /* @__PURE__ */ S("option", {
|
|
126
125
|
value: h.Admin,
|
|
127
|
-
children:
|
|
126
|
+
children: k("admin.tenantMembers.role.admin", "Admin")
|
|
128
127
|
})]
|
|
129
128
|
})]
|
|
130
129
|
}),
|
|
131
|
-
/* @__PURE__ */
|
|
130
|
+
/* @__PURE__ */ C(t, {
|
|
132
131
|
type: "submit",
|
|
133
|
-
disabled:
|
|
134
|
-
children: [
|
|
132
|
+
disabled: U || !j.userId.trim(),
|
|
133
|
+
children: [U ? /* @__PURE__ */ S(_, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ S(y, { className: "mr-2 h-4 w-4" }), k("admin.tenantMembers.form.submit", "Grant access")]
|
|
135
134
|
})
|
|
136
135
|
]
|
|
137
|
-
}), /* @__PURE__ */
|
|
138
|
-
|
|
136
|
+
}), /* @__PURE__ */ S(s, {})] }),
|
|
137
|
+
z && X.length === 0 ? /* @__PURE__ */ C("div", {
|
|
139
138
|
className: "flex h-24 items-center justify-center text-text-secondary",
|
|
140
|
-
children: [/* @__PURE__ */
|
|
141
|
-
}) :
|
|
139
|
+
children: [/* @__PURE__ */ S(_, { className: "mr-2 h-4 w-4 animate-spin" }), k("admin.tenantMembers.loading", "Loading members...")]
|
|
140
|
+
}) : B ? /* @__PURE__ */ S("div", {
|
|
142
141
|
className: "rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-3 text-sm text-status-error-text",
|
|
143
|
-
children:
|
|
144
|
-
}) :
|
|
142
|
+
children: B.message
|
|
143
|
+
}) : X.length === 0 ? /* @__PURE__ */ S("p", {
|
|
145
144
|
className: "text-sm text-text-secondary",
|
|
146
|
-
children:
|
|
147
|
-
}) : /* @__PURE__ */
|
|
145
|
+
children: k("admin.tenantMembers.empty", "No explicit members yet. Grant access above to add one.")
|
|
146
|
+
}) : /* @__PURE__ */ C("div", {
|
|
148
147
|
className: "space-y-2",
|
|
149
|
-
children: [
|
|
148
|
+
children: [X.map((e) => /* @__PURE__ */ C("div", {
|
|
150
149
|
className: "flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border-subtle p-3",
|
|
151
|
-
children: [/* @__PURE__ */
|
|
150
|
+
children: [/* @__PURE__ */ C("div", {
|
|
152
151
|
className: "min-w-0 flex-1",
|
|
153
|
-
children: [/* @__PURE__ */
|
|
152
|
+
children: [/* @__PURE__ */ S("p", {
|
|
154
153
|
className: "truncate font-mono text-xs text-text-primary",
|
|
155
154
|
children: e.userId
|
|
156
|
-
}), /* @__PURE__ */
|
|
155
|
+
}), /* @__PURE__ */ S("p", {
|
|
157
156
|
className: "text-xs text-text-secondary",
|
|
158
|
-
children:
|
|
157
|
+
children: k("admin.tenantMembers.addedAt", "Added {{when}} by {{by}}", {
|
|
159
158
|
when: c(e.createdAt),
|
|
160
159
|
by: e.addedBy
|
|
161
160
|
})
|
|
162
161
|
})]
|
|
163
|
-
}), /* @__PURE__ */
|
|
162
|
+
}), /* @__PURE__ */ C("div", {
|
|
164
163
|
className: "flex items-center gap-2",
|
|
165
|
-
children: [/* @__PURE__ */
|
|
164
|
+
children: [/* @__PURE__ */ C("select", {
|
|
166
165
|
value: e.role,
|
|
167
|
-
onChange: (t) =>
|
|
166
|
+
onChange: (t) => Y(e.userId, t.target.value),
|
|
168
167
|
disabled: !O,
|
|
169
168
|
className: "h-8 rounded-md border border-border-default bg-bg-surface px-2 text-xs",
|
|
170
|
-
children: [/* @__PURE__ */
|
|
169
|
+
children: [/* @__PURE__ */ S("option", {
|
|
171
170
|
value: h.Member,
|
|
172
|
-
children:
|
|
173
|
-
}), /* @__PURE__ */
|
|
171
|
+
children: k("admin.tenantMembers.role.member", "Member")
|
|
172
|
+
}), /* @__PURE__ */ S("option", {
|
|
174
173
|
value: h.Admin,
|
|
175
|
-
children:
|
|
174
|
+
children: k("admin.tenantMembers.role.admin", "Admin")
|
|
176
175
|
})]
|
|
177
|
-
}), O && /* @__PURE__ */
|
|
176
|
+
}), O && /* @__PURE__ */ S(t, {
|
|
178
177
|
variant: "ghost",
|
|
179
178
|
size: "sm",
|
|
180
|
-
onClick: () =>
|
|
181
|
-
disabled:
|
|
182
|
-
"aria-label":
|
|
183
|
-
children: /* @__PURE__ */
|
|
179
|
+
onClick: () => P({ userId: e.userId }),
|
|
180
|
+
disabled: G,
|
|
181
|
+
"aria-label": k("admin.tenantMembers.remove", "Remove"),
|
|
182
|
+
children: /* @__PURE__ */ S(v, { className: "h-4 w-4 text-status-error-text" })
|
|
184
183
|
})]
|
|
185
184
|
})]
|
|
186
|
-
}, e.id)),
|
|
185
|
+
}, e.id)), Q && /* @__PURE__ */ C("div", {
|
|
187
186
|
className: "flex items-center justify-between gap-2 pt-2",
|
|
188
|
-
children: [/* @__PURE__ */
|
|
187
|
+
children: [/* @__PURE__ */ S("span", {
|
|
189
188
|
className: "text-xs text-text-secondary",
|
|
190
|
-
children:
|
|
191
|
-
shown:
|
|
192
|
-
total:
|
|
189
|
+
children: k("admin.tenantMembers.shownOfTotal", "Showing {{shown}} of {{total}}", {
|
|
190
|
+
shown: X.length,
|
|
191
|
+
total: Z
|
|
193
192
|
})
|
|
194
|
-
}), /* @__PURE__ */
|
|
193
|
+
}), /* @__PURE__ */ C(t, {
|
|
195
194
|
variant: "outline",
|
|
196
195
|
size: "sm",
|
|
197
|
-
onClick: () =>
|
|
196
|
+
onClick: () => I((e) => e + T),
|
|
198
197
|
disabled: $,
|
|
199
|
-
children: [$ ? /* @__PURE__ */
|
|
198
|
+
children: [$ ? /* @__PURE__ */ S(_, { className: "mr-2 h-4 w-4 animate-spin" }) : null, k("admin.tenantMembers.loadMore", "Load more")]
|
|
200
199
|
})]
|
|
201
200
|
})]
|
|
202
201
|
})
|
|
203
202
|
]
|
|
204
|
-
})] }), /* @__PURE__ */
|
|
205
|
-
open: !!
|
|
206
|
-
title:
|
|
207
|
-
description:
|
|
208
|
-
confirmLabel:
|
|
203
|
+
})] }), /* @__PURE__ */ S(l, {
|
|
204
|
+
open: !!N,
|
|
205
|
+
title: k("admin.tenantMembers.confirmRemove.title", "Revoke tenant access?"),
|
|
206
|
+
description: k("admin.tenantMembers.confirmRemove.description", "User {{userId}} will no longer be able to create organizations in this tenant. Existing organization and workspace memberships are not affected.", { userId: N?.userId ?? "" }),
|
|
207
|
+
confirmLabel: k("admin.tenantMembers.confirmRemove.confirm", "Revoke"),
|
|
209
208
|
variant: "destructive",
|
|
210
|
-
loading:
|
|
211
|
-
onConfirm:
|
|
212
|
-
onClose: () =>
|
|
209
|
+
loading: G,
|
|
210
|
+
onConfirm: J,
|
|
211
|
+
onClose: () => P(null)
|
|
213
212
|
})] });
|
|
214
213
|
}
|
|
215
214
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TenantMembersPanel.js","names":[],"sources":["../../../src/admin/components/TenantMembersPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Loader2, Trash2, UserPlus, Users } from 'lucide-react';\nimport {\n useListTenantMembersQuery,\n useAddTenantMemberMutation,\n useRemoveTenantMemberMutation,\n useUpdateTenantMemberRoleMutation,\n type TenantMemberFieldsFragment,\n} from '../../generated/global-operations';\nimport { TenantMemberRole } from '../../generated/global-types';\nimport { Card, CardHeader, CardTitle, CardContent, Button, Input, Separator } from './ui';\nimport { useToast } from './Toast';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\nimport { formatDate } from '../utils/formatters';\n\ninterface TenantMembersPanelProps {\n tenantId: string;\n tenantType: string;\n canManage: boolean;\n}\n\nconst EMPTY_ADD_FORM = { userId: '', role: TenantMemberRole.Member };\nconst PAGE_SIZE = 100;\n\n/**\n * Tenant Members panel (BOFF-2652) — super_admin UI to grant non-super-admin\n * users explicit access to a DEDICATED tenant. SHARED-tenant access is\n * implicit, so the panel is hidden for SHARED tenants.\n */\nexport function TenantMembersPanel({ tenantId, tenantType, canManage }: TenantMembersPanelProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string, params?: Record<string, string | number>): string =>\n tWithFallback(t, key, fallback, params);\n const toast = useToast();\n\n const [addForm, setAddForm] = useState(EMPTY_ADD_FORM);\n const [removeTarget, setRemoveTarget] = useState<{ userId: string } | null>(null);\n // Pagination: each \"Load more\" click widens the window by PAGE_SIZE rows;\n // Apollo re-fetches the bigger page rather than appending in JS so we\n // don't need a custom merge type policy. (BOFF-2652 review)\n const [pageSize, setPageSize] = useState(PAGE_SIZE);\n\n const isShared = tenantType?.toUpperCase() === 'SHARED';\n\n const { data, loading, error, refetch } = useListTenantMembersQuery({\n variables: { tenantId, limit: pageSize, offset: 0 },\n skip: !tenantId || isShared,\n fetchPolicy: 'cache-and-network',\n });\n\n const [addMember, { loading: adding }] = useAddTenantMemberMutation();\n const [removeMember, { loading: removing }] = useRemoveTenantMemberMutation();\n const [updateRole] = useUpdateTenantMemberRoleMutation();\n\n if (isShared) {\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.sharedImplicit',\n 'This is the shared tenant. Every authenticated user has implicit access — no explicit membership rows are stored.'\n )}\n </p>\n </CardContent>\n </Card>\n );\n }\n\n const handleAdd = async (e: React.FormEvent) => {\n e.preventDefault();\n const userId = addForm.userId.trim();\n if (!userId) return;\n try {\n await addMember({ variables: { input: { tenantId, userId, role: addForm.role } } });\n toast.success(\n tr('admin.tenantMembers.toast.addedTitle', 'Member added'),\n tr(\n 'admin.tenantMembers.toast.addedDescription',\n 'User {{userId}} can now access this tenant.',\n {\n userId,\n }\n )\n );\n setAddForm(EMPTY_ADD_FORM);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.addFailed', 'Failed to add tenant member.')\n );\n }\n };\n\n const handleRemove = async () => {\n if (!removeTarget) return;\n const { userId } = removeTarget;\n try {\n await removeMember({ variables: { tenantId, userId } });\n toast.success(\n tr('admin.tenantMembers.toast.removedTitle', 'Member removed'),\n tr(\n 'admin.tenantMembers.toast.removedDescription',\n 'User {{userId}} no longer has access.',\n {\n userId,\n }\n )\n );\n setRemoveTarget(null);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.removeFailed', 'Failed to remove tenant member.')\n );\n }\n };\n\n const handleRoleChange = async (userId: string, role: TenantMemberRole) => {\n try {\n await updateRole({ variables: { tenantId, userId, role } });\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.updateRoleFailed', 'Failed to update role.')\n );\n }\n };\n\n const members = data?.tenantMembers?.items ?? [];\n const total = data?.tenantMembers?.total ?? 0;\n const hasMore = data?.tenantMembers?.hasMore ?? false;\n const loadingMore = loading && members.length > 0;\n\n return (\n <>\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n {total > 0 && <span className=\"text-sm text-text-secondary\">({total})</span>}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.description',\n 'Grant a user explicit access to this dedicated tenant. They will be able to create organizations under it and see it in their tenant switcher.'\n )}\n </p>\n\n {canManage && (\n <>\n <form onSubmit={handleAdd} className=\"flex flex-wrap items-end gap-2\">\n <div className=\"flex-1 min-w-[240px]\">\n <label className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {tr('admin.tenantMembers.form.userIdLabel', 'User ID')}\n </label>\n <Input\n value={addForm.userId}\n onChange={(e) => setAddForm({ ...addForm, userId: e.target.value })}\n placeholder={tr(\n 'admin.tenantMembers.form.userIdPlaceholder',\n 'UUID of the user to grant access'\n )}\n required\n disabled={adding}\n />\n </div>\n <div className=\"min-w-[140px]\">\n <label className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {tr('admin.tenantMembers.form.roleLabel', 'Role')}\n </label>\n <select\n value={addForm.role}\n onChange={(e) =>\n setAddForm({ ...addForm, role: e.target.value as TenantMemberRole })\n }\n disabled={adding}\n className=\"h-9 w-full rounded-md border border-border-default bg-bg-surface px-3 text-sm\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n </div>\n <Button type=\"submit\" disabled={adding || !addForm.userId.trim()}>\n {adding ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <UserPlus className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantMembers.form.submit', 'Grant access')}\n </Button>\n </form>\n <Separator />\n </>\n )}\n\n {loading && members.length === 0 ? (\n <div className=\"flex h-24 items-center justify-center text-text-secondary\">\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n {tr('admin.tenantMembers.loading', 'Loading members...')}\n </div>\n ) : error ? (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-3 text-sm text-status-error-text\">\n {error.message}\n </div>\n ) : members.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.empty',\n 'No explicit members yet. Grant access above to add one.'\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n {members.map((m: TenantMemberFieldsFragment) => (\n <div\n key={m.id}\n className=\"flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border-subtle p-3\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-mono text-xs text-text-primary\">{m.userId}</p>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantMembers.addedAt', 'Added {{when}} by {{by}}', {\n when: formatDate(m.createdAt),\n by: m.addedBy,\n })}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={m.role}\n onChange={(e) =>\n handleRoleChange(m.userId, e.target.value as TenantMemberRole)\n }\n disabled={!canManage}\n className=\"h-8 rounded-md border border-border-default bg-bg-surface px-2 text-xs\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n {canManage && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setRemoveTarget({ userId: m.userId })}\n disabled={removing}\n aria-label={tr('admin.tenantMembers.remove', 'Remove')}\n >\n <Trash2 className=\"h-4 w-4 text-status-error-text\" />\n </Button>\n )}\n </div>\n </div>\n ))}\n {hasMore && (\n <div className=\"flex items-center justify-between gap-2 pt-2\">\n <span className=\"text-xs text-text-secondary\">\n {tr('admin.tenantMembers.shownOfTotal', 'Showing {{shown}} of {{total}}', {\n shown: members.length,\n total,\n })}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setPageSize((prev) => prev + PAGE_SIZE)}\n disabled={loadingMore}\n >\n {loadingMore ? <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" /> : null}\n {tr('admin.tenantMembers.loadMore', 'Load more')}\n </Button>\n </div>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n\n <ConfirmDialog\n open={Boolean(removeTarget)}\n title={tr('admin.tenantMembers.confirmRemove.title', 'Revoke tenant access?')}\n description={tr(\n 'admin.tenantMembers.confirmRemove.description',\n 'User {{userId}} will no longer be able to create organizations in this tenant. Existing organization and workspace memberships are not affected.',\n { userId: removeTarget?.userId ?? '' }\n )}\n confirmLabel={tr('admin.tenantMembers.confirmRemove.confirm', 'Revoke')}\n variant=\"destructive\"\n loading={removing}\n onConfirm={handleRemove}\n onClose={() => setRemoveTarget(null)}\n />\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TenantMembersPanel.js","names":[],"sources":["../../../src/admin/components/TenantMembersPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Loader2, Trash2, UserPlus, Users } from 'lucide-react';\nimport {\n useListTenantMembersQuery,\n useAddTenantMemberMutation,\n useRemoveTenantMemberMutation,\n useUpdateTenantMemberRoleMutation,\n type TenantMemberFieldsFragment,\n} from '../../generated/global-operations';\nimport { TenantMemberRole } from '../../generated/global-types';\nimport { Card, CardHeader, CardTitle, CardContent, Button, Input, Separator } from './ui';\nimport { useToast } from './Toast';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { useTr } from '../hooks/useTr';\nimport { formatDate } from '../utils/formatters';\n\ninterface TenantMembersPanelProps {\n tenantId: string;\n tenantType: string;\n canManage: boolean;\n}\n\nconst EMPTY_ADD_FORM = { userId: '', role: TenantMemberRole.Member };\nconst PAGE_SIZE = 100;\n\n/**\n * Tenant Members panel (BOFF-2652) — super_admin UI to grant non-super-admin\n * users explicit access to a DEDICATED tenant. SHARED-tenant access is\n * implicit, so the panel is hidden for SHARED tenants.\n */\nexport function TenantMembersPanel({ tenantId, tenantType, canManage }: TenantMembersPanelProps) {\n const tr = useTr();\n const toast = useToast();\n\n const [addForm, setAddForm] = useState(EMPTY_ADD_FORM);\n const [removeTarget, setRemoveTarget] = useState<{ userId: string } | null>(null);\n // Pagination: each \"Load more\" click widens the window by PAGE_SIZE rows;\n // Apollo re-fetches the bigger page rather than appending in JS so we\n // don't need a custom merge type policy. (BOFF-2652 review)\n const [pageSize, setPageSize] = useState(PAGE_SIZE);\n\n const isShared = tenantType?.toUpperCase() === 'SHARED';\n\n const { data, loading, error, refetch } = useListTenantMembersQuery({\n variables: { tenantId, limit: pageSize, offset: 0 },\n skip: !tenantId || isShared,\n fetchPolicy: 'cache-and-network',\n });\n\n const [addMember, { loading: adding }] = useAddTenantMemberMutation();\n const [removeMember, { loading: removing }] = useRemoveTenantMemberMutation();\n const [updateRole] = useUpdateTenantMemberRoleMutation();\n\n if (isShared) {\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.sharedImplicit',\n 'This is the shared tenant. Every authenticated user has implicit access — no explicit membership rows are stored.'\n )}\n </p>\n </CardContent>\n </Card>\n );\n }\n\n const handleAdd = async (e: React.FormEvent) => {\n e.preventDefault();\n const userId = addForm.userId.trim();\n if (!userId) return;\n try {\n await addMember({ variables: { input: { tenantId, userId, role: addForm.role } } });\n toast.success(\n tr('admin.tenantMembers.toast.addedTitle', 'Member added'),\n tr(\n 'admin.tenantMembers.toast.addedDescription',\n 'User {{userId}} can now access this tenant.',\n {\n userId,\n }\n )\n );\n setAddForm(EMPTY_ADD_FORM);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.addFailed', 'Failed to add tenant member.')\n );\n }\n };\n\n const handleRemove = async () => {\n if (!removeTarget) return;\n const { userId } = removeTarget;\n try {\n await removeMember({ variables: { tenantId, userId } });\n toast.success(\n tr('admin.tenantMembers.toast.removedTitle', 'Member removed'),\n tr(\n 'admin.tenantMembers.toast.removedDescription',\n 'User {{userId}} no longer has access.',\n {\n userId,\n }\n )\n );\n setRemoveTarget(null);\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.removeFailed', 'Failed to remove tenant member.')\n );\n }\n };\n\n const handleRoleChange = async (userId: string, role: TenantMemberRole) => {\n try {\n await updateRole({ variables: { tenantId, userId, role } });\n await refetch();\n } catch (err) {\n toast.error(\n tr('admin.tenantMembers.toast.errorTitle', 'Error'),\n err instanceof Error\n ? err.message\n : tr('admin.tenantMembers.toast.updateRoleFailed', 'Failed to update role.')\n );\n }\n };\n\n const members = data?.tenantMembers?.items ?? [];\n const total = data?.tenantMembers?.total ?? 0;\n const hasMore = data?.tenantMembers?.hasMore ?? false;\n const loadingMore = loading && members.length > 0;\n\n return (\n <>\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-lg\">\n <Users className=\"h-4 w-4\" />\n {tr('admin.tenantMembers.title', 'Tenant Members')}\n {total > 0 && <span className=\"text-sm text-text-secondary\">({total})</span>}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.description',\n 'Grant a user explicit access to this dedicated tenant. They will be able to create organizations under it and see it in their tenant switcher.'\n )}\n </p>\n\n {canManage && (\n <>\n <form onSubmit={handleAdd} className=\"flex flex-wrap items-end gap-2\">\n <div className=\"flex-1 min-w-[240px]\">\n <label className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {tr('admin.tenantMembers.form.userIdLabel', 'User ID')}\n </label>\n <Input\n value={addForm.userId}\n onChange={(e) => setAddForm({ ...addForm, userId: e.target.value })}\n placeholder={tr(\n 'admin.tenantMembers.form.userIdPlaceholder',\n 'UUID of the user to grant access'\n )}\n required\n disabled={adding}\n />\n </div>\n <div className=\"min-w-[140px]\">\n <label className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {tr('admin.tenantMembers.form.roleLabel', 'Role')}\n </label>\n <select\n value={addForm.role}\n onChange={(e) =>\n setAddForm({ ...addForm, role: e.target.value as TenantMemberRole })\n }\n disabled={adding}\n className=\"h-9 w-full rounded-md border border-border-default bg-bg-surface px-3 text-sm\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n </div>\n <Button type=\"submit\" disabled={adding || !addForm.userId.trim()}>\n {adding ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <UserPlus className=\"mr-2 h-4 w-4\" />\n )}\n {tr('admin.tenantMembers.form.submit', 'Grant access')}\n </Button>\n </form>\n <Separator />\n </>\n )}\n\n {loading && members.length === 0 ? (\n <div className=\"flex h-24 items-center justify-center text-text-secondary\">\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n {tr('admin.tenantMembers.loading', 'Loading members...')}\n </div>\n ) : error ? (\n <div className=\"rounded-md border border-status-error-border/50 bg-status-error-bg/10 p-3 text-sm text-status-error-text\">\n {error.message}\n </div>\n ) : members.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'admin.tenantMembers.empty',\n 'No explicit members yet. Grant access above to add one.'\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n {members.map((m: TenantMemberFieldsFragment) => (\n <div\n key={m.id}\n className=\"flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border-subtle p-3\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-mono text-xs text-text-primary\">{m.userId}</p>\n <p className=\"text-xs text-text-secondary\">\n {tr('admin.tenantMembers.addedAt', 'Added {{when}} by {{by}}', {\n when: formatDate(m.createdAt),\n by: m.addedBy,\n })}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={m.role}\n onChange={(e) =>\n handleRoleChange(m.userId, e.target.value as TenantMemberRole)\n }\n disabled={!canManage}\n className=\"h-8 rounded-md border border-border-default bg-bg-surface px-2 text-xs\"\n >\n <option value={TenantMemberRole.Member}>\n {tr('admin.tenantMembers.role.member', 'Member')}\n </option>\n <option value={TenantMemberRole.Admin}>\n {tr('admin.tenantMembers.role.admin', 'Admin')}\n </option>\n </select>\n {canManage && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setRemoveTarget({ userId: m.userId })}\n disabled={removing}\n aria-label={tr('admin.tenantMembers.remove', 'Remove')}\n >\n <Trash2 className=\"h-4 w-4 text-status-error-text\" />\n </Button>\n )}\n </div>\n </div>\n ))}\n {hasMore && (\n <div className=\"flex items-center justify-between gap-2 pt-2\">\n <span className=\"text-xs text-text-secondary\">\n {tr('admin.tenantMembers.shownOfTotal', 'Showing {{shown}} of {{total}}', {\n shown: members.length,\n total,\n })}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setPageSize((prev) => prev + PAGE_SIZE)}\n disabled={loadingMore}\n >\n {loadingMore ? <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" /> : null}\n {tr('admin.tenantMembers.loadMore', 'Load more')}\n </Button>\n </div>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n\n <ConfirmDialog\n open={Boolean(removeTarget)}\n title={tr('admin.tenantMembers.confirmRemove.title', 'Revoke tenant access?')}\n description={tr(\n 'admin.tenantMembers.confirmRemove.description',\n 'User {{userId}} will no longer be able to create organizations in this tenant. Existing organization and workspace memberships are not affected.',\n { userId: removeTarget?.userId ?? '' }\n )}\n confirmLabel={tr('admin.tenantMembers.confirmRemove.confirm', 'Revoke')}\n variant=\"destructive\"\n loading={removing}\n onConfirm={handleRemove}\n onClose={() => setRemoveTarget(null)}\n />\n </>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAdminPermissions.js","names":[],"sources":["../../../src/admin/hooks/useAdminPermissions.ts"],"sourcesContent":["import { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useAuth } from '@burdenoff/fe-libs/shared/providers/shell/AuthProvider';\nimport { ADMIN_PERMISSIONS } from '../constants/permissions';\n\ninterface JwtAccessClaims {\n role: string | null;\n actorType: string | null;\n}\n\nfunction decodeJwtAccessClaims(token: string | undefined): JwtAccessClaims {\n if (!token) return { role: null, actorType: null };\n const parts = token.split('.');\n if (parts.length !== 3) return { role: null, actorType: null };\n try {\n const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');\n const normalized = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');\n const payload = JSON.parse(atob(normalized)) as Record<string, unknown>;\n return {\n role: typeof payload.role === 'string' ? payload.role : null,\n actorType:\n typeof payload.actor_type === 'string'\n ? payload.actor_type\n : typeof payload.actorType === 'string'\n ? payload.actorType\n : null,\n };\n } catch {\n return { role: null, actorType: null };\n }\n}\n\n/**\n * Hook for admin console RBAC permissions.\n *\n * SUPER_ADMIN role / actor type (verified from profile or JWT) bypasses all\n * permission checks.\n * Other users require granular permissions from the global RBAC service.\n */\nexport function useAdminPermissions() {\n const { hasPermission, isLoading } = usePermissions();\n const auth = useAuth();\n\n const jwtClaims = decodeJwtAccessClaims(auth.tokens?.accessToken);\n const userActorType =\n typeof auth.user?.actorType === 'string'\n ? auth.user.actorType\n : typeof auth.user?.actor_type === 'string'\n ? auth.user.actor_type\n : null;\n const isSuperAdmin =\n auth.user?.role === 'SUPER_ADMIN' ||\n auth.user?.role === 'super_admin' ||\n userActorType === 'super_admin' ||\n jwtClaims.role === 'SUPER_ADMIN' ||\n jwtClaims.role === 'super_admin' ||\n jwtClaims.actorType === 'super_admin';\n\n const check = (permission: string) => isSuperAdmin || hasPermission(permission);\n\n // True if the user holds any admin-level permission (read or admin grants)\n const hasAnyAdminAccess =\n isSuperAdmin ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_READ) ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_ADMIN) ||\n hasPermission(ADMIN_PERMISSIONS.USER_READ) ||\n hasPermission(ADMIN_PERMISSIONS.USER_ADMIN) ||\n hasPermission(ADMIN_PERMISSIONS.BILLING_READ) ||\n hasPermission(ADMIN_PERMISSIONS.NOTIFICATION_READ) ||\n hasPermission(ADMIN_PERMISSIONS.RBAC_AUDIT_READ) ||\n hasPermission(ADMIN_PERMISSIONS.WORKSPACE_ADMIN);\n\n return {\n // Tenant management\n canViewTenants: check(ADMIN_PERMISSIONS.TENANT_READ),\n canManageTenants: check(ADMIN_PERMISSIONS.TENANT_WRITE),\n canDeleteTenants: check(ADMIN_PERMISSIONS.TENANT_DELETE),\n isTenantAdmin: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // User management\n canViewUsers: check(ADMIN_PERMISSIONS.USER_READ),\n canUpdateUsers: check(ADMIN_PERMISSIONS.USER_UPDATE),\n canDeleteUsers: check(ADMIN_PERMISSIONS.USER_DELETE),\n isUserAdmin: check(ADMIN_PERMISSIONS.USER_ADMIN),\n\n // Billing\n canViewBilling: check(ADMIN_PERMISSIONS.BILLING_READ),\n\n // Communications\n canViewCommunications: check(ADMIN_PERMISSIONS.NOTIFICATION_READ),\n\n // Audit\n canViewAuditLogs: check(ADMIN_PERMISSIONS.RBAC_AUDIT_READ),\n\n // Platform settings / SRE\n canManageSettings: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n canViewSRE: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // Cross-layer sync outbox monitoring (super-admin only)\n canViewSyncFailures: isSuperAdmin || hasPermission(ADMIN_PERMISSIONS.SYNC_OUTBOX_READ),\n canManageSyncFailures: isSuperAdmin || hasPermission(ADMIN_PERMISSIONS.SYNC_OUTBOX_MANAGE),\n\n // Workspace / Sandbox\n canManageSandbox: check(ADMIN_PERMISSIONS.WORKSPACE_ADMIN),\n\n // Products\n canViewProducts: hasAnyAdminAccess,\n canManageProducts: check(ADMIN_PERMISSIONS.PRODUCT_WRITE),\n\n // Waitlist\n canViewWaitlist: hasAnyAdminAccess,\n canManageWaitlist:\n isSuperAdmin ||\n hasPermission(ADMIN_PERMISSIONS.WAITLIST_MANAGE) ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // General pages — any admin role can access\n canViewDashboard: hasAnyAdminAccess,\n\n isLoading: isSuperAdmin ? false : isLoading,\n };\n}\n"],"mappings":";;;;AASA,SAAS,EAAsB,GAA4C;AACzE,KAAI,CAAC,EAAO,QAAO;EAAE,MAAM;EAAM,WAAW;EAAM;CAClD,IAAM,IAAQ,EAAM,MAAM,IAAI;AAC9B,KAAI,EAAM,WAAW,EAAG,QAAO;EAAE,MAAM;EAAM,WAAW;EAAM;AAC9D,KAAI;EACF,IAAM,IAAS,EAAM,GAAG,QAAQ,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI,EACvD,IAAa,EAAO,OAAO,KAAK,KAAK,EAAO,SAAS,EAAE,GAAG,GAAG,IAAI,EACjE,IAAU,KAAK,MAAM,KAAK,EAAW,CAAC;AAC5C,SAAO;GACL,MAAM,OAAO,EAAQ,QAAS,WAAW,EAAQ,OAAO;GACxD,WACE,OAAO,EAAQ,cAAe,WAC1B,EAAQ,aACR,OAAO,EAAQ,aAAc,WAC3B,EAAQ,YACR;GACT;SACK;AACN,SAAO;GAAE,MAAM;GAAM,WAAW;GAAM;;;AAW1C,SAAgB,IAAsB;CACpC,IAAM,EAAE,kBAAe,iBAAc,GAAgB,EAC/C,IAAO,GAAS,EAEhB,IAAY,EAAsB,EAAK,QAAQ,YAAY,EAC3D,IACJ,OAAO,EAAK,MAAM,aAAc,WAC5B,EAAK,KAAK,YACV,OAAO,EAAK,MAAM,cAAe,WAC/B,EAAK,KAAK,aACV,MACF,IACJ,EAAK,MAAM,SAAS,iBACpB,EAAK,MAAM,SAAS,iBACpB,MAAkB,iBAClB,EAAU,SAAS,iBACnB,EAAU,SAAS,iBACnB,EAAU,cAAc,eAEpB,KAAS,MAAuB,KAAgB,EAAc,EAAW,EAGzE,IACJ,KACA,EAAc,EAAkB,YAAY,IAC5C,EAAc,EAAkB,aAAa,IAC7C,EAAc,EAAkB,UAAU,IAC1C,EAAc,EAAkB,WAAW,IAC3C,EAAc,EAAkB,aAAa,IAC7C,EAAc,EAAkB,kBAAkB,IAClD,EAAc,EAAkB,gBAAgB,IAChD,EAAc,EAAkB,gBAAgB;AAElD,QAAO;EAEL,gBAAgB,EAAM,EAAkB,YAAY;EACpD,kBAAkB,EAAM,EAAkB,aAAa;EACvD,kBAAkB,EAAM,EAAkB,cAAc;EACxD,eAAe,EAAM,EAAkB,aAAa;EAGpD,cAAc,EAAM,EAAkB,UAAU;EAChD,gBAAgB,EAAM,EAAkB,YAAY;EACpD,gBAAgB,EAAM,EAAkB,YAAY;EACpD,aAAa,EAAM,EAAkB,WAAW;EAGhD,gBAAgB,EAAM,EAAkB,aAAa;EAGrD,uBAAuB,EAAM,EAAkB,kBAAkB;EAGjE,kBAAkB,EAAM,EAAkB,gBAAgB;EAG1D,mBAAmB,EAAM,EAAkB,aAAa;EACxD,YAAY,EAAM,EAAkB,aAAa;EAGjD,qBAAqB,KAAgB,EAAc,EAAkB,iBAAiB;EACtF,uBAAuB,KAAgB,EAAc,EAAkB,mBAAmB;EAG1F,kBAAkB,EAAM,EAAkB,gBAAgB;EAG1D,iBAAiB;EACjB,mBAAmB,EAAM,EAAkB,cAAc;EAGzD,iBAAiB;EACjB,mBACE,KACA,EAAc,EAAkB,gBAAgB,IAChD,EAAc,EAAkB,aAAa;EAG/C,kBAAkB;
|
|
1
|
+
{"version":3,"file":"useAdminPermissions.js","names":[],"sources":["../../../src/admin/hooks/useAdminPermissions.ts"],"sourcesContent":["import { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useAuth } from '@burdenoff/fe-libs/shared/providers/shell/AuthProvider';\nimport { ADMIN_PERMISSIONS } from '../constants/permissions';\n\ninterface JwtAccessClaims {\n role: string | null;\n actorType: string | null;\n}\n\nfunction decodeJwtAccessClaims(token: string | undefined): JwtAccessClaims {\n if (!token) return { role: null, actorType: null };\n const parts = token.split('.');\n if (parts.length !== 3) return { role: null, actorType: null };\n try {\n const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');\n const normalized = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');\n const payload = JSON.parse(atob(normalized)) as Record<string, unknown>;\n return {\n role: typeof payload.role === 'string' ? payload.role : null,\n actorType:\n typeof payload.actor_type === 'string'\n ? payload.actor_type\n : typeof payload.actorType === 'string'\n ? payload.actorType\n : null,\n };\n } catch {\n return { role: null, actorType: null };\n }\n}\n\n/**\n * Hook for admin console RBAC permissions.\n *\n * SUPER_ADMIN role / actor type (verified from profile or JWT) bypasses all\n * permission checks.\n * Other users require granular permissions from the global RBAC service.\n */\nexport function useAdminPermissions() {\n const { hasPermission, isLoading } = usePermissions();\n const auth = useAuth();\n\n const jwtClaims = decodeJwtAccessClaims(auth.tokens?.accessToken);\n const userActorType =\n typeof auth.user?.actorType === 'string'\n ? auth.user.actorType\n : typeof auth.user?.actor_type === 'string'\n ? auth.user.actor_type\n : null;\n const isSuperAdmin =\n auth.user?.role === 'SUPER_ADMIN' ||\n auth.user?.role === 'super_admin' ||\n userActorType === 'super_admin' ||\n jwtClaims.role === 'SUPER_ADMIN' ||\n jwtClaims.role === 'super_admin' ||\n jwtClaims.actorType === 'super_admin';\n\n const check = (permission: string) => isSuperAdmin || hasPermission(permission);\n\n // True if the user holds any admin-level permission (read or admin grants)\n const hasAnyAdminAccess =\n isSuperAdmin ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_READ) ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_ADMIN) ||\n hasPermission(ADMIN_PERMISSIONS.USER_READ) ||\n hasPermission(ADMIN_PERMISSIONS.USER_ADMIN) ||\n hasPermission(ADMIN_PERMISSIONS.BILLING_READ) ||\n hasPermission(ADMIN_PERMISSIONS.NOTIFICATION_READ) ||\n hasPermission(ADMIN_PERMISSIONS.RBAC_AUDIT_READ) ||\n hasPermission(ADMIN_PERMISSIONS.WORKSPACE_ADMIN);\n\n return {\n // Tenant management\n canViewTenants: check(ADMIN_PERMISSIONS.TENANT_READ),\n canManageTenants: check(ADMIN_PERMISSIONS.TENANT_WRITE),\n canDeleteTenants: check(ADMIN_PERMISSIONS.TENANT_DELETE),\n isTenantAdmin: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // User management\n canViewUsers: check(ADMIN_PERMISSIONS.USER_READ),\n canUpdateUsers: check(ADMIN_PERMISSIONS.USER_UPDATE),\n canDeleteUsers: check(ADMIN_PERMISSIONS.USER_DELETE),\n isUserAdmin: check(ADMIN_PERMISSIONS.USER_ADMIN),\n\n // Billing\n canViewBilling: check(ADMIN_PERMISSIONS.BILLING_READ),\n\n // Communications\n canViewCommunications: check(ADMIN_PERMISSIONS.NOTIFICATION_READ),\n\n // Audit\n canViewAuditLogs: check(ADMIN_PERMISSIONS.RBAC_AUDIT_READ),\n\n // Platform settings / SRE\n canManageSettings: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n canViewSRE: check(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // Cross-layer sync outbox monitoring (super-admin only)\n canViewSyncFailures: isSuperAdmin || hasPermission(ADMIN_PERMISSIONS.SYNC_OUTBOX_READ),\n canManageSyncFailures: isSuperAdmin || hasPermission(ADMIN_PERMISSIONS.SYNC_OUTBOX_MANAGE),\n\n // Workspace / Sandbox\n canManageSandbox: check(ADMIN_PERMISSIONS.WORKSPACE_ADMIN),\n\n // Products\n canViewProducts: hasAnyAdminAccess,\n canManageProducts: check(ADMIN_PERMISSIONS.PRODUCT_WRITE),\n\n // Waitlist\n canViewWaitlist: hasAnyAdminAccess,\n canManageWaitlist:\n isSuperAdmin ||\n hasPermission(ADMIN_PERMISSIONS.WAITLIST_MANAGE) ||\n hasPermission(ADMIN_PERMISSIONS.TENANT_ADMIN),\n\n // General pages — any admin role can access\n canViewDashboard: hasAnyAdminAccess,\n\n /**\n * Explore conversations: superadmin only, matching the\n * `@rbac(actorTypes: [\"super_admin\"])` on the backing fields. Exposed as its own flag\n * rather than reusing a permission, because the backend gate is actor-type based and a\n * permission grant would not open it — the two must agree or the page renders and then\n * every query comes back FORBIDDEN.\n */\n canViewExploreConversations: isSuperAdmin,\n\n isLoading: isSuperAdmin ? false : isLoading,\n };\n}\n"],"mappings":";;;;AASA,SAAS,EAAsB,GAA4C;AACzE,KAAI,CAAC,EAAO,QAAO;EAAE,MAAM;EAAM,WAAW;EAAM;CAClD,IAAM,IAAQ,EAAM,MAAM,IAAI;AAC9B,KAAI,EAAM,WAAW,EAAG,QAAO;EAAE,MAAM;EAAM,WAAW;EAAM;AAC9D,KAAI;EACF,IAAM,IAAS,EAAM,GAAG,QAAQ,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI,EACvD,IAAa,EAAO,OAAO,KAAK,KAAK,EAAO,SAAS,EAAE,GAAG,GAAG,IAAI,EACjE,IAAU,KAAK,MAAM,KAAK,EAAW,CAAC;AAC5C,SAAO;GACL,MAAM,OAAO,EAAQ,QAAS,WAAW,EAAQ,OAAO;GACxD,WACE,OAAO,EAAQ,cAAe,WAC1B,EAAQ,aACR,OAAO,EAAQ,aAAc,WAC3B,EAAQ,YACR;GACT;SACK;AACN,SAAO;GAAE,MAAM;GAAM,WAAW;GAAM;;;AAW1C,SAAgB,IAAsB;CACpC,IAAM,EAAE,kBAAe,iBAAc,GAAgB,EAC/C,IAAO,GAAS,EAEhB,IAAY,EAAsB,EAAK,QAAQ,YAAY,EAC3D,IACJ,OAAO,EAAK,MAAM,aAAc,WAC5B,EAAK,KAAK,YACV,OAAO,EAAK,MAAM,cAAe,WAC/B,EAAK,KAAK,aACV,MACF,IACJ,EAAK,MAAM,SAAS,iBACpB,EAAK,MAAM,SAAS,iBACpB,MAAkB,iBAClB,EAAU,SAAS,iBACnB,EAAU,SAAS,iBACnB,EAAU,cAAc,eAEpB,KAAS,MAAuB,KAAgB,EAAc,EAAW,EAGzE,IACJ,KACA,EAAc,EAAkB,YAAY,IAC5C,EAAc,EAAkB,aAAa,IAC7C,EAAc,EAAkB,UAAU,IAC1C,EAAc,EAAkB,WAAW,IAC3C,EAAc,EAAkB,aAAa,IAC7C,EAAc,EAAkB,kBAAkB,IAClD,EAAc,EAAkB,gBAAgB,IAChD,EAAc,EAAkB,gBAAgB;AAElD,QAAO;EAEL,gBAAgB,EAAM,EAAkB,YAAY;EACpD,kBAAkB,EAAM,EAAkB,aAAa;EACvD,kBAAkB,EAAM,EAAkB,cAAc;EACxD,eAAe,EAAM,EAAkB,aAAa;EAGpD,cAAc,EAAM,EAAkB,UAAU;EAChD,gBAAgB,EAAM,EAAkB,YAAY;EACpD,gBAAgB,EAAM,EAAkB,YAAY;EACpD,aAAa,EAAM,EAAkB,WAAW;EAGhD,gBAAgB,EAAM,EAAkB,aAAa;EAGrD,uBAAuB,EAAM,EAAkB,kBAAkB;EAGjE,kBAAkB,EAAM,EAAkB,gBAAgB;EAG1D,mBAAmB,EAAM,EAAkB,aAAa;EACxD,YAAY,EAAM,EAAkB,aAAa;EAGjD,qBAAqB,KAAgB,EAAc,EAAkB,iBAAiB;EACtF,uBAAuB,KAAgB,EAAc,EAAkB,mBAAmB;EAG1F,kBAAkB,EAAM,EAAkB,gBAAgB;EAG1D,iBAAiB;EACjB,mBAAmB,EAAM,EAAkB,cAAc;EAGzD,iBAAiB;EACjB,mBACE,KACA,EAAc,EAAkB,gBAAgB,IAChD,EAAc,EAAkB,aAAa;EAG/C,kBAAkB;EASlB,6BAA6B;EAE7B,WAAW,IAAe,KAAQ;EACnC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useCallback as e } from "react";
|
|
2
|
+
import { useI18n as t } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
3
|
+
//#region src/admin/hooks/useTr.ts
|
|
4
|
+
function n() {
|
|
5
|
+
let { t: n } = t();
|
|
6
|
+
return e((e, t, r) => {
|
|
7
|
+
let i = n(e), a = i === e ? t : i;
|
|
8
|
+
return r ? Object.entries(r).reduce((e, [t, n]) => e.replace(RegExp(`\\{\\{${t}\\}\\}`, "g"), String(n)), a) : a;
|
|
9
|
+
}, [n]);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { n as useTr };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=useTr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTr.js","names":[],"sources":["../../../src/admin/hooks/useTr.ts"],"sourcesContent":["import { useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\n/**\n * useTr — translation hook with fallback semantics.\n *\n * Wraps `useI18n().t()` so that when a key has no translation (i.e. the\n * provider returns the key itself), the caller's English fallback is used\n * instead. This keeps consumer call sites consistent and avoids leaking\n * untranslated keys into the UI.\n *\n * Supports optional interpolation params using `{{key}}` placeholders.\n *\n * @example\n * const tr = useTr();\n * return <h1>{tr('admin.dashboard.title', 'Dashboard')}</h1>;\n * return <p>{tr('admin.tenants.toast.createdDescription', '{{name}} has been created.', { name: 'Acme' })}</p>;\n */\nexport function useTr(): (\n key: string,\n fallback: string,\n params?: Record<string, string | number>\n) => string {\n const { t } = useI18n();\n return useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const translated = t(key);\n const raw = translated === key ? fallback : translated;\n if (!params) return raw;\n return Object.entries(params).reduce(\n (str, [k, v]) => str.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), String(v)),\n raw\n );\n },\n [t]\n );\n}\n"],"mappings":";;;AAkBA,SAAgB,IAIJ;CACV,IAAM,EAAE,SAAM,GAAS;AACvB,QAAO,GACJ,GAAa,GAAkB,MAAqD;EACnF,IAAM,IAAa,EAAE,EAAI,EACnB,IAAM,MAAe,IAAM,IAAW;AAE5C,SADK,IACE,OAAO,QAAQ,EAAO,CAAC,QAC3B,GAAK,CAAC,GAAG,OAAO,EAAI,QAAY,OAAO,SAAS,EAAE,SAAS,IAAI,EAAE,OAAO,EAAE,CAAC,EAC5E,EACD,GAJmB;IAMtB,CAAC,EAAE,CACJ"}
|