snipe-auth-rbac 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/index.cjs +515 -0
- package/dist/admin/index.cjs.map +1 -0
- package/dist/admin/index.d.cts +346 -0
- package/dist/admin/index.d.ts +346 -0
- package/dist/admin/index.js +460 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/chunk-4WTV6J44.js +67 -0
- package/dist/chunk-4WTV6J44.js.map +1 -0
- package/dist/chunk-BRCJUCDG.js +55 -0
- package/dist/chunk-BRCJUCDG.js.map +1 -0
- package/dist/index.cjs +148 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +90 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +349 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +221 -0
- package/dist/react/index.d.ts +221 -0
- package/dist/react/index.js +227 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types-BEc5SCIo.d.cts +69 -0
- package/dist/types-BEc5SCIo.d.ts +69 -0
- package/package.json +68 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import {
|
|
2
|
+
groupResources
|
|
3
|
+
} from "../chunk-4WTV6J44.js";
|
|
4
|
+
|
|
5
|
+
// src/admin/transport.ts
|
|
6
|
+
var ACTION_COLUMN = {
|
|
7
|
+
read: "can_read",
|
|
8
|
+
write: "can_write",
|
|
9
|
+
update: "can_update",
|
|
10
|
+
delete: "can_delete"
|
|
11
|
+
};
|
|
12
|
+
function createSupabaseAdminClient(opts) {
|
|
13
|
+
const sb = opts.supabase;
|
|
14
|
+
return {
|
|
15
|
+
async syncResources(resources) {
|
|
16
|
+
if (resources.length === 0) {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
const payload = resources.map((r) => ({
|
|
20
|
+
resource: r.resource,
|
|
21
|
+
scope: r.scope,
|
|
22
|
+
label: r.label,
|
|
23
|
+
description: r.description ?? null,
|
|
24
|
+
group_label: r.group ?? null
|
|
25
|
+
}));
|
|
26
|
+
const { error } = await sb.from("auth_rbac_resources").upsert(payload, { onConflict: "resource" });
|
|
27
|
+
if (error) {
|
|
28
|
+
throw new Error(`syncResources: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
return resources.length;
|
|
31
|
+
},
|
|
32
|
+
async listRoles({ scope, companyId, templatesOnly }) {
|
|
33
|
+
let q = sb.from("auth_rbac_roles").select("*").eq("scope", scope);
|
|
34
|
+
if (templatesOnly) {
|
|
35
|
+
q = q.is("company_id", null);
|
|
36
|
+
} else if (companyId !== void 0) {
|
|
37
|
+
q = companyId === null ? q.is("company_id", null) : q.eq("company_id", companyId);
|
|
38
|
+
}
|
|
39
|
+
const { data, error } = await q.order("name", { ascending: true });
|
|
40
|
+
if (error) {
|
|
41
|
+
throw new Error(`listRoles: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
return data ?? [];
|
|
44
|
+
},
|
|
45
|
+
async listRolePermissions(roleId) {
|
|
46
|
+
const { data, error } = await sb.from("auth_rbac_role_permissions").select("*").eq("role_id", roleId);
|
|
47
|
+
if (error) {
|
|
48
|
+
throw new Error(`listRolePermissions: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
return data ?? [];
|
|
51
|
+
},
|
|
52
|
+
async createRole(input) {
|
|
53
|
+
const row = {
|
|
54
|
+
scope: input.scope,
|
|
55
|
+
company_id: input.companyId ?? null,
|
|
56
|
+
name: input.name,
|
|
57
|
+
description: input.description ?? null,
|
|
58
|
+
frontend_config: input.frontend_config ?? {}
|
|
59
|
+
};
|
|
60
|
+
const { data, error } = await sb.from("auth_rbac_roles").insert(row).select("*").single();
|
|
61
|
+
if (error) {
|
|
62
|
+
throw new Error(`createRole: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
return data;
|
|
65
|
+
},
|
|
66
|
+
async updateRole(id, patch) {
|
|
67
|
+
const { data, error } = await sb.from("auth_rbac_roles").update(patch).eq("id", id).select("*").single();
|
|
68
|
+
if (error) {
|
|
69
|
+
throw new Error(`updateRole: ${error.message}`);
|
|
70
|
+
}
|
|
71
|
+
return data;
|
|
72
|
+
},
|
|
73
|
+
async deleteRole(id) {
|
|
74
|
+
const { error } = await sb.from("auth_rbac_roles").delete().eq("id", id);
|
|
75
|
+
if (error) {
|
|
76
|
+
throw new Error(`deleteRole: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
async setRolePermissionCell({ role_id, resource, action, value }) {
|
|
80
|
+
const column = ACTION_COLUMN[action];
|
|
81
|
+
const row = {
|
|
82
|
+
role_id,
|
|
83
|
+
resource,
|
|
84
|
+
[column]: value
|
|
85
|
+
};
|
|
86
|
+
const { error } = await sb.from("auth_rbac_role_permissions").upsert(row, { onConflict: "role_id,resource" });
|
|
87
|
+
if (error) {
|
|
88
|
+
throw new Error(`setRolePermissionCell: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
async listCompanies() {
|
|
92
|
+
const { data, error } = await sb.from("auth_rbac_companies").select("*").order("name", { ascending: true });
|
|
93
|
+
if (error) {
|
|
94
|
+
throw new Error(`listCompanies: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
return data ?? [];
|
|
97
|
+
},
|
|
98
|
+
async createCompany(input) {
|
|
99
|
+
const { data, error } = await sb.from("auth_rbac_companies").insert({
|
|
100
|
+
name: input.name,
|
|
101
|
+
slug: input.slug ?? null,
|
|
102
|
+
type: input.type ?? null
|
|
103
|
+
}).select("*").single();
|
|
104
|
+
if (error) {
|
|
105
|
+
throw new Error(`createCompany: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
return data;
|
|
108
|
+
},
|
|
109
|
+
async listCompanyMembers(companyId) {
|
|
110
|
+
const { data, error } = await sb.from("auth_rbac_user_company_roles").select("user_id, role_id, assigned_at").eq("company_id", companyId);
|
|
111
|
+
if (error) {
|
|
112
|
+
throw new Error(`listCompanyMembers: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
115
|
+
for (const row of data ?? []) {
|
|
116
|
+
const existing = grouped.get(row.user_id);
|
|
117
|
+
if (existing) {
|
|
118
|
+
existing.role_ids.push(row.role_id);
|
|
119
|
+
} else {
|
|
120
|
+
grouped.set(row.user_id, {
|
|
121
|
+
user_id: row.user_id,
|
|
122
|
+
email: null,
|
|
123
|
+
full_name: null,
|
|
124
|
+
role_ids: [row.role_id],
|
|
125
|
+
invited_at: row.assigned_at,
|
|
126
|
+
invitation_status: "accepted"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return Array.from(grouped.values());
|
|
131
|
+
},
|
|
132
|
+
async inviteCompanyMember({ companyId, email, roleIds }) {
|
|
133
|
+
const { error } = await sb.auth.admin.inviteUserByEmail(email, {
|
|
134
|
+
data: {
|
|
135
|
+
auth_rbac_company_id: companyId,
|
|
136
|
+
auth_rbac_role_ids: roleIds
|
|
137
|
+
},
|
|
138
|
+
redirectTo: opts.inviteRedirectUrl
|
|
139
|
+
});
|
|
140
|
+
if (error) {
|
|
141
|
+
throw new Error(`inviteCompanyMember: ${error.message}`);
|
|
142
|
+
}
|
|
143
|
+
return { invited: true };
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/admin/hooks.tsx
|
|
149
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
150
|
+
import { jsx } from "react/jsx-runtime";
|
|
151
|
+
var AdminTransportContext = createContext(null);
|
|
152
|
+
function AdminTransportProvider(props) {
|
|
153
|
+
return /* @__PURE__ */ jsx(AdminTransportContext.Provider, { value: props.transport, children: props.children });
|
|
154
|
+
}
|
|
155
|
+
function useAdminTransport() {
|
|
156
|
+
const t = useContext(AdminTransportContext);
|
|
157
|
+
if (!t) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
"auth-rbac admin hooks require <AdminTransportProvider> \u2014 wrap your admin pages with one."
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return t;
|
|
163
|
+
}
|
|
164
|
+
function useAsync(loader, deps) {
|
|
165
|
+
const [state, setState] = useState({
|
|
166
|
+
data: null,
|
|
167
|
+
isLoading: true,
|
|
168
|
+
error: null
|
|
169
|
+
});
|
|
170
|
+
const refresh = useCallback(async () => {
|
|
171
|
+
setState((s) => ({ ...s, isLoading: true, error: null }));
|
|
172
|
+
try {
|
|
173
|
+
const data = await loader();
|
|
174
|
+
setState({ data, isLoading: false, error: null });
|
|
175
|
+
} catch (e) {
|
|
176
|
+
setState({
|
|
177
|
+
data: null,
|
|
178
|
+
isLoading: false,
|
|
179
|
+
error: e instanceof Error ? e : new Error(String(e))
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}, deps);
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
void refresh();
|
|
185
|
+
}, [refresh]);
|
|
186
|
+
return { ...state, refresh };
|
|
187
|
+
}
|
|
188
|
+
function useAdminRoles(args) {
|
|
189
|
+
const transport = useAdminTransport();
|
|
190
|
+
return useAsync(
|
|
191
|
+
() => transport.listRoles(args),
|
|
192
|
+
[transport, args.scope, args.companyId, args.templatesOnly]
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
function useAdminRolePermissions(roleId) {
|
|
196
|
+
const transport = useAdminTransport();
|
|
197
|
+
return useAsync(
|
|
198
|
+
async () => roleId == null ? [] : transport.listRolePermissions(roleId),
|
|
199
|
+
[transport, roleId]
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
function useAdminCompanies() {
|
|
203
|
+
const transport = useAdminTransport();
|
|
204
|
+
return useAsync(() => transport.listCompanies(), [transport]);
|
|
205
|
+
}
|
|
206
|
+
function useAdminCompanyMembers(companyId) {
|
|
207
|
+
const transport = useAdminTransport();
|
|
208
|
+
return useAsync(
|
|
209
|
+
async () => companyId == null ? [] : transport.listCompanyMembers(companyId),
|
|
210
|
+
[transport, companyId]
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
function useMutation(fn) {
|
|
214
|
+
const [state, setState] = useState({
|
|
215
|
+
isPending: false,
|
|
216
|
+
error: null
|
|
217
|
+
});
|
|
218
|
+
const mutate = useCallback(
|
|
219
|
+
async (...args) => {
|
|
220
|
+
setState({ isPending: true, error: null });
|
|
221
|
+
try {
|
|
222
|
+
const result = await fn(...args);
|
|
223
|
+
setState({ isPending: false, error: null });
|
|
224
|
+
return result;
|
|
225
|
+
} catch (e) {
|
|
226
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
227
|
+
setState({ isPending: false, error: err });
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
232
|
+
[fn]
|
|
233
|
+
);
|
|
234
|
+
return { mutate, ...state };
|
|
235
|
+
}
|
|
236
|
+
function useCreateRole() {
|
|
237
|
+
const transport = useAdminTransport();
|
|
238
|
+
return useMutation(transport.createRole);
|
|
239
|
+
}
|
|
240
|
+
function useUpdateRole() {
|
|
241
|
+
const transport = useAdminTransport();
|
|
242
|
+
return useMutation(transport.updateRole);
|
|
243
|
+
}
|
|
244
|
+
function useDeleteRole() {
|
|
245
|
+
const transport = useAdminTransport();
|
|
246
|
+
return useMutation(transport.deleteRole);
|
|
247
|
+
}
|
|
248
|
+
function useSetRolePermissionCell() {
|
|
249
|
+
const transport = useAdminTransport();
|
|
250
|
+
return useMutation(transport.setRolePermissionCell);
|
|
251
|
+
}
|
|
252
|
+
function useCreateCompany() {
|
|
253
|
+
const transport = useAdminTransport();
|
|
254
|
+
return useMutation(transport.createCompany);
|
|
255
|
+
}
|
|
256
|
+
function useInviteCompanyMember() {
|
|
257
|
+
const transport = useAdminTransport();
|
|
258
|
+
return useMutation(transport.inviteCompanyMember);
|
|
259
|
+
}
|
|
260
|
+
function useRolePermissionGrid(roleId) {
|
|
261
|
+
const { data, isLoading, error, refresh } = useAdminRolePermissions(roleId);
|
|
262
|
+
const setCell = useSetRolePermissionCell();
|
|
263
|
+
const grid = useMemo(() => {
|
|
264
|
+
const out = {};
|
|
265
|
+
for (const row of data ?? []) {
|
|
266
|
+
out[row.resource] = {
|
|
267
|
+
read: row.can_read,
|
|
268
|
+
write: row.can_write,
|
|
269
|
+
update: row.can_update,
|
|
270
|
+
delete: row.can_delete
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}, [data]);
|
|
275
|
+
const updateCell = useCallback(
|
|
276
|
+
async (resource, action, value) => {
|
|
277
|
+
if (!roleId) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
await setCell.mutate({ role_id: roleId, resource, action, value });
|
|
281
|
+
void refresh();
|
|
282
|
+
},
|
|
283
|
+
[roleId, setCell, refresh]
|
|
284
|
+
);
|
|
285
|
+
return {
|
|
286
|
+
grid,
|
|
287
|
+
isLoading,
|
|
288
|
+
error,
|
|
289
|
+
refresh,
|
|
290
|
+
updateCell,
|
|
291
|
+
isUpdating: setCell.isPending,
|
|
292
|
+
updateError: setCell.error
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/admin/PermissionsMatrix.tsx
|
|
297
|
+
import { useMemo as useMemo2 } from "react";
|
|
298
|
+
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
299
|
+
var ACTIONS = ["read", "write", "update", "delete"];
|
|
300
|
+
function PermissionsMatrix(props) {
|
|
301
|
+
const { grid, isLoading, error, updateCell, isUpdating } = useRolePermissionGrid(props.roleId);
|
|
302
|
+
const groups = useMemo2(
|
|
303
|
+
() => groupResources(props.resources),
|
|
304
|
+
[props.resources]
|
|
305
|
+
);
|
|
306
|
+
const isCellEnabled = (resource, action) => {
|
|
307
|
+
return grid[resource]?.[action] ?? false;
|
|
308
|
+
};
|
|
309
|
+
const setCell = async (resource, action, value) => {
|
|
310
|
+
await updateCell(resource, action, value);
|
|
311
|
+
};
|
|
312
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: props.children({
|
|
313
|
+
groups,
|
|
314
|
+
isCellEnabled,
|
|
315
|
+
setCell,
|
|
316
|
+
isLoading,
|
|
317
|
+
isUpdating,
|
|
318
|
+
error,
|
|
319
|
+
actions: ACTIONS
|
|
320
|
+
}) });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/admin/RolesList.tsx
|
|
324
|
+
import { useCallback as useCallback2, useState as useState2 } from "react";
|
|
325
|
+
import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
|
|
326
|
+
function RolesList(props) {
|
|
327
|
+
const { scope, companyId, autoSelectFirst = true } = props;
|
|
328
|
+
const list = useAdminRoles({ scope, companyId });
|
|
329
|
+
const create = useCreateRole();
|
|
330
|
+
const remove = useDeleteRole();
|
|
331
|
+
const [selectedRoleId, setSelectedRoleId] = useState2(null);
|
|
332
|
+
if (autoSelectFirst && selectedRoleId == null && list.data != null && list.data.length > 0) {
|
|
333
|
+
setSelectedRoleId(list.data[0].id);
|
|
334
|
+
}
|
|
335
|
+
const createRole = useCallback2(
|
|
336
|
+
async (input) => {
|
|
337
|
+
const role = await create.mutate({
|
|
338
|
+
scope,
|
|
339
|
+
companyId: companyId ?? null,
|
|
340
|
+
name: input.name,
|
|
341
|
+
description: input.description
|
|
342
|
+
});
|
|
343
|
+
await list.refresh();
|
|
344
|
+
setSelectedRoleId(role.id);
|
|
345
|
+
return role;
|
|
346
|
+
},
|
|
347
|
+
[create, scope, companyId, list]
|
|
348
|
+
);
|
|
349
|
+
const deleteRole = useCallback2(
|
|
350
|
+
async (id) => {
|
|
351
|
+
await remove.mutate(id);
|
|
352
|
+
if (selectedRoleId === id) {
|
|
353
|
+
setSelectedRoleId(null);
|
|
354
|
+
}
|
|
355
|
+
await list.refresh();
|
|
356
|
+
},
|
|
357
|
+
[remove, list, selectedRoleId]
|
|
358
|
+
);
|
|
359
|
+
return /* @__PURE__ */ jsx3(Fragment2, { children: props.children({
|
|
360
|
+
roles: list.data ?? [],
|
|
361
|
+
isLoading: list.isLoading,
|
|
362
|
+
error: list.error,
|
|
363
|
+
selectedRoleId,
|
|
364
|
+
selectRole: setSelectedRoleId,
|
|
365
|
+
createRole,
|
|
366
|
+
isCreating: create.isPending,
|
|
367
|
+
createError: create.error,
|
|
368
|
+
deleteRole,
|
|
369
|
+
isDeleting: remove.isPending,
|
|
370
|
+
deleteError: remove.error,
|
|
371
|
+
refresh: list.refresh
|
|
372
|
+
}) });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/admin/InviteMemberForm.tsx
|
|
376
|
+
import { useCallback as useCallback3, useState as useState3 } from "react";
|
|
377
|
+
import { Fragment as Fragment3, jsx as jsx4 } from "react/jsx-runtime";
|
|
378
|
+
function InviteMemberForm(props) {
|
|
379
|
+
const rolesQuery = useAdminRoles({
|
|
380
|
+
scope: "company",
|
|
381
|
+
companyId: props.companyId
|
|
382
|
+
});
|
|
383
|
+
const invite = useInviteCompanyMember();
|
|
384
|
+
const [email, setEmail] = useState3("");
|
|
385
|
+
const [selectedRoleIds, setSelectedRoleIds] = useState3(
|
|
386
|
+
/* @__PURE__ */ new Set()
|
|
387
|
+
);
|
|
388
|
+
const [submittedSuccessfully, setSubmittedSuccessfully] = useState3(false);
|
|
389
|
+
const toggleRole = useCallback3((roleId) => {
|
|
390
|
+
setSelectedRoleIds((prev) => {
|
|
391
|
+
const next = new Set(prev);
|
|
392
|
+
if (next.has(roleId)) {
|
|
393
|
+
next.delete(roleId);
|
|
394
|
+
} else {
|
|
395
|
+
next.add(roleId);
|
|
396
|
+
}
|
|
397
|
+
return next;
|
|
398
|
+
});
|
|
399
|
+
}, []);
|
|
400
|
+
const resetForm = useCallback3(() => {
|
|
401
|
+
setEmail("");
|
|
402
|
+
setSelectedRoleIds(/* @__PURE__ */ new Set());
|
|
403
|
+
setSubmittedSuccessfully(false);
|
|
404
|
+
}, []);
|
|
405
|
+
const errors = {};
|
|
406
|
+
if (email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
|
|
407
|
+
errors.email = "Bitte gib eine g\xFCltige E-Mail-Adresse ein.";
|
|
408
|
+
}
|
|
409
|
+
if (selectedRoleIds.size === 0) {
|
|
410
|
+
errors.roles = "Bitte mindestens eine Rolle ausw\xE4hlen.";
|
|
411
|
+
}
|
|
412
|
+
const isValid = email.trim().length > 0 && Object.keys(errors).length === 0;
|
|
413
|
+
const submit = useCallback3(async () => {
|
|
414
|
+
if (!isValid) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
await invite.mutate({
|
|
418
|
+
companyId: props.companyId,
|
|
419
|
+
email: email.trim(),
|
|
420
|
+
roleIds: Array.from(selectedRoleIds)
|
|
421
|
+
});
|
|
422
|
+
setSubmittedSuccessfully(true);
|
|
423
|
+
props.onSuccess?.();
|
|
424
|
+
}, [invite, props, email, selectedRoleIds, isValid]);
|
|
425
|
+
return /* @__PURE__ */ jsx4(Fragment3, { children: props.children({
|
|
426
|
+
email,
|
|
427
|
+
setEmail,
|
|
428
|
+
selectedRoleIds,
|
|
429
|
+
toggleRole,
|
|
430
|
+
resetForm,
|
|
431
|
+
roles: rolesQuery.data ?? [],
|
|
432
|
+
rolesLoading: rolesQuery.isLoading,
|
|
433
|
+
rolesError: rolesQuery.error,
|
|
434
|
+
submit,
|
|
435
|
+
isSubmitting: invite.isPending,
|
|
436
|
+
submitError: invite.error,
|
|
437
|
+
submittedSuccessfully,
|
|
438
|
+
isValid,
|
|
439
|
+
errors
|
|
440
|
+
}) });
|
|
441
|
+
}
|
|
442
|
+
export {
|
|
443
|
+
AdminTransportProvider,
|
|
444
|
+
InviteMemberForm,
|
|
445
|
+
PermissionsMatrix,
|
|
446
|
+
RolesList,
|
|
447
|
+
createSupabaseAdminClient,
|
|
448
|
+
useAdminCompanies,
|
|
449
|
+
useAdminCompanyMembers,
|
|
450
|
+
useAdminRolePermissions,
|
|
451
|
+
useAdminRoles,
|
|
452
|
+
useCreateCompany,
|
|
453
|
+
useCreateRole,
|
|
454
|
+
useDeleteRole,
|
|
455
|
+
useInviteCompanyMember,
|
|
456
|
+
useRolePermissionGrid,
|
|
457
|
+
useSetRolePermissionCell,
|
|
458
|
+
useUpdateRole
|
|
459
|
+
};
|
|
460
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/admin/transport.ts","../../src/admin/hooks.tsx","../../src/admin/PermissionsMatrix.tsx","../../src/admin/RolesList.tsx","../../src/admin/InviteMemberForm.tsx"],"sourcesContent":["/**\n * Default Supabase implementation of the admin transport. Hits the\n * package's tables directly via `from(...)` and the auth admin\n * endpoint for invites.\n *\n * Projects that route admin writes through their own backend\n * (e.g. for audit logging or extra validation) skip this and\n * implement `AdminTransport` themselves.\n */\n\nimport type { Action, ResourceDescriptor } from \"../types.js\";\n\nimport type {\n AdminCompany,\n AdminMember,\n AdminRole,\n AdminRolePermission,\n AdminTransport,\n} from \"./types.js\";\n\ninterface SupabaseAdmin {\n from(table: string): {\n select: (cols: string) => {\n eq: (col: string, value: unknown) => any;\n is: (col: string, value: unknown) => any;\n order: (col: string, opts?: { ascending: boolean }) => any;\n };\n insert: (row: Record<string, unknown>) => {\n select: (cols: string) => { single: () => any };\n };\n update: (patch: Record<string, unknown>) => {\n eq: (col: string, value: unknown) => {\n select: (cols: string) => { single: () => any };\n };\n };\n upsert: (\n row: Record<string, unknown> | Array<Record<string, unknown>>,\n opts?: { onConflict: string },\n ) => Promise<{ error: { message: string } | null }>;\n delete: () => { eq: (col: string, value: unknown) => any };\n };\n auth: {\n admin: {\n inviteUserByEmail: (\n email: string,\n opts?: { data?: Record<string, unknown>; redirectTo?: string },\n ) => Promise<{ data: unknown; error: { message: string } | null }>;\n };\n };\n}\n\nexport interface SupabaseAdminClientOptions {\n supabase: SupabaseAdmin;\n /** Where the invitee should land after setting their password. */\n inviteRedirectUrl?: string;\n}\n\nconst ACTION_COLUMN: Record<Action, string> = {\n read: \"can_read\",\n write: \"can_write\",\n update: \"can_update\",\n delete: \"can_delete\",\n};\n\nexport function createSupabaseAdminClient(\n opts: SupabaseAdminClientOptions,\n): AdminTransport {\n const sb = opts.supabase;\n\n return {\n async syncResources(resources) {\n if (resources.length === 0) {\n return 0;\n }\n const payload = resources.map((r) => ({\n resource: r.resource,\n scope: r.scope,\n label: r.label,\n description: r.description ?? null,\n group_label: r.group ?? null,\n }));\n const { error } = await sb\n .from(\"auth_rbac_resources\")\n .upsert(payload, { onConflict: \"resource\" });\n if (error) {\n throw new Error(`syncResources: ${error.message}`);\n }\n return resources.length;\n },\n\n async listRoles({ scope, companyId, templatesOnly }) {\n let q = sb\n .from(\"auth_rbac_roles\")\n .select(\"*\")\n .eq(\"scope\", scope);\n if (templatesOnly) {\n q = q.is(\"company_id\", null);\n } else if (companyId !== undefined) {\n q = companyId === null ? q.is(\"company_id\", null) : q.eq(\"company_id\", companyId);\n }\n const { data, error } = await q.order(\"name\", { ascending: true });\n if (error) {\n throw new Error(`listRoles: ${error.message}`);\n }\n return (data ?? []) as AdminRole[];\n },\n\n async listRolePermissions(roleId) {\n const { data, error } = await sb\n .from(\"auth_rbac_role_permissions\")\n .select(\"*\")\n .eq(\"role_id\", roleId);\n if (error) {\n throw new Error(`listRolePermissions: ${error.message}`);\n }\n return (data ?? []) as AdminRolePermission[];\n },\n\n async createRole(input) {\n const row = {\n scope: input.scope,\n company_id: input.companyId ?? null,\n name: input.name,\n description: input.description ?? null,\n frontend_config: input.frontend_config ?? {},\n };\n const { data, error } = await sb\n .from(\"auth_rbac_roles\")\n .insert(row)\n .select(\"*\")\n .single();\n if (error) {\n throw new Error(`createRole: ${error.message}`);\n }\n return data as AdminRole;\n },\n\n async updateRole(id, patch) {\n const { data, error } = await sb\n .from(\"auth_rbac_roles\")\n .update(patch)\n .eq(\"id\", id)\n .select(\"*\")\n .single();\n if (error) {\n throw new Error(`updateRole: ${error.message}`);\n }\n return data as AdminRole;\n },\n\n async deleteRole(id) {\n const { error } = await sb\n .from(\"auth_rbac_roles\")\n .delete()\n .eq(\"id\", id);\n if (error) {\n throw new Error(`deleteRole: ${error.message}`);\n }\n },\n\n async setRolePermissionCell({ role_id, resource, action, value }) {\n const column = ACTION_COLUMN[action];\n const row: Record<string, unknown> = {\n role_id,\n resource,\n [column]: value,\n };\n const { error } = await sb\n .from(\"auth_rbac_role_permissions\")\n .upsert(row, { onConflict: \"role_id,resource\" });\n if (error) {\n throw new Error(`setRolePermissionCell: ${error.message}`);\n }\n },\n\n async listCompanies() {\n const { data, error } = await sb\n .from(\"auth_rbac_companies\")\n .select(\"*\")\n .order(\"name\", { ascending: true });\n if (error) {\n throw new Error(`listCompanies: ${error.message}`);\n }\n return (data ?? []) as AdminCompany[];\n },\n\n async createCompany(input) {\n const { data, error } = await sb\n .from(\"auth_rbac_companies\")\n .insert({\n name: input.name,\n slug: input.slug ?? null,\n type: input.type ?? null,\n })\n .select(\"*\")\n .single();\n if (error) {\n throw new Error(`createCompany: ${error.message}`);\n }\n return data as AdminCompany;\n },\n\n async listCompanyMembers(companyId) {\n // The package doesn't ship a view that joins users + invitations\n // out of the box because the host's auth.users schema may differ.\n // Adopters that need a richer join replace this with their own\n // transport. Fallback: list raw assignments.\n const { data, error } = await sb\n .from(\"auth_rbac_user_company_roles\")\n .select(\"user_id, role_id, assigned_at\")\n .eq(\"company_id\", companyId);\n if (error) {\n throw new Error(`listCompanyMembers: ${error.message}`);\n }\n const grouped = new Map<string, AdminMember>();\n for (const row of (data ?? []) as Array<{\n user_id: string;\n role_id: string;\n assigned_at: string;\n }>) {\n const existing = grouped.get(row.user_id);\n if (existing) {\n existing.role_ids.push(row.role_id);\n } else {\n grouped.set(row.user_id, {\n user_id: row.user_id,\n email: null,\n full_name: null,\n role_ids: [row.role_id],\n invited_at: row.assigned_at,\n invitation_status: \"accepted\",\n });\n }\n }\n return Array.from(grouped.values());\n },\n\n async inviteCompanyMember({ companyId, email, roleIds }) {\n const { error } = await sb.auth.admin.inviteUserByEmail(email, {\n data: {\n auth_rbac_company_id: companyId,\n auth_rbac_role_ids: roleIds,\n },\n redirectTo: opts.inviteRedirectUrl,\n });\n if (error) {\n throw new Error(`inviteCompanyMember: ${error.message}`);\n }\n return { invited: true };\n },\n };\n}\n","/**\n * React hooks for the admin surface. UI-kit-agnostic — adopters\n * render whatever JSX they like with the data + mutations these\n * expose. A copy-paste reference page styled with Tailwind primitives\n * lives in `examples/react-admin/`.\n *\n * Pattern: each hook returns `{ data, isLoading, error, refresh }`\n * and where applicable `{ mutate }`. We deliberately avoid pulling in\n * react-query as a dependency so the package stays peer-light;\n * adopters that already use react-query can wrap these primitives\n * with an extra hook of their own (5 lines).\n */\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\n\nimport type { Action, FrontendConfig, ResourceScope } from \"../types.js\";\n\nimport type {\n AdminCompany,\n AdminMember,\n AdminRole,\n AdminRolePermission,\n AdminTransport,\n} from \"./types.js\";\n\n// ─────────────────────────────────────────────────────────────────\n// Context — adopter mounts <AdminTransportProvider> once\n// ─────────────────────────────────────────────────────────────────\n\nconst AdminTransportContext = createContext<AdminTransport | null>(null);\n\nexport interface AdminTransportProviderProps {\n transport: AdminTransport;\n children: React.ReactNode;\n}\n\nexport function AdminTransportProvider(props: AdminTransportProviderProps) {\n return (\n <AdminTransportContext.Provider value={props.transport}>\n {props.children}\n </AdminTransportContext.Provider>\n );\n}\n\nfunction useAdminTransport(): AdminTransport {\n const t = useContext(AdminTransportContext);\n if (!t) {\n throw new Error(\n \"auth-rbac admin hooks require <AdminTransportProvider> — wrap your admin pages with one.\",\n );\n }\n return t;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Tiny generic async-state helper. Avoids reinventing react-query\n// while keeping the boilerplate per-hook to a single line.\n// ─────────────────────────────────────────────────────────────────\n\ninterface AsyncState<T> {\n data: T | null;\n isLoading: boolean;\n error: Error | null;\n}\n\nfunction useAsync<T>(loader: () => Promise<T>, deps: ReadonlyArray<unknown>) {\n const [state, setState] = useState<AsyncState<T>>({\n data: null,\n isLoading: true,\n error: null,\n });\n\n const refresh = useCallback(async () => {\n setState((s) => ({ ...s, isLoading: true, error: null }));\n try {\n const data = await loader();\n setState({ data, isLoading: false, error: null });\n } catch (e) {\n setState({\n data: null,\n isLoading: false,\n error: e instanceof Error ? e : new Error(String(e)),\n });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n return { ...state, refresh };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Reads\n// ─────────────────────────────────────────────────────────────────\n\nexport function useAdminRoles(args: {\n scope: ResourceScope;\n companyId?: string | null;\n templatesOnly?: boolean;\n}) {\n const transport = useAdminTransport();\n return useAsync(\n () => transport.listRoles(args),\n [transport, args.scope, args.companyId, args.templatesOnly],\n );\n}\n\nexport function useAdminRolePermissions(roleId: string | null) {\n const transport = useAdminTransport();\n return useAsync(\n async () =>\n roleId == null ? [] : transport.listRolePermissions(roleId),\n [transport, roleId],\n );\n}\n\nexport function useAdminCompanies() {\n const transport = useAdminTransport();\n return useAsync(() => transport.listCompanies(), [transport]);\n}\n\nexport function useAdminCompanyMembers(companyId: string | null) {\n const transport = useAdminTransport();\n return useAsync(\n async () =>\n companyId == null ? [] : transport.listCompanyMembers(companyId),\n [transport, companyId],\n );\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Mutations — return `{ mutate, isPending, error }`. Adopters wrap\n// these in their own toast / error-boundary as needed.\n// ─────────────────────────────────────────────────────────────────\n\ninterface MutationState {\n isPending: boolean;\n error: Error | null;\n}\n\nfunction useMutation<TArgs extends unknown[], TResult>(\n fn: (...args: TArgs) => Promise<TResult>,\n) {\n const [state, setState] = useState<MutationState>({\n isPending: false,\n error: null,\n });\n\n const mutate = useCallback(\n async (...args: TArgs): Promise<TResult> => {\n setState({ isPending: true, error: null });\n try {\n const result = await fn(...args);\n setState({ isPending: false, error: null });\n return result;\n } catch (e) {\n const err = e instanceof Error ? e : new Error(String(e));\n setState({ isPending: false, error: err });\n throw err;\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [fn],\n );\n\n return { mutate, ...state };\n}\n\nexport function useCreateRole() {\n const transport = useAdminTransport();\n return useMutation(transport.createRole);\n}\n\nexport function useUpdateRole() {\n const transport = useAdminTransport();\n return useMutation(transport.updateRole);\n}\n\nexport function useDeleteRole() {\n const transport = useAdminTransport();\n return useMutation(transport.deleteRole);\n}\n\nexport function useSetRolePermissionCell() {\n const transport = useAdminTransport();\n return useMutation(transport.setRolePermissionCell);\n}\n\nexport function useCreateCompany() {\n const transport = useAdminTransport();\n return useMutation(transport.createCompany);\n}\n\nexport function useInviteCompanyMember() {\n const transport = useAdminTransport();\n return useMutation(transport.inviteCompanyMember);\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Convenience: hold a role's full state (role + permission grid)\n// in one hook, with a `setCell` mutator that optimistically updates\n// the local cache and writes through to the transport.\n// ─────────────────────────────────────────────────────────────────\n\nexport interface RolePermissionGrid {\n // resource → action → boolean\n [resource: string]: { [A in Action]: boolean };\n}\n\nexport function useRolePermissionGrid(roleId: string | null) {\n const { data, isLoading, error, refresh } = useAdminRolePermissions(roleId);\n const setCell = useSetRolePermissionCell();\n\n const grid = useMemo<RolePermissionGrid>(() => {\n const out: RolePermissionGrid = {};\n for (const row of data ?? []) {\n out[row.resource] = {\n read: row.can_read,\n write: row.can_write,\n update: row.can_update,\n delete: row.can_delete,\n };\n }\n return out;\n }, [data]);\n\n const updateCell = useCallback(\n async (resource: string, action: Action, value: boolean) => {\n if (!roleId) {\n return;\n }\n await setCell.mutate({ role_id: roleId, resource, action, value });\n void refresh();\n },\n [roleId, setCell, refresh],\n );\n\n return {\n grid,\n isLoading,\n error,\n refresh,\n updateCell,\n isUpdating: setCell.isPending,\n updateError: setCell.error,\n };\n}\n","/**\n * Headless permissions matrix.\n *\n * Owns:\n * - reading the role's current permission grid\n * - debounced write-through on every cell toggle\n * - grouping resources by `group` for a sectioned UI\n *\n * Owns NOTHING about styling — the consumer renders all JSX via the\n * single `children` render-prop. A copy-paste reference styled with\n * Tailwind + Radix lives in `examples/react-admin/`.\n *\n * @example minimum viable adoption\n *\n * <PermissionsMatrix\n * roleId={role.id}\n * resources={resources.filter(r => r.scope === role.scope)}\n * >\n * {({ groups, isCellEnabled, setCell, isLoading }) =>\n * groups.map((g) => (\n * <section key={g.group}>\n * <h3>{g.group}</h3>\n * {g.resources.map((r) => (\n * <div key={r.resource}>\n * <span>{r.label}</span>\n * {([\"read\", \"write\", \"update\", \"delete\"] as const).map((a) => (\n * <input\n * key={a}\n * type=\"checkbox\"\n * checked={isCellEnabled(r.resource, a)}\n * disabled={isLoading}\n * onChange={(e) => setCell(r.resource, a, e.target.checked)}\n * />\n * ))}\n * </div>\n * ))}\n * </section>\n * ))\n * }\n * </PermissionsMatrix>\n */\n\nimport { useMemo } from \"react\";\n\nimport type {\n Action,\n ResourceDescriptor,\n} from \"../types.js\";\nimport { groupResources } from \"../client.js\";\n\nimport { useRolePermissionGrid } from \"./hooks.js\";\n\nexport interface MatrixGroup {\n group: string;\n resources: ResourceDescriptor[];\n}\n\nexport interface MatrixRenderArgs {\n /** Resources grouped by their `group` label, original insertion order. */\n groups: MatrixGroup[];\n /** Read a single cell from the current grid. */\n isCellEnabled: (resource: string, action: Action) => boolean;\n /** Write a single cell. Optimistic in the local cache + writes through. */\n setCell: (resource: string, action: Action, value: boolean) => Promise<void>;\n isLoading: boolean;\n isUpdating: boolean;\n error: Error | null;\n /** All four actions, exposed for the consumer to render headers. */\n actions: ReadonlyArray<Action>;\n}\n\nexport interface PermissionsMatrixProps {\n roleId: string | null;\n resources: ReadonlyArray<ResourceDescriptor>;\n children: (args: MatrixRenderArgs) => React.ReactNode;\n}\n\nconst ACTIONS = [\"read\", \"write\", \"update\", \"delete\"] as const;\n\nexport function PermissionsMatrix(props: PermissionsMatrixProps) {\n const { grid, isLoading, error, updateCell, isUpdating } =\n useRolePermissionGrid(props.roleId);\n\n const groups = useMemo<MatrixGroup[]>(\n () => groupResources(props.resources),\n [props.resources],\n );\n\n const isCellEnabled = (resource: string, action: Action): boolean => {\n return grid[resource]?.[action] ?? false;\n };\n\n const setCell = async (resource: string, action: Action, value: boolean) => {\n await updateCell(resource, action, value);\n };\n\n return (\n <>\n {props.children({\n groups,\n isCellEnabled,\n setCell,\n isLoading,\n isUpdating,\n error,\n actions: ACTIONS,\n })}\n </>\n );\n}\n","/**\n * Headless roles-list controller. Tracks selection + create/delete\n * mutations; consumer renders the list, the new-role dialog, and\n * the destructive-action confirmation.\n */\n\nimport { useCallback, useState } from \"react\";\n\nimport type { ResourceScope } from \"../types.js\";\n\nimport {\n useAdminRoles,\n useCreateRole,\n useDeleteRole,\n} from \"./hooks.js\";\nimport type { AdminRole } from \"./types.js\";\n\nexport interface RolesListRenderArgs {\n roles: AdminRole[];\n isLoading: boolean;\n error: Error | null;\n\n selectedRoleId: string | null;\n selectRole: (id: string | null) => void;\n\n createRole: (input: {\n name: string;\n description?: string;\n }) => Promise<AdminRole>;\n isCreating: boolean;\n createError: Error | null;\n\n deleteRole: (id: string) => Promise<void>;\n isDeleting: boolean;\n deleteError: Error | null;\n\n refresh: () => Promise<void>;\n}\n\nexport interface RolesListProps {\n scope: ResourceScope;\n /** Required for company-scope. Pass `null` for templates. */\n companyId?: string | null;\n /** Pre-select the first role on load. Default: true. */\n autoSelectFirst?: boolean;\n children: (args: RolesListRenderArgs) => React.ReactNode;\n}\n\nexport function RolesList(props: RolesListProps) {\n const { scope, companyId, autoSelectFirst = true } = props;\n\n const list = useAdminRoles({ scope, companyId });\n const create = useCreateRole();\n const remove = useDeleteRole();\n\n const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);\n\n // Auto-select first role on load.\n if (\n autoSelectFirst &&\n selectedRoleId == null &&\n list.data != null &&\n list.data.length > 0\n ) {\n setSelectedRoleId(list.data[0]!.id);\n }\n\n const createRole = useCallback(\n async (input: { name: string; description?: string }) => {\n const role = await create.mutate({\n scope,\n companyId: companyId ?? null,\n name: input.name,\n description: input.description,\n });\n await list.refresh();\n setSelectedRoleId(role.id);\n return role;\n },\n [create, scope, companyId, list],\n );\n\n const deleteRole = useCallback(\n async (id: string) => {\n await remove.mutate(id);\n if (selectedRoleId === id) {\n setSelectedRoleId(null);\n }\n await list.refresh();\n },\n [remove, list, selectedRoleId],\n );\n\n return (\n <>\n {props.children({\n roles: list.data ?? [],\n isLoading: list.isLoading,\n error: list.error,\n selectedRoleId,\n selectRole: setSelectedRoleId,\n createRole,\n isCreating: create.isPending,\n createError: create.error,\n deleteRole,\n isDeleting: remove.isPending,\n deleteError: remove.error,\n refresh: list.refresh,\n })}\n </>\n );\n}\n","/**\n * Headless invite-member form state. Tracks email + selected role\n * ids, runs basic local validation, and exposes a submit handler\n * that calls the configured transport (Supabase Auth invite by\n * default).\n */\n\nimport { useCallback, useState } from \"react\";\n\nimport { useAdminRoles, useInviteCompanyMember } from \"./hooks.js\";\nimport type { AdminRole } from \"./types.js\";\n\nexport interface InviteMemberFormRenderArgs {\n // form state\n email: string;\n setEmail: (v: string) => void;\n selectedRoleIds: Set<string>;\n toggleRole: (roleId: string) => void;\n resetForm: () => void;\n\n // catalog\n roles: AdminRole[];\n rolesLoading: boolean;\n rolesError: Error | null;\n\n // submission\n submit: () => Promise<void>;\n isSubmitting: boolean;\n submitError: Error | null;\n submittedSuccessfully: boolean;\n\n // validation\n isValid: boolean;\n errors: { email?: string; roles?: string };\n}\n\nexport interface InviteMemberFormProps {\n companyId: string;\n /** Called after a successful invite — typically clears a dialog. */\n onSuccess?: () => void;\n children: (args: InviteMemberFormRenderArgs) => React.ReactNode;\n}\n\nexport function InviteMemberForm(props: InviteMemberFormProps) {\n const rolesQuery = useAdminRoles({\n scope: \"company\",\n companyId: props.companyId,\n });\n const invite = useInviteCompanyMember();\n\n const [email, setEmail] = useState(\"\");\n const [selectedRoleIds, setSelectedRoleIds] = useState<Set<string>>(\n new Set(),\n );\n const [submittedSuccessfully, setSubmittedSuccessfully] = useState(false);\n\n const toggleRole = useCallback((roleId: string) => {\n setSelectedRoleIds((prev) => {\n const next = new Set(prev);\n if (next.has(roleId)) {\n next.delete(roleId);\n } else {\n next.add(roleId);\n }\n return next;\n });\n }, []);\n\n const resetForm = useCallback(() => {\n setEmail(\"\");\n setSelectedRoleIds(new Set());\n setSubmittedSuccessfully(false);\n }, []);\n\n const errors: InviteMemberFormRenderArgs[\"errors\"] = {};\n if (email.trim() && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email.trim())) {\n errors.email = \"Bitte gib eine gültige E-Mail-Adresse ein.\";\n }\n if (selectedRoleIds.size === 0) {\n errors.roles = \"Bitte mindestens eine Rolle auswählen.\";\n }\n const isValid =\n email.trim().length > 0 &&\n Object.keys(errors).length === 0;\n\n const submit = useCallback(async () => {\n if (!isValid) {\n return;\n }\n await invite.mutate({\n companyId: props.companyId,\n email: email.trim(),\n roleIds: Array.from(selectedRoleIds),\n });\n setSubmittedSuccessfully(true);\n props.onSuccess?.();\n }, [invite, props, email, selectedRoleIds, isValid]);\n\n return (\n <>\n {props.children({\n email,\n setEmail,\n selectedRoleIds,\n toggleRole,\n resetForm,\n roles: rolesQuery.data ?? [],\n rolesLoading: rolesQuery.isLoading,\n rolesError: rolesQuery.error,\n submit,\n isSubmitting: invite.isPending,\n submitError: invite.error,\n submittedSuccessfully,\n isValid,\n errors,\n })}\n </>\n );\n}\n"],"mappings":"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","names":["useMemo","jsx","useMemo","useCallback","useState","Fragment","jsx","useState","useCallback","useCallback","useState","Fragment","jsx","useState","useCallback"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
function buildPermissionResolver(resources, profile, defaultCompanyId) {
|
|
3
|
+
const scopeByResource = new Map(
|
|
4
|
+
resources.map((r) => [r.resource, r.scope])
|
|
5
|
+
);
|
|
6
|
+
const can = (resource, action, options) => {
|
|
7
|
+
if (profile.is_super_admin) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
const scope = scopeByResource.get(resource);
|
|
11
|
+
if (!scope) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (scope === "system") {
|
|
15
|
+
return readGrid(profile.system_permissions, resource, action);
|
|
16
|
+
}
|
|
17
|
+
const companyId = options?.companyId ?? defaultCompanyId;
|
|
18
|
+
if (!companyId) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const membership = profile.memberships.find(
|
|
22
|
+
(m) => m.company_id === companyId
|
|
23
|
+
);
|
|
24
|
+
if (!membership) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return readGrid(membership.permissions, resource, action);
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
can,
|
|
31
|
+
/** Permission map for the active (or specified) company. */
|
|
32
|
+
activePermissions: (companyId) => {
|
|
33
|
+
const id = companyId ?? defaultCompanyId;
|
|
34
|
+
if (!id) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
return profile.memberships.find((m) => m.company_id === id)?.permissions ?? {};
|
|
38
|
+
},
|
|
39
|
+
systemPermissions: () => profile.system_permissions
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function readGrid(map, resource, action) {
|
|
43
|
+
const grid = map[resource];
|
|
44
|
+
if (!grid) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return grid[action];
|
|
48
|
+
}
|
|
49
|
+
function groupResources(registry) {
|
|
50
|
+
const order = [];
|
|
51
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const r of registry) {
|
|
53
|
+
const key = r.group ?? "Sonstige";
|
|
54
|
+
if (!buckets.has(key)) {
|
|
55
|
+
buckets.set(key, []);
|
|
56
|
+
order.push(key);
|
|
57
|
+
}
|
|
58
|
+
buckets.get(key).push(r);
|
|
59
|
+
}
|
|
60
|
+
return order.map((g) => ({ group: g, resources: buckets.get(g) }));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
buildPermissionResolver,
|
|
65
|
+
groupResources
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=chunk-4WTV6J44.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["/**\n * Transport-agnostic client: turns an adopter-supplied\n * `AuthRbacFetcher` into a permission resolver. The React provider\n * wraps this; non-React consumers (Node scripts, edge functions)\n * can use it directly.\n */\n\nimport type {\n Action,\n AuthRbacFetcher,\n PermissionMap,\n ResourceDescriptor,\n ResourceRegistry,\n ResourceScope,\n UserProfile,\n} from \"./types.js\";\n\nexport interface AuthRbacClientOptions {\n fetcher: AuthRbacFetcher;\n /**\n * The host project's full resource list. Required so the resolver\n * can look up a resource's scope without a DB round-trip per call.\n * Re-using the same array the host syncs into the\n * `auth_rbac_resources` table at boot keeps everything in lockstep.\n */\n resources: ResourceRegistry;\n}\n\nexport interface CanOptions {\n /**\n * Override the active company. Omit to use the company the\n * caller has currently activated (the React provider tracks\n * this; for direct client use you must pass it).\n */\n companyId?: string | null;\n}\n\n/**\n * Pure resolver. Given a hydrated profile it answers boolean\n * questions instantly — no I/O. The `resourceMap` is built once at\n * construction so per-call work is two map lookups.\n */\nexport function buildPermissionResolver(\n resources: ResourceRegistry,\n profile: UserProfile,\n defaultCompanyId: string | null,\n) {\n const scopeByResource = new Map<string, ResourceScope>(\n resources.map((r) => [r.resource, r.scope]),\n );\n\n const can = (\n resource: string,\n action: Action,\n options?: CanOptions,\n ): boolean => {\n if (profile.is_super_admin) {\n return true;\n }\n const scope = scopeByResource.get(resource);\n if (!scope) {\n // Unknown resource — fail closed.\n return false;\n }\n if (scope === \"system\") {\n return readGrid(profile.system_permissions, resource, action);\n }\n const companyId = options?.companyId ?? defaultCompanyId;\n if (!companyId) {\n return false;\n }\n const membership = profile.memberships.find(\n (m) => m.company_id === companyId,\n );\n if (!membership) {\n return false;\n }\n return readGrid(membership.permissions, resource, action);\n };\n\n return {\n can,\n /** Permission map for the active (or specified) company. */\n activePermissions: (companyId?: string | null): PermissionMap => {\n const id = companyId ?? defaultCompanyId;\n if (!id) {\n return {};\n }\n return (\n profile.memberships.find((m) => m.company_id === id)?.permissions ?? {}\n );\n },\n systemPermissions: (): PermissionMap => profile.system_permissions,\n };\n}\n\nfunction readGrid(\n map: PermissionMap,\n resource: string,\n action: Action,\n): boolean {\n const grid = map[resource];\n if (!grid) {\n return false;\n }\n return grid[action];\n}\n\n/**\n * Helper: groups a resource registry by `group` for the matrix UI.\n * Returns groups in insertion order with their resources.\n */\nexport function groupResources(\n registry: ResourceRegistry,\n): Array<{ group: string; resources: ResourceDescriptor[] }> {\n const order: string[] = [];\n const buckets = new Map<string, ResourceDescriptor[]>();\n for (const r of registry) {\n const key = r.group ?? \"Sonstige\";\n if (!buckets.has(key)) {\n buckets.set(key, []);\n order.push(key);\n }\n buckets.get(key)!.push(r);\n }\n return order.map((g) => ({ group: g, resources: buckets.get(g)! }));\n}\n\nexport type AuthRbacClient = ReturnType<typeof buildPermissionResolver>;\nexport type { AuthRbacClientOptions as ClientOptions };\n"],"mappings":";AA0CO,SAAS,wBACd,WACA,SACA,kBACA;AACA,QAAM,kBAAkB,IAAI;AAAA,IAC1B,UAAU,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC;AAAA,EAC5C;AAEA,QAAM,MAAM,CACV,UACA,QACA,YACY;AACZ,QAAI,QAAQ,gBAAgB;AAC1B,aAAO;AAAA,IACT;AACA,UAAM,QAAQ,gBAAgB,IAAI,QAAQ;AAC1C,QAAI,CAAC,OAAO;AAEV,aAAO;AAAA,IACT;AACA,QAAI,UAAU,UAAU;AACtB,aAAO,SAAS,QAAQ,oBAAoB,UAAU,MAAM;AAAA,IAC9D;AACA,UAAM,YAAY,SAAS,aAAa;AACxC,QAAI,CAAC,WAAW;AACd,aAAO;AAAA,IACT;AACA,UAAM,aAAa,QAAQ,YAAY;AAAA,MACrC,CAAC,MAAM,EAAE,eAAe;AAAA,IAC1B;AACA,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,IACT;AACA,WAAO,SAAS,WAAW,aAAa,UAAU,MAAM;AAAA,EAC1D;AAEA,SAAO;AAAA,IACL;AAAA;AAAA,IAEA,mBAAmB,CAAC,cAA6C;AAC/D,YAAM,KAAK,aAAa;AACxB,UAAI,CAAC,IAAI;AACP,eAAO,CAAC;AAAA,MACV;AACA,aACE,QAAQ,YAAY,KAAK,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,eAAe,CAAC;AAAA,IAE1E;AAAA,IACA,mBAAmB,MAAqB,QAAQ;AAAA,EAClD;AACF;AAEA,SAAS,SACP,KACA,UACA,QACS;AACT,QAAM,OAAO,IAAI,QAAQ;AACzB,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AACA,SAAO,KAAK,MAAM;AACpB;AAMO,SAAS,eACd,UAC2D;AAC3D,QAAM,QAAkB,CAAC;AACzB,QAAM,UAAU,oBAAI,IAAkC;AACtD,aAAW,KAAK,UAAU;AACxB,UAAM,MAAM,EAAE,SAAS;AACvB,QAAI,CAAC,QAAQ,IAAI,GAAG,GAAG;AACrB,cAAQ,IAAI,KAAK,CAAC,CAAC;AACnB,YAAM,KAAK,GAAG;AAAA,IAChB;AACA,YAAQ,IAAI,GAAG,EAAG,KAAK,CAAC;AAAA,EAC1B;AACA,SAAO,MAAM,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,QAAQ,IAAI,CAAC,EAAG,EAAE;AACpE;","names":[]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/fetchers.ts
|
|
2
|
+
function createSupabaseFetcher(opts) {
|
|
3
|
+
return {
|
|
4
|
+
async fetchProfile() {
|
|
5
|
+
const { data, error } = await opts.supabase.rpc(
|
|
6
|
+
"auth_rbac_user_profile",
|
|
7
|
+
{ p_user_id: opts.userId }
|
|
8
|
+
);
|
|
9
|
+
if (error) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`auth-rbac: failed to load user profile via Supabase RPC: ${error.message}`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return normalizeProfile(data);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function createHttpFetcher(opts) {
|
|
19
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
20
|
+
return {
|
|
21
|
+
async fetchProfile() {
|
|
22
|
+
const res = await fetchImpl(opts.url, opts.init);
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`auth-rbac: profile endpoint ${opts.url} returned ${res.status}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const json = await res.json();
|
|
29
|
+
return normalizeProfile(json);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function normalizeProfile(raw) {
|
|
34
|
+
if (!raw || typeof raw !== "object") {
|
|
35
|
+
throw new Error("auth-rbac: profile payload is not an object");
|
|
36
|
+
}
|
|
37
|
+
const p = raw;
|
|
38
|
+
if (typeof p.user_id !== "string") {
|
|
39
|
+
throw new Error("auth-rbac: profile payload missing user_id");
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
user_id: p.user_id,
|
|
43
|
+
is_super_admin: !!p.is_super_admin,
|
|
44
|
+
system_roles: Array.isArray(p.system_roles) ? p.system_roles : [],
|
|
45
|
+
system_permissions: p.system_permissions && typeof p.system_permissions === "object" ? p.system_permissions : {},
|
|
46
|
+
system_frontend_config: p.system_frontend_config && typeof p.system_frontend_config === "object" ? p.system_frontend_config : {},
|
|
47
|
+
memberships: Array.isArray(p.memberships) ? p.memberships : []
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
createSupabaseFetcher,
|
|
53
|
+
createHttpFetcher
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=chunk-BRCJUCDG.js.map
|