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,144 @@
1
+ import { PLUGIN_ID, SERVICES, ERROR_MESSAGES } from '../constants.js';
2
+
3
+ const auditController = ({ strapi }) => ({
4
+ /**
5
+ * Find audit logs with filters
6
+ */
7
+ async find(ctx) {
8
+ const user = ctx.state.user;
9
+ const { realmName, action, limit = 50, offset = 0 } = ctx.query;
10
+
11
+ if (!user) {
12
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
13
+ }
14
+
15
+ try {
16
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
17
+
18
+ // Non-super admins can only see logs for their accessible realms
19
+ if (!permissionService.isSuperAdmin(user)) {
20
+ const accessibleRealms = await permissionService.getAccessibleRealms(user);
21
+ const accessibleRealmNames = accessibleRealms.map((r) => r.name);
22
+
23
+ // If filtering by realm, verify access
24
+ if (realmName && !accessibleRealmNames.includes(realmName)) {
25
+ return ctx.forbidden(ERROR_MESSAGES.REALM_ACCESS_DENIED);
26
+ }
27
+
28
+ // If no realm filter, only return logs for accessible realms
29
+ const auditLogService = strapi.plugin(PLUGIN_ID).service(SERVICES.AUDIT_LOG);
30
+
31
+ if (!realmName) {
32
+ // Fetch logs for all accessible realms
33
+ const allLogs = [];
34
+ for (const rn of accessibleRealmNames) {
35
+ const { entries } = await auditLogService.find({
36
+ realmName: rn,
37
+ action,
38
+ limit: parseInt(limit, 10),
39
+ offset: parseInt(offset, 10),
40
+ });
41
+ allLogs.push(...entries);
42
+ }
43
+
44
+ // Sort by createdAt desc and apply limit
45
+ allLogs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
46
+ ctx.body = { data: allLogs.slice(0, parseInt(limit, 10)) };
47
+ return;
48
+ }
49
+ }
50
+
51
+ const auditLogService = strapi.plugin(PLUGIN_ID).service(SERVICES.AUDIT_LOG);
52
+ const result = await auditLogService.find({
53
+ realmName,
54
+ action,
55
+ limit: parseInt(limit, 10),
56
+ offset: parseInt(offset, 10),
57
+ });
58
+
59
+ ctx.body = { data: result.entries, meta: { total: result.total } };
60
+ } catch (err) {
61
+ strapi.log.error(`[${PLUGIN_ID}] audit.find error:`, err);
62
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
63
+ }
64
+ },
65
+
66
+ /**
67
+ * Find audit logs by realm
68
+ */
69
+ async findByRealm(ctx) {
70
+ const user = ctx.state.user;
71
+ const { realmId } = ctx.params;
72
+ const { limit = 50, offset = 0 } = ctx.query;
73
+
74
+ if (!user) {
75
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
76
+ }
77
+
78
+ try {
79
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
80
+ const realmService = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
81
+
82
+ const canAccess = await permissionService.canAccessRealm(user, realmId, 'canRead');
83
+
84
+ if (!canAccess) {
85
+ return ctx.forbidden(ERROR_MESSAGES.REALM_ACCESS_DENIED);
86
+ }
87
+
88
+ const realm = await realmService.findOne(realmId);
89
+ const auditLogService = strapi.plugin(PLUGIN_ID).service(SERVICES.AUDIT_LOG);
90
+ const result = await auditLogService.findByRealm(realm.name, {
91
+ limit: parseInt(limit, 10),
92
+ offset: parseInt(offset, 10),
93
+ });
94
+
95
+ ctx.body = { data: result.entries, meta: { total: result.total } };
96
+ } catch (err) {
97
+ strapi.log.error(`[${PLUGIN_ID}] audit.findByRealm error:`, err);
98
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
99
+ }
100
+ },
101
+
102
+ /**
103
+ * Find audit logs by Keycloak user
104
+ */
105
+ async findByUser(ctx) {
106
+ const user = ctx.state.user;
107
+ const { keycloakUserId } = ctx.params;
108
+ const { limit = 50, offset = 0 } = ctx.query;
109
+
110
+ if (!user) {
111
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
112
+ }
113
+
114
+ try {
115
+ const auditLogService = strapi.plugin(PLUGIN_ID).service(SERVICES.AUDIT_LOG);
116
+ const result = await auditLogService.findByKeycloakUser(keycloakUserId, {
117
+ limit: parseInt(limit, 10),
118
+ offset: parseInt(offset, 10),
119
+ });
120
+
121
+ // Filter by accessible realms for non-super admins
122
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
123
+
124
+ if (!permissionService.isSuperAdmin(user)) {
125
+ const accessibleRealms = await permissionService.getAccessibleRealms(user);
126
+ const accessibleRealmNames = accessibleRealms.map((r) => r.name);
127
+
128
+ const filteredEntries = result.entries.filter((e) =>
129
+ accessibleRealmNames.includes(e.realmName)
130
+ );
131
+
132
+ ctx.body = { data: filteredEntries, meta: { total: filteredEntries.length } };
133
+ return;
134
+ }
135
+
136
+ ctx.body = { data: result.entries, meta: { total: result.total } };
137
+ } catch (err) {
138
+ strapi.log.error(`[${PLUGIN_ID}] audit.findByUser error:`, err);
139
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
140
+ }
141
+ },
142
+ });
143
+
144
+ export default auditController;
@@ -0,0 +1,9 @@
1
+ import realm from './realm.js';
2
+ import user from './user.js';
3
+ import audit from './audit.js';
4
+
5
+ export default {
6
+ realm,
7
+ user,
8
+ audit,
9
+ };
@@ -0,0 +1,324 @@
1
+ import { PLUGIN_ID, SERVICES, ERROR_MESSAGES } from '../constants.js';
2
+
3
+ const realmController = ({ strapi }) => ({
4
+ /**
5
+ * Get all realms (filtered by user access)
6
+ */
7
+ async find(ctx) {
8
+ const user = ctx.state.user;
9
+
10
+ if (!user) {
11
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
12
+ }
13
+
14
+ try {
15
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
16
+ const realms = await permissionService.getAccessibleRealms(user);
17
+ ctx.body = { data: realms };
18
+ } catch (err) {
19
+ strapi.log.error(`[${PLUGIN_ID}] realm.find error:`, err);
20
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
21
+ }
22
+ },
23
+
24
+ /**
25
+ * Get a single realm by ID
26
+ */
27
+ async findOne(ctx) {
28
+ const user = ctx.state.user;
29
+ const { id } = ctx.params;
30
+
31
+ if (!user) {
32
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
33
+ }
34
+
35
+ try {
36
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
37
+ const realmService = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
38
+
39
+ const canAccess = await permissionService.canAccessRealm(user, id, 'canRead');
40
+
41
+ if (!canAccess) {
42
+ return ctx.forbidden(ERROR_MESSAGES.REALM_ACCESS_DENIED);
43
+ }
44
+
45
+ const realm = await realmService.findOne(id);
46
+ const permissions = await permissionService.getRealmPermissions(user, id);
47
+
48
+ ctx.body = { data: { ...realm, permissions } };
49
+ } catch (err) {
50
+ strapi.log.error(`[${PLUGIN_ID}] realm.findOne error:`, err);
51
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
52
+ }
53
+ },
54
+
55
+ /**
56
+ * Create a new realm (super admin only)
57
+ */
58
+ async create(ctx) {
59
+ const user = ctx.state.user;
60
+ const { data } = ctx.request.body;
61
+
62
+ if (!user) {
63
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
64
+ }
65
+
66
+ try {
67
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
68
+
69
+ if (!permissionService.isSuperAdmin(user)) {
70
+ return ctx.forbidden('Only super admins can create realm configurations.');
71
+ }
72
+
73
+ const realmService = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
74
+ const realm = await realmService.create(data);
75
+
76
+ ctx.body = { data: realm };
77
+ } catch (err) {
78
+ strapi.log.error(`[${PLUGIN_ID}] realm.create error:`, err);
79
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
80
+ }
81
+ },
82
+
83
+ /**
84
+ * Update a realm (super admin only)
85
+ */
86
+ async update(ctx) {
87
+ const user = ctx.state.user;
88
+ const { id } = ctx.params;
89
+ const { data } = ctx.request.body;
90
+
91
+ if (!user) {
92
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
93
+ }
94
+
95
+ try {
96
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
97
+
98
+ if (!permissionService.isSuperAdmin(user)) {
99
+ return ctx.forbidden('Only super admins can update realm configurations.');
100
+ }
101
+
102
+ const realmService = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
103
+ const realm = await realmService.update(id, data);
104
+
105
+ ctx.body = { data: realm };
106
+ } catch (err) {
107
+ strapi.log.error(`[${PLUGIN_ID}] realm.update error:`, err);
108
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
109
+ }
110
+ },
111
+
112
+ /**
113
+ * Delete a realm (super admin only)
114
+ */
115
+ async delete(ctx) {
116
+ const user = ctx.state.user;
117
+ const { id } = ctx.params;
118
+
119
+ if (!user) {
120
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
121
+ }
122
+
123
+ try {
124
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
125
+
126
+ if (!permissionService.isSuperAdmin(user)) {
127
+ return ctx.forbidden('Only super admins can delete realm configurations.');
128
+ }
129
+
130
+ const realmService = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
131
+ await realmService.delete(id);
132
+
133
+ ctx.body = { data: { success: true } };
134
+ } catch (err) {
135
+ strapi.log.error(`[${PLUGIN_ID}] realm.delete error:`, err);
136
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
137
+ }
138
+ },
139
+
140
+ /**
141
+ * Test realm connection
142
+ */
143
+ async testConnection(ctx) {
144
+ const user = ctx.state.user;
145
+ const { id } = ctx.params;
146
+
147
+ if (!user) {
148
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
149
+ }
150
+
151
+ try {
152
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
153
+ const realmService = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
154
+
155
+ const canAccess = await permissionService.canAccessRealm(user, id, 'canRead');
156
+
157
+ if (!canAccess) {
158
+ return ctx.forbidden(ERROR_MESSAGES.REALM_ACCESS_DENIED);
159
+ }
160
+
161
+ const result = await realmService.testConnection(id);
162
+ ctx.body = { data: result };
163
+ } catch (err) {
164
+ strapi.log.error(`[${PLUGIN_ID}] realm.testConnection error:`, err);
165
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
166
+ }
167
+ },
168
+
169
+ /**
170
+ * Test connection before saving (with raw config)
171
+ */
172
+ async testConnectionRaw(ctx) {
173
+ const user = ctx.state.user;
174
+ const { data } = ctx.request.body;
175
+
176
+ if (!user) {
177
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
178
+ }
179
+
180
+ try {
181
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
182
+
183
+ if (!permissionService.isSuperAdmin(user)) {
184
+ return ctx.forbidden('Only super admins can test realm configurations.');
185
+ }
186
+
187
+ const realmService = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
188
+ const result = await realmService.testConnectionRaw(data);
189
+
190
+ ctx.body = { data: result };
191
+ } catch (err) {
192
+ strapi.log.error(`[${PLUGIN_ID}] realm.testConnectionRaw error:`, err);
193
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
194
+ }
195
+ },
196
+
197
+ /**
198
+ * Get admins assigned to a realm
199
+ */
200
+ async getAdmins(ctx) {
201
+ const user = ctx.state.user;
202
+ const { id } = ctx.params;
203
+
204
+ if (!user) {
205
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
206
+ }
207
+
208
+ try {
209
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
210
+
211
+ if (!permissionService.isSuperAdmin(user)) {
212
+ return ctx.forbidden('Only super admins can view realm admin assignments.');
213
+ }
214
+
215
+ const admins = await permissionService.getRealmAdmins(id);
216
+ ctx.body = { data: admins };
217
+ } catch (err) {
218
+ strapi.log.error(`[${PLUGIN_ID}] realm.getAdmins error:`, err);
219
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
220
+ }
221
+ },
222
+
223
+ /**
224
+ * Assign an admin to a realm
225
+ */
226
+ async addAdmin(ctx) {
227
+ const user = ctx.state.user;
228
+ const { id } = ctx.params;
229
+ const { data } = ctx.request.body;
230
+
231
+ if (!user) {
232
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
233
+ }
234
+
235
+ try {
236
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
237
+
238
+ if (!permissionService.isSuperAdmin(user)) {
239
+ return ctx.forbidden('Only super admins can assign realm admins.');
240
+ }
241
+
242
+ const { strapiUserId, strapiUserEmail, permissions } = data;
243
+
244
+ if (!strapiUserId) {
245
+ return ctx.badRequest('strapiUserId is required.');
246
+ }
247
+
248
+ const assignment = await permissionService.assignUserToRealm(
249
+ strapiUserId,
250
+ strapiUserEmail,
251
+ id,
252
+ permissions
253
+ );
254
+
255
+ ctx.body = { data: assignment };
256
+ } catch (err) {
257
+ strapi.log.error(`[${PLUGIN_ID}] realm.addAdmin error:`, err);
258
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
259
+ }
260
+ },
261
+
262
+ /**
263
+ * Update admin permissions for a realm
264
+ */
265
+ async updateAdmin(ctx) {
266
+ const user = ctx.state.user;
267
+ const { id, adminId } = ctx.params;
268
+ const { data } = ctx.request.body;
269
+
270
+ if (!user) {
271
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
272
+ }
273
+
274
+ try {
275
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
276
+
277
+ if (!permissionService.isSuperAdmin(user)) {
278
+ return ctx.forbidden('Only super admins can update realm admin assignments.');
279
+ }
280
+
281
+ const { permissions } = data;
282
+
283
+ const assignment = await permissionService.assignUserToRealm(
284
+ parseInt(adminId, 10),
285
+ null,
286
+ id,
287
+ permissions
288
+ );
289
+
290
+ ctx.body = { data: assignment };
291
+ } catch (err) {
292
+ strapi.log.error(`[${PLUGIN_ID}] realm.updateAdmin error:`, err);
293
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
294
+ }
295
+ },
296
+
297
+ /**
298
+ * Remove an admin from a realm
299
+ */
300
+ async removeAdmin(ctx) {
301
+ const user = ctx.state.user;
302
+ const { id, adminId } = ctx.params;
303
+
304
+ if (!user) {
305
+ return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
306
+ }
307
+
308
+ try {
309
+ const permissionService = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
310
+
311
+ if (!permissionService.isSuperAdmin(user)) {
312
+ return ctx.forbidden('Only super admins can remove realm admins.');
313
+ }
314
+
315
+ await permissionService.removeUserFromRealm(parseInt(adminId, 10), id);
316
+ ctx.body = { data: { success: true } };
317
+ } catch (err) {
318
+ strapi.log.error(`[${PLUGIN_ID}] realm.removeAdmin error:`, err);
319
+ ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
320
+ }
321
+ },
322
+ });
323
+
324
+ export default realmController;