@authhero/react-admin 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +21 -0
- package/.vercelignore +4 -0
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/index.html +125 -0
- package/package.json +61 -0
- package/prettier.config.js +1 -0
- package/public/favicon.ico +0 -0
- package/public/manifest.json +15 -0
- package/src/App.spec.tsx +42 -0
- package/src/App.tsx +232 -0
- package/src/AuthCallback.tsx +138 -0
- package/src/Layout.tsx +12 -0
- package/src/TenantsApp.tsx +115 -0
- package/src/auth0DataProvider.ts +1242 -0
- package/src/authProvider.ts +521 -0
- package/src/components/CertificateErrorDialog.tsx +116 -0
- package/src/components/DomainSelector.tsx +401 -0
- package/src/components/TenantAppBar.tsx +83 -0
- package/src/components/TenantLayout.tsx +25 -0
- package/src/components/TenantsAppBar.tsx +21 -0
- package/src/components/TenantsLayout.tsx +28 -0
- package/src/components/activity/ActivityDashboard.tsx +381 -0
- package/src/components/activity/index.ts +1 -0
- package/src/components/branding/BrandingList.tsx +0 -0
- package/src/components/branding/BrandingShow.tsx +0 -0
- package/src/components/branding/ThemesTab.tsx +286 -0
- package/src/components/branding/edit.tsx +149 -0
- package/src/components/branding/hooks/useThemesData.ts +123 -0
- package/src/components/branding/index.ts +2 -0
- package/src/components/branding/list.tsx +12 -0
- package/src/components/clients/create.tsx +12 -0
- package/src/components/clients/edit.tsx +1285 -0
- package/src/components/clients/index.ts +3 -0
- package/src/components/clients/list.tsx +37 -0
- package/src/components/common/DateAgo.tsx +6 -0
- package/src/components/common/JsonOutput.tsx +26 -0
- package/src/components/common/index.ts +1 -0
- package/src/components/connections/create.tsx +35 -0
- package/src/components/connections/edit.tsx +212 -0
- package/src/components/connections/index.ts +3 -0
- package/src/components/connections/list.tsx +15 -0
- package/src/components/custom-domains/create.tsx +26 -0
- package/src/components/custom-domains/edit.tsx +101 -0
- package/src/components/custom-domains/index.ts +3 -0
- package/src/components/custom-domains/list.tsx +16 -0
- package/src/components/flows/create.tsx +30 -0
- package/src/components/flows/edit.tsx +238 -0
- package/src/components/flows/index.ts +3 -0
- package/src/components/flows/list.tsx +15 -0
- package/src/components/forms/FlowEditor.tsx +1363 -0
- package/src/components/forms/NodeEditor.tsx +1119 -0
- package/src/components/forms/RichTextEditor.tsx +145 -0
- package/src/components/forms/create.tsx +30 -0
- package/src/components/forms/edit.tsx +256 -0
- package/src/components/forms/index.ts +3 -0
- package/src/components/forms/list.tsx +16 -0
- package/src/components/hooks/create.tsx +96 -0
- package/src/components/hooks/edit.tsx +114 -0
- package/src/components/hooks/index.ts +3 -0
- package/src/components/hooks/list.tsx +17 -0
- package/src/components/listActions/PostListActions.tsx +10 -0
- package/src/components/logs/LogIcon.tsx +32 -0
- package/src/components/logs/LogShow.tsx +82 -0
- package/src/components/logs/LogType.tsx +38 -0
- package/src/components/logs/index.ts +4 -0
- package/src/components/logs/list.tsx +41 -0
- package/src/components/organizations/create.tsx +13 -0
- package/src/components/organizations/edit.tsx +682 -0
- package/src/components/organizations/index.ts +3 -0
- package/src/components/organizations/list.tsx +21 -0
- package/src/components/resource-servers/create.tsx +87 -0
- package/src/components/resource-servers/edit.tsx +121 -0
- package/src/components/resource-servers/index.ts +3 -0
- package/src/components/resource-servers/list.tsx +47 -0
- package/src/components/roles/create.tsx +12 -0
- package/src/components/roles/edit.tsx +426 -0
- package/src/components/roles/index.ts +3 -0
- package/src/components/roles/list.tsx +24 -0
- package/src/components/sessions/edit.tsx +101 -0
- package/src/components/sessions/index.ts +3 -0
- package/src/components/sessions/list.tsx +20 -0
- package/src/components/sessions/show.tsx +113 -0
- package/src/components/settings/edit.tsx +236 -0
- package/src/components/settings/index.ts +2 -0
- package/src/components/settings/list.tsx +14 -0
- package/src/components/tenants/create.tsx +20 -0
- package/src/components/tenants/edit.tsx +54 -0
- package/src/components/tenants/index.ts +2 -0
- package/src/components/tenants/list.tsx +67 -0
- package/src/components/themes/edit.tsx +200 -0
- package/src/components/themes/index.ts +2 -0
- package/src/components/themes/list.tsx +12 -0
- package/src/components/users/create.tsx +144 -0
- package/src/components/users/edit.tsx +1711 -0
- package/src/components/users/index.ts +3 -0
- package/src/components/users/list.tsx +35 -0
- package/src/data.json +121 -0
- package/src/dataProvider.ts +97 -0
- package/src/index.tsx +106 -0
- package/src/lib/logs.ts +21 -0
- package/src/types/reactflow.d.ts +86 -0
- package/src/utils/domainUtils.ts +169 -0
- package/src/utils/tokenUtils.ts +75 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +37 -0
- package/tsconfig.node.json +10 -0
- package/vercel.json +17 -0
- package/vite.config.ts +30 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Create,
|
|
3
|
+
SimpleForm,
|
|
4
|
+
TextInput,
|
|
5
|
+
BooleanInput,
|
|
6
|
+
ArrayInput,
|
|
7
|
+
SimpleFormIterator,
|
|
8
|
+
required,
|
|
9
|
+
NumberInput,
|
|
10
|
+
} from "react-admin";
|
|
11
|
+
import { Stack } from "@mui/material";
|
|
12
|
+
|
|
13
|
+
export function ResourceServerCreate() {
|
|
14
|
+
return (
|
|
15
|
+
<Create>
|
|
16
|
+
<SimpleForm>
|
|
17
|
+
<Stack spacing={2}>
|
|
18
|
+
<TextInput source="name" validate={[required()]} />
|
|
19
|
+
<TextInput
|
|
20
|
+
source="identifier"
|
|
21
|
+
validate={[required()]}
|
|
22
|
+
helperText="Unique identifier for this resource server"
|
|
23
|
+
/>
|
|
24
|
+
</Stack>
|
|
25
|
+
|
|
26
|
+
<Stack spacing={2} direction="row" sx={{ mt: 2 }}>
|
|
27
|
+
<BooleanInput source="allow_offline_access" defaultValue={true} />
|
|
28
|
+
<BooleanInput
|
|
29
|
+
source="skip_consent_for_verifiable_first_party_clients"
|
|
30
|
+
defaultValue={true}
|
|
31
|
+
/>
|
|
32
|
+
<BooleanInput source="options.enforce_policies" defaultValue={true} />
|
|
33
|
+
</Stack>
|
|
34
|
+
|
|
35
|
+
<Stack spacing={2} direction="row" sx={{ mt: 2 }}>
|
|
36
|
+
<TextInput
|
|
37
|
+
source="signing_alg"
|
|
38
|
+
defaultValue="RS256"
|
|
39
|
+
helperText="Signing algorithm for tokens"
|
|
40
|
+
/>
|
|
41
|
+
<TextInput
|
|
42
|
+
source="options.token_dialect"
|
|
43
|
+
defaultValue="access_token_authz"
|
|
44
|
+
helperText="Token dialect format"
|
|
45
|
+
/>
|
|
46
|
+
</Stack>
|
|
47
|
+
|
|
48
|
+
<Stack spacing={2} direction="row" sx={{ mt: 2 }}>
|
|
49
|
+
<NumberInput
|
|
50
|
+
source="token_lifetime"
|
|
51
|
+
defaultValue={1209600}
|
|
52
|
+
helperText="Token lifetime in seconds (default: 14 days)"
|
|
53
|
+
/>
|
|
54
|
+
<NumberInput
|
|
55
|
+
source="token_lifetime_for_web"
|
|
56
|
+
defaultValue={7200}
|
|
57
|
+
helperText="Web token lifetime in seconds (default: 2 hours)"
|
|
58
|
+
/>
|
|
59
|
+
</Stack>
|
|
60
|
+
|
|
61
|
+
<ArrayInput source="scopes" label="Scopes">
|
|
62
|
+
<SimpleFormIterator>
|
|
63
|
+
<Stack
|
|
64
|
+
spacing={2}
|
|
65
|
+
direction="row"
|
|
66
|
+
sx={{ width: "100%", alignItems: "flex-start" }}
|
|
67
|
+
>
|
|
68
|
+
<TextInput
|
|
69
|
+
source="value"
|
|
70
|
+
validate={[required()]}
|
|
71
|
+
label="Scope Name"
|
|
72
|
+
helperText="e.g., read:users, write:posts"
|
|
73
|
+
sx={{ flex: 1 }}
|
|
74
|
+
/>
|
|
75
|
+
<TextInput
|
|
76
|
+
source="description"
|
|
77
|
+
label="Description"
|
|
78
|
+
helperText="What this scope allows"
|
|
79
|
+
sx={{ flex: 2 }}
|
|
80
|
+
/>
|
|
81
|
+
</Stack>
|
|
82
|
+
</SimpleFormIterator>
|
|
83
|
+
</ArrayInput>
|
|
84
|
+
</SimpleForm>
|
|
85
|
+
</Create>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Edit,
|
|
3
|
+
TextInput,
|
|
4
|
+
BooleanInput,
|
|
5
|
+
ArrayInput,
|
|
6
|
+
SimpleFormIterator,
|
|
7
|
+
TextField,
|
|
8
|
+
TabbedForm,
|
|
9
|
+
required,
|
|
10
|
+
NumberInput,
|
|
11
|
+
FormDataConsumer,
|
|
12
|
+
} from "react-admin";
|
|
13
|
+
import { Stack } from "@mui/material";
|
|
14
|
+
|
|
15
|
+
export function ResourceServerEdit() {
|
|
16
|
+
return (
|
|
17
|
+
<Edit>
|
|
18
|
+
<TabbedForm>
|
|
19
|
+
<TabbedForm.Tab label="Details">
|
|
20
|
+
<Stack spacing={2}>
|
|
21
|
+
<TextInput source="name" validate={[required()]} />
|
|
22
|
+
<TextInput
|
|
23
|
+
source="identifier"
|
|
24
|
+
validate={[required()]}
|
|
25
|
+
helperText="Unique identifier for this resource server"
|
|
26
|
+
/>
|
|
27
|
+
</Stack>
|
|
28
|
+
|
|
29
|
+
<Stack spacing={2} direction="row" sx={{ mt: 2 }}>
|
|
30
|
+
<BooleanInput
|
|
31
|
+
source="signing_alg_values_supported"
|
|
32
|
+
defaultValue={true}
|
|
33
|
+
/>
|
|
34
|
+
<BooleanInput
|
|
35
|
+
source="skip_consent_for_verifiable_first_party_clients"
|
|
36
|
+
defaultValue={true}
|
|
37
|
+
/>
|
|
38
|
+
<BooleanInput source="allow_offline_access" defaultValue={true} />
|
|
39
|
+
</Stack>
|
|
40
|
+
|
|
41
|
+
<Stack spacing={2} direction="row" sx={{ mt: 2 }}>
|
|
42
|
+
<TextInput
|
|
43
|
+
source="signing_alg"
|
|
44
|
+
defaultValue="RS256"
|
|
45
|
+
helperText="Signing algorithm for tokens"
|
|
46
|
+
/>
|
|
47
|
+
</Stack>
|
|
48
|
+
|
|
49
|
+
<Stack spacing={2} direction="row" sx={{ mt: 2 }}>
|
|
50
|
+
<NumberInput
|
|
51
|
+
source="token_lifetime"
|
|
52
|
+
defaultValue={1209600}
|
|
53
|
+
helperText="Token lifetime in seconds (default: 14 days)"
|
|
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>
|
|
61
|
+
|
|
62
|
+
<Stack spacing={2} direction="row" sx={{ mt: 4 }}>
|
|
63
|
+
<TextField source="created_at" />
|
|
64
|
+
<TextField source="updated_at" />
|
|
65
|
+
</Stack>
|
|
66
|
+
</TabbedForm.Tab>
|
|
67
|
+
|
|
68
|
+
<TabbedForm.Tab label="RBAC">
|
|
69
|
+
<Stack spacing={3}>
|
|
70
|
+
<BooleanInput
|
|
71
|
+
source="options.enforce_policies"
|
|
72
|
+
label="Enable RBAC"
|
|
73
|
+
helperText="Enable Role-Based Access Control for this resource server"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<FormDataConsumer>
|
|
77
|
+
{({ formData }) => (
|
|
78
|
+
<BooleanInput
|
|
79
|
+
source="options.token_dialect"
|
|
80
|
+
label="Add permissions in token"
|
|
81
|
+
helperText="Include permissions directly in the access token"
|
|
82
|
+
disabled={!formData?.options?.enforce_policies}
|
|
83
|
+
format={(value) => value === "access_token_authz"}
|
|
84
|
+
parse={(checked) =>
|
|
85
|
+
checked ? "access_token_authz" : "access_token"
|
|
86
|
+
}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
</FormDataConsumer>
|
|
90
|
+
</Stack>
|
|
91
|
+
</TabbedForm.Tab>
|
|
92
|
+
|
|
93
|
+
<TabbedForm.Tab label="Scopes">
|
|
94
|
+
<ArrayInput source="scopes" label="">
|
|
95
|
+
<SimpleFormIterator>
|
|
96
|
+
<Stack
|
|
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>
|
|
119
|
+
</Edit>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
List,
|
|
3
|
+
Datagrid,
|
|
4
|
+
TextField,
|
|
5
|
+
DateField,
|
|
6
|
+
FunctionField,
|
|
7
|
+
CreateButton,
|
|
8
|
+
TopToolbar,
|
|
9
|
+
} from "react-admin";
|
|
10
|
+
|
|
11
|
+
const ResourceServerActions = () => (
|
|
12
|
+
<TopToolbar>
|
|
13
|
+
<CreateButton />
|
|
14
|
+
</TopToolbar>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export function ResourceServerList() {
|
|
18
|
+
return (
|
|
19
|
+
<List actions={<ResourceServerActions />}>
|
|
20
|
+
<Datagrid rowClick="edit">
|
|
21
|
+
<TextField source="name" />
|
|
22
|
+
<TextField source="identifier" />
|
|
23
|
+
<FunctionField
|
|
24
|
+
source="scopes"
|
|
25
|
+
label="Scopes"
|
|
26
|
+
render={(record: any) => {
|
|
27
|
+
if (!record.scopes || record.scopes.length === 0) {
|
|
28
|
+
return "No scopes";
|
|
29
|
+
}
|
|
30
|
+
return `${record.scopes.length} scope${record.scopes.length === 1 ? "" : "s"}`;
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
<FunctionField
|
|
34
|
+
source="allow_offline_access"
|
|
35
|
+
label="Offline Access"
|
|
36
|
+
render={(record: any) => (record.allow_offline_access ? "Yes" : "No")}
|
|
37
|
+
/>
|
|
38
|
+
<FunctionField
|
|
39
|
+
source="token_lifetime"
|
|
40
|
+
label="Token Lifetime"
|
|
41
|
+
render={(record: any) => `${record.token_lifetime || 86400}s`}
|
|
42
|
+
/>
|
|
43
|
+
<DateField source="created_at" showTime />
|
|
44
|
+
</Datagrid>
|
|
45
|
+
</List>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Create, SimpleForm, TextInput, required } from "react-admin";
|
|
2
|
+
|
|
3
|
+
export function RoleCreate() {
|
|
4
|
+
return (
|
|
5
|
+
<Create>
|
|
6
|
+
<SimpleForm>
|
|
7
|
+
<TextInput source="name" validate={[required()]} />
|
|
8
|
+
<TextInput source="description" multiline />
|
|
9
|
+
</SimpleForm>
|
|
10
|
+
</Create>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Edit,
|
|
3
|
+
TextInput,
|
|
4
|
+
required,
|
|
5
|
+
TabbedForm,
|
|
6
|
+
ReferenceManyField,
|
|
7
|
+
Datagrid,
|
|
8
|
+
Pagination,
|
|
9
|
+
TextField,
|
|
10
|
+
FunctionField,
|
|
11
|
+
useDataProvider,
|
|
12
|
+
useNotify,
|
|
13
|
+
useRefresh,
|
|
14
|
+
useRecordContext,
|
|
15
|
+
} from "react-admin";
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import {
|
|
18
|
+
Box,
|
|
19
|
+
Button,
|
|
20
|
+
Dialog,
|
|
21
|
+
DialogActions,
|
|
22
|
+
DialogContent,
|
|
23
|
+
DialogContentText,
|
|
24
|
+
DialogTitle,
|
|
25
|
+
Typography,
|
|
26
|
+
Autocomplete,
|
|
27
|
+
CircularProgress,
|
|
28
|
+
IconButton,
|
|
29
|
+
TextField as MuiTextField,
|
|
30
|
+
} from "@mui/material";
|
|
31
|
+
import AddIcon from "@mui/icons-material/Add";
|
|
32
|
+
import DeleteIcon from "@mui/icons-material/Delete";
|
|
33
|
+
import { DateAgo } from "../common";
|
|
34
|
+
import { useParams } from "react-router-dom";
|
|
35
|
+
|
|
36
|
+
const AddRolePermissionButton = () => {
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
const [resourceServers, setResourceServers] = useState<any[]>([]);
|
|
39
|
+
const [selectedResourceServer, setSelectedResourceServer] =
|
|
40
|
+
useState<any>(null);
|
|
41
|
+
const [availablePermissions, setAvailablePermissions] = useState<any[]>([]);
|
|
42
|
+
const [selectedPermissions, setSelectedPermissions] = useState<any[]>([]);
|
|
43
|
+
const [loading, setLoading] = useState(false);
|
|
44
|
+
const [loadingPermissions, setLoadingPermissions] = useState(false);
|
|
45
|
+
const dataProvider = useDataProvider();
|
|
46
|
+
const notify = useNotify();
|
|
47
|
+
const refresh = useRefresh();
|
|
48
|
+
|
|
49
|
+
// Get role id from the route params (/:tenantId/roles/:id)
|
|
50
|
+
const { id: roleId } = useParams();
|
|
51
|
+
|
|
52
|
+
const handleOpen = async () => {
|
|
53
|
+
setOpen(true);
|
|
54
|
+
await loadResourceServers();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleClose = () => {
|
|
58
|
+
setOpen(false);
|
|
59
|
+
setSelectedResourceServer(null);
|
|
60
|
+
setAvailablePermissions([]);
|
|
61
|
+
setSelectedPermissions([]);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const loadResourceServers = async () => {
|
|
65
|
+
setLoading(true);
|
|
66
|
+
try {
|
|
67
|
+
const { data } = await dataProvider.getList("resource-servers", {
|
|
68
|
+
pagination: { page: 1, perPage: 100 },
|
|
69
|
+
sort: { field: "name", order: "ASC" },
|
|
70
|
+
filter: {},
|
|
71
|
+
});
|
|
72
|
+
setResourceServers(data);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error("Error loading resource servers:", error);
|
|
75
|
+
notify("Error loading resource servers", { type: "error" });
|
|
76
|
+
} finally {
|
|
77
|
+
setLoading(false);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const loadPermissions = async (resourceServer: any) => {
|
|
82
|
+
setLoadingPermissions(true);
|
|
83
|
+
try {
|
|
84
|
+
// Build available scopes from the selected resource server's property
|
|
85
|
+
const allScopes = (resourceServer?.scopes || []).map((s: any) => ({
|
|
86
|
+
permission_name: s?.permission_name ?? s?.value ?? s,
|
|
87
|
+
description: s?.description ?? "",
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
// Fetch the role's existing permissions from /roles/:id/permissions
|
|
91
|
+
const existingRes = await dataProvider.getList(
|
|
92
|
+
`roles/${roleId}/permissions`,
|
|
93
|
+
{
|
|
94
|
+
pagination: { page: 1, perPage: 200 },
|
|
95
|
+
sort: { field: "permission_name", order: "ASC" },
|
|
96
|
+
filter: {},
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const existingAll = existingRes.data ?? [];
|
|
101
|
+
// Narrow to the selected resource server
|
|
102
|
+
const existingForServer = existingAll.filter((p: any) => {
|
|
103
|
+
const identifier =
|
|
104
|
+
p.resource_server_identifier ?? p.resource_server_id ?? p.audience;
|
|
105
|
+
return identifier === resourceServer?.identifier;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const existingSet = new Set(
|
|
109
|
+
existingForServer.map((p: any) => p.permission_name),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Filter out scopes the role already has
|
|
113
|
+
const filtered = allScopes.filter(
|
|
114
|
+
(p: any) => p.permission_name && !existingSet.has(p.permission_name),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
setAvailablePermissions(filtered);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error("Error loading permissions:", error);
|
|
120
|
+
notify("Error loading permissions", { type: "error" });
|
|
121
|
+
} finally {
|
|
122
|
+
setLoadingPermissions(false);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleResourceServerChange = (resourceServer: any) => {
|
|
127
|
+
setSelectedResourceServer(resourceServer);
|
|
128
|
+
setSelectedPermissions([]);
|
|
129
|
+
if (resourceServer) {
|
|
130
|
+
loadPermissions(resourceServer);
|
|
131
|
+
} else {
|
|
132
|
+
setAvailablePermissions([]);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleAddPermissions = async () => {
|
|
137
|
+
if (!roleId || selectedPermissions.length === 0) {
|
|
138
|
+
notify("Please select at least one permission", { type: "warning" });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const payload = {
|
|
144
|
+
permissions: selectedPermissions.map((permission: any) => ({
|
|
145
|
+
permission_name: permission.permission_name,
|
|
146
|
+
resource_server_identifier: selectedResourceServer.identifier,
|
|
147
|
+
})),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
await dataProvider.create(`roles/${roleId}/permissions`, {
|
|
151
|
+
data: payload,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
notify(`${selectedPermissions.length} permission(s) added successfully`, {
|
|
155
|
+
type: "success",
|
|
156
|
+
});
|
|
157
|
+
handleClose();
|
|
158
|
+
refresh();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error("Error adding permissions:", error);
|
|
161
|
+
notify("Error adding permissions", { type: "error" });
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (!roleId) return null;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<>
|
|
169
|
+
<Button
|
|
170
|
+
variant="contained"
|
|
171
|
+
color="primary"
|
|
172
|
+
startIcon={<AddIcon />}
|
|
173
|
+
onClick={handleOpen}
|
|
174
|
+
sx={{ mb: 2 }}
|
|
175
|
+
>
|
|
176
|
+
Add Permission
|
|
177
|
+
</Button>
|
|
178
|
+
|
|
179
|
+
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
|
|
180
|
+
<DialogTitle>Add Permissions</DialogTitle>
|
|
181
|
+
<DialogContent>
|
|
182
|
+
<Typography variant="body2" sx={{ mb: 3 }}>
|
|
183
|
+
Select a resource server and permissions to assign to this role
|
|
184
|
+
</Typography>
|
|
185
|
+
|
|
186
|
+
<Box sx={{ mb: 3 }}>
|
|
187
|
+
<Autocomplete
|
|
188
|
+
options={resourceServers}
|
|
189
|
+
getOptionLabel={(option) => option.name || option.identifier}
|
|
190
|
+
value={selectedResourceServer}
|
|
191
|
+
onChange={(_, value) => handleResourceServerChange(value)}
|
|
192
|
+
loading={loading}
|
|
193
|
+
isOptionEqualToValue={(option, value) =>
|
|
194
|
+
!!option &&
|
|
195
|
+
!!value &&
|
|
196
|
+
(option.id === value.id ||
|
|
197
|
+
option.identifier === value.identifier ||
|
|
198
|
+
option.audience === value.audience)
|
|
199
|
+
}
|
|
200
|
+
renderInput={(params) => (
|
|
201
|
+
<MuiTextField
|
|
202
|
+
{...params}
|
|
203
|
+
label="Resource Server"
|
|
204
|
+
variant="outlined"
|
|
205
|
+
fullWidth
|
|
206
|
+
InputProps={{
|
|
207
|
+
...params.InputProps,
|
|
208
|
+
endAdornment: (
|
|
209
|
+
<>
|
|
210
|
+
{loading ? (
|
|
211
|
+
<CircularProgress color="inherit" size={20} />
|
|
212
|
+
) : null}
|
|
213
|
+
{params.InputProps.endAdornment}
|
|
214
|
+
</>
|
|
215
|
+
),
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
/>
|
|
220
|
+
</Box>
|
|
221
|
+
|
|
222
|
+
{selectedResourceServer && (
|
|
223
|
+
<>
|
|
224
|
+
<Box sx={{ mb: 3 }}>
|
|
225
|
+
<Autocomplete
|
|
226
|
+
multiple
|
|
227
|
+
options={availablePermissions}
|
|
228
|
+
getOptionLabel={(option) =>
|
|
229
|
+
`${option.permission_name} - ${option.description || "No description"}`
|
|
230
|
+
}
|
|
231
|
+
value={selectedPermissions}
|
|
232
|
+
onChange={(_, value) => setSelectedPermissions(value)}
|
|
233
|
+
loading={loadingPermissions}
|
|
234
|
+
isOptionEqualToValue={(option, value) =>
|
|
235
|
+
option?.permission_name === value?.permission_name
|
|
236
|
+
}
|
|
237
|
+
renderInput={(params) => (
|
|
238
|
+
<MuiTextField
|
|
239
|
+
{...params}
|
|
240
|
+
label="Permissions"
|
|
241
|
+
variant="outlined"
|
|
242
|
+
fullWidth
|
|
243
|
+
InputProps={{
|
|
244
|
+
...params.InputProps,
|
|
245
|
+
endAdornment: (
|
|
246
|
+
<>
|
|
247
|
+
{loadingPermissions ? (
|
|
248
|
+
<CircularProgress color="inherit" size={20} />
|
|
249
|
+
) : null}
|
|
250
|
+
{params.InputProps.endAdornment}
|
|
251
|
+
</>
|
|
252
|
+
),
|
|
253
|
+
}}
|
|
254
|
+
/>
|
|
255
|
+
)}
|
|
256
|
+
renderOption={(props, option) => (
|
|
257
|
+
<li {...props} key={option.permission_name}>
|
|
258
|
+
<Box>
|
|
259
|
+
<Typography variant="body2" fontWeight="medium">
|
|
260
|
+
{option.permission_name}
|
|
261
|
+
</Typography>
|
|
262
|
+
{option.description && (
|
|
263
|
+
<Typography variant="caption" color="text.secondary">
|
|
264
|
+
{option.description}
|
|
265
|
+
</Typography>
|
|
266
|
+
)}
|
|
267
|
+
</Box>
|
|
268
|
+
</li>
|
|
269
|
+
)}
|
|
270
|
+
/>
|
|
271
|
+
</Box>
|
|
272
|
+
|
|
273
|
+
{!loadingPermissions && availablePermissions.length === 0 && (
|
|
274
|
+
<Typography
|
|
275
|
+
variant="body2"
|
|
276
|
+
color="text.secondary"
|
|
277
|
+
sx={{ mb: 2 }}
|
|
278
|
+
>
|
|
279
|
+
This role already has all available scopes for the selected
|
|
280
|
+
resource server.
|
|
281
|
+
</Typography>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{selectedPermissions.length > 0 && (
|
|
285
|
+
<Box sx={{ mt: 2 }}>
|
|
286
|
+
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
|
287
|
+
Selected Permissions ({selectedPermissions.length}):
|
|
288
|
+
</Typography>
|
|
289
|
+
<Box sx={{ maxHeight: 200, overflow: "auto" }}>
|
|
290
|
+
{selectedPermissions.map((permission, index) => (
|
|
291
|
+
<Typography key={index} variant="body2" sx={{ ml: 2 }}>
|
|
292
|
+
• {permission.permission_name}
|
|
293
|
+
</Typography>
|
|
294
|
+
))}
|
|
295
|
+
</Box>
|
|
296
|
+
</Box>
|
|
297
|
+
)}
|
|
298
|
+
</>
|
|
299
|
+
)}
|
|
300
|
+
</DialogContent>
|
|
301
|
+
<DialogActions>
|
|
302
|
+
<Button onClick={handleClose}>Cancel</Button>
|
|
303
|
+
<Button
|
|
304
|
+
onClick={handleAddPermissions}
|
|
305
|
+
variant="contained"
|
|
306
|
+
disabled={selectedPermissions.length === 0}
|
|
307
|
+
>
|
|
308
|
+
Add {selectedPermissions.length} Permission(s)
|
|
309
|
+
</Button>
|
|
310
|
+
</DialogActions>
|
|
311
|
+
</Dialog>
|
|
312
|
+
</>
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const RemoveRolePermissionButton = () => {
|
|
317
|
+
const [open, setOpen] = useState(false);
|
|
318
|
+
const permission = useRecordContext();
|
|
319
|
+
const dataProvider = useDataProvider();
|
|
320
|
+
const notify = useNotify();
|
|
321
|
+
const refresh = useRefresh();
|
|
322
|
+
const { id: roleId } = useParams();
|
|
323
|
+
|
|
324
|
+
if (!permission || !roleId) return null;
|
|
325
|
+
|
|
326
|
+
const handleOpen = () => setOpen(true);
|
|
327
|
+
const handleClose = () => setOpen(false);
|
|
328
|
+
|
|
329
|
+
const handleRemove = async () => {
|
|
330
|
+
try {
|
|
331
|
+
const permissionId = encodeURIComponent(
|
|
332
|
+
`${permission.resource_server_identifier}:${permission.permission_name}`,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
await dataProvider.delete(`roles/${roleId}/permissions`, {
|
|
336
|
+
id: permissionId,
|
|
337
|
+
previousData: permission,
|
|
338
|
+
});
|
|
339
|
+
notify("Permission removed successfully", { type: "success" });
|
|
340
|
+
handleClose();
|
|
341
|
+
refresh();
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error("Error removing permission:", error);
|
|
344
|
+
notify("Error removing permission", { type: "error" });
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<>
|
|
350
|
+
<IconButton onClick={handleOpen} color="error" size="small">
|
|
351
|
+
<DeleteIcon />
|
|
352
|
+
</IconButton>
|
|
353
|
+
|
|
354
|
+
<Dialog open={open} onClose={handleClose}>
|
|
355
|
+
<DialogTitle>Remove Permission</DialogTitle>
|
|
356
|
+
<DialogContent>
|
|
357
|
+
<DialogContentText>
|
|
358
|
+
Are you sure you want to remove the permission "
|
|
359
|
+
{permission.permission_name}" from resource server "
|
|
360
|
+
{permission.resource_server_name ||
|
|
361
|
+
permission.resource_server_identifier}
|
|
362
|
+
"? This action cannot be undone.
|
|
363
|
+
</DialogContentText>
|
|
364
|
+
</DialogContent>
|
|
365
|
+
<DialogActions>
|
|
366
|
+
<Button onClick={handleClose}>Cancel</Button>
|
|
367
|
+
<Button onClick={handleRemove} color="error" autoFocus>
|
|
368
|
+
Remove
|
|
369
|
+
</Button>
|
|
370
|
+
</DialogActions>
|
|
371
|
+
</Dialog>
|
|
372
|
+
</>
|
|
373
|
+
);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export function RoleEdit() {
|
|
377
|
+
return (
|
|
378
|
+
<Edit>
|
|
379
|
+
<TabbedForm>
|
|
380
|
+
<TabbedForm.Tab label="details">
|
|
381
|
+
<Box
|
|
382
|
+
sx={{
|
|
383
|
+
display: "grid",
|
|
384
|
+
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
|
385
|
+
gap: 2,
|
|
386
|
+
width: "100%",
|
|
387
|
+
}}
|
|
388
|
+
>
|
|
389
|
+
<TextInput source="id" disabled fullWidth />
|
|
390
|
+
<TextInput source="name" validate={[required()]} fullWidth />
|
|
391
|
+
<Box sx={{ gridColumn: "1 / -1" }}>
|
|
392
|
+
<TextInput source="description" multiline minRows={6} fullWidth />
|
|
393
|
+
</Box>
|
|
394
|
+
</Box>
|
|
395
|
+
</TabbedForm.Tab>
|
|
396
|
+
<TabbedForm.Tab label="permissions">
|
|
397
|
+
<AddRolePermissionButton />
|
|
398
|
+
<ReferenceManyField
|
|
399
|
+
reference="permissions"
|
|
400
|
+
target="role_id"
|
|
401
|
+
pagination={<Pagination />}
|
|
402
|
+
sort={{ field: "permission_name", order: "ASC" }}
|
|
403
|
+
>
|
|
404
|
+
<Datagrid rowClick="" bulkActionButtons={false}>
|
|
405
|
+
<TextField
|
|
406
|
+
source="resource_server_identifier"
|
|
407
|
+
label="Resource Server"
|
|
408
|
+
/>
|
|
409
|
+
<TextField source="resource_server_name" label="Resource Name" />
|
|
410
|
+
<TextField source="permission_name" label="Permission" />
|
|
411
|
+
<TextField source="description" label="Description" />
|
|
412
|
+
<FunctionField
|
|
413
|
+
source="created_at"
|
|
414
|
+
render={(record: any) =>
|
|
415
|
+
record.created_at ? <DateAgo date={record.created_at} /> : "-"
|
|
416
|
+
}
|
|
417
|
+
label="Assigned"
|
|
418
|
+
/>
|
|
419
|
+
<RemoveRolePermissionButton />
|
|
420
|
+
</Datagrid>
|
|
421
|
+
</ReferenceManyField>
|
|
422
|
+
</TabbedForm.Tab>
|
|
423
|
+
</TabbedForm>
|
|
424
|
+
</Edit>
|
|
425
|
+
);
|
|
426
|
+
}
|