@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,3 @@
1
+ export * from "./create";
2
+ export * from "./list";
3
+ export * from "./edit";
@@ -0,0 +1,35 @@
1
+ import {
2
+ List,
3
+ Datagrid,
4
+ TextField,
5
+ EmailField,
6
+ TextInput,
7
+ FunctionField,
8
+ } from "react-admin";
9
+ import { PostListActions } from "../listActions/PostListActions";
10
+ import { DateAgo } from "../common";
11
+
12
+ export function UsersList() {
13
+ const postFilters = [
14
+ <TextInput key="search" label="Search" source="q" alwaysOn />,
15
+ ];
16
+
17
+ return (
18
+ <List
19
+ actions={<PostListActions />}
20
+ filters={postFilters}
21
+ sort={{ field: "user_id", order: "DESC" }}
22
+ >
23
+ <Datagrid rowClick="edit" bulkActionButtons={false}>
24
+ <EmailField source="email" />
25
+ <TextField source="phone_number" />
26
+ <TextField source="connection" />
27
+ <TextField source="login_count" />
28
+ <FunctionField
29
+ label="Last login"
30
+ render={(record: any) => <DateAgo date={record.last_login} />}
31
+ />
32
+ </Datagrid>
33
+ </List>
34
+ );
35
+ }
package/src/data.json ADDED
@@ -0,0 +1,121 @@
1
+ {
2
+ "posts": [
3
+ {
4
+ "id": 0,
5
+ "title": "Post 1",
6
+ "content": "Content 1"
7
+ },
8
+ {
9
+ "id": 1,
10
+ "title": "Post 2",
11
+ "content": "Content 2"
12
+ },
13
+ {
14
+ "id": 2,
15
+ "title": "Post 3",
16
+ "content": "Content 3"
17
+ },
18
+ {
19
+ "id": 3,
20
+ "title": "Post 4",
21
+ "content": "Content 4"
22
+ },
23
+ {
24
+ "id": 4,
25
+ "title": "Post 5",
26
+ "content": "Content 5"
27
+ },
28
+ {
29
+ "id": 5,
30
+ "title": "Post 6",
31
+ "content": "Content 6"
32
+ },
33
+ {
34
+ "id": 6,
35
+ "title": "Post 7",
36
+ "content": "Content 7"
37
+ },
38
+ {
39
+ "id": 7,
40
+ "title": "Post 8",
41
+ "content": "Content 8"
42
+ },
43
+ {
44
+ "id": 8,
45
+ "title": "Post 9",
46
+ "content": "Content 9"
47
+ },
48
+ {
49
+ "id": 9,
50
+ "title": "Post 10",
51
+ "content": "Content 10"
52
+ },
53
+ {
54
+ "id": 10,
55
+ "title": "Post 11",
56
+ "content": "Content 11"
57
+ },
58
+ {
59
+ "id": 11,
60
+ "title": "Post 12",
61
+ "content": "Content 12"
62
+ }
63
+ ],
64
+ "comments": [
65
+ {
66
+ "id": 0,
67
+ "postId": 0,
68
+ "content": "Comment 1"
69
+ },
70
+ {
71
+ "id": 1,
72
+ "postId": 0,
73
+ "content": "Comment 2"
74
+ },
75
+ {
76
+ "id": 2,
77
+ "postId": 1,
78
+ "content": "Comment 3"
79
+ },
80
+ {
81
+ "id": 3,
82
+ "postId": 1,
83
+ "content": "Comment 4"
84
+ },
85
+ {
86
+ "id": 4,
87
+ "postId": 2,
88
+ "content": "Comment 5"
89
+ },
90
+ {
91
+ "id": 5,
92
+ "postId": 2,
93
+ "content": "Comment 6"
94
+ },
95
+ {
96
+ "id": 6,
97
+ "postId": 3,
98
+ "content": "Comment 7"
99
+ },
100
+ {
101
+ "id": 7,
102
+ "postId": 3,
103
+ "content": "Comment 8"
104
+ },
105
+ {
106
+ "id": 8,
107
+ "postId": 3,
108
+ "content": "Comment 9"
109
+ },
110
+ {
111
+ "id": 9,
112
+ "postId": 4,
113
+ "content": "Comment 10"
114
+ },
115
+ {
116
+ "id": 10,
117
+ "postId": 4,
118
+ "content": "Comment 11"
119
+ }
120
+ ]
121
+ }
@@ -0,0 +1,97 @@
1
+ import { UpdateParams, withLifecycleCallbacks } from "react-admin";
2
+ import { authorizedHttpClient } from "./authProvider";
3
+ import auth0DataProvider from "./auth0DataProvider";
4
+ import {
5
+ getDomainFromStorage,
6
+ buildUrlWithProtocol,
7
+ } from "./utils/domainUtils";
8
+
9
+ async function removeExtraFields(params: UpdateParams) {
10
+ delete params.data?.id;
11
+ delete params.data?.tenant_id;
12
+ delete params.data?.updated_at;
13
+ delete params.data?.created_at;
14
+
15
+ // Remove empty properties
16
+ Object.keys(params.data).forEach((key) => {
17
+ if (params.data[key] === undefined) {
18
+ delete params.data[key];
19
+ }
20
+ });
21
+
22
+ return params;
23
+ }
24
+
25
+ export function getDataprovider(auth0Domain?: string) {
26
+ // Create the complete base URL using the selected domain
27
+ let baseUrl = import.meta.env.VITE_SIMPLE_REST_URL;
28
+
29
+ if (auth0Domain) {
30
+ // Check if there's a custom REST API URL configured for this domain
31
+ const domains = getDomainFromStorage();
32
+ const domainConfig = domains.find((d) => d.url === auth0Domain);
33
+
34
+ if (domainConfig?.restApiUrl) {
35
+ // Use the custom REST API URL if configured
36
+ baseUrl = domainConfig.restApiUrl;
37
+ } else {
38
+ // Otherwise use the auth domain with HTTPS
39
+ baseUrl = buildUrlWithProtocol(auth0Domain);
40
+ }
41
+ }
42
+
43
+ // TODO - duplicate auth0DataProvider to tenantsDataProvider
44
+ // we are introducing non-auth0 endpoints AND we don't require the tenants-id header
45
+ const provider = auth0DataProvider(
46
+ baseUrl,
47
+ authorizedHttpClient,
48
+ undefined,
49
+ auth0Domain,
50
+ );
51
+
52
+ return withLifecycleCallbacks(provider, [
53
+ {
54
+ resource: "tenants",
55
+ beforeUpdate: removeExtraFields,
56
+ },
57
+ ]);
58
+ }
59
+
60
+ export function getDataproviderForTenant(
61
+ tenantId: string,
62
+ auth0Domain?: string,
63
+ ) {
64
+ // Start with a default API URL
65
+ let apiUrl;
66
+
67
+ if (auth0Domain) {
68
+ // Check if there's a custom REST API URL configured for this domain
69
+ const domains = getDomainFromStorage();
70
+
71
+ const domainConfig = domains.find((d) => d.url === auth0Domain);
72
+
73
+ if (domainConfig?.restApiUrl) {
74
+ // Use the custom REST API URL if configured
75
+ apiUrl = domainConfig.restApiUrl;
76
+ } else {
77
+ // Otherwise construct an API URL using the auth0Domain with HTTPS
78
+ apiUrl = buildUrlWithProtocol(auth0Domain);
79
+ }
80
+ } else {
81
+ // Fallback to the environment variable
82
+ apiUrl = import.meta.env.VITE_AUTH0_API_URL;
83
+ }
84
+
85
+ // Ensure apiUrl doesn't end with a slash
86
+ apiUrl = apiUrl.replace(/\/$/, "");
87
+
88
+ // Create the auth0Provider with the API URL, tenant ID, and domain
89
+ const auth0Provider = auth0DataProvider(
90
+ apiUrl,
91
+ authorizedHttpClient,
92
+ tenantId,
93
+ auth0Domain,
94
+ );
95
+
96
+ return auth0Provider;
97
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,106 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { BrowserRouter } from "react-router-dom";
4
+ import { App } from "./App";
5
+ import { TenantsApp } from "./TenantsApp";
6
+ import { AuthCallback } from "./AuthCallback";
7
+ import { DomainSelector } from "./components/DomainSelector";
8
+ import { getSelectedDomainFromStorage } from "./utils/domainUtils";
9
+
10
+ // Check if running on local.authhero.net - if so, auto-connect to localhost:3000
11
+ const isLocalDevelopment = window.location.hostname.startsWith("local.");
12
+ const LOCAL_DOMAIN = "localhost:3000";
13
+
14
+ function Root() {
15
+ const [selectedDomain, setSelectedDomain] = useState<string | null>(
16
+ isLocalDevelopment ? LOCAL_DOMAIN : null,
17
+ );
18
+ const currentPath = location.pathname;
19
+ const isAuthCallback = currentPath === "/auth-callback";
20
+ const isRootPath = currentPath === "/";
21
+ // Only match /tenants exactly or /tenants/create (not /tenants/:id which would be a tenant admin route)
22
+ const isTenantsPath =
23
+ currentPath === "/tenants" ||
24
+ currentPath.startsWith("/tenants/create") ||
25
+ currentPath === "/tenants/";
26
+
27
+ // Load domain from cookies on component mount (skip for local development)
28
+ useEffect(() => {
29
+ if (isLocalDevelopment) {
30
+ // For local development, always use localhost:3000
31
+ return;
32
+ }
33
+ const savedDomain = getSelectedDomainFromStorage();
34
+ if (savedDomain) {
35
+ setSelectedDomain(savedDomain);
36
+ }
37
+ }, []);
38
+
39
+ // Handle auth callback separately without basename
40
+ if (isAuthCallback) {
41
+ return (
42
+ <React.StrictMode>
43
+ <BrowserRouter>
44
+ <AuthCallback onAuthComplete={() => {}} />
45
+ </BrowserRouter>
46
+ </React.StrictMode>
47
+ );
48
+ }
49
+
50
+ // Show domain selector on root path or if no domain is selected
51
+ // Skip for local development - redirect to /tenants instead
52
+ if (!isLocalDevelopment && (isRootPath || !selectedDomain)) {
53
+ return (
54
+ <DomainSelector
55
+ onDomainSelected={(domain) => setSelectedDomain(domain)}
56
+ disableCloseOnRootPath={isRootPath}
57
+ />
58
+ );
59
+ }
60
+
61
+ // For local development on root path, redirect to /tenants
62
+ if (isLocalDevelopment && isRootPath) {
63
+ window.location.href = "/tenants";
64
+ return null;
65
+ }
66
+
67
+ // Handle tenants management routes without basename
68
+ if (isTenantsPath) {
69
+ return (
70
+ <React.StrictMode>
71
+ <BrowserRouter>
72
+ <TenantsApp initialDomain={selectedDomain || ""} />
73
+ </BrowserRouter>
74
+ </React.StrictMode>
75
+ );
76
+ }
77
+
78
+ // Handle tenant-specific routes
79
+ const pathSegments = currentPath.split("/").filter(Boolean);
80
+ const tenantId = pathSegments[0];
81
+
82
+ if (tenantId) {
83
+ return (
84
+ <React.StrictMode>
85
+ <BrowserRouter basename={`/${tenantId}`}>
86
+ <App tenantId={tenantId} initialDomain={selectedDomain || ""} />
87
+ </BrowserRouter>
88
+ </React.StrictMode>
89
+ );
90
+ }
91
+
92
+ // Fallback to domain selector (or redirect for local development)
93
+ if (isLocalDevelopment) {
94
+ window.location.href = "/tenants";
95
+ return null;
96
+ }
97
+
98
+ return (
99
+ <DomainSelector
100
+ onDomainSelected={(domain) => setSelectedDomain(domain)}
101
+ disableCloseOnRootPath={false}
102
+ />
103
+ );
104
+ }
105
+
106
+ ReactDOM.createRoot(document.getElementById("root")!).render(<Root />);
@@ -0,0 +1,21 @@
1
+ // Subset of LogTypes used in react-admin app
2
+ // Values match those defined in @authhero/adapter-interfaces
3
+ export const LogTypes = {
4
+ SUCCESS_API_OPERATION: "sapi",
5
+ SUCCESS_SILENT_AUTH: "ssa",
6
+ FAILED_SILENT_AUTH: "fsa",
7
+ SUCCESS_SIGNUP: "ss",
8
+ FAILED_SIGNUP: "fs",
9
+ SUCCESS_LOGIN: "s",
10
+ FAILED_LOGIN_INCORRECT_PASSWORD: "fp",
11
+ FAILED_LOGIN_INVALID_EMAIL_USERNAME: "fu",
12
+ SUCCESS_LOGOUT: "slo",
13
+ SUCCESS_CROSS_ORIGIN_AUTHENTICATION: "scoa",
14
+ FAILED_CROSS_ORIGIN_AUTHENTICATION: "fcoa",
15
+ CODE_LINK_SENT: "cls", // Updated to match the main schema
16
+ FAILED_LOGIN: "f",
17
+ SUCCESS_EXCHANGE_AUTHORIZATION_CODE_FOR_ACCESS_TOKEN: "seacft",
18
+ FAILED_EXCHANGE_AUTHORIZATION_CODE_FOR_ACCESS_TOKEN: "feacft",
19
+ } as const;
20
+
21
+ export type LogTypes = (typeof LogTypes)[keyof typeof LogTypes];
@@ -0,0 +1,86 @@
1
+ declare module "reactflow" {
2
+ export interface NodeProps<T = Record<string, unknown>> {
3
+ id: string;
4
+ data: T;
5
+ position: { x: number; y: number };
6
+ type?: string;
7
+ className?: string;
8
+ style?: React.CSSProperties;
9
+ }
10
+
11
+ export interface Connection {
12
+ source: string;
13
+ target: string;
14
+ sourceHandle?: string;
15
+ targetHandle?: string;
16
+ }
17
+
18
+ export interface EdgeProps {
19
+ id: string;
20
+ source: string;
21
+ target: string;
22
+ type?: string;
23
+ label?: string;
24
+ animated?: boolean;
25
+ style?: React.CSSProperties;
26
+ }
27
+
28
+ export interface NodeChange {
29
+ id: string;
30
+ type: string;
31
+ position?: { x: number; y: number };
32
+ // Add other properties as needed
33
+ }
34
+
35
+ export interface EdgeChange {
36
+ id: string;
37
+ type: string;
38
+ // Add other properties as needed
39
+ }
40
+
41
+ export type Node = NodeProps;
42
+ export type Edge = EdgeProps;
43
+
44
+ export enum ConnectionLineType {
45
+ Bezier = "bezier",
46
+ Step = "step",
47
+ SmoothStep = "smoothstep",
48
+ Straight = "straight",
49
+ }
50
+
51
+ export function applyNodeChanges(
52
+ changes: NodeChange[],
53
+ nodes: Node[],
54
+ ): Node[];
55
+ export function applyEdgeChanges(
56
+ changes: EdgeChange[],
57
+ edges: Edge[],
58
+ ): Edge[];
59
+
60
+ // Define ReactFlow component
61
+ export interface ReactFlowProps {
62
+ nodes: Node[];
63
+ edges: Edge[];
64
+ onNodesChange?: (changes: NodeChange[]) => void;
65
+ onEdgesChange?: (changes: EdgeChange[]) => void;
66
+ onConnect?: (connection: Connection) => void;
67
+ nodeTypes?: Record<string, React.ComponentType<any>>;
68
+ edgeTypes?: Record<string, React.ComponentType<any>>;
69
+ connectionLineType?: ConnectionLineType;
70
+ fitView?: boolean;
71
+ children?: React.ReactNode;
72
+ }
73
+
74
+ // Export ReactFlowProvider component
75
+ export const ReactFlowProvider: React.FC<{ children: React.ReactNode }>;
76
+
77
+ // Export the Controls component
78
+ export const Controls: React.FC<any>;
79
+
80
+ // Export the Background component
81
+ export const Background: React.FC<any>;
82
+
83
+ // Default export
84
+ const ReactFlow: React.FC<ReactFlowProps>;
85
+ export default ReactFlow;
86
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Shared utility functions for domain management
3
+ */
4
+
5
+ // Storage key constants
6
+ export const DOMAINS_STORAGE_KEY = "authhero_domains";
7
+ export const SELECTED_DOMAIN_STORAGE_KEY = "authhero_selected_domain";
8
+
9
+ // Connection method types
10
+ export type ConnectionMethod = "login" | "token" | "client_credentials";
11
+
12
+ // Domain configuration interface
13
+ export interface DomainConfig {
14
+ url: string;
15
+ connectionMethod: ConnectionMethod;
16
+ // Login method fields
17
+ clientId?: string;
18
+ restApiUrl?: string;
19
+ // Token method field
20
+ token?: string;
21
+ // Client credentials method fields
22
+ clientSecret?: string;
23
+ }
24
+
25
+ /**
26
+ * Gets domains from localStorage
27
+ * Handles both formats (array of objects or array of strings) for backward compatibility
28
+ */
29
+ export const getDomainFromStorage = (): DomainConfig[] => {
30
+ try {
31
+ const storedValue = localStorage.getItem(DOMAINS_STORAGE_KEY);
32
+ if (!storedValue) return [];
33
+
34
+ const parsedData = JSON.parse(storedValue);
35
+
36
+ // Handle both formats: array of objects with url property or array of strings
37
+ if (Array.isArray(parsedData)) {
38
+ return parsedData
39
+ .filter((item) => item !== null && item !== undefined)
40
+ .map((item) => {
41
+ if (typeof item === "object" && item !== null && "url" in item) {
42
+ // Add connectionMethod if it doesn't exist (for backward compatibility)
43
+ if (!("connectionMethod" in item)) {
44
+ return {
45
+ ...item,
46
+ connectionMethod: "login" as ConnectionMethod, // Assume login for existing entries
47
+ } as DomainConfig;
48
+ }
49
+ return item as DomainConfig;
50
+ } else {
51
+ // Convert string domains to DomainConfig format (backward compatibility)
52
+ return {
53
+ url: String(item),
54
+ connectionMethod: "login" as ConnectionMethod,
55
+ clientId: "", // Empty clientId for legacy entries
56
+ };
57
+ }
58
+ })
59
+ .filter((domain) => domain.url.trim() !== ""); // Remove empty domains
60
+ }
61
+ return [];
62
+ } catch (e) {
63
+ console.error("Failed to parse domains from localStorage", e);
64
+ return [];
65
+ }
66
+ };
67
+
68
+ /**
69
+ * Saves domains to localStorage
70
+ */
71
+ export const saveDomainToStorage = (domains: DomainConfig[]): void => {
72
+ try {
73
+ console.log("Saving domains to localStorage:", domains);
74
+ localStorage.setItem(DOMAINS_STORAGE_KEY, JSON.stringify(domains));
75
+ } catch (e) {
76
+ console.error("Failed to save domains to localStorage", e);
77
+ }
78
+ };
79
+
80
+ /**
81
+ * Gets the selected domain from localStorage
82
+ * Falls back to first domain in storage if no selected domain is set
83
+ */
84
+ export const getSelectedDomainFromStorage = (): string => {
85
+ try {
86
+ const selectedDomain = localStorage.getItem(SELECTED_DOMAIN_STORAGE_KEY);
87
+ if (selectedDomain) return selectedDomain;
88
+
89
+ // Fallback to first domain in storage if no selected domain
90
+ const domains = getDomainFromStorage();
91
+ return domains[0]?.url || "";
92
+ } catch (e) {
93
+ console.error("Failed to get selected domain from localStorage", e);
94
+ return "";
95
+ }
96
+ };
97
+
98
+ /**
99
+ * Saves the selected domain to localStorage
100
+ */
101
+ export const saveSelectedDomainToStorage = (domain: string): void => {
102
+ try {
103
+ console.log("Saving selected domain to localStorage:", domain);
104
+ localStorage.setItem(SELECTED_DOMAIN_STORAGE_KEY, domain);
105
+ } catch (e) {
106
+ console.error("Failed to save selected domain to localStorage", e);
107
+ }
108
+ };
109
+
110
+ /**
111
+ * Gets client ID for a specific domain
112
+ * Falls back to environment variable if not found
113
+ */
114
+ export const getClientIdFromStorage = (domain: string): string => {
115
+ // Ensure domain is properly formatted for comparison
116
+ const formattedDomain = formatDomain(domain);
117
+
118
+ const domains = getDomainFromStorage();
119
+
120
+ // Look for matching domain in the array
121
+ for (const d of domains) {
122
+ if (d.url === formattedDomain && d.clientId) {
123
+ return d.clientId;
124
+ }
125
+ }
126
+
127
+ // Fallback to environment variable
128
+ const fallbackClientId = import.meta.env.VITE_AUTH0_CLIENT_ID;
129
+ return fallbackClientId;
130
+ };
131
+
132
+ /**
133
+ * Formats a domain string (removes protocol if present)
134
+ */
135
+ export const formatDomain = (domain: string): string => {
136
+ return domain.trim().replace(/^https?:\/\//, "");
137
+ };
138
+
139
+ /**
140
+ * Constructs a full URL with HTTPS protocol
141
+ * - If domain starts with "local.", connects to https://localhost:3000
142
+ * - Always uses https:// for all domains (including localhost with self-signed certs)
143
+ * - Preserves existing https:// protocol if already present
144
+ * - Converts http:// to https://
145
+ */
146
+ export const buildUrlWithProtocol = (domain: string): string => {
147
+ const trimmedDomain = domain.trim();
148
+
149
+ // Extract hostname without protocol for local. check
150
+ const hostnameOnly = trimmedDomain.replace(/^https?:\/\//, "");
151
+
152
+ // If hostname starts with "local.", redirect to local development server
153
+ if (hostnameOnly.startsWith("local.")) {
154
+ return "https://localhost:3000";
155
+ }
156
+
157
+ // Check if it already has a protocol
158
+ if (trimmedDomain.startsWith("https://")) {
159
+ return trimmedDomain;
160
+ }
161
+
162
+ // Convert http:// to https://
163
+ if (trimmedDomain.startsWith("http://")) {
164
+ return trimmedDomain.replace("http://", "https://");
165
+ }
166
+
167
+ // No protocol specified - add https://
168
+ return `https://${trimmedDomain}`;
169
+ };