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,142 @@
1
+ /**
2
+ * @fileoverview React hook for fetching and managing audit logs.
3
+ * Provides filtering by realm, user, and action type.
4
+ * @module admin/hooks/useAuditLogs
5
+ */
6
+
7
+ import { useState, useEffect, useCallback } from 'react';
8
+ import { useFetchClient, useNotification } from '@strapi/strapi/admin';
9
+ import { API_BASE_PATH } from '../constants';
10
+
11
+ /**
12
+ * @typedef {Object} AuditLogEntry
13
+ * @property {string} documentId - Log entry document ID
14
+ * @property {string} realmName - Realm slug identifier
15
+ * @property {string} realmDisplayName - Realm display name
16
+ * @property {string} action - Action type (e.g., 'CREATE_USER', 'RESET_PASSWORD')
17
+ * @property {string|null} keycloakUserId - Target Keycloak user ID
18
+ * @property {string|null} keycloakUsername - Target Keycloak username
19
+ * @property {Object|null} details - Action-specific metadata
20
+ * @property {number|null} performedById - Strapi admin user ID
21
+ * @property {string} performedByEmail - Strapi admin email
22
+ * @property {string} createdAt - ISO timestamp
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} UseAuditLogsOptions
27
+ * @property {string} [realmId] - Filter by realm document ID
28
+ * @property {string} [keycloakUserId] - Filter by Keycloak user ID
29
+ */
30
+
31
+ /**
32
+ * @typedef {Object} FetchLogsParams
33
+ * @property {number} [limit=50] - Maximum entries to return
34
+ * @property {number} [offset=0] - Starting offset for pagination
35
+ * @property {string} [action] - Filter by action type
36
+ * @property {string} [realmName] - Filter by realm name
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} UseAuditLogsReturn
41
+ * @property {AuditLogEntry[]} logs - Array of audit log entries
42
+ * @property {boolean} isLoading - Loading state
43
+ * @property {number} total - Total matching entries
44
+ * @property {Function} fetchLogs - Fetch logs with optional filters
45
+ */
46
+
47
+ /**
48
+ * Hook for fetching audit logs with optional filtering.
49
+ * Supports filtering by realm, user, and action type.
50
+ *
51
+ * @param {UseAuditLogsOptions} [options={}] - Filter options
52
+ * @returns {UseAuditLogsReturn} Audit log data and fetch function
53
+ *
54
+ * @example
55
+ * // Fetch all logs
56
+ * function AuditLogPage() {
57
+ * const { logs, isLoading, total, fetchLogs } = useAuditLogs();
58
+ *
59
+ * return (
60
+ * <LogTable
61
+ * entries={logs}
62
+ * total={total}
63
+ * onPageChange={(offset) => fetchLogs({ offset })}
64
+ * />
65
+ * );
66
+ * }
67
+ *
68
+ * @example
69
+ * // Fetch logs for a specific realm
70
+ * function RealmAuditLogs({ realmId }) {
71
+ * const { logs } = useAuditLogs({ realmId });
72
+ * // logs are automatically filtered by realm
73
+ * }
74
+ *
75
+ * @example
76
+ * // Fetch logs for a specific user
77
+ * function UserActivityLog({ keycloakUserId }) {
78
+ * const { logs } = useAuditLogs({ keycloakUserId });
79
+ * // Shows all actions performed on this user
80
+ * }
81
+ */
82
+ export const useAuditLogs = (options = {}) => {
83
+ const { realmId, keycloakUserId } = options;
84
+ const client = useFetchClient();
85
+ const { toggleNotification } = useNotification();
86
+
87
+ const [logs, setLogs] = useState([]);
88
+ const [isLoading, setIsLoading] = useState(false);
89
+ const [total, setTotal] = useState(0);
90
+
91
+ /**
92
+ * Fetches audit logs with optional pagination and filters.
93
+ * URL is determined by the hook options (realmId or keycloakUserId).
94
+ *
95
+ * @param {FetchLogsParams} [params={}] - Query parameters
96
+ * @returns {Promise<void>}
97
+ */
98
+ const fetchLogs = useCallback(
99
+ async ({ limit = 50, offset = 0, action, realmName } = {}) => {
100
+ setIsLoading(true);
101
+ try {
102
+ let url = `${API_BASE_PATH}/audit-logs`;
103
+ const params = { limit, offset };
104
+
105
+ if (realmId) {
106
+ url = `${API_BASE_PATH}/audit-logs/realm/${realmId}`;
107
+ } else if (keycloakUserId) {
108
+ url = `${API_BASE_PATH}/audit-logs/user/${keycloakUserId}`;
109
+ } else {
110
+ if (action) params.action = action;
111
+ if (realmName) params.realmName = realmName;
112
+ }
113
+
114
+ const { data } = await client.get(url, { params });
115
+ setLogs(data.data || []);
116
+ setTotal(data.meta?.total || data.data?.length || 0);
117
+ } catch (err) {
118
+ toggleNotification({
119
+ type: 'danger',
120
+ message: err.response?.data?.error?.message || 'Failed to fetch audit logs',
121
+ });
122
+ } finally {
123
+ setIsLoading(false);
124
+ }
125
+ },
126
+ [client, toggleNotification, realmId, keycloakUserId]
127
+ );
128
+
129
+ // Fetch logs on mount and when options change
130
+ useEffect(() => {
131
+ fetchLogs();
132
+ }, [fetchLogs]);
133
+
134
+ return {
135
+ logs,
136
+ isLoading,
137
+ total,
138
+ fetchLogs,
139
+ };
140
+ };
141
+
142
+ export default useAuditLogs;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @fileoverview React hook for managing Keycloak realm roles.
3
+ * Provides role fetching and user role assignment functionality.
4
+ * @module admin/hooks/useKeycloakRoles
5
+ */
6
+
7
+ import { useState, useEffect, useCallback } from 'react';
8
+ import { useFetchClient, useNotification } from '@strapi/strapi/admin';
9
+ import { API_BASE_PATH } from '../constants';
10
+
11
+ /**
12
+ * @typedef {Object} KeycloakRole
13
+ * @property {string} id - Role unique identifier
14
+ * @property {string} name - Role name
15
+ * @property {string} [description] - Role description
16
+ * @property {boolean} [composite] - Whether role contains other roles
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} UseKeycloakRolesReturn
21
+ * @property {KeycloakRole[]} roles - Array of available realm roles
22
+ * @property {boolean} isLoading - Loading state
23
+ * @property {Function} fetchRoles - Refetch all roles
24
+ * @property {Function} getUserRoles - Get roles assigned to a user
25
+ * @property {Function} assignRoles - Assign roles to a user
26
+ * @property {Function} removeRoles - Remove roles from a user
27
+ */
28
+
29
+ /**
30
+ * Hook for managing Keycloak realm roles for a specific realm.
31
+ * Provides functionality to fetch roles and manage user role assignments.
32
+ *
33
+ * @param {string} realmId - Realm document ID
34
+ * @returns {UseKeycloakRolesReturn} Role management functions and state
35
+ *
36
+ * @example
37
+ * function UserRolesEditor({ realmId, userId }) {
38
+ * const { roles, getUserRoles, assignRoles, removeRoles } = useKeycloakRoles(realmId);
39
+ * const [userRoles, setUserRoles] = useState([]);
40
+ *
41
+ * useEffect(() => {
42
+ * getUserRoles(userId).then(setUserRoles);
43
+ * }, [userId]);
44
+ *
45
+ * return (
46
+ * <RoleSelector
47
+ * availableRoles={roles}
48
+ * assignedRoles={userRoles}
49
+ * onAssign={(role) => assignRoles(userId, [role])}
50
+ * onRemove={(role) => removeRoles(userId, [role])}
51
+ * />
52
+ * );
53
+ * }
54
+ */
55
+ export const useKeycloakRoles = (realmId) => {
56
+ const client = useFetchClient();
57
+ const { toggleNotification } = useNotification();
58
+
59
+ const [roles, setRoles] = useState([]);
60
+ const [isLoading, setIsLoading] = useState(false);
61
+
62
+ /**
63
+ * Fetches all available realm roles from Keycloak.
64
+ *
65
+ * @returns {Promise<void>}
66
+ */
67
+ const fetchRoles = useCallback(async () => {
68
+ if (!realmId) return;
69
+
70
+ setIsLoading(true);
71
+ try {
72
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/roles`);
73
+ setRoles(data.data || []);
74
+ } catch (err) {
75
+ toggleNotification({
76
+ type: 'danger',
77
+ message: err.response?.data?.error?.message || 'Failed to fetch roles',
78
+ });
79
+ } finally {
80
+ setIsLoading(false);
81
+ }
82
+ }, [realmId, client, toggleNotification]);
83
+
84
+ /**
85
+ * Fetches roles currently assigned to a specific user.
86
+ *
87
+ * @param {string} userId - Keycloak user ID
88
+ * @returns {Promise<KeycloakRole[]>} Array of assigned roles
89
+ * @throws {Error} If fetch fails
90
+ */
91
+ const getUserRoles = useCallback(
92
+ async (userId) => {
93
+ try {
94
+ const { data } = await client.get(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/roles`);
95
+ return data.data || [];
96
+ } catch (err) {
97
+ toggleNotification({
98
+ type: 'danger',
99
+ message: err.response?.data?.error?.message || 'Failed to fetch user roles',
100
+ });
101
+ throw err;
102
+ }
103
+ },
104
+ [realmId, client, toggleNotification]
105
+ );
106
+
107
+ /**
108
+ * Assigns roles to a user.
109
+ *
110
+ * @param {string} userId - Keycloak user ID
111
+ * @param {KeycloakRole[]} rolesToAssign - Roles to assign (must include id and name)
112
+ * @returns {Promise<void>}
113
+ * @throws {Error} If assignment fails
114
+ */
115
+ const assignRoles = useCallback(
116
+ async (userId, rolesToAssign) => {
117
+ try {
118
+ await client.post(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/roles`, {
119
+ data: { roles: rolesToAssign },
120
+ });
121
+ toggleNotification({
122
+ type: 'success',
123
+ message: 'Roles assigned successfully',
124
+ });
125
+ } catch (err) {
126
+ toggleNotification({
127
+ type: 'danger',
128
+ message: err.response?.data?.error?.message || 'Failed to assign roles',
129
+ });
130
+ throw err;
131
+ }
132
+ },
133
+ [realmId, client, toggleNotification]
134
+ );
135
+
136
+ /**
137
+ * Removes roles from a user.
138
+ *
139
+ * @param {string} userId - Keycloak user ID
140
+ * @param {KeycloakRole[]} rolesToRemove - Roles to remove (must include id and name)
141
+ * @returns {Promise<void>}
142
+ * @throws {Error} If removal fails
143
+ */
144
+ const removeRoles = useCallback(
145
+ async (userId, rolesToRemove) => {
146
+ try {
147
+ await client.del(`${API_BASE_PATH}/realms/${realmId}/users/${userId}/roles`, {
148
+ data: { roles: rolesToRemove },
149
+ });
150
+ toggleNotification({
151
+ type: 'success',
152
+ message: 'Roles removed successfully',
153
+ });
154
+ } catch (err) {
155
+ toggleNotification({
156
+ type: 'danger',
157
+ message: err.response?.data?.error?.message || 'Failed to remove roles',
158
+ });
159
+ throw err;
160
+ }
161
+ },
162
+ [realmId, client, toggleNotification]
163
+ );
164
+
165
+ // Fetch roles when realmId changes
166
+ useEffect(() => {
167
+ if (realmId) {
168
+ fetchRoles();
169
+ }
170
+ }, [realmId, fetchRoles]);
171
+
172
+ return {
173
+ roles,
174
+ isLoading,
175
+ fetchRoles,
176
+ getUserRoles,
177
+ assignRoles,
178
+ removeRoles,
179
+ };
180
+ };
181
+
182
+ export default useKeycloakRoles;