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,249 @@
1
+ /**
2
+ * @fileoverview React hook for managing realm admin assignments.
3
+ * Provides functionality to assign Strapi users as realm administrators with granular permissions.
4
+ * @module admin/hooks/useRealmAdmins
5
+ */
6
+
7
+ import { useState, useCallback } from 'react';
8
+ import { useFetchClient, useNotification } from '@strapi/strapi/admin';
9
+ import { API_BASE_PATH } from '../constants';
10
+
11
+ /**
12
+ * @typedef {Object} RealmPermissions
13
+ * @property {boolean} canRead - Can view users in the realm
14
+ * @property {boolean} canCreate - Can create new users
15
+ * @property {boolean} canUpdate - Can modify users (includes enable/disable)
16
+ * @property {boolean} canDelete - Can permanently delete users
17
+ * @property {boolean} canManageRoles - Can assign/remove realm roles
18
+ * @property {boolean} canResetPassword - Can reset passwords and send password reset emails
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} RealmAdminAssignment
23
+ * @property {string} documentId - Assignment document ID
24
+ * @property {number} strapiUserId - Strapi admin user ID
25
+ * @property {string} strapiUserEmail - Strapi admin email
26
+ * @property {RealmPermissions} permissions - Granted permissions
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} StrapiUser
31
+ * @property {number} id - User ID
32
+ * @property {string} email - User email
33
+ * @property {string} [firstname] - First name
34
+ * @property {string} [lastname] - Last name
35
+ */
36
+
37
+ /**
38
+ * @typedef {Object} UseRealmAdminsReturn
39
+ * @property {RealmAdminAssignment[]} admins - Array of admin assignments
40
+ * @property {StrapiUser[]} strapiUsers - Array of available Strapi users
41
+ * @property {boolean} isLoading - Loading state
42
+ * @property {Function} fetchAdmins - Refetch admin assignments
43
+ * @property {Function} fetchStrapiUsers - Fetch available Strapi users
44
+ * @property {Function} addAdmin - Add new admin assignment
45
+ * @property {Function} updateAdmin - Update admin permissions
46
+ * @property {Function} removeAdmin - Remove admin assignment
47
+ */
48
+
49
+ /**
50
+ * Hook for managing realm admin assignments.
51
+ * Allows assigning Strapi users as administrators of a specific realm
52
+ * with granular permission control.
53
+ *
54
+ * @param {string} realmId - Realm document ID
55
+ * @returns {UseRealmAdminsReturn} Admin management functions and state
56
+ *
57
+ * @example
58
+ * function RealmAdminManager({ realmId }) {
59
+ * const {
60
+ * admins,
61
+ * strapiUsers,
62
+ * fetchStrapiUsers,
63
+ * addAdmin,
64
+ * updateAdmin,
65
+ * removeAdmin,
66
+ * } = useRealmAdmins(realmId);
67
+ *
68
+ * useEffect(() => {
69
+ * fetchStrapiUsers();
70
+ * }, []);
71
+ *
72
+ * const handleAddAdmin = async (userId, email) => {
73
+ * await addAdmin(userId, email, {
74
+ * canRead: true,
75
+ * canCreate: true,
76
+ * canUpdate: false,
77
+ * canDelete: false,
78
+ * canManageRoles: false,
79
+ * canResetPassword: false,
80
+ * });
81
+ * };
82
+ *
83
+ * return (
84
+ * <AdminList
85
+ * admins={admins}
86
+ * availableUsers={strapiUsers}
87
+ * onAdd={handleAddAdmin}
88
+ * onRemove={removeAdmin}
89
+ * />
90
+ * );
91
+ * }
92
+ */
93
+ export const useRealmAdmins = (realmId) => {
94
+ const client = useFetchClient();
95
+ const { toggleNotification } = useNotification();
96
+
97
+ const [admins, setAdmins] = useState([]);
98
+ const [strapiUsers, setStrapiUsers] = useState([]);
99
+ const [isLoading, setIsLoading] = useState(false);
100
+
101
+ /**
102
+ * Fetches all admin assignments for the realm.
103
+ *
104
+ * @returns {Promise<void>}
105
+ */
106
+ const fetchAdmins = useCallback(async () => {
107
+ if (!realmId) return;
108
+
109
+ setIsLoading(true);
110
+ try {
111
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/admins`);
112
+ setAdmins(data.data || []);
113
+ } catch (err) {
114
+ toggleNotification({
115
+ type: 'danger',
116
+ message: err.response?.data?.error?.message || 'Failed to fetch realm admins',
117
+ });
118
+ } finally {
119
+ setIsLoading(false);
120
+ }
121
+ }, [realmId, client, toggleNotification]);
122
+
123
+ /**
124
+ * Fetches all Strapi admin users available for assignment.
125
+ *
126
+ * @returns {Promise<void>}
127
+ */
128
+ const fetchStrapiUsers = useCallback(async () => {
129
+ try {
130
+ const { data } = await client.get('/admin/users');
131
+ setStrapiUsers(data.data?.results || data.data || []);
132
+ } catch (err) {
133
+ toggleNotification({
134
+ type: 'danger',
135
+ message: 'Failed to fetch Strapi users',
136
+ });
137
+ }
138
+ }, [client, toggleNotification]);
139
+
140
+ /**
141
+ * Adds a Strapi user as a realm admin with specified permissions.
142
+ * Automatically refreshes the admin list on success.
143
+ *
144
+ * @param {number} strapiUserId - Strapi user ID
145
+ * @param {string} strapiUserEmail - Strapi user email
146
+ * @param {RealmPermissions} permissions - Permissions to grant
147
+ * @returns {Promise<RealmAdminAssignment>} Created assignment
148
+ * @throws {Error} If creation fails
149
+ */
150
+ const addAdmin = useCallback(
151
+ async (strapiUserId, strapiUserEmail, permissions) => {
152
+ try {
153
+ const { data } = await client.post(`${API_BASE_PATH}/realms/${realmId}/admins`, {
154
+ data: {
155
+ strapiUserId,
156
+ strapiUserEmail,
157
+ permissions,
158
+ },
159
+ });
160
+ toggleNotification({
161
+ type: 'success',
162
+ message: 'Admin added successfully',
163
+ });
164
+ await fetchAdmins();
165
+ return data.data;
166
+ } catch (err) {
167
+ toggleNotification({
168
+ type: 'danger',
169
+ message: err.response?.data?.error?.message || 'Failed to add admin',
170
+ });
171
+ throw err;
172
+ }
173
+ },
174
+ [realmId, client, toggleNotification, fetchAdmins]
175
+ );
176
+
177
+ /**
178
+ * Updates an existing admin assignment's permissions.
179
+ * Automatically refreshes the admin list on success.
180
+ *
181
+ * @param {string} adminId - Admin assignment document ID
182
+ * @param {RealmPermissions} permissions - New permissions
183
+ * @returns {Promise<RealmAdminAssignment>} Updated assignment
184
+ * @throws {Error} If update fails
185
+ */
186
+ const updateAdmin = useCallback(
187
+ async (adminId, permissions) => {
188
+ try {
189
+ const { data } = await client.put(`${API_BASE_PATH}/realms/${realmId}/admins/${adminId}`, {
190
+ data: { permissions },
191
+ });
192
+ toggleNotification({
193
+ type: 'success',
194
+ message: 'Admin permissions updated',
195
+ });
196
+ await fetchAdmins();
197
+ return data.data;
198
+ } catch (err) {
199
+ toggleNotification({
200
+ type: 'danger',
201
+ message: err.response?.data?.error?.message || 'Failed to update admin',
202
+ });
203
+ throw err;
204
+ }
205
+ },
206
+ [realmId, client, toggleNotification, fetchAdmins]
207
+ );
208
+
209
+ /**
210
+ * Removes a realm admin assignment.
211
+ * Automatically refreshes the admin list on success.
212
+ *
213
+ * @param {string} adminId - Admin assignment document ID
214
+ * @returns {Promise<void>}
215
+ * @throws {Error} If removal fails
216
+ */
217
+ const removeAdmin = useCallback(
218
+ async (adminId) => {
219
+ try {
220
+ await client.del(`${API_BASE_PATH}/realms/${realmId}/admins/${adminId}`);
221
+ toggleNotification({
222
+ type: 'success',
223
+ message: 'Admin removed successfully',
224
+ });
225
+ await fetchAdmins();
226
+ } catch (err) {
227
+ toggleNotification({
228
+ type: 'danger',
229
+ message: err.response?.data?.error?.message || 'Failed to remove admin',
230
+ });
231
+ throw err;
232
+ }
233
+ },
234
+ [realmId, client, toggleNotification, fetchAdmins]
235
+ );
236
+
237
+ return {
238
+ admins,
239
+ strapiUsers,
240
+ isLoading,
241
+ fetchAdmins,
242
+ fetchStrapiUsers,
243
+ addAdmin,
244
+ updateAdmin,
245
+ removeAdmin,
246
+ };
247
+ };
248
+
249
+ export default useRealmAdmins;
@@ -0,0 +1,269 @@
1
+ /**
2
+ * @fileoverview React hook for managing Keycloak realm configurations.
3
+ * Provides CRUD operations, connection testing, and state management for realms.
4
+ * @module admin/hooks/useRealms
5
+ */
6
+
7
+ import { useState, useEffect, useCallback } from 'react';
8
+ import { useFetchClient, useNotification } from '@strapi/strapi/admin';
9
+ import { API_BASE_PATH } from '../constants';
10
+
11
+ /**
12
+ * @typedef {Object} RealmConfig
13
+ * @property {string} documentId - Strapi document ID
14
+ * @property {string} name - Unique slug identifier
15
+ * @property {string} displayName - Human-readable name
16
+ * @property {string} serverUrl - Keycloak server URL
17
+ * @property {string} realmName - Keycloak realm name
18
+ * @property {string} clientId - OAuth client ID
19
+ * @property {boolean} enabled - Active status
20
+ * @property {string} color - UI accent color (hex)
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} ConnectionTestResult
25
+ * @property {boolean} success - Whether connection succeeded
26
+ * @property {string} [message] - Error message if failed
27
+ * @property {string} [realmDisplayName] - Realm name from Keycloak if successful
28
+ */
29
+
30
+ /**
31
+ * @typedef {Object} UseRealmsReturn
32
+ * @property {RealmConfig[]} realms - Array of realm configurations
33
+ * @property {boolean} isLoading - Loading state for initial fetch
34
+ * @property {Function} fetchAll - Refetch all realms
35
+ * @property {Function} fetchOne - Fetch single realm by ID
36
+ * @property {Function} create - Create new realm
37
+ * @property {Function} update - Update existing realm
38
+ * @property {Function} remove - Delete realm
39
+ * @property {Function} testConnection - Test saved realm connection
40
+ * @property {Function} testConnectionRaw - Test connection with raw config
41
+ */
42
+
43
+ /**
44
+ * Hook for managing Keycloak realm configurations.
45
+ * Provides complete CRUD operations and connection testing functionality.
46
+ *
47
+ * @returns {UseRealmsReturn} Realm management functions and state
48
+ *
49
+ * @example
50
+ * function RealmsPage() {
51
+ * const { realms, isLoading, create, update, remove, testConnection } = useRealms();
52
+ *
53
+ * if (isLoading) return <Loader />;
54
+ *
55
+ * return (
56
+ * <ul>
57
+ * {realms.map(realm => (
58
+ * <li key={realm.documentId}>{realm.displayName}</li>
59
+ * ))}
60
+ * </ul>
61
+ * );
62
+ * }
63
+ */
64
+ export const useRealms = () => {
65
+ const client = useFetchClient();
66
+ const { toggleNotification } = useNotification();
67
+
68
+ const [realms, setRealms] = useState([]);
69
+ const [isLoading, setIsLoading] = useState(true);
70
+
71
+ /**
72
+ * Fetches all realm configurations from the API.
73
+ * Updates the realms state and handles errors with notifications.
74
+ *
75
+ * @returns {Promise<void>}
76
+ */
77
+ const fetchAll = useCallback(async () => {
78
+ setIsLoading(true);
79
+ try {
80
+ const { data } = await client.get(`${API_BASE_PATH}/realms`);
81
+ setRealms(data.data || []);
82
+ } catch (err) {
83
+ toggleNotification({
84
+ type: 'danger',
85
+ message: err.response?.data?.error?.message || 'Failed to fetch realms',
86
+ });
87
+ } finally {
88
+ setIsLoading(false);
89
+ }
90
+ }, [client, toggleNotification]);
91
+
92
+ /**
93
+ * Fetches a single realm by its document ID.
94
+ *
95
+ * @param {string} id - Realm document ID
96
+ * @returns {Promise<RealmConfig>} The realm configuration
97
+ * @throws {Error} If fetch fails
98
+ */
99
+ const fetchOne = useCallback(
100
+ async (id) => {
101
+ try {
102
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${id}`);
103
+ return data.data;
104
+ } catch (err) {
105
+ toggleNotification({
106
+ type: 'danger',
107
+ message: err.response?.data?.error?.message || 'Failed to fetch realm',
108
+ });
109
+ throw err;
110
+ }
111
+ },
112
+ [client, toggleNotification]
113
+ );
114
+
115
+ /**
116
+ * Creates a new realm configuration.
117
+ * Automatically refreshes the realms list on success.
118
+ *
119
+ * @param {Object} realmData - Realm configuration data
120
+ * @param {string} realmData.name - Unique slug identifier
121
+ * @param {string} realmData.displayName - Human-readable name
122
+ * @param {string} realmData.serverUrl - Keycloak server URL
123
+ * @param {string} realmData.realmName - Keycloak realm name
124
+ * @param {string} realmData.clientId - OAuth client ID
125
+ * @param {string} [realmData.clientSecret] - OAuth client secret
126
+ * @param {boolean} [realmData.enabled=true] - Active status
127
+ * @param {string} [realmData.color] - UI accent color
128
+ * @returns {Promise<RealmConfig>} The created realm
129
+ * @throws {Error} If creation fails
130
+ */
131
+ const create = useCallback(
132
+ async (realmData) => {
133
+ try {
134
+ const { data } = await client.post(`${API_BASE_PATH}/realms`, { data: realmData });
135
+ toggleNotification({
136
+ type: 'success',
137
+ message: 'Realm created successfully',
138
+ });
139
+ await fetchAll();
140
+ return data.data;
141
+ } catch (err) {
142
+ toggleNotification({
143
+ type: 'danger',
144
+ message: err.response?.data?.error?.message || 'Failed to create realm',
145
+ });
146
+ throw err;
147
+ }
148
+ },
149
+ [client, toggleNotification, fetchAll]
150
+ );
151
+
152
+ /**
153
+ * Updates an existing realm configuration.
154
+ * Automatically refreshes the realms list on success.
155
+ *
156
+ * @param {string} id - Realm document ID
157
+ * @param {Partial<RealmConfig>} realmData - Fields to update
158
+ * @returns {Promise<RealmConfig>} The updated realm
159
+ * @throws {Error} If update fails
160
+ */
161
+ const update = useCallback(
162
+ async (id, realmData) => {
163
+ try {
164
+ const { data } = await client.put(`${API_BASE_PATH}/realms/${id}`, { data: realmData });
165
+ toggleNotification({
166
+ type: 'success',
167
+ message: 'Realm updated successfully',
168
+ });
169
+ await fetchAll();
170
+ return data.data;
171
+ } catch (err) {
172
+ toggleNotification({
173
+ type: 'danger',
174
+ message: err.response?.data?.error?.message || 'Failed to update realm',
175
+ });
176
+ throw err;
177
+ }
178
+ },
179
+ [client, toggleNotification, fetchAll]
180
+ );
181
+
182
+ /**
183
+ * Deletes a realm configuration and all associated admin assignments.
184
+ * Automatically refreshes the realms list on success.
185
+ *
186
+ * @param {string} id - Realm document ID
187
+ * @returns {Promise<void>}
188
+ * @throws {Error} If deletion fails
189
+ */
190
+ const remove = useCallback(
191
+ async (id) => {
192
+ try {
193
+ await client.del(`${API_BASE_PATH}/realms/${id}`);
194
+ toggleNotification({
195
+ type: 'success',
196
+ message: 'Realm deleted successfully',
197
+ });
198
+ await fetchAll();
199
+ } catch (err) {
200
+ toggleNotification({
201
+ type: 'danger',
202
+ message: err.response?.data?.error?.message || 'Failed to delete realm',
203
+ });
204
+ throw err;
205
+ }
206
+ },
207
+ [client, toggleNotification, fetchAll]
208
+ );
209
+
210
+ /**
211
+ * Tests the Keycloak connection for a saved realm.
212
+ *
213
+ * @param {string} id - Realm document ID
214
+ * @returns {Promise<ConnectionTestResult>} Test result with success status
215
+ */
216
+ const testConnection = useCallback(
217
+ async (id) => {
218
+ try {
219
+ const { data } = await client.post(`${API_BASE_PATH}/realms/${id}/test`);
220
+ return data.data;
221
+ } catch (err) {
222
+ return { success: false, message: err.response?.data?.error?.message || 'Connection failed' };
223
+ }
224
+ },
225
+ [client]
226
+ );
227
+
228
+ /**
229
+ * Tests a Keycloak connection with raw configuration data.
230
+ * Useful for validating credentials before saving a realm.
231
+ *
232
+ * @param {Object} config - Raw connection configuration
233
+ * @param {string} config.serverUrl - Keycloak server URL
234
+ * @param {string} config.realmName - Keycloak realm name
235
+ * @param {string} config.clientId - OAuth client ID
236
+ * @param {string} config.clientSecret - OAuth client secret
237
+ * @returns {Promise<ConnectionTestResult>} Test result with success status
238
+ */
239
+ const testConnectionRaw = useCallback(
240
+ async (config) => {
241
+ try {
242
+ const { data } = await client.post(`${API_BASE_PATH}/realms/test-connection`, { data: config });
243
+ return data.data;
244
+ } catch (err) {
245
+ return { success: false, message: err.response?.data?.error?.message || 'Connection failed' };
246
+ }
247
+ },
248
+ [client]
249
+ );
250
+
251
+ // Fetch realms on mount
252
+ useEffect(() => {
253
+ fetchAll();
254
+ }, [fetchAll]);
255
+
256
+ return {
257
+ realms,
258
+ isLoading,
259
+ fetchAll,
260
+ fetchOne,
261
+ create,
262
+ update,
263
+ remove,
264
+ testConnection,
265
+ testConnectionRaw,
266
+ };
267
+ };
268
+
269
+ export default useRealms;
@@ -0,0 +1,46 @@
1
+ import { PLUGIN_ID } from './pluginId';
2
+ import Initializer from './components/Initializer';
3
+ import { getTrad } from './constants';
4
+
5
+ export default {
6
+ register(app) {
7
+ app.registerPlugin({
8
+ id: PLUGIN_ID,
9
+ initializer: Initializer,
10
+ isReady: false,
11
+ name: PLUGIN_ID,
12
+ });
13
+ },
14
+
15
+ bootstrap(app) {
16
+ // Add settings link
17
+ app.addSettingsLink('global', {
18
+ id: PLUGIN_ID,
19
+ to: PLUGIN_ID,
20
+ intlLabel: {
21
+ id: getTrad('settings.title'),
22
+ defaultMessage: 'Keycloak Users',
23
+ },
24
+ Component: () => import('./pages/App'),
25
+ permissions: [],
26
+ });
27
+ },
28
+
29
+ async registerTrads({ locales }) {
30
+ const importedTrads = await Promise.all(
31
+ locales.map((locale) => {
32
+ return import(`./translations/${locale}.json`)
33
+ .then(({ default: data }) => ({
34
+ data,
35
+ locale,
36
+ }))
37
+ .catch(() => ({
38
+ data: {},
39
+ locale,
40
+ }));
41
+ })
42
+ );
43
+
44
+ return importedTrads;
45
+ },
46
+ };
@@ -0,0 +1,21 @@
1
+ import { Routes, Route } from 'react-router-dom';
2
+
3
+ import RealmsPage from './RealmsPage';
4
+ import UsersPage from './UsersPage';
5
+ import AuditPage from './AuditPage';
6
+
7
+ const App = () => {
8
+ return (
9
+ <Routes>
10
+ <Route index element={<RealmsPage.List />} />
11
+ <Route path="realms/create" element={<RealmsPage.Edit />} />
12
+ <Route path="realms/:id" element={<RealmsPage.Edit />} />
13
+ <Route path="realms/:realmId/users" element={<UsersPage.List />} />
14
+ <Route path="realms/:realmId/users/create" element={<UsersPage.Edit />} />
15
+ <Route path="realms/:realmId/users/:userId" element={<UsersPage.Edit />} />
16
+ <Route path="audit" element={<AuditPage />} />
17
+ </Routes>
18
+ );
19
+ };
20
+
21
+ export default App;