@authhero/react-admin 0.21.0 → 0.22.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/.env.example +27 -0
- package/CHANGELOG.md +12 -0
- package/README.md +25 -0
- package/package.json +1 -1
- package/src/auth0DataProvider.ts +50 -0
- package/src/authProvider.ts +22 -4
- package/src/components/users/edit.tsx +184 -0
- package/src/utils/domainUtils.ts +80 -46
package/.env.example
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# AuthHero React Admin - Environment Configuration
|
|
2
|
+
|
|
3
|
+
# Default domain configuration (automatically added to domain list if not present)
|
|
4
|
+
# This is useful for development or single-instance deployments
|
|
5
|
+
|
|
6
|
+
# Auth domain for production (e.g., login.sesamy.com)
|
|
7
|
+
VITE_AUTH0_DOMAIN=login.sesamy.com
|
|
8
|
+
|
|
9
|
+
# For local development, use localhost:3000
|
|
10
|
+
# VITE_AUTH0_DOMAIN=localhost:3000
|
|
11
|
+
|
|
12
|
+
# Client ID to use for authentication
|
|
13
|
+
VITE_AUTH0_CLIENT_ID=auth-admin
|
|
14
|
+
|
|
15
|
+
# API URL for management endpoints (e.g., https://auth2.sesamy.com)
|
|
16
|
+
VITE_AUTH0_API_URL=https://auth2.sesamy.com
|
|
17
|
+
|
|
18
|
+
# For local development API
|
|
19
|
+
# VITE_AUTH0_API_URL=https://localhost:3000
|
|
20
|
+
|
|
21
|
+
# Optional: Management API audience (defaults to urn:authhero:management)
|
|
22
|
+
# VITE_AUTH0_AUDIENCE=urn:authhero:management
|
|
23
|
+
|
|
24
|
+
# Notes:
|
|
25
|
+
# - If VITE_AUTH0_DOMAIN is set, it will be automatically added to the domain list
|
|
26
|
+
# - Users can still add additional domains through the UI
|
|
27
|
+
# - For HTTPS on localhost, ensure your local server has a valid certificate
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @authhero/react-admin
|
|
2
2
|
|
|
3
|
+
## 0.22.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 00d2f83: Update versions to get latest build
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [00d2f83]
|
|
12
|
+
- @authhero/adapter-interfaces@0.120.0
|
|
13
|
+
- @authhero/widget@0.6.3
|
|
14
|
+
|
|
3
15
|
## 0.21.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -10,6 +10,31 @@ npm install
|
|
|
10
10
|
yarn install
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
### Environment Variables
|
|
16
|
+
|
|
17
|
+
You can configure the default domain connection using environment variables. Create a `.env` file in the `apps/react-admin` directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Production
|
|
21
|
+
VITE_AUTH0_DOMAIN=login.sesamy.com
|
|
22
|
+
VITE_AUTH0_CLIENT_ID=auth-admin
|
|
23
|
+
VITE_AUTH0_API_URL=https://auth2.sesamy.com
|
|
24
|
+
|
|
25
|
+
# Or for local development
|
|
26
|
+
VITE_AUTH0_DOMAIN=localhost:3000
|
|
27
|
+
VITE_AUTH0_CLIENT_ID=auth-admin
|
|
28
|
+
VITE_AUTH0_API_URL=https://localhost:3000
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
See `.env.example` for more details.
|
|
32
|
+
|
|
33
|
+
**Notes:**
|
|
34
|
+
|
|
35
|
+
- If `VITE_AUTH0_DOMAIN` is set, it will be automatically added to the domain list
|
|
36
|
+
- Users can still add additional domains through the UI
|
|
37
|
+
|
|
13
38
|
## Development
|
|
14
39
|
|
|
15
40
|
Start the application in development mode by running:
|
package/package.json
CHANGED
package/src/auth0DataProvider.ts
CHANGED
|
@@ -124,6 +124,11 @@ export default (
|
|
|
124
124
|
let managementClientPromise: Promise<ManagementClient> | null = null;
|
|
125
125
|
const getManagementClient = async () => {
|
|
126
126
|
if (!managementClientPromise) {
|
|
127
|
+
if (!apiUrl) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"API URL is not configured. Please set restApiUrl in domain configuration or VITE_SIMPLE_REST_URL environment variable.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
127
132
|
// Extract API domain from apiUrl
|
|
128
133
|
const apiDomain = apiUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
129
134
|
// Pass both API domain and OAuth domain for authentication
|
|
@@ -233,6 +238,11 @@ export default (
|
|
|
233
238
|
resourceKey: "custom_domains",
|
|
234
239
|
idKey: "custom_domain_id",
|
|
235
240
|
},
|
|
241
|
+
hooks: {
|
|
242
|
+
fetch: (client: any) => client.hooks.list(),
|
|
243
|
+
resourceKey: "hooks",
|
|
244
|
+
idKey: "hook_id",
|
|
245
|
+
},
|
|
236
246
|
};
|
|
237
247
|
|
|
238
248
|
// Handle SDK resources (only for top-level resources, not nested paths like users/{id}/roles)
|
|
@@ -317,6 +327,46 @@ export default (
|
|
|
317
327
|
}
|
|
318
328
|
}
|
|
319
329
|
|
|
330
|
+
// User organizations - when filtering by user_id
|
|
331
|
+
if (resource === "user-organizations" && params.filter?.user_id) {
|
|
332
|
+
const userId = params.filter.user_id;
|
|
333
|
+
const result = await managementClient.users.organizations.list(userId, {
|
|
334
|
+
page: page - 1,
|
|
335
|
+
per_page: perPage,
|
|
336
|
+
});
|
|
337
|
+
const response = (result as any).response || result;
|
|
338
|
+
|
|
339
|
+
let organizationsData: any[];
|
|
340
|
+
let total: number;
|
|
341
|
+
|
|
342
|
+
if (Array.isArray(response)) {
|
|
343
|
+
organizationsData = response;
|
|
344
|
+
total = response.length;
|
|
345
|
+
} else if (response.organizations) {
|
|
346
|
+
organizationsData = response.organizations;
|
|
347
|
+
total = response.total || organizationsData.length;
|
|
348
|
+
} else {
|
|
349
|
+
organizationsData = [];
|
|
350
|
+
total = 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
data: organizationsData.map((org: any) => ({
|
|
355
|
+
id: org.id || org.organization_id,
|
|
356
|
+
user_id: userId,
|
|
357
|
+
name: org.name,
|
|
358
|
+
display_name: org.display_name,
|
|
359
|
+
branding: org.branding,
|
|
360
|
+
metadata: org.metadata || {},
|
|
361
|
+
token_quota: org.token_quota,
|
|
362
|
+
created_at: org.created_at,
|
|
363
|
+
updated_at: org.updated_at,
|
|
364
|
+
...org,
|
|
365
|
+
})),
|
|
366
|
+
total,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
320
370
|
// Use HTTP client for all other list operations
|
|
321
371
|
const headers = createHeaders(tenantId);
|
|
322
372
|
|
package/src/authProvider.ts
CHANGED
|
@@ -50,15 +50,23 @@ export const createAuth0Client = (domain: string) => {
|
|
|
50
50
|
const audience =
|
|
51
51
|
import.meta.env.VITE_AUTH0_AUDIENCE || "urn:authhero:management";
|
|
52
52
|
|
|
53
|
+
const clientId = getClientIdFromStorage(domain);
|
|
54
|
+
console.log(
|
|
55
|
+
"[createAuth0Client] Creating client for domain:",
|
|
56
|
+
domain,
|
|
57
|
+
"with clientId:",
|
|
58
|
+
clientId,
|
|
59
|
+
);
|
|
60
|
+
|
|
53
61
|
const auth0Client = new Auth0Client({
|
|
54
62
|
domain: fullDomain,
|
|
55
|
-
clientId
|
|
63
|
+
clientId,
|
|
56
64
|
cacheLocation: "localstorage",
|
|
57
65
|
useRefreshTokens: false,
|
|
58
66
|
authorizationParams: {
|
|
59
67
|
audience,
|
|
60
68
|
redirect_uri: redirectUri,
|
|
61
|
-
scope: "openid profile email
|
|
69
|
+
scope: "openid profile email",
|
|
62
70
|
},
|
|
63
71
|
});
|
|
64
72
|
|
|
@@ -143,9 +151,19 @@ export const createAuth0ClientForOrg = (
|
|
|
143
151
|
const audience =
|
|
144
152
|
import.meta.env.VITE_AUTH0_AUDIENCE || "urn:authhero:management";
|
|
145
153
|
|
|
154
|
+
const clientId = getClientIdFromStorage(domain);
|
|
155
|
+
console.log(
|
|
156
|
+
"[createAuth0ClientForOrg] Creating client for domain:",
|
|
157
|
+
domain,
|
|
158
|
+
"org:",
|
|
159
|
+
normalizedOrgId,
|
|
160
|
+
"with clientId:",
|
|
161
|
+
clientId,
|
|
162
|
+
);
|
|
163
|
+
|
|
146
164
|
const auth0Client = new Auth0Client({
|
|
147
165
|
domain: fullDomain,
|
|
148
|
-
clientId
|
|
166
|
+
clientId,
|
|
149
167
|
useRefreshTokens: false,
|
|
150
168
|
// Use organization-specific cache to isolate tokens
|
|
151
169
|
// Note: Don't use cacheLocation when providing a custom cache
|
|
@@ -153,7 +171,7 @@ export const createAuth0ClientForOrg = (
|
|
|
153
171
|
authorizationParams: {
|
|
154
172
|
audience,
|
|
155
173
|
redirect_uri: redirectUri,
|
|
156
|
-
scope: "openid profile email
|
|
174
|
+
scope: "openid profile email",
|
|
157
175
|
organization: normalizedOrgId,
|
|
158
176
|
},
|
|
159
177
|
});
|
|
@@ -1119,6 +1119,189 @@ const UserRolesTable = ({
|
|
|
1119
1119
|
);
|
|
1120
1120
|
};
|
|
1121
1121
|
|
|
1122
|
+
// Add organization management: add user to organizations
|
|
1123
|
+
const AddOrganizationButton = () => {
|
|
1124
|
+
const [open, setOpen] = useState(false);
|
|
1125
|
+
const [availableOrganizations, setAvailableOrganizations] = useState<any[]>(
|
|
1126
|
+
[],
|
|
1127
|
+
);
|
|
1128
|
+
const [selectedOrganizations, setSelectedOrganizations] = useState<any[]>([]);
|
|
1129
|
+
const [loading, setLoading] = useState(false);
|
|
1130
|
+
const [searchText, setSearchText] = useState("");
|
|
1131
|
+
const dataProvider = useDataProvider();
|
|
1132
|
+
const notify = useNotify();
|
|
1133
|
+
const refresh = useRefresh();
|
|
1134
|
+
|
|
1135
|
+
const { id: userId } = useParams();
|
|
1136
|
+
|
|
1137
|
+
const handleOpen = async () => {
|
|
1138
|
+
setOpen(true);
|
|
1139
|
+
await loadAvailableOrganizations();
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
const handleClose = () => {
|
|
1143
|
+
setOpen(false);
|
|
1144
|
+
setSelectedOrganizations([]);
|
|
1145
|
+
setAvailableOrganizations([]);
|
|
1146
|
+
setSearchText("");
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
const loadAvailableOrganizations = async () => {
|
|
1150
|
+
if (!userId) return;
|
|
1151
|
+
setLoading(true);
|
|
1152
|
+
try {
|
|
1153
|
+
// Load all organizations
|
|
1154
|
+
const allOrgsRes = await dataProvider.getList("organizations", {
|
|
1155
|
+
pagination: { page: 1, perPage: 1000 },
|
|
1156
|
+
sort: { field: "name", order: "ASC" },
|
|
1157
|
+
filter: {},
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
// Load organizations the user is already a member of
|
|
1161
|
+
const userOrgsRes = await dataProvider.getList("user-organizations", {
|
|
1162
|
+
pagination: { page: 1, perPage: 1000 },
|
|
1163
|
+
sort: { field: "name", order: "ASC" },
|
|
1164
|
+
filter: { user_id: userId },
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
const userOrgIds = new Set(userOrgsRes.data.map((org: any) => org.id));
|
|
1168
|
+
const available = allOrgsRes.data.filter(
|
|
1169
|
+
(org: any) => !userOrgIds.has(org.id),
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
setAvailableOrganizations(available);
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
console.error("Error loading organizations:", error);
|
|
1175
|
+
notify("Error loading organizations", { type: "error" });
|
|
1176
|
+
} finally {
|
|
1177
|
+
setLoading(false);
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
const handleAddOrganizations = async () => {
|
|
1182
|
+
if (!userId || selectedOrganizations.length === 0) {
|
|
1183
|
+
notify("Please select at least one organization", { type: "warning" });
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
try {
|
|
1188
|
+
// Add user to each selected organization
|
|
1189
|
+
for (const org of selectedOrganizations) {
|
|
1190
|
+
await dataProvider.create("organization-members", {
|
|
1191
|
+
data: {
|
|
1192
|
+
organization_id: org.id,
|
|
1193
|
+
user_ids: [userId],
|
|
1194
|
+
},
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
notify(
|
|
1199
|
+
`Added user to ${selectedOrganizations.length} organization(s) successfully`,
|
|
1200
|
+
{ type: "success" },
|
|
1201
|
+
);
|
|
1202
|
+
handleClose();
|
|
1203
|
+
refresh();
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
console.error("Error adding user to organizations:", error);
|
|
1206
|
+
notify("Error adding user to organizations", { type: "error" });
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
if (!userId) return null;
|
|
1211
|
+
|
|
1212
|
+
const filteredOrganizations = availableOrganizations.filter(
|
|
1213
|
+
(org) =>
|
|
1214
|
+
!searchText ||
|
|
1215
|
+
org.name?.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
1216
|
+
org.display_name?.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
1217
|
+
org.id?.toLowerCase().includes(searchText.toLowerCase()),
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1220
|
+
return (
|
|
1221
|
+
<>
|
|
1222
|
+
<Button
|
|
1223
|
+
variant="contained"
|
|
1224
|
+
color="primary"
|
|
1225
|
+
startIcon={<AddIcon />}
|
|
1226
|
+
onClick={handleOpen}
|
|
1227
|
+
sx={{ mb: 2 }}
|
|
1228
|
+
>
|
|
1229
|
+
Add to Organization
|
|
1230
|
+
</Button>
|
|
1231
|
+
|
|
1232
|
+
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
|
1233
|
+
<DialogTitle>Add to Organizations</DialogTitle>
|
|
1234
|
+
<DialogContent>
|
|
1235
|
+
<Typography variant="body2" sx={{ mb: 3 }}>
|
|
1236
|
+
Select one or more organizations to add this user to
|
|
1237
|
+
</Typography>
|
|
1238
|
+
|
|
1239
|
+
<Autocomplete
|
|
1240
|
+
multiple
|
|
1241
|
+
options={filteredOrganizations}
|
|
1242
|
+
getOptionLabel={(option) =>
|
|
1243
|
+
option.display_name || option.name || option.id
|
|
1244
|
+
}
|
|
1245
|
+
value={selectedOrganizations}
|
|
1246
|
+
onChange={(_, value) => setSelectedOrganizations(value)}
|
|
1247
|
+
loading={loading}
|
|
1248
|
+
isOptionEqualToValue={(option, value) => option?.id === value?.id}
|
|
1249
|
+
onInputChange={(_, value) => setSearchText(value)}
|
|
1250
|
+
renderInput={(params) => (
|
|
1251
|
+
<MuiTextField
|
|
1252
|
+
{...params}
|
|
1253
|
+
label="Organizations"
|
|
1254
|
+
variant="outlined"
|
|
1255
|
+
fullWidth
|
|
1256
|
+
InputProps={{
|
|
1257
|
+
...params.InputProps,
|
|
1258
|
+
endAdornment: (
|
|
1259
|
+
<>
|
|
1260
|
+
{loading ? (
|
|
1261
|
+
<CircularProgress color="inherit" size={20} />
|
|
1262
|
+
) : null}
|
|
1263
|
+
{params.InputProps.endAdornment}
|
|
1264
|
+
</>
|
|
1265
|
+
),
|
|
1266
|
+
}}
|
|
1267
|
+
/>
|
|
1268
|
+
)}
|
|
1269
|
+
renderOption={(props, option) => (
|
|
1270
|
+
<li {...props} key={option.id}>
|
|
1271
|
+
<Box>
|
|
1272
|
+
<Typography variant="body2" fontWeight="medium">
|
|
1273
|
+
{option.display_name || option.name || option.id}
|
|
1274
|
+
</Typography>
|
|
1275
|
+
<Typography variant="caption" color="text.secondary">
|
|
1276
|
+
ID: {option.id}
|
|
1277
|
+
</Typography>
|
|
1278
|
+
</Box>
|
|
1279
|
+
</li>
|
|
1280
|
+
)}
|
|
1281
|
+
/>
|
|
1282
|
+
|
|
1283
|
+
{selectedOrganizations.length > 0 && (
|
|
1284
|
+
<Typography variant="caption" sx={{ mt: 2, display: "block" }}>
|
|
1285
|
+
{selectedOrganizations.length} organization(s) selected
|
|
1286
|
+
</Typography>
|
|
1287
|
+
)}
|
|
1288
|
+
</DialogContent>
|
|
1289
|
+
<DialogActions>
|
|
1290
|
+
<Button onClick={handleClose}>Cancel</Button>
|
|
1291
|
+
<Button
|
|
1292
|
+
onClick={handleAddOrganizations}
|
|
1293
|
+
variant="contained"
|
|
1294
|
+
color="primary"
|
|
1295
|
+
disabled={selectedOrganizations.length === 0 || loading}
|
|
1296
|
+
>
|
|
1297
|
+
Add to Organizations
|
|
1298
|
+
</Button>
|
|
1299
|
+
</DialogActions>
|
|
1300
|
+
</Dialog>
|
|
1301
|
+
</>
|
|
1302
|
+
);
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1122
1305
|
// Add roles management: add roles and remove roles for a user
|
|
1123
1306
|
const AddRoleButton = ({ onRolesChanged }: { onRolesChanged?: () => void }) => {
|
|
1124
1307
|
const [open, setOpen] = useState(false);
|
|
@@ -1672,6 +1855,7 @@ export function UserEdit() {
|
|
|
1672
1855
|
<RolesManagement />
|
|
1673
1856
|
</TabbedForm.Tab>
|
|
1674
1857
|
<TabbedForm.Tab label="organizations">
|
|
1858
|
+
<AddOrganizationButton />
|
|
1675
1859
|
<ReferenceManyField
|
|
1676
1860
|
reference="user-organizations"
|
|
1677
1861
|
target="user_id"
|
package/src/utils/domainUtils.ts
CHANGED
|
@@ -22,46 +22,98 @@ export interface DomainConfig {
|
|
|
22
22
|
clientSecret?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Formats a domain string (removes protocol if present)
|
|
27
|
+
*/
|
|
28
|
+
export const formatDomain = (domain: string): string => {
|
|
29
|
+
return domain.trim().replace(/^https?:\/\//, "");
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets default domain configuration from environment variables
|
|
34
|
+
*/
|
|
35
|
+
const getDefaultDomainFromEnv = (): DomainConfig | null => {
|
|
36
|
+
const domain = import.meta.env.VITE_AUTH0_DOMAIN;
|
|
37
|
+
const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID;
|
|
38
|
+
const apiUrl = import.meta.env.VITE_AUTH0_API_URL;
|
|
39
|
+
|
|
40
|
+
if (!domain) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
url: formatDomain(domain),
|
|
46
|
+
connectionMethod: "login",
|
|
47
|
+
clientId: clientId || undefined,
|
|
48
|
+
restApiUrl: apiUrl || undefined,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
25
52
|
/**
|
|
26
53
|
* Gets domains from localStorage
|
|
27
54
|
* Handles both formats (array of objects or array of strings) for backward compatibility
|
|
55
|
+
* Includes default domain from environment variables if not already in storage
|
|
28
56
|
*/
|
|
29
57
|
export const getDomainFromStorage = (): DomainConfig[] => {
|
|
30
58
|
try {
|
|
31
59
|
const storedValue = localStorage.getItem(DOMAINS_STORAGE_KEY);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
let domains: DomainConfig[] = [];
|
|
61
|
+
|
|
62
|
+
if (storedValue) {
|
|
63
|
+
const parsedData = JSON.parse(storedValue);
|
|
64
|
+
|
|
65
|
+
// Handle both formats: array of objects with url property or array of strings
|
|
66
|
+
if (Array.isArray(parsedData)) {
|
|
67
|
+
domains = parsedData
|
|
68
|
+
.filter((item) => item !== null && item !== undefined)
|
|
69
|
+
.map((item) => {
|
|
70
|
+
if (typeof item === "object" && item !== null && "url" in item) {
|
|
71
|
+
// Add connectionMethod if it doesn't exist (for backward compatibility)
|
|
72
|
+
if (!("connectionMethod" in item)) {
|
|
73
|
+
return {
|
|
74
|
+
...item,
|
|
75
|
+
connectionMethod: "login" as ConnectionMethod, // Assume login for existing entries
|
|
76
|
+
} as DomainConfig;
|
|
77
|
+
}
|
|
78
|
+
return item as DomainConfig;
|
|
79
|
+
} else {
|
|
80
|
+
// Convert string domains to DomainConfig format (backward compatibility)
|
|
44
81
|
return {
|
|
45
|
-
|
|
46
|
-
connectionMethod: "login" as ConnectionMethod,
|
|
47
|
-
|
|
82
|
+
url: String(item),
|
|
83
|
+
connectionMethod: "login" as ConnectionMethod,
|
|
84
|
+
clientId: "", // Empty clientId for legacy entries
|
|
85
|
+
};
|
|
48
86
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
87
|
+
})
|
|
88
|
+
.filter((domain) => domain.url.trim() !== ""); // Remove empty domains
|
|
89
|
+
}
|
|
60
90
|
}
|
|
61
|
-
|
|
91
|
+
|
|
92
|
+
// Add or update default domain from environment if configured
|
|
93
|
+
const defaultDomain = getDefaultDomainFromEnv();
|
|
94
|
+
if (defaultDomain) {
|
|
95
|
+
const existingIndex = domains.findIndex(
|
|
96
|
+
(d) => formatDomain(d.url) === defaultDomain.url,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (existingIndex >= 0) {
|
|
100
|
+
// Update existing domain with environment config (env takes precedence)
|
|
101
|
+
domains[existingIndex] = {
|
|
102
|
+
...domains[existingIndex],
|
|
103
|
+
...defaultDomain,
|
|
104
|
+
};
|
|
105
|
+
} else {
|
|
106
|
+
// Add new domain at the beginning
|
|
107
|
+
domains.unshift(defaultDomain);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return domains;
|
|
62
112
|
} catch (e) {
|
|
63
113
|
console.error("Failed to parse domains from localStorage", e);
|
|
64
|
-
|
|
114
|
+
// Return default domain from env if storage fails
|
|
115
|
+
const defaultDomain = getDefaultDomainFromEnv();
|
|
116
|
+
return defaultDomain ? [defaultDomain] : [];
|
|
65
117
|
}
|
|
66
118
|
};
|
|
67
119
|
|
|
@@ -70,7 +122,6 @@ export const getDomainFromStorage = (): DomainConfig[] => {
|
|
|
70
122
|
*/
|
|
71
123
|
export const saveDomainToStorage = (domains: DomainConfig[]): void => {
|
|
72
124
|
try {
|
|
73
|
-
console.log("Saving domains to localStorage:", domains);
|
|
74
125
|
localStorage.setItem(DOMAINS_STORAGE_KEY, JSON.stringify(domains));
|
|
75
126
|
} catch (e) {
|
|
76
127
|
console.error("Failed to save domains to localStorage", e);
|
|
@@ -100,7 +151,6 @@ export const getSelectedDomainFromStorage = (): string => {
|
|
|
100
151
|
*/
|
|
101
152
|
export const saveSelectedDomainToStorage = (domain: string): void => {
|
|
102
153
|
try {
|
|
103
|
-
console.log("Saving selected domain to localStorage:", domain);
|
|
104
154
|
localStorage.setItem(SELECTED_DOMAIN_STORAGE_KEY, domain);
|
|
105
155
|
} catch (e) {
|
|
106
156
|
console.error("Failed to save selected domain to localStorage", e);
|
|
@@ -129,16 +179,8 @@ export const getClientIdFromStorage = (domain: string): string => {
|
|
|
129
179
|
return fallbackClientId;
|
|
130
180
|
};
|
|
131
181
|
|
|
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
182
|
/**
|
|
140
183
|
* Constructs a full URL with HTTPS protocol
|
|
141
|
-
* - If domain starts with "local.", connects to https://localhost:3000
|
|
142
184
|
* - Always uses https:// for all domains (including localhost with self-signed certs)
|
|
143
185
|
* - Preserves existing https:// protocol if already present
|
|
144
186
|
* - Converts http:// to https://
|
|
@@ -146,14 +188,6 @@ export const formatDomain = (domain: string): string => {
|
|
|
146
188
|
export const buildUrlWithProtocol = (domain: string): string => {
|
|
147
189
|
const trimmedDomain = domain.trim();
|
|
148
190
|
|
|
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
191
|
// Check if it already has a protocol
|
|
158
192
|
if (trimmedDomain.startsWith("https://")) {
|
|
159
193
|
return trimmedDomain;
|