@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.
- package/.eslintrc.js +21 -0
- package/.vercelignore +4 -0
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/index.html +125 -0
- package/package.json +61 -0
- package/prettier.config.js +1 -0
- package/public/favicon.ico +0 -0
- package/public/manifest.json +15 -0
- package/src/App.spec.tsx +42 -0
- package/src/App.tsx +232 -0
- package/src/AuthCallback.tsx +138 -0
- package/src/Layout.tsx +12 -0
- package/src/TenantsApp.tsx +115 -0
- package/src/auth0DataProvider.ts +1242 -0
- package/src/authProvider.ts +521 -0
- package/src/components/CertificateErrorDialog.tsx +116 -0
- package/src/components/DomainSelector.tsx +401 -0
- package/src/components/TenantAppBar.tsx +83 -0
- package/src/components/TenantLayout.tsx +25 -0
- package/src/components/TenantsAppBar.tsx +21 -0
- package/src/components/TenantsLayout.tsx +28 -0
- package/src/components/activity/ActivityDashboard.tsx +381 -0
- package/src/components/activity/index.ts +1 -0
- package/src/components/branding/BrandingList.tsx +0 -0
- package/src/components/branding/BrandingShow.tsx +0 -0
- package/src/components/branding/ThemesTab.tsx +286 -0
- package/src/components/branding/edit.tsx +149 -0
- package/src/components/branding/hooks/useThemesData.ts +123 -0
- package/src/components/branding/index.ts +2 -0
- package/src/components/branding/list.tsx +12 -0
- package/src/components/clients/create.tsx +12 -0
- package/src/components/clients/edit.tsx +1285 -0
- package/src/components/clients/index.ts +3 -0
- package/src/components/clients/list.tsx +37 -0
- package/src/components/common/DateAgo.tsx +6 -0
- package/src/components/common/JsonOutput.tsx +26 -0
- package/src/components/common/index.ts +1 -0
- package/src/components/connections/create.tsx +35 -0
- package/src/components/connections/edit.tsx +212 -0
- package/src/components/connections/index.ts +3 -0
- package/src/components/connections/list.tsx +15 -0
- package/src/components/custom-domains/create.tsx +26 -0
- package/src/components/custom-domains/edit.tsx +101 -0
- package/src/components/custom-domains/index.ts +3 -0
- package/src/components/custom-domains/list.tsx +16 -0
- package/src/components/flows/create.tsx +30 -0
- package/src/components/flows/edit.tsx +238 -0
- package/src/components/flows/index.ts +3 -0
- package/src/components/flows/list.tsx +15 -0
- package/src/components/forms/FlowEditor.tsx +1363 -0
- package/src/components/forms/NodeEditor.tsx +1119 -0
- package/src/components/forms/RichTextEditor.tsx +145 -0
- package/src/components/forms/create.tsx +30 -0
- package/src/components/forms/edit.tsx +256 -0
- package/src/components/forms/index.ts +3 -0
- package/src/components/forms/list.tsx +16 -0
- package/src/components/hooks/create.tsx +96 -0
- package/src/components/hooks/edit.tsx +114 -0
- package/src/components/hooks/index.ts +3 -0
- package/src/components/hooks/list.tsx +17 -0
- package/src/components/listActions/PostListActions.tsx +10 -0
- package/src/components/logs/LogIcon.tsx +32 -0
- package/src/components/logs/LogShow.tsx +82 -0
- package/src/components/logs/LogType.tsx +38 -0
- package/src/components/logs/index.ts +4 -0
- package/src/components/logs/list.tsx +41 -0
- package/src/components/organizations/create.tsx +13 -0
- package/src/components/organizations/edit.tsx +682 -0
- package/src/components/organizations/index.ts +3 -0
- package/src/components/organizations/list.tsx +21 -0
- package/src/components/resource-servers/create.tsx +87 -0
- package/src/components/resource-servers/edit.tsx +121 -0
- package/src/components/resource-servers/index.ts +3 -0
- package/src/components/resource-servers/list.tsx +47 -0
- package/src/components/roles/create.tsx +12 -0
- package/src/components/roles/edit.tsx +426 -0
- package/src/components/roles/index.ts +3 -0
- package/src/components/roles/list.tsx +24 -0
- package/src/components/sessions/edit.tsx +101 -0
- package/src/components/sessions/index.ts +3 -0
- package/src/components/sessions/list.tsx +20 -0
- package/src/components/sessions/show.tsx +113 -0
- package/src/components/settings/edit.tsx +236 -0
- package/src/components/settings/index.ts +2 -0
- package/src/components/settings/list.tsx +14 -0
- package/src/components/tenants/create.tsx +20 -0
- package/src/components/tenants/edit.tsx +54 -0
- package/src/components/tenants/index.ts +2 -0
- package/src/components/tenants/list.tsx +67 -0
- package/src/components/themes/edit.tsx +200 -0
- package/src/components/themes/index.ts +2 -0
- package/src/components/themes/list.tsx +12 -0
- package/src/components/users/create.tsx +144 -0
- package/src/components/users/edit.tsx +1711 -0
- package/src/components/users/index.ts +3 -0
- package/src/components/users/list.tsx +35 -0
- package/src/data.json +121 -0
- package/src/dataProvider.ts +97 -0
- package/src/index.tsx +106 -0
- package/src/lib/logs.ts +21 -0
- package/src/types/reactflow.d.ts +86 -0
- package/src/utils/domainUtils.ts +169 -0
- package/src/utils/tokenUtils.ts +75 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +37 -0
- package/tsconfig.node.json +10 -0
- package/vercel.json +17 -0
- package/vite.config.ts +30 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { Auth0AuthProvider, httpClient } from "ra-auth-auth0";
|
|
2
|
+
import { Auth0Client } from "@auth0/auth0-spa-js";
|
|
3
|
+
import { ManagementClient } from "auth0";
|
|
4
|
+
import {
|
|
5
|
+
getSelectedDomainFromStorage,
|
|
6
|
+
getClientIdFromStorage,
|
|
7
|
+
getDomainFromStorage,
|
|
8
|
+
buildUrlWithProtocol,
|
|
9
|
+
} from "./utils/domainUtils";
|
|
10
|
+
import getToken from "./utils/tokenUtils";
|
|
11
|
+
|
|
12
|
+
// Track auth requests globally
|
|
13
|
+
let authRequestInProgress = false;
|
|
14
|
+
let lastAuthRequestTime = 0;
|
|
15
|
+
const AUTH_REQUEST_DEBOUNCE_MS = 1000; // Debounce time between auth requests
|
|
16
|
+
|
|
17
|
+
// Store active sessions by domain
|
|
18
|
+
const activeSessions = new Map<string, boolean>();
|
|
19
|
+
|
|
20
|
+
// Cache for auth0 clients
|
|
21
|
+
const auth0ClientCache = new Map<string, Auth0Client>();
|
|
22
|
+
|
|
23
|
+
// Cache for management clients
|
|
24
|
+
const managementClientCache = new Map<string, ManagementClient>();
|
|
25
|
+
|
|
26
|
+
// Create a function to get Auth0Client with the specified domain
|
|
27
|
+
export const createAuth0Client = (domain: string) => {
|
|
28
|
+
// Check cache first to avoid creating multiple clients for the same domain
|
|
29
|
+
if (auth0ClientCache.has(domain)) {
|
|
30
|
+
return auth0ClientCache.get(domain)!;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Build full domain URL with HTTPS
|
|
34
|
+
const fullDomain = buildUrlWithProtocol(domain);
|
|
35
|
+
|
|
36
|
+
// Get redirect URI from current app URL
|
|
37
|
+
const currentUrl = new URL(window.location.href);
|
|
38
|
+
const redirectUri = `${currentUrl.protocol}//${currentUrl.host}/auth-callback`;
|
|
39
|
+
|
|
40
|
+
const auth0Client = new Auth0Client({
|
|
41
|
+
domain: fullDomain,
|
|
42
|
+
clientId: getClientIdFromStorage(domain),
|
|
43
|
+
cacheLocation: "localstorage",
|
|
44
|
+
useRefreshTokens: true,
|
|
45
|
+
authorizationParams: {
|
|
46
|
+
audience: import.meta.env.VITE_AUTH0_AUDIENCE,
|
|
47
|
+
redirect_uri: redirectUri,
|
|
48
|
+
scope: "openid profile email auth:read auth:write",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Patch the loginWithRedirect method to prevent multiple calls
|
|
53
|
+
const originalLoginWithRedirect =
|
|
54
|
+
auth0Client.loginWithRedirect.bind(auth0Client);
|
|
55
|
+
auth0Client.loginWithRedirect = async (options?: any) => {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
|
|
58
|
+
// Check if we already have an active session
|
|
59
|
+
const hasActiveSession = await auth0Client.isAuthenticated();
|
|
60
|
+
if (hasActiveSession) {
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Prevent multiple auth requests in parallel or in quick succession
|
|
65
|
+
if (
|
|
66
|
+
authRequestInProgress ||
|
|
67
|
+
now - lastAuthRequestTime < AUTH_REQUEST_DEBOUNCE_MS
|
|
68
|
+
) {
|
|
69
|
+
return Promise.resolve();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
authRequestInProgress = true;
|
|
74
|
+
lastAuthRequestTime = now;
|
|
75
|
+
activeSessions.set(domain, true);
|
|
76
|
+
return await originalLoginWithRedirect(options);
|
|
77
|
+
} finally {
|
|
78
|
+
// Reset after redirect completes or fails
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
authRequestInProgress = false;
|
|
81
|
+
}, 1000); // Give a short delay to prevent immediate retries
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Also patch the handleRedirectCallback to signal when the auth flow is complete
|
|
86
|
+
const originalHandleRedirectCallback =
|
|
87
|
+
auth0Client.handleRedirectCallback.bind(auth0Client);
|
|
88
|
+
auth0Client.handleRedirectCallback = async (url?: string) => {
|
|
89
|
+
try {
|
|
90
|
+
const result = await originalHandleRedirectCallback(url);
|
|
91
|
+
return result;
|
|
92
|
+
} finally {
|
|
93
|
+
// Mark that this domain's auth flow is complete
|
|
94
|
+
activeSessions.delete(domain);
|
|
95
|
+
authRequestInProgress = false;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Store in cache
|
|
100
|
+
auth0ClientCache.set(domain, auth0Client);
|
|
101
|
+
return auth0Client;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Create a Management API client
|
|
105
|
+
export const createManagementClient = async (
|
|
106
|
+
apiDomain: string,
|
|
107
|
+
tenantId?: string,
|
|
108
|
+
oauthDomain?: string,
|
|
109
|
+
): Promise<ManagementClient> => {
|
|
110
|
+
const cacheKey = tenantId ? `${apiDomain}:${tenantId}` : apiDomain;
|
|
111
|
+
|
|
112
|
+
// Check cache first
|
|
113
|
+
if (managementClientCache.has(cacheKey)) {
|
|
114
|
+
return managementClientCache.get(cacheKey)!;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Use oauthDomain for finding credentials, fallback to apiDomain
|
|
118
|
+
const domainForAuth = oauthDomain || apiDomain;
|
|
119
|
+
const domains = getDomainFromStorage();
|
|
120
|
+
const domainConfig = domains.find((d) => d.url === domainForAuth);
|
|
121
|
+
|
|
122
|
+
if (!domainConfig) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`No domain configuration found for domain: ${domainForAuth}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Get auth0Client if using login method
|
|
129
|
+
let auth0Client: Auth0Client | undefined;
|
|
130
|
+
if (domainConfig.connectionMethod === "login") {
|
|
131
|
+
auth0Client = createAuth0Client(domainForAuth);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const token = await getToken(domainConfig, auth0Client);
|
|
135
|
+
|
|
136
|
+
// ManagementClient expects domain WITHOUT protocol (it adds https:// internally)
|
|
137
|
+
const managementClient = new ManagementClient({
|
|
138
|
+
domain: apiDomain,
|
|
139
|
+
token,
|
|
140
|
+
headers: tenantId ? { "tenant-id": tenantId } : undefined,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
managementClientCache.set(cacheKey, managementClient);
|
|
144
|
+
return managementClient;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Clear management client cache when token might be expired
|
|
148
|
+
// Note: Clears the entire cache since cache keys use apiDomain[:tenantId]
|
|
149
|
+
// but callers typically only have access to the OAuth domain
|
|
150
|
+
export const clearManagementClientCache = () => {
|
|
151
|
+
managementClientCache.clear();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Create a function to get the auth provider with the specified domain
|
|
155
|
+
export const getAuthProvider = (
|
|
156
|
+
domain: string,
|
|
157
|
+
onAuthComplete?: () => void,
|
|
158
|
+
) => {
|
|
159
|
+
// Get domain config to check connection method
|
|
160
|
+
const domains = getDomainFromStorage();
|
|
161
|
+
const domainConfig = domains.find((d) => d.url === domain);
|
|
162
|
+
|
|
163
|
+
// If using token auth or client credentials, create a simple auth provider that uses the token
|
|
164
|
+
if (
|
|
165
|
+
["token", "client_credentials"].includes(
|
|
166
|
+
domainConfig?.connectionMethod || "",
|
|
167
|
+
)
|
|
168
|
+
) {
|
|
169
|
+
return {
|
|
170
|
+
login: async () => {
|
|
171
|
+
// Token auth is already authenticated
|
|
172
|
+
return Promise.resolve();
|
|
173
|
+
},
|
|
174
|
+
logout: async () => {
|
|
175
|
+
// Clear management client cache on logout
|
|
176
|
+
clearManagementClientCache();
|
|
177
|
+
return Promise.resolve();
|
|
178
|
+
},
|
|
179
|
+
checkError: async (error: any) => {
|
|
180
|
+
if (error.status === 401 || error.statusCode === 401) {
|
|
181
|
+
clearManagementClientCache();
|
|
182
|
+
return Promise.reject();
|
|
183
|
+
}
|
|
184
|
+
return Promise.resolve();
|
|
185
|
+
},
|
|
186
|
+
checkAuth: async () => {
|
|
187
|
+
// Verify that credentials are actually available
|
|
188
|
+
if (!domainConfig) {
|
|
189
|
+
return Promise.reject(new Error("No domain configuration found"));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (domainConfig.connectionMethod === "token" && !domainConfig.token) {
|
|
193
|
+
return Promise.reject(
|
|
194
|
+
new Error("Token authentication selected but no token provided"),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
domainConfig.connectionMethod === "client_credentials" &&
|
|
200
|
+
(!domainConfig.clientId || !domainConfig.clientSecret)
|
|
201
|
+
) {
|
|
202
|
+
return Promise.reject(
|
|
203
|
+
new Error(
|
|
204
|
+
"Client credentials authentication selected but credentials missing",
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return Promise.resolve();
|
|
210
|
+
},
|
|
211
|
+
getIdentity: async () => {
|
|
212
|
+
// Return a dummy UserIdentity for token-based auth
|
|
213
|
+
return Promise.resolve({
|
|
214
|
+
id: "token-user",
|
|
215
|
+
fullName: "API Token User",
|
|
216
|
+
avatar: undefined,
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
getPermissions: async () => {
|
|
220
|
+
return Promise.resolve(["admin"]);
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// For non-token auth, use Auth0
|
|
226
|
+
const auth0 = createAuth0Client(domain);
|
|
227
|
+
|
|
228
|
+
// Get the current app's URL for redirect
|
|
229
|
+
const currentUrl = new URL(window.location.href);
|
|
230
|
+
const redirectUri = `${currentUrl.protocol}//${currentUrl.host}`;
|
|
231
|
+
|
|
232
|
+
const baseAuthProvider = Auth0AuthProvider(auth0, {
|
|
233
|
+
// Use the current app's URL with the auth-callback path
|
|
234
|
+
loginRedirectUri: `${redirectUri}/auth-callback`,
|
|
235
|
+
// Use the current app's URL for logout
|
|
236
|
+
logoutRedirectUri: redirectUri,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Enhance the auth provider to signal when auth operations complete
|
|
240
|
+
return {
|
|
241
|
+
...baseAuthProvider,
|
|
242
|
+
login: async (params: any) => {
|
|
243
|
+
try {
|
|
244
|
+
const result = await baseAuthProvider.login(params);
|
|
245
|
+
if (onAuthComplete) onAuthComplete();
|
|
246
|
+
return result;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (onAuthComplete) onAuthComplete();
|
|
249
|
+
activeSessions.delete(domain);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
logout: async (params: any) => {
|
|
254
|
+
try {
|
|
255
|
+
clearManagementClientCache();
|
|
256
|
+
const result = await baseAuthProvider.logout(params);
|
|
257
|
+
if (onAuthComplete) onAuthComplete();
|
|
258
|
+
return result;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (onAuthComplete) onAuthComplete();
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
checkAuth: async (params: any): Promise<void> => {
|
|
265
|
+
try {
|
|
266
|
+
// Don't check auth while on the callback page - we're in the middle of authenticating
|
|
267
|
+
if (window.location.pathname === "/auth-callback") {
|
|
268
|
+
// Return success to prevent redirect loops during callback processing
|
|
269
|
+
return Promise.resolve();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// If auth is in progress, wait for it to complete
|
|
273
|
+
if (authRequestInProgress || activeSessions.has(domain)) {
|
|
274
|
+
return new Promise<void>((resolve, reject) => {
|
|
275
|
+
const checkInterval = setInterval(() => {
|
|
276
|
+
if (!authRequestInProgress && !activeSessions.has(domain)) {
|
|
277
|
+
clearInterval(checkInterval);
|
|
278
|
+
// Re-check auth now that the redirect is complete
|
|
279
|
+
baseAuthProvider.checkAuth(params).then(resolve).catch(reject);
|
|
280
|
+
}
|
|
281
|
+
}, 100);
|
|
282
|
+
|
|
283
|
+
// Timeout after 30 seconds
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
clearInterval(checkInterval);
|
|
286
|
+
reject(new Error("Authentication timeout"));
|
|
287
|
+
}, 30000);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await baseAuthProvider.checkAuth(params);
|
|
292
|
+
return;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (onAuthComplete) onAuthComplete();
|
|
295
|
+
activeSessions.delete(domain);
|
|
296
|
+
clearManagementClientCache();
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Create auth provider for the selected domain
|
|
304
|
+
const authProvider = getAuthProvider(getSelectedDomainFromStorage());
|
|
305
|
+
|
|
306
|
+
// Create a debounced http client to prevent parallel token requests
|
|
307
|
+
let pendingRequests = new Map<string, Promise<any>>();
|
|
308
|
+
interface HttpOptions extends RequestInit {
|
|
309
|
+
headers?: HeadersInit;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const authorizedHttpClient = (url: string, options: HttpOptions = {}) => {
|
|
313
|
+
const requestKey = `${url}-${JSON.stringify(options)}`;
|
|
314
|
+
|
|
315
|
+
// If there's already a pending request for this URL and options, return it
|
|
316
|
+
if (pendingRequests.has(requestKey)) {
|
|
317
|
+
return pendingRequests.get(requestKey)!;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Prevent API calls while on the auth callback page
|
|
321
|
+
if (window.location.pathname === "/auth-callback") {
|
|
322
|
+
return Promise.reject({
|
|
323
|
+
json: {
|
|
324
|
+
message: "Authentication in progress. Please wait...",
|
|
325
|
+
},
|
|
326
|
+
status: 401,
|
|
327
|
+
headers: new Headers(),
|
|
328
|
+
body: JSON.stringify({ message: "Authentication in progress" }),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check if we're using token-based auth or client credentials
|
|
333
|
+
const domains = getDomainFromStorage();
|
|
334
|
+
const selectedDomain = getSelectedDomainFromStorage();
|
|
335
|
+
const domainConfig = domains.find((d) => d.url === selectedDomain);
|
|
336
|
+
|
|
337
|
+
// If using login method and auth request is in progress, delay the request
|
|
338
|
+
if (
|
|
339
|
+
domainConfig?.connectionMethod === "login" &&
|
|
340
|
+
(authRequestInProgress || activeSessions.has(selectedDomain))
|
|
341
|
+
) {
|
|
342
|
+
// Return a promise that waits for auth to complete
|
|
343
|
+
const delayedRequest = new Promise((resolve, reject) => {
|
|
344
|
+
const checkInterval = setInterval(() => {
|
|
345
|
+
if (!authRequestInProgress && !activeSessions.has(selectedDomain)) {
|
|
346
|
+
clearInterval(checkInterval);
|
|
347
|
+
// Retry the request now that auth is complete
|
|
348
|
+
authorizedHttpClient(url, options).then(resolve).catch(reject);
|
|
349
|
+
}
|
|
350
|
+
}, 100);
|
|
351
|
+
|
|
352
|
+
// Timeout after 30 seconds
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
clearInterval(checkInterval);
|
|
355
|
+
reject(new Error("Authentication timeout"));
|
|
356
|
+
}, 30000);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
pendingRequests.set(requestKey, delayedRequest);
|
|
360
|
+
delayedRequest.finally(() => {
|
|
361
|
+
pendingRequests.delete(requestKey);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return delayedRequest;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// If no domain config found, throw an error
|
|
368
|
+
if (!domainConfig) {
|
|
369
|
+
return Promise.reject({
|
|
370
|
+
json: {
|
|
371
|
+
message:
|
|
372
|
+
"No domain configuration found. Please select a domain and configure authentication.",
|
|
373
|
+
},
|
|
374
|
+
status: 401,
|
|
375
|
+
headers: new Headers(),
|
|
376
|
+
body: JSON.stringify({ message: "No domain configuration found" }),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let request;
|
|
381
|
+
if (
|
|
382
|
+
["token", "client_credentials"].includes(
|
|
383
|
+
domainConfig.connectionMethod || "",
|
|
384
|
+
)
|
|
385
|
+
) {
|
|
386
|
+
// For token auth or client credentials, use the getToken helper
|
|
387
|
+
request = getToken(domainConfig)
|
|
388
|
+
.catch((error) => {
|
|
389
|
+
// If we can't get a token, throw a more helpful error
|
|
390
|
+
throw new Error(
|
|
391
|
+
`Authentication failed: ${error.message}. Please configure your credentials in the domain selector.`,
|
|
392
|
+
);
|
|
393
|
+
})
|
|
394
|
+
.then((token) => {
|
|
395
|
+
let headersObj: Headers;
|
|
396
|
+
const method = (options.method || "GET").toUpperCase();
|
|
397
|
+
if (method === "GET") {
|
|
398
|
+
// Only send Authorization for GET to avoid CORS issues
|
|
399
|
+
headersObj = new Headers();
|
|
400
|
+
headersObj.set("Authorization", `Bearer ${token}`);
|
|
401
|
+
} else if (
|
|
402
|
+
method === "POST" ||
|
|
403
|
+
method === "DELETE" ||
|
|
404
|
+
method === "PATCH"
|
|
405
|
+
) {
|
|
406
|
+
// For POST, DELETE, PATCH: only send Authorization and content-type (force application/json for POST/PATCH)
|
|
407
|
+
headersObj = new Headers();
|
|
408
|
+
headersObj.set("Authorization", `Bearer ${token}`);
|
|
409
|
+
if (method === "POST" || method === "PATCH") {
|
|
410
|
+
headersObj.set("content-type", "application/json");
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
// For other methods, merge all headers and set Authorization
|
|
414
|
+
headersObj = new Headers(options.headers || {});
|
|
415
|
+
headersObj.set("Authorization", `Bearer ${token}`);
|
|
416
|
+
}
|
|
417
|
+
return fetch(url, { ...options, headers: headersObj });
|
|
418
|
+
})
|
|
419
|
+
.then(async (response) => {
|
|
420
|
+
if (response.status < 200 || response.status >= 300) {
|
|
421
|
+
const text = await response.text();
|
|
422
|
+
|
|
423
|
+
// Check for 401 with "Bad audience" message on /v2/tenants endpoint - Auth0 doesn't support this endpoint
|
|
424
|
+
if (
|
|
425
|
+
response.status === 401 &&
|
|
426
|
+
text.includes("Bad audience") &&
|
|
427
|
+
url.includes("/v2/tenants")
|
|
428
|
+
) {
|
|
429
|
+
console.warn(
|
|
430
|
+
"Auth0 server detected without multi-tenant support. Navigating to Auth0 page",
|
|
431
|
+
);
|
|
432
|
+
// Clean up pending request to prevent deadlock
|
|
433
|
+
pendingRequests.delete(requestKey);
|
|
434
|
+
// Use history.pushState for a soft navigation instead of a hard redirect
|
|
435
|
+
window.history.pushState({}, "", "/auth0");
|
|
436
|
+
// Dispatch a navigation event so the app can respond to the URL change
|
|
437
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
438
|
+
// Reject with a clear error message instead of hanging
|
|
439
|
+
throw new Error("Navigating to Auth0 configuration");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
throw new Error(response.statusText);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const text = await response.text();
|
|
446
|
+
let json;
|
|
447
|
+
try {
|
|
448
|
+
json = JSON.parse(text);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
json = text;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Return in the format expected by react-admin's dataProvider
|
|
454
|
+
return {
|
|
455
|
+
json,
|
|
456
|
+
status: response.status,
|
|
457
|
+
headers: response.headers,
|
|
458
|
+
body: text,
|
|
459
|
+
};
|
|
460
|
+
})
|
|
461
|
+
.catch((error) => {
|
|
462
|
+
// Check for certificate errors (network failures when connecting to HTTPS with untrusted cert)
|
|
463
|
+
if (error.message === "Failed to fetch" || error.name === "TypeError") {
|
|
464
|
+
const urlObj = new URL(url);
|
|
465
|
+
if (
|
|
466
|
+
urlObj.hostname === "localhost" ||
|
|
467
|
+
urlObj.hostname === "127.0.0.1"
|
|
468
|
+
) {
|
|
469
|
+
const certError = new Error(
|
|
470
|
+
`Unable to connect to ${urlObj.origin}. This may be due to an untrusted SSL certificate.\n\n` +
|
|
471
|
+
`Please visit ${urlObj.origin} in your browser and accept the security warning to trust the certificate, then refresh this page.`,
|
|
472
|
+
);
|
|
473
|
+
(certError as any).isCertificateError = true;
|
|
474
|
+
(certError as any).serverUrl = urlObj.origin;
|
|
475
|
+
throw certError;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
throw error;
|
|
479
|
+
});
|
|
480
|
+
} else {
|
|
481
|
+
// For Auth0 login method, create a client for the current domain
|
|
482
|
+
const currentAuth0Client = createAuth0Client(selectedDomain);
|
|
483
|
+
// Ensure headers is a Headers instance (ra-auth-auth0 expects this)
|
|
484
|
+
const normalizedOptions = {
|
|
485
|
+
...options,
|
|
486
|
+
headers: new Headers(options.headers || {}),
|
|
487
|
+
};
|
|
488
|
+
request = httpClient(currentAuth0Client)(url, normalizedOptions).catch(
|
|
489
|
+
(error) => {
|
|
490
|
+
// Check for certificate errors
|
|
491
|
+
if (error.message === "Failed to fetch" || error.name === "TypeError") {
|
|
492
|
+
const urlObj = new URL(url);
|
|
493
|
+
if (
|
|
494
|
+
urlObj.hostname === "localhost" ||
|
|
495
|
+
urlObj.hostname === "127.0.0.1"
|
|
496
|
+
) {
|
|
497
|
+
const certError = new Error(
|
|
498
|
+
`Unable to connect to ${urlObj.origin}. This may be due to an untrusted SSL certificate.\n\n` +
|
|
499
|
+
`Please visit ${urlObj.origin} in your browser and accept the security warning to trust the certificate, then refresh this page.`,
|
|
500
|
+
);
|
|
501
|
+
(certError as any).isCertificateError = true;
|
|
502
|
+
(certError as any).serverUrl = urlObj.origin;
|
|
503
|
+
throw certError;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
throw error;
|
|
507
|
+
},
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Handle cleanup when request is done
|
|
512
|
+
request.finally(() => {
|
|
513
|
+
pendingRequests.delete(requestKey);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Cache the request
|
|
517
|
+
pendingRequests.set(requestKey, request);
|
|
518
|
+
return request;
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
export { authProvider, authorizedHttpClient };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogTitle,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogActions,
|
|
7
|
+
Button,
|
|
8
|
+
Typography,
|
|
9
|
+
Alert,
|
|
10
|
+
Link,
|
|
11
|
+
Box,
|
|
12
|
+
} from "@mui/material";
|
|
13
|
+
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
|
|
14
|
+
|
|
15
|
+
interface CertificateErrorDialogProps {
|
|
16
|
+
open: boolean;
|
|
17
|
+
serverUrl: string;
|
|
18
|
+
onClose: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const CertificateErrorDialog: React.FC<CertificateErrorDialogProps> = ({
|
|
22
|
+
open,
|
|
23
|
+
serverUrl,
|
|
24
|
+
onClose,
|
|
25
|
+
}) => {
|
|
26
|
+
const handleVisitServer = () => {
|
|
27
|
+
window.open(serverUrl, "_blank", "noopener,noreferrer");
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
|
32
|
+
<DialogTitle sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
33
|
+
<WarningAmberIcon color="warning" />
|
|
34
|
+
SSL Certificate Not Trusted
|
|
35
|
+
</DialogTitle>
|
|
36
|
+
<DialogContent>
|
|
37
|
+
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
38
|
+
Unable to connect to the AuthHero server due to an untrusted SSL
|
|
39
|
+
certificate.
|
|
40
|
+
</Alert>
|
|
41
|
+
|
|
42
|
+
<Typography variant="body1" paragraph>
|
|
43
|
+
Your local AuthHero server is using a self-signed SSL certificate that
|
|
44
|
+
your browser doesn't trust yet.
|
|
45
|
+
</Typography>
|
|
46
|
+
|
|
47
|
+
<Typography variant="body1" paragraph>
|
|
48
|
+
To fix this, please:
|
|
49
|
+
</Typography>
|
|
50
|
+
|
|
51
|
+
<Box component="ol" sx={{ pl: 2 }}>
|
|
52
|
+
<li>
|
|
53
|
+
<Typography variant="body2" paragraph>
|
|
54
|
+
Click the button below to visit{" "}
|
|
55
|
+
<Link href={serverUrl} target="_blank" rel="noopener">
|
|
56
|
+
{serverUrl}
|
|
57
|
+
</Link>
|
|
58
|
+
</Typography>
|
|
59
|
+
</li>
|
|
60
|
+
<li>
|
|
61
|
+
<Typography variant="body2" paragraph>
|
|
62
|
+
When you see the security warning, click{" "}
|
|
63
|
+
<strong>"Advanced"</strong> and then{" "}
|
|
64
|
+
<strong>"Proceed to localhost (unsafe)"</strong>
|
|
65
|
+
</Typography>
|
|
66
|
+
</li>
|
|
67
|
+
<li>
|
|
68
|
+
<Typography variant="body2" paragraph>
|
|
69
|
+
Return to this page and refresh
|
|
70
|
+
</Typography>
|
|
71
|
+
</li>
|
|
72
|
+
</Box>
|
|
73
|
+
|
|
74
|
+
<Alert severity="info" sx={{ mt: 2 }}>
|
|
75
|
+
<Typography variant="body2">
|
|
76
|
+
<strong>Tip:</strong> For a better experience, install{" "}
|
|
77
|
+
<Link
|
|
78
|
+
href="https://github.com/FiloSottile/mkcert"
|
|
79
|
+
target="_blank"
|
|
80
|
+
rel="noopener"
|
|
81
|
+
>
|
|
82
|
+
mkcert
|
|
83
|
+
</Link>{" "}
|
|
84
|
+
to create locally-trusted certificates:
|
|
85
|
+
</Typography>
|
|
86
|
+
<Box
|
|
87
|
+
component="pre"
|
|
88
|
+
sx={{
|
|
89
|
+
mt: 1,
|
|
90
|
+
p: 1,
|
|
91
|
+
bgcolor: "grey.100",
|
|
92
|
+
borderRadius: 1,
|
|
93
|
+
fontSize: "0.85em",
|
|
94
|
+
overflow: "auto",
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
brew install mkcert{"\n"}
|
|
98
|
+
mkcert -install
|
|
99
|
+
</Box>
|
|
100
|
+
<Typography variant="body2" sx={{ mt: 1 }}>
|
|
101
|
+
Then delete the <code>.certs</code> folder in your auth-server
|
|
102
|
+
directory and restart the server.
|
|
103
|
+
</Typography>
|
|
104
|
+
</Alert>
|
|
105
|
+
</DialogContent>
|
|
106
|
+
<DialogActions>
|
|
107
|
+
<Button onClick={onClose}>Cancel</Button>
|
|
108
|
+
<Button variant="contained" onClick={handleVisitServer}>
|
|
109
|
+
Visit {serverUrl}
|
|
110
|
+
</Button>
|
|
111
|
+
</DialogActions>
|
|
112
|
+
</Dialog>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default CertificateErrorDialog;
|