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,1975 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { useNavigate, useParams, Routes, Route } from "react-router-dom";
3
+ import { useState, useCallback, useEffect, useRef } from "react";
4
+ import { useIntl } from "react-intl";
5
+ import { Flex, Loader, Button, EmptyStateLayout, Table, Thead, Tr, Th, Typography, Tbody, Td, Box, Badge, IconButton, Dialog, Tabs, Modal, Field, SingleSelect, SingleSelectOption, Checkbox, Alert, Grid, TextInput, Toggle, Pagination, MultiSelect, MultiSelectOption } from "@strapi/design-system";
6
+ import { useFetchClient, useNotification, Layouts } from "@strapi/strapi/admin";
7
+ import { Plus, User, Play, Pencil, Trash, Check, ArrowLeft, Download, Search, Key, Cross, Mail, File } from "@strapi/icons";
8
+ import { A as API_BASE_PATH, g as getTrad, P as PLUGIN_ID } from "./index-jTVd7LdQ.mjs";
9
+ const useRealms = () => {
10
+ const client = useFetchClient();
11
+ const { toggleNotification } = useNotification();
12
+ const [realms, setRealms] = useState([]);
13
+ const [isLoading, setIsLoading] = useState(true);
14
+ const fetchAll = useCallback(async () => {
15
+ setIsLoading(true);
16
+ try {
17
+ const { data } = await client.get(`${API_BASE_PATH}/realms`);
18
+ setRealms(data.data || []);
19
+ } catch (err) {
20
+ toggleNotification({
21
+ type: "danger",
22
+ message: err.response?.data?.error?.message || "Failed to fetch realms"
23
+ });
24
+ } finally {
25
+ setIsLoading(false);
26
+ }
27
+ }, [client, toggleNotification]);
28
+ const fetchOne = useCallback(
29
+ async (id) => {
30
+ try {
31
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${id}`);
32
+ return data.data;
33
+ } catch (err) {
34
+ toggleNotification({
35
+ type: "danger",
36
+ message: err.response?.data?.error?.message || "Failed to fetch realm"
37
+ });
38
+ throw err;
39
+ }
40
+ },
41
+ [client, toggleNotification]
42
+ );
43
+ const create = useCallback(
44
+ async (realmData) => {
45
+ try {
46
+ const { data } = await client.post(`${API_BASE_PATH}/realms`, { data: realmData });
47
+ toggleNotification({
48
+ type: "success",
49
+ message: "Realm created successfully"
50
+ });
51
+ await fetchAll();
52
+ return data.data;
53
+ } catch (err) {
54
+ toggleNotification({
55
+ type: "danger",
56
+ message: err.response?.data?.error?.message || "Failed to create realm"
57
+ });
58
+ throw err;
59
+ }
60
+ },
61
+ [client, toggleNotification, fetchAll]
62
+ );
63
+ const update = useCallback(
64
+ async (id, realmData) => {
65
+ try {
66
+ const { data } = await client.put(`${API_BASE_PATH}/realms/${id}`, { data: realmData });
67
+ toggleNotification({
68
+ type: "success",
69
+ message: "Realm updated successfully"
70
+ });
71
+ await fetchAll();
72
+ return data.data;
73
+ } catch (err) {
74
+ toggleNotification({
75
+ type: "danger",
76
+ message: err.response?.data?.error?.message || "Failed to update realm"
77
+ });
78
+ throw err;
79
+ }
80
+ },
81
+ [client, toggleNotification, fetchAll]
82
+ );
83
+ const remove = useCallback(
84
+ async (id) => {
85
+ try {
86
+ await client.del(`${API_BASE_PATH}/realms/${id}`);
87
+ toggleNotification({
88
+ type: "success",
89
+ message: "Realm deleted successfully"
90
+ });
91
+ await fetchAll();
92
+ } catch (err) {
93
+ toggleNotification({
94
+ type: "danger",
95
+ message: err.response?.data?.error?.message || "Failed to delete realm"
96
+ });
97
+ throw err;
98
+ }
99
+ },
100
+ [client, toggleNotification, fetchAll]
101
+ );
102
+ const testConnection = useCallback(
103
+ async (id) => {
104
+ try {
105
+ const { data } = await client.post(`${API_BASE_PATH}/realms/${id}/test`);
106
+ return data.data;
107
+ } catch (err) {
108
+ return { success: false, message: err.response?.data?.error?.message || "Connection failed" };
109
+ }
110
+ },
111
+ [client]
112
+ );
113
+ const testConnectionRaw = useCallback(
114
+ async (config) => {
115
+ try {
116
+ const { data } = await client.post(`${API_BASE_PATH}/realms/test-connection`, { data: config });
117
+ return data.data;
118
+ } catch (err) {
119
+ return { success: false, message: err.response?.data?.error?.message || "Connection failed" };
120
+ }
121
+ },
122
+ [client]
123
+ );
124
+ useEffect(() => {
125
+ fetchAll();
126
+ }, [fetchAll]);
127
+ return {
128
+ realms,
129
+ isLoading,
130
+ fetchAll,
131
+ fetchOne,
132
+ create,
133
+ update,
134
+ remove,
135
+ testConnection,
136
+ testConnectionRaw
137
+ };
138
+ };
139
+ const RealmListPage = () => {
140
+ const { formatMessage } = useIntl();
141
+ const navigate = useNavigate();
142
+ const { realms, isLoading, remove, testConnection } = useRealms();
143
+ const [deleteId, setDeleteId] = useState(null);
144
+ const [testingId, setTestingId] = useState(null);
145
+ const [testResults, setTestResults] = useState({});
146
+ const handleDelete = async () => {
147
+ if (deleteId) {
148
+ await remove(deleteId);
149
+ setDeleteId(null);
150
+ }
151
+ };
152
+ const handleTest = async (id) => {
153
+ setTestingId(id);
154
+ const result = await testConnection(id);
155
+ setTestResults((prev) => ({ ...prev, [id]: result }));
156
+ setTestingId(null);
157
+ };
158
+ if (isLoading) {
159
+ return /* @__PURE__ */ jsx(Layouts.Root, { children: /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, {}) }) }) });
160
+ }
161
+ return /* @__PURE__ */ jsxs(Layouts.Root, { children: [
162
+ /* @__PURE__ */ jsx(
163
+ Layouts.Header,
164
+ {
165
+ title: formatMessage({ id: getTrad("realms.title"), defaultMessage: "Keycloak Realms" }),
166
+ subtitle: formatMessage({
167
+ id: getTrad("realms.subtitle"),
168
+ defaultMessage: "Configure Keycloak realm connections"
169
+ }),
170
+ primaryAction: /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: () => navigate(`/settings/${PLUGIN_ID}/realms/create`), children: formatMessage({ id: getTrad("realms.create"), defaultMessage: "Add Realm" }) })
171
+ }
172
+ ),
173
+ /* @__PURE__ */ jsx(Layouts.Content, { children: realms.length === 0 ? /* @__PURE__ */ jsx(
174
+ EmptyStateLayout,
175
+ {
176
+ icon: /* @__PURE__ */ jsx(User, { width: "10rem", height: "10rem" }),
177
+ content: formatMessage({
178
+ id: getTrad("realms.empty.description"),
179
+ defaultMessage: "Add your first Keycloak realm to start managing users."
180
+ }),
181
+ action: /* @__PURE__ */ jsx(
182
+ Button,
183
+ {
184
+ variant: "secondary",
185
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
186
+ onClick: () => navigate(`/settings/${PLUGIN_ID}/realms/create`),
187
+ children: formatMessage({ id: getTrad("realms.create"), defaultMessage: "Add Realm" })
188
+ }
189
+ )
190
+ }
191
+ ) : /* @__PURE__ */ jsxs(Table, { colCount: 6, rowCount: realms.length + 1, children: [
192
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
193
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("realm.displayName"), defaultMessage: "Display Name" }) }) }),
194
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("realm.name"), defaultMessage: "Name" }) }) }),
195
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("realm.realmName"), defaultMessage: "Realm Name" }) }) }),
196
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("realm.enabled"), defaultMessage: "Enabled" }) }) }),
197
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Status" }) }),
198
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Actions" }) })
199
+ ] }) }),
200
+ /* @__PURE__ */ jsx(Tbody, { children: realms.map((realm) => /* @__PURE__ */ jsxs(Tr, { children: [
201
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
202
+ /* @__PURE__ */ jsx(
203
+ Box,
204
+ {
205
+ width: "12px",
206
+ height: "12px",
207
+ borderRadius: "50%",
208
+ background: realm.color || "primary600"
209
+ }
210
+ ),
211
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", fontWeight: "bold", children: realm.displayName })
212
+ ] }) }),
213
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: realm.name }) }),
214
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: realm.realmName }) }),
215
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Badge, { backgroundColor: realm.enabled ? "success100" : "neutral150", children: realm.enabled ? "Enabled" : "Disabled" }) }),
216
+ /* @__PURE__ */ jsx(Td, { children: testResults[realm.documentId] ? /* @__PURE__ */ jsx(
217
+ Badge,
218
+ {
219
+ backgroundColor: testResults[realm.documentId].success ? "success100" : "danger100",
220
+ children: testResults[realm.documentId].success ? "Connected" : "Failed"
221
+ }
222
+ ) : /* @__PURE__ */ jsx(Typography, { textColor: "neutral400", children: "-" }) }),
223
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
224
+ /* @__PURE__ */ jsx(
225
+ IconButton,
226
+ {
227
+ withTooltip: false,
228
+ label: "Test Connection",
229
+ onClick: () => handleTest(realm.documentId),
230
+ loading: testingId === realm.documentId,
231
+ children: /* @__PURE__ */ jsx(Play, {})
232
+ }
233
+ ),
234
+ /* @__PURE__ */ jsx(
235
+ IconButton,
236
+ {
237
+ withTooltip: false,
238
+ label: "Manage Users",
239
+ onClick: () => navigate(`/settings/${PLUGIN_ID}/realms/${realm.documentId}/users`),
240
+ children: /* @__PURE__ */ jsx(User, {})
241
+ }
242
+ ),
243
+ /* @__PURE__ */ jsx(
244
+ IconButton,
245
+ {
246
+ withTooltip: false,
247
+ label: "Edit",
248
+ onClick: () => navigate(`/settings/${PLUGIN_ID}/realms/${realm.documentId}`),
249
+ children: /* @__PURE__ */ jsx(Pencil, {})
250
+ }
251
+ ),
252
+ /* @__PURE__ */ jsx(
253
+ IconButton,
254
+ {
255
+ withTooltip: false,
256
+ label: "Delete",
257
+ onClick: () => setDeleteId(realm.documentId),
258
+ children: /* @__PURE__ */ jsx(Trash, {})
259
+ }
260
+ )
261
+ ] }) })
262
+ ] }, realm.documentId)) })
263
+ ] }) }),
264
+ /* @__PURE__ */ jsx(Dialog.Root, { open: !!deleteId, onOpenChange: () => setDeleteId(null), children: /* @__PURE__ */ jsxs(Dialog.Content, { children: [
265
+ /* @__PURE__ */ jsx(Dialog.Header, { children: "Delete Realm" }),
266
+ /* @__PURE__ */ jsx(Dialog.Body, { children: formatMessage({
267
+ id: getTrad("realm.delete.confirm"),
268
+ defaultMessage: "Are you sure you want to delete this realm configuration?"
269
+ }) }),
270
+ /* @__PURE__ */ jsxs(Dialog.Footer, { children: [
271
+ /* @__PURE__ */ jsx(Dialog.Cancel, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: "Cancel" }) }),
272
+ /* @__PURE__ */ jsx(Dialog.Action, { children: /* @__PURE__ */ jsx(Button, { variant: "danger-light", onClick: handleDelete, children: "Delete" }) })
273
+ ] })
274
+ ] }) })
275
+ ] });
276
+ };
277
+ const useRealmAdmins = (realmId) => {
278
+ const client = useFetchClient();
279
+ const { toggleNotification } = useNotification();
280
+ const [admins, setAdmins] = useState([]);
281
+ const [strapiUsers, setStrapiUsers] = useState([]);
282
+ const [isLoading, setIsLoading] = useState(false);
283
+ const fetchAdmins = useCallback(async () => {
284
+ if (!realmId) return;
285
+ setIsLoading(true);
286
+ try {
287
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/admins`);
288
+ setAdmins(data.data || []);
289
+ } catch (err) {
290
+ toggleNotification({
291
+ type: "danger",
292
+ message: err.response?.data?.error?.message || "Failed to fetch realm admins"
293
+ });
294
+ } finally {
295
+ setIsLoading(false);
296
+ }
297
+ }, [realmId, client, toggleNotification]);
298
+ const fetchStrapiUsers = useCallback(async () => {
299
+ try {
300
+ const { data } = await client.get("/admin/users");
301
+ setStrapiUsers(data.data?.results || data.data || []);
302
+ } catch (err) {
303
+ toggleNotification({
304
+ type: "danger",
305
+ message: "Failed to fetch Strapi users"
306
+ });
307
+ }
308
+ }, [client, toggleNotification]);
309
+ const addAdmin = useCallback(
310
+ async (strapiUserId, strapiUserEmail, permissions) => {
311
+ try {
312
+ const { data } = await client.post(`${API_BASE_PATH}/realms/${realmId}/admins`, {
313
+ data: {
314
+ strapiUserId,
315
+ strapiUserEmail,
316
+ permissions
317
+ }
318
+ });
319
+ toggleNotification({
320
+ type: "success",
321
+ message: "Admin added successfully"
322
+ });
323
+ await fetchAdmins();
324
+ return data.data;
325
+ } catch (err) {
326
+ toggleNotification({
327
+ type: "danger",
328
+ message: err.response?.data?.error?.message || "Failed to add admin"
329
+ });
330
+ throw err;
331
+ }
332
+ },
333
+ [realmId, client, toggleNotification, fetchAdmins]
334
+ );
335
+ const updateAdmin = useCallback(
336
+ async (adminId, permissions) => {
337
+ try {
338
+ const { data } = await client.put(`${API_BASE_PATH}/realms/${realmId}/admins/${adminId}`, {
339
+ data: { permissions }
340
+ });
341
+ toggleNotification({
342
+ type: "success",
343
+ message: "Admin permissions updated"
344
+ });
345
+ await fetchAdmins();
346
+ return data.data;
347
+ } catch (err) {
348
+ toggleNotification({
349
+ type: "danger",
350
+ message: err.response?.data?.error?.message || "Failed to update admin"
351
+ });
352
+ throw err;
353
+ }
354
+ },
355
+ [realmId, client, toggleNotification, fetchAdmins]
356
+ );
357
+ const removeAdmin = useCallback(
358
+ async (adminId) => {
359
+ try {
360
+ await client.del(`${API_BASE_PATH}/realms/${realmId}/admins/${adminId}`);
361
+ toggleNotification({
362
+ type: "success",
363
+ message: "Admin removed successfully"
364
+ });
365
+ await fetchAdmins();
366
+ } catch (err) {
367
+ toggleNotification({
368
+ type: "danger",
369
+ message: err.response?.data?.error?.message || "Failed to remove admin"
370
+ });
371
+ throw err;
372
+ }
373
+ },
374
+ [realmId, client, toggleNotification, fetchAdmins]
375
+ );
376
+ return {
377
+ admins,
378
+ strapiUsers,
379
+ isLoading,
380
+ fetchAdmins,
381
+ fetchStrapiUsers,
382
+ addAdmin,
383
+ updateAdmin,
384
+ removeAdmin
385
+ };
386
+ };
387
+ const PERMISSIONS = [
388
+ { key: "canRead", label: "Read", description: "View users" },
389
+ { key: "canCreate", label: "Create", description: "Add new users" },
390
+ { key: "canUpdate", label: "Update", description: "Edit user details" },
391
+ { key: "canDelete", label: "Delete", description: "Remove users" },
392
+ { key: "canResetPassword", label: "Reset Password", description: "Change user passwords" },
393
+ { key: "canManageRoles", label: "Manage Roles", description: "Assign/remove Keycloak roles" }
394
+ ];
395
+ const RealmEditPage = () => {
396
+ const { id } = useParams();
397
+ const navigate = useNavigate();
398
+ const { formatMessage } = useIntl();
399
+ const { fetchOne, create, update, testConnectionRaw } = useRealms();
400
+ const {
401
+ admins,
402
+ strapiUsers,
403
+ isLoading: isLoadingAdmins,
404
+ fetchAdmins,
405
+ fetchStrapiUsers,
406
+ addAdmin,
407
+ updateAdmin,
408
+ removeAdmin
409
+ } = useRealmAdmins(id);
410
+ const isEditMode = !!id;
411
+ const [activeTab, setActiveTab] = useState("configuration");
412
+ const [isLoading, setIsLoading] = useState(isEditMode);
413
+ const [isSaving, setIsSaving] = useState(false);
414
+ const [isTesting, setIsTesting] = useState(false);
415
+ const [testResult, setTestResult] = useState(null);
416
+ const [formData, setFormData] = useState({
417
+ name: "",
418
+ displayName: "",
419
+ serverUrl: "",
420
+ realmName: "",
421
+ clientId: "",
422
+ clientSecret: "",
423
+ enabled: true,
424
+ color: "#4945ff"
425
+ });
426
+ const [errors, setErrors] = useState({});
427
+ const [showAddModal, setShowAddModal] = useState(false);
428
+ const [showEditModal, setShowEditModal] = useState(false);
429
+ const [selectedAdmin, setSelectedAdmin] = useState(null);
430
+ const [deleteAdminId, setDeleteAdminId] = useState(null);
431
+ const [newAdminUserId, setNewAdminUserId] = useState("");
432
+ const [adminPermissions, setAdminPermissions] = useState({
433
+ canRead: true,
434
+ canCreate: false,
435
+ canUpdate: false,
436
+ canDelete: false,
437
+ canResetPassword: false,
438
+ canManageRoles: false
439
+ });
440
+ useEffect(() => {
441
+ if (isEditMode) {
442
+ fetchOne(id).then((realm) => {
443
+ setFormData({
444
+ name: realm.name || "",
445
+ displayName: realm.displayName || "",
446
+ serverUrl: realm.serverUrl || "",
447
+ realmName: realm.realmName || "",
448
+ clientId: realm.clientId || "",
449
+ clientSecret: "",
450
+ enabled: realm.enabled !== false,
451
+ color: realm.color || "#4945ff"
452
+ });
453
+ }).catch(() => {
454
+ navigate(`/settings/${PLUGIN_ID}`);
455
+ }).finally(() => {
456
+ setIsLoading(false);
457
+ });
458
+ }
459
+ }, [id, isEditMode, fetchOne, navigate]);
460
+ useEffect(() => {
461
+ if (isEditMode && activeTab === "admins") {
462
+ fetchAdmins();
463
+ fetchStrapiUsers();
464
+ }
465
+ }, [isEditMode, activeTab, fetchAdmins, fetchStrapiUsers]);
466
+ const handleChange = (field) => (e) => {
467
+ const value = e.target ? e.target.value : e;
468
+ setFormData((prev) => ({ ...prev, [field]: value }));
469
+ setErrors((prev) => ({ ...prev, [field]: null }));
470
+ setTestResult(null);
471
+ };
472
+ const handleToggle = (field) => () => {
473
+ setFormData((prev) => ({ ...prev, [field]: !prev[field] }));
474
+ };
475
+ const validate = () => {
476
+ const newErrors = {};
477
+ if (!formData.name) {
478
+ newErrors.name = "Name is required";
479
+ } else if (!/^[a-z0-9-]+$/.test(formData.name)) {
480
+ newErrors.name = "Name must contain only lowercase letters, numbers, and hyphens";
481
+ }
482
+ if (!formData.displayName) {
483
+ newErrors.displayName = "Display name is required";
484
+ }
485
+ if (!formData.serverUrl) {
486
+ newErrors.serverUrl = "Server URL is required";
487
+ }
488
+ if (!formData.realmName) {
489
+ newErrors.realmName = "Realm name is required";
490
+ }
491
+ if (!formData.clientId) {
492
+ newErrors.clientId = "Client ID is required";
493
+ }
494
+ if (!isEditMode && !formData.clientSecret) {
495
+ newErrors.clientSecret = "Client secret is required for new realms";
496
+ }
497
+ setErrors(newErrors);
498
+ return Object.keys(newErrors).length === 0;
499
+ };
500
+ const handleTestConnection = async () => {
501
+ if (!formData.serverUrl || !formData.realmName || !formData.clientId) {
502
+ setTestResult({ success: false, message: "Please fill in server URL, realm name, and client ID" });
503
+ return;
504
+ }
505
+ setIsTesting(true);
506
+ setTestResult(null);
507
+ try {
508
+ const result = await testConnectionRaw({
509
+ serverUrl: formData.serverUrl,
510
+ realmName: formData.realmName,
511
+ clientId: formData.clientId,
512
+ clientSecret: formData.clientSecret || void 0
513
+ });
514
+ setTestResult(result);
515
+ } catch {
516
+ setTestResult({ success: false, message: "Connection test failed" });
517
+ } finally {
518
+ setIsTesting(false);
519
+ }
520
+ };
521
+ const handleSubmit = async () => {
522
+ if (!validate()) return;
523
+ setIsSaving(true);
524
+ try {
525
+ const dataToSave = { ...formData };
526
+ if (!dataToSave.clientSecret) {
527
+ delete dataToSave.clientSecret;
528
+ }
529
+ if (isEditMode) {
530
+ await update(id, dataToSave);
531
+ } else {
532
+ await create(dataToSave);
533
+ }
534
+ navigate(`/settings/${PLUGIN_ID}`);
535
+ } catch {
536
+ } finally {
537
+ setIsSaving(false);
538
+ }
539
+ };
540
+ const handleOpenAddModal = () => {
541
+ setNewAdminUserId("");
542
+ setAdminPermissions({
543
+ canRead: true,
544
+ canCreate: false,
545
+ canUpdate: false,
546
+ canDelete: false,
547
+ canResetPassword: false,
548
+ canManageRoles: false
549
+ });
550
+ setShowAddModal(true);
551
+ };
552
+ const handleOpenEditModal = (admin) => {
553
+ setSelectedAdmin(admin);
554
+ setAdminPermissions({
555
+ canRead: admin.canRead || false,
556
+ canCreate: admin.canCreate || false,
557
+ canUpdate: admin.canUpdate || false,
558
+ canDelete: admin.canDelete || false,
559
+ canResetPassword: admin.canResetPassword || false,
560
+ canManageRoles: admin.canManageRoles || false
561
+ });
562
+ setShowEditModal(true);
563
+ };
564
+ const handleAddAdmin = async () => {
565
+ if (!newAdminUserId) return;
566
+ const selectedUser = strapiUsers.find((u) => u.id === parseInt(newAdminUserId, 10));
567
+ try {
568
+ await addAdmin(parseInt(newAdminUserId, 10), selectedUser?.email, adminPermissions);
569
+ setShowAddModal(false);
570
+ } catch {
571
+ }
572
+ };
573
+ const handleUpdateAdmin = async () => {
574
+ if (!selectedAdmin) return;
575
+ try {
576
+ await updateAdmin(selectedAdmin.strapiUserId, adminPermissions);
577
+ setShowEditModal(false);
578
+ setSelectedAdmin(null);
579
+ } catch {
580
+ }
581
+ };
582
+ const handleDeleteAdmin = async () => {
583
+ if (!deleteAdminId) return;
584
+ try {
585
+ await removeAdmin(deleteAdminId);
586
+ setDeleteAdminId(null);
587
+ } catch {
588
+ }
589
+ };
590
+ const handlePermissionChange = (key) => () => {
591
+ setAdminPermissions((prev) => ({ ...prev, [key]: !prev[key] }));
592
+ };
593
+ const availableUsers = strapiUsers.filter(
594
+ (user) => !admins.some((admin) => admin.strapiUserId === user.id)
595
+ );
596
+ if (isLoading) {
597
+ return /* @__PURE__ */ jsx(Layouts.Root, { children: /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, {}) }) }) });
598
+ }
599
+ return /* @__PURE__ */ jsxs(Layouts.Root, { children: [
600
+ /* @__PURE__ */ jsx(
601
+ Layouts.Header,
602
+ {
603
+ title: isEditMode ? "Edit Realm" : "Create Realm",
604
+ subtitle: isEditMode && formData.displayName,
605
+ navigationAction: /* @__PURE__ */ jsx(
606
+ Button,
607
+ {
608
+ startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}),
609
+ variant: "ghost",
610
+ onClick: () => navigate(`/settings/${PLUGIN_ID}`),
611
+ children: formatMessage({ id: getTrad("common.back"), defaultMessage: "Back" })
612
+ }
613
+ ),
614
+ primaryAction: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
615
+ activeTab === "configuration" && /* @__PURE__ */ jsxs(Fragment, { children: [
616
+ /* @__PURE__ */ jsx(
617
+ Button,
618
+ {
619
+ variant: "secondary",
620
+ startIcon: /* @__PURE__ */ jsx(Play, {}),
621
+ onClick: handleTestConnection,
622
+ loading: isTesting,
623
+ children: formatMessage({ id: getTrad("realm.testConnection"), defaultMessage: "Test Connection" })
624
+ }
625
+ ),
626
+ /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Check, {}), onClick: handleSubmit, loading: isSaving, children: formatMessage({ id: getTrad("common.save"), defaultMessage: "Save" }) })
627
+ ] }),
628
+ activeTab === "admins" && /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: handleOpenAddModal, children: "Add Admin" })
629
+ ] })
630
+ }
631
+ ),
632
+ /* @__PURE__ */ jsx(Layouts.Content, { children: isEditMode ? /* @__PURE__ */ jsxs(Tabs.Root, { value: activeTab, onValueChange: setActiveTab, children: [
633
+ /* @__PURE__ */ jsxs(Tabs.List, { children: [
634
+ /* @__PURE__ */ jsx(Tabs.Trigger, { value: "configuration", children: "Configuration" }),
635
+ /* @__PURE__ */ jsx(Tabs.Trigger, { value: "admins", children: "Admins" })
636
+ ] }),
637
+ /* @__PURE__ */ jsxs(Box, { marginTop: 4, children: [
638
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "configuration", children: /* @__PURE__ */ jsx(
639
+ ConfigurationTab,
640
+ {
641
+ formData,
642
+ errors,
643
+ testResult,
644
+ isEditMode,
645
+ handleChange,
646
+ handleToggle,
647
+ setTestResult,
648
+ formatMessage
649
+ }
650
+ ) }),
651
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "admins", children: /* @__PURE__ */ jsx(
652
+ AdminsTab,
653
+ {
654
+ admins,
655
+ isLoading: isLoadingAdmins,
656
+ onEdit: handleOpenEditModal,
657
+ onDelete: setDeleteAdminId
658
+ }
659
+ ) })
660
+ ] })
661
+ ] }) : /* @__PURE__ */ jsx(
662
+ ConfigurationTab,
663
+ {
664
+ formData,
665
+ errors,
666
+ testResult,
667
+ isEditMode,
668
+ handleChange,
669
+ handleToggle,
670
+ setTestResult,
671
+ formatMessage
672
+ }
673
+ ) }),
674
+ /* @__PURE__ */ jsx(Modal.Root, { open: showAddModal, onOpenChange: () => setShowAddModal(false), children: /* @__PURE__ */ jsxs(Modal.Content, { children: [
675
+ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsx(Modal.Title, { children: "Add Realm Admin" }) }),
676
+ /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
677
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
678
+ /* @__PURE__ */ jsx(Field.Label, { required: true, children: "Select User" }),
679
+ /* @__PURE__ */ jsx(
680
+ SingleSelect,
681
+ {
682
+ value: newAdminUserId,
683
+ onChange: setNewAdminUserId,
684
+ placeholder: "Select a Strapi user...",
685
+ children: availableUsers.map((user) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: String(user.id), children: [
686
+ user.firstname,
687
+ " ",
688
+ user.lastname,
689
+ " (",
690
+ user.email,
691
+ ")"
692
+ ] }, user.id))
693
+ }
694
+ )
695
+ ] }),
696
+ /* @__PURE__ */ jsxs(Box, { children: [
697
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: "Permissions" }),
698
+ /* @__PURE__ */ jsx(Box, { marginTop: 2, children: PERMISSIONS.map((perm) => /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, marginBottom: 2, children: [
699
+ /* @__PURE__ */ jsx(
700
+ Checkbox,
701
+ {
702
+ checked: adminPermissions[perm.key],
703
+ onCheckedChange: handlePermissionChange(perm.key)
704
+ }
705
+ ),
706
+ /* @__PURE__ */ jsxs(Box, { children: [
707
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: perm.label }),
708
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: perm.description })
709
+ ] })
710
+ ] }, perm.key)) })
711
+ ] })
712
+ ] }) }),
713
+ /* @__PURE__ */ jsxs(Modal.Footer, { children: [
714
+ /* @__PURE__ */ jsx(Modal.Close, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: "Cancel" }) }),
715
+ /* @__PURE__ */ jsx(Button, { onClick: handleAddAdmin, disabled: !newAdminUserId, children: "Add Admin" })
716
+ ] })
717
+ ] }) }),
718
+ /* @__PURE__ */ jsx(Modal.Root, { open: showEditModal, onOpenChange: () => setShowEditModal(false), children: /* @__PURE__ */ jsxs(Modal.Content, { children: [
719
+ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsx(Modal.Title, { children: "Edit Admin Permissions" }) }),
720
+ /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
721
+ selectedAdmin && /* @__PURE__ */ jsx(Box, { background: "neutral100", padding: 3, hasRadius: true, children: /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: selectedAdmin.strapiUserEmail }) }),
722
+ /* @__PURE__ */ jsxs(Box, { children: [
723
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: "Permissions" }),
724
+ /* @__PURE__ */ jsx(Box, { marginTop: 2, children: PERMISSIONS.map((perm) => /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, marginBottom: 2, children: [
725
+ /* @__PURE__ */ jsx(
726
+ Checkbox,
727
+ {
728
+ checked: adminPermissions[perm.key],
729
+ onCheckedChange: handlePermissionChange(perm.key)
730
+ }
731
+ ),
732
+ /* @__PURE__ */ jsxs(Box, { children: [
733
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: perm.label }),
734
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: perm.description })
735
+ ] })
736
+ ] }, perm.key)) })
737
+ ] })
738
+ ] }) }),
739
+ /* @__PURE__ */ jsxs(Modal.Footer, { children: [
740
+ /* @__PURE__ */ jsx(Modal.Close, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: "Cancel" }) }),
741
+ /* @__PURE__ */ jsx(Button, { onClick: handleUpdateAdmin, children: "Save Permissions" })
742
+ ] })
743
+ ] }) }),
744
+ /* @__PURE__ */ jsx(Dialog.Root, { open: !!deleteAdminId, onOpenChange: () => setDeleteAdminId(null), children: /* @__PURE__ */ jsxs(Dialog.Content, { children: [
745
+ /* @__PURE__ */ jsx(Dialog.Header, { children: "Remove Admin" }),
746
+ /* @__PURE__ */ jsx(Dialog.Body, { children: "Are you sure you want to remove this admin from the realm? They will lose all permissions." }),
747
+ /* @__PURE__ */ jsxs(Dialog.Footer, { children: [
748
+ /* @__PURE__ */ jsx(Dialog.Cancel, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: "Cancel" }) }),
749
+ /* @__PURE__ */ jsx(Dialog.Action, { children: /* @__PURE__ */ jsx(Button, { variant: "danger-light", onClick: handleDeleteAdmin, children: "Remove" }) })
750
+ ] })
751
+ ] }) })
752
+ ] });
753
+ };
754
+ const ConfigurationTab = ({
755
+ formData,
756
+ errors,
757
+ testResult,
758
+ isEditMode,
759
+ handleChange,
760
+ handleToggle,
761
+ setTestResult,
762
+ formatMessage
763
+ }) => /* @__PURE__ */ jsxs(Box, { background: "neutral0", padding: 6, shadow: "filterShadow", hasRadius: true, children: [
764
+ testResult && /* @__PURE__ */ jsx(Box, { marginBottom: 4, children: /* @__PURE__ */ jsx(
765
+ Alert,
766
+ {
767
+ variant: testResult.success ? "success" : "danger",
768
+ title: testResult.success ? "Connection Successful" : "Connection Failed",
769
+ onClose: () => setTestResult(null),
770
+ closeLabel: "Close",
771
+ children: testResult.success ? `Connected to realm: ${testResult.realmDisplayName || formData.realmName}` : testResult.message
772
+ }
773
+ ) }),
774
+ /* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
775
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Field.Root, { error: errors.name, children: [
776
+ /* @__PURE__ */ jsx(Field.Label, { required: true, children: formatMessage({ id: getTrad("realm.name"), defaultMessage: "Name" }) }),
777
+ /* @__PURE__ */ jsx(
778
+ TextInput,
779
+ {
780
+ name: "name",
781
+ value: formData.name,
782
+ onChange: handleChange("name"),
783
+ disabled: isEditMode
784
+ }
785
+ ),
786
+ /* @__PURE__ */ jsx(Field.Hint, { children: formatMessage({
787
+ id: getTrad("realm.name.hint"),
788
+ defaultMessage: "Unique identifier (lowercase, numbers, hyphens only)"
789
+ }) }),
790
+ /* @__PURE__ */ jsx(Field.Error, {})
791
+ ] }) }),
792
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Field.Root, { error: errors.displayName, children: [
793
+ /* @__PURE__ */ jsx(Field.Label, { required: true, children: formatMessage({ id: getTrad("realm.displayName"), defaultMessage: "Display Name" }) }),
794
+ /* @__PURE__ */ jsx(
795
+ TextInput,
796
+ {
797
+ name: "displayName",
798
+ value: formData.displayName,
799
+ onChange: handleChange("displayName")
800
+ }
801
+ ),
802
+ /* @__PURE__ */ jsx(Field.Error, {})
803
+ ] }) }),
804
+ /* @__PURE__ */ jsx(Grid.Item, { col: 12, children: /* @__PURE__ */ jsxs(Field.Root, { error: errors.serverUrl, children: [
805
+ /* @__PURE__ */ jsx(Field.Label, { required: true, children: formatMessage({ id: getTrad("realm.serverUrl"), defaultMessage: "Server URL" }) }),
806
+ /* @__PURE__ */ jsx(
807
+ TextInput,
808
+ {
809
+ name: "serverUrl",
810
+ value: formData.serverUrl,
811
+ onChange: handleChange("serverUrl")
812
+ }
813
+ ),
814
+ /* @__PURE__ */ jsx(Field.Hint, { children: formatMessage({
815
+ id: getTrad("realm.serverUrl.hint"),
816
+ defaultMessage: "e.g., https://keycloak.example.com"
817
+ }) }),
818
+ /* @__PURE__ */ jsx(Field.Error, {})
819
+ ] }) }),
820
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Field.Root, { error: errors.realmName, children: [
821
+ /* @__PURE__ */ jsx(Field.Label, { required: true, children: formatMessage({ id: getTrad("realm.realmName"), defaultMessage: "Realm Name" }) }),
822
+ /* @__PURE__ */ jsx(
823
+ TextInput,
824
+ {
825
+ name: "realmName",
826
+ value: formData.realmName,
827
+ onChange: handleChange("realmName")
828
+ }
829
+ ),
830
+ /* @__PURE__ */ jsx(Field.Hint, { children: formatMessage({
831
+ id: getTrad("realm.realmName.hint"),
832
+ defaultMessage: "The Keycloak realm name"
833
+ }) }),
834
+ /* @__PURE__ */ jsx(Field.Error, {})
835
+ ] }) }),
836
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Field.Root, { error: errors.clientId, children: [
837
+ /* @__PURE__ */ jsx(Field.Label, { required: true, children: formatMessage({ id: getTrad("realm.clientId"), defaultMessage: "Client ID" }) }),
838
+ /* @__PURE__ */ jsx(
839
+ TextInput,
840
+ {
841
+ name: "clientId",
842
+ value: formData.clientId,
843
+ onChange: handleChange("clientId")
844
+ }
845
+ ),
846
+ /* @__PURE__ */ jsx(Field.Hint, { children: formatMessage({
847
+ id: getTrad("realm.clientId.hint"),
848
+ defaultMessage: "Service account client ID with admin permissions"
849
+ }) }),
850
+ /* @__PURE__ */ jsx(Field.Error, {})
851
+ ] }) }),
852
+ /* @__PURE__ */ jsx(Grid.Item, { col: 12, children: /* @__PURE__ */ jsxs(Field.Root, { error: errors.clientSecret, children: [
853
+ /* @__PURE__ */ jsx(Field.Label, { required: !isEditMode, children: formatMessage({ id: getTrad("realm.clientSecret"), defaultMessage: "Client Secret" }) }),
854
+ /* @__PURE__ */ jsx(
855
+ TextInput,
856
+ {
857
+ name: "clientSecret",
858
+ type: "password",
859
+ value: formData.clientSecret,
860
+ onChange: handleChange("clientSecret"),
861
+ placeholder: isEditMode ? "Leave empty to keep current secret" : ""
862
+ }
863
+ ),
864
+ /* @__PURE__ */ jsx(Field.Error, {})
865
+ ] }) }),
866
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
867
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTrad("realm.color"), defaultMessage: "Color" }) }),
868
+ /* @__PURE__ */ jsx(
869
+ TextInput,
870
+ {
871
+ name: "color",
872
+ type: "color",
873
+ value: formData.color,
874
+ onChange: handleChange("color")
875
+ }
876
+ )
877
+ ] }) }),
878
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, children: [
879
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: formatMessage({ id: getTrad("realm.enabled"), defaultMessage: "Enabled" }) }),
880
+ /* @__PURE__ */ jsx(
881
+ Toggle,
882
+ {
883
+ checked: formData.enabled,
884
+ onChange: handleToggle("enabled"),
885
+ onLabel: "On",
886
+ offLabel: "Off"
887
+ }
888
+ )
889
+ ] }) })
890
+ ] })
891
+ ] });
892
+ const AdminsTab = ({ admins, isLoading, onEdit, onDelete }) => {
893
+ if (isLoading) {
894
+ return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, {}) });
895
+ }
896
+ if (admins.length === 0) {
897
+ return /* @__PURE__ */ jsx(Box, { background: "neutral0", padding: 6, shadow: "filterShadow", hasRadius: true, children: /* @__PURE__ */ jsx(
898
+ EmptyStateLayout,
899
+ {
900
+ icon: /* @__PURE__ */ jsx(User, { width: "6rem", height: "6rem" }),
901
+ content: "No admins assigned to this realm yet. Add an admin to allow them to manage users."
902
+ }
903
+ ) });
904
+ }
905
+ return /* @__PURE__ */ jsx(Box, { background: "neutral0", padding: 6, shadow: "filterShadow", hasRadius: true, children: /* @__PURE__ */ jsxs(Table, { colCount: 8, rowCount: admins.length + 1, children: [
906
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
907
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "User" }) }),
908
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Read" }) }),
909
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Create" }) }),
910
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Update" }) }),
911
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Delete" }) }),
912
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Password" }) }),
913
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Roles" }) }),
914
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Actions" }) })
915
+ ] }) }),
916
+ /* @__PURE__ */ jsx(Tbody, { children: admins.map((admin) => /* @__PURE__ */ jsxs(Tr, { children: [
917
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: admin.strapiUserEmail }) }),
918
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(PermissionBadge, { enabled: admin.canRead }) }),
919
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(PermissionBadge, { enabled: admin.canCreate }) }),
920
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(PermissionBadge, { enabled: admin.canUpdate }) }),
921
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(PermissionBadge, { enabled: admin.canDelete }) }),
922
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(PermissionBadge, { enabled: admin.canResetPassword }) }),
923
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(PermissionBadge, { enabled: admin.canManageRoles }) }),
924
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
925
+ /* @__PURE__ */ jsx(IconButton, { withTooltip: false, label: "Edit", onClick: () => onEdit(admin), children: /* @__PURE__ */ jsx(Pencil, {}) }),
926
+ /* @__PURE__ */ jsx(
927
+ IconButton,
928
+ {
929
+ withTooltip: false,
930
+ label: "Remove",
931
+ onClick: () => onDelete(admin.strapiUserId),
932
+ children: /* @__PURE__ */ jsx(Trash, {})
933
+ }
934
+ )
935
+ ] }) })
936
+ ] }, admin.documentId)) })
937
+ ] }) });
938
+ };
939
+ const PermissionBadge = ({ enabled }) => /* @__PURE__ */ jsx(Badge, { backgroundColor: enabled ? "success100" : "neutral150", children: enabled ? "Yes" : "No" });
940
+ const RealmsPage = {
941
+ List: RealmListPage,
942
+ Edit: RealmEditPage
943
+ };
944
+ const useKeycloakUsers = (realmId) => {
945
+ const client = useFetchClient();
946
+ const { toggleNotification } = useNotification();
947
+ const [users, setUsers] = useState([]);
948
+ const [isLoading, setIsLoading] = useState(false);
949
+ const [pagination, setPagination] = useState({
950
+ page: 1,
951
+ pageSize: 25,
952
+ total: 0,
953
+ pageCount: 0
954
+ });
955
+ const fetchUsers = useCallback(
956
+ async ({ search = "", page = 1, pageSize = 25 } = {}) => {
957
+ if (!realmId) return;
958
+ setIsLoading(true);
959
+ try {
960
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/users`, {
961
+ params: { search, page, pageSize }
962
+ });
963
+ setUsers(data.data?.users || []);
964
+ setPagination(data.data?.pagination || { page: 1, pageSize: 25, total: 0, pageCount: 0 });
965
+ } catch (err) {
966
+ toggleNotification({
967
+ type: "danger",
968
+ message: err.response?.data?.error?.message || "Failed to fetch users"
969
+ });
970
+ } finally {
971
+ setIsLoading(false);
972
+ }
973
+ },
974
+ [realmId, client, toggleNotification]
975
+ );
976
+ const getUser = useCallback(
977
+ async (userId) => {
978
+ try {
979
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/users/${userId}`);
980
+ return data.data;
981
+ } catch (err) {
982
+ toggleNotification({
983
+ type: "danger",
984
+ message: err.response?.data?.error?.message || "Failed to fetch user"
985
+ });
986
+ throw err;
987
+ }
988
+ },
989
+ [realmId, client, toggleNotification]
990
+ );
991
+ const createUser = useCallback(
992
+ async (userData) => {
993
+ try {
994
+ const { data } = await client.post(`${API_BASE_PATH}/realms/${realmId}/users`, {
995
+ data: userData
996
+ });
997
+ toggleNotification({
998
+ type: "success",
999
+ message: "User created successfully"
1000
+ });
1001
+ await fetchUsers();
1002
+ return data.data;
1003
+ } catch (err) {
1004
+ toggleNotification({
1005
+ type: "danger",
1006
+ message: err.response?.data?.error?.message || "Failed to create user"
1007
+ });
1008
+ throw err;
1009
+ }
1010
+ },
1011
+ [realmId, client, toggleNotification, fetchUsers]
1012
+ );
1013
+ const updateUser = useCallback(
1014
+ async (userId, userData) => {
1015
+ try {
1016
+ const { data } = await client.put(`${API_BASE_PATH}/realms/${realmId}/users/${userId}`, {
1017
+ data: userData
1018
+ });
1019
+ toggleNotification({
1020
+ type: "success",
1021
+ message: "User updated successfully"
1022
+ });
1023
+ await fetchUsers();
1024
+ return data.data;
1025
+ } catch (err) {
1026
+ toggleNotification({
1027
+ type: "danger",
1028
+ message: err.response?.data?.error?.message || "Failed to update user"
1029
+ });
1030
+ throw err;
1031
+ }
1032
+ },
1033
+ [realmId, client, toggleNotification, fetchUsers]
1034
+ );
1035
+ const deleteUser = useCallback(
1036
+ async (userId) => {
1037
+ try {
1038
+ await client.del(`${API_BASE_PATH}/realms/${realmId}/users/${userId}`);
1039
+ toggleNotification({
1040
+ type: "success",
1041
+ message: "User deleted successfully"
1042
+ });
1043
+ await fetchUsers();
1044
+ } catch (err) {
1045
+ toggleNotification({
1046
+ type: "danger",
1047
+ message: err.response?.data?.error?.message || "Failed to delete user"
1048
+ });
1049
+ throw err;
1050
+ }
1051
+ },
1052
+ [realmId, client, toggleNotification, fetchUsers]
1053
+ );
1054
+ const resetPassword = useCallback(
1055
+ async (userId, password, temporary = true) => {
1056
+ try {
1057
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/reset-password`, {
1058
+ data: { password, temporary }
1059
+ });
1060
+ toggleNotification({
1061
+ type: "success",
1062
+ message: "Password reset successfully"
1063
+ });
1064
+ } catch (err) {
1065
+ toggleNotification({
1066
+ type: "danger",
1067
+ message: err.response?.data?.error?.message || "Failed to reset password"
1068
+ });
1069
+ throw err;
1070
+ }
1071
+ },
1072
+ [realmId, client, toggleNotification]
1073
+ );
1074
+ const enableUser = useCallback(
1075
+ async (userId) => {
1076
+ try {
1077
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/enable`);
1078
+ toggleNotification({
1079
+ type: "success",
1080
+ message: "User enabled successfully"
1081
+ });
1082
+ await fetchUsers();
1083
+ } catch (err) {
1084
+ toggleNotification({
1085
+ type: "danger",
1086
+ message: err.response?.data?.error?.message || "Failed to enable user"
1087
+ });
1088
+ throw err;
1089
+ }
1090
+ },
1091
+ [realmId, client, toggleNotification, fetchUsers]
1092
+ );
1093
+ const disableUser = useCallback(
1094
+ async (userId) => {
1095
+ try {
1096
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/disable`);
1097
+ toggleNotification({
1098
+ type: "success",
1099
+ message: "User disabled successfully"
1100
+ });
1101
+ await fetchUsers();
1102
+ } catch (err) {
1103
+ toggleNotification({
1104
+ type: "danger",
1105
+ message: err.response?.data?.error?.message || "Failed to disable user"
1106
+ });
1107
+ throw err;
1108
+ }
1109
+ },
1110
+ [realmId, client, toggleNotification, fetchUsers]
1111
+ );
1112
+ const sendVerificationEmail = useCallback(
1113
+ async (userId) => {
1114
+ try {
1115
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/send-verify-email`);
1116
+ toggleNotification({
1117
+ type: "success",
1118
+ message: "Verification email sent"
1119
+ });
1120
+ } catch (err) {
1121
+ toggleNotification({
1122
+ type: "danger",
1123
+ message: err.response?.data?.error?.message || "Failed to send verification email"
1124
+ });
1125
+ throw err;
1126
+ }
1127
+ },
1128
+ [realmId, client, toggleNotification]
1129
+ );
1130
+ const sendResetPasswordEmail = useCallback(
1131
+ async (userId) => {
1132
+ try {
1133
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/send-reset-password-email`);
1134
+ toggleNotification({
1135
+ type: "success",
1136
+ message: "Password reset email sent"
1137
+ });
1138
+ } catch (err) {
1139
+ toggleNotification({
1140
+ type: "danger",
1141
+ message: err.response?.data?.error?.message || "Failed to send reset email"
1142
+ });
1143
+ throw err;
1144
+ }
1145
+ },
1146
+ [realmId, client, toggleNotification]
1147
+ );
1148
+ const bulkImport = useCallback(
1149
+ async (usersData) => {
1150
+ try {
1151
+ const { data } = await client.post(`${API_BASE_PATH}/realms/${realmId}/users/import`, {
1152
+ data: { users: usersData }
1153
+ });
1154
+ const result = data.data;
1155
+ toggleNotification({
1156
+ type: result.failed.length > 0 ? "warning" : "success",
1157
+ message: `Imported ${result.success.length} users. ${result.failed.length} failed.`
1158
+ });
1159
+ await fetchUsers();
1160
+ return result;
1161
+ } catch (err) {
1162
+ toggleNotification({
1163
+ type: "danger",
1164
+ message: err.response?.data?.error?.message || "Failed to import users"
1165
+ });
1166
+ throw err;
1167
+ }
1168
+ },
1169
+ [realmId, client, toggleNotification, fetchUsers]
1170
+ );
1171
+ const exportUsers = useCallback(
1172
+ async (format = "json") => {
1173
+ try {
1174
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/users/export`, {
1175
+ params: { format }
1176
+ });
1177
+ return format === "csv" ? data : data.data;
1178
+ } catch (err) {
1179
+ toggleNotification({
1180
+ type: "danger",
1181
+ message: err.response?.data?.error?.message || "Failed to export users"
1182
+ });
1183
+ throw err;
1184
+ }
1185
+ },
1186
+ [realmId, client, toggleNotification]
1187
+ );
1188
+ useEffect(() => {
1189
+ if (realmId) {
1190
+ fetchUsers();
1191
+ }
1192
+ }, [realmId]);
1193
+ return {
1194
+ users,
1195
+ isLoading,
1196
+ pagination,
1197
+ fetchUsers,
1198
+ getUser,
1199
+ createUser,
1200
+ updateUser,
1201
+ deleteUser,
1202
+ resetPassword,
1203
+ enableUser,
1204
+ disableUser,
1205
+ sendVerificationEmail,
1206
+ sendResetPasswordEmail,
1207
+ bulkImport,
1208
+ exportUsers
1209
+ };
1210
+ };
1211
+ const UserListPage = () => {
1212
+ const { realmId } = useParams();
1213
+ const navigate = useNavigate();
1214
+ const { formatMessage } = useIntl();
1215
+ const { fetchOne: fetchRealm } = useRealms();
1216
+ const {
1217
+ users,
1218
+ isLoading,
1219
+ pagination,
1220
+ fetchUsers,
1221
+ deleteUser,
1222
+ resetPassword,
1223
+ enableUser,
1224
+ disableUser,
1225
+ sendVerificationEmail,
1226
+ exportUsers
1227
+ } = useKeycloakUsers(realmId);
1228
+ const [realm, setRealm] = useState(null);
1229
+ const [search, setSearch] = useState("");
1230
+ const [deleteId, setDeleteId] = useState(null);
1231
+ const [resetPasswordUser, setResetPasswordUser] = useState(null);
1232
+ const [newPassword, setNewPassword] = useState("");
1233
+ const [temporaryPassword, setTemporaryPassword] = useState(true);
1234
+ useEffect(() => {
1235
+ fetchRealm(realmId).then(setRealm).catch(() => navigate(`/settings/${PLUGIN_ID}`));
1236
+ }, [realmId, fetchRealm, navigate]);
1237
+ const debounceRef = useRef(null);
1238
+ const handleSearch = useCallback(
1239
+ (value) => {
1240
+ setSearch(value);
1241
+ if (debounceRef.current) {
1242
+ clearTimeout(debounceRef.current);
1243
+ }
1244
+ debounceRef.current = setTimeout(() => {
1245
+ fetchUsers({ search: value, page: 1 });
1246
+ }, 300);
1247
+ },
1248
+ [fetchUsers]
1249
+ );
1250
+ const handlePageChange = (page) => {
1251
+ fetchUsers({ search, page });
1252
+ };
1253
+ const handleDelete = async () => {
1254
+ if (deleteId) {
1255
+ await deleteUser(deleteId);
1256
+ setDeleteId(null);
1257
+ }
1258
+ };
1259
+ const handleResetPassword = async () => {
1260
+ if (resetPasswordUser && newPassword) {
1261
+ await resetPassword(resetPasswordUser.id, newPassword, temporaryPassword);
1262
+ setResetPasswordUser(null);
1263
+ setNewPassword("");
1264
+ setTemporaryPassword(true);
1265
+ }
1266
+ };
1267
+ const handleExport = async (format) => {
1268
+ try {
1269
+ const data = await exportUsers(format);
1270
+ const blob = new Blob(
1271
+ [format === "csv" ? data : JSON.stringify(data, null, 2)],
1272
+ { type: format === "csv" ? "text/csv" : "application/json" }
1273
+ );
1274
+ const url = URL.createObjectURL(blob);
1275
+ const a = document.createElement("a");
1276
+ a.href = url;
1277
+ a.download = `keycloak-users-${realm?.name || realmId}.${format}`;
1278
+ a.click();
1279
+ URL.revokeObjectURL(url);
1280
+ } catch {
1281
+ }
1282
+ };
1283
+ if (!realm) {
1284
+ return /* @__PURE__ */ jsx(Layouts.Root, { children: /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, {}) }) }) });
1285
+ }
1286
+ const canCreate = realm.permissions?.canCreate;
1287
+ const canUpdate = realm.permissions?.canUpdate;
1288
+ const canDelete = realm.permissions?.canDelete;
1289
+ const canResetPassword = realm.permissions?.canResetPassword;
1290
+ return /* @__PURE__ */ jsxs(Layouts.Root, { children: [
1291
+ /* @__PURE__ */ jsx(
1292
+ Layouts.Header,
1293
+ {
1294
+ title: formatMessage({ id: getTrad("users.title"), defaultMessage: "Users" }),
1295
+ subtitle: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
1296
+ /* @__PURE__ */ jsx(
1297
+ Box,
1298
+ {
1299
+ width: "12px",
1300
+ height: "12px",
1301
+ borderRadius: "50%",
1302
+ background: realm.color || "primary600"
1303
+ }
1304
+ ),
1305
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: realm.displayName })
1306
+ ] }),
1307
+ navigationAction: /* @__PURE__ */ jsx(
1308
+ Button,
1309
+ {
1310
+ startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}),
1311
+ variant: "ghost",
1312
+ onClick: () => navigate(`/settings/${PLUGIN_ID}`),
1313
+ children: formatMessage({ id: getTrad("common.back"), defaultMessage: "Back" })
1314
+ }
1315
+ ),
1316
+ primaryAction: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1317
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsx(Download, {}), onClick: () => handleExport("json"), children: formatMessage({ id: getTrad("users.export"), defaultMessage: "Export" }) }),
1318
+ canCreate && /* @__PURE__ */ jsx(
1319
+ Button,
1320
+ {
1321
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
1322
+ onClick: () => navigate(`/settings/${PLUGIN_ID}/realms/${realmId}/users/create`),
1323
+ children: formatMessage({ id: getTrad("users.create"), defaultMessage: "Create User" })
1324
+ }
1325
+ )
1326
+ ] })
1327
+ }
1328
+ ),
1329
+ /* @__PURE__ */ jsxs(Layouts.Content, { children: [
1330
+ /* @__PURE__ */ jsx(Box, { marginBottom: 4, children: /* @__PURE__ */ jsx(Field.Root, { children: /* @__PURE__ */ jsx(
1331
+ TextInput,
1332
+ {
1333
+ name: "search",
1334
+ placeholder: formatMessage({
1335
+ id: getTrad("users.search"),
1336
+ defaultMessage: "Search users..."
1337
+ }),
1338
+ value: search,
1339
+ onChange: (e) => handleSearch(e.target.value),
1340
+ startAction: /* @__PURE__ */ jsx(Search, {})
1341
+ }
1342
+ ) }) }),
1343
+ users.length === 0 && !isLoading ? /* @__PURE__ */ jsx(
1344
+ EmptyStateLayout,
1345
+ {
1346
+ icon: /* @__PURE__ */ jsx(User, { width: "10rem", height: "10rem" }),
1347
+ content: formatMessage({
1348
+ id: getTrad("users.empty.description"),
1349
+ defaultMessage: "Create your first user or adjust your search."
1350
+ }),
1351
+ action: canCreate && /* @__PURE__ */ jsx(
1352
+ Button,
1353
+ {
1354
+ variant: "secondary",
1355
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
1356
+ onClick: () => navigate(`/settings/${PLUGIN_ID}/realms/${realmId}/users/create`),
1357
+ children: formatMessage({ id: getTrad("users.create"), defaultMessage: "Create User" })
1358
+ }
1359
+ )
1360
+ }
1361
+ ) : /* @__PURE__ */ jsxs(Box, { style: { opacity: isLoading ? 0.5 : 1, transition: "opacity 0.2s" }, children: [
1362
+ /* @__PURE__ */ jsxs(Table, { colCount: 7, rowCount: users.length + 1, children: [
1363
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
1364
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("user.username"), defaultMessage: "Username" }) }) }),
1365
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("user.email"), defaultMessage: "Email" }) }) }),
1366
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("user.firstName"), defaultMessage: "First Name" }) }) }),
1367
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("user.lastName"), defaultMessage: "Last Name" }) }) }),
1368
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("user.enabled"), defaultMessage: "Enabled" }) }) }),
1369
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("user.emailVerified"), defaultMessage: "Verified" }) }) }),
1370
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Actions" }) })
1371
+ ] }) }),
1372
+ /* @__PURE__ */ jsx(Tbody, { children: users.map((user) => /* @__PURE__ */ jsxs(Tr, { children: [
1373
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", fontWeight: "bold", children: user.username }) }),
1374
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: user.email || "-" }) }),
1375
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: user.firstName || "-" }) }),
1376
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: user.lastName || "-" }) }),
1377
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Badge, { backgroundColor: user.enabled ? "success100" : "danger100", children: user.enabled ? "Yes" : "No" }) }),
1378
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Badge, { backgroundColor: user.emailVerified ? "success100" : "neutral150", children: user.emailVerified ? "Yes" : "No" }) }),
1379
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
1380
+ canResetPassword && /* @__PURE__ */ jsx(
1381
+ IconButton,
1382
+ {
1383
+ withTooltip: false,
1384
+ label: "Reset Password",
1385
+ onClick: () => setResetPasswordUser(user),
1386
+ children: /* @__PURE__ */ jsx(Key, {})
1387
+ }
1388
+ ),
1389
+ canUpdate && /* @__PURE__ */ jsxs(Fragment, { children: [
1390
+ /* @__PURE__ */ jsx(
1391
+ IconButton,
1392
+ {
1393
+ withTooltip: false,
1394
+ label: user.enabled ? "Disable" : "Enable",
1395
+ onClick: () => user.enabled ? disableUser(user.id) : enableUser(user.id),
1396
+ children: user.enabled ? /* @__PURE__ */ jsx(Cross, {}) : /* @__PURE__ */ jsx(Check, {})
1397
+ }
1398
+ ),
1399
+ !user.emailVerified && /* @__PURE__ */ jsx(
1400
+ IconButton,
1401
+ {
1402
+ withTooltip: false,
1403
+ label: "Send Verification Email",
1404
+ onClick: () => sendVerificationEmail(user.id),
1405
+ children: /* @__PURE__ */ jsx(Mail, {})
1406
+ }
1407
+ ),
1408
+ /* @__PURE__ */ jsx(
1409
+ IconButton,
1410
+ {
1411
+ withTooltip: false,
1412
+ label: "Edit",
1413
+ onClick: () => navigate(`/settings/${PLUGIN_ID}/realms/${realmId}/users/${user.id}`),
1414
+ children: /* @__PURE__ */ jsx(Pencil, {})
1415
+ }
1416
+ )
1417
+ ] }),
1418
+ canDelete && /* @__PURE__ */ jsx(IconButton, { withTooltip: false, label: "Delete", onClick: () => setDeleteId(user.id), children: /* @__PURE__ */ jsx(Trash, {}) })
1419
+ ] }) })
1420
+ ] }, user.id)) })
1421
+ ] }),
1422
+ pagination.pageCount > 1 && /* @__PURE__ */ jsx(Box, { marginTop: 4, children: /* @__PURE__ */ jsx(
1423
+ Pagination,
1424
+ {
1425
+ activePage: pagination.page,
1426
+ pageCount: pagination.pageCount,
1427
+ onChangePage: handlePageChange
1428
+ }
1429
+ ) })
1430
+ ] })
1431
+ ] }),
1432
+ /* @__PURE__ */ jsx(Dialog.Root, { open: !!deleteId, onOpenChange: () => setDeleteId(null), children: /* @__PURE__ */ jsxs(Dialog.Content, { children: [
1433
+ /* @__PURE__ */ jsx(Dialog.Header, { children: "Delete User" }),
1434
+ /* @__PURE__ */ jsx(Dialog.Body, { children: formatMessage({
1435
+ id: getTrad("user.delete.confirm"),
1436
+ defaultMessage: "Are you sure you want to delete this user?"
1437
+ }) }),
1438
+ /* @__PURE__ */ jsxs(Dialog.Footer, { children: [
1439
+ /* @__PURE__ */ jsx(Dialog.Cancel, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: "Cancel" }) }),
1440
+ /* @__PURE__ */ jsx(Dialog.Action, { children: /* @__PURE__ */ jsx(Button, { variant: "danger-light", onClick: handleDelete, children: "Delete" }) })
1441
+ ] })
1442
+ ] }) }),
1443
+ /* @__PURE__ */ jsx(Modal.Root, { open: !!resetPasswordUser, onOpenChange: () => setResetPasswordUser(null), children: /* @__PURE__ */ jsxs(Modal.Content, { children: [
1444
+ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsx(Modal.Title, { children: formatMessage({ id: getTrad("user.resetPassword"), defaultMessage: "Reset Password" }) }) }),
1445
+ /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
1446
+ /* @__PURE__ */ jsxs(Typography, { children: [
1447
+ "Reset password for user: ",
1448
+ /* @__PURE__ */ jsx("strong", { children: resetPasswordUser?.username })
1449
+ ] }),
1450
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
1451
+ /* @__PURE__ */ jsx(Field.Label, { required: true, children: formatMessage({ id: getTrad("password.new"), defaultMessage: "New Password" }) }),
1452
+ /* @__PURE__ */ jsx(
1453
+ TextInput,
1454
+ {
1455
+ type: "password",
1456
+ value: newPassword,
1457
+ onChange: (e) => setNewPassword(e.target.value)
1458
+ }
1459
+ )
1460
+ ] }),
1461
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
1462
+ /* @__PURE__ */ jsx(
1463
+ Toggle,
1464
+ {
1465
+ checked: temporaryPassword,
1466
+ onChange: () => setTemporaryPassword(!temporaryPassword),
1467
+ onLabel: "Yes",
1468
+ offLabel: "No"
1469
+ }
1470
+ ),
1471
+ /* @__PURE__ */ jsx(Typography, { children: formatMessage({
1472
+ id: getTrad("password.temporary"),
1473
+ defaultMessage: "Temporary (user must change on login)"
1474
+ }) })
1475
+ ] })
1476
+ ] }) }),
1477
+ /* @__PURE__ */ jsxs(Modal.Footer, { children: [
1478
+ /* @__PURE__ */ jsx(Modal.Close, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: "Cancel" }) }),
1479
+ /* @__PURE__ */ jsx(Button, { onClick: handleResetPassword, disabled: !newPassword, children: "Reset Password" })
1480
+ ] })
1481
+ ] }) })
1482
+ ] });
1483
+ };
1484
+ const useKeycloakRoles = (realmId) => {
1485
+ const client = useFetchClient();
1486
+ const { toggleNotification } = useNotification();
1487
+ const [roles, setRoles] = useState([]);
1488
+ const [isLoading, setIsLoading] = useState(false);
1489
+ const fetchRoles = useCallback(async () => {
1490
+ if (!realmId) return;
1491
+ setIsLoading(true);
1492
+ try {
1493
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/roles`);
1494
+ setRoles(data.data || []);
1495
+ } catch (err) {
1496
+ toggleNotification({
1497
+ type: "danger",
1498
+ message: err.response?.data?.error?.message || "Failed to fetch roles"
1499
+ });
1500
+ } finally {
1501
+ setIsLoading(false);
1502
+ }
1503
+ }, [realmId, client, toggleNotification]);
1504
+ const getUserRoles = useCallback(
1505
+ async (userId) => {
1506
+ try {
1507
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/roles`);
1508
+ return data.data || [];
1509
+ } catch (err) {
1510
+ toggleNotification({
1511
+ type: "danger",
1512
+ message: err.response?.data?.error?.message || "Failed to fetch user roles"
1513
+ });
1514
+ throw err;
1515
+ }
1516
+ },
1517
+ [realmId, client, toggleNotification]
1518
+ );
1519
+ const assignRoles = useCallback(
1520
+ async (userId, rolesToAssign) => {
1521
+ try {
1522
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/roles`, {
1523
+ data: { roles: rolesToAssign }
1524
+ });
1525
+ toggleNotification({
1526
+ type: "success",
1527
+ message: "Roles assigned successfully"
1528
+ });
1529
+ } catch (err) {
1530
+ toggleNotification({
1531
+ type: "danger",
1532
+ message: err.response?.data?.error?.message || "Failed to assign roles"
1533
+ });
1534
+ throw err;
1535
+ }
1536
+ },
1537
+ [realmId, client, toggleNotification]
1538
+ );
1539
+ const removeRoles = useCallback(
1540
+ async (userId, rolesToRemove) => {
1541
+ try {
1542
+ await client.del(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/roles`, {
1543
+ data: { roles: rolesToRemove }
1544
+ });
1545
+ toggleNotification({
1546
+ type: "success",
1547
+ message: "Roles removed successfully"
1548
+ });
1549
+ } catch (err) {
1550
+ toggleNotification({
1551
+ type: "danger",
1552
+ message: err.response?.data?.error?.message || "Failed to remove roles"
1553
+ });
1554
+ throw err;
1555
+ }
1556
+ },
1557
+ [realmId, client, toggleNotification]
1558
+ );
1559
+ useEffect(() => {
1560
+ if (realmId) {
1561
+ fetchRoles();
1562
+ }
1563
+ }, [realmId, fetchRoles]);
1564
+ return {
1565
+ roles,
1566
+ isLoading,
1567
+ fetchRoles,
1568
+ getUserRoles,
1569
+ assignRoles,
1570
+ removeRoles
1571
+ };
1572
+ };
1573
+ const UserEditPage = () => {
1574
+ const { realmId, userId } = useParams();
1575
+ const navigate = useNavigate();
1576
+ const { formatMessage } = useIntl();
1577
+ const { fetchOne: fetchRealm } = useRealms();
1578
+ const { getUser, createUser, updateUser } = useKeycloakUsers(realmId);
1579
+ const { roles, getUserRoles, assignRoles, removeRoles } = useKeycloakRoles(realmId);
1580
+ const isEditMode = !!userId;
1581
+ const [realm, setRealm] = useState(null);
1582
+ const [isLoading, setIsLoading] = useState(true);
1583
+ const [isSaving, setIsSaving] = useState(false);
1584
+ const [formData, setFormData] = useState({
1585
+ username: "",
1586
+ email: "",
1587
+ firstName: "",
1588
+ lastName: "",
1589
+ enabled: true,
1590
+ emailVerified: false
1591
+ });
1592
+ const [selectedRoles, setSelectedRoles] = useState([]);
1593
+ const [originalRoles, setOriginalRoles] = useState([]);
1594
+ const [errors, setErrors] = useState({});
1595
+ useEffect(() => {
1596
+ const loadData = async () => {
1597
+ try {
1598
+ const realmData = await fetchRealm(realmId);
1599
+ setRealm(realmData);
1600
+ if (isEditMode) {
1601
+ const [userData, userRoles] = await Promise.all([
1602
+ getUser(userId),
1603
+ getUserRoles(userId)
1604
+ ]);
1605
+ setFormData({
1606
+ username: userData.username || "",
1607
+ email: userData.email || "",
1608
+ firstName: userData.firstName || "",
1609
+ lastName: userData.lastName || "",
1610
+ enabled: userData.enabled !== false,
1611
+ emailVerified: userData.emailVerified === true
1612
+ });
1613
+ const roleNames = userRoles.map((r) => r.name);
1614
+ setSelectedRoles(roleNames);
1615
+ setOriginalRoles(roleNames);
1616
+ }
1617
+ } catch {
1618
+ navigate(`/settings/${PLUGIN_ID}/realms/${realmId}/users`);
1619
+ } finally {
1620
+ setIsLoading(false);
1621
+ }
1622
+ };
1623
+ loadData();
1624
+ }, [realmId, userId, isEditMode, fetchRealm, getUser, getUserRoles, navigate]);
1625
+ const handleChange = (field) => (e) => {
1626
+ const value = e.target ? e.target.value : e;
1627
+ setFormData((prev) => ({ ...prev, [field]: value }));
1628
+ setErrors((prev) => ({ ...prev, [field]: null }));
1629
+ };
1630
+ const handleToggle = (field) => () => {
1631
+ setFormData((prev) => ({ ...prev, [field]: !prev[field] }));
1632
+ };
1633
+ const validate = () => {
1634
+ const newErrors = {};
1635
+ if (!formData.username) {
1636
+ newErrors.username = "Username is required";
1637
+ }
1638
+ if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
1639
+ newErrors.email = "Invalid email format";
1640
+ }
1641
+ setErrors(newErrors);
1642
+ return Object.keys(newErrors).length === 0;
1643
+ };
1644
+ const handleSubmit = async () => {
1645
+ if (!validate()) return;
1646
+ setIsSaving(true);
1647
+ try {
1648
+ if (isEditMode) {
1649
+ await updateUser(userId, formData);
1650
+ const rolesToAssign = selectedRoles.filter((r) => !originalRoles.includes(r));
1651
+ const rolesToRemove = originalRoles.filter((r) => !selectedRoles.includes(r));
1652
+ if (rolesToAssign.length > 0) {
1653
+ const roleObjects = roles.filter((r) => rolesToAssign.includes(r.name)).map((r) => ({ id: r.id, name: r.name }));
1654
+ await assignRoles(userId, roleObjects);
1655
+ }
1656
+ if (rolesToRemove.length > 0) {
1657
+ const roleObjects = roles.filter((r) => rolesToRemove.includes(r.name)).map((r) => ({ id: r.id, name: r.name }));
1658
+ await removeRoles(userId, roleObjects);
1659
+ }
1660
+ } else {
1661
+ const newUser = await createUser(formData);
1662
+ if (selectedRoles.length > 0 && newUser?.id) {
1663
+ const roleObjects = roles.filter((r) => selectedRoles.includes(r.name)).map((r) => ({ id: r.id, name: r.name }));
1664
+ await assignRoles(newUser.id, roleObjects);
1665
+ }
1666
+ }
1667
+ navigate(`/settings/${PLUGIN_ID}/realms/${realmId}/users`);
1668
+ } catch {
1669
+ } finally {
1670
+ setIsSaving(false);
1671
+ }
1672
+ };
1673
+ if (isLoading) {
1674
+ return /* @__PURE__ */ jsx(Layouts.Root, { children: /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, {}) }) }) });
1675
+ }
1676
+ return /* @__PURE__ */ jsxs(Layouts.Root, { children: [
1677
+ /* @__PURE__ */ jsx(
1678
+ Layouts.Header,
1679
+ {
1680
+ title: isEditMode ? "Edit User" : "Create User",
1681
+ subtitle: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
1682
+ /* @__PURE__ */ jsx(
1683
+ Box,
1684
+ {
1685
+ width: "12px",
1686
+ height: "12px",
1687
+ borderRadius: "50%",
1688
+ background: realm?.color || "primary600"
1689
+ }
1690
+ ),
1691
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: realm?.displayName })
1692
+ ] }),
1693
+ navigationAction: /* @__PURE__ */ jsx(
1694
+ Button,
1695
+ {
1696
+ startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}),
1697
+ variant: "ghost",
1698
+ onClick: () => navigate(`/settings/${PLUGIN_ID}/realms/${realmId}/users`),
1699
+ children: formatMessage({ id: getTrad("common.back"), defaultMessage: "Back" })
1700
+ }
1701
+ ),
1702
+ primaryAction: /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Check, {}), onClick: handleSubmit, loading: isSaving, children: formatMessage({ id: getTrad("common.save"), defaultMessage: "Save" }) })
1703
+ }
1704
+ ),
1705
+ /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsx(Box, { background: "neutral0", padding: 6, shadow: "filterShadow", hasRadius: true, children: /* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
1706
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Field.Root, { error: errors.username, children: [
1707
+ /* @__PURE__ */ jsx(Field.Label, { required: true, children: formatMessage({ id: getTrad("user.username"), defaultMessage: "Username" }) }),
1708
+ /* @__PURE__ */ jsx(
1709
+ TextInput,
1710
+ {
1711
+ name: "username",
1712
+ value: formData.username,
1713
+ onChange: handleChange("username"),
1714
+ disabled: isEditMode
1715
+ }
1716
+ ),
1717
+ /* @__PURE__ */ jsx(Field.Error, {})
1718
+ ] }) }),
1719
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Field.Root, { error: errors.email, children: [
1720
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTrad("user.email"), defaultMessage: "Email" }) }),
1721
+ /* @__PURE__ */ jsx(
1722
+ TextInput,
1723
+ {
1724
+ name: "email",
1725
+ type: "email",
1726
+ value: formData.email,
1727
+ onChange: handleChange("email")
1728
+ }
1729
+ ),
1730
+ /* @__PURE__ */ jsx(Field.Error, {})
1731
+ ] }) }),
1732
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
1733
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTrad("user.firstName"), defaultMessage: "First Name" }) }),
1734
+ /* @__PURE__ */ jsx(
1735
+ TextInput,
1736
+ {
1737
+ name: "firstName",
1738
+ value: formData.firstName,
1739
+ onChange: handleChange("firstName")
1740
+ }
1741
+ )
1742
+ ] }) }),
1743
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
1744
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTrad("user.lastName"), defaultMessage: "Last Name" }) }),
1745
+ /* @__PURE__ */ jsx(
1746
+ TextInput,
1747
+ {
1748
+ name: "lastName",
1749
+ value: formData.lastName,
1750
+ onChange: handleChange("lastName")
1751
+ }
1752
+ )
1753
+ ] }) }),
1754
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, children: [
1755
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: formatMessage({ id: getTrad("user.enabled"), defaultMessage: "Enabled" }) }),
1756
+ /* @__PURE__ */ jsx(
1757
+ Toggle,
1758
+ {
1759
+ checked: formData.enabled,
1760
+ onChange: handleToggle("enabled"),
1761
+ onLabel: "Yes",
1762
+ offLabel: "No"
1763
+ }
1764
+ )
1765
+ ] }) }),
1766
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, children: [
1767
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: formatMessage({ id: getTrad("user.emailVerified"), defaultMessage: "Email Verified" }) }),
1768
+ /* @__PURE__ */ jsx(
1769
+ Toggle,
1770
+ {
1771
+ checked: formData.emailVerified,
1772
+ onChange: handleToggle("emailVerified"),
1773
+ onLabel: "Yes",
1774
+ offLabel: "No"
1775
+ }
1776
+ )
1777
+ ] }) }),
1778
+ roles.length > 0 && /* @__PURE__ */ jsx(Grid.Item, { col: 12, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
1779
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTrad("user.roles"), defaultMessage: "Roles" }) }),
1780
+ /* @__PURE__ */ jsx(
1781
+ MultiSelect,
1782
+ {
1783
+ value: selectedRoles,
1784
+ onChange: setSelectedRoles,
1785
+ placeholder: "Select roles...",
1786
+ children: roles.map((role) => /* @__PURE__ */ jsx(MultiSelectOption, { value: role.name, children: role.name }, role.id))
1787
+ }
1788
+ )
1789
+ ] }) })
1790
+ ] }) }) })
1791
+ ] });
1792
+ };
1793
+ const UsersPage = {
1794
+ List: UserListPage,
1795
+ Edit: UserEditPage
1796
+ };
1797
+ const useAuditLogs = (options = {}) => {
1798
+ const { realmId, keycloakUserId } = options;
1799
+ const client = useFetchClient();
1800
+ const { toggleNotification } = useNotification();
1801
+ const [logs, setLogs] = useState([]);
1802
+ const [isLoading, setIsLoading] = useState(false);
1803
+ const [total, setTotal] = useState(0);
1804
+ const fetchLogs = useCallback(
1805
+ async ({ limit = 50, offset = 0, action, realmName } = {}) => {
1806
+ setIsLoading(true);
1807
+ try {
1808
+ let url = `${API_BASE_PATH}/audit-logs`;
1809
+ const params = { limit, offset };
1810
+ if (realmId) {
1811
+ url = `${API_BASE_PATH}/audit-logs/realm/${realmId}`;
1812
+ } else if (keycloakUserId) {
1813
+ url = `${API_BASE_PATH}/audit-logs/user/${keycloakUserId}`;
1814
+ } else {
1815
+ if (action) params.action = action;
1816
+ if (realmName) params.realmName = realmName;
1817
+ }
1818
+ const { data } = await client.get(url, { params });
1819
+ setLogs(data.data || []);
1820
+ setTotal(data.meta?.total || data.data?.length || 0);
1821
+ } catch (err) {
1822
+ toggleNotification({
1823
+ type: "danger",
1824
+ message: err.response?.data?.error?.message || "Failed to fetch audit logs"
1825
+ });
1826
+ } finally {
1827
+ setIsLoading(false);
1828
+ }
1829
+ },
1830
+ [client, toggleNotification, realmId, keycloakUserId]
1831
+ );
1832
+ useEffect(() => {
1833
+ fetchLogs();
1834
+ }, [fetchLogs]);
1835
+ return {
1836
+ logs,
1837
+ isLoading,
1838
+ total,
1839
+ fetchLogs
1840
+ };
1841
+ };
1842
+ const ACTION_COLORS = {
1843
+ CREATE_USER: "success100",
1844
+ UPDATE_USER: "primary100",
1845
+ DELETE_USER: "danger100",
1846
+ RESET_PASSWORD: "warning100",
1847
+ ASSIGN_ROLE: "success100",
1848
+ REMOVE_ROLE: "warning100",
1849
+ ENABLE_USER: "success100",
1850
+ DISABLE_USER: "warning100",
1851
+ SEND_VERIFY_EMAIL: "primary100",
1852
+ SEND_RESET_PASSWORD_EMAIL: "primary100",
1853
+ BULK_IMPORT: "success100"
1854
+ };
1855
+ const formatDate = (dateString) => {
1856
+ const date = new Date(dateString);
1857
+ return date.toLocaleString();
1858
+ };
1859
+ const AuditPage = () => {
1860
+ const navigate = useNavigate();
1861
+ const { formatMessage } = useIntl();
1862
+ const { realms } = useRealms();
1863
+ const [selectedRealm, setSelectedRealm] = useState("");
1864
+ const [selectedAction, setSelectedAction] = useState("");
1865
+ const { logs, isLoading, fetchLogs } = useAuditLogs();
1866
+ const handleRealmChange = (value) => {
1867
+ setSelectedRealm(value);
1868
+ fetchLogs({ realmName: value || void 0, action: selectedAction || void 0 });
1869
+ };
1870
+ const handleActionChange = (value) => {
1871
+ setSelectedAction(value);
1872
+ fetchLogs({ realmName: selectedRealm || void 0, action: value || void 0 });
1873
+ };
1874
+ return /* @__PURE__ */ jsxs(Layouts.Root, { children: [
1875
+ /* @__PURE__ */ jsx(
1876
+ Layouts.Header,
1877
+ {
1878
+ title: formatMessage({ id: getTrad("audit.title"), defaultMessage: "Audit Log" }),
1879
+ subtitle: formatMessage({
1880
+ id: getTrad("audit.subtitle"),
1881
+ defaultMessage: "Track user management actions"
1882
+ }),
1883
+ navigationAction: /* @__PURE__ */ jsx(
1884
+ Button,
1885
+ {
1886
+ startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}),
1887
+ variant: "ghost",
1888
+ onClick: () => navigate(`/settings/${PLUGIN_ID}`),
1889
+ children: formatMessage({ id: getTrad("common.back"), defaultMessage: "Back" })
1890
+ }
1891
+ )
1892
+ }
1893
+ ),
1894
+ /* @__PURE__ */ jsxs(Layouts.Content, { children: [
1895
+ /* @__PURE__ */ jsx(Box, { marginBottom: 4, children: /* @__PURE__ */ jsxs(Flex, { gap: 4, children: [
1896
+ /* @__PURE__ */ jsx(Box, { width: "200px", children: /* @__PURE__ */ jsxs(Field.Root, { children: [
1897
+ /* @__PURE__ */ jsx(Field.Label, { children: "Filter by Realm" }),
1898
+ /* @__PURE__ */ jsxs(
1899
+ SingleSelect,
1900
+ {
1901
+ value: selectedRealm,
1902
+ onChange: handleRealmChange,
1903
+ placeholder: "All realms",
1904
+ children: [
1905
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "", children: "All realms" }),
1906
+ realms.map((realm) => /* @__PURE__ */ jsx(SingleSelectOption, { value: realm.name, children: realm.displayName }, realm.documentId))
1907
+ ]
1908
+ }
1909
+ )
1910
+ ] }) }),
1911
+ /* @__PURE__ */ jsx(Box, { width: "200px", children: /* @__PURE__ */ jsxs(Field.Root, { children: [
1912
+ /* @__PURE__ */ jsx(Field.Label, { children: "Filter by Action" }),
1913
+ /* @__PURE__ */ jsxs(
1914
+ SingleSelect,
1915
+ {
1916
+ value: selectedAction,
1917
+ onChange: handleActionChange,
1918
+ placeholder: "All actions",
1919
+ children: [
1920
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "", children: "All actions" }),
1921
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "CREATE_USER", children: "Create User" }),
1922
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "UPDATE_USER", children: "Update User" }),
1923
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "DELETE_USER", children: "Delete User" }),
1924
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "RESET_PASSWORD", children: "Reset Password" }),
1925
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "ASSIGN_ROLE", children: "Assign Role" }),
1926
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "REMOVE_ROLE", children: "Remove Role" }),
1927
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "ENABLE_USER", children: "Enable User" }),
1928
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "DISABLE_USER", children: "Disable User" }),
1929
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "BULK_IMPORT", children: "Bulk Import" })
1930
+ ]
1931
+ }
1932
+ )
1933
+ ] }) })
1934
+ ] }) }),
1935
+ isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, {}) }) : logs.length === 0 ? /* @__PURE__ */ jsx(
1936
+ EmptyStateLayout,
1937
+ {
1938
+ icon: /* @__PURE__ */ jsx(File, { width: "10rem", height: "10rem" }),
1939
+ content: "No audit logs found"
1940
+ }
1941
+ ) : /* @__PURE__ */ jsxs(Table, { colCount: 6, rowCount: logs.length + 1, children: [
1942
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
1943
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("audit.date"), defaultMessage: "Date" }) }) }),
1944
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("audit.action"), defaultMessage: "Action" }) }) }),
1945
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("audit.realm"), defaultMessage: "Realm" }) }) }),
1946
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("audit.user"), defaultMessage: "Keycloak User" }) }) }),
1947
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: formatMessage({ id: getTrad("audit.performedBy"), defaultMessage: "Performed By" }) }) }),
1948
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Details" }) })
1949
+ ] }) }),
1950
+ /* @__PURE__ */ jsx(Tbody, { children: logs.map((log) => /* @__PURE__ */ jsxs(Tr, { children: [
1951
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: formatDate(log.createdAt) }) }),
1952
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Badge, { backgroundColor: ACTION_COLORS[log.action] || "neutral150", children: log.action.replace(/_/g, " ") }) }),
1953
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: log.realmDisplayName || log.realmName }) }),
1954
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: log.keycloakUsername || log.keycloakUserId || "-" }) }),
1955
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: log.performedByEmail || "-" }) }),
1956
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", ellipsis: true, children: log.details ? JSON.stringify(log.details).substring(0, 50) : "-" }) })
1957
+ ] }, log.documentId)) })
1958
+ ] })
1959
+ ] })
1960
+ ] });
1961
+ };
1962
+ const App = () => {
1963
+ return /* @__PURE__ */ jsxs(Routes, { children: [
1964
+ /* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(RealmsPage.List, {}) }),
1965
+ /* @__PURE__ */ jsx(Route, { path: "realms/create", element: /* @__PURE__ */ jsx(RealmsPage.Edit, {}) }),
1966
+ /* @__PURE__ */ jsx(Route, { path: "realms/:id", element: /* @__PURE__ */ jsx(RealmsPage.Edit, {}) }),
1967
+ /* @__PURE__ */ jsx(Route, { path: "realms/:realmId/users", element: /* @__PURE__ */ jsx(UsersPage.List, {}) }),
1968
+ /* @__PURE__ */ jsx(Route, { path: "realms/:realmId/users/create", element: /* @__PURE__ */ jsx(UsersPage.Edit, {}) }),
1969
+ /* @__PURE__ */ jsx(Route, { path: "realms/:realmId/users/:userId", element: /* @__PURE__ */ jsx(UsersPage.Edit, {}) }),
1970
+ /* @__PURE__ */ jsx(Route, { path: "audit", element: /* @__PURE__ */ jsx(AuditPage, {}) })
1971
+ ] });
1972
+ };
1973
+ export {
1974
+ App as default
1975
+ };