@authhero/react-admin 0.20.0 → 0.21.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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/auth0DataProvider.ts +33 -7
- package/src/authProvider.ts +34 -18
- package/src/components/flows/edit.tsx +10 -3
- package/src/components/hooks/list.tsx +1 -1
- package/src/utils/tokenUtils.ts +4 -2
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/auth0DataProvider.ts
CHANGED
|
@@ -340,7 +340,7 @@ export default (
|
|
|
340
340
|
if (Array.isArray(res.json)) {
|
|
341
341
|
return {
|
|
342
342
|
data: res.json.map((item) => ({
|
|
343
|
-
id: item.
|
|
343
|
+
id: item.id,
|
|
344
344
|
...item,
|
|
345
345
|
})),
|
|
346
346
|
total: res.json.length,
|
|
@@ -351,7 +351,7 @@ export default (
|
|
|
351
351
|
return {
|
|
352
352
|
data:
|
|
353
353
|
res.json[resource]?.map((item: any) => ({
|
|
354
|
-
id: item.
|
|
354
|
+
id: item.id,
|
|
355
355
|
...item,
|
|
356
356
|
})) || [],
|
|
357
357
|
total: res.json.total || res.json.length || 0,
|
|
@@ -382,15 +382,30 @@ export default (
|
|
|
382
382
|
fetch: (id) => (managementClient as any).customDomains.get(id),
|
|
383
383
|
idKey: "custom_domain_id",
|
|
384
384
|
},
|
|
385
|
+
flows: {
|
|
386
|
+
fetch: (id) => (managementClient as any).flows.get(id),
|
|
387
|
+
idKey: "id",
|
|
388
|
+
},
|
|
389
|
+
hooks: {
|
|
390
|
+
fetch: (id) => (managementClient as any).hooks.get(id),
|
|
391
|
+
idKey: "hook_id",
|
|
392
|
+
},
|
|
393
|
+
forms: {
|
|
394
|
+
fetch: (id) => managementClient.forms.get(id),
|
|
395
|
+
idKey: "id",
|
|
396
|
+
},
|
|
385
397
|
};
|
|
386
398
|
|
|
387
399
|
const handler = sdkGetHandlers[resource];
|
|
388
400
|
if (handler) {
|
|
389
401
|
const result = await handler.fetch(params.id as string);
|
|
402
|
+
// Unwrap SDK response wrapper if present
|
|
403
|
+
const data = (result as any).response || result;
|
|
404
|
+
|
|
390
405
|
return {
|
|
391
406
|
data: {
|
|
392
|
-
id:
|
|
393
|
-
...
|
|
407
|
+
id: data[handler.idKey] || data.id,
|
|
408
|
+
...data,
|
|
394
409
|
},
|
|
395
410
|
};
|
|
396
411
|
}
|
|
@@ -926,8 +941,12 @@ export default (
|
|
|
926
941
|
body: JSON.stringify(cleanParams.data),
|
|
927
942
|
}).then(({ json }) => {
|
|
928
943
|
if (!json.id) {
|
|
929
|
-
|
|
930
|
-
|
|
944
|
+
// Try singular form of resource name (e.g., hooks -> hook_id)
|
|
945
|
+
const singularResource = resource.endsWith("s")
|
|
946
|
+
? resource.slice(0, -1)
|
|
947
|
+
: resource;
|
|
948
|
+
json.id =
|
|
949
|
+
json[`${singularResource}_id`] || json[`${resource}_id`] || json.id;
|
|
931
950
|
}
|
|
932
951
|
return { data: json };
|
|
933
952
|
});
|
|
@@ -1073,10 +1092,17 @@ export default (
|
|
|
1073
1092
|
|
|
1074
1093
|
// Default create (for endpoints not in SDK)
|
|
1075
1094
|
const res = await post(resource, params.data);
|
|
1095
|
+
// Try singular form of resource name (e.g., hooks -> hook_id)
|
|
1096
|
+
const singularResource = resource.endsWith("s")
|
|
1097
|
+
? resource.slice(0, -1)
|
|
1098
|
+
: resource;
|
|
1076
1099
|
return {
|
|
1077
1100
|
data: {
|
|
1078
1101
|
...res.json,
|
|
1079
|
-
id:
|
|
1102
|
+
id:
|
|
1103
|
+
res.json[`${singularResource}_id`] ||
|
|
1104
|
+
res.json[`${resource}_id`] ||
|
|
1105
|
+
res.json.id,
|
|
1080
1106
|
},
|
|
1081
1107
|
};
|
|
1082
1108
|
},
|
package/src/authProvider.ts
CHANGED
|
@@ -122,7 +122,9 @@ export const createAuth0ClientForOrg = (
|
|
|
122
122
|
domain: string,
|
|
123
123
|
organizationId: string,
|
|
124
124
|
) => {
|
|
125
|
-
|
|
125
|
+
// Normalize organization ID to lowercase to avoid casing mismatches
|
|
126
|
+
const normalizedOrgId = organizationId.toLowerCase();
|
|
127
|
+
const cacheKey = `${domain}:${normalizedOrgId}`;
|
|
126
128
|
|
|
127
129
|
// Check cache first
|
|
128
130
|
if (auth0OrgClientCache.has(cacheKey)) {
|
|
@@ -147,12 +149,12 @@ export const createAuth0ClientForOrg = (
|
|
|
147
149
|
useRefreshTokens: false,
|
|
148
150
|
// Use organization-specific cache to isolate tokens
|
|
149
151
|
// Note: Don't use cacheLocation when providing a custom cache
|
|
150
|
-
cache: new OrgCache(
|
|
152
|
+
cache: new OrgCache(normalizedOrgId),
|
|
151
153
|
authorizationParams: {
|
|
152
154
|
audience,
|
|
153
155
|
redirect_uri: redirectUri,
|
|
154
156
|
scope: "openid profile email auth:read auth:write",
|
|
155
|
-
organization:
|
|
157
|
+
organization: normalizedOrgId,
|
|
156
158
|
},
|
|
157
159
|
});
|
|
158
160
|
|
|
@@ -167,7 +169,11 @@ export const createManagementClient = async (
|
|
|
167
169
|
tenantId?: string,
|
|
168
170
|
oauthDomain?: string,
|
|
169
171
|
): Promise<ManagementClient> => {
|
|
170
|
-
|
|
172
|
+
// Normalize tenant ID to lowercase to avoid casing mismatches
|
|
173
|
+
const normalizedTenantId = tenantId?.toLowerCase();
|
|
174
|
+
const cacheKey = normalizedTenantId
|
|
175
|
+
? `${apiDomain}:${normalizedTenantId}`
|
|
176
|
+
: apiDomain;
|
|
171
177
|
|
|
172
178
|
// Check cache first
|
|
173
179
|
if (managementClientCache.has(cacheKey)) {
|
|
@@ -193,18 +199,21 @@ export const createManagementClient = async (
|
|
|
193
199
|
const storedFlag = sessionStorage.getItem("isSingleTenant");
|
|
194
200
|
const isSingleTenant = storedFlag?.endsWith("|true") || storedFlag === "true";
|
|
195
201
|
|
|
196
|
-
if (
|
|
202
|
+
if (normalizedTenantId && !isSingleTenant) {
|
|
197
203
|
// When accessing tenant-specific resources in MULTI-TENANT mode, use org-scoped token
|
|
198
204
|
if (domainConfig.connectionMethod === "login") {
|
|
199
205
|
// For OAuth login, use organization-scoped client
|
|
200
|
-
const orgAuth0Client = createAuth0ClientForOrg(
|
|
206
|
+
const orgAuth0Client = createAuth0ClientForOrg(
|
|
207
|
+
domainForAuth,
|
|
208
|
+
normalizedTenantId,
|
|
209
|
+
);
|
|
201
210
|
const audience =
|
|
202
211
|
import.meta.env.VITE_AUTH0_AUDIENCE || "urn:authhero:management";
|
|
203
212
|
try {
|
|
204
213
|
token = await orgAuth0Client.getTokenSilently({
|
|
205
214
|
authorizationParams: {
|
|
206
215
|
audience,
|
|
207
|
-
organization:
|
|
216
|
+
organization: normalizedTenantId,
|
|
208
217
|
},
|
|
209
218
|
});
|
|
210
219
|
} catch (error) {
|
|
@@ -216,7 +225,7 @@ export const createManagementClient = async (
|
|
|
216
225
|
// Redirect to login with organization
|
|
217
226
|
await orgAuth0Client.loginWithRedirect({
|
|
218
227
|
authorizationParams: {
|
|
219
|
-
organization:
|
|
228
|
+
organization: normalizedTenantId,
|
|
220
229
|
login_hint: user?.email,
|
|
221
230
|
},
|
|
222
231
|
appState: {
|
|
@@ -225,11 +234,13 @@ export const createManagementClient = async (
|
|
|
225
234
|
});
|
|
226
235
|
|
|
227
236
|
// This won't be reached as loginWithRedirect redirects the page
|
|
228
|
-
throw new Error(
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Redirecting to login for organization ${normalizedTenantId}`,
|
|
239
|
+
);
|
|
229
240
|
}
|
|
230
241
|
} else {
|
|
231
242
|
// For token/client_credentials, use getOrganizationToken
|
|
232
|
-
token = await getOrganizationToken(domainConfig,
|
|
243
|
+
token = await getOrganizationToken(domainConfig, normalizedTenantId);
|
|
233
244
|
}
|
|
234
245
|
} else {
|
|
235
246
|
// No tenantId - get non-org-scoped token for tenant management endpoints
|
|
@@ -244,7 +255,9 @@ export const createManagementClient = async (
|
|
|
244
255
|
const managementClient = new ManagementClient({
|
|
245
256
|
domain: apiDomain,
|
|
246
257
|
token,
|
|
247
|
-
headers:
|
|
258
|
+
headers: normalizedTenantId
|
|
259
|
+
? { "tenant-id": normalizedTenantId }
|
|
260
|
+
: undefined,
|
|
248
261
|
});
|
|
249
262
|
|
|
250
263
|
managementClientCache.set(cacheKey, managementClient);
|
|
@@ -688,8 +701,11 @@ const authorizedHttpClient = (url: string, options: HttpOptions = {}) => {
|
|
|
688
701
|
* @returns An HTTP client function that uses organization-scoped tokens
|
|
689
702
|
*/
|
|
690
703
|
export const createOrganizationHttpClient = (organizationId: string) => {
|
|
704
|
+
// Normalize organization ID to lowercase to avoid casing mismatches
|
|
705
|
+
const normalizedOrgId = organizationId.toLowerCase();
|
|
706
|
+
|
|
691
707
|
return (url: string, options: HttpOptions = {}) => {
|
|
692
|
-
const requestKey = `${
|
|
708
|
+
const requestKey = `${normalizedOrgId}:${url}-${JSON.stringify(options)}`;
|
|
693
709
|
|
|
694
710
|
// If there's already a pending request for this URL and options, return it
|
|
695
711
|
if (pendingRequests.has(requestKey)) {
|
|
@@ -728,7 +744,7 @@ export const createOrganizationHttpClient = (organizationId: string) => {
|
|
|
728
744
|
!activeSessions.has(formattedSelectedDomain)
|
|
729
745
|
) {
|
|
730
746
|
clearInterval(checkInterval);
|
|
731
|
-
createOrganizationHttpClient(
|
|
747
|
+
createOrganizationHttpClient(normalizedOrgId)(url, options)
|
|
732
748
|
.then(resolve)
|
|
733
749
|
.catch(reject);
|
|
734
750
|
}
|
|
@@ -768,7 +784,7 @@ export const createOrganizationHttpClient = (organizationId: string) => {
|
|
|
768
784
|
) {
|
|
769
785
|
// For token/client_credentials, use organization-scoped token
|
|
770
786
|
// This includes the org_id claim for accessing tenant-specific resources
|
|
771
|
-
request = getOrganizationToken(domainConfig,
|
|
787
|
+
request = getOrganizationToken(domainConfig, normalizedOrgId)
|
|
772
788
|
.catch((error) => {
|
|
773
789
|
throw new Error(
|
|
774
790
|
`Authentication failed: ${error.message}. Please configure your credentials in the domain selector.`,
|
|
@@ -838,7 +854,7 @@ export const createOrganizationHttpClient = (organizationId: string) => {
|
|
|
838
854
|
// For OAuth login, use an organization-specific client with isolated cache
|
|
839
855
|
const orgAuth0Client = createAuth0ClientForOrg(
|
|
840
856
|
selectedDomain,
|
|
841
|
-
|
|
857
|
+
normalizedOrgId,
|
|
842
858
|
);
|
|
843
859
|
|
|
844
860
|
// Use the management API audience for cross-tenant operations
|
|
@@ -851,7 +867,7 @@ export const createOrganizationHttpClient = (organizationId: string) => {
|
|
|
851
867
|
.getTokenSilently({
|
|
852
868
|
authorizationParams: {
|
|
853
869
|
audience,
|
|
854
|
-
organization:
|
|
870
|
+
organization: normalizedOrgId,
|
|
855
871
|
},
|
|
856
872
|
})
|
|
857
873
|
.catch(async (_error) => {
|
|
@@ -863,7 +879,7 @@ export const createOrganizationHttpClient = (organizationId: string) => {
|
|
|
863
879
|
// Redirect to login with organization
|
|
864
880
|
await orgAuth0Client.loginWithRedirect({
|
|
865
881
|
authorizationParams: {
|
|
866
|
-
organization:
|
|
882
|
+
organization: normalizedOrgId,
|
|
867
883
|
login_hint: user?.email,
|
|
868
884
|
},
|
|
869
885
|
appState: {
|
|
@@ -873,7 +889,7 @@ export const createOrganizationHttpClient = (organizationId: string) => {
|
|
|
873
889
|
|
|
874
890
|
// This won't be reached as loginWithRedirect redirects the page
|
|
875
891
|
throw new Error(
|
|
876
|
-
`Redirecting to login for organization ${
|
|
892
|
+
`Redirecting to login for organization ${normalizedOrgId}`,
|
|
877
893
|
);
|
|
878
894
|
})
|
|
879
895
|
.then((token) => {
|
|
@@ -89,9 +89,16 @@ export const FlowEdit = () => {
|
|
|
89
89
|
return (
|
|
90
90
|
<Edit
|
|
91
91
|
queryOptions={{
|
|
92
|
-
select: (
|
|
93
|
-
data
|
|
94
|
-
|
|
92
|
+
select: (response) => {
|
|
93
|
+
const data = response?.data as Record<string, unknown>;
|
|
94
|
+
if (!data) {
|
|
95
|
+
console.error("FlowEdit: No data in response", response);
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
data: parseFlowData(data),
|
|
100
|
+
};
|
|
101
|
+
},
|
|
95
102
|
}}
|
|
96
103
|
transform={(data: Record<string, unknown>) => {
|
|
97
104
|
// Transform actions to include required fields
|
|
@@ -5,7 +5,7 @@ export function HookList() {
|
|
|
5
5
|
return (
|
|
6
6
|
<List actions={<PostListActions />}>
|
|
7
7
|
<Datagrid rowClick="edit" bulkActionButtons={false}>
|
|
8
|
-
<TextField source="
|
|
8
|
+
<TextField source="hook_id" />
|
|
9
9
|
<TextField source="trigger_id" />
|
|
10
10
|
<TextField source="url" />
|
|
11
11
|
<TextField source="form_id" label="Form" />
|
package/src/utils/tokenUtils.ts
CHANGED
|
@@ -18,7 +18,8 @@ export class OrgCache implements ICache {
|
|
|
18
18
|
private orgId: string;
|
|
19
19
|
|
|
20
20
|
constructor(orgId: string) {
|
|
21
|
-
|
|
21
|
+
// Normalize organization ID to lowercase to avoid casing mismatches
|
|
22
|
+
this.orgId = orgId.toLowerCase();
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
public set<T = Cacheable>(key: string, entry: T) {
|
|
@@ -77,8 +78,9 @@ async function fetchTokenWithClientCredentials(
|
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
// Add organization if specified - this will include org_id in the token
|
|
81
|
+
// Normalize to lowercase to avoid casing mismatches
|
|
80
82
|
if (organizationId) {
|
|
81
|
-
body.organization = organizationId;
|
|
83
|
+
body.organization = organizationId.toLowerCase();
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
const response = await fetch(`https://proxy.authhe.ro/oauth/token`, {
|