strapi-plugin-keycloak-realm-users 1.0.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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +485 -0
  3. package/__tests__/constants.test.mjs +207 -0
  4. package/__tests__/mocks/strapi.mjs +182 -0
  5. package/__tests__/services/audit-log.test.mjs +283 -0
  6. package/__tests__/services/keycloak-client.test.mjs +651 -0
  7. package/__tests__/services/permission.test.mjs +374 -0
  8. package/__tests__/services/realm.test.mjs +415 -0
  9. package/__tests__/services/user.test.mjs +487 -0
  10. package/__tests__/utils/errors.test.mjs +109 -0
  11. package/admin/src/components/Initializer.jsx +14 -0
  12. package/admin/src/components/RealmBadge.jsx +17 -0
  13. package/admin/src/constants.js +14 -0
  14. package/admin/src/hooks/useAuditLogs.js +142 -0
  15. package/admin/src/hooks/useKeycloakRoles.js +182 -0
  16. package/admin/src/hooks/useKeycloakUsers.js +477 -0
  17. package/admin/src/hooks/useRealmAdmins.js +249 -0
  18. package/admin/src/hooks/useRealms.js +269 -0
  19. package/admin/src/index.js +46 -0
  20. package/admin/src/pages/App.jsx +21 -0
  21. package/admin/src/pages/AuditPage/index.jsx +213 -0
  22. package/admin/src/pages/RealmsPage/RealmEditPage.jsx +791 -0
  23. package/admin/src/pages/RealmsPage/RealmListPage.jsx +231 -0
  24. package/admin/src/pages/RealmsPage/index.jsx +7 -0
  25. package/admin/src/pages/UsersPage/UserEditPage.jsx +313 -0
  26. package/admin/src/pages/UsersPage/UserListPage.jsx +437 -0
  27. package/admin/src/pages/UsersPage/index.jsx +7 -0
  28. package/admin/src/pluginId.js +2 -0
  29. package/admin/src/translations/en.json +77 -0
  30. package/admin/src/translations/fr.json +77 -0
  31. package/babel.config.cjs +17 -0
  32. package/coverage/clover.xml +422 -0
  33. package/coverage/coverage-final.json +8 -0
  34. package/coverage/lcov-report/base.css +224 -0
  35. package/coverage/lcov-report/block-navigation.js +87 -0
  36. package/coverage/lcov-report/favicon.png +0 -0
  37. package/coverage/lcov-report/index.html +146 -0
  38. package/coverage/lcov-report/prettify.css +1 -0
  39. package/coverage/lcov-report/prettify.js +2 -0
  40. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  41. package/coverage/lcov-report/sorter.js +210 -0
  42. package/coverage/lcov-report/src/bootstrap.js.html +346 -0
  43. package/coverage/lcov-report/src/config/index.html +116 -0
  44. package/coverage/lcov-report/src/config/index.js.html +106 -0
  45. package/coverage/lcov-report/src/constants.js.html +850 -0
  46. package/coverage/lcov-report/src/content-types/audit-log/index.html +116 -0
  47. package/coverage/lcov-report/src/content-types/audit-log/index.js.html +94 -0
  48. package/coverage/lcov-report/src/content-types/index.html +116 -0
  49. package/coverage/lcov-report/src/content-types/index.js.html +112 -0
  50. package/coverage/lcov-report/src/content-types/realm-admin/index.html +116 -0
  51. package/coverage/lcov-report/src/content-types/realm-admin/index.js.html +94 -0
  52. package/coverage/lcov-report/src/content-types/realm-config/index.html +116 -0
  53. package/coverage/lcov-report/src/content-types/realm-config/index.js.html +94 -0
  54. package/coverage/lcov-report/src/controllers/audit.js.html +517 -0
  55. package/coverage/lcov-report/src/controllers/index.html +161 -0
  56. package/coverage/lcov-report/src/controllers/index.js.html +112 -0
  57. package/coverage/lcov-report/src/controllers/realm.js.html +1057 -0
  58. package/coverage/lcov-report/src/controllers/user.js.html +1324 -0
  59. package/coverage/lcov-report/src/destroy.js.html +100 -0
  60. package/coverage/lcov-report/src/index.html +116 -0
  61. package/coverage/lcov-report/src/policies/can-access-realm.js.html +163 -0
  62. package/coverage/lcov-report/src/policies/index.html +146 -0
  63. package/coverage/lcov-report/src/policies/index.js.html +106 -0
  64. package/coverage/lcov-report/src/policies/is-authenticated.js.html +100 -0
  65. package/coverage/lcov-report/src/register.js.html +106 -0
  66. package/coverage/lcov-report/src/routes/admin.js.html +844 -0
  67. package/coverage/lcov-report/src/routes/index.html +131 -0
  68. package/coverage/lcov-report/src/routes/index.js.html +109 -0
  69. package/coverage/lcov-report/src/services/audit-log.js.html +673 -0
  70. package/coverage/lcov-report/src/services/index.html +176 -0
  71. package/coverage/lcov-report/src/services/index.js.html +124 -0
  72. package/coverage/lcov-report/src/services/keycloak-client.js.html +2359 -0
  73. package/coverage/lcov-report/src/services/permission.js.html +955 -0
  74. package/coverage/lcov-report/src/services/realm.js.html +1207 -0
  75. package/coverage/lcov-report/src/services/user.js.html +1924 -0
  76. package/coverage/lcov-report/src/utils/errors.js.html +274 -0
  77. package/coverage/lcov-report/src/utils/index.html +116 -0
  78. package/coverage/lcov-report/src/utils/index.js.html +103 -0
  79. package/coverage/lcov.info +804 -0
  80. package/dist/_chunks/App-BaKrvCeS.mjs +1975 -0
  81. package/dist/_chunks/App-DO6syS77.js +1975 -0
  82. package/dist/_chunks/en-Li-XBDe9.mjs +72 -0
  83. package/dist/_chunks/en-aCyfgNfr.js +72 -0
  84. package/dist/_chunks/fr-Cj33Q8jI.js +72 -0
  85. package/dist/_chunks/fr-vLrXph-Z.mjs +72 -0
  86. package/dist/_chunks/index-DwDO4-0C.js +69 -0
  87. package/dist/_chunks/index-jTVd7LdQ.mjs +70 -0
  88. package/dist/admin/index.js +3 -0
  89. package/dist/admin/index.mjs +4 -0
  90. package/dist/server/index.js +3003 -0
  91. package/dist/server/index.mjs +3004 -0
  92. package/jest.config.cjs +50 -0
  93. package/package.json +55 -0
  94. package/server/src/bootstrap.js +87 -0
  95. package/server/src/config/index.js +7 -0
  96. package/server/src/constants.js +255 -0
  97. package/server/src/content-types/audit-log/index.js +3 -0
  98. package/server/src/content-types/audit-log/schema.json +61 -0
  99. package/server/src/content-types/index.js +9 -0
  100. package/server/src/content-types/realm-admin/index.js +3 -0
  101. package/server/src/content-types/realm-admin/schema.json +45 -0
  102. package/server/src/content-types/realm-config/index.js +3 -0
  103. package/server/src/content-types/realm-config/schema.json +56 -0
  104. package/server/src/controllers/audit.js +144 -0
  105. package/server/src/controllers/index.js +9 -0
  106. package/server/src/controllers/realm.js +324 -0
  107. package/server/src/controllers/user.js +413 -0
  108. package/server/src/destroy.js +5 -0
  109. package/server/src/index.js +21 -0
  110. package/server/src/policies/can-access-realm.js +26 -0
  111. package/server/src/policies/index.js +7 -0
  112. package/server/src/policies/is-authenticated.js +5 -0
  113. package/server/src/register.js +7 -0
  114. package/server/src/routes/admin.js +253 -0
  115. package/server/src/routes/index.js +8 -0
  116. package/server/src/services/audit-log.js +196 -0
  117. package/server/src/services/index.js +13 -0
  118. package/server/src/services/keycloak-client.js +758 -0
  119. package/server/src/services/permission.js +290 -0
  120. package/server/src/services/realm.js +374 -0
  121. package/server/src/services/user.js +613 -0
  122. package/server/src/utils/errors.js +63 -0
  123. package/server/src/utils/index.js +6 -0
@@ -0,0 +1,231 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useIntl } from 'react-intl';
4
+ import {
5
+ Button,
6
+ Table,
7
+ Thead,
8
+ Tbody,
9
+ Tr,
10
+ Th,
11
+ Td,
12
+ Typography,
13
+ Badge,
14
+ IconButton,
15
+ Flex,
16
+ Box,
17
+ Dialog,
18
+ Loader,
19
+ EmptyStateLayout,
20
+ } from '@strapi/design-system';
21
+ import { Layouts } from '@strapi/strapi/admin';
22
+ import { Plus, Pencil, Trash, User, Play } from '@strapi/icons';
23
+
24
+ import { getTrad } from '../../constants';
25
+ import useRealms from '../../hooks/useRealms';
26
+ import pluginId from '../../pluginId';
27
+
28
+ const RealmListPage = () => {
29
+ const { formatMessage } = useIntl();
30
+ const navigate = useNavigate();
31
+ const { realms, isLoading, remove, testConnection } = useRealms();
32
+
33
+ const [deleteId, setDeleteId] = useState(null);
34
+ const [testingId, setTestingId] = useState(null);
35
+ const [testResults, setTestResults] = useState({});
36
+
37
+ const handleDelete = async () => {
38
+ if (deleteId) {
39
+ await remove(deleteId);
40
+ setDeleteId(null);
41
+ }
42
+ };
43
+
44
+ const handleTest = async (id) => {
45
+ setTestingId(id);
46
+ const result = await testConnection(id);
47
+ setTestResults((prev) => ({ ...prev, [id]: result }));
48
+ setTestingId(null);
49
+ };
50
+
51
+ if (isLoading) {
52
+ return (
53
+ <Layouts.Root>
54
+ <Layouts.Content>
55
+ <Flex justifyContent="center" padding={8}>
56
+ <Loader />
57
+ </Flex>
58
+ </Layouts.Content>
59
+ </Layouts.Root>
60
+ );
61
+ }
62
+
63
+ return (
64
+ <Layouts.Root>
65
+ <Layouts.Header
66
+ title={formatMessage({ id: getTrad('realms.title'), defaultMessage: 'Keycloak Realms' })}
67
+ subtitle={formatMessage({
68
+ id: getTrad('realms.subtitle'),
69
+ defaultMessage: 'Configure Keycloak realm connections',
70
+ })}
71
+ primaryAction={
72
+ <Button startIcon={<Plus />} onClick={() => navigate(`/settings/${pluginId}/realms/create`)}>
73
+ {formatMessage({ id: getTrad('realms.create'), defaultMessage: 'Add Realm' })}
74
+ </Button>
75
+ }
76
+ />
77
+
78
+ <Layouts.Content>
79
+ {realms.length === 0 ? (
80
+ <EmptyStateLayout
81
+ icon={<User width="10rem" height="10rem" />}
82
+ content={formatMessage({
83
+ id: getTrad('realms.empty.description'),
84
+ defaultMessage: 'Add your first Keycloak realm to start managing users.',
85
+ })}
86
+ action={
87
+ <Button
88
+ variant="secondary"
89
+ startIcon={<Plus />}
90
+ onClick={() => navigate(`/settings/${pluginId}/realms/create`)}
91
+ >
92
+ {formatMessage({ id: getTrad('realms.create'), defaultMessage: 'Add Realm' })}
93
+ </Button>
94
+ }
95
+ />
96
+ ) : (
97
+ <Table colCount={6} rowCount={realms.length + 1}>
98
+ <Thead>
99
+ <Tr>
100
+ <Th>
101
+ <Typography variant="sigma">
102
+ {formatMessage({ id: getTrad('realm.displayName'), defaultMessage: 'Display Name' })}
103
+ </Typography>
104
+ </Th>
105
+ <Th>
106
+ <Typography variant="sigma">
107
+ {formatMessage({ id: getTrad('realm.name'), defaultMessage: 'Name' })}
108
+ </Typography>
109
+ </Th>
110
+ <Th>
111
+ <Typography variant="sigma">
112
+ {formatMessage({ id: getTrad('realm.realmName'), defaultMessage: 'Realm Name' })}
113
+ </Typography>
114
+ </Th>
115
+ <Th>
116
+ <Typography variant="sigma">
117
+ {formatMessage({ id: getTrad('realm.enabled'), defaultMessage: 'Enabled' })}
118
+ </Typography>
119
+ </Th>
120
+ <Th>
121
+ <Typography variant="sigma">Status</Typography>
122
+ </Th>
123
+ <Th>
124
+ <Typography variant="sigma">Actions</Typography>
125
+ </Th>
126
+ </Tr>
127
+ </Thead>
128
+ <Tbody>
129
+ {realms.map((realm) => (
130
+ <Tr key={realm.documentId}>
131
+ <Td>
132
+ <Flex alignItems="center" gap={2}>
133
+ <Box
134
+ width="12px"
135
+ height="12px"
136
+ borderRadius="50%"
137
+ background={realm.color || 'primary600'}
138
+ />
139
+ <Typography textColor="neutral800" fontWeight="bold">
140
+ {realm.displayName}
141
+ </Typography>
142
+ </Flex>
143
+ </Td>
144
+ <Td>
145
+ <Typography textColor="neutral600">{realm.name}</Typography>
146
+ </Td>
147
+ <Td>
148
+ <Typography textColor="neutral600">{realm.realmName}</Typography>
149
+ </Td>
150
+ <Td>
151
+ <Badge backgroundColor={realm.enabled ? 'success100' : 'neutral150'}>
152
+ {realm.enabled ? 'Enabled' : 'Disabled'}
153
+ </Badge>
154
+ </Td>
155
+ <Td>
156
+ {testResults[realm.documentId] ? (
157
+ <Badge
158
+ backgroundColor={testResults[realm.documentId].success ? 'success100' : 'danger100'}
159
+ >
160
+ {testResults[realm.documentId].success ? 'Connected' : 'Failed'}
161
+ </Badge>
162
+ ) : (
163
+ <Typography textColor="neutral400">-</Typography>
164
+ )}
165
+ </Td>
166
+ <Td>
167
+ <Flex gap={1}>
168
+ <IconButton
169
+ withTooltip={false}
170
+ label="Test Connection"
171
+ onClick={() => handleTest(realm.documentId)}
172
+ loading={testingId === realm.documentId}
173
+ >
174
+ <Play />
175
+ </IconButton>
176
+ <IconButton
177
+ withTooltip={false}
178
+ label="Manage Users"
179
+ onClick={() => navigate(`/settings/${pluginId}/realms/${realm.documentId}/users`)}
180
+ >
181
+ <User />
182
+ </IconButton>
183
+ <IconButton
184
+ withTooltip={false}
185
+ label="Edit"
186
+ onClick={() => navigate(`/settings/${pluginId}/realms/${realm.documentId}`)}
187
+ >
188
+ <Pencil />
189
+ </IconButton>
190
+ <IconButton
191
+ withTooltip={false}
192
+ label="Delete"
193
+ onClick={() => setDeleteId(realm.documentId)}
194
+ >
195
+ <Trash />
196
+ </IconButton>
197
+ </Flex>
198
+ </Td>
199
+ </Tr>
200
+ ))}
201
+ </Tbody>
202
+ </Table>
203
+ )}
204
+ </Layouts.Content>
205
+
206
+ <Dialog.Root open={!!deleteId} onOpenChange={() => setDeleteId(null)}>
207
+ <Dialog.Content>
208
+ <Dialog.Header>Delete Realm</Dialog.Header>
209
+ <Dialog.Body>
210
+ {formatMessage({
211
+ id: getTrad('realm.delete.confirm'),
212
+ defaultMessage: 'Are you sure you want to delete this realm configuration?',
213
+ })}
214
+ </Dialog.Body>
215
+ <Dialog.Footer>
216
+ <Dialog.Cancel>
217
+ <Button variant="tertiary">Cancel</Button>
218
+ </Dialog.Cancel>
219
+ <Dialog.Action>
220
+ <Button variant="danger-light" onClick={handleDelete}>
221
+ Delete
222
+ </Button>
223
+ </Dialog.Action>
224
+ </Dialog.Footer>
225
+ </Dialog.Content>
226
+ </Dialog.Root>
227
+ </Layouts.Root>
228
+ );
229
+ };
230
+
231
+ export default RealmListPage;
@@ -0,0 +1,7 @@
1
+ import RealmListPage from './RealmListPage';
2
+ import RealmEditPage from './RealmEditPage';
3
+
4
+ export default {
5
+ List: RealmListPage,
6
+ Edit: RealmEditPage,
7
+ };
@@ -0,0 +1,313 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useParams, useNavigate } from 'react-router-dom';
3
+ import { useIntl } from 'react-intl';
4
+ import {
5
+ Button,
6
+ Box,
7
+ Grid,
8
+ TextInput,
9
+ Toggle,
10
+ Typography,
11
+ Flex,
12
+ Loader,
13
+ MultiSelect,
14
+ MultiSelectOption,
15
+ Field,
16
+ } from '@strapi/design-system';
17
+ import { Layouts } from '@strapi/strapi/admin';
18
+ import { ArrowLeft, Check } from '@strapi/icons';
19
+
20
+ import { getTrad } from '../../constants';
21
+ import useRealms from '../../hooks/useRealms';
22
+ import useKeycloakUsers from '../../hooks/useKeycloakUsers';
23
+ import useKeycloakRoles from '../../hooks/useKeycloakRoles';
24
+ import pluginId from '../../pluginId';
25
+
26
+ const UserEditPage = () => {
27
+ const { realmId, userId } = useParams();
28
+ const navigate = useNavigate();
29
+ const { formatMessage } = useIntl();
30
+
31
+ const { fetchOne: fetchRealm } = useRealms();
32
+ const { getUser, createUser, updateUser } = useKeycloakUsers(realmId);
33
+ const { roles, getUserRoles, assignRoles, removeRoles } = useKeycloakRoles(realmId);
34
+
35
+ const isEditMode = !!userId;
36
+
37
+ const [realm, setRealm] = useState(null);
38
+ const [isLoading, setIsLoading] = useState(true);
39
+ const [isSaving, setIsSaving] = useState(false);
40
+ const [formData, setFormData] = useState({
41
+ username: '',
42
+ email: '',
43
+ firstName: '',
44
+ lastName: '',
45
+ enabled: true,
46
+ emailVerified: false,
47
+ });
48
+ const [selectedRoles, setSelectedRoles] = useState([]);
49
+ const [originalRoles, setOriginalRoles] = useState([]);
50
+ const [errors, setErrors] = useState({});
51
+
52
+ useEffect(() => {
53
+ const loadData = async () => {
54
+ try {
55
+ const realmData = await fetchRealm(realmId);
56
+ setRealm(realmData);
57
+
58
+ if (isEditMode) {
59
+ const [userData, userRoles] = await Promise.all([
60
+ getUser(userId),
61
+ getUserRoles(userId),
62
+ ]);
63
+
64
+ setFormData({
65
+ username: userData.username || '',
66
+ email: userData.email || '',
67
+ firstName: userData.firstName || '',
68
+ lastName: userData.lastName || '',
69
+ enabled: userData.enabled !== false,
70
+ emailVerified: userData.emailVerified === true,
71
+ });
72
+
73
+ const roleNames = userRoles.map((r) => r.name);
74
+ setSelectedRoles(roleNames);
75
+ setOriginalRoles(roleNames);
76
+ }
77
+ } catch {
78
+ navigate(`/settings/${pluginId}/realms/${realmId}/users`);
79
+ } finally {
80
+ setIsLoading(false);
81
+ }
82
+ };
83
+
84
+ loadData();
85
+ }, [realmId, userId, isEditMode, fetchRealm, getUser, getUserRoles, navigate]);
86
+
87
+ const handleChange = (field) => (e) => {
88
+ const value = e.target ? e.target.value : e;
89
+ setFormData((prev) => ({ ...prev, [field]: value }));
90
+ setErrors((prev) => ({ ...prev, [field]: null }));
91
+ };
92
+
93
+ const handleToggle = (field) => () => {
94
+ setFormData((prev) => ({ ...prev, [field]: !prev[field] }));
95
+ };
96
+
97
+ const validate = () => {
98
+ const newErrors = {};
99
+
100
+ if (!formData.username) {
101
+ newErrors.username = 'Username is required';
102
+ }
103
+
104
+ if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
105
+ newErrors.email = 'Invalid email format';
106
+ }
107
+
108
+ setErrors(newErrors);
109
+ return Object.keys(newErrors).length === 0;
110
+ };
111
+
112
+ const handleSubmit = async () => {
113
+ if (!validate()) return;
114
+
115
+ setIsSaving(true);
116
+
117
+ try {
118
+ if (isEditMode) {
119
+ await updateUser(userId, formData);
120
+
121
+ const rolesToAssign = selectedRoles.filter((r) => !originalRoles.includes(r));
122
+ const rolesToRemove = originalRoles.filter((r) => !selectedRoles.includes(r));
123
+
124
+ if (rolesToAssign.length > 0) {
125
+ const roleObjects = roles
126
+ .filter((r) => rolesToAssign.includes(r.name))
127
+ .map((r) => ({ id: r.id, name: r.name }));
128
+ await assignRoles(userId, roleObjects);
129
+ }
130
+
131
+ if (rolesToRemove.length > 0) {
132
+ const roleObjects = roles
133
+ .filter((r) => rolesToRemove.includes(r.name))
134
+ .map((r) => ({ id: r.id, name: r.name }));
135
+ await removeRoles(userId, roleObjects);
136
+ }
137
+ } else {
138
+ const newUser = await createUser(formData);
139
+
140
+ if (selectedRoles.length > 0 && newUser?.id) {
141
+ const roleObjects = roles
142
+ .filter((r) => selectedRoles.includes(r.name))
143
+ .map((r) => ({ id: r.id, name: r.name }));
144
+ await assignRoles(newUser.id, roleObjects);
145
+ }
146
+ }
147
+
148
+ navigate(`/settings/${pluginId}/realms/${realmId}/users`);
149
+ } catch {
150
+ // Error handled by hook
151
+ } finally {
152
+ setIsSaving(false);
153
+ }
154
+ };
155
+
156
+ if (isLoading) {
157
+ return (
158
+ <Layouts.Root>
159
+ <Layouts.Content>
160
+ <Flex justifyContent="center" padding={8}>
161
+ <Loader />
162
+ </Flex>
163
+ </Layouts.Content>
164
+ </Layouts.Root>
165
+ );
166
+ }
167
+
168
+ return (
169
+ <Layouts.Root>
170
+ <Layouts.Header
171
+ title={isEditMode ? 'Edit User' : 'Create User'}
172
+ subtitle={
173
+ <Flex alignItems="center" gap={2}>
174
+ <Box
175
+ width="12px"
176
+ height="12px"
177
+ borderRadius="50%"
178
+ background={realm?.color || 'primary600'}
179
+ />
180
+ <Typography textColor="neutral600">{realm?.displayName}</Typography>
181
+ </Flex>
182
+ }
183
+ navigationAction={
184
+ <Button
185
+ startIcon={<ArrowLeft />}
186
+ variant="ghost"
187
+ onClick={() => navigate(`/settings/${pluginId}/realms/${realmId}/users`)}
188
+ >
189
+ {formatMessage({ id: getTrad('common.back'), defaultMessage: 'Back' })}
190
+ </Button>
191
+ }
192
+ primaryAction={
193
+ <Button startIcon={<Check />} onClick={handleSubmit} loading={isSaving}>
194
+ {formatMessage({ id: getTrad('common.save'), defaultMessage: 'Save' })}
195
+ </Button>
196
+ }
197
+ />
198
+
199
+ <Layouts.Content>
200
+ <Box background="neutral0" padding={6} shadow="filterShadow" hasRadius>
201
+ <Grid.Root gap={4}>
202
+ <Grid.Item col={6} s={12}>
203
+ <Field.Root error={errors.username}>
204
+ <Field.Label required>
205
+ {formatMessage({ id: getTrad('user.username'), defaultMessage: 'Username' })}
206
+ </Field.Label>
207
+ <TextInput
208
+ name="username"
209
+ value={formData.username}
210
+ onChange={handleChange('username')}
211
+ disabled={isEditMode}
212
+ />
213
+ <Field.Error />
214
+ </Field.Root>
215
+ </Grid.Item>
216
+
217
+ <Grid.Item col={6} s={12}>
218
+ <Field.Root error={errors.email}>
219
+ <Field.Label>
220
+ {formatMessage({ id: getTrad('user.email'), defaultMessage: 'Email' })}
221
+ </Field.Label>
222
+ <TextInput
223
+ name="email"
224
+ type="email"
225
+ value={formData.email}
226
+ onChange={handleChange('email')}
227
+ />
228
+ <Field.Error />
229
+ </Field.Root>
230
+ </Grid.Item>
231
+
232
+ <Grid.Item col={6} s={12}>
233
+ <Field.Root>
234
+ <Field.Label>
235
+ {formatMessage({ id: getTrad('user.firstName'), defaultMessage: 'First Name' })}
236
+ </Field.Label>
237
+ <TextInput
238
+ name="firstName"
239
+ value={formData.firstName}
240
+ onChange={handleChange('firstName')}
241
+ />
242
+ </Field.Root>
243
+ </Grid.Item>
244
+
245
+ <Grid.Item col={6} s={12}>
246
+ <Field.Root>
247
+ <Field.Label>
248
+ {formatMessage({ id: getTrad('user.lastName'), defaultMessage: 'Last Name' })}
249
+ </Field.Label>
250
+ <TextInput
251
+ name="lastName"
252
+ value={formData.lastName}
253
+ onChange={handleChange('lastName')}
254
+ />
255
+ </Field.Root>
256
+ </Grid.Item>
257
+
258
+ <Grid.Item col={6} s={12}>
259
+ <Flex direction="column" gap={1}>
260
+ <Typography variant="pi" fontWeight="bold">
261
+ {formatMessage({ id: getTrad('user.enabled'), defaultMessage: 'Enabled' })}
262
+ </Typography>
263
+ <Toggle
264
+ checked={formData.enabled}
265
+ onChange={handleToggle('enabled')}
266
+ onLabel="Yes"
267
+ offLabel="No"
268
+ />
269
+ </Flex>
270
+ </Grid.Item>
271
+
272
+ <Grid.Item col={6} s={12}>
273
+ <Flex direction="column" gap={1}>
274
+ <Typography variant="pi" fontWeight="bold">
275
+ {formatMessage({ id: getTrad('user.emailVerified'), defaultMessage: 'Email Verified' })}
276
+ </Typography>
277
+ <Toggle
278
+ checked={formData.emailVerified}
279
+ onChange={handleToggle('emailVerified')}
280
+ onLabel="Yes"
281
+ offLabel="No"
282
+ />
283
+ </Flex>
284
+ </Grid.Item>
285
+
286
+ {roles.length > 0 && (
287
+ <Grid.Item col={12}>
288
+ <Field.Root>
289
+ <Field.Label>
290
+ {formatMessage({ id: getTrad('user.roles'), defaultMessage: 'Roles' })}
291
+ </Field.Label>
292
+ <MultiSelect
293
+ value={selectedRoles}
294
+ onChange={setSelectedRoles}
295
+ placeholder="Select roles..."
296
+ >
297
+ {roles.map((role) => (
298
+ <MultiSelectOption key={role.id} value={role.name}>
299
+ {role.name}
300
+ </MultiSelectOption>
301
+ ))}
302
+ </MultiSelect>
303
+ </Field.Root>
304
+ </Grid.Item>
305
+ )}
306
+ </Grid.Root>
307
+ </Box>
308
+ </Layouts.Content>
309
+ </Layouts.Root>
310
+ );
311
+ };
312
+
313
+ export default UserEditPage;