@authhero/react-admin 0.10.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.
Files changed (110) hide show
  1. package/.eslintrc.js +21 -0
  2. package/.vercelignore +4 -0
  3. package/CHANGELOG.md +56 -0
  4. package/LICENSE +21 -0
  5. package/README.md +50 -0
  6. package/index.html +125 -0
  7. package/package.json +61 -0
  8. package/prettier.config.js +1 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/manifest.json +15 -0
  11. package/src/App.spec.tsx +42 -0
  12. package/src/App.tsx +232 -0
  13. package/src/AuthCallback.tsx +138 -0
  14. package/src/Layout.tsx +12 -0
  15. package/src/TenantsApp.tsx +115 -0
  16. package/src/auth0DataProvider.ts +1242 -0
  17. package/src/authProvider.ts +521 -0
  18. package/src/components/CertificateErrorDialog.tsx +116 -0
  19. package/src/components/DomainSelector.tsx +401 -0
  20. package/src/components/TenantAppBar.tsx +83 -0
  21. package/src/components/TenantLayout.tsx +25 -0
  22. package/src/components/TenantsAppBar.tsx +21 -0
  23. package/src/components/TenantsLayout.tsx +28 -0
  24. package/src/components/activity/ActivityDashboard.tsx +381 -0
  25. package/src/components/activity/index.ts +1 -0
  26. package/src/components/branding/BrandingList.tsx +0 -0
  27. package/src/components/branding/BrandingShow.tsx +0 -0
  28. package/src/components/branding/ThemesTab.tsx +286 -0
  29. package/src/components/branding/edit.tsx +149 -0
  30. package/src/components/branding/hooks/useThemesData.ts +123 -0
  31. package/src/components/branding/index.ts +2 -0
  32. package/src/components/branding/list.tsx +12 -0
  33. package/src/components/clients/create.tsx +12 -0
  34. package/src/components/clients/edit.tsx +1285 -0
  35. package/src/components/clients/index.ts +3 -0
  36. package/src/components/clients/list.tsx +37 -0
  37. package/src/components/common/DateAgo.tsx +6 -0
  38. package/src/components/common/JsonOutput.tsx +26 -0
  39. package/src/components/common/index.ts +1 -0
  40. package/src/components/connections/create.tsx +35 -0
  41. package/src/components/connections/edit.tsx +212 -0
  42. package/src/components/connections/index.ts +3 -0
  43. package/src/components/connections/list.tsx +15 -0
  44. package/src/components/custom-domains/create.tsx +26 -0
  45. package/src/components/custom-domains/edit.tsx +101 -0
  46. package/src/components/custom-domains/index.ts +3 -0
  47. package/src/components/custom-domains/list.tsx +16 -0
  48. package/src/components/flows/create.tsx +30 -0
  49. package/src/components/flows/edit.tsx +238 -0
  50. package/src/components/flows/index.ts +3 -0
  51. package/src/components/flows/list.tsx +15 -0
  52. package/src/components/forms/FlowEditor.tsx +1363 -0
  53. package/src/components/forms/NodeEditor.tsx +1119 -0
  54. package/src/components/forms/RichTextEditor.tsx +145 -0
  55. package/src/components/forms/create.tsx +30 -0
  56. package/src/components/forms/edit.tsx +256 -0
  57. package/src/components/forms/index.ts +3 -0
  58. package/src/components/forms/list.tsx +16 -0
  59. package/src/components/hooks/create.tsx +96 -0
  60. package/src/components/hooks/edit.tsx +114 -0
  61. package/src/components/hooks/index.ts +3 -0
  62. package/src/components/hooks/list.tsx +17 -0
  63. package/src/components/listActions/PostListActions.tsx +10 -0
  64. package/src/components/logs/LogIcon.tsx +32 -0
  65. package/src/components/logs/LogShow.tsx +82 -0
  66. package/src/components/logs/LogType.tsx +38 -0
  67. package/src/components/logs/index.ts +4 -0
  68. package/src/components/logs/list.tsx +41 -0
  69. package/src/components/organizations/create.tsx +13 -0
  70. package/src/components/organizations/edit.tsx +682 -0
  71. package/src/components/organizations/index.ts +3 -0
  72. package/src/components/organizations/list.tsx +21 -0
  73. package/src/components/resource-servers/create.tsx +87 -0
  74. package/src/components/resource-servers/edit.tsx +121 -0
  75. package/src/components/resource-servers/index.ts +3 -0
  76. package/src/components/resource-servers/list.tsx +47 -0
  77. package/src/components/roles/create.tsx +12 -0
  78. package/src/components/roles/edit.tsx +426 -0
  79. package/src/components/roles/index.ts +3 -0
  80. package/src/components/roles/list.tsx +24 -0
  81. package/src/components/sessions/edit.tsx +101 -0
  82. package/src/components/sessions/index.ts +3 -0
  83. package/src/components/sessions/list.tsx +20 -0
  84. package/src/components/sessions/show.tsx +113 -0
  85. package/src/components/settings/edit.tsx +236 -0
  86. package/src/components/settings/index.ts +2 -0
  87. package/src/components/settings/list.tsx +14 -0
  88. package/src/components/tenants/create.tsx +20 -0
  89. package/src/components/tenants/edit.tsx +54 -0
  90. package/src/components/tenants/index.ts +2 -0
  91. package/src/components/tenants/list.tsx +67 -0
  92. package/src/components/themes/edit.tsx +200 -0
  93. package/src/components/themes/index.ts +2 -0
  94. package/src/components/themes/list.tsx +12 -0
  95. package/src/components/users/create.tsx +144 -0
  96. package/src/components/users/edit.tsx +1711 -0
  97. package/src/components/users/index.ts +3 -0
  98. package/src/components/users/list.tsx +35 -0
  99. package/src/data.json +121 -0
  100. package/src/dataProvider.ts +97 -0
  101. package/src/index.tsx +106 -0
  102. package/src/lib/logs.ts +21 -0
  103. package/src/types/reactflow.d.ts +86 -0
  104. package/src/utils/domainUtils.ts +169 -0
  105. package/src/utils/tokenUtils.ts +75 -0
  106. package/src/vite-env.d.ts +1 -0
  107. package/tsconfig.json +37 -0
  108. package/tsconfig.node.json +10 -0
  109. package/vercel.json +17 -0
  110. package/vite.config.ts +30 -0
@@ -0,0 +1,1242 @@
1
+ import { fetchUtils, DataProvider } from "ra-core";
2
+ import { UpdateParams } from "react-admin";
3
+ import { createManagementClient } from "./authProvider";
4
+ import { ManagementClient } from "auth0";
5
+
6
+ // Add this at the top of the file with other imports
7
+ function stringify(obj: Record<string, any>): string {
8
+ return Object.entries(obj)
9
+ .filter(([_, value]) => value !== undefined)
10
+ .map(
11
+ ([key, value]) =>
12
+ `${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
13
+ )
14
+ .join("&");
15
+ }
16
+
17
+ function removeExtraFields(params: UpdateParams) {
18
+ // delete params.data?.id; // this is required for patch... but not for put?
19
+ delete params.data?.tenant_id;
20
+ delete params.data?.updated_at;
21
+ delete params.data?.created_at;
22
+ delete params.data?.identities;
23
+
24
+ // hmmmmm, this is an issue we have here with mismatching structure?
25
+ // seems like we need to modify our endpoints to accept connections.
26
+ // TBD with Markus
27
+ delete params.data?.connections;
28
+
29
+ // actually Auth0 does not require this for patching. seems dangerous not to rely on an auto-id
30
+ // as may get rejected for having the same id
31
+ delete params.data?.id;
32
+ // for user we don't want to include this
33
+ delete params.data?.user_id;
34
+
35
+ // extra user fields
36
+ delete params.data?.last_login;
37
+ delete params.data?.provider;
38
+
39
+ // Remove empty properties
40
+ Object.keys(params.data).forEach((key) => {
41
+ if (params.data[key] === undefined) {
42
+ delete params.data[key];
43
+ }
44
+ });
45
+
46
+ return params;
47
+ }
48
+
49
+ function parseResource(resourcePath: string) {
50
+ return resourcePath.split("/").pop() || resourcePath;
51
+ }
52
+
53
+ // Helper to normalize SDK response format variations
54
+ function normalizeSDKResponse(
55
+ result: any,
56
+ resourceKey: string,
57
+ ): { data: any[]; total: number } {
58
+ const response = (result as any).response || {};
59
+
60
+ // Handle direct array format
61
+ if (Array.isArray(response)) {
62
+ return { data: response, total: response.length };
63
+ }
64
+
65
+ // Handle SDK wrapper format with resource key
66
+ if (response[resourceKey]) {
67
+ return {
68
+ data: response[resourceKey],
69
+ total: response.total || response.length || response[resourceKey].length,
70
+ };
71
+ }
72
+
73
+ // Handle result itself being the array
74
+ if (Array.isArray(result)) {
75
+ return { data: result, total: result.length };
76
+ }
77
+
78
+ // Fallback to empty array
79
+ return { data: [], total: 0 };
80
+ }
81
+
82
+ // Helper to create headers with tenant ID
83
+ function createHeaders(tenantId?: string): Headers {
84
+ const headers = new Headers();
85
+ if (tenantId) {
86
+ headers.set("tenant-id", tenantId);
87
+ }
88
+ return headers;
89
+ }
90
+
91
+ // Helper to handle singleton resource fetching
92
+ async function fetchSingleton(
93
+ resource: string,
94
+ fetcher: () => Promise<any>,
95
+ ): Promise<{ data: any[]; total: number }> {
96
+ try {
97
+ const result = await fetcher();
98
+ const data = (result as any).response || result;
99
+ // Spread data first, then override id to ensure it matches the resource name
100
+ return {
101
+ data: [{ ...data, id: resource }],
102
+ total: 1,
103
+ };
104
+ } catch (error) {
105
+ console.error(`Error in getList for ${resource}:`, error);
106
+ return {
107
+ data: [{ id: resource }],
108
+ total: 1,
109
+ };
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Maps react-admin queries to the auth0 management api
115
+ * Uses HTTP client for all API calls with custom headers for tenant isolation
116
+ */
117
+ export default (
118
+ apiUrl: string,
119
+ httpClient = fetchUtils.fetchJson,
120
+ tenantId?: string,
121
+ domain?: string,
122
+ ): DataProvider => {
123
+ // Get or create management client for SDK calls
124
+ let managementClientPromise: Promise<ManagementClient> | null = null;
125
+ const getManagementClient = async () => {
126
+ if (!managementClientPromise) {
127
+ // Extract API domain from apiUrl
128
+ const apiDomain = apiUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
129
+ // Pass both API domain and OAuth domain for authentication
130
+ managementClientPromise = createManagementClient(
131
+ apiDomain,
132
+ tenantId,
133
+ domain,
134
+ );
135
+ }
136
+ return managementClientPromise;
137
+ };
138
+
139
+ return {
140
+ getList: async (resourcePath, params) => {
141
+ const resource = parseResource(resourcePath);
142
+ const { page = 1, perPage } = params.pagination || {};
143
+ const { field, order } = params.sort || {};
144
+ const managementClient = await getManagementClient();
145
+
146
+ // SDK resource handlers configuration
147
+ const sdkHandlers: Record<
148
+ string,
149
+ {
150
+ fetch: (client: ManagementClient) => Promise<any>;
151
+ resourceKey: string;
152
+ idKey: string;
153
+ }
154
+ > = {
155
+ users: {
156
+ fetch: (client) =>
157
+ client.users.list({
158
+ page: page - 1,
159
+ per_page: perPage,
160
+ sort:
161
+ field && order
162
+ ? `${field}:${order === "DESC" ? "-1" : "1"}`
163
+ : undefined,
164
+ q: params.filter?.q,
165
+ include_totals: true,
166
+ }),
167
+ resourceKey: "users",
168
+ idKey: "user_id",
169
+ },
170
+ clients: {
171
+ fetch: (client) =>
172
+ client.clients.list({
173
+ page: page - 1,
174
+ per_page: perPage,
175
+ include_totals: true,
176
+ }),
177
+ resourceKey: "clients",
178
+ idKey: "client_id",
179
+ },
180
+ connections: {
181
+ fetch: (client) => client.connections.list(),
182
+ resourceKey: "connections",
183
+ idKey: "id",
184
+ },
185
+ roles: {
186
+ fetch: (client) =>
187
+ client.roles.list({
188
+ page: page - 1,
189
+ per_page: perPage,
190
+ }),
191
+ resourceKey: "roles",
192
+ idKey: "id",
193
+ },
194
+ "resource-servers": {
195
+ fetch: (client) => client.resourceServers.list(),
196
+ resourceKey: "resource_servers",
197
+ idKey: "id",
198
+ },
199
+ organizations: {
200
+ fetch: (client) =>
201
+ client.organizations.list({
202
+ from: String((page - 1) * (perPage || 10)),
203
+ take: perPage || 10,
204
+ }),
205
+ resourceKey: "organizations",
206
+ idKey: "id",
207
+ },
208
+ logs: {
209
+ fetch: (client) => {
210
+ // Build the query string, combining search query and IP filter
211
+ let query = params.filter?.q || "";
212
+ if (params.filter?.ip) {
213
+ const ipQuery = `ip:${params.filter.ip}`;
214
+ query = query ? `${query} AND ${ipQuery}` : ipQuery;
215
+ }
216
+
217
+ return client.logs.list({
218
+ page: page - 1,
219
+ per_page: perPage,
220
+ q: query || undefined,
221
+ sort:
222
+ field && order
223
+ ? `${field}:${order === "DESC" ? "-1" : "1"}`
224
+ : undefined,
225
+ include_totals: true,
226
+ });
227
+ },
228
+ resourceKey: "logs",
229
+ idKey: "log_id",
230
+ },
231
+ rules: {
232
+ fetch: (client) => client.rules.list(),
233
+ resourceKey: "rules",
234
+ idKey: "id",
235
+ },
236
+ "client-grants": {
237
+ fetch: (client) =>
238
+ client.clientGrants.list({
239
+ page: page - 1,
240
+ per_page: perPage,
241
+ ...(params.filter?.client_id && {
242
+ client_id: params.filter.client_id,
243
+ }),
244
+ }),
245
+ resourceKey: "client_grants",
246
+ idKey: "id",
247
+ },
248
+ forms: {
249
+ fetch: (client) => client.forms.list(),
250
+ resourceKey: "forms",
251
+ idKey: "id",
252
+ },
253
+ "custom-domains": {
254
+ fetch: (client: any) => client.customDomains.list(),
255
+ resourceKey: "custom_domains",
256
+ idKey: "custom_domain_id",
257
+ },
258
+ };
259
+
260
+ // Handle SDK resources
261
+ const handler = sdkHandlers[resource];
262
+ if (handler) {
263
+ const result = await handler.fetch(managementClient);
264
+ const { data, total } = normalizeSDKResponse(
265
+ result,
266
+ handler.resourceKey,
267
+ );
268
+ return {
269
+ data: data.map((item: any) => ({
270
+ id: item[handler.idKey] || item.id,
271
+ ...item,
272
+ })),
273
+ total,
274
+ };
275
+ }
276
+
277
+ // Handle singleton resources
278
+ if (resource === "branding") {
279
+ return fetchSingleton(resource, () => managementClient.branding.get());
280
+ }
281
+
282
+ if (resource === "settings") {
283
+ return fetchSingleton(resource, () =>
284
+ managementClient.tenants.settings.get(),
285
+ );
286
+ }
287
+
288
+ // Handle stats/daily endpoint
289
+ if (resourcePath === "stats/daily") {
290
+ const headers = createHeaders(tenantId);
291
+ const query: any = {};
292
+ if (params.filter?.from) query.from = params.filter.from;
293
+ if (params.filter?.to) query.to = params.filter.to;
294
+
295
+ const url = `${apiUrl}/api/v2/stats/daily${Object.keys(query).length ? `?${stringify(query)}` : ""}`;
296
+ try {
297
+ const res = await httpClient(url, { headers });
298
+ // Stats endpoint returns an array directly
299
+ const data = Array.isArray(res.json) ? res.json : [];
300
+ return {
301
+ data: data.map((item: any, index: number) => ({
302
+ id: item.date || index,
303
+ ...item,
304
+ })),
305
+ total: data.length,
306
+ };
307
+ } catch (error) {
308
+ console.error("Error fetching daily stats:", error);
309
+ return { data: [], total: 0 };
310
+ }
311
+ }
312
+
313
+ // Use HTTP client for all other list operations
314
+ const headers = createHeaders(tenantId);
315
+
316
+ const query: any = {
317
+ include_totals: true,
318
+ page: page - 1,
319
+ per_page: perPage,
320
+ sort:
321
+ field && order
322
+ ? `${field}:${order === "DESC" ? "-1" : "1"}`
323
+ : undefined,
324
+ q: params.filter?.q,
325
+ };
326
+
327
+ const url = `${apiUrl}/api/v2/${resourcePath}?${stringify(query)}`;
328
+
329
+ try {
330
+ const res = await httpClient(url, { headers });
331
+
332
+ // Handle case where API returns an array directly (like custom_domains)
333
+ if (Array.isArray(res.json)) {
334
+ return {
335
+ data: res.json.map((item) => ({
336
+ id: item.custom_domain_id || item.id,
337
+ ...item,
338
+ })),
339
+ total: res.json.length,
340
+ };
341
+ }
342
+
343
+ // Handle standard case where API returns an object with a property named after the resource
344
+ return {
345
+ data:
346
+ res.json[resource]?.map((item: any) => ({
347
+ id: item.custom_domain_id || item.id,
348
+ ...item,
349
+ })) || [],
350
+ total: res.json.total || res.json.length || 0,
351
+ };
352
+ } catch (error) {
353
+ console.error("Error in getList:", error);
354
+ throw error;
355
+ }
356
+ },
357
+
358
+ getOne: async (resource, params) => {
359
+ const managementClient = await getManagementClient();
360
+
361
+ // SDK resource handlers for getOne
362
+ const sdkGetHandlers: Record<
363
+ string,
364
+ { fetch: (id: string) => Promise<any>; idKey: string }
365
+ > = {
366
+ users: {
367
+ fetch: (id) => managementClient.users.get(id),
368
+ idKey: "user_id",
369
+ },
370
+ clients: {
371
+ fetch: (id) => managementClient.clients.get(id),
372
+ idKey: "client_id",
373
+ },
374
+ "custom-domains": {
375
+ fetch: (id) => (managementClient as any).customDomains.get(id),
376
+ idKey: "custom_domain_id",
377
+ },
378
+ };
379
+
380
+ const handler = sdkGetHandlers[resource];
381
+ if (handler) {
382
+ const result = await handler.fetch(params.id as string);
383
+ return {
384
+ data: {
385
+ id: result[handler.idKey] || result.id,
386
+ ...result,
387
+ },
388
+ };
389
+ }
390
+
391
+ // Handle singleton resources
392
+ if (resource === "branding") {
393
+ const result = await managementClient.branding.get();
394
+ return {
395
+ data: {
396
+ ...result,
397
+ id: resource,
398
+ },
399
+ };
400
+ }
401
+
402
+ if (resource === "settings") {
403
+ const result = await managementClient.tenants.settings.get();
404
+ return {
405
+ data: {
406
+ ...result,
407
+ id: resource,
408
+ },
409
+ };
410
+ }
411
+
412
+ // Handle stats/active-users endpoint
413
+ if (resource === "stats/active-users") {
414
+ const headers = createHeaders(tenantId);
415
+ try {
416
+ const res = await httpClient(`${apiUrl}/api/v2/stats/active-users`, {
417
+ headers,
418
+ });
419
+ // API returns a number directly
420
+ const count = typeof res.json === "number" ? res.json : 0;
421
+ return {
422
+ data: {
423
+ id: "count",
424
+ count,
425
+ },
426
+ };
427
+ } catch (error) {
428
+ console.error("Error fetching active users:", error);
429
+ return {
430
+ data: {
431
+ id: "count",
432
+ count: 0,
433
+ },
434
+ };
435
+ }
436
+ }
437
+
438
+ // Special handling for tenants - fetch from list and find by ID
439
+ if (resource === "tenants") {
440
+ const headers = createHeaders(tenantId);
441
+
442
+ try {
443
+ const res = await httpClient(`${apiUrl}/api/v2/tenants`, {
444
+ headers,
445
+ });
446
+
447
+ const tenants = res.json.tenants || [];
448
+ const tenant = tenants.find(
449
+ (t: any) => t.id === params.id || t.tenant_id === params.id,
450
+ );
451
+
452
+ if (tenant) {
453
+ return {
454
+ data: {
455
+ id: tenant.id || tenant.tenant_id,
456
+ ...tenant,
457
+ },
458
+ };
459
+ }
460
+
461
+ return {
462
+ data: {
463
+ id: params.id,
464
+ name: params.id,
465
+ },
466
+ };
467
+ } catch (error) {
468
+ console.warn(`Could not fetch tenant ${params.id}:`, error);
469
+ return {
470
+ data: {
471
+ id: params.id,
472
+ name: params.id,
473
+ },
474
+ };
475
+ }
476
+ }
477
+
478
+ // HTTP for other resources
479
+ const headers = createHeaders(tenantId);
480
+ return httpClient(`${apiUrl}/api/v2/${resource}/${params.id}`, {
481
+ headers,
482
+ }).then(({ json }) => ({
483
+ data: {
484
+ id: json.id,
485
+ ...json,
486
+ },
487
+ }));
488
+ },
489
+
490
+ getMany: (resourcePath, params) => {
491
+ const query = `id:(${params.ids.join(" ")})`;
492
+
493
+ const url = `${apiUrl}/api/v2/${resourcePath}?q=${query}`;
494
+ return httpClient(url).then(({ json }) => ({
495
+ data: {
496
+ id: json.id,
497
+ ...json,
498
+ },
499
+ }));
500
+ },
501
+
502
+ getManyReference: async (resource, params) => {
503
+ const { page, perPage } = params.pagination;
504
+ const { field, order } = params.sort;
505
+ const managementClient = await getManagementClient();
506
+
507
+ // Build common query params for pagination
508
+ const buildPaginationParams = () => ({
509
+ page: page - 1,
510
+ per_page: perPage,
511
+ });
512
+
513
+ // Sessions nested under users
514
+ if (resource === "sessions") {
515
+ // Sessions are user-specific, use HTTP
516
+ const headers = createHeaders(tenantId);
517
+ const res = await httpClient(
518
+ `${apiUrl}/api/v2/users/${params.id}/sessions?${stringify({
519
+ include_totals: true,
520
+ ...buildPaginationParams(),
521
+ sort: `${field}:${order === "DESC" ? "-1" : "1"}`,
522
+ })}`,
523
+ { headers },
524
+ );
525
+ return {
526
+ data: res.json.sessions.map((item: any) => ({
527
+ id: item.id,
528
+ ...item,
529
+ })),
530
+ total: res.json.length || 0,
531
+ };
532
+ }
533
+
534
+ // Permissions nested under users
535
+ if (resource === "permissions" && params.target === "user_id") {
536
+ const result = await managementClient.users.permissions.list(
537
+ params.id as string,
538
+ buildPaginationParams(),
539
+ );
540
+ const permissions = (result as any).response || result;
541
+ const permissionsArray = Array.isArray(permissions)
542
+ ? permissions
543
+ : permissions.permissions || [];
544
+ return {
545
+ data: permissionsArray.map((item: any) => ({
546
+ id: `${item.resource_server_identifier}:${item.permission_name}`,
547
+ ...item,
548
+ })),
549
+ total: permissionsArray.length || 0,
550
+ };
551
+ }
552
+
553
+ // Permissions nested under roles
554
+ if (resource === "permissions" && params.target === "role_id") {
555
+ const result = await managementClient.roles.permissions.list(
556
+ params.id as string,
557
+ buildPaginationParams(),
558
+ );
559
+ const permissions = (result as any).response || result;
560
+ const permissionsArray = Array.isArray(permissions)
561
+ ? permissions
562
+ : permissions.permissions || [];
563
+ return {
564
+ data: permissionsArray.map((item: any) => ({
565
+ id: `${item.resource_server_identifier}:${item.permission_name}`,
566
+ ...item,
567
+ })),
568
+ total: permissionsArray.length || 0,
569
+ };
570
+ }
571
+
572
+ // Roles nested under users
573
+ if (resource === "roles" && params.target === "user_id") {
574
+ const result = await managementClient.users.roles.list(
575
+ params.id as string,
576
+ );
577
+ const roles = (result as any).response || result;
578
+ const rolesArray = Array.isArray(roles) ? roles : [];
579
+ return {
580
+ data: rolesArray.map((item: any) => ({
581
+ id: item.id,
582
+ ...item,
583
+ })),
584
+ total: rolesArray.length,
585
+ };
586
+ }
587
+
588
+ // Organization members
589
+ if (
590
+ resource === "organization-members" &&
591
+ params.target === "organization_id"
592
+ ) {
593
+ const result = await managementClient.organizations.members.list(
594
+ params.id as string,
595
+ {
596
+ from: String((page - 1) * perPage),
597
+ take: perPage,
598
+ },
599
+ );
600
+ const response = (result as any).response || result;
601
+ const membersData = Array.isArray(response)
602
+ ? response
603
+ : response.members || [];
604
+ const total = response.total || membersData.length;
605
+
606
+ return {
607
+ data: membersData.map((item: any) => ({
608
+ id: `${params.id}_${item.user_id}`,
609
+ organization_id: params.id,
610
+ ...item,
611
+ })),
612
+ total,
613
+ };
614
+ }
615
+
616
+ // Organization invitations
617
+ if (
618
+ resource === "organization-invitations" &&
619
+ params.target === "organization_id"
620
+ ) {
621
+ const result = await managementClient.organizations.invitations.list(
622
+ params.id as string,
623
+ {
624
+ page: page - 1,
625
+ per_page: perPage,
626
+ },
627
+ );
628
+ const response = (result as any).response || result;
629
+ const invitationsData = Array.isArray(response)
630
+ ? response
631
+ : response.invitations || [];
632
+ const total = response.total || invitationsData.length;
633
+
634
+ return {
635
+ data: invitationsData.map((item: any) => ({
636
+ id: item.id,
637
+ organization_id: params.id,
638
+ ...item,
639
+ })),
640
+ total,
641
+ };
642
+ }
643
+
644
+ // User organizations
645
+ if (resource === "user-organizations" && params.target === "user_id") {
646
+ const result = await managementClient.users.organizations.list(
647
+ params.id as string,
648
+ {
649
+ page: page - 1,
650
+ per_page: perPage,
651
+ },
652
+ );
653
+ const response = (result as any).response || result;
654
+
655
+ let organizationsData: any[];
656
+ let total: number;
657
+
658
+ if (Array.isArray(response)) {
659
+ organizationsData = response;
660
+ total = response.length;
661
+ } else if (response.organizations) {
662
+ organizationsData = response.organizations;
663
+ total = response.total || organizationsData.length;
664
+ } else {
665
+ organizationsData = [];
666
+ total = 0;
667
+ }
668
+
669
+ return {
670
+ data: organizationsData.map((org: any) => ({
671
+ id: org.id || org.organization_id,
672
+ user_id: params.id,
673
+ name: org.name,
674
+ display_name: org.display_name,
675
+ branding: org.branding,
676
+ metadata: org.metadata || {},
677
+ token_quota: org.token_quota,
678
+ created_at: org.created_at,
679
+ updated_at: org.updated_at,
680
+ ...org,
681
+ })),
682
+ total,
683
+ };
684
+ }
685
+
686
+ // Client grants filtered by client_id
687
+ if (resource === "client-grants" && params.target === "client_id") {
688
+ const result = await managementClient.clientGrants.list({
689
+ ...buildPaginationParams(),
690
+ client_id: params.id as string,
691
+ });
692
+ const response = (result as any).response || result;
693
+ const grantsData = Array.isArray(response)
694
+ ? response
695
+ : response.client_grants || [];
696
+ const total = response.total || grantsData.length;
697
+
698
+ return {
699
+ data: grantsData.map((item: any) => ({
700
+ id: item.id,
701
+ ...item,
702
+ })),
703
+ total,
704
+ };
705
+ }
706
+
707
+ // Logs filtered by user_id
708
+ if (resource === "logs" && params.target === "user_id") {
709
+ const result = await managementClient.logs.list({
710
+ page: page - 1,
711
+ per_page: perPage,
712
+ q: `user_id:${params.id}`,
713
+ sort:
714
+ field && order
715
+ ? `${field}:${order === "DESC" ? "-1" : "1"}`
716
+ : undefined,
717
+ include_totals: true,
718
+ });
719
+
720
+ const normalized = normalizeSDKResponse(result, "logs");
721
+ return {
722
+ data: normalized.data.map((log: any) => ({ id: log.log_id, ...log })),
723
+ total: normalized.total,
724
+ };
725
+ }
726
+
727
+ // Default implementation for other resources - use HTTP fallback
728
+ const headers = createHeaders(tenantId);
729
+ const res = await httpClient(
730
+ `${apiUrl}/api/v2/${resource}?${stringify({
731
+ include_totals: true,
732
+ ...buildPaginationParams(),
733
+ sort: `${field}:${order === "DESC" ? "-1" : "1"}`,
734
+ q: `user_id:${params.id}`,
735
+ })}`,
736
+ { headers },
737
+ );
738
+
739
+ return {
740
+ data: res.json[resource].map((item: any) => ({
741
+ id: item.id,
742
+ ...item,
743
+ })),
744
+ total: res.json.total,
745
+ };
746
+ },
747
+
748
+ update: async (resource, params) => {
749
+ const cleanParams = removeExtraFields(params);
750
+ const managementClient = await getManagementClient();
751
+ const headers = createHeaders(tenantId);
752
+
753
+ // Handle singleton resources
754
+ if (resource === "settings") {
755
+ const result = await managementClient.tenants.settings.update(
756
+ cleanParams.data,
757
+ );
758
+ return {
759
+ data: { ...result, id: resource },
760
+ };
761
+ }
762
+
763
+ // Special handling for branding to update theme data separately
764
+ if (resource === "branding") {
765
+ // Update branding
766
+ const brandingResult = await managementClient.branding.update(
767
+ cleanParams.data,
768
+ );
769
+
770
+ // Update themes if provided
771
+ const result: any = {
772
+ id: resource,
773
+ ...brandingResult,
774
+ };
775
+
776
+ if (cleanParams.data.themes) {
777
+ const themeUpdateResult = await (
778
+ managementClient.branding.themes as any
779
+ ).default.patch(cleanParams.data.themes);
780
+ result.themes =
781
+ (themeUpdateResult as any).response || themeUpdateResult;
782
+ }
783
+
784
+ return { data: result };
785
+ }
786
+
787
+ // SDK-handled resources
788
+ if (resource === "users") {
789
+ const result = await managementClient.users.update(
790
+ params.id as string,
791
+ cleanParams.data,
792
+ );
793
+ return {
794
+ data: {
795
+ id: result.user_id,
796
+ ...result,
797
+ },
798
+ };
799
+ }
800
+
801
+ if (resource === "clients") {
802
+ const result = await managementClient.clients.update(
803
+ params.id as string,
804
+ cleanParams.data,
805
+ );
806
+ return {
807
+ data: {
808
+ id: result.client_id,
809
+ ...result,
810
+ },
811
+ };
812
+ }
813
+
814
+ if (resource === "connections") {
815
+ const result = await managementClient.connections.update(
816
+ params.id as string,
817
+ cleanParams.data,
818
+ );
819
+ return {
820
+ data: {
821
+ id: result.id,
822
+ ...result,
823
+ },
824
+ };
825
+ }
826
+
827
+ if (resource === "roles") {
828
+ const result = await managementClient.roles.update(
829
+ params.id as string,
830
+ cleanParams.data,
831
+ );
832
+ return {
833
+ data: {
834
+ id: result.id,
835
+ ...result,
836
+ },
837
+ };
838
+ }
839
+
840
+ if (resource === "resource-servers") {
841
+ const result = await managementClient.resourceServers.update(
842
+ params.id as string,
843
+ cleanParams.data,
844
+ );
845
+ return {
846
+ data: {
847
+ id: result.id,
848
+ ...result,
849
+ },
850
+ };
851
+ }
852
+
853
+ if (resource === "organizations") {
854
+ const result = await managementClient.organizations.update(
855
+ params.id as string,
856
+ cleanParams.data,
857
+ );
858
+ return {
859
+ data: {
860
+ id: result.id,
861
+ ...result,
862
+ },
863
+ };
864
+ }
865
+
866
+ if (resource === "rules") {
867
+ const result = await managementClient.rules.update(
868
+ params.id as string,
869
+ cleanParams.data,
870
+ );
871
+ return {
872
+ data: {
873
+ id: result.id,
874
+ ...result,
875
+ },
876
+ };
877
+ }
878
+
879
+ if (resource === "client-grants") {
880
+ const result = await managementClient.clientGrants.update(
881
+ params.id as string,
882
+ cleanParams.data,
883
+ );
884
+ return {
885
+ data: {
886
+ id: result.id,
887
+ ...result,
888
+ },
889
+ };
890
+ }
891
+
892
+ if (resource === "custom-domains") {
893
+ const result = await (managementClient as any).customDomains.update(
894
+ params.id as string,
895
+ cleanParams.data,
896
+ );
897
+ return {
898
+ data: {
899
+ id: result.custom_domain_id || result.id,
900
+ ...result,
901
+ },
902
+ };
903
+ }
904
+
905
+ // HTTP fallback for other resources
906
+ return httpClient(`${apiUrl}/api/v2/${resource}/${params.id}`, {
907
+ headers,
908
+ method: "PATCH",
909
+ body: JSON.stringify(cleanParams.data),
910
+ }).then(({ json }) => {
911
+ if (!json.id) {
912
+ json.id = json[`${resource}_id`];
913
+ delete json[`${resource}_id`];
914
+ }
915
+ return { data: json };
916
+ });
917
+ },
918
+
919
+ updateMany: () => Promise.reject("not supporting updateMany"),
920
+
921
+ create: async (resource, params) => {
922
+ const headers = new Headers({ "content-type": "application/json" });
923
+ if (tenantId) headers.set("tenant-id", tenantId);
924
+ const managementClient = await getManagementClient();
925
+
926
+ // Helper for POST requests
927
+ const post = async (endpoint: string, body: any) =>
928
+ httpClient(`${apiUrl}/api/v2/${endpoint}`, {
929
+ method: "POST",
930
+ body: JSON.stringify(body),
931
+ headers,
932
+ });
933
+
934
+ // SDK resource handlers for create
935
+ const sdkCreateHandlers: Record<
936
+ string,
937
+ { create: (data: any) => Promise<any>; idKey: string }
938
+ > = {
939
+ users: {
940
+ create: (data) => managementClient.users.create(data),
941
+ idKey: "user_id",
942
+ },
943
+ clients: {
944
+ create: (data) => managementClient.clients.create(data),
945
+ idKey: "client_id",
946
+ },
947
+ connections: {
948
+ create: (data) => managementClient.connections.create(data),
949
+ idKey: "id",
950
+ },
951
+ roles: {
952
+ create: (data) => managementClient.roles.create(data),
953
+ idKey: "id",
954
+ },
955
+ "resource-servers": {
956
+ create: (data) => managementClient.resourceServers.create(data),
957
+ idKey: "id",
958
+ },
959
+ organizations: {
960
+ create: (data) => managementClient.organizations.create(data),
961
+ idKey: "id",
962
+ },
963
+ rules: {
964
+ create: (data) => managementClient.rules.create(data),
965
+ idKey: "id",
966
+ },
967
+ "client-grants": {
968
+ create: (data) => managementClient.clientGrants.create(data),
969
+ idKey: "id",
970
+ },
971
+ "custom-domains": {
972
+ create: (data) =>
973
+ (managementClient as any).customDomains.create(data),
974
+ idKey: "custom_domain_id",
975
+ },
976
+ };
977
+
978
+ const handler = sdkCreateHandlers[resource];
979
+ if (handler) {
980
+ const result = await handler.create(params.data);
981
+ return {
982
+ data: {
983
+ id: result[handler.idKey] || result.id,
984
+ ...result,
985
+ },
986
+ };
987
+ }
988
+
989
+ // Organization invitations
990
+ if (resource === "organization-invitations") {
991
+ const { organization_id, ...inviteData } = params.data;
992
+ const res = await post(
993
+ `organizations/${organization_id}/invitations`,
994
+ inviteData,
995
+ );
996
+ return {
997
+ data: {
998
+ id: res.json.id,
999
+ organization_id,
1000
+ ...res.json,
1001
+ },
1002
+ };
1003
+ }
1004
+
1005
+ // Organization members
1006
+ if (resource === "organization-members") {
1007
+ const { organization_id, user_id, user_ids } = params.data;
1008
+ const usersToAdd = user_ids || [user_id];
1009
+ const res = await post(`organizations/${organization_id}/members`, {
1010
+ members: usersToAdd,
1011
+ });
1012
+ return {
1013
+ data: {
1014
+ id: `${organization_id}_${usersToAdd.join("_")}`,
1015
+ organization_id,
1016
+ user_id: user_id || usersToAdd[0],
1017
+ members: usersToAdd,
1018
+ ...res.json,
1019
+ },
1020
+ };
1021
+ }
1022
+
1023
+ // User organizations (same endpoint as org members)
1024
+ if (resource === "user-organizations") {
1025
+ const { organization_id, user_id, user_ids } = params.data;
1026
+ const usersToAdd = user_ids || [user_id];
1027
+ const res = await post(`organizations/${organization_id}/members`, {
1028
+ members: usersToAdd,
1029
+ });
1030
+ return {
1031
+ data: {
1032
+ id: organization_id,
1033
+ user_id: user_id || usersToAdd[0],
1034
+ members: usersToAdd,
1035
+ organization_id,
1036
+ ...res.json,
1037
+ },
1038
+ };
1039
+ }
1040
+
1041
+ // User roles assignment
1042
+ const userRolesMatch = resource.match(/^users\/([^/]+)\/roles$/);
1043
+ if (userRolesMatch) {
1044
+ const res = await post(resource, params.data);
1045
+ const userId = userRolesMatch[1];
1046
+ const roleIds = params.data?.roles || [];
1047
+ return {
1048
+ data: {
1049
+ id: `${userId}_${roleIds.join("_")}`,
1050
+ user_id: userId,
1051
+ roles: roleIds,
1052
+ ...res.json,
1053
+ },
1054
+ };
1055
+ }
1056
+
1057
+ // Default create (for endpoints not in SDK)
1058
+ const res = await post(resource, params.data);
1059
+ return {
1060
+ data: {
1061
+ ...res.json,
1062
+ id: res.json.id,
1063
+ },
1064
+ };
1065
+ },
1066
+
1067
+ delete: async (resource, params) => {
1068
+ const managementClient = await getManagementClient();
1069
+ const headers = new Headers({ "content-type": "application/json" });
1070
+ if (tenantId) headers.set("tenant-id", tenantId);
1071
+
1072
+ // Helper for DELETE requests
1073
+ const del = async (endpoint: string, body?: any) =>
1074
+ httpClient(`${apiUrl}/api/v2/${endpoint}`, {
1075
+ method: "DELETE",
1076
+ headers,
1077
+ body: body ? JSON.stringify(body) : undefined,
1078
+ });
1079
+
1080
+ // Organization invitations
1081
+ if (resource === "organization-invitations") {
1082
+ const invitation_id = params.id;
1083
+ const organization_id = params.previousData?.organization_id;
1084
+
1085
+ if (!organization_id || !invitation_id) {
1086
+ throw new Error(
1087
+ "Missing organization_id or invitation_id for invitation deletion",
1088
+ );
1089
+ }
1090
+
1091
+ const res = await del(
1092
+ `organizations/${organization_id}/invitations/${invitation_id}`,
1093
+ );
1094
+ return { data: res.json || { id: invitation_id } };
1095
+ }
1096
+
1097
+ // Organization members
1098
+ if (resource === "organization-members") {
1099
+ let organization_id, user_ids;
1100
+
1101
+ if (params.previousData?.members) {
1102
+ organization_id = params.id;
1103
+ user_ids = params.previousData.members;
1104
+ } else if (typeof params.id === "string" && params.id.includes("_")) {
1105
+ [organization_id, ...user_ids] = params.id.split("_");
1106
+ } else if (params.previousData) {
1107
+ organization_id = params.previousData.organization_id || params.id;
1108
+ user_ids = params.previousData.user_ids || [
1109
+ params.previousData.user_id,
1110
+ ];
1111
+ }
1112
+
1113
+ if (!organization_id || !user_ids || user_ids.length === 0) {
1114
+ throw new Error(
1115
+ "Missing organization_id or user_id(s) for organization member deletion",
1116
+ );
1117
+ }
1118
+
1119
+ const res = await del(`organizations/${organization_id}/members`, {
1120
+ members: user_ids,
1121
+ });
1122
+ return { data: res.json };
1123
+ }
1124
+
1125
+ // User organizations
1126
+ if (resource === "user-organizations") {
1127
+ let organization_id, user_id;
1128
+
1129
+ if (params.previousData) {
1130
+ user_id = params.previousData.user_id;
1131
+ organization_id = params.id;
1132
+ } else if (typeof params.id === "string" && params.id.includes("_")) {
1133
+ [user_id, organization_id] = params.id.split("_");
1134
+ }
1135
+
1136
+ if (!organization_id || !user_id) {
1137
+ throw new Error(
1138
+ "Missing organization_id or user_id for user organization deletion",
1139
+ );
1140
+ }
1141
+
1142
+ await managementClient.organizations.members.delete(organization_id, {
1143
+ members: [user_id],
1144
+ });
1145
+ return { data: { id: params.id } };
1146
+ }
1147
+
1148
+ // Custom-domains using SDK
1149
+ if (resource === "custom-domains") {
1150
+ await (managementClient as any).customDomains.delete(
1151
+ params.id as string,
1152
+ );
1153
+ return { data: { id: params.id } };
1154
+ }
1155
+
1156
+ // Nested permissions/roles detection
1157
+ const isNestedPermissionsDelete =
1158
+ /(^|\/)users\/[^/]+\/permissions$/.test(resource) ||
1159
+ /(^|\/)roles\/[^/]+\/permissions$/.test(resource);
1160
+ const isNestedRolesDelete = /(^|\/)users\/[^/]+\/roles$/.test(resource);
1161
+ const isOrgMemberRolesDelete =
1162
+ /(^|\/)organizations\/[^/]+\/members\/[^/]+\/roles$/.test(resource);
1163
+
1164
+ const hasId =
1165
+ params?.id !== undefined &&
1166
+ params?.id !== null &&
1167
+ String(params.id) !== "";
1168
+
1169
+ const shouldAppendId =
1170
+ hasId &&
1171
+ !(
1172
+ isNestedPermissionsDelete ||
1173
+ isNestedRolesDelete ||
1174
+ isOrgMemberRolesDelete
1175
+ );
1176
+
1177
+ const resourceUrl = shouldAppendId
1178
+ ? `${resource}/${encodeURIComponent(String(params.id))}`
1179
+ : resource;
1180
+
1181
+ let body: any = undefined;
1182
+
1183
+ if (isNestedPermissionsDelete) {
1184
+ const prev: any = params?.previousData ?? {};
1185
+ const parsedFromId = (() => {
1186
+ if (!hasId)
1187
+ return {
1188
+ resource_server_identifier: undefined,
1189
+ permission_name: undefined,
1190
+ };
1191
+ try {
1192
+ const decoded = decodeURIComponent(String(params.id));
1193
+ const [rsi, pname] = decoded.split(":");
1194
+ return { resource_server_identifier: rsi, permission_name: pname };
1195
+ } catch {
1196
+ return {
1197
+ resource_server_identifier: undefined,
1198
+ permission_name: undefined,
1199
+ };
1200
+ }
1201
+ })();
1202
+
1203
+ body = {
1204
+ permissions: [
1205
+ {
1206
+ permission_name:
1207
+ prev.permission_name ?? parsedFromId.permission_name,
1208
+ resource_server_identifier:
1209
+ prev.resource_server_identifier ??
1210
+ parsedFromId.resource_server_identifier,
1211
+ },
1212
+ ],
1213
+ };
1214
+ } else if (isNestedRolesDelete || isOrgMemberRolesDelete) {
1215
+ const roles = Array.isArray(params?.previousData?.roles)
1216
+ ? params.previousData.roles
1217
+ : hasId
1218
+ ? [String(params.id)]
1219
+ : [];
1220
+ body = { roles };
1221
+ }
1222
+
1223
+ // Update headers for nested endpoints
1224
+ if (
1225
+ !isNestedPermissionsDelete &&
1226
+ !isNestedRolesDelete &&
1227
+ !isOrgMemberRolesDelete
1228
+ ) {
1229
+ headers.set("Content-Type", "text/plain");
1230
+ }
1231
+
1232
+ const res = await httpClient(`${apiUrl}/api/v2/${resourceUrl}`, {
1233
+ method: "DELETE",
1234
+ headers,
1235
+ body: body ? JSON.stringify(body) : undefined,
1236
+ });
1237
+ return { data: res.json };
1238
+ },
1239
+
1240
+ deleteMany: () => Promise.reject("not supporting deleteMany"),
1241
+ };
1242
+ };