@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,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;