@authhero/react-admin 0.18.0 → 0.19.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 +13 -0
- package/package.json +4 -3
- package/src/App.tsx +60 -3
- package/src/TenantsApp.tsx +84 -2
- package/src/authProvider.ts +7 -5
- package/src/components/TenantAppBar.tsx +18 -2
- package/src/components/branding/BrandingPreview.tsx +421 -0
- package/src/components/branding/edit.tsx +67 -25
- package/src/dataProvider.ts +33 -13
- package/vercel.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @authhero/react-admin
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 47fe928: Refactor create authhero
|
|
8
|
+
- f4b74e7: Add widget to react-admin
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- Updated dependencies [f4b74e7]
|
|
13
|
+
- Updated dependencies [b6d3411]
|
|
14
|
+
- @authhero/widget@0.5.0
|
|
15
|
+
|
|
3
16
|
## 0.18.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@authhero/react-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"packageManager": "pnpm@10.20.0",
|
|
4
5
|
"private": false,
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
@@ -8,6 +9,8 @@
|
|
|
8
9
|
},
|
|
9
10
|
"dependencies": {
|
|
10
11
|
"@auth0/auth0-spa-js": "^2.1.3",
|
|
12
|
+
"@authhero/adapter-interfaces": "^0.116.0",
|
|
13
|
+
"@authhero/widget": "^0.4.1",
|
|
11
14
|
"@mui/icons-material": "^7.1.0",
|
|
12
15
|
"@mui/material": "^7.1.0",
|
|
13
16
|
"@tiptap/extension-link": "^3.13.0",
|
|
@@ -43,13 +46,11 @@
|
|
|
43
46
|
"eslint-plugin-react": "^7.37.5",
|
|
44
47
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
45
48
|
"jsdom": "^26.1.0",
|
|
46
|
-
"pnpm": "^8.15.3",
|
|
47
49
|
"prettier": "^3.5.3",
|
|
48
50
|
"typescript": "^5.8.3",
|
|
49
51
|
"vite": "^6.3.5",
|
|
50
52
|
"vitest": "^3.1.3"
|
|
51
53
|
},
|
|
52
|
-
"packageManager": "pnpm@8.15.3",
|
|
53
54
|
"scripts": {
|
|
54
55
|
"dev": "vite",
|
|
55
56
|
"build": "pnpm install --no-frozen-lockfile && vite build",
|
package/src/App.tsx
CHANGED
|
@@ -43,12 +43,13 @@ import DnsIcon from "@mui/icons-material/Dns";
|
|
|
43
43
|
import PaletteIcon from "@mui/icons-material/Palette";
|
|
44
44
|
import StorageIcon from "@mui/icons-material/Storage";
|
|
45
45
|
import AccountTreeIcon from "@mui/icons-material/AccountTree";
|
|
46
|
-
import { useMemo, useState } from "react";
|
|
46
|
+
import { useMemo, useState, useEffect } from "react";
|
|
47
47
|
import { RoleCreate, RoleEdit, RoleList } from "./components/roles";
|
|
48
48
|
import SecurityIcon from "@mui/icons-material/Security";
|
|
49
49
|
import { SettingsList, SettingsEdit } from "./components/settings";
|
|
50
50
|
import { CertificateErrorDialog } from "./components/CertificateErrorDialog";
|
|
51
51
|
import { ActivityDashboard } from "./components/activity";
|
|
52
|
+
import { buildUrlWithProtocol } from "./utils/domainUtils";
|
|
52
53
|
|
|
53
54
|
interface AppProps {
|
|
54
55
|
tenantId: string;
|
|
@@ -58,11 +59,61 @@ interface AppProps {
|
|
|
58
59
|
|
|
59
60
|
export function App(props: AppProps) {
|
|
60
61
|
const [certErrorUrl, setCertErrorUrl] = useState<string | null>(null);
|
|
61
|
-
|
|
62
|
+
|
|
62
63
|
// Use a default domain for now - in the working project, domain selection might be handled differently
|
|
63
64
|
const selectedDomain =
|
|
64
65
|
props.initialDomain || import.meta.env.VITE_AUTH0_DOMAIN || "";
|
|
65
66
|
|
|
67
|
+
// Check if we've already verified single-tenant mode for THIS domain
|
|
68
|
+
// The flag is stored as "domain|true" or "domain|false" to ensure we re-check when domain changes
|
|
69
|
+
// Using | as separator since domains can contain : (e.g., localhost:3000)
|
|
70
|
+
const storedFlag = sessionStorage.getItem('isSingleTenant');
|
|
71
|
+
const separatorIndex = storedFlag?.lastIndexOf('|') ?? -1;
|
|
72
|
+
const [storedDomain, storedValue] = separatorIndex > -1
|
|
73
|
+
? [storedFlag!.substring(0, separatorIndex), storedFlag!.substring(separatorIndex + 1)]
|
|
74
|
+
: [null, storedFlag];
|
|
75
|
+
|
|
76
|
+
const [isSingleTenantChecked, setIsSingleTenantChecked] = useState<boolean>(
|
|
77
|
+
// Only skip check if we have a flag for the SAME domain
|
|
78
|
+
storedDomain === selectedDomain && storedValue !== null
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Check for single-tenant mode on mount if not already checked
|
|
82
|
+
// This handles the case where user navigates directly to /{tenantId} without going through /tenants
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (isSingleTenantChecked || !selectedDomain) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const checkSingleTenant = async () => {
|
|
89
|
+
const apiUrl = buildUrlWithProtocol(selectedDomain);
|
|
90
|
+
try {
|
|
91
|
+
// Try to fetch the tenants endpoint - if it exists, we're in multi-tenant mode
|
|
92
|
+
const response = await fetch(`${apiUrl}/api/v2/tenants?per_page=1`, {
|
|
93
|
+
method: 'GET',
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// If we get a 401/403 (auth required) or 200, the endpoint exists -> multi-tenant
|
|
100
|
+
// If we get a 404, the endpoint doesn't exist -> single-tenant
|
|
101
|
+
if (response.status === 404) {
|
|
102
|
+
// Store domain|value so we know which domain was checked (using | since domain can contain :)
|
|
103
|
+
sessionStorage.setItem('isSingleTenant', `${selectedDomain}|true`);
|
|
104
|
+
} else {
|
|
105
|
+
sessionStorage.setItem('isSingleTenant', `${selectedDomain}|false`);
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Network error or endpoint doesn't exist -> assume single-tenant
|
|
109
|
+
sessionStorage.setItem('isSingleTenant', `${selectedDomain}|true`);
|
|
110
|
+
}
|
|
111
|
+
setIsSingleTenantChecked(true);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
checkSingleTenant();
|
|
115
|
+
}, [selectedDomain, isSingleTenantChecked]);
|
|
116
|
+
|
|
66
117
|
// Use memoization for creating the auth provider to prevent re-authentication
|
|
67
118
|
const authProvider = useMemo(() => {
|
|
68
119
|
if (!selectedDomain) return null;
|
|
@@ -71,6 +122,7 @@ export function App(props: AppProps) {
|
|
|
71
122
|
|
|
72
123
|
// Memoize the data provider to prevent unnecessary re-creations
|
|
73
124
|
// Wrap it to catch certificate errors
|
|
125
|
+
// Re-create when isSingleTenantChecked changes to pick up the new sessionStorage value
|
|
74
126
|
const dataProvider = useMemo(() => {
|
|
75
127
|
const baseProvider = getDataproviderForTenant(
|
|
76
128
|
props.tenantId,
|
|
@@ -97,12 +149,17 @@ export function App(props: AppProps) {
|
|
|
97
149
|
}
|
|
98
150
|
}
|
|
99
151
|
return wrappedProvider;
|
|
100
|
-
}, [props.tenantId, selectedDomain]);
|
|
152
|
+
}, [props.tenantId, selectedDomain, isSingleTenantChecked]);
|
|
101
153
|
|
|
102
154
|
const handleCloseCertError = () => {
|
|
103
155
|
setCertErrorUrl(null);
|
|
104
156
|
};
|
|
105
157
|
|
|
158
|
+
// If not done checking single-tenant mode, show loading
|
|
159
|
+
if (!isSingleTenantChecked) {
|
|
160
|
+
return <div>Checking tenant mode...</div>;
|
|
161
|
+
}
|
|
162
|
+
|
|
106
163
|
// If no domain is selected, show a loading state
|
|
107
164
|
if (!authProvider || !selectedDomain) {
|
|
108
165
|
return <div>Loading...</div>;
|
package/src/TenantsApp.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Admin, Resource } from "react-admin";
|
|
2
2
|
import { getDataprovider } from "./dataProvider";
|
|
3
|
-
import { getAuthProvider } from "./authProvider";
|
|
3
|
+
import { getAuthProvider, createAuth0Client } from "./authProvider";
|
|
4
4
|
import { TenantsList } from "./components/tenants/list";
|
|
5
5
|
import { TenantsCreate } from "./components/tenants/create";
|
|
6
|
-
import { useMemo, useState } from "react";
|
|
6
|
+
import { useMemo, useState, useEffect } from "react";
|
|
7
7
|
import { Button } from "@mui/material";
|
|
8
8
|
import { DomainSelector } from "./components/DomainSelector";
|
|
9
9
|
import { saveSelectedDomainToStorage } from "./utils/domainUtils";
|
|
@@ -24,6 +24,8 @@ export function TenantsApp(props: TenantsAppProps = {}) {
|
|
|
24
24
|
);
|
|
25
25
|
const [showDomainDialog, setShowDomainDialog] = useState<boolean>(false);
|
|
26
26
|
const [certErrorUrl, setCertErrorUrl] = useState<string | null>(null);
|
|
27
|
+
const [isCheckingSingleTenant, setIsCheckingSingleTenant] =
|
|
28
|
+
useState<boolean>(true);
|
|
27
29
|
|
|
28
30
|
// Use useMemo to prevent recreating the auth provider on every render
|
|
29
31
|
const authProvider = useMemo(
|
|
@@ -60,6 +62,70 @@ export function TenantsApp(props: TenantsAppProps = {}) {
|
|
|
60
62
|
return wrappedProvider;
|
|
61
63
|
}, [selectedDomain]);
|
|
62
64
|
|
|
65
|
+
// Check for single tenant mode on mount
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!selectedDomain) {
|
|
68
|
+
setIsCheckingSingleTenant(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Try to fetch tenants list
|
|
73
|
+
dataProvider
|
|
74
|
+
.getList("tenants", {
|
|
75
|
+
pagination: { page: 1, perPage: 2 }, // Only need to know if there's 1 or more
|
|
76
|
+
sort: { field: "id", order: "ASC" },
|
|
77
|
+
filter: {},
|
|
78
|
+
})
|
|
79
|
+
.then((result) => {
|
|
80
|
+
// Multi-tenant mode - tenants endpoint exists
|
|
81
|
+
// Mark as multi-tenant and show the tenants list (don't auto-redirect)
|
|
82
|
+
sessionStorage.setItem("isSingleTenant", `${selectedDomain}|false`);
|
|
83
|
+
setIsCheckingSingleTenant(false);
|
|
84
|
+
})
|
|
85
|
+
.catch(async (error) => {
|
|
86
|
+
console.log("Tenants endpoint check:", error);
|
|
87
|
+
// If we get a 404 or any error, the tenants endpoint doesn't exist
|
|
88
|
+
// In single-tenant mode without multi-tenancy package, the endpoint won't exist
|
|
89
|
+
|
|
90
|
+
// Mark as single-tenant mode immediately (before trying to fetch settings)
|
|
91
|
+
// This ensures subsequent requests won't try to use organization tokens
|
|
92
|
+
sessionStorage.setItem("isSingleTenant", `${selectedDomain}|true`);
|
|
93
|
+
|
|
94
|
+
// Try to use the /tenants/settings endpoint which works in single-tenant mode
|
|
95
|
+
// We need to get a token and make a direct fetch to avoid organization logic
|
|
96
|
+
try {
|
|
97
|
+
const apiUrl = selectedDomain.startsWith("http")
|
|
98
|
+
? selectedDomain
|
|
99
|
+
: `https://${selectedDomain}`;
|
|
100
|
+
|
|
101
|
+
// Get a non-org token
|
|
102
|
+
const auth0Client = createAuth0Client(selectedDomain);
|
|
103
|
+
const token = await auth0Client.getTokenSilently();
|
|
104
|
+
|
|
105
|
+
const response = await fetch(`${apiUrl}/api/v2/tenants/settings`, {
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
Authorization: `Bearer ${token}`,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (response.ok) {
|
|
113
|
+
const settings = await response.json();
|
|
114
|
+
if (settings?.id) {
|
|
115
|
+
window.location.href = `/${settings.id}`;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (settingsError) {
|
|
120
|
+
console.log("Settings endpoint also failed:", settingsError);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// If both endpoints fail, clear the flag and show the tenants list (which will show an error)
|
|
124
|
+
sessionStorage.removeItem("isSingleTenant");
|
|
125
|
+
setIsCheckingSingleTenant(false);
|
|
126
|
+
});
|
|
127
|
+
}, [selectedDomain, dataProvider]);
|
|
128
|
+
|
|
63
129
|
const openDomainManager = () => {
|
|
64
130
|
setShowDomainDialog(true);
|
|
65
131
|
};
|
|
@@ -74,6 +140,22 @@ export function TenantsApp(props: TenantsAppProps = {}) {
|
|
|
74
140
|
setCertErrorUrl(null);
|
|
75
141
|
};
|
|
76
142
|
|
|
143
|
+
// Show loading while checking for single tenant
|
|
144
|
+
if (isCheckingSingleTenant) {
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
style={{
|
|
148
|
+
display: "flex",
|
|
149
|
+
justifyContent: "center",
|
|
150
|
+
alignItems: "center",
|
|
151
|
+
height: "100vh",
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
Checking tenant configuration...
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
77
159
|
// Create the domain selector button that will be passed to the AppBar
|
|
78
160
|
const DomainSelectorButton = (
|
|
79
161
|
<Button
|
package/src/authProvider.ts
CHANGED
|
@@ -189,8 +189,12 @@ export const createManagementClient = async (
|
|
|
189
189
|
|
|
190
190
|
let token: string;
|
|
191
191
|
|
|
192
|
-
if
|
|
193
|
-
|
|
192
|
+
// Check if we're in single-tenant mode
|
|
193
|
+
const storedFlag = sessionStorage.getItem("isSingleTenant");
|
|
194
|
+
const isSingleTenant = storedFlag?.endsWith("|true") || storedFlag === "true";
|
|
195
|
+
|
|
196
|
+
if (tenantId && !isSingleTenant) {
|
|
197
|
+
// When accessing tenant-specific resources in MULTI-TENANT mode, use org-scoped token
|
|
194
198
|
if (domainConfig.connectionMethod === "login") {
|
|
195
199
|
// For OAuth login, use organization-scoped client
|
|
196
200
|
const orgAuth0Client = createAuth0ClientForOrg(domainForAuth, tenantId);
|
|
@@ -221,9 +225,7 @@ export const createManagementClient = async (
|
|
|
221
225
|
});
|
|
222
226
|
|
|
223
227
|
// This won't be reached as loginWithRedirect redirects the page
|
|
224
|
-
throw new Error(
|
|
225
|
-
`Redirecting to login for organization ${tenantId}`,
|
|
226
|
-
);
|
|
228
|
+
throw new Error(`Redirecting to login for organization ${tenantId}`);
|
|
227
229
|
}
|
|
228
230
|
} else {
|
|
229
231
|
// For token/client_credentials, use getOrganizationToken
|
|
@@ -66,8 +66,24 @@ export function TenantAppBar(props: TenantAppBarProps) {
|
|
|
66
66
|
} as TenantResponse);
|
|
67
67
|
}
|
|
68
68
|
})
|
|
69
|
-
.catch((error) => {
|
|
70
|
-
console.error("Failed to fetch tenant:", error);
|
|
69
|
+
.catch(async (error) => {
|
|
70
|
+
console.error("Failed to fetch tenant list:", error);
|
|
71
|
+
|
|
72
|
+
// In single-tenant mode, the tenants list endpoint might not exist
|
|
73
|
+
// Try to fetch from the settings endpoint instead
|
|
74
|
+
try {
|
|
75
|
+
const settings = await tenantsDataProvider.getOne("tenants", {
|
|
76
|
+
id: "settings",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (settings?.data && settings.data.id === tenantId) {
|
|
80
|
+
setTenant(settings.data as TenantResponse);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
} catch (settingsError) {
|
|
84
|
+
console.error("Failed to fetch tenant settings:", settingsError);
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
// Set a minimal tenant object on error
|
|
72
88
|
setTenant({
|
|
73
89
|
id: tenantId,
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useWatch } from "react-hook-form";
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
Typography,
|
|
6
|
+
Paper,
|
|
7
|
+
ToggleButtonGroup,
|
|
8
|
+
ToggleButton,
|
|
9
|
+
} from "@mui/material";
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { defineCustomElements } from "@authhero/widget/loader";
|
|
12
|
+
|
|
13
|
+
// Initialize the widget custom elements
|
|
14
|
+
if (typeof window !== "undefined") {
|
|
15
|
+
defineCustomElements(window);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Types for the widget screen configuration
|
|
19
|
+
interface FormComponent {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
category: "FIELD" | "BLOCK" | "WIDGET";
|
|
23
|
+
visible: boolean;
|
|
24
|
+
label?: string;
|
|
25
|
+
config?: Record<string, unknown>;
|
|
26
|
+
required?: boolean;
|
|
27
|
+
sensitive?: boolean;
|
|
28
|
+
order: number;
|
|
29
|
+
messages?: Array<{ text: string; type: "error" | "success" }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ScreenLink {
|
|
33
|
+
id?: string;
|
|
34
|
+
text: string;
|
|
35
|
+
linkText?: string;
|
|
36
|
+
href: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface UiScreen {
|
|
40
|
+
action: string;
|
|
41
|
+
method: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
components: FormComponent[];
|
|
45
|
+
links?: ScreenLink[];
|
|
46
|
+
messages?: Array<{ text: string; type: "error" | "success" }>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Sample screen to preview
|
|
50
|
+
const sampleScreen: UiScreen = {
|
|
51
|
+
action: "#",
|
|
52
|
+
method: "POST",
|
|
53
|
+
title: "Welcome",
|
|
54
|
+
description: "Sign in to continue",
|
|
55
|
+
components: [
|
|
56
|
+
{
|
|
57
|
+
id: "social-buttons",
|
|
58
|
+
type: "SOCIAL",
|
|
59
|
+
category: "FIELD",
|
|
60
|
+
visible: true,
|
|
61
|
+
config: {
|
|
62
|
+
providers: ["google-oauth2"],
|
|
63
|
+
},
|
|
64
|
+
order: 0,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "divider",
|
|
68
|
+
type: "DIVIDER",
|
|
69
|
+
category: "BLOCK",
|
|
70
|
+
visible: true,
|
|
71
|
+
order: 1,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "username",
|
|
75
|
+
type: "EMAIL",
|
|
76
|
+
category: "FIELD",
|
|
77
|
+
visible: true,
|
|
78
|
+
label: "Email address",
|
|
79
|
+
config: {
|
|
80
|
+
placeholder: "name@example.com",
|
|
81
|
+
},
|
|
82
|
+
required: true,
|
|
83
|
+
order: 2,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "submit",
|
|
87
|
+
type: "NEXT_BUTTON",
|
|
88
|
+
category: "BLOCK",
|
|
89
|
+
visible: true,
|
|
90
|
+
config: {
|
|
91
|
+
text: "Continue",
|
|
92
|
+
},
|
|
93
|
+
order: 3,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
links: [
|
|
97
|
+
{
|
|
98
|
+
id: "signup",
|
|
99
|
+
text: "Don't have an account?",
|
|
100
|
+
linkText: "Sign up",
|
|
101
|
+
href: "#",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type PreviewScreen = "login" | "signup" | "password";
|
|
107
|
+
|
|
108
|
+
const screenConfigs: Record<PreviewScreen, UiScreen> = {
|
|
109
|
+
login: sampleScreen,
|
|
110
|
+
signup: {
|
|
111
|
+
...sampleScreen,
|
|
112
|
+
title: "Create account",
|
|
113
|
+
description: "Sign up to get started",
|
|
114
|
+
components: [
|
|
115
|
+
...sampleScreen.components.slice(0, 3),
|
|
116
|
+
{
|
|
117
|
+
id: "password",
|
|
118
|
+
type: "PASSWORD",
|
|
119
|
+
category: "FIELD",
|
|
120
|
+
visible: true,
|
|
121
|
+
label: "Password",
|
|
122
|
+
config: {
|
|
123
|
+
placeholder: "Enter your password",
|
|
124
|
+
},
|
|
125
|
+
required: true,
|
|
126
|
+
sensitive: true,
|
|
127
|
+
order: 3,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "submit",
|
|
131
|
+
type: "NEXT_BUTTON",
|
|
132
|
+
category: "BLOCK",
|
|
133
|
+
visible: true,
|
|
134
|
+
config: {
|
|
135
|
+
text: "Sign up",
|
|
136
|
+
},
|
|
137
|
+
order: 4,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
links: [
|
|
141
|
+
{
|
|
142
|
+
id: "login",
|
|
143
|
+
text: "Already have an account?",
|
|
144
|
+
linkText: "Sign in",
|
|
145
|
+
href: "#",
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
password: {
|
|
150
|
+
action: "#",
|
|
151
|
+
method: "POST",
|
|
152
|
+
title: "Enter your password",
|
|
153
|
+
components: [
|
|
154
|
+
{
|
|
155
|
+
id: "email-display",
|
|
156
|
+
type: "RICH_TEXT",
|
|
157
|
+
category: "BLOCK",
|
|
158
|
+
visible: true,
|
|
159
|
+
config: {
|
|
160
|
+
content: "Signing in as <strong>user@example.com</strong>",
|
|
161
|
+
},
|
|
162
|
+
order: 0,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "password",
|
|
166
|
+
type: "PASSWORD",
|
|
167
|
+
category: "FIELD",
|
|
168
|
+
visible: true,
|
|
169
|
+
label: "Password",
|
|
170
|
+
config: {
|
|
171
|
+
placeholder: "Enter your password",
|
|
172
|
+
},
|
|
173
|
+
required: true,
|
|
174
|
+
sensitive: true,
|
|
175
|
+
order: 1,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "submit",
|
|
179
|
+
type: "NEXT_BUTTON",
|
|
180
|
+
category: "BLOCK",
|
|
181
|
+
visible: true,
|
|
182
|
+
config: {
|
|
183
|
+
text: "Continue",
|
|
184
|
+
},
|
|
185
|
+
order: 2,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
links: [
|
|
189
|
+
{
|
|
190
|
+
id: "forgot",
|
|
191
|
+
text: "Forgot your password?",
|
|
192
|
+
linkText: "Reset it",
|
|
193
|
+
href: "#",
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
interface WidgetBranding {
|
|
200
|
+
colors?: {
|
|
201
|
+
primary?: string;
|
|
202
|
+
page_background?:
|
|
203
|
+
| {
|
|
204
|
+
type?: string;
|
|
205
|
+
start?: string;
|
|
206
|
+
end?: string;
|
|
207
|
+
angle_deg?: number;
|
|
208
|
+
}
|
|
209
|
+
| string;
|
|
210
|
+
};
|
|
211
|
+
logo_url?: string;
|
|
212
|
+
favicon_url?: string;
|
|
213
|
+
font?: {
|
|
214
|
+
url?: string;
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
interface WidgetTheme {
|
|
219
|
+
borders?: {
|
|
220
|
+
button_border_radius?: number;
|
|
221
|
+
button_border_weight?: number;
|
|
222
|
+
buttons_style?: "pill" | "rounded" | "sharp";
|
|
223
|
+
input_border_radius?: number;
|
|
224
|
+
input_border_weight?: number;
|
|
225
|
+
inputs_style?: "pill" | "rounded" | "sharp";
|
|
226
|
+
show_widget_shadow?: boolean;
|
|
227
|
+
widget_border_weight?: number;
|
|
228
|
+
widget_corner_radius?: number;
|
|
229
|
+
};
|
|
230
|
+
colors?: {
|
|
231
|
+
base_focus_color?: string;
|
|
232
|
+
base_hover_color?: string;
|
|
233
|
+
body_text?: string;
|
|
234
|
+
error?: string;
|
|
235
|
+
header?: string;
|
|
236
|
+
icons?: string;
|
|
237
|
+
input_background?: string;
|
|
238
|
+
input_border?: string;
|
|
239
|
+
input_filled_text?: string;
|
|
240
|
+
input_labels_placeholders?: string;
|
|
241
|
+
links_focused_components?: string;
|
|
242
|
+
primary_button?: string;
|
|
243
|
+
primary_button_label?: string;
|
|
244
|
+
secondary_button_border?: string;
|
|
245
|
+
secondary_button_label?: string;
|
|
246
|
+
success?: string;
|
|
247
|
+
widget_background?: string;
|
|
248
|
+
widget_border?: string;
|
|
249
|
+
};
|
|
250
|
+
fonts?: {
|
|
251
|
+
body_text?: { bold?: boolean; size?: number };
|
|
252
|
+
buttons_text?: { bold?: boolean; size?: number };
|
|
253
|
+
font_url?: string;
|
|
254
|
+
input_labels?: { bold?: boolean; size?: number };
|
|
255
|
+
links?: { bold?: boolean; size?: number };
|
|
256
|
+
links_style?: "normal" | "underlined";
|
|
257
|
+
reference_text_size?: number;
|
|
258
|
+
subtitle?: { bold?: boolean; size?: number };
|
|
259
|
+
title?: { bold?: boolean; size?: number };
|
|
260
|
+
};
|
|
261
|
+
page_background?: {
|
|
262
|
+
background_color?: string;
|
|
263
|
+
background_image_url?: string;
|
|
264
|
+
page_layout?: "center" | "left" | "right";
|
|
265
|
+
};
|
|
266
|
+
widget?: {
|
|
267
|
+
header_text_alignment?: "center" | "left" | "right";
|
|
268
|
+
logo_height?: number;
|
|
269
|
+
logo_position?: "center" | "left" | "none" | "right";
|
|
270
|
+
logo_url?: string;
|
|
271
|
+
social_buttons_layout?: "bottom" | "top";
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function BrandingPreview() {
|
|
276
|
+
const widgetRef = useRef<HTMLElement>(null);
|
|
277
|
+
const [previewScreen, setPreviewScreen] = useState<PreviewScreen>("login");
|
|
278
|
+
|
|
279
|
+
// Watch for form changes
|
|
280
|
+
const colors = useWatch({ name: "colors" });
|
|
281
|
+
const logoUrl = useWatch({ name: "logo_url" });
|
|
282
|
+
const faviconUrl = useWatch({ name: "favicon_url" });
|
|
283
|
+
const font = useWatch({ name: "font" });
|
|
284
|
+
const themes = useWatch({ name: "themes" });
|
|
285
|
+
|
|
286
|
+
// Convert form values to widget branding format
|
|
287
|
+
const branding: WidgetBranding = {
|
|
288
|
+
colors: {
|
|
289
|
+
primary: colors?.primary,
|
|
290
|
+
page_background:
|
|
291
|
+
typeof colors?.page_background === "string"
|
|
292
|
+
? { type: "solid", start: colors.page_background }
|
|
293
|
+
: colors?.page_background,
|
|
294
|
+
},
|
|
295
|
+
logo_url: logoUrl,
|
|
296
|
+
favicon_url: faviconUrl,
|
|
297
|
+
font: font,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Convert themes to widget theme format
|
|
301
|
+
const theme: WidgetTheme | undefined = themes
|
|
302
|
+
? {
|
|
303
|
+
borders: themes.borders,
|
|
304
|
+
colors: themes.colors,
|
|
305
|
+
fonts: themes.fonts,
|
|
306
|
+
page_background: themes.page_background,
|
|
307
|
+
widget: themes.widget,
|
|
308
|
+
}
|
|
309
|
+
: undefined;
|
|
310
|
+
|
|
311
|
+
// Get background style for the preview container
|
|
312
|
+
const getBackgroundStyle = () => {
|
|
313
|
+
// Check theme page_background first
|
|
314
|
+
if (theme?.page_background?.background_color) {
|
|
315
|
+
return { background: theme.page_background.background_color };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Fall back to branding page_background
|
|
319
|
+
const bg = branding.colors?.page_background;
|
|
320
|
+
if (!bg) return { background: "#f5f5f5" };
|
|
321
|
+
|
|
322
|
+
if (typeof bg === "string") {
|
|
323
|
+
return { background: bg };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (bg.type === "linear-gradient" && bg.start && bg.end) {
|
|
327
|
+
const angle = bg.angle_deg ?? 180;
|
|
328
|
+
return {
|
|
329
|
+
background: `linear-gradient(${angle}deg, ${bg.start}, ${bg.end})`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (bg.start) {
|
|
334
|
+
return { background: bg.start };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { background: "#f5f5f5" };
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Update widget props when values change
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
if (widgetRef.current) {
|
|
343
|
+
const widget = widgetRef.current.querySelector(
|
|
344
|
+
"authhero-widget",
|
|
345
|
+
) as HTMLElement & {
|
|
346
|
+
screen?: UiScreen;
|
|
347
|
+
branding?: WidgetBranding;
|
|
348
|
+
theme?: WidgetTheme;
|
|
349
|
+
};
|
|
350
|
+
if (widget) {
|
|
351
|
+
widget.screen = screenConfigs[previewScreen];
|
|
352
|
+
widget.branding = branding;
|
|
353
|
+
if (theme) {
|
|
354
|
+
widget.theme = theme;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}, [branding, theme, previewScreen]);
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<Paper
|
|
362
|
+
elevation={0}
|
|
363
|
+
sx={{
|
|
364
|
+
p: 2,
|
|
365
|
+
height: "100%",
|
|
366
|
+
display: "flex",
|
|
367
|
+
flexDirection: "column",
|
|
368
|
+
bgcolor: "grey.100",
|
|
369
|
+
borderRadius: 2,
|
|
370
|
+
}}
|
|
371
|
+
>
|
|
372
|
+
<Box
|
|
373
|
+
sx={{
|
|
374
|
+
display: "flex",
|
|
375
|
+
justifyContent: "space-between",
|
|
376
|
+
alignItems: "center",
|
|
377
|
+
mb: 2,
|
|
378
|
+
}}
|
|
379
|
+
>
|
|
380
|
+
<Typography variant="subtitle2" color="text.secondary">
|
|
381
|
+
Preview
|
|
382
|
+
</Typography>
|
|
383
|
+
<ToggleButtonGroup
|
|
384
|
+
size="small"
|
|
385
|
+
value={previewScreen}
|
|
386
|
+
exclusive
|
|
387
|
+
onChange={(_, value) => value && setPreviewScreen(value)}
|
|
388
|
+
>
|
|
389
|
+
<ToggleButton value="login">Login</ToggleButton>
|
|
390
|
+
<ToggleButton value="password">Password</ToggleButton>
|
|
391
|
+
<ToggleButton value="signup">Signup</ToggleButton>
|
|
392
|
+
</ToggleButtonGroup>
|
|
393
|
+
</Box>
|
|
394
|
+
|
|
395
|
+
<Box
|
|
396
|
+
sx={{
|
|
397
|
+
flex: 1,
|
|
398
|
+
borderRadius: 1,
|
|
399
|
+
overflow: "hidden",
|
|
400
|
+
display: "flex",
|
|
401
|
+
alignItems: "center",
|
|
402
|
+
justifyContent: "center",
|
|
403
|
+
minHeight: 500,
|
|
404
|
+
...getBackgroundStyle(),
|
|
405
|
+
}}
|
|
406
|
+
>
|
|
407
|
+
{/* Using dangerouslySetInnerHTML to bypass JSX type issues with web components */}
|
|
408
|
+
<div
|
|
409
|
+
ref={widgetRef as React.RefObject<HTMLDivElement>}
|
|
410
|
+
dangerouslySetInnerHTML={{
|
|
411
|
+
__html: `<authhero-widget
|
|
412
|
+
screen='${JSON.stringify(screenConfigs[previewScreen]).replace(/'/g, "'")}'
|
|
413
|
+
branding='${JSON.stringify(branding).replace(/'/g, "'")}'
|
|
414
|
+
${theme ? `theme='${JSON.stringify(theme).replace(/'/g, "'")}'` : ""}
|
|
415
|
+
></authhero-widget>`,
|
|
416
|
+
}}
|
|
417
|
+
/>
|
|
418
|
+
</Box>
|
|
419
|
+
</Paper>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
import { ColorInput } from "react-admin-color-picker";
|
|
10
10
|
import { useInput, useRecordContext } from "react-admin";
|
|
11
11
|
import { useState, useEffect } from "react";
|
|
12
|
+
import { Box } from "@mui/material";
|
|
12
13
|
import { ThemesTab } from "./ThemesTab";
|
|
14
|
+
import { BrandingPreview } from "./BrandingPreview";
|
|
13
15
|
|
|
14
16
|
function PageBackgroundInput(props) {
|
|
15
17
|
const { field } = useInput(props);
|
|
@@ -56,7 +58,7 @@ function PageBackgroundInput(props) {
|
|
|
56
58
|
{mode === "color" ? (
|
|
57
59
|
<>
|
|
58
60
|
<ColorInput
|
|
59
|
-
key=
|
|
61
|
+
key="page-background-solid"
|
|
60
62
|
source={props.source}
|
|
61
63
|
label="Solid Color"
|
|
62
64
|
// No value prop, uncontrolled
|
|
@@ -79,7 +81,7 @@ function PageBackgroundInput(props) {
|
|
|
79
81
|
}
|
|
80
82
|
/>
|
|
81
83
|
<ColorInput
|
|
82
|
-
key=
|
|
84
|
+
key="page-background-start"
|
|
83
85
|
source="colors.page_background.start"
|
|
84
86
|
label="Start Color"
|
|
85
87
|
/>
|
|
@@ -92,7 +94,7 @@ function PageBackgroundInput(props) {
|
|
|
92
94
|
}
|
|
93
95
|
/>
|
|
94
96
|
<ColorInput
|
|
95
|
-
key=
|
|
97
|
+
key="page-background-end"
|
|
96
98
|
source="colors.page_background.end"
|
|
97
99
|
label="End Color"
|
|
98
100
|
/>
|
|
@@ -119,31 +121,71 @@ function PageBackgroundInput(props) {
|
|
|
119
121
|
);
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
// Wrapper component that provides the preview inside the form context
|
|
125
|
+
function BrandingFormContent() {
|
|
126
|
+
return (
|
|
127
|
+
<Box sx={{ display: "flex", gap: 3, p: 0 }}>
|
|
128
|
+
{/* Form Section */}
|
|
129
|
+
<Box sx={{ flex: "1 1 60%", minWidth: 0 }}>
|
|
130
|
+
<TabbedForm>
|
|
131
|
+
<TabbedForm.Tab label="Info">
|
|
132
|
+
<TextInput source="id" />
|
|
133
|
+
<TextInput source="name" />
|
|
134
|
+
<Labeled label={<FieldTitle source="created_at" />}>
|
|
135
|
+
<DateField source="created_at" showTime={true} />
|
|
136
|
+
</Labeled>
|
|
137
|
+
<Labeled label={<FieldTitle source="updated_at" />}>
|
|
138
|
+
<DateField source="updated_at" showTime={true} />
|
|
139
|
+
</Labeled>
|
|
140
|
+
</TabbedForm.Tab>
|
|
141
|
+
<TabbedForm.Tab label="Style">
|
|
142
|
+
<ColorInput source="colors.primary" label="Primary Color" />
|
|
143
|
+
<PageBackgroundInput source="colors.page_background" />
|
|
144
|
+
<TextInput source="favicon_url" label="Favicon URL" />
|
|
145
|
+
<TextInput source="logo_url" label="Logo URL" />
|
|
146
|
+
<TextInput source="font.url" label="Font URL" />
|
|
147
|
+
{/* Preview inside the form context */}
|
|
148
|
+
<Box
|
|
149
|
+
sx={{
|
|
150
|
+
position: "fixed",
|
|
151
|
+
right: 24,
|
|
152
|
+
top: 80,
|
|
153
|
+
width: 400,
|
|
154
|
+
height: "calc(100vh - 120px)",
|
|
155
|
+
display: { xs: "none", lg: "block" },
|
|
156
|
+
zIndex: 1000,
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<BrandingPreview />
|
|
160
|
+
</Box>
|
|
161
|
+
</TabbedForm.Tab>
|
|
162
|
+
<TabbedForm.Tab label="Themes">
|
|
163
|
+
<ThemesTab />
|
|
164
|
+
{/* Preview inside the form context */}
|
|
165
|
+
<Box
|
|
166
|
+
sx={{
|
|
167
|
+
position: "fixed",
|
|
168
|
+
right: 24,
|
|
169
|
+
top: 80,
|
|
170
|
+
width: 400,
|
|
171
|
+
height: "calc(100vh - 120px)",
|
|
172
|
+
display: { xs: "none", lg: "block" },
|
|
173
|
+
zIndex: 1000,
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
<BrandingPreview />
|
|
177
|
+
</Box>
|
|
178
|
+
</TabbedForm.Tab>
|
|
179
|
+
</TabbedForm>
|
|
180
|
+
</Box>
|
|
181
|
+
</Box>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
122
185
|
export function BrandingEdit() {
|
|
123
186
|
return (
|
|
124
187
|
<Edit>
|
|
125
|
-
<
|
|
126
|
-
<TabbedForm.Tab label="Info">
|
|
127
|
-
<TextInput source="id" />
|
|
128
|
-
<TextInput source="name" />
|
|
129
|
-
<Labeled label={<FieldTitle source="created_at" />}>
|
|
130
|
-
<DateField source="created_at" showTime={true} />
|
|
131
|
-
</Labeled>
|
|
132
|
-
<Labeled label={<FieldTitle source="updated_at" />}>
|
|
133
|
-
<DateField source="updated_at" showTime={true} />
|
|
134
|
-
</Labeled>
|
|
135
|
-
</TabbedForm.Tab>
|
|
136
|
-
<TabbedForm.Tab label="Style">
|
|
137
|
-
<ColorInput source="colors.primary" label="Primary Color" />
|
|
138
|
-
<PageBackgroundInput source="colors.page_background" />
|
|
139
|
-
<TextInput source="favicon_url" label="Favicon URL" />
|
|
140
|
-
<TextInput source="logo_url" label="Logo URL" />
|
|
141
|
-
<TextInput source="font.url" label="Font URL" />
|
|
142
|
-
</TabbedForm.Tab>
|
|
143
|
-
<TabbedForm.Tab label="Themes">
|
|
144
|
-
<ThemesTab />
|
|
145
|
-
</TabbedForm.Tab>
|
|
146
|
-
</TabbedForm>
|
|
188
|
+
<BrandingFormContent />
|
|
147
189
|
</Edit>
|
|
148
190
|
);
|
|
149
191
|
}
|
package/src/dataProvider.ts
CHANGED
|
@@ -34,15 +34,20 @@ export function getDataprovider(auth0Domain?: string) {
|
|
|
34
34
|
// Check if there's a custom REST API URL configured for this domain
|
|
35
35
|
const domains = getDomainFromStorage();
|
|
36
36
|
const formattedAuth0Domain = formatDomain(auth0Domain);
|
|
37
|
-
const domainConfig = domains.find(
|
|
37
|
+
const domainConfig = domains.find(
|
|
38
|
+
(d) => formatDomain(d.url) === formattedAuth0Domain,
|
|
39
|
+
);
|
|
38
40
|
|
|
39
41
|
if (domainConfig?.restApiUrl) {
|
|
40
|
-
// Use the custom REST API URL if configured
|
|
41
|
-
baseUrl = domainConfig.restApiUrl;
|
|
42
|
+
// Use the custom REST API URL if configured (ensure HTTPS)
|
|
43
|
+
baseUrl = buildUrlWithProtocol(domainConfig.restApiUrl);
|
|
42
44
|
} else {
|
|
43
45
|
// Otherwise use the auth domain with HTTPS
|
|
44
46
|
baseUrl = buildUrlWithProtocol(auth0Domain);
|
|
45
47
|
}
|
|
48
|
+
} else if (baseUrl) {
|
|
49
|
+
// Ensure env variable URL also uses HTTPS
|
|
50
|
+
baseUrl = buildUrlWithProtocol(baseUrl);
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
// TODO - duplicate auth0DataProvider to tenantsDataProvider
|
|
@@ -73,31 +78,46 @@ export function getDataproviderForTenant(
|
|
|
73
78
|
// Check if there's a custom REST API URL configured for this domain
|
|
74
79
|
const domains = getDomainFromStorage();
|
|
75
80
|
const formattedAuth0Domain = formatDomain(auth0Domain);
|
|
76
|
-
const domainConfig = domains.find(
|
|
81
|
+
const domainConfig = domains.find(
|
|
82
|
+
(d) => formatDomain(d.url) === formattedAuth0Domain,
|
|
83
|
+
);
|
|
77
84
|
|
|
78
85
|
if (domainConfig?.restApiUrl) {
|
|
79
|
-
// Use the custom REST API URL if configured
|
|
80
|
-
apiUrl = domainConfig.restApiUrl;
|
|
86
|
+
// Use the custom REST API URL if configured (ensure HTTPS)
|
|
87
|
+
apiUrl = buildUrlWithProtocol(domainConfig.restApiUrl);
|
|
81
88
|
} else {
|
|
82
89
|
// Otherwise construct an API URL using the auth0Domain with HTTPS
|
|
83
90
|
apiUrl = buildUrlWithProtocol(auth0Domain);
|
|
84
91
|
}
|
|
85
92
|
} else {
|
|
86
|
-
// Fallback to the environment variable
|
|
87
|
-
apiUrl = import.meta.env.VITE_AUTH0_API_URL;
|
|
93
|
+
// Fallback to the environment variable (ensure HTTPS)
|
|
94
|
+
apiUrl = buildUrlWithProtocol(import.meta.env.VITE_AUTH0_API_URL || "");
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
// Ensure apiUrl doesn't end with a slash
|
|
91
98
|
apiUrl = apiUrl.replace(/\/$/, "");
|
|
92
99
|
|
|
93
|
-
// Create
|
|
94
|
-
// This
|
|
95
|
-
const
|
|
100
|
+
// Create a dynamic httpClient that checks single-tenant mode at REQUEST TIME
|
|
101
|
+
// This is important because the mode may not be known when the dataProvider is created
|
|
102
|
+
const dynamicHttpClient = (url: string, options?: any) => {
|
|
103
|
+
// Check single-tenant mode at request time, not at creation time
|
|
104
|
+
const storedFlag = sessionStorage.getItem("isSingleTenant");
|
|
105
|
+
const isSingleTenant =
|
|
106
|
+
storedFlag?.endsWith("|true") || storedFlag === "true";
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
// In single-tenant mode, use the regular authorized client without organization scope
|
|
109
|
+
// In multi-tenant mode, use organization-scoped client for proper access control
|
|
110
|
+
if (isSingleTenant) {
|
|
111
|
+
return authorizedHttpClient(url, options);
|
|
112
|
+
} else {
|
|
113
|
+
return createOrganizationHttpClient(tenantId)(url, options);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Create the auth0Provider with the API URL, tenant ID, domain, and dynamic client
|
|
98
118
|
const auth0Provider = auth0DataProvider(
|
|
99
119
|
apiUrl,
|
|
100
|
-
|
|
120
|
+
dynamicHttpClient,
|
|
101
121
|
tenantId,
|
|
102
122
|
auth0Domain,
|
|
103
123
|
);
|
package/vercel.json
CHANGED