@authhero/react-admin 0.27.0 → 0.29.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 +25 -0
- package/package.json +3 -2
- package/src/App.spec.tsx +8 -4
- package/src/authProvider.ts +2 -0
- package/src/components/TenantAppBar.tsx +3 -4
- package/src/components/branding/BrandingPreview.tsx +4 -2
- package/src/components/clients/edit.tsx +21 -0
- package/src/components/resource-servers/edit.tsx +183 -93
- package/src/dataProvider.ts +12 -0
- package/vite.config.ts +31 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @authhero/react-admin
|
|
2
2
|
|
|
3
|
+
## 0.29.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- de7cb56: Use https for local dev
|
|
8
|
+
- 154993d: Improve react-admin experience by clearing caches and setting cores
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- Updated dependencies [154993d]
|
|
13
|
+
- @authhero/adapter-interfaces@0.126.0
|
|
14
|
+
- @authhero/widget@0.8.3
|
|
15
|
+
|
|
16
|
+
## 0.28.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 2d0a7f4: Add a auth0-conformance flag
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [2d0a7f4]
|
|
25
|
+
- @authhero/adapter-interfaces@0.123.0
|
|
26
|
+
- @authhero/widget@0.7.2
|
|
27
|
+
|
|
3
28
|
## 0.27.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@authhero/react-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"packageManager": "pnpm@10.20.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"repository": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@auth0/auth0-spa-js": "^2.1.3",
|
|
12
12
|
"@authhero/adapter-interfaces": "^0.116.0",
|
|
13
|
-
"@authhero/widget": "^0.
|
|
13
|
+
"@authhero/widget": "^0.8.1",
|
|
14
14
|
"@mui/icons-material": "^7.1.0",
|
|
15
15
|
"@mui/material": "^7.1.0",
|
|
16
16
|
"@tiptap/extension-link": "^3.13.0",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@types/react-dom": "^19.1.3",
|
|
41
41
|
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
|
42
42
|
"@typescript-eslint/parser": "^8.32.0",
|
|
43
|
+
"@vitejs/plugin-basic-ssl": "^2.1.4",
|
|
43
44
|
"@vitejs/plugin-react": "^4.4.1",
|
|
44
45
|
"eslint": "^9.26.0",
|
|
45
46
|
"eslint-config-prettier": "^10.1.5",
|
package/src/App.spec.tsx
CHANGED
|
@@ -29,10 +29,14 @@ vi.mock("./utils/domainUtils", () => ({
|
|
|
29
29
|
buildUrlWithProtocol: (url: string) => `https://${url}`,
|
|
30
30
|
}));
|
|
31
31
|
|
|
32
|
-
vi.mock("react-router-dom", () =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
vi.mock("react-router-dom", async (importOriginal) => {
|
|
33
|
+
const actual = (await importOriginal()) as any;
|
|
34
|
+
return {
|
|
35
|
+
...actual,
|
|
36
|
+
useNavigate: () => vi.fn(),
|
|
37
|
+
useLocation: () => ({ pathname: "/" }),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
36
40
|
|
|
37
41
|
// Mock color picker to avoid CSS import issues in tests
|
|
38
42
|
vi.mock("react-admin-color-picker", () => ({
|
package/src/authProvider.ts
CHANGED
|
@@ -225,8 +225,10 @@ export const createManagementClient = async (
|
|
|
225
225
|
domainForAuth,
|
|
226
226
|
normalizedTenantId,
|
|
227
227
|
);
|
|
228
|
+
|
|
228
229
|
const audience =
|
|
229
230
|
import.meta.env.VITE_AUTH0_AUDIENCE || "urn:authhero:management";
|
|
231
|
+
|
|
230
232
|
try {
|
|
231
233
|
token = await orgAuth0Client.getTokenSilently({
|
|
232
234
|
authorizationParams: {
|
|
@@ -2,7 +2,7 @@ import { AppBar, TitlePortal } from "react-admin";
|
|
|
2
2
|
import { useEffect, useState, useMemo } from "react";
|
|
3
3
|
import { Link, Box } from "@mui/material";
|
|
4
4
|
import { getDataprovider } from "../dataProvider";
|
|
5
|
-
import {
|
|
5
|
+
import { getSelectedDomainFromStorage } from "../utils/domainUtils";
|
|
6
6
|
|
|
7
7
|
type TenantResponse = {
|
|
8
8
|
audience: string;
|
|
@@ -31,9 +31,8 @@ export function TenantAppBar(props: TenantAppBarProps) {
|
|
|
31
31
|
|
|
32
32
|
// Get the selected domain from storage or environment
|
|
33
33
|
const selectedDomain = useMemo(() => {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
return selected?.url || import.meta.env.VITE_AUTH0_DOMAIN || "";
|
|
34
|
+
const selected = getSelectedDomainFromStorage();
|
|
35
|
+
return selected || import.meta.env.VITE_AUTH0_DOMAIN || "";
|
|
37
36
|
}, []);
|
|
38
37
|
|
|
39
38
|
// Use the non-org data provider for fetching tenants list
|
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
import { useState } from "react";
|
|
11
11
|
import { defineCustomElements } from "@authhero/widget/loader";
|
|
12
12
|
|
|
13
|
-
// Initialize the widget custom elements
|
|
13
|
+
// Initialize the widget custom elements with CDN path for assets
|
|
14
14
|
if (typeof window !== "undefined") {
|
|
15
|
-
defineCustomElements(window
|
|
15
|
+
defineCustomElements(window, {
|
|
16
|
+
resourcesUrl: "https://unpkg.com/@authhero/widget@latest/dist/authhero-widget/",
|
|
17
|
+
});
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
// Types for the widget screen configuration
|
|
@@ -1226,6 +1226,12 @@ export function ClientEdit() {
|
|
|
1226
1226
|
format={(value) => value === "true" || value === true}
|
|
1227
1227
|
parse={(value) => (value ? "true" : "false")}
|
|
1228
1228
|
/>
|
|
1229
|
+
<BooleanInput
|
|
1230
|
+
source="auth0_conformant"
|
|
1231
|
+
label="Auth0 Conformant Mode"
|
|
1232
|
+
helperText="Enable Auth0-compatible behavior. Disable for strict OIDC compliance."
|
|
1233
|
+
defaultValue={true}
|
|
1234
|
+
/>
|
|
1229
1235
|
<ClientMetadataInput source="client_metadata" />
|
|
1230
1236
|
<GrantTypesInput source="grant_types" />
|
|
1231
1237
|
<ArrayInput source="callbacks">
|
|
@@ -1318,6 +1324,21 @@ export function ClientEdit() {
|
|
|
1318
1324
|
<TabbedForm.Tab label="Connections">
|
|
1319
1325
|
<ConnectionsTab />
|
|
1320
1326
|
</TabbedForm.Tab>
|
|
1327
|
+
<TabbedForm.Tab label="Advanced">
|
|
1328
|
+
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
|
|
1329
|
+
These settings control OAuth/OIDC protocol conformance behavior.
|
|
1330
|
+
</Typography>
|
|
1331
|
+
<BooleanInput
|
|
1332
|
+
source="oidc_conformant"
|
|
1333
|
+
label="OIDC Conformant"
|
|
1334
|
+
helperText="When enabled, the client will strictly follow the OIDC specification. This affects token claims, scopes, and other protocol behaviors."
|
|
1335
|
+
/>
|
|
1336
|
+
<BooleanInput
|
|
1337
|
+
source="is_first_party"
|
|
1338
|
+
label="First Party Application"
|
|
1339
|
+
helperText="First party applications are trusted applications that don't require user consent for standard scopes."
|
|
1340
|
+
/>
|
|
1341
|
+
</TabbedForm.Tab>
|
|
1321
1342
|
<TabbedForm.Tab label="Raw JSON">
|
|
1322
1343
|
<FunctionField
|
|
1323
1344
|
source="date"
|
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
FormDataConsumer,
|
|
10
10
|
useRecordContext,
|
|
11
11
|
} from "react-admin";
|
|
12
|
-
import { Stack, Alert,
|
|
12
|
+
import { Stack, Alert, Box, Typography, Button, IconButton, TextField as MuiTextField, InputAdornment, TableContainer, Table, TableHead, TableBody, TableRow, TableCell, TableSortLabel, Paper, TablePagination } from "@mui/material";
|
|
13
13
|
import DeleteIcon from "@mui/icons-material/Delete";
|
|
14
14
|
import AddIcon from "@mui/icons-material/Add";
|
|
15
|
+
import SearchIcon from "@mui/icons-material/Search";
|
|
15
16
|
import { useState, useMemo, useCallback } from "react";
|
|
16
17
|
import { useFormContext, useWatch } from "react-hook-form";
|
|
17
18
|
|
|
@@ -28,127 +29,216 @@ function SystemEntityAlert() {
|
|
|
28
29
|
);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const SCOPES_PER_PAGE = 10;
|
|
32
|
-
|
|
33
32
|
interface Scope {
|
|
34
33
|
value: string;
|
|
35
34
|
description?: string;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
type SortField = "value" | "description";
|
|
38
|
+
type SortOrder = "asc" | "desc";
|
|
39
|
+
|
|
40
|
+
function ScopesListInput({ disabled }: { disabled?: boolean }) {
|
|
39
41
|
const { setValue, getValues } = useFormContext();
|
|
40
42
|
const scopes: Scope[] = useWatch({ name: "scopes" }) || [];
|
|
41
|
-
const [
|
|
43
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
44
|
+
const [sortField, setSortField] = useState<SortField>("value");
|
|
45
|
+
const [sortOrder, setSortOrder] = useState<SortOrder>("asc");
|
|
46
|
+
const [page, setPage] = useState(0);
|
|
47
|
+
const [rowsPerPage, setRowsPerPage] = useState(25);
|
|
48
|
+
|
|
49
|
+
const filteredAndSortedScopes = useMemo(() => {
|
|
50
|
+
let result = scopes.map((scope, index) => ({ ...scope, originalIndex: index }));
|
|
51
|
+
|
|
52
|
+
// Filter by search query
|
|
53
|
+
if (searchQuery) {
|
|
54
|
+
const query = searchQuery.toLowerCase();
|
|
55
|
+
result = result.filter(
|
|
56
|
+
(scope) =>
|
|
57
|
+
scope.value?.toLowerCase().includes(query) ||
|
|
58
|
+
scope.description?.toLowerCase().includes(query)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Sort
|
|
63
|
+
result.sort((a, b) => {
|
|
64
|
+
const aValue = (a[sortField] || "").toLowerCase();
|
|
65
|
+
const bValue = (b[sortField] || "").toLowerCase();
|
|
66
|
+
const comparison = aValue.localeCompare(bValue);
|
|
67
|
+
return sortOrder === "asc" ? comparison : -comparison;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}, [scopes, searchQuery, sortField, sortOrder]);
|
|
42
72
|
|
|
43
|
-
const totalPages = Math.max(1, Math.ceil(scopes.length / SCOPES_PER_PAGE));
|
|
44
|
-
|
|
45
73
|
const paginatedScopes = useMemo(() => {
|
|
46
|
-
const start =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
...scope,
|
|
50
|
-
actualIndex: start + index,
|
|
51
|
-
}));
|
|
52
|
-
}, [scopes, page]);
|
|
74
|
+
const start = page * rowsPerPage;
|
|
75
|
+
return filteredAndSortedScopes.slice(start, start + rowsPerPage);
|
|
76
|
+
}, [filteredAndSortedScopes, page, rowsPerPage]);
|
|
53
77
|
|
|
54
|
-
const
|
|
55
|
-
|
|
78
|
+
const handleSort = (field: SortField) => {
|
|
79
|
+
if (sortField === field) {
|
|
80
|
+
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
|
81
|
+
} else {
|
|
82
|
+
setSortField(field);
|
|
83
|
+
setSortOrder("asc");
|
|
84
|
+
}
|
|
56
85
|
};
|
|
57
86
|
|
|
58
87
|
const handleAdd = useCallback(() => {
|
|
59
88
|
const currentScopes = getValues("scopes") || [];
|
|
60
89
|
setValue("scopes", [...currentScopes, { value: "", description: "" }], { shouldDirty: true });
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
// Clear search and go to last page to show the new item
|
|
91
|
+
setSearchQuery("");
|
|
92
|
+
const newTotal = currentScopes.length + 1;
|
|
93
|
+
setPage(Math.floor(newTotal / rowsPerPage));
|
|
94
|
+
}, [getValues, setValue, rowsPerPage]);
|
|
65
95
|
|
|
66
|
-
const handleRemove = useCallback((
|
|
96
|
+
const handleRemove = useCallback((originalIndex: number) => {
|
|
67
97
|
const currentScopes = getValues("scopes") || [];
|
|
68
|
-
const newScopes = currentScopes.filter((_: Scope, i: number) => i !==
|
|
98
|
+
const newScopes = currentScopes.filter((_: Scope, i: number) => i !== originalIndex);
|
|
69
99
|
setValue("scopes", newScopes, { shouldDirty: true });
|
|
70
|
-
|
|
71
|
-
const newTotalPages = Math.ceil(newScopes.length / SCOPES_PER_PAGE);
|
|
72
|
-
if (page > newTotalPages && newTotalPages > 0) {
|
|
73
|
-
setPage(newTotalPages);
|
|
74
|
-
}
|
|
75
|
-
}, [getValues, setValue, page]);
|
|
100
|
+
}, [getValues, setValue]);
|
|
76
101
|
|
|
77
|
-
const handleScopeChange = useCallback((
|
|
102
|
+
const handleScopeChange = useCallback((originalIndex: number, field: "value" | "description", newValue: string) => {
|
|
78
103
|
const currentScopes = getValues("scopes") || [];
|
|
79
104
|
const newScopes = [...currentScopes];
|
|
80
|
-
newScopes[
|
|
105
|
+
newScopes[originalIndex] = { ...newScopes[originalIndex], [field]: newValue };
|
|
81
106
|
setValue("scopes", newScopes, { shouldDirty: true });
|
|
82
107
|
}, [getValues, setValue]);
|
|
83
108
|
|
|
109
|
+
const handleChangePage = (_event: unknown, newPage: number) => {
|
|
110
|
+
setPage(newPage);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
114
|
+
setRowsPerPage(parseInt(event.target.value, 10));
|
|
115
|
+
setPage(0);
|
|
116
|
+
};
|
|
117
|
+
|
|
84
118
|
return (
|
|
85
119
|
<Box>
|
|
86
|
-
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
>
|
|
109
|
-
<MuiTextField
|
|
110
|
-
value={scope.value || ""}
|
|
111
|
-
onChange={(e) => handleScopeChange(scope.actualIndex, "value", e.target.value)}
|
|
112
|
-
label="Scope Name"
|
|
113
|
-
helperText="e.g., read:users, write:posts"
|
|
114
|
-
sx={{ flex: 1 }}
|
|
115
|
-
disabled={disabled}
|
|
116
|
-
size="small"
|
|
117
|
-
required
|
|
118
|
-
/>
|
|
119
|
-
<MuiTextField
|
|
120
|
-
value={scope.description || ""}
|
|
121
|
-
onChange={(e) => handleScopeChange(scope.actualIndex, "description", e.target.value)}
|
|
122
|
-
label="Description"
|
|
123
|
-
helperText="What this scope allows"
|
|
124
|
-
sx={{ flex: 2 }}
|
|
125
|
-
disabled={disabled}
|
|
126
|
-
size="small"
|
|
127
|
-
/>
|
|
120
|
+
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, gap: 2 }}>
|
|
121
|
+
<MuiTextField
|
|
122
|
+
placeholder="Search scopes..."
|
|
123
|
+
value={searchQuery}
|
|
124
|
+
onChange={(e) => {
|
|
125
|
+
setSearchQuery(e.target.value);
|
|
126
|
+
setPage(0);
|
|
127
|
+
}}
|
|
128
|
+
size="small"
|
|
129
|
+
sx={{ width: 300 }}
|
|
130
|
+
InputProps={{
|
|
131
|
+
startAdornment: (
|
|
132
|
+
<InputAdornment position="start">
|
|
133
|
+
<SearchIcon />
|
|
134
|
+
</InputAdornment>
|
|
135
|
+
),
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
|
139
|
+
<Typography variant="body2" color="text.secondary">
|
|
140
|
+
{filteredAndSortedScopes.length} of {scopes.length} scope{scopes.length !== 1 ? "s" : ""}
|
|
141
|
+
</Typography>
|
|
128
142
|
{!disabled && (
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
<Button
|
|
144
|
+
startIcon={<AddIcon />}
|
|
145
|
+
onClick={handleAdd}
|
|
146
|
+
size="small"
|
|
147
|
+
variant="contained"
|
|
133
148
|
>
|
|
134
|
-
|
|
135
|
-
</
|
|
149
|
+
Add Scope
|
|
150
|
+
</Button>
|
|
136
151
|
)}
|
|
137
|
-
</Stack>
|
|
138
|
-
))}
|
|
139
|
-
|
|
140
|
-
{totalPages > 1 && (
|
|
141
|
-
<Box sx={{ display: "flex", justifyContent: "center", mt: 2 }}>
|
|
142
|
-
<Pagination
|
|
143
|
-
count={totalPages}
|
|
144
|
-
page={page}
|
|
145
|
-
onChange={handlePageChange}
|
|
146
|
-
color="primary"
|
|
147
|
-
showFirstButton
|
|
148
|
-
showLastButton
|
|
149
|
-
/>
|
|
150
152
|
</Box>
|
|
151
|
-
|
|
153
|
+
</Box>
|
|
154
|
+
|
|
155
|
+
<TableContainer component={Paper} variant="outlined">
|
|
156
|
+
<Table size="small">
|
|
157
|
+
<TableHead>
|
|
158
|
+
<TableRow>
|
|
159
|
+
<TableCell sx={{ width: "30%" }}>
|
|
160
|
+
<TableSortLabel
|
|
161
|
+
active={sortField === "value"}
|
|
162
|
+
direction={sortField === "value" ? sortOrder : "asc"}
|
|
163
|
+
onClick={() => handleSort("value")}
|
|
164
|
+
>
|
|
165
|
+
Scope Name
|
|
166
|
+
</TableSortLabel>
|
|
167
|
+
</TableCell>
|
|
168
|
+
<TableCell>
|
|
169
|
+
<TableSortLabel
|
|
170
|
+
active={sortField === "description"}
|
|
171
|
+
direction={sortField === "description" ? sortOrder : "asc"}
|
|
172
|
+
onClick={() => handleSort("description")}
|
|
173
|
+
>
|
|
174
|
+
Description
|
|
175
|
+
</TableSortLabel>
|
|
176
|
+
</TableCell>
|
|
177
|
+
{!disabled && <TableCell sx={{ width: 50 }} />}
|
|
178
|
+
</TableRow>
|
|
179
|
+
</TableHead>
|
|
180
|
+
<TableBody>
|
|
181
|
+
{paginatedScopes.length === 0 ? (
|
|
182
|
+
<TableRow>
|
|
183
|
+
<TableCell colSpan={disabled ? 2 : 3} align="center" sx={{ py: 4 }}>
|
|
184
|
+
<Typography color="text.secondary">
|
|
185
|
+
{searchQuery ? "No scopes match your search" : "No scopes defined"}
|
|
186
|
+
</Typography>
|
|
187
|
+
</TableCell>
|
|
188
|
+
</TableRow>
|
|
189
|
+
) : (
|
|
190
|
+
paginatedScopes.map((scope) => (
|
|
191
|
+
<TableRow key={scope.originalIndex} hover>
|
|
192
|
+
<TableCell>
|
|
193
|
+
<MuiTextField
|
|
194
|
+
value={scope.value || ""}
|
|
195
|
+
onChange={(e) => handleScopeChange(scope.originalIndex, "value", e.target.value)}
|
|
196
|
+
size="small"
|
|
197
|
+
fullWidth
|
|
198
|
+
disabled={disabled}
|
|
199
|
+
variant="standard"
|
|
200
|
+
placeholder="e.g., read:users"
|
|
201
|
+
InputProps={{ disableUnderline: disabled }}
|
|
202
|
+
/>
|
|
203
|
+
</TableCell>
|
|
204
|
+
<TableCell>
|
|
205
|
+
<MuiTextField
|
|
206
|
+
value={scope.description || ""}
|
|
207
|
+
onChange={(e) => handleScopeChange(scope.originalIndex, "description", e.target.value)}
|
|
208
|
+
size="small"
|
|
209
|
+
fullWidth
|
|
210
|
+
disabled={disabled}
|
|
211
|
+
variant="standard"
|
|
212
|
+
placeholder="What this scope allows"
|
|
213
|
+
InputProps={{ disableUnderline: disabled }}
|
|
214
|
+
/>
|
|
215
|
+
</TableCell>
|
|
216
|
+
{!disabled && (
|
|
217
|
+
<TableCell>
|
|
218
|
+
<IconButton
|
|
219
|
+
onClick={() => handleRemove(scope.originalIndex)}
|
|
220
|
+
color="error"
|
|
221
|
+
size="small"
|
|
222
|
+
>
|
|
223
|
+
<DeleteIcon fontSize="small" />
|
|
224
|
+
</IconButton>
|
|
225
|
+
</TableCell>
|
|
226
|
+
)}
|
|
227
|
+
</TableRow>
|
|
228
|
+
))
|
|
229
|
+
)}
|
|
230
|
+
</TableBody>
|
|
231
|
+
</Table>
|
|
232
|
+
<TablePagination
|
|
233
|
+
component="div"
|
|
234
|
+
count={filteredAndSortedScopes.length}
|
|
235
|
+
page={page}
|
|
236
|
+
onPageChange={handleChangePage}
|
|
237
|
+
rowsPerPage={rowsPerPage}
|
|
238
|
+
onRowsPerPageChange={handleChangeRowsPerPage}
|
|
239
|
+
rowsPerPageOptions={[10, 25, 50, 100]}
|
|
240
|
+
/>
|
|
241
|
+
</TableContainer>
|
|
152
242
|
</Box>
|
|
153
243
|
);
|
|
154
244
|
}
|
|
@@ -242,7 +332,7 @@ function ResourceServerForm() {
|
|
|
242
332
|
</TabbedForm.Tab>
|
|
243
333
|
|
|
244
334
|
<TabbedForm.Tab label="Scopes">
|
|
245
|
-
<
|
|
335
|
+
<ScopesListInput disabled={isSystem} />
|
|
246
336
|
</TabbedForm.Tab>
|
|
247
337
|
</TabbedForm>
|
|
248
338
|
);
|
package/src/dataProvider.ts
CHANGED
|
@@ -30,6 +30,8 @@ export function getDataprovider(auth0Domain?: string) {
|
|
|
30
30
|
// Create the complete base URL using the selected domain
|
|
31
31
|
let baseUrl = import.meta.env.VITE_SIMPLE_REST_URL;
|
|
32
32
|
|
|
33
|
+
console.log("[getDataprovider] auth0Domain:", auth0Domain, "VITE_SIMPLE_REST_URL:", import.meta.env.VITE_SIMPLE_REST_URL);
|
|
34
|
+
|
|
33
35
|
if (auth0Domain) {
|
|
34
36
|
// Check if there's a custom REST API URL configured for this domain
|
|
35
37
|
const domains = getDomainFromStorage();
|
|
@@ -38,6 +40,8 @@ export function getDataprovider(auth0Domain?: string) {
|
|
|
38
40
|
(d) => formatDomain(d.url) === formattedAuth0Domain,
|
|
39
41
|
);
|
|
40
42
|
|
|
43
|
+
console.log("[getDataprovider] domains:", domains, "domainConfig:", domainConfig);
|
|
44
|
+
|
|
41
45
|
if (domainConfig?.restApiUrl) {
|
|
42
46
|
// Use the custom REST API URL if configured (ensure HTTPS)
|
|
43
47
|
baseUrl = buildUrlWithProtocol(domainConfig.restApiUrl);
|
|
@@ -50,6 +54,8 @@ export function getDataprovider(auth0Domain?: string) {
|
|
|
50
54
|
baseUrl = buildUrlWithProtocol(baseUrl);
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
console.log("[getDataprovider] final baseUrl:", baseUrl);
|
|
58
|
+
|
|
53
59
|
// TODO - duplicate auth0DataProvider to tenantsDataProvider
|
|
54
60
|
// we are introducing non-auth0 endpoints AND we don't require the tenants-id header
|
|
55
61
|
const provider = auth0DataProvider(
|
|
@@ -74,6 +80,8 @@ export function getDataproviderForTenant(
|
|
|
74
80
|
// Start with a default API URL
|
|
75
81
|
let apiUrl;
|
|
76
82
|
|
|
83
|
+
console.log("[getDataproviderForTenant] tenantId:", tenantId, "auth0Domain:", auth0Domain);
|
|
84
|
+
|
|
77
85
|
if (auth0Domain) {
|
|
78
86
|
// Check if there's a custom REST API URL configured for this domain
|
|
79
87
|
const domains = getDomainFromStorage();
|
|
@@ -82,6 +90,8 @@ export function getDataproviderForTenant(
|
|
|
82
90
|
(d) => formatDomain(d.url) === formattedAuth0Domain,
|
|
83
91
|
);
|
|
84
92
|
|
|
93
|
+
console.log("[getDataproviderForTenant] domains:", domains, "domainConfig:", domainConfig);
|
|
94
|
+
|
|
85
95
|
if (domainConfig?.restApiUrl) {
|
|
86
96
|
// Use the custom REST API URL if configured (ensure HTTPS)
|
|
87
97
|
apiUrl = buildUrlWithProtocol(domainConfig.restApiUrl);
|
|
@@ -97,6 +107,8 @@ export function getDataproviderForTenant(
|
|
|
97
107
|
// Ensure apiUrl doesn't end with a slash
|
|
98
108
|
apiUrl = apiUrl.replace(/\/$/, "");
|
|
99
109
|
|
|
110
|
+
console.log("[getDataproviderForTenant] final apiUrl:", apiUrl);
|
|
111
|
+
|
|
100
112
|
// Create a dynamic httpClient that checks single-tenant mode at REQUEST TIME
|
|
101
113
|
// This is important because the mode may not be known when the dataProvider is created
|
|
102
114
|
const dynamicHttpClient = (url: string, options?: any) => {
|
package/vite.config.ts
CHANGED
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
/// <reference types="vitest" />
|
|
2
2
|
import { defineConfig } from "vite";
|
|
3
3
|
import react from "@vitejs/plugin-react";
|
|
4
|
+
import basicSsl from "@vitejs/plugin-basic-ssl";
|
|
5
|
+
import http from "http";
|
|
6
|
+
|
|
7
|
+
// Redirect HTTP to HTTPS
|
|
8
|
+
const HTTPS_PORT = 5173;
|
|
9
|
+
const HTTP_PORT = 5172;
|
|
10
|
+
|
|
11
|
+
function startHttpRedirectServer() {
|
|
12
|
+
const server = http.createServer((req, res) => {
|
|
13
|
+
const host = req.headers.host?.replace(`:${HTTP_PORT}`, `:${HTTPS_PORT}`);
|
|
14
|
+
res.writeHead(301, { Location: `https://${host}${req.url}` });
|
|
15
|
+
res.end();
|
|
16
|
+
});
|
|
17
|
+
server.listen(HTTP_PORT, () => {
|
|
18
|
+
console.log(
|
|
19
|
+
`HTTP redirect server running on http://localhost:${HTTP_PORT} -> https://localhost:${HTTPS_PORT}`,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
4
23
|
|
|
5
24
|
// https://vitejs.dev/config/
|
|
6
25
|
export default defineConfig({
|
|
7
|
-
plugins: [
|
|
26
|
+
plugins: [
|
|
27
|
+
react(),
|
|
28
|
+
basicSsl(),
|
|
29
|
+
{
|
|
30
|
+
name: "http-redirect",
|
|
31
|
+
configureServer() {
|
|
32
|
+
startHttpRedirectServer();
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
8
36
|
define: {
|
|
9
37
|
"process.env": process.env,
|
|
10
38
|
},
|
|
11
39
|
server: {
|
|
12
40
|
host: true,
|
|
41
|
+
https: {},
|
|
42
|
+
port: HTTPS_PORT,
|
|
13
43
|
},
|
|
14
44
|
base: "./",
|
|
15
|
-
// @ts-expect-error
|
|
16
45
|
test: {
|
|
17
46
|
environment: "jsdom", // Set JSDOM as the default test environment
|
|
18
47
|
globals: true, // Make test globals available
|