@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.
Files changed (110) hide show
  1. package/.eslintrc.js +21 -0
  2. package/.vercelignore +4 -0
  3. package/CHANGELOG.md +56 -0
  4. package/LICENSE +21 -0
  5. package/README.md +50 -0
  6. package/index.html +125 -0
  7. package/package.json +61 -0
  8. package/prettier.config.js +1 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/manifest.json +15 -0
  11. package/src/App.spec.tsx +42 -0
  12. package/src/App.tsx +232 -0
  13. package/src/AuthCallback.tsx +138 -0
  14. package/src/Layout.tsx +12 -0
  15. package/src/TenantsApp.tsx +115 -0
  16. package/src/auth0DataProvider.ts +1242 -0
  17. package/src/authProvider.ts +521 -0
  18. package/src/components/CertificateErrorDialog.tsx +116 -0
  19. package/src/components/DomainSelector.tsx +401 -0
  20. package/src/components/TenantAppBar.tsx +83 -0
  21. package/src/components/TenantLayout.tsx +25 -0
  22. package/src/components/TenantsAppBar.tsx +21 -0
  23. package/src/components/TenantsLayout.tsx +28 -0
  24. package/src/components/activity/ActivityDashboard.tsx +381 -0
  25. package/src/components/activity/index.ts +1 -0
  26. package/src/components/branding/BrandingList.tsx +0 -0
  27. package/src/components/branding/BrandingShow.tsx +0 -0
  28. package/src/components/branding/ThemesTab.tsx +286 -0
  29. package/src/components/branding/edit.tsx +149 -0
  30. package/src/components/branding/hooks/useThemesData.ts +123 -0
  31. package/src/components/branding/index.ts +2 -0
  32. package/src/components/branding/list.tsx +12 -0
  33. package/src/components/clients/create.tsx +12 -0
  34. package/src/components/clients/edit.tsx +1285 -0
  35. package/src/components/clients/index.ts +3 -0
  36. package/src/components/clients/list.tsx +37 -0
  37. package/src/components/common/DateAgo.tsx +6 -0
  38. package/src/components/common/JsonOutput.tsx +26 -0
  39. package/src/components/common/index.ts +1 -0
  40. package/src/components/connections/create.tsx +35 -0
  41. package/src/components/connections/edit.tsx +212 -0
  42. package/src/components/connections/index.ts +3 -0
  43. package/src/components/connections/list.tsx +15 -0
  44. package/src/components/custom-domains/create.tsx +26 -0
  45. package/src/components/custom-domains/edit.tsx +101 -0
  46. package/src/components/custom-domains/index.ts +3 -0
  47. package/src/components/custom-domains/list.tsx +16 -0
  48. package/src/components/flows/create.tsx +30 -0
  49. package/src/components/flows/edit.tsx +238 -0
  50. package/src/components/flows/index.ts +3 -0
  51. package/src/components/flows/list.tsx +15 -0
  52. package/src/components/forms/FlowEditor.tsx +1363 -0
  53. package/src/components/forms/NodeEditor.tsx +1119 -0
  54. package/src/components/forms/RichTextEditor.tsx +145 -0
  55. package/src/components/forms/create.tsx +30 -0
  56. package/src/components/forms/edit.tsx +256 -0
  57. package/src/components/forms/index.ts +3 -0
  58. package/src/components/forms/list.tsx +16 -0
  59. package/src/components/hooks/create.tsx +96 -0
  60. package/src/components/hooks/edit.tsx +114 -0
  61. package/src/components/hooks/index.ts +3 -0
  62. package/src/components/hooks/list.tsx +17 -0
  63. package/src/components/listActions/PostListActions.tsx +10 -0
  64. package/src/components/logs/LogIcon.tsx +32 -0
  65. package/src/components/logs/LogShow.tsx +82 -0
  66. package/src/components/logs/LogType.tsx +38 -0
  67. package/src/components/logs/index.ts +4 -0
  68. package/src/components/logs/list.tsx +41 -0
  69. package/src/components/organizations/create.tsx +13 -0
  70. package/src/components/organizations/edit.tsx +682 -0
  71. package/src/components/organizations/index.ts +3 -0
  72. package/src/components/organizations/list.tsx +21 -0
  73. package/src/components/resource-servers/create.tsx +87 -0
  74. package/src/components/resource-servers/edit.tsx +121 -0
  75. package/src/components/resource-servers/index.ts +3 -0
  76. package/src/components/resource-servers/list.tsx +47 -0
  77. package/src/components/roles/create.tsx +12 -0
  78. package/src/components/roles/edit.tsx +426 -0
  79. package/src/components/roles/index.ts +3 -0
  80. package/src/components/roles/list.tsx +24 -0
  81. package/src/components/sessions/edit.tsx +101 -0
  82. package/src/components/sessions/index.ts +3 -0
  83. package/src/components/sessions/list.tsx +20 -0
  84. package/src/components/sessions/show.tsx +113 -0
  85. package/src/components/settings/edit.tsx +236 -0
  86. package/src/components/settings/index.ts +2 -0
  87. package/src/components/settings/list.tsx +14 -0
  88. package/src/components/tenants/create.tsx +20 -0
  89. package/src/components/tenants/edit.tsx +54 -0
  90. package/src/components/tenants/index.ts +2 -0
  91. package/src/components/tenants/list.tsx +67 -0
  92. package/src/components/themes/edit.tsx +200 -0
  93. package/src/components/themes/index.ts +2 -0
  94. package/src/components/themes/list.tsx +12 -0
  95. package/src/components/users/create.tsx +144 -0
  96. package/src/components/users/edit.tsx +1711 -0
  97. package/src/components/users/index.ts +3 -0
  98. package/src/components/users/list.tsx +35 -0
  99. package/src/data.json +121 -0
  100. package/src/dataProvider.ts +97 -0
  101. package/src/index.tsx +106 -0
  102. package/src/lib/logs.ts +21 -0
  103. package/src/types/reactflow.d.ts +86 -0
  104. package/src/utils/domainUtils.ts +169 -0
  105. package/src/utils/tokenUtils.ts +75 -0
  106. package/src/vite-env.d.ts +1 -0
  107. package/tsconfig.json +37 -0
  108. package/tsconfig.node.json +10 -0
  109. package/vercel.json +17 -0
  110. 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,3 @@
1
+ export { ResourceServerEdit } from "./edit";
2
+ export { ResourceServerList } from "./list";
3
+ export { ResourceServerCreate } from "./create";
@@ -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
+ }