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.
- package/LICENSE +21 -0
- package/README.md +485 -0
- package/__tests__/constants.test.mjs +207 -0
- package/__tests__/mocks/strapi.mjs +182 -0
- package/__tests__/services/audit-log.test.mjs +283 -0
- package/__tests__/services/keycloak-client.test.mjs +651 -0
- package/__tests__/services/permission.test.mjs +374 -0
- package/__tests__/services/realm.test.mjs +415 -0
- package/__tests__/services/user.test.mjs +487 -0
- package/__tests__/utils/errors.test.mjs +109 -0
- package/admin/src/components/Initializer.jsx +14 -0
- package/admin/src/components/RealmBadge.jsx +17 -0
- package/admin/src/constants.js +14 -0
- package/admin/src/hooks/useAuditLogs.js +142 -0
- package/admin/src/hooks/useKeycloakRoles.js +182 -0
- package/admin/src/hooks/useKeycloakUsers.js +477 -0
- package/admin/src/hooks/useRealmAdmins.js +249 -0
- package/admin/src/hooks/useRealms.js +269 -0
- package/admin/src/index.js +46 -0
- package/admin/src/pages/App.jsx +21 -0
- package/admin/src/pages/AuditPage/index.jsx +213 -0
- package/admin/src/pages/RealmsPage/RealmEditPage.jsx +791 -0
- package/admin/src/pages/RealmsPage/RealmListPage.jsx +231 -0
- package/admin/src/pages/RealmsPage/index.jsx +7 -0
- package/admin/src/pages/UsersPage/UserEditPage.jsx +313 -0
- package/admin/src/pages/UsersPage/UserListPage.jsx +437 -0
- package/admin/src/pages/UsersPage/index.jsx +7 -0
- package/admin/src/pluginId.js +2 -0
- package/admin/src/translations/en.json +77 -0
- package/admin/src/translations/fr.json +77 -0
- package/babel.config.cjs +17 -0
- package/coverage/clover.xml +422 -0
- package/coverage/coverage-final.json +8 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/bootstrap.js.html +346 -0
- package/coverage/lcov-report/src/config/index.html +116 -0
- package/coverage/lcov-report/src/config/index.js.html +106 -0
- package/coverage/lcov-report/src/constants.js.html +850 -0
- package/coverage/lcov-report/src/content-types/audit-log/index.html +116 -0
- package/coverage/lcov-report/src/content-types/audit-log/index.js.html +94 -0
- package/coverage/lcov-report/src/content-types/index.html +116 -0
- package/coverage/lcov-report/src/content-types/index.js.html +112 -0
- package/coverage/lcov-report/src/content-types/realm-admin/index.html +116 -0
- package/coverage/lcov-report/src/content-types/realm-admin/index.js.html +94 -0
- package/coverage/lcov-report/src/content-types/realm-config/index.html +116 -0
- package/coverage/lcov-report/src/content-types/realm-config/index.js.html +94 -0
- package/coverage/lcov-report/src/controllers/audit.js.html +517 -0
- package/coverage/lcov-report/src/controllers/index.html +161 -0
- package/coverage/lcov-report/src/controllers/index.js.html +112 -0
- package/coverage/lcov-report/src/controllers/realm.js.html +1057 -0
- package/coverage/lcov-report/src/controllers/user.js.html +1324 -0
- package/coverage/lcov-report/src/destroy.js.html +100 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/policies/can-access-realm.js.html +163 -0
- package/coverage/lcov-report/src/policies/index.html +146 -0
- package/coverage/lcov-report/src/policies/index.js.html +106 -0
- package/coverage/lcov-report/src/policies/is-authenticated.js.html +100 -0
- package/coverage/lcov-report/src/register.js.html +106 -0
- package/coverage/lcov-report/src/routes/admin.js.html +844 -0
- package/coverage/lcov-report/src/routes/index.html +131 -0
- package/coverage/lcov-report/src/routes/index.js.html +109 -0
- package/coverage/lcov-report/src/services/audit-log.js.html +673 -0
- package/coverage/lcov-report/src/services/index.html +176 -0
- package/coverage/lcov-report/src/services/index.js.html +124 -0
- package/coverage/lcov-report/src/services/keycloak-client.js.html +2359 -0
- package/coverage/lcov-report/src/services/permission.js.html +955 -0
- package/coverage/lcov-report/src/services/realm.js.html +1207 -0
- package/coverage/lcov-report/src/services/user.js.html +1924 -0
- package/coverage/lcov-report/src/utils/errors.js.html +274 -0
- package/coverage/lcov-report/src/utils/index.html +116 -0
- package/coverage/lcov-report/src/utils/index.js.html +103 -0
- package/coverage/lcov.info +804 -0
- package/dist/_chunks/App-BaKrvCeS.mjs +1975 -0
- package/dist/_chunks/App-DO6syS77.js +1975 -0
- package/dist/_chunks/en-Li-XBDe9.mjs +72 -0
- package/dist/_chunks/en-aCyfgNfr.js +72 -0
- package/dist/_chunks/fr-Cj33Q8jI.js +72 -0
- package/dist/_chunks/fr-vLrXph-Z.mjs +72 -0
- package/dist/_chunks/index-DwDO4-0C.js +69 -0
- package/dist/_chunks/index-jTVd7LdQ.mjs +70 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/server/index.js +3003 -0
- package/dist/server/index.mjs +3004 -0
- package/jest.config.cjs +50 -0
- package/package.json +55 -0
- package/server/src/bootstrap.js +87 -0
- package/server/src/config/index.js +7 -0
- package/server/src/constants.js +255 -0
- package/server/src/content-types/audit-log/index.js +3 -0
- package/server/src/content-types/audit-log/schema.json +61 -0
- package/server/src/content-types/index.js +9 -0
- package/server/src/content-types/realm-admin/index.js +3 -0
- package/server/src/content-types/realm-admin/schema.json +45 -0
- package/server/src/content-types/realm-config/index.js +3 -0
- package/server/src/content-types/realm-config/schema.json +56 -0
- package/server/src/controllers/audit.js +144 -0
- package/server/src/controllers/index.js +9 -0
- package/server/src/controllers/realm.js +324 -0
- package/server/src/controllers/user.js +413 -0
- package/server/src/destroy.js +5 -0
- package/server/src/index.js +21 -0
- package/server/src/policies/can-access-realm.js +26 -0
- package/server/src/policies/index.js +7 -0
- package/server/src/policies/is-authenticated.js +5 -0
- package/server/src/register.js +7 -0
- package/server/src/routes/admin.js +253 -0
- package/server/src/routes/index.js +8 -0
- package/server/src/services/audit-log.js +196 -0
- package/server/src/services/index.js +13 -0
- package/server/src/services/keycloak-client.js +758 -0
- package/server/src/services/permission.js +290 -0
- package/server/src/services/realm.js +374 -0
- package/server/src/services/user.js +613 -0
- package/server/src/utils/errors.js +63 -0
- package/server/src/utils/index.js +6 -0
|
@@ -0,0 +1,791 @@
|
|
|
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
|
+
Alert,
|
|
13
|
+
Loader,
|
|
14
|
+
Field,
|
|
15
|
+
Tabs,
|
|
16
|
+
Table,
|
|
17
|
+
Thead,
|
|
18
|
+
Tbody,
|
|
19
|
+
Tr,
|
|
20
|
+
Th,
|
|
21
|
+
Td,
|
|
22
|
+
IconButton,
|
|
23
|
+
Checkbox,
|
|
24
|
+
Dialog,
|
|
25
|
+
Modal,
|
|
26
|
+
SingleSelect,
|
|
27
|
+
SingleSelectOption,
|
|
28
|
+
Badge,
|
|
29
|
+
EmptyStateLayout,
|
|
30
|
+
} from '@strapi/design-system';
|
|
31
|
+
import { Layouts } from '@strapi/strapi/admin';
|
|
32
|
+
import { ArrowLeft, Check, Play, Plus, Trash, Pencil, User } from '@strapi/icons';
|
|
33
|
+
|
|
34
|
+
import { getTrad } from '../../constants';
|
|
35
|
+
import useRealms from '../../hooks/useRealms';
|
|
36
|
+
import useRealmAdmins from '../../hooks/useRealmAdmins';
|
|
37
|
+
import pluginId from '../../pluginId';
|
|
38
|
+
|
|
39
|
+
const PERMISSIONS = [
|
|
40
|
+
{ key: 'canRead', label: 'Read', description: 'View users' },
|
|
41
|
+
{ key: 'canCreate', label: 'Create', description: 'Add new users' },
|
|
42
|
+
{ key: 'canUpdate', label: 'Update', description: 'Edit user details' },
|
|
43
|
+
{ key: 'canDelete', label: 'Delete', description: 'Remove users' },
|
|
44
|
+
{ key: 'canResetPassword', label: 'Reset Password', description: 'Change user passwords' },
|
|
45
|
+
{ key: 'canManageRoles', label: 'Manage Roles', description: 'Assign/remove Keycloak roles' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const RealmEditPage = () => {
|
|
49
|
+
const { id } = useParams();
|
|
50
|
+
const navigate = useNavigate();
|
|
51
|
+
const { formatMessage } = useIntl();
|
|
52
|
+
const { fetchOne, create, update, testConnectionRaw } = useRealms();
|
|
53
|
+
const {
|
|
54
|
+
admins,
|
|
55
|
+
strapiUsers,
|
|
56
|
+
isLoading: isLoadingAdmins,
|
|
57
|
+
fetchAdmins,
|
|
58
|
+
fetchStrapiUsers,
|
|
59
|
+
addAdmin,
|
|
60
|
+
updateAdmin,
|
|
61
|
+
removeAdmin,
|
|
62
|
+
} = useRealmAdmins(id);
|
|
63
|
+
|
|
64
|
+
const isEditMode = !!id;
|
|
65
|
+
|
|
66
|
+
const [activeTab, setActiveTab] = useState('configuration');
|
|
67
|
+
const [isLoading, setIsLoading] = useState(isEditMode);
|
|
68
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
69
|
+
const [isTesting, setIsTesting] = useState(false);
|
|
70
|
+
const [testResult, setTestResult] = useState(null);
|
|
71
|
+
const [formData, setFormData] = useState({
|
|
72
|
+
name: '',
|
|
73
|
+
displayName: '',
|
|
74
|
+
serverUrl: '',
|
|
75
|
+
realmName: '',
|
|
76
|
+
clientId: '',
|
|
77
|
+
clientSecret: '',
|
|
78
|
+
enabled: true,
|
|
79
|
+
color: '#4945ff',
|
|
80
|
+
});
|
|
81
|
+
const [errors, setErrors] = useState({});
|
|
82
|
+
|
|
83
|
+
// Admin management state
|
|
84
|
+
const [showAddModal, setShowAddModal] = useState(false);
|
|
85
|
+
const [showEditModal, setShowEditModal] = useState(false);
|
|
86
|
+
const [selectedAdmin, setSelectedAdmin] = useState(null);
|
|
87
|
+
const [deleteAdminId, setDeleteAdminId] = useState(null);
|
|
88
|
+
const [newAdminUserId, setNewAdminUserId] = useState('');
|
|
89
|
+
const [adminPermissions, setAdminPermissions] = useState({
|
|
90
|
+
canRead: true,
|
|
91
|
+
canCreate: false,
|
|
92
|
+
canUpdate: false,
|
|
93
|
+
canDelete: false,
|
|
94
|
+
canResetPassword: false,
|
|
95
|
+
canManageRoles: false,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (isEditMode) {
|
|
100
|
+
fetchOne(id)
|
|
101
|
+
.then((realm) => {
|
|
102
|
+
setFormData({
|
|
103
|
+
name: realm.name || '',
|
|
104
|
+
displayName: realm.displayName || '',
|
|
105
|
+
serverUrl: realm.serverUrl || '',
|
|
106
|
+
realmName: realm.realmName || '',
|
|
107
|
+
clientId: realm.clientId || '',
|
|
108
|
+
clientSecret: '',
|
|
109
|
+
enabled: realm.enabled !== false,
|
|
110
|
+
color: realm.color || '#4945ff',
|
|
111
|
+
});
|
|
112
|
+
})
|
|
113
|
+
.catch(() => {
|
|
114
|
+
navigate(`/settings/${pluginId}`);
|
|
115
|
+
})
|
|
116
|
+
.finally(() => {
|
|
117
|
+
setIsLoading(false);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}, [id, isEditMode, fetchOne, navigate]);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (isEditMode && activeTab === 'admins') {
|
|
124
|
+
fetchAdmins();
|
|
125
|
+
fetchStrapiUsers();
|
|
126
|
+
}
|
|
127
|
+
}, [isEditMode, activeTab, fetchAdmins, fetchStrapiUsers]);
|
|
128
|
+
|
|
129
|
+
const handleChange = (field) => (e) => {
|
|
130
|
+
const value = e.target ? e.target.value : e;
|
|
131
|
+
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
132
|
+
setErrors((prev) => ({ ...prev, [field]: null }));
|
|
133
|
+
setTestResult(null);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleToggle = (field) => () => {
|
|
137
|
+
setFormData((prev) => ({ ...prev, [field]: !prev[field] }));
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const validate = () => {
|
|
141
|
+
const newErrors = {};
|
|
142
|
+
|
|
143
|
+
if (!formData.name) {
|
|
144
|
+
newErrors.name = 'Name is required';
|
|
145
|
+
} else if (!/^[a-z0-9-]+$/.test(formData.name)) {
|
|
146
|
+
newErrors.name = 'Name must contain only lowercase letters, numbers, and hyphens';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!formData.displayName) {
|
|
150
|
+
newErrors.displayName = 'Display name is required';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!formData.serverUrl) {
|
|
154
|
+
newErrors.serverUrl = 'Server URL is required';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!formData.realmName) {
|
|
158
|
+
newErrors.realmName = 'Realm name is required';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!formData.clientId) {
|
|
162
|
+
newErrors.clientId = 'Client ID is required';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!isEditMode && !formData.clientSecret) {
|
|
166
|
+
newErrors.clientSecret = 'Client secret is required for new realms';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setErrors(newErrors);
|
|
170
|
+
return Object.keys(newErrors).length === 0;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleTestConnection = async () => {
|
|
174
|
+
if (!formData.serverUrl || !formData.realmName || !formData.clientId) {
|
|
175
|
+
setTestResult({ success: false, message: 'Please fill in server URL, realm name, and client ID' });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
setIsTesting(true);
|
|
180
|
+
setTestResult(null);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const result = await testConnectionRaw({
|
|
184
|
+
serverUrl: formData.serverUrl,
|
|
185
|
+
realmName: formData.realmName,
|
|
186
|
+
clientId: formData.clientId,
|
|
187
|
+
clientSecret: formData.clientSecret || undefined,
|
|
188
|
+
});
|
|
189
|
+
setTestResult(result);
|
|
190
|
+
} catch {
|
|
191
|
+
setTestResult({ success: false, message: 'Connection test failed' });
|
|
192
|
+
} finally {
|
|
193
|
+
setIsTesting(false);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleSubmit = async () => {
|
|
198
|
+
if (!validate()) return;
|
|
199
|
+
|
|
200
|
+
setIsSaving(true);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const dataToSave = { ...formData };
|
|
204
|
+
|
|
205
|
+
if (!dataToSave.clientSecret) {
|
|
206
|
+
delete dataToSave.clientSecret;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (isEditMode) {
|
|
210
|
+
await update(id, dataToSave);
|
|
211
|
+
} else {
|
|
212
|
+
await create(dataToSave);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
navigate(`/settings/${pluginId}`);
|
|
216
|
+
} catch {
|
|
217
|
+
// Error handled by hook
|
|
218
|
+
} finally {
|
|
219
|
+
setIsSaving(false);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const handleOpenAddModal = () => {
|
|
224
|
+
setNewAdminUserId('');
|
|
225
|
+
setAdminPermissions({
|
|
226
|
+
canRead: true,
|
|
227
|
+
canCreate: false,
|
|
228
|
+
canUpdate: false,
|
|
229
|
+
canDelete: false,
|
|
230
|
+
canResetPassword: false,
|
|
231
|
+
canManageRoles: false,
|
|
232
|
+
});
|
|
233
|
+
setShowAddModal(true);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const handleOpenEditModal = (admin) => {
|
|
237
|
+
setSelectedAdmin(admin);
|
|
238
|
+
setAdminPermissions({
|
|
239
|
+
canRead: admin.canRead || false,
|
|
240
|
+
canCreate: admin.canCreate || false,
|
|
241
|
+
canUpdate: admin.canUpdate || false,
|
|
242
|
+
canDelete: admin.canDelete || false,
|
|
243
|
+
canResetPassword: admin.canResetPassword || false,
|
|
244
|
+
canManageRoles: admin.canManageRoles || false,
|
|
245
|
+
});
|
|
246
|
+
setShowEditModal(true);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const handleAddAdmin = async () => {
|
|
250
|
+
if (!newAdminUserId) return;
|
|
251
|
+
|
|
252
|
+
const selectedUser = strapiUsers.find((u) => u.id === parseInt(newAdminUserId, 10));
|
|
253
|
+
try {
|
|
254
|
+
await addAdmin(parseInt(newAdminUserId, 10), selectedUser?.email, adminPermissions);
|
|
255
|
+
setShowAddModal(false);
|
|
256
|
+
} catch {
|
|
257
|
+
// Error handled by hook
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const handleUpdateAdmin = async () => {
|
|
262
|
+
if (!selectedAdmin) return;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await updateAdmin(selectedAdmin.strapiUserId, adminPermissions);
|
|
266
|
+
setShowEditModal(false);
|
|
267
|
+
setSelectedAdmin(null);
|
|
268
|
+
} catch {
|
|
269
|
+
// Error handled by hook
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const handleDeleteAdmin = async () => {
|
|
274
|
+
if (!deleteAdminId) return;
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
await removeAdmin(deleteAdminId);
|
|
278
|
+
setDeleteAdminId(null);
|
|
279
|
+
} catch {
|
|
280
|
+
// Error handled by hook
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const handlePermissionChange = (key) => () => {
|
|
285
|
+
setAdminPermissions((prev) => ({ ...prev, [key]: !prev[key] }));
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Filter out users already assigned as admins
|
|
289
|
+
const availableUsers = strapiUsers.filter(
|
|
290
|
+
(user) => !admins.some((admin) => admin.strapiUserId === user.id)
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (isLoading) {
|
|
294
|
+
return (
|
|
295
|
+
<Layouts.Root>
|
|
296
|
+
<Layouts.Content>
|
|
297
|
+
<Flex justifyContent="center" padding={8}>
|
|
298
|
+
<Loader />
|
|
299
|
+
</Flex>
|
|
300
|
+
</Layouts.Content>
|
|
301
|
+
</Layouts.Root>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<Layouts.Root>
|
|
307
|
+
<Layouts.Header
|
|
308
|
+
title={isEditMode ? 'Edit Realm' : 'Create Realm'}
|
|
309
|
+
subtitle={isEditMode && formData.displayName}
|
|
310
|
+
navigationAction={
|
|
311
|
+
<Button
|
|
312
|
+
startIcon={<ArrowLeft />}
|
|
313
|
+
variant="ghost"
|
|
314
|
+
onClick={() => navigate(`/settings/${pluginId}`)}
|
|
315
|
+
>
|
|
316
|
+
{formatMessage({ id: getTrad('common.back'), defaultMessage: 'Back' })}
|
|
317
|
+
</Button>
|
|
318
|
+
}
|
|
319
|
+
primaryAction={
|
|
320
|
+
<Flex gap={2}>
|
|
321
|
+
{activeTab === 'configuration' && (
|
|
322
|
+
<>
|
|
323
|
+
<Button
|
|
324
|
+
variant="secondary"
|
|
325
|
+
startIcon={<Play />}
|
|
326
|
+
onClick={handleTestConnection}
|
|
327
|
+
loading={isTesting}
|
|
328
|
+
>
|
|
329
|
+
{formatMessage({ id: getTrad('realm.testConnection'), defaultMessage: 'Test Connection' })}
|
|
330
|
+
</Button>
|
|
331
|
+
<Button startIcon={<Check />} onClick={handleSubmit} loading={isSaving}>
|
|
332
|
+
{formatMessage({ id: getTrad('common.save'), defaultMessage: 'Save' })}
|
|
333
|
+
</Button>
|
|
334
|
+
</>
|
|
335
|
+
)}
|
|
336
|
+
{activeTab === 'admins' && (
|
|
337
|
+
<Button startIcon={<Plus />} onClick={handleOpenAddModal}>
|
|
338
|
+
Add Admin
|
|
339
|
+
</Button>
|
|
340
|
+
)}
|
|
341
|
+
</Flex>
|
|
342
|
+
}
|
|
343
|
+
/>
|
|
344
|
+
|
|
345
|
+
<Layouts.Content>
|
|
346
|
+
{isEditMode ? (
|
|
347
|
+
<Tabs.Root value={activeTab} onValueChange={setActiveTab}>
|
|
348
|
+
<Tabs.List>
|
|
349
|
+
<Tabs.Trigger value="configuration">Configuration</Tabs.Trigger>
|
|
350
|
+
<Tabs.Trigger value="admins">Admins</Tabs.Trigger>
|
|
351
|
+
</Tabs.List>
|
|
352
|
+
|
|
353
|
+
<Box marginTop={4}>
|
|
354
|
+
<Tabs.Content value="configuration">
|
|
355
|
+
<ConfigurationTab
|
|
356
|
+
formData={formData}
|
|
357
|
+
errors={errors}
|
|
358
|
+
testResult={testResult}
|
|
359
|
+
isEditMode={isEditMode}
|
|
360
|
+
handleChange={handleChange}
|
|
361
|
+
handleToggle={handleToggle}
|
|
362
|
+
setTestResult={setTestResult}
|
|
363
|
+
formatMessage={formatMessage}
|
|
364
|
+
/>
|
|
365
|
+
</Tabs.Content>
|
|
366
|
+
|
|
367
|
+
<Tabs.Content value="admins">
|
|
368
|
+
<AdminsTab
|
|
369
|
+
admins={admins}
|
|
370
|
+
isLoading={isLoadingAdmins}
|
|
371
|
+
onEdit={handleOpenEditModal}
|
|
372
|
+
onDelete={setDeleteAdminId}
|
|
373
|
+
/>
|
|
374
|
+
</Tabs.Content>
|
|
375
|
+
</Box>
|
|
376
|
+
</Tabs.Root>
|
|
377
|
+
) : (
|
|
378
|
+
<ConfigurationTab
|
|
379
|
+
formData={formData}
|
|
380
|
+
errors={errors}
|
|
381
|
+
testResult={testResult}
|
|
382
|
+
isEditMode={isEditMode}
|
|
383
|
+
handleChange={handleChange}
|
|
384
|
+
handleToggle={handleToggle}
|
|
385
|
+
setTestResult={setTestResult}
|
|
386
|
+
formatMessage={formatMessage}
|
|
387
|
+
/>
|
|
388
|
+
)}
|
|
389
|
+
</Layouts.Content>
|
|
390
|
+
|
|
391
|
+
{/* Add Admin Modal */}
|
|
392
|
+
<Modal.Root open={showAddModal} onOpenChange={() => setShowAddModal(false)}>
|
|
393
|
+
<Modal.Content>
|
|
394
|
+
<Modal.Header>
|
|
395
|
+
<Modal.Title>Add Realm Admin</Modal.Title>
|
|
396
|
+
</Modal.Header>
|
|
397
|
+
<Modal.Body>
|
|
398
|
+
<Flex direction="column" gap={4}>
|
|
399
|
+
<Field.Root>
|
|
400
|
+
<Field.Label required>Select User</Field.Label>
|
|
401
|
+
<SingleSelect
|
|
402
|
+
value={newAdminUserId}
|
|
403
|
+
onChange={setNewAdminUserId}
|
|
404
|
+
placeholder="Select a Strapi user..."
|
|
405
|
+
>
|
|
406
|
+
{availableUsers.map((user) => (
|
|
407
|
+
<SingleSelectOption key={user.id} value={String(user.id)}>
|
|
408
|
+
{user.firstname} {user.lastname} ({user.email})
|
|
409
|
+
</SingleSelectOption>
|
|
410
|
+
))}
|
|
411
|
+
</SingleSelect>
|
|
412
|
+
</Field.Root>
|
|
413
|
+
|
|
414
|
+
<Box>
|
|
415
|
+
<Typography variant="pi" fontWeight="bold">
|
|
416
|
+
Permissions
|
|
417
|
+
</Typography>
|
|
418
|
+
<Box marginTop={2}>
|
|
419
|
+
{PERMISSIONS.map((perm) => (
|
|
420
|
+
<Flex key={perm.key} alignItems="center" gap={2} marginBottom={2}>
|
|
421
|
+
<Checkbox
|
|
422
|
+
checked={adminPermissions[perm.key]}
|
|
423
|
+
onCheckedChange={handlePermissionChange(perm.key)}
|
|
424
|
+
/>
|
|
425
|
+
<Box>
|
|
426
|
+
<Typography fontWeight="semiBold">{perm.label}</Typography>
|
|
427
|
+
<Typography variant="pi" textColor="neutral600">
|
|
428
|
+
{perm.description}
|
|
429
|
+
</Typography>
|
|
430
|
+
</Box>
|
|
431
|
+
</Flex>
|
|
432
|
+
))}
|
|
433
|
+
</Box>
|
|
434
|
+
</Box>
|
|
435
|
+
</Flex>
|
|
436
|
+
</Modal.Body>
|
|
437
|
+
<Modal.Footer>
|
|
438
|
+
<Modal.Close>
|
|
439
|
+
<Button variant="tertiary">Cancel</Button>
|
|
440
|
+
</Modal.Close>
|
|
441
|
+
<Button onClick={handleAddAdmin} disabled={!newAdminUserId}>
|
|
442
|
+
Add Admin
|
|
443
|
+
</Button>
|
|
444
|
+
</Modal.Footer>
|
|
445
|
+
</Modal.Content>
|
|
446
|
+
</Modal.Root>
|
|
447
|
+
|
|
448
|
+
{/* Edit Admin Modal */}
|
|
449
|
+
<Modal.Root open={showEditModal} onOpenChange={() => setShowEditModal(false)}>
|
|
450
|
+
<Modal.Content>
|
|
451
|
+
<Modal.Header>
|
|
452
|
+
<Modal.Title>Edit Admin Permissions</Modal.Title>
|
|
453
|
+
</Modal.Header>
|
|
454
|
+
<Modal.Body>
|
|
455
|
+
<Flex direction="column" gap={4}>
|
|
456
|
+
{selectedAdmin && (
|
|
457
|
+
<Box background="neutral100" padding={3} hasRadius>
|
|
458
|
+
<Typography fontWeight="bold">{selectedAdmin.strapiUserEmail}</Typography>
|
|
459
|
+
</Box>
|
|
460
|
+
)}
|
|
461
|
+
|
|
462
|
+
<Box>
|
|
463
|
+
<Typography variant="pi" fontWeight="bold">
|
|
464
|
+
Permissions
|
|
465
|
+
</Typography>
|
|
466
|
+
<Box marginTop={2}>
|
|
467
|
+
{PERMISSIONS.map((perm) => (
|
|
468
|
+
<Flex key={perm.key} alignItems="center" gap={2} marginBottom={2}>
|
|
469
|
+
<Checkbox
|
|
470
|
+
checked={adminPermissions[perm.key]}
|
|
471
|
+
onCheckedChange={handlePermissionChange(perm.key)}
|
|
472
|
+
/>
|
|
473
|
+
<Box>
|
|
474
|
+
<Typography fontWeight="semiBold">{perm.label}</Typography>
|
|
475
|
+
<Typography variant="pi" textColor="neutral600">
|
|
476
|
+
{perm.description}
|
|
477
|
+
</Typography>
|
|
478
|
+
</Box>
|
|
479
|
+
</Flex>
|
|
480
|
+
))}
|
|
481
|
+
</Box>
|
|
482
|
+
</Box>
|
|
483
|
+
</Flex>
|
|
484
|
+
</Modal.Body>
|
|
485
|
+
<Modal.Footer>
|
|
486
|
+
<Modal.Close>
|
|
487
|
+
<Button variant="tertiary">Cancel</Button>
|
|
488
|
+
</Modal.Close>
|
|
489
|
+
<Button onClick={handleUpdateAdmin}>Save Permissions</Button>
|
|
490
|
+
</Modal.Footer>
|
|
491
|
+
</Modal.Content>
|
|
492
|
+
</Modal.Root>
|
|
493
|
+
|
|
494
|
+
{/* Delete Admin Dialog */}
|
|
495
|
+
<Dialog.Root open={!!deleteAdminId} onOpenChange={() => setDeleteAdminId(null)}>
|
|
496
|
+
<Dialog.Content>
|
|
497
|
+
<Dialog.Header>Remove Admin</Dialog.Header>
|
|
498
|
+
<Dialog.Body>
|
|
499
|
+
Are you sure you want to remove this admin from the realm? They will lose all permissions.
|
|
500
|
+
</Dialog.Body>
|
|
501
|
+
<Dialog.Footer>
|
|
502
|
+
<Dialog.Cancel>
|
|
503
|
+
<Button variant="tertiary">Cancel</Button>
|
|
504
|
+
</Dialog.Cancel>
|
|
505
|
+
<Dialog.Action>
|
|
506
|
+
<Button variant="danger-light" onClick={handleDeleteAdmin}>
|
|
507
|
+
Remove
|
|
508
|
+
</Button>
|
|
509
|
+
</Dialog.Action>
|
|
510
|
+
</Dialog.Footer>
|
|
511
|
+
</Dialog.Content>
|
|
512
|
+
</Dialog.Root>
|
|
513
|
+
</Layouts.Root>
|
|
514
|
+
);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Configuration Tab Component
|
|
518
|
+
const ConfigurationTab = ({
|
|
519
|
+
formData,
|
|
520
|
+
errors,
|
|
521
|
+
testResult,
|
|
522
|
+
isEditMode,
|
|
523
|
+
handleChange,
|
|
524
|
+
handleToggle,
|
|
525
|
+
setTestResult,
|
|
526
|
+
formatMessage,
|
|
527
|
+
}) => (
|
|
528
|
+
<Box background="neutral0" padding={6} shadow="filterShadow" hasRadius>
|
|
529
|
+
{testResult && (
|
|
530
|
+
<Box marginBottom={4}>
|
|
531
|
+
<Alert
|
|
532
|
+
variant={testResult.success ? 'success' : 'danger'}
|
|
533
|
+
title={testResult.success ? 'Connection Successful' : 'Connection Failed'}
|
|
534
|
+
onClose={() => setTestResult(null)}
|
|
535
|
+
closeLabel="Close"
|
|
536
|
+
>
|
|
537
|
+
{testResult.success
|
|
538
|
+
? `Connected to realm: ${testResult.realmDisplayName || formData.realmName}`
|
|
539
|
+
: testResult.message}
|
|
540
|
+
</Alert>
|
|
541
|
+
</Box>
|
|
542
|
+
)}
|
|
543
|
+
|
|
544
|
+
<Grid.Root gap={4}>
|
|
545
|
+
<Grid.Item col={6} s={12}>
|
|
546
|
+
<Field.Root error={errors.name}>
|
|
547
|
+
<Field.Label required>
|
|
548
|
+
{formatMessage({ id: getTrad('realm.name'), defaultMessage: 'Name' })}
|
|
549
|
+
</Field.Label>
|
|
550
|
+
<TextInput
|
|
551
|
+
name="name"
|
|
552
|
+
value={formData.name}
|
|
553
|
+
onChange={handleChange('name')}
|
|
554
|
+
disabled={isEditMode}
|
|
555
|
+
/>
|
|
556
|
+
<Field.Hint>
|
|
557
|
+
{formatMessage({
|
|
558
|
+
id: getTrad('realm.name.hint'),
|
|
559
|
+
defaultMessage: 'Unique identifier (lowercase, numbers, hyphens only)',
|
|
560
|
+
})}
|
|
561
|
+
</Field.Hint>
|
|
562
|
+
<Field.Error />
|
|
563
|
+
</Field.Root>
|
|
564
|
+
</Grid.Item>
|
|
565
|
+
|
|
566
|
+
<Grid.Item col={6} s={12}>
|
|
567
|
+
<Field.Root error={errors.displayName}>
|
|
568
|
+
<Field.Label required>
|
|
569
|
+
{formatMessage({ id: getTrad('realm.displayName'), defaultMessage: 'Display Name' })}
|
|
570
|
+
</Field.Label>
|
|
571
|
+
<TextInput
|
|
572
|
+
name="displayName"
|
|
573
|
+
value={formData.displayName}
|
|
574
|
+
onChange={handleChange('displayName')}
|
|
575
|
+
/>
|
|
576
|
+
<Field.Error />
|
|
577
|
+
</Field.Root>
|
|
578
|
+
</Grid.Item>
|
|
579
|
+
|
|
580
|
+
<Grid.Item col={12}>
|
|
581
|
+
<Field.Root error={errors.serverUrl}>
|
|
582
|
+
<Field.Label required>
|
|
583
|
+
{formatMessage({ id: getTrad('realm.serverUrl'), defaultMessage: 'Server URL' })}
|
|
584
|
+
</Field.Label>
|
|
585
|
+
<TextInput
|
|
586
|
+
name="serverUrl"
|
|
587
|
+
value={formData.serverUrl}
|
|
588
|
+
onChange={handleChange('serverUrl')}
|
|
589
|
+
/>
|
|
590
|
+
<Field.Hint>
|
|
591
|
+
{formatMessage({
|
|
592
|
+
id: getTrad('realm.serverUrl.hint'),
|
|
593
|
+
defaultMessage: 'e.g., https://keycloak.example.com',
|
|
594
|
+
})}
|
|
595
|
+
</Field.Hint>
|
|
596
|
+
<Field.Error />
|
|
597
|
+
</Field.Root>
|
|
598
|
+
</Grid.Item>
|
|
599
|
+
|
|
600
|
+
<Grid.Item col={6} s={12}>
|
|
601
|
+
<Field.Root error={errors.realmName}>
|
|
602
|
+
<Field.Label required>
|
|
603
|
+
{formatMessage({ id: getTrad('realm.realmName'), defaultMessage: 'Realm Name' })}
|
|
604
|
+
</Field.Label>
|
|
605
|
+
<TextInput
|
|
606
|
+
name="realmName"
|
|
607
|
+
value={formData.realmName}
|
|
608
|
+
onChange={handleChange('realmName')}
|
|
609
|
+
/>
|
|
610
|
+
<Field.Hint>
|
|
611
|
+
{formatMessage({
|
|
612
|
+
id: getTrad('realm.realmName.hint'),
|
|
613
|
+
defaultMessage: 'The Keycloak realm name',
|
|
614
|
+
})}
|
|
615
|
+
</Field.Hint>
|
|
616
|
+
<Field.Error />
|
|
617
|
+
</Field.Root>
|
|
618
|
+
</Grid.Item>
|
|
619
|
+
|
|
620
|
+
<Grid.Item col={6} s={12}>
|
|
621
|
+
<Field.Root error={errors.clientId}>
|
|
622
|
+
<Field.Label required>
|
|
623
|
+
{formatMessage({ id: getTrad('realm.clientId'), defaultMessage: 'Client ID' })}
|
|
624
|
+
</Field.Label>
|
|
625
|
+
<TextInput
|
|
626
|
+
name="clientId"
|
|
627
|
+
value={formData.clientId}
|
|
628
|
+
onChange={handleChange('clientId')}
|
|
629
|
+
/>
|
|
630
|
+
<Field.Hint>
|
|
631
|
+
{formatMessage({
|
|
632
|
+
id: getTrad('realm.clientId.hint'),
|
|
633
|
+
defaultMessage: 'Service account client ID with admin permissions',
|
|
634
|
+
})}
|
|
635
|
+
</Field.Hint>
|
|
636
|
+
<Field.Error />
|
|
637
|
+
</Field.Root>
|
|
638
|
+
</Grid.Item>
|
|
639
|
+
|
|
640
|
+
<Grid.Item col={12}>
|
|
641
|
+
<Field.Root error={errors.clientSecret}>
|
|
642
|
+
<Field.Label required={!isEditMode}>
|
|
643
|
+
{formatMessage({ id: getTrad('realm.clientSecret'), defaultMessage: 'Client Secret' })}
|
|
644
|
+
</Field.Label>
|
|
645
|
+
<TextInput
|
|
646
|
+
name="clientSecret"
|
|
647
|
+
type="password"
|
|
648
|
+
value={formData.clientSecret}
|
|
649
|
+
onChange={handleChange('clientSecret')}
|
|
650
|
+
placeholder={isEditMode ? 'Leave empty to keep current secret' : ''}
|
|
651
|
+
/>
|
|
652
|
+
<Field.Error />
|
|
653
|
+
</Field.Root>
|
|
654
|
+
</Grid.Item>
|
|
655
|
+
|
|
656
|
+
<Grid.Item col={6} s={12}>
|
|
657
|
+
<Field.Root>
|
|
658
|
+
<Field.Label>
|
|
659
|
+
{formatMessage({ id: getTrad('realm.color'), defaultMessage: 'Color' })}
|
|
660
|
+
</Field.Label>
|
|
661
|
+
<TextInput
|
|
662
|
+
name="color"
|
|
663
|
+
type="color"
|
|
664
|
+
value={formData.color}
|
|
665
|
+
onChange={handleChange('color')}
|
|
666
|
+
/>
|
|
667
|
+
</Field.Root>
|
|
668
|
+
</Grid.Item>
|
|
669
|
+
|
|
670
|
+
<Grid.Item col={6} s={12}>
|
|
671
|
+
<Flex direction="column" gap={1}>
|
|
672
|
+
<Typography variant="pi" fontWeight="bold">
|
|
673
|
+
{formatMessage({ id: getTrad('realm.enabled'), defaultMessage: 'Enabled' })}
|
|
674
|
+
</Typography>
|
|
675
|
+
<Toggle
|
|
676
|
+
checked={formData.enabled}
|
|
677
|
+
onChange={handleToggle('enabled')}
|
|
678
|
+
onLabel="On"
|
|
679
|
+
offLabel="Off"
|
|
680
|
+
/>
|
|
681
|
+
</Flex>
|
|
682
|
+
</Grid.Item>
|
|
683
|
+
</Grid.Root>
|
|
684
|
+
</Box>
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
// Admins Tab Component
|
|
688
|
+
const AdminsTab = ({ admins, isLoading, onEdit, onDelete }) => {
|
|
689
|
+
if (isLoading) {
|
|
690
|
+
return (
|
|
691
|
+
<Flex justifyContent="center" padding={8}>
|
|
692
|
+
<Loader />
|
|
693
|
+
</Flex>
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (admins.length === 0) {
|
|
698
|
+
return (
|
|
699
|
+
<Box background="neutral0" padding={6} shadow="filterShadow" hasRadius>
|
|
700
|
+
<EmptyStateLayout
|
|
701
|
+
icon={<User width="6rem" height="6rem" />}
|
|
702
|
+
content="No admins assigned to this realm yet. Add an admin to allow them to manage users."
|
|
703
|
+
/>
|
|
704
|
+
</Box>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return (
|
|
709
|
+
<Box background="neutral0" padding={6} shadow="filterShadow" hasRadius>
|
|
710
|
+
<Table colCount={8} rowCount={admins.length + 1}>
|
|
711
|
+
<Thead>
|
|
712
|
+
<Tr>
|
|
713
|
+
<Th>
|
|
714
|
+
<Typography variant="sigma">User</Typography>
|
|
715
|
+
</Th>
|
|
716
|
+
<Th>
|
|
717
|
+
<Typography variant="sigma">Read</Typography>
|
|
718
|
+
</Th>
|
|
719
|
+
<Th>
|
|
720
|
+
<Typography variant="sigma">Create</Typography>
|
|
721
|
+
</Th>
|
|
722
|
+
<Th>
|
|
723
|
+
<Typography variant="sigma">Update</Typography>
|
|
724
|
+
</Th>
|
|
725
|
+
<Th>
|
|
726
|
+
<Typography variant="sigma">Delete</Typography>
|
|
727
|
+
</Th>
|
|
728
|
+
<Th>
|
|
729
|
+
<Typography variant="sigma">Password</Typography>
|
|
730
|
+
</Th>
|
|
731
|
+
<Th>
|
|
732
|
+
<Typography variant="sigma">Roles</Typography>
|
|
733
|
+
</Th>
|
|
734
|
+
<Th>
|
|
735
|
+
<Typography variant="sigma">Actions</Typography>
|
|
736
|
+
</Th>
|
|
737
|
+
</Tr>
|
|
738
|
+
</Thead>
|
|
739
|
+
<Tbody>
|
|
740
|
+
{admins.map((admin) => (
|
|
741
|
+
<Tr key={admin.documentId}>
|
|
742
|
+
<Td>
|
|
743
|
+
<Typography fontWeight="bold">{admin.strapiUserEmail}</Typography>
|
|
744
|
+
</Td>
|
|
745
|
+
<Td>
|
|
746
|
+
<PermissionBadge enabled={admin.canRead} />
|
|
747
|
+
</Td>
|
|
748
|
+
<Td>
|
|
749
|
+
<PermissionBadge enabled={admin.canCreate} />
|
|
750
|
+
</Td>
|
|
751
|
+
<Td>
|
|
752
|
+
<PermissionBadge enabled={admin.canUpdate} />
|
|
753
|
+
</Td>
|
|
754
|
+
<Td>
|
|
755
|
+
<PermissionBadge enabled={admin.canDelete} />
|
|
756
|
+
</Td>
|
|
757
|
+
<Td>
|
|
758
|
+
<PermissionBadge enabled={admin.canResetPassword} />
|
|
759
|
+
</Td>
|
|
760
|
+
<Td>
|
|
761
|
+
<PermissionBadge enabled={admin.canManageRoles} />
|
|
762
|
+
</Td>
|
|
763
|
+
<Td>
|
|
764
|
+
<Flex gap={1}>
|
|
765
|
+
<IconButton withTooltip={false} label="Edit" onClick={() => onEdit(admin)}>
|
|
766
|
+
<Pencil />
|
|
767
|
+
</IconButton>
|
|
768
|
+
<IconButton
|
|
769
|
+
withTooltip={false}
|
|
770
|
+
label="Remove"
|
|
771
|
+
onClick={() => onDelete(admin.strapiUserId)}
|
|
772
|
+
>
|
|
773
|
+
<Trash />
|
|
774
|
+
</IconButton>
|
|
775
|
+
</Flex>
|
|
776
|
+
</Td>
|
|
777
|
+
</Tr>
|
|
778
|
+
))}
|
|
779
|
+
</Tbody>
|
|
780
|
+
</Table>
|
|
781
|
+
</Box>
|
|
782
|
+
);
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const PermissionBadge = ({ enabled }) => (
|
|
786
|
+
<Badge backgroundColor={enabled ? 'success100' : 'neutral150'}>
|
|
787
|
+
{enabled ? 'Yes' : 'No'}
|
|
788
|
+
</Badge>
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
export default RealmEditPage;
|