@authhero/react-admin 0.10.0 → 0.12.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 +12 -0
- package/package.json +1 -1
- package/src/AuthCallback.tsx +41 -7
- package/src/authProvider.ts +415 -31
- package/src/components/TenantAppBar.tsx +22 -7
- package/src/components/clients/edit.tsx +43 -2
- package/src/components/organizations/edit.tsx +238 -2
- package/src/components/resource-servers/edit.tsx +130 -97
- package/src/dataProvider.ts +15 -6
- package/src/utils/tokenUtils.ts +142 -18
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { AppBar, TitlePortal
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { AppBar, TitlePortal } from "react-admin";
|
|
2
|
+
import { useEffect, useState, useMemo } from "react";
|
|
3
3
|
import { Link, Box } from "@mui/material";
|
|
4
|
+
import { getDataprovider } from "../dataProvider";
|
|
5
|
+
import { getDomainFromStorage } from "../utils/domainUtils";
|
|
4
6
|
|
|
5
7
|
type TenantResponse = {
|
|
6
8
|
audience: string;
|
|
@@ -26,12 +28,25 @@ export function TenantAppBar(props: TenantAppBarProps) {
|
|
|
26
28
|
const pathSegments = location.pathname.split("/").filter(Boolean);
|
|
27
29
|
const tenantId = pathSegments[0];
|
|
28
30
|
const [tenant, setTenant] = useState<TenantResponse>();
|
|
29
|
-
|
|
31
|
+
|
|
32
|
+
// Get the selected domain from storage or environment
|
|
33
|
+
const selectedDomain = useMemo(() => {
|
|
34
|
+
const domains = getDomainFromStorage();
|
|
35
|
+
const selected = domains.find((d) => d.isSelected);
|
|
36
|
+
return selected?.url || import.meta.env.VITE_AUTH0_DOMAIN || "";
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
// Use the non-org data provider for fetching tenants list
|
|
40
|
+
// This is necessary because tenants list requires a non-org token
|
|
41
|
+
const tenantsDataProvider = useMemo(
|
|
42
|
+
() => getDataprovider(selectedDomain),
|
|
43
|
+
[selectedDomain],
|
|
44
|
+
);
|
|
30
45
|
|
|
31
46
|
useEffect(() => {
|
|
32
|
-
// Use the dataProvider to fetch tenants list
|
|
33
|
-
//
|
|
34
|
-
|
|
47
|
+
// Use the non-org dataProvider to fetch tenants list
|
|
48
|
+
// The tenants endpoint requires non-org scoped tokens
|
|
49
|
+
tenantsDataProvider
|
|
35
50
|
.getList("tenants", {
|
|
36
51
|
pagination: { page: 1, perPage: 100 },
|
|
37
52
|
sort: { field: "id", order: "ASC" },
|
|
@@ -59,7 +74,7 @@ export function TenantAppBar(props: TenantAppBarProps) {
|
|
|
59
74
|
name: tenantId,
|
|
60
75
|
} as TenantResponse);
|
|
61
76
|
});
|
|
62
|
-
}, [tenantId,
|
|
77
|
+
}, [tenantId, tenantsDataProvider]);
|
|
63
78
|
|
|
64
79
|
const isDefaultSettings = tenantId === "DEFAULT_SETTINGS";
|
|
65
80
|
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
getDomainFromStorage,
|
|
61
61
|
getSelectedDomainFromStorage,
|
|
62
62
|
buildUrlWithProtocol,
|
|
63
|
+
formatDomain,
|
|
63
64
|
} from "../../utils/domainUtils";
|
|
64
65
|
|
|
65
66
|
const AddClientGrantButton = () => {
|
|
@@ -746,7 +747,20 @@ const ClientMetadataInput = ({ source }: { source: string }) => {
|
|
|
746
747
|
};
|
|
747
748
|
|
|
748
749
|
const updateFormData = (array: Array<{ key: string; value: string }>) => {
|
|
750
|
+
// Fields managed by other inputs (BooleanInput, SelectInput, etc.)
|
|
751
|
+
const preservedFields = ["disable_sign_ups", "email_validation"];
|
|
752
|
+
|
|
753
|
+
// Start with preserved fields from current value
|
|
749
754
|
const newObject: Record<string, any> = {};
|
|
755
|
+
if (value && typeof value === "object") {
|
|
756
|
+
preservedFields.forEach((field) => {
|
|
757
|
+
if (field in value) {
|
|
758
|
+
newObject[field] = value[field];
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Add the metadata array values
|
|
750
764
|
array.forEach((item) => {
|
|
751
765
|
if (item.key && item.key.trim()) {
|
|
752
766
|
newObject[item.key.trim()] = item.value;
|
|
@@ -808,7 +822,8 @@ interface Connection {
|
|
|
808
822
|
const getApiBaseUrl = (): string => {
|
|
809
823
|
const selectedDomain = getSelectedDomainFromStorage();
|
|
810
824
|
const domains = getDomainFromStorage();
|
|
811
|
-
const
|
|
825
|
+
const formattedDomain = formatDomain(selectedDomain);
|
|
826
|
+
const domainConfig = domains.find((d) => formatDomain(d.url) === formattedDomain);
|
|
812
827
|
|
|
813
828
|
if (domainConfig?.restApiUrl) {
|
|
814
829
|
return domainConfig.restApiUrl.replace(/\/$/, "");
|
|
@@ -1157,8 +1172,34 @@ const ConnectionsTab = () => {
|
|
|
1157
1172
|
};
|
|
1158
1173
|
|
|
1159
1174
|
export function ClientEdit() {
|
|
1175
|
+
// Transform data before submission to ensure client_metadata values are strings
|
|
1176
|
+
const transformClientData = (data: Record<string, unknown>) => {
|
|
1177
|
+
const transformed = { ...data };
|
|
1178
|
+
|
|
1179
|
+
// Ensure client_metadata values are strings (Auth0 requirement)
|
|
1180
|
+
if (
|
|
1181
|
+
transformed.client_metadata &&
|
|
1182
|
+
typeof transformed.client_metadata === "object"
|
|
1183
|
+
) {
|
|
1184
|
+
const metadata = transformed.client_metadata as Record<string, unknown>;
|
|
1185
|
+
const stringifiedMetadata: Record<string, string> = {};
|
|
1186
|
+
|
|
1187
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
1188
|
+
if (typeof value === "boolean") {
|
|
1189
|
+
stringifiedMetadata[key] = value ? "true" : "false";
|
|
1190
|
+
} else if (value !== null && value !== undefined) {
|
|
1191
|
+
stringifiedMetadata[key] = String(value);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
transformed.client_metadata = stringifiedMetadata;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
return transformed;
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1160
1201
|
return (
|
|
1161
|
-
<Edit>
|
|
1202
|
+
<Edit transform={transformClientData}>
|
|
1162
1203
|
<SimpleShowLayout>
|
|
1163
1204
|
<TextField source="name" />
|
|
1164
1205
|
<TextField source="id" />
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
useRecordContext,
|
|
15
15
|
useRedirect,
|
|
16
16
|
} from "react-admin";
|
|
17
|
-
import { useState } from "react";
|
|
17
|
+
import { useState, useEffect } from "react";
|
|
18
18
|
import {
|
|
19
19
|
Box,
|
|
20
20
|
Button,
|
|
@@ -34,9 +34,15 @@ import {
|
|
|
34
34
|
Checkbox,
|
|
35
35
|
Chip,
|
|
36
36
|
Stack,
|
|
37
|
+
FormControl,
|
|
38
|
+
InputLabel,
|
|
39
|
+
Select,
|
|
40
|
+
MenuItem,
|
|
41
|
+
OutlinedInput,
|
|
37
42
|
} from "@mui/material";
|
|
38
43
|
import AddIcon from "@mui/icons-material/Add";
|
|
39
44
|
import DeleteIcon from "@mui/icons-material/Delete";
|
|
45
|
+
import EditIcon from "@mui/icons-material/Edit";
|
|
40
46
|
import { useParams } from "react-router-dom";
|
|
41
47
|
|
|
42
48
|
const AddOrganizationMemberButton = () => {
|
|
@@ -401,6 +407,227 @@ const RemoveMemberButton = ({ record }: { record: any }) => {
|
|
|
401
407
|
);
|
|
402
408
|
};
|
|
403
409
|
|
|
410
|
+
const ManageMemberRolesButton = ({ record }: { record: any }) => {
|
|
411
|
+
const [open, setOpen] = useState(false);
|
|
412
|
+
const [roles, setRoles] = useState<any[]>([]);
|
|
413
|
+
const [memberRoles, setMemberRoles] = useState<string[]>([]);
|
|
414
|
+
const [loading, setLoading] = useState(false);
|
|
415
|
+
const dataProvider = useDataProvider();
|
|
416
|
+
const notify = useNotify();
|
|
417
|
+
const refresh = useRefresh();
|
|
418
|
+
const { id: organizationId } = useParams();
|
|
419
|
+
|
|
420
|
+
const handleOpen = async () => {
|
|
421
|
+
setOpen(true);
|
|
422
|
+
setLoading(true);
|
|
423
|
+
try {
|
|
424
|
+
// Fetch all available roles
|
|
425
|
+
const { data: allRoles } = await dataProvider.getList("roles", {
|
|
426
|
+
pagination: { page: 1, perPage: 100 },
|
|
427
|
+
sort: { field: "name", order: "ASC" },
|
|
428
|
+
filter: {},
|
|
429
|
+
});
|
|
430
|
+
setRoles(allRoles);
|
|
431
|
+
|
|
432
|
+
// Fetch member's current roles in this organization
|
|
433
|
+
if (organizationId && record?.user_id) {
|
|
434
|
+
const response = await dataProvider.getList(
|
|
435
|
+
`organizations/${organizationId}/members/${record.user_id}/roles`,
|
|
436
|
+
{
|
|
437
|
+
pagination: { page: 1, perPage: 100 },
|
|
438
|
+
sort: { field: "name", order: "ASC" },
|
|
439
|
+
filter: {},
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
setMemberRoles(response.data.map((r: any) => r.id));
|
|
443
|
+
}
|
|
444
|
+
} catch (error) {
|
|
445
|
+
notify("Error loading roles", { type: "error" });
|
|
446
|
+
} finally {
|
|
447
|
+
setLoading(false);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const handleClose = () => {
|
|
452
|
+
setOpen(false);
|
|
453
|
+
setMemberRoles([]);
|
|
454
|
+
setRoles([]);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const handleSaveRoles = async () => {
|
|
458
|
+
if (!organizationId || !record?.user_id) return;
|
|
459
|
+
|
|
460
|
+
setLoading(true);
|
|
461
|
+
try {
|
|
462
|
+
// Get current roles to determine what to add/remove
|
|
463
|
+
const currentResponse = await dataProvider.getList(
|
|
464
|
+
`organizations/${organizationId}/members/${record.user_id}/roles`,
|
|
465
|
+
{
|
|
466
|
+
pagination: { page: 1, perPage: 100 },
|
|
467
|
+
sort: { field: "name", order: "ASC" },
|
|
468
|
+
filter: {},
|
|
469
|
+
},
|
|
470
|
+
);
|
|
471
|
+
const currentRoleIds = currentResponse.data.map((r: any) => r.id);
|
|
472
|
+
|
|
473
|
+
const rolesToAdd = memberRoles.filter((r) => !currentRoleIds.includes(r));
|
|
474
|
+
const rolesToRemove = currentRoleIds.filter(
|
|
475
|
+
(r: string) => !memberRoles.includes(r),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
// Add new roles
|
|
479
|
+
if (rolesToAdd.length > 0) {
|
|
480
|
+
await dataProvider.create(
|
|
481
|
+
`organizations/${organizationId}/members/${record.user_id}/roles`,
|
|
482
|
+
{
|
|
483
|
+
data: { roles: rolesToAdd },
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Remove old roles
|
|
489
|
+
if (rolesToRemove.length > 0) {
|
|
490
|
+
await dataProvider.delete(
|
|
491
|
+
`organizations/${organizationId}/members/${record.user_id}/roles`,
|
|
492
|
+
{
|
|
493
|
+
id: "",
|
|
494
|
+
previousData: { roles: rolesToRemove },
|
|
495
|
+
},
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
notify("Member roles updated successfully", { type: "success" });
|
|
500
|
+
refresh();
|
|
501
|
+
handleClose();
|
|
502
|
+
} catch (error) {
|
|
503
|
+
notify("Error updating member roles", { type: "error" });
|
|
504
|
+
} finally {
|
|
505
|
+
setLoading(false);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
return (
|
|
510
|
+
<>
|
|
511
|
+
<IconButton
|
|
512
|
+
onClick={handleOpen}
|
|
513
|
+
color="primary"
|
|
514
|
+
size="small"
|
|
515
|
+
title="Manage roles"
|
|
516
|
+
>
|
|
517
|
+
<EditIcon />
|
|
518
|
+
</IconButton>
|
|
519
|
+
|
|
520
|
+
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
|
521
|
+
<DialogTitle>
|
|
522
|
+
Manage Roles for {record?.email || record?.user_id}
|
|
523
|
+
</DialogTitle>
|
|
524
|
+
<DialogContent>
|
|
525
|
+
{loading ? (
|
|
526
|
+
<Box display="flex" justifyContent="center" p={3}>
|
|
527
|
+
<CircularProgress />
|
|
528
|
+
</Box>
|
|
529
|
+
) : (
|
|
530
|
+
<>
|
|
531
|
+
<DialogContentText sx={{ mb: 2 }}>
|
|
532
|
+
Select roles to assign to this member in this organization.
|
|
533
|
+
</DialogContentText>
|
|
534
|
+
|
|
535
|
+
<FormControl fullWidth>
|
|
536
|
+
<InputLabel id="member-roles-label">Roles</InputLabel>
|
|
537
|
+
<Select
|
|
538
|
+
labelId="member-roles-label"
|
|
539
|
+
multiple
|
|
540
|
+
value={memberRoles}
|
|
541
|
+
onChange={(e) => setMemberRoles(e.target.value as string[])}
|
|
542
|
+
input={<OutlinedInput label="Roles" />}
|
|
543
|
+
renderValue={(selected) => (
|
|
544
|
+
<Stack direction="row" spacing={0.5} flexWrap="wrap">
|
|
545
|
+
{selected.map((roleId) => {
|
|
546
|
+
const role = roles.find((r) => r.id === roleId);
|
|
547
|
+
return (
|
|
548
|
+
<Chip
|
|
549
|
+
key={roleId}
|
|
550
|
+
label={role?.name || roleId}
|
|
551
|
+
size="small"
|
|
552
|
+
/>
|
|
553
|
+
);
|
|
554
|
+
})}
|
|
555
|
+
</Stack>
|
|
556
|
+
)}
|
|
557
|
+
>
|
|
558
|
+
{roles.map((role) => (
|
|
559
|
+
<MenuItem key={role.id} value={role.id}>
|
|
560
|
+
<Checkbox checked={memberRoles.includes(role.id)} />
|
|
561
|
+
<ListItemText
|
|
562
|
+
primary={role.name}
|
|
563
|
+
secondary={role.description}
|
|
564
|
+
/>
|
|
565
|
+
</MenuItem>
|
|
566
|
+
))}
|
|
567
|
+
</Select>
|
|
568
|
+
</FormControl>
|
|
569
|
+
</>
|
|
570
|
+
)}
|
|
571
|
+
</DialogContent>
|
|
572
|
+
<DialogActions>
|
|
573
|
+
<Button onClick={handleClose}>Cancel</Button>
|
|
574
|
+
<Button onClick={handleSaveRoles} variant="contained" disabled={loading}>
|
|
575
|
+
Save Roles
|
|
576
|
+
</Button>
|
|
577
|
+
</DialogActions>
|
|
578
|
+
</Dialog>
|
|
579
|
+
</>
|
|
580
|
+
);
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const MemberRolesDisplay = ({ record }: { record: any }) => {
|
|
584
|
+
const [roles, setRoles] = useState<any[]>([]);
|
|
585
|
+
const [loading, setLoading] = useState(true);
|
|
586
|
+
const dataProvider = useDataProvider();
|
|
587
|
+
const { id: organizationId } = useParams();
|
|
588
|
+
|
|
589
|
+
useEffect(() => {
|
|
590
|
+
const fetchRoles = async () => {
|
|
591
|
+
if (!organizationId || !record?.user_id) {
|
|
592
|
+
setLoading(false);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
const response = await dataProvider.getList(
|
|
597
|
+
`organizations/${organizationId}/members/${record.user_id}/roles`,
|
|
598
|
+
{
|
|
599
|
+
pagination: { page: 1, perPage: 100 },
|
|
600
|
+
sort: { field: "name", order: "ASC" },
|
|
601
|
+
filter: {},
|
|
602
|
+
},
|
|
603
|
+
);
|
|
604
|
+
setRoles(response.data);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
console.error("Error fetching member roles:", error);
|
|
607
|
+
} finally {
|
|
608
|
+
setLoading(false);
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
fetchRoles();
|
|
612
|
+
}, [dataProvider, organizationId, record?.user_id]);
|
|
613
|
+
|
|
614
|
+
if (loading) {
|
|
615
|
+
return <CircularProgress size={16} />;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (roles.length === 0) {
|
|
619
|
+
return <Typography color="text.secondary">No roles</Typography>;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return (
|
|
623
|
+
<Stack direction="row" spacing={0.5} flexWrap="wrap">
|
|
624
|
+
{roles.map((role) => (
|
|
625
|
+
<Chip key={role.id} label={role.name} size="small" variant="outlined" />
|
|
626
|
+
))}
|
|
627
|
+
</Stack>
|
|
628
|
+
);
|
|
629
|
+
};
|
|
630
|
+
|
|
404
631
|
const OrganizationGeneralTab = () => (
|
|
405
632
|
<Box>
|
|
406
633
|
<TextInput source="name" validate={[required()]} fullWidth />
|
|
@@ -456,9 +683,18 @@ const OrganizationMembersTab = () => {
|
|
|
456
683
|
)}
|
|
457
684
|
/>
|
|
458
685
|
<TextField source="email" label="Email" />
|
|
686
|
+
<FunctionField
|
|
687
|
+
label="Roles"
|
|
688
|
+
render={(record) => <MemberRolesDisplay record={record} />}
|
|
689
|
+
/>
|
|
459
690
|
<FunctionField
|
|
460
691
|
label="Actions"
|
|
461
|
-
render={(record) =>
|
|
692
|
+
render={(record) => (
|
|
693
|
+
<Box sx={{ display: "flex", gap: 0.5 }}>
|
|
694
|
+
<ManageMemberRolesButton record={record} />
|
|
695
|
+
<RemoveMemberButton record={record} />
|
|
696
|
+
</Box>
|
|
697
|
+
)}
|
|
462
698
|
/>
|
|
463
699
|
</Datagrid>
|
|
464
700
|
</ReferenceManyField>
|
|
@@ -9,113 +9,146 @@ import {
|
|
|
9
9
|
required,
|
|
10
10
|
NumberInput,
|
|
11
11
|
FormDataConsumer,
|
|
12
|
+
useRecordContext,
|
|
12
13
|
} from "react-admin";
|
|
13
|
-
import { Stack } from "@mui/material";
|
|
14
|
+
import { Stack, Alert } from "@mui/material";
|
|
15
|
+
|
|
16
|
+
function SystemEntityAlert() {
|
|
17
|
+
const record = useRecordContext();
|
|
18
|
+
if (!record?.is_system) return null;
|
|
14
19
|
|
|
15
|
-
export function ResourceServerEdit() {
|
|
16
20
|
return (
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
<Alert severity="info" sx={{ mb: 2 }}>
|
|
22
|
+
This Resource Server represents a system entity and cannot be modified or
|
|
23
|
+
deleted. You can still authorize applications to consume this resource
|
|
24
|
+
server.
|
|
25
|
+
</Alert>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ResourceServerForm() {
|
|
30
|
+
const record = useRecordContext();
|
|
31
|
+
const isSystem = record?.is_system;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<TabbedForm>
|
|
35
|
+
<TabbedForm.Tab label="Details">
|
|
36
|
+
<SystemEntityAlert />
|
|
37
|
+
<Stack spacing={2}>
|
|
38
|
+
<TextInput source="name" validate={[required()]} disabled={isSystem} />
|
|
39
|
+
<TextInput
|
|
40
|
+
source="identifier"
|
|
41
|
+
validate={[required()]}
|
|
42
|
+
helperText="Unique identifier for this resource server"
|
|
43
|
+
disabled={isSystem}
|
|
44
|
+
/>
|
|
45
|
+
</Stack>
|
|
46
|
+
|
|
47
|
+
<Stack spacing={2} direction="row" sx={{ mt: 2 }}>
|
|
48
|
+
<BooleanInput
|
|
49
|
+
source="signing_alg_values_supported"
|
|
50
|
+
defaultValue={true}
|
|
51
|
+
disabled={isSystem}
|
|
52
|
+
/>
|
|
53
|
+
<BooleanInput
|
|
54
|
+
source="skip_consent_for_verifiable_first_party_clients"
|
|
55
|
+
defaultValue={true}
|
|
56
|
+
disabled={isSystem}
|
|
57
|
+
/>
|
|
58
|
+
<BooleanInput source="allow_offline_access" defaultValue={true} disabled={isSystem} />
|
|
59
|
+
</Stack>
|
|
28
60
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/>
|
|
38
|
-
<BooleanInput source="allow_offline_access" defaultValue={true} />
|
|
39
|
-
</Stack>
|
|
61
|
+
<Stack spacing={2} direction="row" sx={{ mt: 2 }}>
|
|
62
|
+
<TextInput
|
|
63
|
+
source="signing_alg"
|
|
64
|
+
defaultValue="RS256"
|
|
65
|
+
helperText="Signing algorithm for tokens"
|
|
66
|
+
disabled={isSystem}
|
|
67
|
+
/>
|
|
68
|
+
</Stack>
|
|
40
69
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
70
|
+
<Stack spacing={2} direction="row" sx={{ mt: 2 }}>
|
|
71
|
+
<NumberInput
|
|
72
|
+
source="token_lifetime"
|
|
73
|
+
defaultValue={1209600}
|
|
74
|
+
helperText="Token lifetime in seconds (default: 14 days)"
|
|
75
|
+
disabled={isSystem}
|
|
76
|
+
/>
|
|
77
|
+
<NumberInput
|
|
78
|
+
source="token_lifetime_for_web"
|
|
79
|
+
defaultValue={7200}
|
|
80
|
+
helperText="Web token lifetime in seconds (default: 2 hours)"
|
|
81
|
+
disabled={isSystem}
|
|
82
|
+
/>
|
|
83
|
+
</Stack>
|
|
48
84
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/>
|
|
55
|
-
<NumberInput
|
|
56
|
-
source="token_lifetime_for_web"
|
|
57
|
-
defaultValue={7200}
|
|
58
|
-
helperText="Web token lifetime in seconds (default: 2 hours)"
|
|
59
|
-
/>
|
|
60
|
-
</Stack>
|
|
85
|
+
<Stack spacing={2} direction="row" sx={{ mt: 4 }}>
|
|
86
|
+
<TextField source="created_at" />
|
|
87
|
+
<TextField source="updated_at" />
|
|
88
|
+
</Stack>
|
|
89
|
+
</TabbedForm.Tab>
|
|
61
90
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
91
|
+
<TabbedForm.Tab label="RBAC">
|
|
92
|
+
<Stack spacing={3}>
|
|
93
|
+
<BooleanInput
|
|
94
|
+
source="options.enforce_policies"
|
|
95
|
+
label="Enable RBAC"
|
|
96
|
+
helperText="Enable Role-Based Access Control for this resource server"
|
|
97
|
+
disabled={isSystem}
|
|
98
|
+
/>
|
|
67
99
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
<FormDataConsumer>
|
|
101
|
+
{({ formData }) => (
|
|
102
|
+
<BooleanInput
|
|
103
|
+
source="options.token_dialect"
|
|
104
|
+
label="Add permissions in token"
|
|
105
|
+
helperText="Include permissions directly in the access token"
|
|
106
|
+
disabled={isSystem || !formData?.options?.enforce_policies}
|
|
107
|
+
format={(value) => value === "access_token_authz"}
|
|
108
|
+
parse={(checked) =>
|
|
109
|
+
checked ? "access_token_authz" : "access_token"
|
|
110
|
+
}
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
</FormDataConsumer>
|
|
114
|
+
</Stack>
|
|
115
|
+
</TabbedForm.Tab>
|
|
75
116
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
117
|
+
<TabbedForm.Tab label="Scopes">
|
|
118
|
+
<ArrayInput source="scopes" label="" disabled={isSystem}>
|
|
119
|
+
<SimpleFormIterator disableAdd={isSystem} disableRemove={isSystem} disableReordering={isSystem}>
|
|
120
|
+
<Stack
|
|
121
|
+
spacing={2}
|
|
122
|
+
direction="row"
|
|
123
|
+
sx={{ width: "100%", alignItems: "flex-start" }}
|
|
124
|
+
>
|
|
125
|
+
<TextInput
|
|
126
|
+
source="value"
|
|
127
|
+
validate={[required()]}
|
|
128
|
+
label="Scope Name"
|
|
129
|
+
helperText="e.g., read:users, write:posts"
|
|
130
|
+
sx={{ flex: 1 }}
|
|
131
|
+
disabled={isSystem}
|
|
132
|
+
/>
|
|
133
|
+
<TextInput
|
|
134
|
+
source="description"
|
|
135
|
+
label="Description"
|
|
136
|
+
helperText="What this scope allows"
|
|
137
|
+
sx={{ flex: 2 }}
|
|
138
|
+
disabled={isSystem}
|
|
139
|
+
/>
|
|
140
|
+
</Stack>
|
|
141
|
+
</SimpleFormIterator>
|
|
142
|
+
</ArrayInput>
|
|
143
|
+
</TabbedForm.Tab>
|
|
144
|
+
</TabbedForm>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
92
147
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
spacing={2}
|
|
98
|
-
direction="row"
|
|
99
|
-
sx={{ width: "100%", alignItems: "flex-start" }}
|
|
100
|
-
>
|
|
101
|
-
<TextInput
|
|
102
|
-
source="value"
|
|
103
|
-
validate={[required()]}
|
|
104
|
-
label="Scope Name"
|
|
105
|
-
helperText="e.g., read:users, write:posts"
|
|
106
|
-
sx={{ flex: 1 }}
|
|
107
|
-
/>
|
|
108
|
-
<TextInput
|
|
109
|
-
source="description"
|
|
110
|
-
label="Description"
|
|
111
|
-
helperText="What this scope allows"
|
|
112
|
-
sx={{ flex: 2 }}
|
|
113
|
-
/>
|
|
114
|
-
</Stack>
|
|
115
|
-
</SimpleFormIterator>
|
|
116
|
-
</ArrayInput>
|
|
117
|
-
</TabbedForm.Tab>
|
|
118
|
-
</TabbedForm>
|
|
148
|
+
export function ResourceServerEdit() {
|
|
149
|
+
return (
|
|
150
|
+
<Edit>
|
|
151
|
+
<ResourceServerForm />
|
|
119
152
|
</Edit>
|
|
120
153
|
);
|
|
121
154
|
}
|