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,253 @@
1
+ export default [
2
+ // ==================== REALM MANAGEMENT ====================
3
+
4
+ // Get all realms (filtered by user access)
5
+ {
6
+ method: 'GET',
7
+ path: '/realms',
8
+ handler: 'realm.find',
9
+ config: { policies: [] },
10
+ },
11
+
12
+ // Test connection with raw config (before saving)
13
+ {
14
+ method: 'POST',
15
+ path: '/realms/test-connection',
16
+ handler: 'realm.testConnectionRaw',
17
+ config: { policies: [] },
18
+ },
19
+
20
+ // Get a single realm
21
+ {
22
+ method: 'GET',
23
+ path: '/realms/:id',
24
+ handler: 'realm.findOne',
25
+ config: { policies: [] },
26
+ },
27
+
28
+ // Create a new realm
29
+ {
30
+ method: 'POST',
31
+ path: '/realms',
32
+ handler: 'realm.create',
33
+ config: { policies: [] },
34
+ },
35
+
36
+ // Update a realm
37
+ {
38
+ method: 'PUT',
39
+ path: '/realms/:id',
40
+ handler: 'realm.update',
41
+ config: { policies: [] },
42
+ },
43
+
44
+ // Delete a realm
45
+ {
46
+ method: 'DELETE',
47
+ path: '/realms/:id',
48
+ handler: 'realm.delete',
49
+ config: { policies: [] },
50
+ },
51
+
52
+ // Test realm connection
53
+ {
54
+ method: 'POST',
55
+ path: '/realms/:id/test',
56
+ handler: 'realm.testConnection',
57
+ config: { policies: [] },
58
+ },
59
+
60
+ // ==================== REALM ADMIN ASSIGNMENTS ====================
61
+
62
+ // Get admins for a realm
63
+ {
64
+ method: 'GET',
65
+ path: '/realms/:id/admins',
66
+ handler: 'realm.getAdmins',
67
+ config: { policies: [] },
68
+ },
69
+
70
+ // Add admin to a realm
71
+ {
72
+ method: 'POST',
73
+ path: '/realms/:id/admins',
74
+ handler: 'realm.addAdmin',
75
+ config: { policies: [] },
76
+ },
77
+
78
+ // Update admin permissions
79
+ {
80
+ method: 'PUT',
81
+ path: '/realms/:id/admins/:adminId',
82
+ handler: 'realm.updateAdmin',
83
+ config: { policies: [] },
84
+ },
85
+
86
+ // Remove admin from realm
87
+ {
88
+ method: 'DELETE',
89
+ path: '/realms/:id/admins/:adminId',
90
+ handler: 'realm.removeAdmin',
91
+ config: { policies: [] },
92
+ },
93
+
94
+ // ==================== KEYCLOAK USERS ====================
95
+
96
+ // List users in a realm
97
+ {
98
+ method: 'GET',
99
+ path: '/realms/:realmId/users',
100
+ handler: 'user.find',
101
+ config: { policies: [] },
102
+ },
103
+
104
+ // Export users
105
+ {
106
+ method: 'GET',
107
+ path: '/realms/:realmId/users/export',
108
+ handler: 'user.exportUsers',
109
+ config: { policies: [] },
110
+ },
111
+
112
+ // Bulk import users
113
+ {
114
+ method: 'POST',
115
+ path: '/realms/:realmId/users/import',
116
+ handler: 'user.bulkImport',
117
+ config: { policies: [] },
118
+ },
119
+
120
+ // Get a single user
121
+ {
122
+ method: 'GET',
123
+ path: '/realms/:realmId/users/:id',
124
+ handler: 'user.findOne',
125
+ config: { policies: [] },
126
+ },
127
+
128
+ // Create a user
129
+ {
130
+ method: 'POST',
131
+ path: '/realms/:realmId/users',
132
+ handler: 'user.create',
133
+ config: { policies: [] },
134
+ },
135
+
136
+ // Update a user
137
+ {
138
+ method: 'PUT',
139
+ path: '/realms/:realmId/users/:id',
140
+ handler: 'user.update',
141
+ config: { policies: [] },
142
+ },
143
+
144
+ // Delete a user
145
+ {
146
+ method: 'DELETE',
147
+ path: '/realms/:realmId/users/:id',
148
+ handler: 'user.delete',
149
+ config: { policies: [] },
150
+ },
151
+
152
+ // ==================== USER ACTIONS ====================
153
+
154
+ // Reset password
155
+ {
156
+ method: 'POST',
157
+ path: '/realms/:realmId/users/:id/reset-password',
158
+ handler: 'user.resetPassword',
159
+ config: { policies: [] },
160
+ },
161
+
162
+ // Enable user
163
+ {
164
+ method: 'POST',
165
+ path: '/realms/:realmId/users/:id/enable',
166
+ handler: 'user.enable',
167
+ config: { policies: [] },
168
+ },
169
+
170
+ // Disable user
171
+ {
172
+ method: 'POST',
173
+ path: '/realms/:realmId/users/:id/disable',
174
+ handler: 'user.disable',
175
+ config: { policies: [] },
176
+ },
177
+
178
+ // Send verification email
179
+ {
180
+ method: 'POST',
181
+ path: '/realms/:realmId/users/:id/send-verify-email',
182
+ handler: 'user.sendVerifyEmail',
183
+ config: { policies: [] },
184
+ },
185
+
186
+ // Send password reset email
187
+ {
188
+ method: 'POST',
189
+ path: '/realms/:realmId/users/:id/send-reset-password-email',
190
+ handler: 'user.sendResetPasswordEmail',
191
+ config: { policies: [] },
192
+ },
193
+
194
+ // ==================== ROLES ====================
195
+
196
+ // Get realm roles
197
+ {
198
+ method: 'GET',
199
+ path: '/realms/:realmId/roles',
200
+ handler: 'user.getRoles',
201
+ config: { policies: [] },
202
+ },
203
+
204
+ // Get user's roles
205
+ {
206
+ method: 'GET',
207
+ path: '/realms/:realmId/users/:id/roles',
208
+ handler: 'user.getUserRoles',
209
+ config: { policies: [] },
210
+ },
211
+
212
+ // Assign roles to user
213
+ {
214
+ method: 'POST',
215
+ path: '/realms/:realmId/users/:id/roles',
216
+ handler: 'user.assignRoles',
217
+ config: { policies: [] },
218
+ },
219
+
220
+ // Remove roles from user
221
+ {
222
+ method: 'DELETE',
223
+ path: '/realms/:realmId/users/:id/roles',
224
+ handler: 'user.removeRoles',
225
+ config: { policies: [] },
226
+ },
227
+
228
+ // ==================== AUDIT LOGS ====================
229
+
230
+ // Get all audit logs
231
+ {
232
+ method: 'GET',
233
+ path: '/audit-logs',
234
+ handler: 'audit.find',
235
+ config: { policies: [] },
236
+ },
237
+
238
+ // Get audit logs by realm
239
+ {
240
+ method: 'GET',
241
+ path: '/audit-logs/realm/:realmId',
242
+ handler: 'audit.findByRealm',
243
+ config: { policies: [] },
244
+ },
245
+
246
+ // Get audit logs by Keycloak user
247
+ {
248
+ method: 'GET',
249
+ path: '/audit-logs/user/:keycloakUserId',
250
+ handler: 'audit.findByUser',
251
+ config: { policies: [] },
252
+ },
253
+ ];
@@ -0,0 +1,8 @@
1
+ import admin from './admin.js';
2
+
3
+ export default {
4
+ admin: {
5
+ type: 'admin',
6
+ routes: admin,
7
+ },
8
+ };
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @fileoverview Audit logging service for tracking user management actions.
3
+ * Records all significant operations for compliance and debugging purposes.
4
+ *
5
+ * @module services/audit-log
6
+ */
7
+
8
+ import {
9
+ PLUGIN_ID,
10
+ CONTENT_TYPES,
11
+ PAGINATION,
12
+ UNKNOWN_USER_EMAIL,
13
+ } from '../constants.js';
14
+
15
+ /**
16
+ * @typedef {Object} AuditLogEntry
17
+ * @property {string} documentId - Entry document ID
18
+ * @property {string} realmName - Realm slug identifier
19
+ * @property {string} realmDisplayName - Realm display name
20
+ * @property {string} action - Action type (from AUDIT_ACTIONS constant)
21
+ * @property {string|null} keycloakUserId - Target Keycloak user ID
22
+ * @property {string|null} keycloakUsername - Target Keycloak username
23
+ * @property {Object|null} details - Action-specific metadata
24
+ * @property {number|null} performedById - Strapi admin user ID
25
+ * @property {string} performedByEmail - Strapi admin email
26
+ * @property {Date} createdAt - Entry creation timestamp
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} AuditLogParams
31
+ * @property {string} realmName - Realm slug identifier
32
+ * @property {string} [realmDisplayName] - Realm display name (defaults to realmName)
33
+ * @property {string} action - Action type from AUDIT_ACTIONS
34
+ * @property {string} [keycloakUserId] - Target Keycloak user ID
35
+ * @property {string} [keycloakUsername] - Target Keycloak username
36
+ * @property {Object} [details] - Action-specific metadata
37
+ * @property {Object} [user] - Strapi user who performed the action
38
+ * @property {number} [user.id] - User ID
39
+ * @property {string} [user.email] - User email
40
+ */
41
+
42
+ /**
43
+ * @typedef {Object} AuditLogQueryParams
44
+ * @property {string} [realmName] - Filter by realm
45
+ * @property {string} [action] - Filter by action type
46
+ * @property {string} [keycloakUserId] - Filter by Keycloak user
47
+ * @property {number} [limit=50] - Maximum entries to return
48
+ * @property {number} [offset=0] - Starting offset for pagination
49
+ */
50
+
51
+ /**
52
+ * @typedef {Object} AuditLogQueryResult
53
+ * @property {AuditLogEntry[]} entries - Log entries
54
+ * @property {number} total - Total matching entries
55
+ */
56
+
57
+ /**
58
+ * Creates the audit log service.
59
+ *
60
+ * @param {Object} params - Service parameters
61
+ * @param {Object} params.strapi - Strapi instance
62
+ * @returns {Object} Audit log service methods
63
+ */
64
+ const auditLogService = ({ strapi }) => ({
65
+ /**
66
+ * Creates an audit log entry.
67
+ * This method is designed to be non-blocking and fail-safe - audit logging
68
+ * errors are caught and logged but do not interrupt the main operation.
69
+ *
70
+ * @param {AuditLogParams} params - Log entry parameters
71
+ * @returns {Promise<AuditLogEntry|undefined>} Created entry or undefined on error
72
+ *
73
+ * @example
74
+ * await auditLogService.log({
75
+ * realmName: 'production',
76
+ * realmDisplayName: 'Production Users',
77
+ * action: AUDIT_ACTIONS.CREATE_USER,
78
+ * keycloakUserId: 'user-123',
79
+ * keycloakUsername: 'john.doe',
80
+ * details: { email: 'john@example.com' },
81
+ * user: ctx.state.user
82
+ * });
83
+ */
84
+ async log({ realmName, realmDisplayName, action, keycloakUserId, keycloakUsername, details, user }) {
85
+ try {
86
+ return await strapi.documents(CONTENT_TYPES.AUDIT_LOG).create({
87
+ data: {
88
+ realmName,
89
+ realmDisplayName: realmDisplayName || realmName,
90
+ action,
91
+ keycloakUserId: keycloakUserId || null,
92
+ keycloakUsername: keycloakUsername || null,
93
+ details: details || null,
94
+ performedById: user?.id || null,
95
+ performedByEmail: user?.email || UNKNOWN_USER_EMAIL,
96
+ },
97
+ });
98
+ } catch (err) {
99
+ // Log error but don't throw - audit logging should not break main operations
100
+ strapi.log.error(`[${PLUGIN_ID}] Failed to create audit log entry:`, err);
101
+ }
102
+ },
103
+
104
+ /**
105
+ * Queries audit log entries with optional filters.
106
+ *
107
+ * @param {AuditLogQueryParams} [params={}] - Query parameters
108
+ * @returns {Promise<AuditLogQueryResult>} Paginated log entries with total count
109
+ *
110
+ * @example
111
+ * // Get all entries for a realm
112
+ * const { entries, total } = await auditLogService.find({
113
+ * realmName: 'production',
114
+ * limit: 25,
115
+ * offset: 0
116
+ * });
117
+ *
118
+ * @example
119
+ * // Get all password reset actions
120
+ * const { entries } = await auditLogService.find({
121
+ * action: AUDIT_ACTIONS.RESET_PASSWORD
122
+ * });
123
+ */
124
+ async find({
125
+ realmName,
126
+ action,
127
+ keycloakUserId,
128
+ limit = PAGINATION.AUDIT_LOG_LIMIT,
129
+ offset = 0
130
+ } = {}) {
131
+ // Build filters object
132
+ const filters = {};
133
+
134
+ if (realmName) {
135
+ filters.realmName = realmName;
136
+ }
137
+ if (action) {
138
+ filters.action = action;
139
+ }
140
+ if (keycloakUserId) {
141
+ filters.keycloakUserId = keycloakUserId;
142
+ }
143
+
144
+ // Execute queries in parallel
145
+ const [entries, count] = await Promise.all([
146
+ strapi.documents(CONTENT_TYPES.AUDIT_LOG).findMany({
147
+ filters,
148
+ sort: { createdAt: 'desc' },
149
+ limit,
150
+ offset,
151
+ }),
152
+ strapi.documents(CONTENT_TYPES.AUDIT_LOG).count({ filters }),
153
+ ]);
154
+
155
+ return { entries, total: count };
156
+ },
157
+
158
+ /**
159
+ * Retrieves audit logs for a specific realm.
160
+ * Convenience method that wraps find() with realm filter.
161
+ *
162
+ * @param {string} realmName - Realm slug identifier
163
+ * @param {Object} [options={}] - Query options
164
+ * @param {number} [options.limit=50] - Maximum entries
165
+ * @param {number} [options.offset=0] - Starting offset
166
+ * @returns {Promise<AuditLogQueryResult>} Paginated log entries
167
+ *
168
+ * @example
169
+ * const { entries, total } = await auditLogService.findByRealm('production', {
170
+ * limit: 100
171
+ * });
172
+ */
173
+ async findByRealm(realmName, { limit = PAGINATION.AUDIT_LOG_LIMIT, offset = 0 } = {}) {
174
+ return this.find({ realmName, limit, offset });
175
+ },
176
+
177
+ /**
178
+ * Retrieves audit logs for a specific Keycloak user.
179
+ * Useful for viewing all actions performed on a single user.
180
+ *
181
+ * @param {string} keycloakUserId - Keycloak user ID
182
+ * @param {Object} [options={}] - Query options
183
+ * @param {number} [options.limit=50] - Maximum entries
184
+ * @param {number} [options.offset=0] - Starting offset
185
+ * @returns {Promise<AuditLogQueryResult>} Paginated log entries
186
+ *
187
+ * @example
188
+ * const { entries } = await auditLogService.findByKeycloakUser('user-123');
189
+ * // Shows: created, updated, password reset, role changes, etc.
190
+ */
191
+ async findByKeycloakUser(keycloakUserId, { limit = PAGINATION.AUDIT_LOG_LIMIT, offset = 0 } = {}) {
192
+ return this.find({ keycloakUserId, limit, offset });
193
+ },
194
+ });
195
+
196
+ export default auditLogService;
@@ -0,0 +1,13 @@
1
+ import keycloakClient from './keycloak-client.js';
2
+ import auditLog from './audit-log.js';
3
+ import permission from './permission.js';
4
+ import realm from './realm.js';
5
+ import user from './user.js';
6
+
7
+ export default {
8
+ 'keycloak-client': keycloakClient,
9
+ 'audit-log': auditLog,
10
+ permission,
11
+ realm,
12
+ user,
13
+ };