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,477 @@
1
+ /**
2
+ * @fileoverview React hook for managing Keycloak users within a realm.
3
+ * Provides comprehensive CRUD operations, password management, and bulk operations.
4
+ * @module admin/hooks/useKeycloakUsers
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} KeycloakUser
13
+ * @property {string} id - Unique user identifier
14
+ * @property {string} username - Username
15
+ * @property {string} [email] - Email address
16
+ * @property {string} [firstName] - First name
17
+ * @property {string} [lastName] - Last name
18
+ * @property {boolean} enabled - Account enabled status
19
+ * @property {boolean} emailVerified - Email verification status
20
+ * @property {number} createdTimestamp - Account creation timestamp
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} Pagination
25
+ * @property {number} page - Current page number (1-indexed)
26
+ * @property {number} pageSize - Items per page
27
+ * @property {number} total - Total items
28
+ * @property {number} pageCount - Total pages
29
+ */
30
+
31
+ /**
32
+ * @typedef {Object} FetchUsersParams
33
+ * @property {string} [search=''] - Search query (matches username, email, name)
34
+ * @property {number} [page=1] - Page number
35
+ * @property {number} [pageSize=25] - Items per page
36
+ */
37
+
38
+ /**
39
+ * @typedef {Object} BulkImportResult
40
+ * @property {Object[]} success - Successfully created users
41
+ * @property {Object[]} failed - Failed creations with error messages
42
+ */
43
+
44
+ /**
45
+ * @typedef {Object} UseKeycloakUsersReturn
46
+ * @property {KeycloakUser[]} users - Array of users
47
+ * @property {boolean} isLoading - Loading state
48
+ * @property {Pagination} pagination - Pagination metadata
49
+ * @property {Function} fetchUsers - Fetch users with search/pagination
50
+ * @property {Function} getUser - Get single user by ID
51
+ * @property {Function} createUser - Create new user
52
+ * @property {Function} updateUser - Update user attributes
53
+ * @property {Function} deleteUser - Permanently delete user
54
+ * @property {Function} resetPassword - Set user password
55
+ * @property {Function} enableUser - Enable user account
56
+ * @property {Function} disableUser - Disable user account
57
+ * @property {Function} sendVerificationEmail - Send email verification link
58
+ * @property {Function} sendResetPasswordEmail - Send password reset email
59
+ * @property {Function} bulkImport - Import multiple users
60
+ * @property {Function} exportUsers - Export all users
61
+ */
62
+
63
+ /**
64
+ * Hook for managing Keycloak users within a specific realm.
65
+ * Provides comprehensive user management including CRUD operations,
66
+ * password management, account status control, and bulk operations.
67
+ *
68
+ * @param {string} realmId - Realm document ID
69
+ * @returns {UseKeycloakUsersReturn} User management functions and state
70
+ *
71
+ * @example
72
+ * function UsersPage({ realmId }) {
73
+ * const {
74
+ * users,
75
+ * isLoading,
76
+ * pagination,
77
+ * fetchUsers,
78
+ * createUser,
79
+ * deleteUser,
80
+ * } = useKeycloakUsers(realmId);
81
+ *
82
+ * return (
83
+ * <UserTable
84
+ * users={users}
85
+ * loading={isLoading}
86
+ * pagination={pagination}
87
+ * onSearch={(search) => fetchUsers({ search })}
88
+ * onPageChange={(page) => fetchUsers({ page })}
89
+ * onDelete={(user) => deleteUser(user.id)}
90
+ * />
91
+ * );
92
+ * }
93
+ */
94
+ export const useKeycloakUsers = (realmId) => {
95
+ const client = useFetchClient();
96
+ const { toggleNotification } = useNotification();
97
+
98
+ const [users, setUsers] = useState([]);
99
+ const [isLoading, setIsLoading] = useState(false);
100
+ const [pagination, setPagination] = useState({
101
+ page: 1,
102
+ pageSize: 25,
103
+ total: 0,
104
+ pageCount: 0,
105
+ });
106
+
107
+ /**
108
+ * Fetches users with optional search and pagination.
109
+ *
110
+ * @param {FetchUsersParams} [params={}] - Query parameters
111
+ * @returns {Promise<void>}
112
+ */
113
+ const fetchUsers = useCallback(
114
+ async ({ search = '', page = 1, pageSize = 25 } = {}) => {
115
+ if (!realmId) return;
116
+
117
+ setIsLoading(true);
118
+ try {
119
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/users`, {
120
+ params: { search, page, pageSize },
121
+ });
122
+ setUsers(data.data?.users || []);
123
+ setPagination(data.data?.pagination || { page: 1, pageSize: 25, total: 0, pageCount: 0 });
124
+ } catch (err) {
125
+ toggleNotification({
126
+ type: 'danger',
127
+ message: err.response?.data?.error?.message || 'Failed to fetch users',
128
+ });
129
+ } finally {
130
+ setIsLoading(false);
131
+ }
132
+ },
133
+ [realmId, client, toggleNotification]
134
+ );
135
+
136
+ /**
137
+ * Fetches a single user by their Keycloak ID.
138
+ *
139
+ * @param {string} userId - Keycloak user ID
140
+ * @returns {Promise<KeycloakUser>} User details
141
+ * @throws {Error} If fetch fails
142
+ */
143
+ const getUser = useCallback(
144
+ async (userId) => {
145
+ try {
146
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/users/${userId}`);
147
+ return data.data;
148
+ } catch (err) {
149
+ toggleNotification({
150
+ type: 'danger',
151
+ message: err.response?.data?.error?.message || 'Failed to fetch user',
152
+ });
153
+ throw err;
154
+ }
155
+ },
156
+ [realmId, client, toggleNotification]
157
+ );
158
+
159
+ /**
160
+ * Creates a new user in Keycloak.
161
+ * Automatically refreshes the user list on success.
162
+ *
163
+ * @param {Object} userData - User data
164
+ * @param {string} userData.username - Required username
165
+ * @param {string} [userData.email] - Email address
166
+ * @param {string} [userData.firstName] - First name
167
+ * @param {string} [userData.lastName] - Last name
168
+ * @param {boolean} [userData.enabled=true] - Account enabled status
169
+ * @returns {Promise<KeycloakUser>} Created user
170
+ * @throws {Error} If creation fails
171
+ */
172
+ const createUser = useCallback(
173
+ async (userData) => {
174
+ try {
175
+ const { data } = await client.post(`${API_BASE_PATH}/realms/${realmId}/users`, {
176
+ data: userData,
177
+ });
178
+ toggleNotification({
179
+ type: 'success',
180
+ message: 'User created successfully',
181
+ });
182
+ await fetchUsers();
183
+ return data.data;
184
+ } catch (err) {
185
+ toggleNotification({
186
+ type: 'danger',
187
+ message: err.response?.data?.error?.message || 'Failed to create user',
188
+ });
189
+ throw err;
190
+ }
191
+ },
192
+ [realmId, client, toggleNotification, fetchUsers]
193
+ );
194
+
195
+ /**
196
+ * Updates an existing user's attributes.
197
+ * Automatically refreshes the user list on success.
198
+ *
199
+ * @param {string} userId - Keycloak user ID
200
+ * @param {Object} userData - Fields to update
201
+ * @returns {Promise<KeycloakUser>} Updated user
202
+ * @throws {Error} If update fails
203
+ */
204
+ const updateUser = useCallback(
205
+ async (userId, userData) => {
206
+ try {
207
+ const { data } = await client.put(`${API_BASE_PATH}/realms/${realmId}/users/${userId}`, {
208
+ data: userData,
209
+ });
210
+ toggleNotification({
211
+ type: 'success',
212
+ message: 'User updated successfully',
213
+ });
214
+ await fetchUsers();
215
+ return data.data;
216
+ } catch (err) {
217
+ toggleNotification({
218
+ type: 'danger',
219
+ message: err.response?.data?.error?.message || 'Failed to update user',
220
+ });
221
+ throw err;
222
+ }
223
+ },
224
+ [realmId, client, toggleNotification, fetchUsers]
225
+ );
226
+
227
+ /**
228
+ * Permanently deletes a user from Keycloak.
229
+ * Automatically refreshes the user list on success.
230
+ *
231
+ * @param {string} userId - Keycloak user ID
232
+ * @returns {Promise<void>}
233
+ * @throws {Error} If deletion fails
234
+ */
235
+ const deleteUser = useCallback(
236
+ async (userId) => {
237
+ try {
238
+ await client.del(`${API_BASE_PATH}/realms/${realmId}/users/${userId}`);
239
+ toggleNotification({
240
+ type: 'success',
241
+ message: 'User deleted successfully',
242
+ });
243
+ await fetchUsers();
244
+ } catch (err) {
245
+ toggleNotification({
246
+ type: 'danger',
247
+ message: err.response?.data?.error?.message || 'Failed to delete user',
248
+ });
249
+ throw err;
250
+ }
251
+ },
252
+ [realmId, client, toggleNotification, fetchUsers]
253
+ );
254
+
255
+ /**
256
+ * Sets a new password for a user.
257
+ *
258
+ * @param {string} userId - Keycloak user ID
259
+ * @param {string} password - New password
260
+ * @param {boolean} [temporary=true] - If true, user must change password on next login
261
+ * @returns {Promise<void>}
262
+ * @throws {Error} If password reset fails
263
+ */
264
+ const resetPassword = useCallback(
265
+ async (userId, password, temporary = true) => {
266
+ try {
267
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/reset-password`, {
268
+ data: { password, temporary },
269
+ });
270
+ toggleNotification({
271
+ type: 'success',
272
+ message: 'Password reset successfully',
273
+ });
274
+ } catch (err) {
275
+ toggleNotification({
276
+ type: 'danger',
277
+ message: err.response?.data?.error?.message || 'Failed to reset password',
278
+ });
279
+ throw err;
280
+ }
281
+ },
282
+ [realmId, client, toggleNotification]
283
+ );
284
+
285
+ /**
286
+ * Enables a user account.
287
+ * Automatically refreshes the user list on success.
288
+ *
289
+ * @param {string} userId - Keycloak user ID
290
+ * @returns {Promise<void>}
291
+ * @throws {Error} If enable fails
292
+ */
293
+ const enableUser = useCallback(
294
+ async (userId) => {
295
+ try {
296
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/enable`);
297
+ toggleNotification({
298
+ type: 'success',
299
+ message: 'User enabled successfully',
300
+ });
301
+ await fetchUsers();
302
+ } catch (err) {
303
+ toggleNotification({
304
+ type: 'danger',
305
+ message: err.response?.data?.error?.message || 'Failed to enable user',
306
+ });
307
+ throw err;
308
+ }
309
+ },
310
+ [realmId, client, toggleNotification, fetchUsers]
311
+ );
312
+
313
+ /**
314
+ * Disables a user account.
315
+ * Automatically refreshes the user list on success.
316
+ *
317
+ * @param {string} userId - Keycloak user ID
318
+ * @returns {Promise<void>}
319
+ * @throws {Error} If disable fails
320
+ */
321
+ const disableUser = useCallback(
322
+ async (userId) => {
323
+ try {
324
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/disable`);
325
+ toggleNotification({
326
+ type: 'success',
327
+ message: 'User disabled successfully',
328
+ });
329
+ await fetchUsers();
330
+ } catch (err) {
331
+ toggleNotification({
332
+ type: 'danger',
333
+ message: err.response?.data?.error?.message || 'Failed to disable user',
334
+ });
335
+ throw err;
336
+ }
337
+ },
338
+ [realmId, client, toggleNotification, fetchUsers]
339
+ );
340
+
341
+ /**
342
+ * Sends an email verification link to the user.
343
+ *
344
+ * @param {string} userId - Keycloak user ID
345
+ * @returns {Promise<void>}
346
+ * @throws {Error} If email send fails
347
+ */
348
+ const sendVerificationEmail = useCallback(
349
+ async (userId) => {
350
+ try {
351
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/send-verify-email`);
352
+ toggleNotification({
353
+ type: 'success',
354
+ message: 'Verification email sent',
355
+ });
356
+ } catch (err) {
357
+ toggleNotification({
358
+ type: 'danger',
359
+ message: err.response?.data?.error?.message || 'Failed to send verification email',
360
+ });
361
+ throw err;
362
+ }
363
+ },
364
+ [realmId, client, toggleNotification]
365
+ );
366
+
367
+ /**
368
+ * Sends a password reset email to the user.
369
+ *
370
+ * @param {string} userId - Keycloak user ID
371
+ * @returns {Promise<void>}
372
+ * @throws {Error} If email send fails
373
+ */
374
+ const sendResetPasswordEmail = useCallback(
375
+ async (userId) => {
376
+ try {
377
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/send-reset-password-email`);
378
+ toggleNotification({
379
+ type: 'success',
380
+ message: 'Password reset email sent',
381
+ });
382
+ } catch (err) {
383
+ toggleNotification({
384
+ type: 'danger',
385
+ message: err.response?.data?.error?.message || 'Failed to send reset email',
386
+ });
387
+ throw err;
388
+ }
389
+ },
390
+ [realmId, client, toggleNotification]
391
+ );
392
+
393
+ /**
394
+ * Bulk imports multiple users.
395
+ * Automatically refreshes the user list on completion.
396
+ *
397
+ * @param {Object[]} usersData - Array of user data objects
398
+ * @returns {Promise<BulkImportResult>} Import results with success/failed arrays
399
+ * @throws {Error} If import fails entirely
400
+ */
401
+ const bulkImport = useCallback(
402
+ async (usersData) => {
403
+ try {
404
+ const { data } = await client.post(`${API_BASE_PATH}/realms/${realmId}/users/import`, {
405
+ data: { users: usersData },
406
+ });
407
+ const result = data.data;
408
+ toggleNotification({
409
+ type: result.failed.length > 0 ? 'warning' : 'success',
410
+ message: `Imported ${result.success.length} users. ${result.failed.length} failed.`,
411
+ });
412
+ await fetchUsers();
413
+ return result;
414
+ } catch (err) {
415
+ toggleNotification({
416
+ type: 'danger',
417
+ message: err.response?.data?.error?.message || 'Failed to import users',
418
+ });
419
+ throw err;
420
+ }
421
+ },
422
+ [realmId, client, toggleNotification, fetchUsers]
423
+ );
424
+
425
+ /**
426
+ * Exports all users from the realm.
427
+ *
428
+ * @param {string} [format='json'] - Export format ('json' or 'csv')
429
+ * @returns {Promise<KeycloakUser[]|string>} Users array or CSV string
430
+ * @throws {Error} If export fails
431
+ */
432
+ const exportUsers = useCallback(
433
+ async (format = 'json') => {
434
+ try {
435
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/users/export`, {
436
+ params: { format },
437
+ });
438
+ return format === 'csv' ? data : data.data;
439
+ } catch (err) {
440
+ toggleNotification({
441
+ type: 'danger',
442
+ message: err.response?.data?.error?.message || 'Failed to export users',
443
+ });
444
+ throw err;
445
+ }
446
+ },
447
+ [realmId, client, toggleNotification]
448
+ );
449
+
450
+ // Fetch users when realmId changes
451
+ useEffect(() => {
452
+ if (realmId) {
453
+ fetchUsers();
454
+ }
455
+ // eslint-disable-next-line react-hooks/exhaustive-deps
456
+ }, [realmId]);
457
+
458
+ return {
459
+ users,
460
+ isLoading,
461
+ pagination,
462
+ fetchUsers,
463
+ getUser,
464
+ createUser,
465
+ updateUser,
466
+ deleteUser,
467
+ resetPassword,
468
+ enableUser,
469
+ disableUser,
470
+ sendVerificationEmail,
471
+ sendResetPasswordEmail,
472
+ bulkImport,
473
+ exportUsers,
474
+ };
475
+ };
476
+
477
+ export default useKeycloakUsers;