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.
- package/LICENSE +21 -0
- package/README.md +485 -0
- package/__tests__/constants.test.mjs +207 -0
- package/__tests__/mocks/strapi.mjs +182 -0
- package/__tests__/services/audit-log.test.mjs +283 -0
- package/__tests__/services/keycloak-client.test.mjs +651 -0
- package/__tests__/services/permission.test.mjs +374 -0
- package/__tests__/services/realm.test.mjs +415 -0
- package/__tests__/services/user.test.mjs +487 -0
- package/__tests__/utils/errors.test.mjs +109 -0
- package/admin/src/components/Initializer.jsx +14 -0
- package/admin/src/components/RealmBadge.jsx +17 -0
- package/admin/src/constants.js +14 -0
- package/admin/src/hooks/useAuditLogs.js +142 -0
- package/admin/src/hooks/useKeycloakRoles.js +182 -0
- package/admin/src/hooks/useKeycloakUsers.js +477 -0
- package/admin/src/hooks/useRealmAdmins.js +249 -0
- package/admin/src/hooks/useRealms.js +269 -0
- package/admin/src/index.js +46 -0
- package/admin/src/pages/App.jsx +21 -0
- package/admin/src/pages/AuditPage/index.jsx +213 -0
- package/admin/src/pages/RealmsPage/RealmEditPage.jsx +791 -0
- package/admin/src/pages/RealmsPage/RealmListPage.jsx +231 -0
- package/admin/src/pages/RealmsPage/index.jsx +7 -0
- package/admin/src/pages/UsersPage/UserEditPage.jsx +313 -0
- package/admin/src/pages/UsersPage/UserListPage.jsx +437 -0
- package/admin/src/pages/UsersPage/index.jsx +7 -0
- package/admin/src/pluginId.js +2 -0
- package/admin/src/translations/en.json +77 -0
- package/admin/src/translations/fr.json +77 -0
- package/babel.config.cjs +17 -0
- package/coverage/clover.xml +422 -0
- package/coverage/coverage-final.json +8 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/bootstrap.js.html +346 -0
- package/coverage/lcov-report/src/config/index.html +116 -0
- package/coverage/lcov-report/src/config/index.js.html +106 -0
- package/coverage/lcov-report/src/constants.js.html +850 -0
- package/coverage/lcov-report/src/content-types/audit-log/index.html +116 -0
- package/coverage/lcov-report/src/content-types/audit-log/index.js.html +94 -0
- package/coverage/lcov-report/src/content-types/index.html +116 -0
- package/coverage/lcov-report/src/content-types/index.js.html +112 -0
- package/coverage/lcov-report/src/content-types/realm-admin/index.html +116 -0
- package/coverage/lcov-report/src/content-types/realm-admin/index.js.html +94 -0
- package/coverage/lcov-report/src/content-types/realm-config/index.html +116 -0
- package/coverage/lcov-report/src/content-types/realm-config/index.js.html +94 -0
- package/coverage/lcov-report/src/controllers/audit.js.html +517 -0
- package/coverage/lcov-report/src/controllers/index.html +161 -0
- package/coverage/lcov-report/src/controllers/index.js.html +112 -0
- package/coverage/lcov-report/src/controllers/realm.js.html +1057 -0
- package/coverage/lcov-report/src/controllers/user.js.html +1324 -0
- package/coverage/lcov-report/src/destroy.js.html +100 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/policies/can-access-realm.js.html +163 -0
- package/coverage/lcov-report/src/policies/index.html +146 -0
- package/coverage/lcov-report/src/policies/index.js.html +106 -0
- package/coverage/lcov-report/src/policies/is-authenticated.js.html +100 -0
- package/coverage/lcov-report/src/register.js.html +106 -0
- package/coverage/lcov-report/src/routes/admin.js.html +844 -0
- package/coverage/lcov-report/src/routes/index.html +131 -0
- package/coverage/lcov-report/src/routes/index.js.html +109 -0
- package/coverage/lcov-report/src/services/audit-log.js.html +673 -0
- package/coverage/lcov-report/src/services/index.html +176 -0
- package/coverage/lcov-report/src/services/index.js.html +124 -0
- package/coverage/lcov-report/src/services/keycloak-client.js.html +2359 -0
- package/coverage/lcov-report/src/services/permission.js.html +955 -0
- package/coverage/lcov-report/src/services/realm.js.html +1207 -0
- package/coverage/lcov-report/src/services/user.js.html +1924 -0
- package/coverage/lcov-report/src/utils/errors.js.html +274 -0
- package/coverage/lcov-report/src/utils/index.html +116 -0
- package/coverage/lcov-report/src/utils/index.js.html +103 -0
- package/coverage/lcov.info +804 -0
- package/dist/_chunks/App-BaKrvCeS.mjs +1975 -0
- package/dist/_chunks/App-DO6syS77.js +1975 -0
- package/dist/_chunks/en-Li-XBDe9.mjs +72 -0
- package/dist/_chunks/en-aCyfgNfr.js +72 -0
- package/dist/_chunks/fr-Cj33Q8jI.js +72 -0
- package/dist/_chunks/fr-vLrXph-Z.mjs +72 -0
- package/dist/_chunks/index-DwDO4-0C.js +69 -0
- package/dist/_chunks/index-jTVd7LdQ.mjs +70 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/server/index.js +3003 -0
- package/dist/server/index.mjs +3004 -0
- package/jest.config.cjs +50 -0
- package/package.json +55 -0
- package/server/src/bootstrap.js +87 -0
- package/server/src/config/index.js +7 -0
- package/server/src/constants.js +255 -0
- package/server/src/content-types/audit-log/index.js +3 -0
- package/server/src/content-types/audit-log/schema.json +61 -0
- package/server/src/content-types/index.js +9 -0
- package/server/src/content-types/realm-admin/index.js +3 -0
- package/server/src/content-types/realm-admin/schema.json +45 -0
- package/server/src/content-types/realm-config/index.js +3 -0
- package/server/src/content-types/realm-config/schema.json +56 -0
- package/server/src/controllers/audit.js +144 -0
- package/server/src/controllers/index.js +9 -0
- package/server/src/controllers/realm.js +324 -0
- package/server/src/controllers/user.js +413 -0
- package/server/src/destroy.js +5 -0
- package/server/src/index.js +21 -0
- package/server/src/policies/can-access-realm.js +26 -0
- package/server/src/policies/index.js +7 -0
- package/server/src/policies/is-authenticated.js +5 -0
- package/server/src/register.js +7 -0
- package/server/src/routes/admin.js +253 -0
- package/server/src/routes/index.js +8 -0
- package/server/src/services/audit-log.js +196 -0
- package/server/src/services/index.js +13 -0
- package/server/src/services/keycloak-client.js +758 -0
- package/server/src/services/permission.js +290 -0
- package/server/src/services/realm.js +374 -0
- package/server/src/services/user.js +613 -0
- package/server/src/utils/errors.js +63 -0
- package/server/src/utils/index.js +6 -0
|
@@ -0,0 +1,3003 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const PLUGIN_ID = "strapi-plugin-keycloak-realm-users";
|
|
3
|
+
const CONTENT_TYPES = {
|
|
4
|
+
REALM_CONFIG: `plugin::${PLUGIN_ID}.realm-config`,
|
|
5
|
+
REALM_ADMIN: `plugin::${PLUGIN_ID}.realm-admin`,
|
|
6
|
+
AUDIT_LOG: `plugin::${PLUGIN_ID}.audit-log`
|
|
7
|
+
};
|
|
8
|
+
const SERVICES = {
|
|
9
|
+
REALM: "realm",
|
|
10
|
+
KEYCLOAK_CLIENT: "keycloak-client",
|
|
11
|
+
USER: "user",
|
|
12
|
+
PERMISSION: "permission",
|
|
13
|
+
AUDIT_LOG: "audit-log"
|
|
14
|
+
};
|
|
15
|
+
const AUDIT_ACTIONS = {
|
|
16
|
+
/** User was created in Keycloak */
|
|
17
|
+
CREATE_USER: "CREATE_USER",
|
|
18
|
+
/** User details were modified */
|
|
19
|
+
UPDATE_USER: "UPDATE_USER",
|
|
20
|
+
/** User was permanently deleted */
|
|
21
|
+
DELETE_USER: "DELETE_USER",
|
|
22
|
+
/** User password was reset */
|
|
23
|
+
RESET_PASSWORD: "RESET_PASSWORD",
|
|
24
|
+
/** Realm role was assigned to user */
|
|
25
|
+
ASSIGN_ROLE: "ASSIGN_ROLE",
|
|
26
|
+
/** Realm role was removed from user */
|
|
27
|
+
REMOVE_ROLE: "REMOVE_ROLE",
|
|
28
|
+
/** User account was enabled */
|
|
29
|
+
ENABLE_USER: "ENABLE_USER",
|
|
30
|
+
/** User account was disabled */
|
|
31
|
+
DISABLE_USER: "DISABLE_USER",
|
|
32
|
+
/** Email verification was sent */
|
|
33
|
+
SEND_VERIFY_EMAIL: "SEND_VERIFY_EMAIL",
|
|
34
|
+
/** Password reset email was sent */
|
|
35
|
+
SEND_RESET_PASSWORD_EMAIL: "SEND_RESET_PASSWORD_EMAIL",
|
|
36
|
+
/** Bulk user import was performed */
|
|
37
|
+
BULK_IMPORT: "BULK_IMPORT"
|
|
38
|
+
};
|
|
39
|
+
const ERROR_MESSAGES = {
|
|
40
|
+
REALM_NOT_FOUND: "Realm configuration not found.",
|
|
41
|
+
REALM_DISABLED: "This realm is currently disabled.",
|
|
42
|
+
USER_NOT_FOUND: "Keycloak user not found.",
|
|
43
|
+
INSUFFICIENT_PERMISSIONS: "You do not have permission to perform this action.",
|
|
44
|
+
REALM_ACCESS_DENIED: "You do not have access to this realm.",
|
|
45
|
+
KEYCLOAK_CONNECTION_FAILED: "Failed to connect to Keycloak server.",
|
|
46
|
+
KEYCLOAK_AUTH_FAILED: "Failed to authenticate with Keycloak.",
|
|
47
|
+
INVALID_USER_DATA: "Invalid user data provided.",
|
|
48
|
+
MISSING_REQUIRED_FIELDS: "Missing required fields.",
|
|
49
|
+
UNKNOWN_ERROR: "An unexpected error occurred.",
|
|
50
|
+
USER_ALREADY_EXISTS: "A user with this username or email already exists.",
|
|
51
|
+
PASSWORD_RESET_FAILED: "Failed to reset password.",
|
|
52
|
+
EMAIL_SEND_FAILED: "Failed to send email.",
|
|
53
|
+
ROLE_ASSIGNMENT_FAILED: "Failed to assign roles.",
|
|
54
|
+
ROLE_REMOVAL_FAILED: "Failed to remove roles.",
|
|
55
|
+
LOGOUT_FAILED: "Failed to logout user.",
|
|
56
|
+
INVALID_NAME_FORMAT: "Name must contain only lowercase letters, numbers, and hyphens.",
|
|
57
|
+
REALM_NAME_EXISTS: "A realm with this name already exists."
|
|
58
|
+
};
|
|
59
|
+
const DEFAULT_REALM_PERMISSIONS = {
|
|
60
|
+
canRead: true,
|
|
61
|
+
canCreate: false,
|
|
62
|
+
canUpdate: false,
|
|
63
|
+
canDelete: false,
|
|
64
|
+
canManageRoles: false,
|
|
65
|
+
canResetPassword: false
|
|
66
|
+
};
|
|
67
|
+
const STRAPI_SUPER_ADMIN_ROLE = "strapi-super-admin";
|
|
68
|
+
const TOKEN_EXPIRY_BUFFER_MS = 6e4;
|
|
69
|
+
const EMAIL_ACTION_LIFESPAN_SECONDS = 43200;
|
|
70
|
+
const DEFAULT_REALM_COLOR = "#4945ff";
|
|
71
|
+
const PAGINATION = {
|
|
72
|
+
PAGE_SIZE: 25,
|
|
73
|
+
AUDIT_LOG_LIMIT: 50,
|
|
74
|
+
EXPORT_BATCH_SIZE: 100
|
|
75
|
+
};
|
|
76
|
+
const HTTP_STATUS = {
|
|
77
|
+
NO_CONTENT: 204,
|
|
78
|
+
NOT_FOUND: 404,
|
|
79
|
+
CONFLICT: 409
|
|
80
|
+
};
|
|
81
|
+
const HTTP_HEADERS = {
|
|
82
|
+
CONTENT_TYPE_JSON: "application/json",
|
|
83
|
+
CONTENT_TYPE_FORM: "application/x-www-form-urlencoded"
|
|
84
|
+
};
|
|
85
|
+
const KEYCLOAK_EMAIL_ACTIONS = {
|
|
86
|
+
VERIFY_EMAIL: "VERIFY_EMAIL",
|
|
87
|
+
UPDATE_PASSWORD: "UPDATE_PASSWORD"
|
|
88
|
+
};
|
|
89
|
+
const REALM_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
90
|
+
const UNKNOWN_USER_EMAIL = "unknown";
|
|
91
|
+
const bootstrap = async ({ strapi }) => {
|
|
92
|
+
try {
|
|
93
|
+
await strapi.admin.services.permission.actionProvider.registerMany([
|
|
94
|
+
{
|
|
95
|
+
uid: `plugin::${PLUGIN_ID}.realm.read`,
|
|
96
|
+
displayName: "Read Realms",
|
|
97
|
+
pluginName: PLUGIN_ID,
|
|
98
|
+
section: "settings",
|
|
99
|
+
category: "Keycloak User Management",
|
|
100
|
+
subCategory: "Realms"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
uid: `plugin::${PLUGIN_ID}.realm.create`,
|
|
104
|
+
displayName: "Create Realms",
|
|
105
|
+
pluginName: PLUGIN_ID,
|
|
106
|
+
section: "settings",
|
|
107
|
+
category: "Keycloak User Management",
|
|
108
|
+
subCategory: "Realms"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
uid: `plugin::${PLUGIN_ID}.realm.update`,
|
|
112
|
+
displayName: "Update Realms",
|
|
113
|
+
pluginName: PLUGIN_ID,
|
|
114
|
+
section: "settings",
|
|
115
|
+
category: "Keycloak User Management",
|
|
116
|
+
subCategory: "Realms"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
uid: `plugin::${PLUGIN_ID}.realm.delete`,
|
|
120
|
+
displayName: "Delete Realms",
|
|
121
|
+
pluginName: PLUGIN_ID,
|
|
122
|
+
section: "settings",
|
|
123
|
+
category: "Keycloak User Management",
|
|
124
|
+
subCategory: "Realms"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
uid: `plugin::${PLUGIN_ID}.user.read`,
|
|
128
|
+
displayName: "Read Keycloak Users",
|
|
129
|
+
pluginName: PLUGIN_ID,
|
|
130
|
+
section: "settings",
|
|
131
|
+
category: "Keycloak User Management",
|
|
132
|
+
subCategory: "Users"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
uid: `plugin::${PLUGIN_ID}.user.create`,
|
|
136
|
+
displayName: "Create Keycloak Users",
|
|
137
|
+
pluginName: PLUGIN_ID,
|
|
138
|
+
section: "settings",
|
|
139
|
+
category: "Keycloak User Management",
|
|
140
|
+
subCategory: "Users"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
uid: `plugin::${PLUGIN_ID}.user.update`,
|
|
144
|
+
displayName: "Update Keycloak Users",
|
|
145
|
+
pluginName: PLUGIN_ID,
|
|
146
|
+
section: "settings",
|
|
147
|
+
category: "Keycloak User Management",
|
|
148
|
+
subCategory: "Users"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
uid: `plugin::${PLUGIN_ID}.user.delete`,
|
|
152
|
+
displayName: "Delete Keycloak Users",
|
|
153
|
+
pluginName: PLUGIN_ID,
|
|
154
|
+
section: "settings",
|
|
155
|
+
category: "Keycloak User Management",
|
|
156
|
+
subCategory: "Users"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
uid: `plugin::${PLUGIN_ID}.audit.read`,
|
|
160
|
+
displayName: "Read Audit Logs",
|
|
161
|
+
pluginName: PLUGIN_ID,
|
|
162
|
+
section: "settings",
|
|
163
|
+
category: "Keycloak User Management",
|
|
164
|
+
subCategory: "Audit"
|
|
165
|
+
}
|
|
166
|
+
]);
|
|
167
|
+
strapi.log.info(`[${PLUGIN_ID}] Plugin bootstrapped with permissions registered`);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
strapi.log.error(`[${PLUGIN_ID}] Failed to register permissions:`, err);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const register = ({ strapi }) => {
|
|
173
|
+
strapi.log.info(`[${PLUGIN_ID}] Plugin registered successfully`);
|
|
174
|
+
};
|
|
175
|
+
const destroy = ({ strapi }) => {
|
|
176
|
+
};
|
|
177
|
+
const config = {
|
|
178
|
+
default: {},
|
|
179
|
+
validator(config2) {
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const kind$2 = "collectionType";
|
|
183
|
+
const collectionName$2 = "kru_realm_configs";
|
|
184
|
+
const info$2 = {
|
|
185
|
+
singularName: "realm-config",
|
|
186
|
+
pluralName: "realm-configs",
|
|
187
|
+
displayName: "Keycloak Realm"
|
|
188
|
+
};
|
|
189
|
+
const options$2 = {
|
|
190
|
+
draftAndPublish: false
|
|
191
|
+
};
|
|
192
|
+
const pluginOptions$2 = {
|
|
193
|
+
"content-manager": {
|
|
194
|
+
visible: false
|
|
195
|
+
},
|
|
196
|
+
"content-type-builder": {
|
|
197
|
+
visible: false
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const attributes$2 = {
|
|
201
|
+
name: {
|
|
202
|
+
type: "string",
|
|
203
|
+
required: true,
|
|
204
|
+
unique: true,
|
|
205
|
+
regex: "^[a-z0-9-]+$"
|
|
206
|
+
},
|
|
207
|
+
displayName: {
|
|
208
|
+
type: "string",
|
|
209
|
+
required: true
|
|
210
|
+
},
|
|
211
|
+
serverUrl: {
|
|
212
|
+
type: "string",
|
|
213
|
+
required: true
|
|
214
|
+
},
|
|
215
|
+
realmName: {
|
|
216
|
+
type: "string",
|
|
217
|
+
required: true
|
|
218
|
+
},
|
|
219
|
+
clientId: {
|
|
220
|
+
type: "string",
|
|
221
|
+
required: true
|
|
222
|
+
},
|
|
223
|
+
clientSecret: {
|
|
224
|
+
type: "string",
|
|
225
|
+
"private": true
|
|
226
|
+
},
|
|
227
|
+
enabled: {
|
|
228
|
+
type: "boolean",
|
|
229
|
+
"default": true
|
|
230
|
+
},
|
|
231
|
+
color: {
|
|
232
|
+
type: "string",
|
|
233
|
+
"default": "#4945ff"
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const schema$2 = {
|
|
237
|
+
kind: kind$2,
|
|
238
|
+
collectionName: collectionName$2,
|
|
239
|
+
info: info$2,
|
|
240
|
+
options: options$2,
|
|
241
|
+
pluginOptions: pluginOptions$2,
|
|
242
|
+
attributes: attributes$2
|
|
243
|
+
};
|
|
244
|
+
const realmConfig = { schema: schema$2 };
|
|
245
|
+
const kind$1 = "collectionType";
|
|
246
|
+
const collectionName$1 = "kru_realm_admins";
|
|
247
|
+
const info$1 = {
|
|
248
|
+
singularName: "realm-admin",
|
|
249
|
+
pluralName: "realm-admins",
|
|
250
|
+
displayName: "Realm Admin Assignment"
|
|
251
|
+
};
|
|
252
|
+
const options$1 = {
|
|
253
|
+
draftAndPublish: false
|
|
254
|
+
};
|
|
255
|
+
const pluginOptions$1 = {
|
|
256
|
+
"content-manager": {
|
|
257
|
+
visible: false
|
|
258
|
+
},
|
|
259
|
+
"content-type-builder": {
|
|
260
|
+
visible: false
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const attributes$1 = {
|
|
264
|
+
strapiUserId: {
|
|
265
|
+
type: "integer",
|
|
266
|
+
required: true
|
|
267
|
+
},
|
|
268
|
+
strapiUserEmail: {
|
|
269
|
+
type: "string"
|
|
270
|
+
},
|
|
271
|
+
realmConfig: {
|
|
272
|
+
type: "relation",
|
|
273
|
+
relation: "manyToOne",
|
|
274
|
+
target: "plugin::strapi-plugin-keycloak-realm-users.realm-config"
|
|
275
|
+
},
|
|
276
|
+
permissions: {
|
|
277
|
+
type: "json",
|
|
278
|
+
"default": {
|
|
279
|
+
canRead: true,
|
|
280
|
+
canCreate: false,
|
|
281
|
+
canUpdate: false,
|
|
282
|
+
canDelete: false,
|
|
283
|
+
canManageRoles: false,
|
|
284
|
+
canResetPassword: false
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const schema$1 = {
|
|
289
|
+
kind: kind$1,
|
|
290
|
+
collectionName: collectionName$1,
|
|
291
|
+
info: info$1,
|
|
292
|
+
options: options$1,
|
|
293
|
+
pluginOptions: pluginOptions$1,
|
|
294
|
+
attributes: attributes$1
|
|
295
|
+
};
|
|
296
|
+
const realmAdmin = { schema: schema$1 };
|
|
297
|
+
const kind = "collectionType";
|
|
298
|
+
const collectionName = "kru_audit_logs";
|
|
299
|
+
const info = {
|
|
300
|
+
singularName: "audit-log",
|
|
301
|
+
pluralName: "audit-logs",
|
|
302
|
+
displayName: "Keycloak Audit Log"
|
|
303
|
+
};
|
|
304
|
+
const options = {
|
|
305
|
+
draftAndPublish: false
|
|
306
|
+
};
|
|
307
|
+
const pluginOptions = {
|
|
308
|
+
"content-manager": {
|
|
309
|
+
visible: true
|
|
310
|
+
},
|
|
311
|
+
"content-type-builder": {
|
|
312
|
+
visible: false
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const attributes = {
|
|
316
|
+
realmName: {
|
|
317
|
+
type: "string",
|
|
318
|
+
required: true
|
|
319
|
+
},
|
|
320
|
+
realmDisplayName: {
|
|
321
|
+
type: "string"
|
|
322
|
+
},
|
|
323
|
+
action: {
|
|
324
|
+
type: "enumeration",
|
|
325
|
+
"enum": [
|
|
326
|
+
"CREATE_USER",
|
|
327
|
+
"UPDATE_USER",
|
|
328
|
+
"DELETE_USER",
|
|
329
|
+
"RESET_PASSWORD",
|
|
330
|
+
"ASSIGN_ROLE",
|
|
331
|
+
"REMOVE_ROLE",
|
|
332
|
+
"ENABLE_USER",
|
|
333
|
+
"DISABLE_USER",
|
|
334
|
+
"SEND_VERIFY_EMAIL",
|
|
335
|
+
"SEND_RESET_PASSWORD_EMAIL",
|
|
336
|
+
"BULK_IMPORT"
|
|
337
|
+
],
|
|
338
|
+
required: true
|
|
339
|
+
},
|
|
340
|
+
keycloakUserId: {
|
|
341
|
+
type: "string"
|
|
342
|
+
},
|
|
343
|
+
keycloakUsername: {
|
|
344
|
+
type: "string"
|
|
345
|
+
},
|
|
346
|
+
details: {
|
|
347
|
+
type: "json"
|
|
348
|
+
},
|
|
349
|
+
performedById: {
|
|
350
|
+
type: "integer"
|
|
351
|
+
},
|
|
352
|
+
performedByEmail: {
|
|
353
|
+
type: "string"
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const schema = {
|
|
357
|
+
kind,
|
|
358
|
+
collectionName,
|
|
359
|
+
info,
|
|
360
|
+
options,
|
|
361
|
+
pluginOptions,
|
|
362
|
+
attributes
|
|
363
|
+
};
|
|
364
|
+
const auditLog = { schema };
|
|
365
|
+
const contentTypes = {
|
|
366
|
+
"realm-config": realmConfig,
|
|
367
|
+
"realm-admin": realmAdmin,
|
|
368
|
+
"audit-log": auditLog
|
|
369
|
+
};
|
|
370
|
+
const realmController = ({ strapi }) => ({
|
|
371
|
+
/**
|
|
372
|
+
* Get all realms (filtered by user access)
|
|
373
|
+
*/
|
|
374
|
+
async find(ctx) {
|
|
375
|
+
const user = ctx.state.user;
|
|
376
|
+
if (!user) {
|
|
377
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
381
|
+
const realms = await permissionService2.getAccessibleRealms(user);
|
|
382
|
+
ctx.body = { data: realms };
|
|
383
|
+
} catch (err) {
|
|
384
|
+
strapi.log.error(`[${PLUGIN_ID}] realm.find error:`, err);
|
|
385
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
/**
|
|
389
|
+
* Get a single realm by ID
|
|
390
|
+
*/
|
|
391
|
+
async findOne(ctx) {
|
|
392
|
+
const user = ctx.state.user;
|
|
393
|
+
const { id } = ctx.params;
|
|
394
|
+
if (!user) {
|
|
395
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
399
|
+
const realmService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
|
|
400
|
+
const canAccess = await permissionService2.canAccessRealm(user, id, "canRead");
|
|
401
|
+
if (!canAccess) {
|
|
402
|
+
return ctx.forbidden(ERROR_MESSAGES.REALM_ACCESS_DENIED);
|
|
403
|
+
}
|
|
404
|
+
const realm = await realmService2.findOne(id);
|
|
405
|
+
const permissions = await permissionService2.getRealmPermissions(user, id);
|
|
406
|
+
ctx.body = { data: { ...realm, permissions } };
|
|
407
|
+
} catch (err) {
|
|
408
|
+
strapi.log.error(`[${PLUGIN_ID}] realm.findOne error:`, err);
|
|
409
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
/**
|
|
413
|
+
* Create a new realm (super admin only)
|
|
414
|
+
*/
|
|
415
|
+
async create(ctx) {
|
|
416
|
+
const user = ctx.state.user;
|
|
417
|
+
const { data } = ctx.request.body;
|
|
418
|
+
if (!user) {
|
|
419
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
423
|
+
if (!permissionService2.isSuperAdmin(user)) {
|
|
424
|
+
return ctx.forbidden("Only super admins can create realm configurations.");
|
|
425
|
+
}
|
|
426
|
+
const realmService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
|
|
427
|
+
const realm = await realmService2.create(data);
|
|
428
|
+
ctx.body = { data: realm };
|
|
429
|
+
} catch (err) {
|
|
430
|
+
strapi.log.error(`[${PLUGIN_ID}] realm.create error:`, err);
|
|
431
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
/**
|
|
435
|
+
* Update a realm (super admin only)
|
|
436
|
+
*/
|
|
437
|
+
async update(ctx) {
|
|
438
|
+
const user = ctx.state.user;
|
|
439
|
+
const { id } = ctx.params;
|
|
440
|
+
const { data } = ctx.request.body;
|
|
441
|
+
if (!user) {
|
|
442
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
446
|
+
if (!permissionService2.isSuperAdmin(user)) {
|
|
447
|
+
return ctx.forbidden("Only super admins can update realm configurations.");
|
|
448
|
+
}
|
|
449
|
+
const realmService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
|
|
450
|
+
const realm = await realmService2.update(id, data);
|
|
451
|
+
ctx.body = { data: realm };
|
|
452
|
+
} catch (err) {
|
|
453
|
+
strapi.log.error(`[${PLUGIN_ID}] realm.update error:`, err);
|
|
454
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
/**
|
|
458
|
+
* Delete a realm (super admin only)
|
|
459
|
+
*/
|
|
460
|
+
async delete(ctx) {
|
|
461
|
+
const user = ctx.state.user;
|
|
462
|
+
const { id } = ctx.params;
|
|
463
|
+
if (!user) {
|
|
464
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
468
|
+
if (!permissionService2.isSuperAdmin(user)) {
|
|
469
|
+
return ctx.forbidden("Only super admins can delete realm configurations.");
|
|
470
|
+
}
|
|
471
|
+
const realmService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
|
|
472
|
+
await realmService2.delete(id);
|
|
473
|
+
ctx.body = { data: { success: true } };
|
|
474
|
+
} catch (err) {
|
|
475
|
+
strapi.log.error(`[${PLUGIN_ID}] realm.delete error:`, err);
|
|
476
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
/**
|
|
480
|
+
* Test realm connection
|
|
481
|
+
*/
|
|
482
|
+
async testConnection(ctx) {
|
|
483
|
+
const user = ctx.state.user;
|
|
484
|
+
const { id } = ctx.params;
|
|
485
|
+
if (!user) {
|
|
486
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
490
|
+
const realmService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
|
|
491
|
+
const canAccess = await permissionService2.canAccessRealm(user, id, "canRead");
|
|
492
|
+
if (!canAccess) {
|
|
493
|
+
return ctx.forbidden(ERROR_MESSAGES.REALM_ACCESS_DENIED);
|
|
494
|
+
}
|
|
495
|
+
const result = await realmService2.testConnection(id);
|
|
496
|
+
ctx.body = { data: result };
|
|
497
|
+
} catch (err) {
|
|
498
|
+
strapi.log.error(`[${PLUGIN_ID}] realm.testConnection error:`, err);
|
|
499
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
/**
|
|
503
|
+
* Test connection before saving (with raw config)
|
|
504
|
+
*/
|
|
505
|
+
async testConnectionRaw(ctx) {
|
|
506
|
+
const user = ctx.state.user;
|
|
507
|
+
const { data } = ctx.request.body;
|
|
508
|
+
if (!user) {
|
|
509
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
513
|
+
if (!permissionService2.isSuperAdmin(user)) {
|
|
514
|
+
return ctx.forbidden("Only super admins can test realm configurations.");
|
|
515
|
+
}
|
|
516
|
+
const realmService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
|
|
517
|
+
const result = await realmService2.testConnectionRaw(data);
|
|
518
|
+
ctx.body = { data: result };
|
|
519
|
+
} catch (err) {
|
|
520
|
+
strapi.log.error(`[${PLUGIN_ID}] realm.testConnectionRaw error:`, err);
|
|
521
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
/**
|
|
525
|
+
* Get admins assigned to a realm
|
|
526
|
+
*/
|
|
527
|
+
async getAdmins(ctx) {
|
|
528
|
+
const user = ctx.state.user;
|
|
529
|
+
const { id } = ctx.params;
|
|
530
|
+
if (!user) {
|
|
531
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
535
|
+
if (!permissionService2.isSuperAdmin(user)) {
|
|
536
|
+
return ctx.forbidden("Only super admins can view realm admin assignments.");
|
|
537
|
+
}
|
|
538
|
+
const admins = await permissionService2.getRealmAdmins(id);
|
|
539
|
+
ctx.body = { data: admins };
|
|
540
|
+
} catch (err) {
|
|
541
|
+
strapi.log.error(`[${PLUGIN_ID}] realm.getAdmins error:`, err);
|
|
542
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
/**
|
|
546
|
+
* Assign an admin to a realm
|
|
547
|
+
*/
|
|
548
|
+
async addAdmin(ctx) {
|
|
549
|
+
const user = ctx.state.user;
|
|
550
|
+
const { id } = ctx.params;
|
|
551
|
+
const { data } = ctx.request.body;
|
|
552
|
+
if (!user) {
|
|
553
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
557
|
+
if (!permissionService2.isSuperAdmin(user)) {
|
|
558
|
+
return ctx.forbidden("Only super admins can assign realm admins.");
|
|
559
|
+
}
|
|
560
|
+
const { strapiUserId, strapiUserEmail, permissions } = data;
|
|
561
|
+
if (!strapiUserId) {
|
|
562
|
+
return ctx.badRequest("strapiUserId is required.");
|
|
563
|
+
}
|
|
564
|
+
const assignment = await permissionService2.assignUserToRealm(
|
|
565
|
+
strapiUserId,
|
|
566
|
+
strapiUserEmail,
|
|
567
|
+
id,
|
|
568
|
+
permissions
|
|
569
|
+
);
|
|
570
|
+
ctx.body = { data: assignment };
|
|
571
|
+
} catch (err) {
|
|
572
|
+
strapi.log.error(`[${PLUGIN_ID}] realm.addAdmin error:`, err);
|
|
573
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
/**
|
|
577
|
+
* Update admin permissions for a realm
|
|
578
|
+
*/
|
|
579
|
+
async updateAdmin(ctx) {
|
|
580
|
+
const user = ctx.state.user;
|
|
581
|
+
const { id, adminId } = ctx.params;
|
|
582
|
+
const { data } = ctx.request.body;
|
|
583
|
+
if (!user) {
|
|
584
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
588
|
+
if (!permissionService2.isSuperAdmin(user)) {
|
|
589
|
+
return ctx.forbidden("Only super admins can update realm admin assignments.");
|
|
590
|
+
}
|
|
591
|
+
const { permissions } = data;
|
|
592
|
+
const assignment = await permissionService2.assignUserToRealm(
|
|
593
|
+
parseInt(adminId, 10),
|
|
594
|
+
null,
|
|
595
|
+
id,
|
|
596
|
+
permissions
|
|
597
|
+
);
|
|
598
|
+
ctx.body = { data: assignment };
|
|
599
|
+
} catch (err) {
|
|
600
|
+
strapi.log.error(`[${PLUGIN_ID}] realm.updateAdmin error:`, err);
|
|
601
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
/**
|
|
605
|
+
* Remove an admin from a realm
|
|
606
|
+
*/
|
|
607
|
+
async removeAdmin(ctx) {
|
|
608
|
+
const user = ctx.state.user;
|
|
609
|
+
const { id, adminId } = ctx.params;
|
|
610
|
+
if (!user) {
|
|
611
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
615
|
+
if (!permissionService2.isSuperAdmin(user)) {
|
|
616
|
+
return ctx.forbidden("Only super admins can remove realm admins.");
|
|
617
|
+
}
|
|
618
|
+
await permissionService2.removeUserFromRealm(parseInt(adminId, 10), id);
|
|
619
|
+
ctx.body = { data: { success: true } };
|
|
620
|
+
} catch (err) {
|
|
621
|
+
strapi.log.error(`[${PLUGIN_ID}] realm.removeAdmin error:`, err);
|
|
622
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
const userController = ({ strapi }) => ({
|
|
627
|
+
/**
|
|
628
|
+
* List users in a realm
|
|
629
|
+
*/
|
|
630
|
+
async find(ctx) {
|
|
631
|
+
const user = ctx.state.user;
|
|
632
|
+
const { realmId } = ctx.params;
|
|
633
|
+
const { search, page = 1, pageSize = 25 } = ctx.query;
|
|
634
|
+
if (!user) {
|
|
635
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
639
|
+
const result = await userService2.listUsers(
|
|
640
|
+
realmId,
|
|
641
|
+
{ search, page: parseInt(page, 10), pageSize: parseInt(pageSize, 10) },
|
|
642
|
+
user
|
|
643
|
+
);
|
|
644
|
+
ctx.body = { data: result };
|
|
645
|
+
} catch (err) {
|
|
646
|
+
strapi.log.error(`[${PLUGIN_ID}] user.find error:`, err);
|
|
647
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
/**
|
|
651
|
+
* Get a single user
|
|
652
|
+
*/
|
|
653
|
+
async findOne(ctx) {
|
|
654
|
+
const user = ctx.state.user;
|
|
655
|
+
const { realmId, id } = ctx.params;
|
|
656
|
+
if (!user) {
|
|
657
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
661
|
+
const keycloakUser = await userService2.getUser(realmId, id, user);
|
|
662
|
+
ctx.body = { data: keycloakUser };
|
|
663
|
+
} catch (err) {
|
|
664
|
+
strapi.log.error(`[${PLUGIN_ID}] user.findOne error:`, err);
|
|
665
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
/**
|
|
669
|
+
* Create a new user
|
|
670
|
+
*/
|
|
671
|
+
async create(ctx) {
|
|
672
|
+
const user = ctx.state.user;
|
|
673
|
+
const { realmId } = ctx.params;
|
|
674
|
+
const { data } = ctx.request.body;
|
|
675
|
+
if (!user) {
|
|
676
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
680
|
+
const keycloakUser = await userService2.createUser(realmId, data, user);
|
|
681
|
+
ctx.body = { data: keycloakUser };
|
|
682
|
+
} catch (err) {
|
|
683
|
+
strapi.log.error(`[${PLUGIN_ID}] user.create error:`, err);
|
|
684
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
/**
|
|
688
|
+
* Update a user
|
|
689
|
+
*/
|
|
690
|
+
async update(ctx) {
|
|
691
|
+
const user = ctx.state.user;
|
|
692
|
+
const { realmId, id } = ctx.params;
|
|
693
|
+
const { data } = ctx.request.body;
|
|
694
|
+
if (!user) {
|
|
695
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
699
|
+
const keycloakUser = await userService2.updateUser(realmId, id, data, user);
|
|
700
|
+
ctx.body = { data: keycloakUser };
|
|
701
|
+
} catch (err) {
|
|
702
|
+
strapi.log.error(`[${PLUGIN_ID}] user.update error:`, err);
|
|
703
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
/**
|
|
707
|
+
* Delete a user
|
|
708
|
+
*/
|
|
709
|
+
async delete(ctx) {
|
|
710
|
+
const user = ctx.state.user;
|
|
711
|
+
const { realmId, id } = ctx.params;
|
|
712
|
+
if (!user) {
|
|
713
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
717
|
+
await userService2.deleteUser(realmId, id, user);
|
|
718
|
+
ctx.body = { data: { success: true } };
|
|
719
|
+
} catch (err) {
|
|
720
|
+
strapi.log.error(`[${PLUGIN_ID}] user.delete error:`, err);
|
|
721
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
/**
|
|
725
|
+
* Reset user password
|
|
726
|
+
*/
|
|
727
|
+
async resetPassword(ctx) {
|
|
728
|
+
const user = ctx.state.user;
|
|
729
|
+
const { realmId, id } = ctx.params;
|
|
730
|
+
const { data } = ctx.request.body;
|
|
731
|
+
if (!user) {
|
|
732
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
const { password, temporary = true } = data;
|
|
736
|
+
if (!password) {
|
|
737
|
+
return ctx.badRequest("Password is required.");
|
|
738
|
+
}
|
|
739
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
740
|
+
await userService2.resetPassword(realmId, id, password, temporary, user);
|
|
741
|
+
ctx.body = { data: { success: true } };
|
|
742
|
+
} catch (err) {
|
|
743
|
+
strapi.log.error(`[${PLUGIN_ID}] user.resetPassword error:`, err);
|
|
744
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
/**
|
|
748
|
+
* Enable a user
|
|
749
|
+
*/
|
|
750
|
+
async enable(ctx) {
|
|
751
|
+
const user = ctx.state.user;
|
|
752
|
+
const { realmId, id } = ctx.params;
|
|
753
|
+
if (!user) {
|
|
754
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
758
|
+
await userService2.enableUser(realmId, id, user);
|
|
759
|
+
ctx.body = { data: { success: true } };
|
|
760
|
+
} catch (err) {
|
|
761
|
+
strapi.log.error(`[${PLUGIN_ID}] user.enable error:`, err);
|
|
762
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
/**
|
|
766
|
+
* Disable a user
|
|
767
|
+
*/
|
|
768
|
+
async disable(ctx) {
|
|
769
|
+
const user = ctx.state.user;
|
|
770
|
+
const { realmId, id } = ctx.params;
|
|
771
|
+
if (!user) {
|
|
772
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
776
|
+
await userService2.disableUser(realmId, id, user);
|
|
777
|
+
ctx.body = { data: { success: true } };
|
|
778
|
+
} catch (err) {
|
|
779
|
+
strapi.log.error(`[${PLUGIN_ID}] user.disable error:`, err);
|
|
780
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
/**
|
|
784
|
+
* Send verification email
|
|
785
|
+
*/
|
|
786
|
+
async sendVerifyEmail(ctx) {
|
|
787
|
+
const user = ctx.state.user;
|
|
788
|
+
const { realmId, id } = ctx.params;
|
|
789
|
+
if (!user) {
|
|
790
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
791
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
794
|
+
await userService2.sendVerificationEmail(realmId, id, user);
|
|
795
|
+
ctx.body = { data: { success: true } };
|
|
796
|
+
} catch (err) {
|
|
797
|
+
strapi.log.error(`[${PLUGIN_ID}] user.sendVerifyEmail error:`, err);
|
|
798
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
/**
|
|
802
|
+
* Send password reset email
|
|
803
|
+
*/
|
|
804
|
+
async sendResetPasswordEmail(ctx) {
|
|
805
|
+
const user = ctx.state.user;
|
|
806
|
+
const { realmId, id } = ctx.params;
|
|
807
|
+
if (!user) {
|
|
808
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
812
|
+
await userService2.sendResetPasswordEmail(realmId, id, user);
|
|
813
|
+
ctx.body = { data: { success: true } };
|
|
814
|
+
} catch (err) {
|
|
815
|
+
strapi.log.error(`[${PLUGIN_ID}] user.sendResetPasswordEmail error:`, err);
|
|
816
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
/**
|
|
820
|
+
* Get realm roles
|
|
821
|
+
*/
|
|
822
|
+
async getRoles(ctx) {
|
|
823
|
+
const user = ctx.state.user;
|
|
824
|
+
const { realmId } = ctx.params;
|
|
825
|
+
if (!user) {
|
|
826
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
830
|
+
const roles = await userService2.getRoles(realmId, user);
|
|
831
|
+
ctx.body = { data: roles };
|
|
832
|
+
} catch (err) {
|
|
833
|
+
strapi.log.error(`[${PLUGIN_ID}] user.getRoles error:`, err);
|
|
834
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
/**
|
|
838
|
+
* Get user's roles
|
|
839
|
+
*/
|
|
840
|
+
async getUserRoles(ctx) {
|
|
841
|
+
const user = ctx.state.user;
|
|
842
|
+
const { realmId, id } = ctx.params;
|
|
843
|
+
if (!user) {
|
|
844
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
848
|
+
const roles = await userService2.getUserRoles(realmId, id, user);
|
|
849
|
+
ctx.body = { data: roles };
|
|
850
|
+
} catch (err) {
|
|
851
|
+
strapi.log.error(`[${PLUGIN_ID}] user.getUserRoles error:`, err);
|
|
852
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
/**
|
|
856
|
+
* Assign roles to user
|
|
857
|
+
*/
|
|
858
|
+
async assignRoles(ctx) {
|
|
859
|
+
const user = ctx.state.user;
|
|
860
|
+
const { realmId, id } = ctx.params;
|
|
861
|
+
const { data } = ctx.request.body;
|
|
862
|
+
if (!user) {
|
|
863
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
const { roles } = data;
|
|
867
|
+
if (!roles || !Array.isArray(roles)) {
|
|
868
|
+
return ctx.badRequest("Roles array is required.");
|
|
869
|
+
}
|
|
870
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
871
|
+
await userService2.assignRoles(realmId, id, roles, user);
|
|
872
|
+
ctx.body = { data: { success: true } };
|
|
873
|
+
} catch (err) {
|
|
874
|
+
strapi.log.error(`[${PLUGIN_ID}] user.assignRoles error:`, err);
|
|
875
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
/**
|
|
879
|
+
* Remove roles from user
|
|
880
|
+
*/
|
|
881
|
+
async removeRoles(ctx) {
|
|
882
|
+
const user = ctx.state.user;
|
|
883
|
+
const { realmId, id } = ctx.params;
|
|
884
|
+
const { data } = ctx.request.body;
|
|
885
|
+
if (!user) {
|
|
886
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
const { roles } = data;
|
|
890
|
+
if (!roles || !Array.isArray(roles)) {
|
|
891
|
+
return ctx.badRequest("Roles array is required.");
|
|
892
|
+
}
|
|
893
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
894
|
+
await userService2.removeRoles(realmId, id, roles, user);
|
|
895
|
+
ctx.body = { data: { success: true } };
|
|
896
|
+
} catch (err) {
|
|
897
|
+
strapi.log.error(`[${PLUGIN_ID}] user.removeRoles error:`, err);
|
|
898
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
/**
|
|
902
|
+
* Bulk import users
|
|
903
|
+
*/
|
|
904
|
+
async bulkImport(ctx) {
|
|
905
|
+
const user = ctx.state.user;
|
|
906
|
+
const { realmId } = ctx.params;
|
|
907
|
+
const { data } = ctx.request.body;
|
|
908
|
+
if (!user) {
|
|
909
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
910
|
+
}
|
|
911
|
+
try {
|
|
912
|
+
const { users } = data;
|
|
913
|
+
if (!users || !Array.isArray(users)) {
|
|
914
|
+
return ctx.badRequest("Users array is required.");
|
|
915
|
+
}
|
|
916
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
917
|
+
const result = await userService2.bulkImport(realmId, users, user);
|
|
918
|
+
ctx.body = { data: result };
|
|
919
|
+
} catch (err) {
|
|
920
|
+
strapi.log.error(`[${PLUGIN_ID}] user.bulkImport error:`, err);
|
|
921
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
/**
|
|
925
|
+
* Export users
|
|
926
|
+
*/
|
|
927
|
+
async exportUsers(ctx) {
|
|
928
|
+
const user = ctx.state.user;
|
|
929
|
+
const { realmId } = ctx.params;
|
|
930
|
+
const { format = "json" } = ctx.query;
|
|
931
|
+
if (!user) {
|
|
932
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
const userService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.USER);
|
|
936
|
+
const users = await userService2.exportUsers(realmId, user);
|
|
937
|
+
if (format === "csv") {
|
|
938
|
+
const headers = ["id", "username", "email", "firstName", "lastName", "enabled", "emailVerified"];
|
|
939
|
+
const csvRows = [headers.join(",")];
|
|
940
|
+
for (const u of users) {
|
|
941
|
+
const row = headers.map((h) => {
|
|
942
|
+
const val = u[h];
|
|
943
|
+
if (val === void 0 || val === null) return "";
|
|
944
|
+
if (typeof val === "string" && val.includes(",")) return `"${val}"`;
|
|
945
|
+
return String(val);
|
|
946
|
+
});
|
|
947
|
+
csvRows.push(row.join(","));
|
|
948
|
+
}
|
|
949
|
+
ctx.set("Content-Type", "text/csv");
|
|
950
|
+
ctx.set("Content-Disposition", `attachment; filename="keycloak-users-${realmId}.csv"`);
|
|
951
|
+
ctx.body = csvRows.join("\n");
|
|
952
|
+
} else {
|
|
953
|
+
ctx.body = { data: users };
|
|
954
|
+
}
|
|
955
|
+
} catch (err) {
|
|
956
|
+
strapi.log.error(`[${PLUGIN_ID}] user.exportUsers error:`, err);
|
|
957
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
const auditController = ({ strapi }) => ({
|
|
962
|
+
/**
|
|
963
|
+
* Find audit logs with filters
|
|
964
|
+
*/
|
|
965
|
+
async find(ctx) {
|
|
966
|
+
const user = ctx.state.user;
|
|
967
|
+
const { realmName, action, limit = 50, offset = 0 } = ctx.query;
|
|
968
|
+
if (!user) {
|
|
969
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
973
|
+
if (!permissionService2.isSuperAdmin(user)) {
|
|
974
|
+
const accessibleRealms = await permissionService2.getAccessibleRealms(user);
|
|
975
|
+
const accessibleRealmNames = accessibleRealms.map((r) => r.name);
|
|
976
|
+
if (realmName && !accessibleRealmNames.includes(realmName)) {
|
|
977
|
+
return ctx.forbidden(ERROR_MESSAGES.REALM_ACCESS_DENIED);
|
|
978
|
+
}
|
|
979
|
+
const auditLogService3 = strapi.plugin(PLUGIN_ID).service(SERVICES.AUDIT_LOG);
|
|
980
|
+
if (!realmName) {
|
|
981
|
+
const allLogs = [];
|
|
982
|
+
for (const rn of accessibleRealmNames) {
|
|
983
|
+
const { entries } = await auditLogService3.find({
|
|
984
|
+
realmName: rn,
|
|
985
|
+
action,
|
|
986
|
+
limit: parseInt(limit, 10),
|
|
987
|
+
offset: parseInt(offset, 10)
|
|
988
|
+
});
|
|
989
|
+
allLogs.push(...entries);
|
|
990
|
+
}
|
|
991
|
+
allLogs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
992
|
+
ctx.body = { data: allLogs.slice(0, parseInt(limit, 10)) };
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const auditLogService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.AUDIT_LOG);
|
|
997
|
+
const result = await auditLogService2.find({
|
|
998
|
+
realmName,
|
|
999
|
+
action,
|
|
1000
|
+
limit: parseInt(limit, 10),
|
|
1001
|
+
offset: parseInt(offset, 10)
|
|
1002
|
+
});
|
|
1003
|
+
ctx.body = { data: result.entries, meta: { total: result.total } };
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
strapi.log.error(`[${PLUGIN_ID}] audit.find error:`, err);
|
|
1006
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
/**
|
|
1010
|
+
* Find audit logs by realm
|
|
1011
|
+
*/
|
|
1012
|
+
async findByRealm(ctx) {
|
|
1013
|
+
const user = ctx.state.user;
|
|
1014
|
+
const { realmId } = ctx.params;
|
|
1015
|
+
const { limit = 50, offset = 0 } = ctx.query;
|
|
1016
|
+
if (!user) {
|
|
1017
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
1021
|
+
const realmService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
|
|
1022
|
+
const canAccess = await permissionService2.canAccessRealm(user, realmId, "canRead");
|
|
1023
|
+
if (!canAccess) {
|
|
1024
|
+
return ctx.forbidden(ERROR_MESSAGES.REALM_ACCESS_DENIED);
|
|
1025
|
+
}
|
|
1026
|
+
const realm = await realmService2.findOne(realmId);
|
|
1027
|
+
const auditLogService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.AUDIT_LOG);
|
|
1028
|
+
const result = await auditLogService2.findByRealm(realm.name, {
|
|
1029
|
+
limit: parseInt(limit, 10),
|
|
1030
|
+
offset: parseInt(offset, 10)
|
|
1031
|
+
});
|
|
1032
|
+
ctx.body = { data: result.entries, meta: { total: result.total } };
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
strapi.log.error(`[${PLUGIN_ID}] audit.findByRealm error:`, err);
|
|
1035
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
/**
|
|
1039
|
+
* Find audit logs by Keycloak user
|
|
1040
|
+
*/
|
|
1041
|
+
async findByUser(ctx) {
|
|
1042
|
+
const user = ctx.state.user;
|
|
1043
|
+
const { keycloakUserId } = ctx.params;
|
|
1044
|
+
const { limit = 50, offset = 0 } = ctx.query;
|
|
1045
|
+
if (!user) {
|
|
1046
|
+
return ctx.unauthorized(ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS);
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
const auditLogService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.AUDIT_LOG);
|
|
1050
|
+
const result = await auditLogService2.findByKeycloakUser(keycloakUserId, {
|
|
1051
|
+
limit: parseInt(limit, 10),
|
|
1052
|
+
offset: parseInt(offset, 10)
|
|
1053
|
+
});
|
|
1054
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
1055
|
+
if (!permissionService2.isSuperAdmin(user)) {
|
|
1056
|
+
const accessibleRealms = await permissionService2.getAccessibleRealms(user);
|
|
1057
|
+
const accessibleRealmNames = accessibleRealms.map((r) => r.name);
|
|
1058
|
+
const filteredEntries = result.entries.filter(
|
|
1059
|
+
(e) => accessibleRealmNames.includes(e.realmName)
|
|
1060
|
+
);
|
|
1061
|
+
ctx.body = { data: filteredEntries, meta: { total: filteredEntries.length } };
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
ctx.body = { data: result.entries, meta: { total: result.total } };
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
strapi.log.error(`[${PLUGIN_ID}] audit.findByUser error:`, err);
|
|
1067
|
+
ctx.throw(400, err.sanitizedMessage || ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
const controllers = {
|
|
1072
|
+
realm: realmController,
|
|
1073
|
+
user: userController,
|
|
1074
|
+
audit: auditController
|
|
1075
|
+
};
|
|
1076
|
+
const createSanitizedError = (internalMessage, sanitizedMessage) => {
|
|
1077
|
+
const err = new Error(internalMessage);
|
|
1078
|
+
err.sanitizedMessage = sanitizedMessage;
|
|
1079
|
+
return err;
|
|
1080
|
+
};
|
|
1081
|
+
const TOKEN_CACHE = /* @__PURE__ */ new Map();
|
|
1082
|
+
const getCacheKey = (realmConfig2) => `${realmConfig2.serverUrl}:${realmConfig2.realmName}:${realmConfig2.clientId}`;
|
|
1083
|
+
const buildTokenUrl = (serverUrl, realmName) => `${serverUrl}/realms/${realmName}/protocol/openid-connect/token`;
|
|
1084
|
+
const buildAdminUrl = (serverUrl, realmName, path = "") => `${serverUrl}/admin/realms/${realmName}${path}`;
|
|
1085
|
+
const keycloakClientService = ({ strapi }) => ({
|
|
1086
|
+
/**
|
|
1087
|
+
* Retrieves or refreshes an access token for the specified realm.
|
|
1088
|
+
* Tokens are cached and automatically refreshed before expiration.
|
|
1089
|
+
*
|
|
1090
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1091
|
+
* @returns {Promise<string>} Valid access token
|
|
1092
|
+
* @throws {Error} If authentication fails
|
|
1093
|
+
*
|
|
1094
|
+
* @example
|
|
1095
|
+
* const token = await keycloakClient.getAccessToken(realmConfig);
|
|
1096
|
+
* // Use token for API requests
|
|
1097
|
+
*/
|
|
1098
|
+
async getAccessToken(realmConfig2) {
|
|
1099
|
+
const cacheKey = getCacheKey(realmConfig2);
|
|
1100
|
+
const cached = TOKEN_CACHE.get(cacheKey);
|
|
1101
|
+
if (cached && Date.now() < cached.expiresAt - TOKEN_EXPIRY_BUFFER_MS) {
|
|
1102
|
+
return cached.accessToken;
|
|
1103
|
+
}
|
|
1104
|
+
try {
|
|
1105
|
+
const tokenUrl = buildTokenUrl(realmConfig2.serverUrl, realmConfig2.realmName);
|
|
1106
|
+
const response = await fetch(tokenUrl, {
|
|
1107
|
+
method: "POST",
|
|
1108
|
+
headers: {
|
|
1109
|
+
"Content-Type": HTTP_HEADERS.CONTENT_TYPE_FORM
|
|
1110
|
+
},
|
|
1111
|
+
body: new URLSearchParams({
|
|
1112
|
+
grant_type: "client_credentials",
|
|
1113
|
+
client_id: realmConfig2.clientId,
|
|
1114
|
+
client_secret: realmConfig2.clientSecret
|
|
1115
|
+
})
|
|
1116
|
+
});
|
|
1117
|
+
if (!response.ok) {
|
|
1118
|
+
const errorText = await response.text();
|
|
1119
|
+
strapi.log.error(`[${PLUGIN_ID}] Token fetch failed: ${errorText}`);
|
|
1120
|
+
throw createSanitizedError(
|
|
1121
|
+
`Token fetch failed: ${errorText}`,
|
|
1122
|
+
ERROR_MESSAGES.KEYCLOAK_AUTH_FAILED
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
const data = await response.json();
|
|
1126
|
+
TOKEN_CACHE.set(cacheKey, {
|
|
1127
|
+
accessToken: data.access_token,
|
|
1128
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
1129
|
+
});
|
|
1130
|
+
return data.access_token;
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
if (err.sanitizedMessage) throw err;
|
|
1133
|
+
strapi.log.error(`[${PLUGIN_ID}] Token error:`, err);
|
|
1134
|
+
throw createSanitizedError(err.message, ERROR_MESSAGES.KEYCLOAK_CONNECTION_FAILED);
|
|
1135
|
+
}
|
|
1136
|
+
},
|
|
1137
|
+
/**
|
|
1138
|
+
* Tests connection to a Keycloak realm by attempting authentication
|
|
1139
|
+
* and fetching realm metadata.
|
|
1140
|
+
*
|
|
1141
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1142
|
+
* @returns {Promise<ConnectionTestResult>} Test result with success status
|
|
1143
|
+
*
|
|
1144
|
+
* @example
|
|
1145
|
+
* const result = await keycloakClient.testConnection(config);
|
|
1146
|
+
* if (result.success) {
|
|
1147
|
+
* console.log(`Connected to ${result.realmDisplayName}`);
|
|
1148
|
+
* }
|
|
1149
|
+
*/
|
|
1150
|
+
async testConnection(realmConfig2) {
|
|
1151
|
+
try {
|
|
1152
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1153
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName);
|
|
1154
|
+
const response = await fetch(url, {
|
|
1155
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1156
|
+
});
|
|
1157
|
+
if (!response.ok) {
|
|
1158
|
+
return { success: false, message: `HTTP ${response.status}: ${response.statusText}` };
|
|
1159
|
+
}
|
|
1160
|
+
const realm = await response.json();
|
|
1161
|
+
return { success: true, realmDisplayName: realm.displayName || realm.realm };
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
return { success: false, message: err.sanitizedMessage || err.message };
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1166
|
+
/**
|
|
1167
|
+
* Fetches a paginated list of users from Keycloak.
|
|
1168
|
+
*
|
|
1169
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1170
|
+
* @param {Object} [options={}] - Query options
|
|
1171
|
+
* @param {string} [options.search=''] - Search query (matches username, email, name)
|
|
1172
|
+
* @param {number} [options.first=0] - Starting index for pagination
|
|
1173
|
+
* @param {number} [options.max=25] - Maximum results to return
|
|
1174
|
+
* @param {boolean} [options.briefRepresentation=true] - Return minimal user data
|
|
1175
|
+
* @returns {Promise<KeycloakUser[]>} Array of user objects
|
|
1176
|
+
* @throws {Error} If the request fails
|
|
1177
|
+
*
|
|
1178
|
+
* @example
|
|
1179
|
+
* const users = await keycloakClient.getUsers(realm, {
|
|
1180
|
+
* search: 'john',
|
|
1181
|
+
* first: 0,
|
|
1182
|
+
* max: 10
|
|
1183
|
+
* });
|
|
1184
|
+
*/
|
|
1185
|
+
async getUsers(realmConfig2, { search = "", first = 0, max = PAGINATION.PAGE_SIZE, briefRepresentation = true } = {}) {
|
|
1186
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1187
|
+
const params = new URLSearchParams({
|
|
1188
|
+
first: String(first),
|
|
1189
|
+
max: String(max),
|
|
1190
|
+
briefRepresentation: String(briefRepresentation)
|
|
1191
|
+
});
|
|
1192
|
+
if (search) {
|
|
1193
|
+
params.set("search", search);
|
|
1194
|
+
}
|
|
1195
|
+
const url = `${buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, "/users")}?${params}`;
|
|
1196
|
+
const response = await fetch(url, {
|
|
1197
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1198
|
+
});
|
|
1199
|
+
if (!response.ok) {
|
|
1200
|
+
const errorText = await response.text();
|
|
1201
|
+
strapi.log.error(`[${PLUGIN_ID}] Failed to fetch users: ${errorText}`);
|
|
1202
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.KEYCLOAK_CONNECTION_FAILED);
|
|
1203
|
+
}
|
|
1204
|
+
return response.json();
|
|
1205
|
+
},
|
|
1206
|
+
/**
|
|
1207
|
+
* Counts total users in a realm, optionally filtered by search query.
|
|
1208
|
+
*
|
|
1209
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1210
|
+
* @param {Object} [options={}] - Query options
|
|
1211
|
+
* @param {string} [options.search=''] - Search query to filter count
|
|
1212
|
+
* @returns {Promise<number>} Total user count
|
|
1213
|
+
*
|
|
1214
|
+
* @example
|
|
1215
|
+
* const total = await keycloakClient.countUsers(realm, { search: 'admin' });
|
|
1216
|
+
*/
|
|
1217
|
+
async countUsers(realmConfig2, { search = "" } = {}) {
|
|
1218
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1219
|
+
const params = new URLSearchParams();
|
|
1220
|
+
if (search) params.set("search", search);
|
|
1221
|
+
const url = `${buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, "/users/count")}?${params}`;
|
|
1222
|
+
const response = await fetch(url, {
|
|
1223
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1224
|
+
});
|
|
1225
|
+
if (!response.ok) {
|
|
1226
|
+
return 0;
|
|
1227
|
+
}
|
|
1228
|
+
return response.json();
|
|
1229
|
+
},
|
|
1230
|
+
/**
|
|
1231
|
+
* Retrieves a single user by their Keycloak ID.
|
|
1232
|
+
*
|
|
1233
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1234
|
+
* @param {string} userId - Keycloak user ID
|
|
1235
|
+
* @returns {Promise<KeycloakUser>} User object with full details
|
|
1236
|
+
* @throws {Error} If user not found or request fails
|
|
1237
|
+
*
|
|
1238
|
+
* @example
|
|
1239
|
+
* const user = await keycloakClient.getUserById(realm, 'abc-123');
|
|
1240
|
+
*/
|
|
1241
|
+
async getUserById(realmConfig2, userId) {
|
|
1242
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1243
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, `/users/${userId}`);
|
|
1244
|
+
const response = await fetch(url, {
|
|
1245
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1246
|
+
});
|
|
1247
|
+
if (!response.ok) {
|
|
1248
|
+
if (response.status === HTTP_STATUS.NOT_FOUND) {
|
|
1249
|
+
throw createSanitizedError(`User ${userId} not found`, ERROR_MESSAGES.USER_NOT_FOUND);
|
|
1250
|
+
}
|
|
1251
|
+
const errorText = await response.text();
|
|
1252
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.KEYCLOAK_CONNECTION_FAILED);
|
|
1253
|
+
}
|
|
1254
|
+
return response.json();
|
|
1255
|
+
},
|
|
1256
|
+
/**
|
|
1257
|
+
* Creates a new user in Keycloak.
|
|
1258
|
+
*
|
|
1259
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1260
|
+
* @param {Object} userData - User data to create
|
|
1261
|
+
* @param {string} userData.username - Required username
|
|
1262
|
+
* @param {string} [userData.email] - Email address
|
|
1263
|
+
* @param {string} [userData.firstName] - First name
|
|
1264
|
+
* @param {string} [userData.lastName] - Last name
|
|
1265
|
+
* @param {boolean} [userData.enabled=true] - Account enabled status
|
|
1266
|
+
* @param {boolean} [userData.emailVerified=false] - Email verification status
|
|
1267
|
+
* @returns {Promise<{userId: string|null, location: string|null}>} Created user info
|
|
1268
|
+
* @throws {Error} If user creation fails (e.g., duplicate username/email)
|
|
1269
|
+
*
|
|
1270
|
+
* @example
|
|
1271
|
+
* const result = await keycloakClient.createUser(realm, {
|
|
1272
|
+
* username: 'newuser',
|
|
1273
|
+
* email: 'new@example.com',
|
|
1274
|
+
* enabled: true
|
|
1275
|
+
* });
|
|
1276
|
+
*/
|
|
1277
|
+
async createUser(realmConfig2, userData) {
|
|
1278
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1279
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, "/users");
|
|
1280
|
+
const response = await fetch(url, {
|
|
1281
|
+
method: "POST",
|
|
1282
|
+
headers: {
|
|
1283
|
+
Authorization: `Bearer ${token}`,
|
|
1284
|
+
"Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON
|
|
1285
|
+
},
|
|
1286
|
+
body: JSON.stringify(userData)
|
|
1287
|
+
});
|
|
1288
|
+
if (!response.ok) {
|
|
1289
|
+
const errorText = await response.text();
|
|
1290
|
+
strapi.log.error(`[${PLUGIN_ID}] Failed to create user: ${errorText}`);
|
|
1291
|
+
if (response.status === HTTP_STATUS.CONFLICT) {
|
|
1292
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.USER_ALREADY_EXISTS);
|
|
1293
|
+
}
|
|
1294
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.INVALID_USER_DATA);
|
|
1295
|
+
}
|
|
1296
|
+
const location = response.headers.get("Location");
|
|
1297
|
+
const userId = location ? location.split("/").pop() : null;
|
|
1298
|
+
return { userId, location };
|
|
1299
|
+
},
|
|
1300
|
+
/**
|
|
1301
|
+
* Updates an existing user's attributes.
|
|
1302
|
+
*
|
|
1303
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1304
|
+
* @param {string} userId - Keycloak user ID
|
|
1305
|
+
* @param {Object} userData - Updated user attributes
|
|
1306
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
1307
|
+
* @throws {Error} If user not found or update fails
|
|
1308
|
+
*
|
|
1309
|
+
* @example
|
|
1310
|
+
* await keycloakClient.updateUser(realm, userId, {
|
|
1311
|
+
* firstName: 'Updated',
|
|
1312
|
+
* enabled: false
|
|
1313
|
+
* });
|
|
1314
|
+
*/
|
|
1315
|
+
async updateUser(realmConfig2, userId, userData) {
|
|
1316
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1317
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, `/users/${userId}`);
|
|
1318
|
+
const response = await fetch(url, {
|
|
1319
|
+
method: "PUT",
|
|
1320
|
+
headers: {
|
|
1321
|
+
Authorization: `Bearer ${token}`,
|
|
1322
|
+
"Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON
|
|
1323
|
+
},
|
|
1324
|
+
body: JSON.stringify(userData)
|
|
1325
|
+
});
|
|
1326
|
+
if (!response.ok) {
|
|
1327
|
+
const errorText = await response.text();
|
|
1328
|
+
strapi.log.error(`[${PLUGIN_ID}] Failed to update user: ${errorText}`);
|
|
1329
|
+
if (response.status === HTTP_STATUS.NOT_FOUND) {
|
|
1330
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.USER_NOT_FOUND);
|
|
1331
|
+
}
|
|
1332
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.INVALID_USER_DATA);
|
|
1333
|
+
}
|
|
1334
|
+
return { success: true };
|
|
1335
|
+
},
|
|
1336
|
+
/**
|
|
1337
|
+
* Permanently deletes a user from Keycloak.
|
|
1338
|
+
*
|
|
1339
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1340
|
+
* @param {string} userId - Keycloak user ID
|
|
1341
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
1342
|
+
* @throws {Error} If user not found or deletion fails
|
|
1343
|
+
*
|
|
1344
|
+
* @example
|
|
1345
|
+
* await keycloakClient.deleteUser(realm, 'user-id-123');
|
|
1346
|
+
*/
|
|
1347
|
+
async deleteUser(realmConfig2, userId) {
|
|
1348
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1349
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, `/users/${userId}`);
|
|
1350
|
+
const response = await fetch(url, {
|
|
1351
|
+
method: "DELETE",
|
|
1352
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1353
|
+
});
|
|
1354
|
+
if (!response.ok) {
|
|
1355
|
+
const errorText = await response.text();
|
|
1356
|
+
if (response.status === HTTP_STATUS.NOT_FOUND) {
|
|
1357
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.USER_NOT_FOUND);
|
|
1358
|
+
}
|
|
1359
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.KEYCLOAK_CONNECTION_FAILED);
|
|
1360
|
+
}
|
|
1361
|
+
return { success: true };
|
|
1362
|
+
},
|
|
1363
|
+
/**
|
|
1364
|
+
* Sets a new password for a user.
|
|
1365
|
+
*
|
|
1366
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1367
|
+
* @param {string} userId - Keycloak user ID
|
|
1368
|
+
* @param {string} password - New password value
|
|
1369
|
+
* @param {boolean} [temporary=true] - If true, user must change password on next login
|
|
1370
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
1371
|
+
* @throws {Error} If user not found or password reset fails
|
|
1372
|
+
*
|
|
1373
|
+
* @example
|
|
1374
|
+
* // Set temporary password (user must change on login)
|
|
1375
|
+
* await keycloakClient.resetPassword(realm, userId, 'TempPass123!', true);
|
|
1376
|
+
*
|
|
1377
|
+
* // Set permanent password
|
|
1378
|
+
* await keycloakClient.resetPassword(realm, userId, 'NewPass123!', false);
|
|
1379
|
+
*/
|
|
1380
|
+
async resetPassword(realmConfig2, userId, password, temporary = true) {
|
|
1381
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1382
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, `/users/${userId}/reset-password`);
|
|
1383
|
+
const response = await fetch(url, {
|
|
1384
|
+
method: "PUT",
|
|
1385
|
+
headers: {
|
|
1386
|
+
Authorization: `Bearer ${token}`,
|
|
1387
|
+
"Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON
|
|
1388
|
+
},
|
|
1389
|
+
body: JSON.stringify({
|
|
1390
|
+
type: "password",
|
|
1391
|
+
value: password,
|
|
1392
|
+
temporary
|
|
1393
|
+
})
|
|
1394
|
+
});
|
|
1395
|
+
if (!response.ok) {
|
|
1396
|
+
const errorText = await response.text();
|
|
1397
|
+
if (response.status === HTTP_STATUS.NOT_FOUND) {
|
|
1398
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.USER_NOT_FOUND);
|
|
1399
|
+
}
|
|
1400
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.PASSWORD_RESET_FAILED);
|
|
1401
|
+
}
|
|
1402
|
+
return { success: true };
|
|
1403
|
+
},
|
|
1404
|
+
/**
|
|
1405
|
+
* Enables a user account.
|
|
1406
|
+
*
|
|
1407
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1408
|
+
* @param {string} userId - Keycloak user ID
|
|
1409
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
1410
|
+
*/
|
|
1411
|
+
async enableUser(realmConfig2, userId) {
|
|
1412
|
+
return this.updateUser(realmConfig2, userId, { enabled: true });
|
|
1413
|
+
},
|
|
1414
|
+
/**
|
|
1415
|
+
* Disables a user account.
|
|
1416
|
+
*
|
|
1417
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1418
|
+
* @param {string} userId - Keycloak user ID
|
|
1419
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
1420
|
+
*/
|
|
1421
|
+
async disableUser(realmConfig2, userId) {
|
|
1422
|
+
return this.updateUser(realmConfig2, userId, { enabled: false });
|
|
1423
|
+
},
|
|
1424
|
+
/**
|
|
1425
|
+
* Triggers email actions for a user (e.g., verify email, update password).
|
|
1426
|
+
*
|
|
1427
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1428
|
+
* @param {string} userId - Keycloak user ID
|
|
1429
|
+
* @param {string[]} actions - Array of action types (e.g., ['VERIFY_EMAIL'])
|
|
1430
|
+
* @param {number} [lifespan] - Link validity in seconds (default: 12 hours)
|
|
1431
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
1432
|
+
* @throws {Error} If user not found or email fails
|
|
1433
|
+
*
|
|
1434
|
+
* @example
|
|
1435
|
+
* // Send verification email
|
|
1436
|
+
* await keycloakClient.executeEmailActions(realm, userId, ['VERIFY_EMAIL']);
|
|
1437
|
+
*
|
|
1438
|
+
* // Send password reset with custom lifespan (1 hour)
|
|
1439
|
+
* await keycloakClient.executeEmailActions(realm, userId, ['UPDATE_PASSWORD'], 3600);
|
|
1440
|
+
*/
|
|
1441
|
+
async executeEmailActions(realmConfig2, userId, actions, lifespan = EMAIL_ACTION_LIFESPAN_SECONDS) {
|
|
1442
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1443
|
+
const params = new URLSearchParams({ lifespan: String(lifespan) });
|
|
1444
|
+
const url = `${buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, `/users/${userId}/execute-actions-email`)}?${params}`;
|
|
1445
|
+
const response = await fetch(url, {
|
|
1446
|
+
method: "PUT",
|
|
1447
|
+
headers: {
|
|
1448
|
+
Authorization: `Bearer ${token}`,
|
|
1449
|
+
"Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON
|
|
1450
|
+
},
|
|
1451
|
+
body: JSON.stringify(actions)
|
|
1452
|
+
});
|
|
1453
|
+
if (!response.ok) {
|
|
1454
|
+
const errorText = await response.text();
|
|
1455
|
+
if (response.status === HTTP_STATUS.NOT_FOUND) {
|
|
1456
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.USER_NOT_FOUND);
|
|
1457
|
+
}
|
|
1458
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.EMAIL_SEND_FAILED);
|
|
1459
|
+
}
|
|
1460
|
+
return { success: true };
|
|
1461
|
+
},
|
|
1462
|
+
/**
|
|
1463
|
+
* Sends an email verification link to the user.
|
|
1464
|
+
*
|
|
1465
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1466
|
+
* @param {string} userId - Keycloak user ID
|
|
1467
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
1468
|
+
*/
|
|
1469
|
+
async sendVerificationEmail(realmConfig2, userId) {
|
|
1470
|
+
return this.executeEmailActions(realmConfig2, userId, [KEYCLOAK_EMAIL_ACTIONS.VERIFY_EMAIL]);
|
|
1471
|
+
},
|
|
1472
|
+
/**
|
|
1473
|
+
* Sends a password reset email to the user.
|
|
1474
|
+
*
|
|
1475
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1476
|
+
* @param {string} userId - Keycloak user ID
|
|
1477
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
1478
|
+
*/
|
|
1479
|
+
async sendResetPasswordEmail(realmConfig2, userId) {
|
|
1480
|
+
return this.executeEmailActions(realmConfig2, userId, [KEYCLOAK_EMAIL_ACTIONS.UPDATE_PASSWORD]);
|
|
1481
|
+
},
|
|
1482
|
+
/**
|
|
1483
|
+
* Retrieves all realm-level roles.
|
|
1484
|
+
*
|
|
1485
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1486
|
+
* @returns {Promise<KeycloakRole[]>} Array of realm roles
|
|
1487
|
+
*
|
|
1488
|
+
* @example
|
|
1489
|
+
* const roles = await keycloakClient.getRealmRoles(realm);
|
|
1490
|
+
* // [{ id: '...', name: 'admin', ... }, ...]
|
|
1491
|
+
*/
|
|
1492
|
+
async getRealmRoles(realmConfig2) {
|
|
1493
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1494
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, "/roles");
|
|
1495
|
+
const response = await fetch(url, {
|
|
1496
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1497
|
+
});
|
|
1498
|
+
if (!response.ok) {
|
|
1499
|
+
const errorText = await response.text();
|
|
1500
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.KEYCLOAK_CONNECTION_FAILED);
|
|
1501
|
+
}
|
|
1502
|
+
return response.json();
|
|
1503
|
+
},
|
|
1504
|
+
/**
|
|
1505
|
+
* Gets realm roles assigned to a specific user.
|
|
1506
|
+
*
|
|
1507
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1508
|
+
* @param {string} userId - Keycloak user ID
|
|
1509
|
+
* @returns {Promise<KeycloakRole[]>} Array of assigned roles
|
|
1510
|
+
*
|
|
1511
|
+
* @example
|
|
1512
|
+
* const userRoles = await keycloakClient.getUserRoles(realm, userId);
|
|
1513
|
+
*/
|
|
1514
|
+
async getUserRoles(realmConfig2, userId) {
|
|
1515
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1516
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, `/users/${userId}/role-mappings/realm`);
|
|
1517
|
+
const response = await fetch(url, {
|
|
1518
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1519
|
+
});
|
|
1520
|
+
if (!response.ok) {
|
|
1521
|
+
if (response.status === HTTP_STATUS.NOT_FOUND) {
|
|
1522
|
+
throw createSanitizedError(`User ${userId} not found`, ERROR_MESSAGES.USER_NOT_FOUND);
|
|
1523
|
+
}
|
|
1524
|
+
const errorText = await response.text();
|
|
1525
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.KEYCLOAK_CONNECTION_FAILED);
|
|
1526
|
+
}
|
|
1527
|
+
return response.json();
|
|
1528
|
+
},
|
|
1529
|
+
/**
|
|
1530
|
+
* Assigns realm roles to a user.
|
|
1531
|
+
*
|
|
1532
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1533
|
+
* @param {string} userId - Keycloak user ID
|
|
1534
|
+
* @param {KeycloakRole[]} roles - Array of role objects with id and name
|
|
1535
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
1536
|
+
*
|
|
1537
|
+
* @example
|
|
1538
|
+
* await keycloakClient.assignRoles(realm, userId, [
|
|
1539
|
+
* { id: 'role-id-1', name: 'admin' },
|
|
1540
|
+
* { id: 'role-id-2', name: 'editor' }
|
|
1541
|
+
* ]);
|
|
1542
|
+
*/
|
|
1543
|
+
async assignRoles(realmConfig2, userId, roles) {
|
|
1544
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1545
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, `/users/${userId}/role-mappings/realm`);
|
|
1546
|
+
const response = await fetch(url, {
|
|
1547
|
+
method: "POST",
|
|
1548
|
+
headers: {
|
|
1549
|
+
Authorization: `Bearer ${token}`,
|
|
1550
|
+
"Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON
|
|
1551
|
+
},
|
|
1552
|
+
body: JSON.stringify(roles)
|
|
1553
|
+
});
|
|
1554
|
+
if (!response.ok) {
|
|
1555
|
+
const errorText = await response.text();
|
|
1556
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.ROLE_ASSIGNMENT_FAILED);
|
|
1557
|
+
}
|
|
1558
|
+
return { success: true };
|
|
1559
|
+
},
|
|
1560
|
+
/**
|
|
1561
|
+
* Removes realm roles from a user.
|
|
1562
|
+
*
|
|
1563
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1564
|
+
* @param {string} userId - Keycloak user ID
|
|
1565
|
+
* @param {KeycloakRole[]} roles - Array of role objects to remove
|
|
1566
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
1567
|
+
*
|
|
1568
|
+
* @example
|
|
1569
|
+
* await keycloakClient.removeRoles(realm, userId, [
|
|
1570
|
+
* { id: 'role-id-1', name: 'admin' }
|
|
1571
|
+
* ]);
|
|
1572
|
+
*/
|
|
1573
|
+
async removeRoles(realmConfig2, userId, roles) {
|
|
1574
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1575
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, `/users/${userId}/role-mappings/realm`);
|
|
1576
|
+
const response = await fetch(url, {
|
|
1577
|
+
method: "DELETE",
|
|
1578
|
+
headers: {
|
|
1579
|
+
Authorization: `Bearer ${token}`,
|
|
1580
|
+
"Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON
|
|
1581
|
+
},
|
|
1582
|
+
body: JSON.stringify(roles)
|
|
1583
|
+
});
|
|
1584
|
+
if (!response.ok) {
|
|
1585
|
+
const errorText = await response.text();
|
|
1586
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.ROLE_REMOVAL_FAILED);
|
|
1587
|
+
}
|
|
1588
|
+
return { success: true };
|
|
1589
|
+
},
|
|
1590
|
+
/**
|
|
1591
|
+
* Gets active sessions for a user.
|
|
1592
|
+
*
|
|
1593
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1594
|
+
* @param {string} userId - Keycloak user ID
|
|
1595
|
+
* @returns {Promise<Object[]>} Array of session objects
|
|
1596
|
+
*
|
|
1597
|
+
* @example
|
|
1598
|
+
* const sessions = await keycloakClient.getUserSessions(realm, userId);
|
|
1599
|
+
*/
|
|
1600
|
+
async getUserSessions(realmConfig2, userId) {
|
|
1601
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1602
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, `/users/${userId}/sessions`);
|
|
1603
|
+
const response = await fetch(url, {
|
|
1604
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1605
|
+
});
|
|
1606
|
+
if (!response.ok) {
|
|
1607
|
+
return [];
|
|
1608
|
+
}
|
|
1609
|
+
return response.json();
|
|
1610
|
+
},
|
|
1611
|
+
/**
|
|
1612
|
+
* Terminates all active sessions for a user.
|
|
1613
|
+
*
|
|
1614
|
+
* @param {RealmConfig} realmConfig - Realm connection configuration
|
|
1615
|
+
* @param {string} userId - Keycloak user ID
|
|
1616
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
1617
|
+
*
|
|
1618
|
+
* @example
|
|
1619
|
+
* await keycloakClient.logoutUser(realm, userId);
|
|
1620
|
+
*/
|
|
1621
|
+
async logoutUser(realmConfig2, userId) {
|
|
1622
|
+
const token = await this.getAccessToken(realmConfig2);
|
|
1623
|
+
const url = buildAdminUrl(realmConfig2.serverUrl, realmConfig2.realmName, `/users/${userId}/logout`);
|
|
1624
|
+
const response = await fetch(url, {
|
|
1625
|
+
method: "POST",
|
|
1626
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1627
|
+
});
|
|
1628
|
+
if (!response.ok && response.status !== HTTP_STATUS.NO_CONTENT) {
|
|
1629
|
+
const errorText = await response.text();
|
|
1630
|
+
throw createSanitizedError(errorText, ERROR_MESSAGES.LOGOUT_FAILED);
|
|
1631
|
+
}
|
|
1632
|
+
return { success: true };
|
|
1633
|
+
},
|
|
1634
|
+
/**
|
|
1635
|
+
* Clears cached access tokens.
|
|
1636
|
+
* Call this when realm configuration changes to force re-authentication.
|
|
1637
|
+
*
|
|
1638
|
+
* @param {RealmConfig} [realmConfig] - Specific realm to clear, or all if omitted
|
|
1639
|
+
*
|
|
1640
|
+
* @example
|
|
1641
|
+
* // Clear specific realm's token
|
|
1642
|
+
* keycloakClient.clearTokenCache(realmConfig);
|
|
1643
|
+
*
|
|
1644
|
+
* // Clear all cached tokens
|
|
1645
|
+
* keycloakClient.clearTokenCache();
|
|
1646
|
+
*/
|
|
1647
|
+
clearTokenCache(realmConfig2) {
|
|
1648
|
+
if (realmConfig2) {
|
|
1649
|
+
const cacheKey = getCacheKey(realmConfig2);
|
|
1650
|
+
TOKEN_CACHE.delete(cacheKey);
|
|
1651
|
+
} else {
|
|
1652
|
+
TOKEN_CACHE.clear();
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
const auditLogService = ({ strapi }) => ({
|
|
1657
|
+
/**
|
|
1658
|
+
* Creates an audit log entry.
|
|
1659
|
+
* This method is designed to be non-blocking and fail-safe - audit logging
|
|
1660
|
+
* errors are caught and logged but do not interrupt the main operation.
|
|
1661
|
+
*
|
|
1662
|
+
* @param {AuditLogParams} params - Log entry parameters
|
|
1663
|
+
* @returns {Promise<AuditLogEntry|undefined>} Created entry or undefined on error
|
|
1664
|
+
*
|
|
1665
|
+
* @example
|
|
1666
|
+
* await auditLogService.log({
|
|
1667
|
+
* realmName: 'production',
|
|
1668
|
+
* realmDisplayName: 'Production Users',
|
|
1669
|
+
* action: AUDIT_ACTIONS.CREATE_USER,
|
|
1670
|
+
* keycloakUserId: 'user-123',
|
|
1671
|
+
* keycloakUsername: 'john.doe',
|
|
1672
|
+
* details: { email: 'john@example.com' },
|
|
1673
|
+
* user: ctx.state.user
|
|
1674
|
+
* });
|
|
1675
|
+
*/
|
|
1676
|
+
async log({ realmName, realmDisplayName, action, keycloakUserId, keycloakUsername, details, user }) {
|
|
1677
|
+
try {
|
|
1678
|
+
return await strapi.documents(CONTENT_TYPES.AUDIT_LOG).create({
|
|
1679
|
+
data: {
|
|
1680
|
+
realmName,
|
|
1681
|
+
realmDisplayName: realmDisplayName || realmName,
|
|
1682
|
+
action,
|
|
1683
|
+
keycloakUserId: keycloakUserId || null,
|
|
1684
|
+
keycloakUsername: keycloakUsername || null,
|
|
1685
|
+
details: details || null,
|
|
1686
|
+
performedById: user?.id || null,
|
|
1687
|
+
performedByEmail: user?.email || UNKNOWN_USER_EMAIL
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
strapi.log.error(`[${PLUGIN_ID}] Failed to create audit log entry:`, err);
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
/**
|
|
1695
|
+
* Queries audit log entries with optional filters.
|
|
1696
|
+
*
|
|
1697
|
+
* @param {AuditLogQueryParams} [params={}] - Query parameters
|
|
1698
|
+
* @returns {Promise<AuditLogQueryResult>} Paginated log entries with total count
|
|
1699
|
+
*
|
|
1700
|
+
* @example
|
|
1701
|
+
* // Get all entries for a realm
|
|
1702
|
+
* const { entries, total } = await auditLogService.find({
|
|
1703
|
+
* realmName: 'production',
|
|
1704
|
+
* limit: 25,
|
|
1705
|
+
* offset: 0
|
|
1706
|
+
* });
|
|
1707
|
+
*
|
|
1708
|
+
* @example
|
|
1709
|
+
* // Get all password reset actions
|
|
1710
|
+
* const { entries } = await auditLogService.find({
|
|
1711
|
+
* action: AUDIT_ACTIONS.RESET_PASSWORD
|
|
1712
|
+
* });
|
|
1713
|
+
*/
|
|
1714
|
+
async find({
|
|
1715
|
+
realmName,
|
|
1716
|
+
action,
|
|
1717
|
+
keycloakUserId,
|
|
1718
|
+
limit = PAGINATION.AUDIT_LOG_LIMIT,
|
|
1719
|
+
offset = 0
|
|
1720
|
+
} = {}) {
|
|
1721
|
+
const filters = {};
|
|
1722
|
+
if (realmName) {
|
|
1723
|
+
filters.realmName = realmName;
|
|
1724
|
+
}
|
|
1725
|
+
if (action) {
|
|
1726
|
+
filters.action = action;
|
|
1727
|
+
}
|
|
1728
|
+
if (keycloakUserId) {
|
|
1729
|
+
filters.keycloakUserId = keycloakUserId;
|
|
1730
|
+
}
|
|
1731
|
+
const [entries, count] = await Promise.all([
|
|
1732
|
+
strapi.documents(CONTENT_TYPES.AUDIT_LOG).findMany({
|
|
1733
|
+
filters,
|
|
1734
|
+
sort: { createdAt: "desc" },
|
|
1735
|
+
limit,
|
|
1736
|
+
offset
|
|
1737
|
+
}),
|
|
1738
|
+
strapi.documents(CONTENT_TYPES.AUDIT_LOG).count({ filters })
|
|
1739
|
+
]);
|
|
1740
|
+
return { entries, total: count };
|
|
1741
|
+
},
|
|
1742
|
+
/**
|
|
1743
|
+
* Retrieves audit logs for a specific realm.
|
|
1744
|
+
* Convenience method that wraps find() with realm filter.
|
|
1745
|
+
*
|
|
1746
|
+
* @param {string} realmName - Realm slug identifier
|
|
1747
|
+
* @param {Object} [options={}] - Query options
|
|
1748
|
+
* @param {number} [options.limit=50] - Maximum entries
|
|
1749
|
+
* @param {number} [options.offset=0] - Starting offset
|
|
1750
|
+
* @returns {Promise<AuditLogQueryResult>} Paginated log entries
|
|
1751
|
+
*
|
|
1752
|
+
* @example
|
|
1753
|
+
* const { entries, total } = await auditLogService.findByRealm('production', {
|
|
1754
|
+
* limit: 100
|
|
1755
|
+
* });
|
|
1756
|
+
*/
|
|
1757
|
+
async findByRealm(realmName, { limit = PAGINATION.AUDIT_LOG_LIMIT, offset = 0 } = {}) {
|
|
1758
|
+
return this.find({ realmName, limit, offset });
|
|
1759
|
+
},
|
|
1760
|
+
/**
|
|
1761
|
+
* Retrieves audit logs for a specific Keycloak user.
|
|
1762
|
+
* Useful for viewing all actions performed on a single user.
|
|
1763
|
+
*
|
|
1764
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
1765
|
+
* @param {Object} [options={}] - Query options
|
|
1766
|
+
* @param {number} [options.limit=50] - Maximum entries
|
|
1767
|
+
* @param {number} [options.offset=0] - Starting offset
|
|
1768
|
+
* @returns {Promise<AuditLogQueryResult>} Paginated log entries
|
|
1769
|
+
*
|
|
1770
|
+
* @example
|
|
1771
|
+
* const { entries } = await auditLogService.findByKeycloakUser('user-123');
|
|
1772
|
+
* // Shows: created, updated, password reset, role changes, etc.
|
|
1773
|
+
*/
|
|
1774
|
+
async findByKeycloakUser(keycloakUserId, { limit = PAGINATION.AUDIT_LOG_LIMIT, offset = 0 } = {}) {
|
|
1775
|
+
return this.find({ keycloakUserId, limit, offset });
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
const SUPER_ADMIN_PERMISSIONS = {
|
|
1779
|
+
canRead: true,
|
|
1780
|
+
canCreate: true,
|
|
1781
|
+
canUpdate: true,
|
|
1782
|
+
canDelete: true,
|
|
1783
|
+
canManageRoles: true,
|
|
1784
|
+
canResetPassword: true
|
|
1785
|
+
};
|
|
1786
|
+
const permissionService = ({ strapi }) => ({
|
|
1787
|
+
/**
|
|
1788
|
+
* Checks if a user has the Strapi super admin role.
|
|
1789
|
+
* Super admins have unrestricted access to all realms and operations.
|
|
1790
|
+
*
|
|
1791
|
+
* @param {StrapiUser} user - Strapi user object with roles
|
|
1792
|
+
* @returns {boolean} True if user is a super admin
|
|
1793
|
+
*
|
|
1794
|
+
* @example
|
|
1795
|
+
* if (permissionService.isSuperAdmin(ctx.state.user)) {
|
|
1796
|
+
* // Allow unrestricted access
|
|
1797
|
+
* }
|
|
1798
|
+
*/
|
|
1799
|
+
isSuperAdmin(user) {
|
|
1800
|
+
if (!user || !user.roles) return false;
|
|
1801
|
+
return user.roles.some((role) => role.code === STRAPI_SUPER_ADMIN_ROLE);
|
|
1802
|
+
},
|
|
1803
|
+
/**
|
|
1804
|
+
* Retrieves the realm admin assignment for a specific user and realm.
|
|
1805
|
+
*
|
|
1806
|
+
* @param {number} strapiUserId - Strapi user ID
|
|
1807
|
+
* @param {string} realmConfigId - Realm document ID
|
|
1808
|
+
* @returns {Promise<RealmAdminAssignment|null>} Assignment or null if none exists
|
|
1809
|
+
*
|
|
1810
|
+
* @example
|
|
1811
|
+
* const assignment = await permissionService.getRealmAdminAssignment(user.id, realmId);
|
|
1812
|
+
* if (assignment?.permissions.canCreate) {
|
|
1813
|
+
* // User can create
|
|
1814
|
+
* }
|
|
1815
|
+
*/
|
|
1816
|
+
async getRealmAdminAssignment(strapiUserId, realmConfigId) {
|
|
1817
|
+
const assignments = await strapi.documents(CONTENT_TYPES.REALM_ADMIN).findMany({
|
|
1818
|
+
filters: {
|
|
1819
|
+
strapiUserId,
|
|
1820
|
+
realmConfig: { documentId: realmConfigId }
|
|
1821
|
+
},
|
|
1822
|
+
populate: ["realmConfig"]
|
|
1823
|
+
});
|
|
1824
|
+
return assignments[0] || null;
|
|
1825
|
+
},
|
|
1826
|
+
/**
|
|
1827
|
+
* Checks if a user has a specific permission for a realm.
|
|
1828
|
+
*
|
|
1829
|
+
* @param {StrapiUser} user - Strapi user object
|
|
1830
|
+
* @param {string} realmConfigId - Realm document ID
|
|
1831
|
+
* @param {keyof RealmPermissions} [permission='canRead'] - Permission to check
|
|
1832
|
+
* @returns {Promise<boolean>} True if user has the permission
|
|
1833
|
+
*
|
|
1834
|
+
* @example
|
|
1835
|
+
* const canDelete = await permissionService.canAccessRealm(user, realmId, 'canDelete');
|
|
1836
|
+
* if (!canDelete) {
|
|
1837
|
+
* throw new ForbiddenError();
|
|
1838
|
+
* }
|
|
1839
|
+
*/
|
|
1840
|
+
async canAccessRealm(user, realmConfigId, permission = "canRead") {
|
|
1841
|
+
if (this.isSuperAdmin(user)) {
|
|
1842
|
+
return true;
|
|
1843
|
+
}
|
|
1844
|
+
const assignment = await this.getRealmAdminAssignment(user.id, realmConfigId);
|
|
1845
|
+
if (!assignment) {
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
const permissions = assignment.permissions || DEFAULT_REALM_PERMISSIONS;
|
|
1849
|
+
return permissions[permission] === true;
|
|
1850
|
+
},
|
|
1851
|
+
/**
|
|
1852
|
+
* Retrieves all realms accessible by a user with their permissions.
|
|
1853
|
+
* Super admins see all enabled realms with full permissions.
|
|
1854
|
+
* Regular users see only realms they're assigned to.
|
|
1855
|
+
*
|
|
1856
|
+
* @param {StrapiUser} user - Strapi user object
|
|
1857
|
+
* @returns {Promise<Array<Object & {permissions: RealmPermissions}>>} Accessible realms with permissions
|
|
1858
|
+
*
|
|
1859
|
+
* @example
|
|
1860
|
+
* const realms = await permissionService.getAccessibleRealms(ctx.state.user);
|
|
1861
|
+
* // Returns realms with permissions attached
|
|
1862
|
+
*/
|
|
1863
|
+
async getAccessibleRealms(user) {
|
|
1864
|
+
if (this.isSuperAdmin(user)) {
|
|
1865
|
+
const allRealms = await strapi.documents(CONTENT_TYPES.REALM_CONFIG).findMany({
|
|
1866
|
+
filters: { enabled: true },
|
|
1867
|
+
sort: { displayName: "asc" }
|
|
1868
|
+
});
|
|
1869
|
+
return allRealms.map((realm) => ({
|
|
1870
|
+
...realm,
|
|
1871
|
+
permissions: SUPER_ADMIN_PERMISSIONS
|
|
1872
|
+
}));
|
|
1873
|
+
}
|
|
1874
|
+
const assignments = await strapi.documents(CONTENT_TYPES.REALM_ADMIN).findMany({
|
|
1875
|
+
filters: {
|
|
1876
|
+
strapiUserId: user.id,
|
|
1877
|
+
realmConfig: { enabled: true }
|
|
1878
|
+
},
|
|
1879
|
+
populate: ["realmConfig"]
|
|
1880
|
+
});
|
|
1881
|
+
return assignments.filter((assignment) => assignment.realmConfig).map((assignment) => ({
|
|
1882
|
+
...assignment.realmConfig,
|
|
1883
|
+
permissions: assignment.permissions || DEFAULT_REALM_PERMISSIONS
|
|
1884
|
+
}));
|
|
1885
|
+
},
|
|
1886
|
+
/**
|
|
1887
|
+
* Gets a user's specific permissions for a realm.
|
|
1888
|
+
*
|
|
1889
|
+
* @param {StrapiUser} user - Strapi user object
|
|
1890
|
+
* @param {string} realmConfigId - Realm document ID
|
|
1891
|
+
* @returns {Promise<RealmPermissions|null>} User's permissions or null if no access
|
|
1892
|
+
*
|
|
1893
|
+
* @example
|
|
1894
|
+
* const permissions = await permissionService.getRealmPermissions(user, realmId);
|
|
1895
|
+
* if (permissions?.canUpdate) {
|
|
1896
|
+
* // Show edit button
|
|
1897
|
+
* }
|
|
1898
|
+
*/
|
|
1899
|
+
async getRealmPermissions(user, realmConfigId) {
|
|
1900
|
+
if (this.isSuperAdmin(user)) {
|
|
1901
|
+
return SUPER_ADMIN_PERMISSIONS;
|
|
1902
|
+
}
|
|
1903
|
+
const assignment = await this.getRealmAdminAssignment(user.id, realmConfigId);
|
|
1904
|
+
return assignment?.permissions || null;
|
|
1905
|
+
},
|
|
1906
|
+
/**
|
|
1907
|
+
* Assigns a Strapi user to a realm with specified permissions.
|
|
1908
|
+
* If an assignment already exists, it updates the permissions.
|
|
1909
|
+
*
|
|
1910
|
+
* @param {number} strapiUserId - Strapi user ID to assign
|
|
1911
|
+
* @param {string} strapiUserEmail - User's email for reference
|
|
1912
|
+
* @param {string} realmConfigId - Realm document ID
|
|
1913
|
+
* @param {Partial<RealmPermissions>} [permissions={}] - Permissions to grant
|
|
1914
|
+
* @returns {Promise<RealmAdminAssignment>} Created or updated assignment
|
|
1915
|
+
*
|
|
1916
|
+
* @example
|
|
1917
|
+
* await permissionService.assignUserToRealm(
|
|
1918
|
+
* userId,
|
|
1919
|
+
* 'admin@example.com',
|
|
1920
|
+
* realmId,
|
|
1921
|
+
* { canRead: true, canCreate: true, canUpdate: true }
|
|
1922
|
+
* );
|
|
1923
|
+
*/
|
|
1924
|
+
async assignUserToRealm(strapiUserId, strapiUserEmail, realmConfigId, permissions = {}) {
|
|
1925
|
+
const existing = await this.getRealmAdminAssignment(strapiUserId, realmConfigId);
|
|
1926
|
+
const mergedPermissions = { ...DEFAULT_REALM_PERMISSIONS, ...permissions };
|
|
1927
|
+
if (existing) {
|
|
1928
|
+
return strapi.documents(CONTENT_TYPES.REALM_ADMIN).update({
|
|
1929
|
+
documentId: existing.documentId,
|
|
1930
|
+
data: {
|
|
1931
|
+
permissions: mergedPermissions
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
return strapi.documents(CONTENT_TYPES.REALM_ADMIN).create({
|
|
1936
|
+
data: {
|
|
1937
|
+
strapiUserId,
|
|
1938
|
+
strapiUserEmail,
|
|
1939
|
+
realmConfig: realmConfigId,
|
|
1940
|
+
permissions: mergedPermissions
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
},
|
|
1944
|
+
/**
|
|
1945
|
+
* Removes a user's access to a realm.
|
|
1946
|
+
*
|
|
1947
|
+
* @param {number} strapiUserId - Strapi user ID
|
|
1948
|
+
* @param {string} realmConfigId - Realm document ID
|
|
1949
|
+
* @returns {Promise<boolean>} True if assignment was removed, false if none existed
|
|
1950
|
+
*
|
|
1951
|
+
* @example
|
|
1952
|
+
* const removed = await permissionService.removeUserFromRealm(userId, realmId);
|
|
1953
|
+
*/
|
|
1954
|
+
async removeUserFromRealm(strapiUserId, realmConfigId) {
|
|
1955
|
+
const assignment = await this.getRealmAdminAssignment(strapiUserId, realmConfigId);
|
|
1956
|
+
if (assignment) {
|
|
1957
|
+
await strapi.documents(CONTENT_TYPES.REALM_ADMIN).delete({
|
|
1958
|
+
documentId: assignment.documentId
|
|
1959
|
+
});
|
|
1960
|
+
return true;
|
|
1961
|
+
}
|
|
1962
|
+
return false;
|
|
1963
|
+
},
|
|
1964
|
+
/**
|
|
1965
|
+
* Retrieves all admin assignments for a specific realm.
|
|
1966
|
+
*
|
|
1967
|
+
* @param {string} realmConfigId - Realm document ID
|
|
1968
|
+
* @returns {Promise<RealmAdminAssignment[]>} Array of admin assignments
|
|
1969
|
+
*
|
|
1970
|
+
* @example
|
|
1971
|
+
* const admins = await permissionService.getRealmAdmins(realmId);
|
|
1972
|
+
* // Display list of users who can manage this realm
|
|
1973
|
+
*/
|
|
1974
|
+
async getRealmAdmins(realmConfigId) {
|
|
1975
|
+
const assignments = await strapi.documents(CONTENT_TYPES.REALM_ADMIN).findMany({
|
|
1976
|
+
filters: {
|
|
1977
|
+
realmConfig: { documentId: realmConfigId }
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
return assignments;
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
const realmService = ({ strapi }) => ({
|
|
1984
|
+
/**
|
|
1985
|
+
* Gets the Keycloak client service instance.
|
|
1986
|
+
*
|
|
1987
|
+
* @returns {Object} Keycloak client service
|
|
1988
|
+
* @private
|
|
1989
|
+
*/
|
|
1990
|
+
get keycloakClient() {
|
|
1991
|
+
return strapi.plugin(PLUGIN_ID).service(SERVICES.KEYCLOAK_CLIENT);
|
|
1992
|
+
},
|
|
1993
|
+
/**
|
|
1994
|
+
* Retrieves all realm configurations.
|
|
1995
|
+
* Note: clientSecret is not included in results (private field).
|
|
1996
|
+
*
|
|
1997
|
+
* @returns {Promise<RealmConfig[]>} Array of realm configurations sorted by displayName
|
|
1998
|
+
*
|
|
1999
|
+
* @example
|
|
2000
|
+
* const realms = await realmService.findAll();
|
|
2001
|
+
*/
|
|
2002
|
+
async findAll() {
|
|
2003
|
+
return strapi.documents(CONTENT_TYPES.REALM_CONFIG).findMany({
|
|
2004
|
+
sort: { displayName: "asc" }
|
|
2005
|
+
});
|
|
2006
|
+
},
|
|
2007
|
+
/**
|
|
2008
|
+
* Retrieves a single realm by document ID.
|
|
2009
|
+
* Note: clientSecret is not included (private field).
|
|
2010
|
+
*
|
|
2011
|
+
* @param {string} documentId - Strapi document ID
|
|
2012
|
+
* @returns {Promise<RealmConfig>} Realm configuration
|
|
2013
|
+
* @throws {Error} If realm not found
|
|
2014
|
+
*
|
|
2015
|
+
* @example
|
|
2016
|
+
* const realm = await realmService.findOne('abc123');
|
|
2017
|
+
*/
|
|
2018
|
+
async findOne(documentId) {
|
|
2019
|
+
const realm = await strapi.documents(CONTENT_TYPES.REALM_CONFIG).findOne({
|
|
2020
|
+
documentId
|
|
2021
|
+
});
|
|
2022
|
+
if (!realm) {
|
|
2023
|
+
throw createSanitizedError(
|
|
2024
|
+
`Realm ${documentId} not found`,
|
|
2025
|
+
ERROR_MESSAGES.REALM_NOT_FOUND
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
return realm;
|
|
2029
|
+
},
|
|
2030
|
+
/**
|
|
2031
|
+
* Retrieves a realm with its clientSecret for internal Keycloak API calls.
|
|
2032
|
+
* Uses db.query to bypass Strapi's private field sanitization.
|
|
2033
|
+
*
|
|
2034
|
+
* SECURITY: Only use this method when the clientSecret is actually needed
|
|
2035
|
+
* for Keycloak authentication. Never expose the result to API responses.
|
|
2036
|
+
*
|
|
2037
|
+
* @param {string} documentId - Strapi document ID
|
|
2038
|
+
* @returns {Promise<RealmConfig & {clientSecret: string}>} Full realm config with secret
|
|
2039
|
+
* @throws {Error} If realm not found
|
|
2040
|
+
*
|
|
2041
|
+
* @example
|
|
2042
|
+
* // Internal use only - for Keycloak API calls
|
|
2043
|
+
* const realm = await realmService.findOneWithSecret(documentId);
|
|
2044
|
+
* const token = await keycloakClient.getAccessToken(realm);
|
|
2045
|
+
*/
|
|
2046
|
+
async findOneWithSecret(documentId) {
|
|
2047
|
+
const realm = await strapi.db.query(CONTENT_TYPES.REALM_CONFIG).findOne({
|
|
2048
|
+
where: { documentId }
|
|
2049
|
+
});
|
|
2050
|
+
if (!realm) {
|
|
2051
|
+
throw createSanitizedError(
|
|
2052
|
+
`Realm ${documentId} not found`,
|
|
2053
|
+
ERROR_MESSAGES.REALM_NOT_FOUND
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
return realm;
|
|
2057
|
+
},
|
|
2058
|
+
/**
|
|
2059
|
+
* Finds a realm by its unique slug name.
|
|
2060
|
+
*
|
|
2061
|
+
* @param {string} name - Realm slug name
|
|
2062
|
+
* @returns {Promise<RealmConfig|null>} Realm configuration or null if not found
|
|
2063
|
+
*
|
|
2064
|
+
* @example
|
|
2065
|
+
* const realm = await realmService.findByName('production-users');
|
|
2066
|
+
*/
|
|
2067
|
+
async findByName(name) {
|
|
2068
|
+
const realms = await strapi.documents(CONTENT_TYPES.REALM_CONFIG).findMany({
|
|
2069
|
+
filters: { name }
|
|
2070
|
+
});
|
|
2071
|
+
return realms[0] || null;
|
|
2072
|
+
},
|
|
2073
|
+
/**
|
|
2074
|
+
* Validates realm name format.
|
|
2075
|
+
*
|
|
2076
|
+
* @param {string} name - Name to validate
|
|
2077
|
+
* @returns {boolean} True if valid
|
|
2078
|
+
* @private
|
|
2079
|
+
*/
|
|
2080
|
+
isValidName(name) {
|
|
2081
|
+
return REALM_NAME_PATTERN.test(name);
|
|
2082
|
+
},
|
|
2083
|
+
/**
|
|
2084
|
+
* Normalizes a server URL by removing trailing slashes.
|
|
2085
|
+
*
|
|
2086
|
+
* @param {string} url - URL to normalize
|
|
2087
|
+
* @returns {string} Normalized URL
|
|
2088
|
+
* @private
|
|
2089
|
+
*/
|
|
2090
|
+
normalizeServerUrl(url) {
|
|
2091
|
+
return url.replace(/\/$/, "");
|
|
2092
|
+
},
|
|
2093
|
+
/**
|
|
2094
|
+
* Creates a new realm configuration.
|
|
2095
|
+
*
|
|
2096
|
+
* @param {RealmConfigData} data - Realm configuration data
|
|
2097
|
+
* @returns {Promise<RealmConfig>} Created realm configuration
|
|
2098
|
+
* @throws {Error} If validation fails or name already exists
|
|
2099
|
+
*
|
|
2100
|
+
* @example
|
|
2101
|
+
* const realm = await realmService.create({
|
|
2102
|
+
* name: 'production-users',
|
|
2103
|
+
* displayName: 'Production Users',
|
|
2104
|
+
* serverUrl: 'https://keycloak.example.com',
|
|
2105
|
+
* realmName: 'production',
|
|
2106
|
+
* clientId: 'strapi-admin',
|
|
2107
|
+
* clientSecret: 'secret-value'
|
|
2108
|
+
* });
|
|
2109
|
+
*/
|
|
2110
|
+
async create(data) {
|
|
2111
|
+
const { name, displayName, serverUrl, realmName, clientId, clientSecret } = data;
|
|
2112
|
+
if (!name || !displayName || !serverUrl || !realmName || !clientId) {
|
|
2113
|
+
throw createSanitizedError(
|
|
2114
|
+
"Missing required fields",
|
|
2115
|
+
ERROR_MESSAGES.MISSING_REQUIRED_FIELDS
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
if (!this.isValidName(name)) {
|
|
2119
|
+
throw createSanitizedError(
|
|
2120
|
+
`Invalid name format: ${name}`,
|
|
2121
|
+
ERROR_MESSAGES.INVALID_NAME_FORMAT
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
const existing = await this.findByName(name);
|
|
2125
|
+
if (existing) {
|
|
2126
|
+
throw createSanitizedError(
|
|
2127
|
+
`Realm with name ${name} already exists`,
|
|
2128
|
+
ERROR_MESSAGES.REALM_NAME_EXISTS
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
return strapi.documents(CONTENT_TYPES.REALM_CONFIG).create({
|
|
2132
|
+
data: {
|
|
2133
|
+
name,
|
|
2134
|
+
displayName,
|
|
2135
|
+
serverUrl: this.normalizeServerUrl(serverUrl),
|
|
2136
|
+
realmName,
|
|
2137
|
+
clientId,
|
|
2138
|
+
clientSecret: clientSecret || null,
|
|
2139
|
+
enabled: data.enabled !== false,
|
|
2140
|
+
color: data.color || DEFAULT_REALM_COLOR
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
},
|
|
2144
|
+
/**
|
|
2145
|
+
* Updates an existing realm configuration.
|
|
2146
|
+
*
|
|
2147
|
+
* @param {string} documentId - Strapi document ID
|
|
2148
|
+
* @param {Partial<RealmConfigData>} data - Fields to update
|
|
2149
|
+
* @returns {Promise<RealmConfig>} Updated realm configuration
|
|
2150
|
+
* @throws {Error} If validation fails or realm not found
|
|
2151
|
+
*
|
|
2152
|
+
* @example
|
|
2153
|
+
* const updated = await realmService.update(documentId, {
|
|
2154
|
+
* displayName: 'New Display Name',
|
|
2155
|
+
* enabled: false
|
|
2156
|
+
* });
|
|
2157
|
+
*/
|
|
2158
|
+
async update(documentId, data) {
|
|
2159
|
+
const existing = await this.findOne(documentId);
|
|
2160
|
+
if (data.name && data.name !== existing.name) {
|
|
2161
|
+
if (!this.isValidName(data.name)) {
|
|
2162
|
+
throw createSanitizedError(
|
|
2163
|
+
`Invalid name format: ${data.name}`,
|
|
2164
|
+
ERROR_MESSAGES.INVALID_NAME_FORMAT
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
const duplicate = await this.findByName(data.name);
|
|
2168
|
+
if (duplicate && duplicate.documentId !== documentId) {
|
|
2169
|
+
throw createSanitizedError(
|
|
2170
|
+
`Realm with name ${data.name} already exists`,
|
|
2171
|
+
ERROR_MESSAGES.REALM_NAME_EXISTS
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const updateData = { ...data };
|
|
2176
|
+
if (updateData.serverUrl) {
|
|
2177
|
+
updateData.serverUrl = this.normalizeServerUrl(updateData.serverUrl);
|
|
2178
|
+
}
|
|
2179
|
+
const connectionFieldsChanged = updateData.serverUrl || updateData.realmName || updateData.clientId || updateData.clientSecret;
|
|
2180
|
+
if (connectionFieldsChanged) {
|
|
2181
|
+
this.keycloakClient.clearTokenCache(existing);
|
|
2182
|
+
}
|
|
2183
|
+
return strapi.documents(CONTENT_TYPES.REALM_CONFIG).update({
|
|
2184
|
+
documentId,
|
|
2185
|
+
data: updateData
|
|
2186
|
+
});
|
|
2187
|
+
},
|
|
2188
|
+
/**
|
|
2189
|
+
* Deletes a realm configuration and all associated admin assignments.
|
|
2190
|
+
*
|
|
2191
|
+
* @param {string} documentId - Strapi document ID
|
|
2192
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
2193
|
+
* @throws {Error} If realm not found
|
|
2194
|
+
*
|
|
2195
|
+
* @example
|
|
2196
|
+
* await realmService.delete(documentId);
|
|
2197
|
+
*/
|
|
2198
|
+
async delete(documentId) {
|
|
2199
|
+
const realm = await this.findOne(documentId);
|
|
2200
|
+
this.keycloakClient.clearTokenCache(realm);
|
|
2201
|
+
const assignments = await strapi.documents(CONTENT_TYPES.REALM_ADMIN).findMany({
|
|
2202
|
+
filters: {
|
|
2203
|
+
realmConfig: { documentId }
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
for (const assignment of assignments) {
|
|
2207
|
+
await strapi.documents(CONTENT_TYPES.REALM_ADMIN).delete({
|
|
2208
|
+
documentId: assignment.documentId
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
await strapi.documents(CONTENT_TYPES.REALM_CONFIG).delete({
|
|
2212
|
+
documentId
|
|
2213
|
+
});
|
|
2214
|
+
return { success: true };
|
|
2215
|
+
},
|
|
2216
|
+
/**
|
|
2217
|
+
* Tests connection to a saved realm configuration.
|
|
2218
|
+
*
|
|
2219
|
+
* @param {string} documentId - Strapi document ID
|
|
2220
|
+
* @returns {Promise<{success: boolean, message?: string, realmDisplayName?: string}>} Test result
|
|
2221
|
+
*
|
|
2222
|
+
* @example
|
|
2223
|
+
* const result = await realmService.testConnection(documentId);
|
|
2224
|
+
* if (result.success) {
|
|
2225
|
+
* console.log(`Connected to ${result.realmDisplayName}`);
|
|
2226
|
+
* } else {
|
|
2227
|
+
* console.error(result.message);
|
|
2228
|
+
* }
|
|
2229
|
+
*/
|
|
2230
|
+
async testConnection(documentId) {
|
|
2231
|
+
const realmBasic = await this.findOne(documentId);
|
|
2232
|
+
if (!realmBasic.enabled) {
|
|
2233
|
+
return { success: false, message: ERROR_MESSAGES.REALM_DISABLED };
|
|
2234
|
+
}
|
|
2235
|
+
const realm = await this.findOneWithSecret(documentId);
|
|
2236
|
+
return this.keycloakClient.testConnection(realm);
|
|
2237
|
+
},
|
|
2238
|
+
/**
|
|
2239
|
+
* Tests connection with raw configuration data (before saving).
|
|
2240
|
+
* Useful for validating credentials during realm creation/editing.
|
|
2241
|
+
*
|
|
2242
|
+
* @param {RealmConfigData} config - Raw realm configuration
|
|
2243
|
+
* @returns {Promise<{success: boolean, message?: string, realmDisplayName?: string}>} Test result
|
|
2244
|
+
*
|
|
2245
|
+
* @example
|
|
2246
|
+
* const result = await realmService.testConnectionRaw({
|
|
2247
|
+
* serverUrl: 'https://keycloak.example.com',
|
|
2248
|
+
* realmName: 'my-realm',
|
|
2249
|
+
* clientId: 'strapi-admin',
|
|
2250
|
+
* clientSecret: 'secret'
|
|
2251
|
+
* });
|
|
2252
|
+
*/
|
|
2253
|
+
async testConnectionRaw(config2) {
|
|
2254
|
+
return this.keycloakClient.testConnection(config2);
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
const userService = ({ strapi }) => ({
|
|
2258
|
+
/**
|
|
2259
|
+
* Gets the realm service instance.
|
|
2260
|
+
* @returns {Object} Realm service
|
|
2261
|
+
* @private
|
|
2262
|
+
*/
|
|
2263
|
+
get realmService() {
|
|
2264
|
+
return strapi.plugin(PLUGIN_ID).service(SERVICES.REALM);
|
|
2265
|
+
},
|
|
2266
|
+
/**
|
|
2267
|
+
* Gets the Keycloak client service instance.
|
|
2268
|
+
* @returns {Object} Keycloak client service
|
|
2269
|
+
* @private
|
|
2270
|
+
*/
|
|
2271
|
+
get keycloakClient() {
|
|
2272
|
+
return strapi.plugin(PLUGIN_ID).service(SERVICES.KEYCLOAK_CLIENT);
|
|
2273
|
+
},
|
|
2274
|
+
/**
|
|
2275
|
+
* Gets the permission service instance.
|
|
2276
|
+
* @returns {Object} Permission service
|
|
2277
|
+
* @private
|
|
2278
|
+
*/
|
|
2279
|
+
get permissionService() {
|
|
2280
|
+
return strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
2281
|
+
},
|
|
2282
|
+
/**
|
|
2283
|
+
* Gets the audit log service instance.
|
|
2284
|
+
* @returns {Object} Audit log service
|
|
2285
|
+
* @private
|
|
2286
|
+
*/
|
|
2287
|
+
get auditLogService() {
|
|
2288
|
+
return strapi.plugin(PLUGIN_ID).service(SERVICES.AUDIT_LOG);
|
|
2289
|
+
},
|
|
2290
|
+
/**
|
|
2291
|
+
* Validates user permission and returns realm config with credentials.
|
|
2292
|
+
* This is the primary permission gate for all user operations.
|
|
2293
|
+
*
|
|
2294
|
+
* @param {string} realmId - Realm document ID
|
|
2295
|
+
* @param {StrapiUser} user - Strapi user requesting access
|
|
2296
|
+
* @param {keyof RealmPermissions} permission - Required permission
|
|
2297
|
+
* @returns {Promise<Object>} Realm config with clientSecret for API calls
|
|
2298
|
+
* @throws {Error} If realm disabled or user lacks permission
|
|
2299
|
+
* @private
|
|
2300
|
+
*
|
|
2301
|
+
* @example
|
|
2302
|
+
* const realm = await this.getRealmWithPermission(realmId, user, 'canCreate');
|
|
2303
|
+
* // Now safe to call Keycloak APIs
|
|
2304
|
+
*/
|
|
2305
|
+
async getRealmWithPermission(realmId, user, permission) {
|
|
2306
|
+
const realmBasic = await this.realmService.findOne(realmId);
|
|
2307
|
+
if (!realmBasic.enabled) {
|
|
2308
|
+
throw createSanitizedError("Realm disabled", ERROR_MESSAGES.REALM_DISABLED);
|
|
2309
|
+
}
|
|
2310
|
+
const hasPermission = await this.permissionService.canAccessRealm(user, realmId, permission);
|
|
2311
|
+
if (!hasPermission) {
|
|
2312
|
+
throw createSanitizedError(
|
|
2313
|
+
`User ${user.id} lacks ${permission} for realm ${realmId}`,
|
|
2314
|
+
ERROR_MESSAGES.REALM_ACCESS_DENIED
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
return this.realmService.findOneWithSecret(realmId);
|
|
2318
|
+
},
|
|
2319
|
+
/**
|
|
2320
|
+
* Lists users from a Keycloak realm with pagination.
|
|
2321
|
+
*
|
|
2322
|
+
* @param {string} realmId - Realm document ID
|
|
2323
|
+
* @param {Object} [options={}] - Query options
|
|
2324
|
+
* @param {string} [options.search=''] - Search query
|
|
2325
|
+
* @param {number} [options.page=1] - Page number (1-indexed)
|
|
2326
|
+
* @param {number} [options.pageSize=25] - Items per page
|
|
2327
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2328
|
+
* @returns {Promise<PaginatedUsersResult>} Paginated users
|
|
2329
|
+
*
|
|
2330
|
+
* @example
|
|
2331
|
+
* const { users, pagination } = await userService.listUsers(
|
|
2332
|
+
* realmId,
|
|
2333
|
+
* { search: 'john', page: 1, pageSize: 10 },
|
|
2334
|
+
* ctx.state.user
|
|
2335
|
+
* );
|
|
2336
|
+
*/
|
|
2337
|
+
async listUsers(realmId, { search = "", page = 1, pageSize = PAGINATION.PAGE_SIZE } = {}, strapiUser) {
|
|
2338
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canRead");
|
|
2339
|
+
const first = (page - 1) * pageSize;
|
|
2340
|
+
const [users, total] = await Promise.all([
|
|
2341
|
+
this.keycloakClient.getUsers(realm, { search, first, max: pageSize }),
|
|
2342
|
+
this.keycloakClient.countUsers(realm, { search })
|
|
2343
|
+
]);
|
|
2344
|
+
return {
|
|
2345
|
+
users,
|
|
2346
|
+
pagination: {
|
|
2347
|
+
page,
|
|
2348
|
+
pageSize,
|
|
2349
|
+
total,
|
|
2350
|
+
pageCount: Math.ceil(total / pageSize)
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
},
|
|
2354
|
+
/**
|
|
2355
|
+
* Retrieves a single user by Keycloak ID.
|
|
2356
|
+
*
|
|
2357
|
+
* @param {string} realmId - Realm document ID
|
|
2358
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
2359
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2360
|
+
* @returns {Promise<KeycloakUser>} User details
|
|
2361
|
+
*
|
|
2362
|
+
* @example
|
|
2363
|
+
* const user = await userService.getUser(realmId, 'kc-user-123', ctx.state.user);
|
|
2364
|
+
*/
|
|
2365
|
+
async getUser(realmId, keycloakUserId, strapiUser) {
|
|
2366
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canRead");
|
|
2367
|
+
return this.keycloakClient.getUserById(realm, keycloakUserId);
|
|
2368
|
+
},
|
|
2369
|
+
/**
|
|
2370
|
+
* Creates a new user in Keycloak.
|
|
2371
|
+
*
|
|
2372
|
+
* @param {string} realmId - Realm document ID
|
|
2373
|
+
* @param {Object} userData - User data
|
|
2374
|
+
* @param {string} userData.username - Required username
|
|
2375
|
+
* @param {string} [userData.email] - Email address
|
|
2376
|
+
* @param {string} [userData.firstName] - First name
|
|
2377
|
+
* @param {string} [userData.lastName] - Last name
|
|
2378
|
+
* @param {boolean} [userData.enabled=true] - Account enabled
|
|
2379
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2380
|
+
* @returns {Promise<KeycloakUser>} Created user
|
|
2381
|
+
*
|
|
2382
|
+
* @example
|
|
2383
|
+
* const user = await userService.createUser(
|
|
2384
|
+
* realmId,
|
|
2385
|
+
* { username: 'newuser', email: 'new@example.com' },
|
|
2386
|
+
* ctx.state.user
|
|
2387
|
+
* );
|
|
2388
|
+
*/
|
|
2389
|
+
async createUser(realmId, userData, strapiUser) {
|
|
2390
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canCreate");
|
|
2391
|
+
const result = await this.keycloakClient.createUser(realm, userData);
|
|
2392
|
+
await this.auditLogService.log({
|
|
2393
|
+
realmName: realm.name,
|
|
2394
|
+
realmDisplayName: realm.displayName,
|
|
2395
|
+
action: AUDIT_ACTIONS.CREATE_USER,
|
|
2396
|
+
keycloakUserId: result.userId,
|
|
2397
|
+
keycloakUsername: userData.username,
|
|
2398
|
+
details: {
|
|
2399
|
+
email: userData.email,
|
|
2400
|
+
firstName: userData.firstName,
|
|
2401
|
+
lastName: userData.lastName
|
|
2402
|
+
},
|
|
2403
|
+
user: strapiUser
|
|
2404
|
+
});
|
|
2405
|
+
if (result.userId) {
|
|
2406
|
+
return this.keycloakClient.getUserById(realm, result.userId);
|
|
2407
|
+
}
|
|
2408
|
+
return result;
|
|
2409
|
+
},
|
|
2410
|
+
/**
|
|
2411
|
+
* Updates an existing user's attributes.
|
|
2412
|
+
*
|
|
2413
|
+
* @param {string} realmId - Realm document ID
|
|
2414
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
2415
|
+
* @param {Object} userData - Fields to update
|
|
2416
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2417
|
+
* @returns {Promise<KeycloakUser>} Updated user
|
|
2418
|
+
*
|
|
2419
|
+
* @example
|
|
2420
|
+
* const user = await userService.updateUser(
|
|
2421
|
+
* realmId,
|
|
2422
|
+
* 'kc-user-123',
|
|
2423
|
+
* { firstName: 'Updated', lastName: 'Name' },
|
|
2424
|
+
* ctx.state.user
|
|
2425
|
+
* );
|
|
2426
|
+
*/
|
|
2427
|
+
async updateUser(realmId, keycloakUserId, userData, strapiUser) {
|
|
2428
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canUpdate");
|
|
2429
|
+
const currentUser = await this.keycloakClient.getUserById(realm, keycloakUserId);
|
|
2430
|
+
await this.keycloakClient.updateUser(realm, keycloakUserId, userData);
|
|
2431
|
+
await this.auditLogService.log({
|
|
2432
|
+
realmName: realm.name,
|
|
2433
|
+
realmDisplayName: realm.displayName,
|
|
2434
|
+
action: AUDIT_ACTIONS.UPDATE_USER,
|
|
2435
|
+
keycloakUserId,
|
|
2436
|
+
keycloakUsername: currentUser.username,
|
|
2437
|
+
details: { changes: userData },
|
|
2438
|
+
user: strapiUser
|
|
2439
|
+
});
|
|
2440
|
+
return this.keycloakClient.getUserById(realm, keycloakUserId);
|
|
2441
|
+
},
|
|
2442
|
+
/**
|
|
2443
|
+
* Permanently deletes a user from Keycloak.
|
|
2444
|
+
*
|
|
2445
|
+
* @param {string} realmId - Realm document ID
|
|
2446
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
2447
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2448
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
2449
|
+
*
|
|
2450
|
+
* @example
|
|
2451
|
+
* await userService.deleteUser(realmId, 'kc-user-123', ctx.state.user);
|
|
2452
|
+
*/
|
|
2453
|
+
async deleteUser(realmId, keycloakUserId, strapiUser) {
|
|
2454
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canDelete");
|
|
2455
|
+
const user = await this.keycloakClient.getUserById(realm, keycloakUserId);
|
|
2456
|
+
await this.keycloakClient.deleteUser(realm, keycloakUserId);
|
|
2457
|
+
await this.auditLogService.log({
|
|
2458
|
+
realmName: realm.name,
|
|
2459
|
+
realmDisplayName: realm.displayName,
|
|
2460
|
+
action: AUDIT_ACTIONS.DELETE_USER,
|
|
2461
|
+
keycloakUserId,
|
|
2462
|
+
keycloakUsername: user.username,
|
|
2463
|
+
details: { email: user.email },
|
|
2464
|
+
user: strapiUser
|
|
2465
|
+
});
|
|
2466
|
+
return { success: true };
|
|
2467
|
+
},
|
|
2468
|
+
/**
|
|
2469
|
+
* Resets a user's password.
|
|
2470
|
+
* Requires canResetPassword permission (separate from canUpdate for compliance).
|
|
2471
|
+
*
|
|
2472
|
+
* @param {string} realmId - Realm document ID
|
|
2473
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
2474
|
+
* @param {string} password - New password
|
|
2475
|
+
* @param {boolean} temporary - Whether password must be changed on next login
|
|
2476
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2477
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
2478
|
+
*
|
|
2479
|
+
* @example
|
|
2480
|
+
* // Set temporary password
|
|
2481
|
+
* await userService.resetPassword(realmId, userId, 'TempPass123!', true, user);
|
|
2482
|
+
*/
|
|
2483
|
+
async resetPassword(realmId, keycloakUserId, password, temporary, strapiUser) {
|
|
2484
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canResetPassword");
|
|
2485
|
+
const user = await this.keycloakClient.getUserById(realm, keycloakUserId);
|
|
2486
|
+
await this.keycloakClient.resetPassword(realm, keycloakUserId, password, temporary);
|
|
2487
|
+
await this.auditLogService.log({
|
|
2488
|
+
realmName: realm.name,
|
|
2489
|
+
realmDisplayName: realm.displayName,
|
|
2490
|
+
action: AUDIT_ACTIONS.RESET_PASSWORD,
|
|
2491
|
+
keycloakUserId,
|
|
2492
|
+
keycloakUsername: user.username,
|
|
2493
|
+
details: { temporary },
|
|
2494
|
+
user: strapiUser
|
|
2495
|
+
});
|
|
2496
|
+
return { success: true };
|
|
2497
|
+
},
|
|
2498
|
+
/**
|
|
2499
|
+
* Enables a user account.
|
|
2500
|
+
*
|
|
2501
|
+
* @param {string} realmId - Realm document ID
|
|
2502
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
2503
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2504
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
2505
|
+
*/
|
|
2506
|
+
async enableUser(realmId, keycloakUserId, strapiUser) {
|
|
2507
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canUpdate");
|
|
2508
|
+
const user = await this.keycloakClient.getUserById(realm, keycloakUserId);
|
|
2509
|
+
await this.keycloakClient.enableUser(realm, keycloakUserId);
|
|
2510
|
+
await this.auditLogService.log({
|
|
2511
|
+
realmName: realm.name,
|
|
2512
|
+
realmDisplayName: realm.displayName,
|
|
2513
|
+
action: AUDIT_ACTIONS.ENABLE_USER,
|
|
2514
|
+
keycloakUserId,
|
|
2515
|
+
keycloakUsername: user.username,
|
|
2516
|
+
user: strapiUser
|
|
2517
|
+
});
|
|
2518
|
+
return { success: true };
|
|
2519
|
+
},
|
|
2520
|
+
/**
|
|
2521
|
+
* Disables a user account.
|
|
2522
|
+
*
|
|
2523
|
+
* @param {string} realmId - Realm document ID
|
|
2524
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
2525
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2526
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
2527
|
+
*/
|
|
2528
|
+
async disableUser(realmId, keycloakUserId, strapiUser) {
|
|
2529
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canUpdate");
|
|
2530
|
+
const user = await this.keycloakClient.getUserById(realm, keycloakUserId);
|
|
2531
|
+
await this.keycloakClient.disableUser(realm, keycloakUserId);
|
|
2532
|
+
await this.auditLogService.log({
|
|
2533
|
+
realmName: realm.name,
|
|
2534
|
+
realmDisplayName: realm.displayName,
|
|
2535
|
+
action: AUDIT_ACTIONS.DISABLE_USER,
|
|
2536
|
+
keycloakUserId,
|
|
2537
|
+
keycloakUsername: user.username,
|
|
2538
|
+
user: strapiUser
|
|
2539
|
+
});
|
|
2540
|
+
return { success: true };
|
|
2541
|
+
},
|
|
2542
|
+
/**
|
|
2543
|
+
* Sends an email verification link to the user.
|
|
2544
|
+
*
|
|
2545
|
+
* @param {string} realmId - Realm document ID
|
|
2546
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
2547
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2548
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
2549
|
+
*/
|
|
2550
|
+
async sendVerificationEmail(realmId, keycloakUserId, strapiUser) {
|
|
2551
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canUpdate");
|
|
2552
|
+
const user = await this.keycloakClient.getUserById(realm, keycloakUserId);
|
|
2553
|
+
await this.keycloakClient.sendVerificationEmail(realm, keycloakUserId);
|
|
2554
|
+
await this.auditLogService.log({
|
|
2555
|
+
realmName: realm.name,
|
|
2556
|
+
realmDisplayName: realm.displayName,
|
|
2557
|
+
action: AUDIT_ACTIONS.SEND_VERIFY_EMAIL,
|
|
2558
|
+
keycloakUserId,
|
|
2559
|
+
keycloakUsername: user.username,
|
|
2560
|
+
user: strapiUser
|
|
2561
|
+
});
|
|
2562
|
+
return { success: true };
|
|
2563
|
+
},
|
|
2564
|
+
/**
|
|
2565
|
+
* Sends a password reset email to the user.
|
|
2566
|
+
* Requires canResetPassword permission.
|
|
2567
|
+
*
|
|
2568
|
+
* @param {string} realmId - Realm document ID
|
|
2569
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
2570
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2571
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
2572
|
+
*/
|
|
2573
|
+
async sendResetPasswordEmail(realmId, keycloakUserId, strapiUser) {
|
|
2574
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canResetPassword");
|
|
2575
|
+
const user = await this.keycloakClient.getUserById(realm, keycloakUserId);
|
|
2576
|
+
await this.keycloakClient.sendResetPasswordEmail(realm, keycloakUserId);
|
|
2577
|
+
await this.auditLogService.log({
|
|
2578
|
+
realmName: realm.name,
|
|
2579
|
+
realmDisplayName: realm.displayName,
|
|
2580
|
+
action: AUDIT_ACTIONS.SEND_RESET_PASSWORD_EMAIL,
|
|
2581
|
+
keycloakUserId,
|
|
2582
|
+
keycloakUsername: user.username,
|
|
2583
|
+
user: strapiUser
|
|
2584
|
+
});
|
|
2585
|
+
return { success: true };
|
|
2586
|
+
},
|
|
2587
|
+
/**
|
|
2588
|
+
* Retrieves all realm roles.
|
|
2589
|
+
*
|
|
2590
|
+
* @param {string} realmId - Realm document ID
|
|
2591
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2592
|
+
* @returns {Promise<KeycloakRole[]>} Array of realm roles
|
|
2593
|
+
*/
|
|
2594
|
+
async getRoles(realmId, strapiUser) {
|
|
2595
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canRead");
|
|
2596
|
+
return this.keycloakClient.getRealmRoles(realm);
|
|
2597
|
+
},
|
|
2598
|
+
/**
|
|
2599
|
+
* Gets roles assigned to a user.
|
|
2600
|
+
*
|
|
2601
|
+
* @param {string} realmId - Realm document ID
|
|
2602
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
2603
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2604
|
+
* @returns {Promise<KeycloakRole[]>} User's assigned roles
|
|
2605
|
+
*/
|
|
2606
|
+
async getUserRoles(realmId, keycloakUserId, strapiUser) {
|
|
2607
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canRead");
|
|
2608
|
+
return this.keycloakClient.getUserRoles(realm, keycloakUserId);
|
|
2609
|
+
},
|
|
2610
|
+
/**
|
|
2611
|
+
* Assigns roles to a user.
|
|
2612
|
+
*
|
|
2613
|
+
* @param {string} realmId - Realm document ID
|
|
2614
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
2615
|
+
* @param {KeycloakRole[]} roles - Roles to assign
|
|
2616
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2617
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
2618
|
+
*/
|
|
2619
|
+
async assignRoles(realmId, keycloakUserId, roles, strapiUser) {
|
|
2620
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canManageRoles");
|
|
2621
|
+
const user = await this.keycloakClient.getUserById(realm, keycloakUserId);
|
|
2622
|
+
await this.keycloakClient.assignRoles(realm, keycloakUserId, roles);
|
|
2623
|
+
await this.auditLogService.log({
|
|
2624
|
+
realmName: realm.name,
|
|
2625
|
+
realmDisplayName: realm.displayName,
|
|
2626
|
+
action: AUDIT_ACTIONS.ASSIGN_ROLE,
|
|
2627
|
+
keycloakUserId,
|
|
2628
|
+
keycloakUsername: user.username,
|
|
2629
|
+
details: { roles: roles.map((r) => r.name) },
|
|
2630
|
+
user: strapiUser
|
|
2631
|
+
});
|
|
2632
|
+
return { success: true };
|
|
2633
|
+
},
|
|
2634
|
+
/**
|
|
2635
|
+
* Removes roles from a user.
|
|
2636
|
+
*
|
|
2637
|
+
* @param {string} realmId - Realm document ID
|
|
2638
|
+
* @param {string} keycloakUserId - Keycloak user ID
|
|
2639
|
+
* @param {KeycloakRole[]} roles - Roles to remove
|
|
2640
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2641
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
2642
|
+
*/
|
|
2643
|
+
async removeRoles(realmId, keycloakUserId, roles, strapiUser) {
|
|
2644
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canManageRoles");
|
|
2645
|
+
const user = await this.keycloakClient.getUserById(realm, keycloakUserId);
|
|
2646
|
+
await this.keycloakClient.removeRoles(realm, keycloakUserId, roles);
|
|
2647
|
+
await this.auditLogService.log({
|
|
2648
|
+
realmName: realm.name,
|
|
2649
|
+
realmDisplayName: realm.displayName,
|
|
2650
|
+
action: AUDIT_ACTIONS.REMOVE_ROLE,
|
|
2651
|
+
keycloakUserId,
|
|
2652
|
+
keycloakUsername: user.username,
|
|
2653
|
+
details: { roles: roles.map((r) => r.name) },
|
|
2654
|
+
user: strapiUser
|
|
2655
|
+
});
|
|
2656
|
+
return { success: true };
|
|
2657
|
+
},
|
|
2658
|
+
/**
|
|
2659
|
+
* Bulk imports users from an array of user data.
|
|
2660
|
+
* Processes users sequentially to handle errors gracefully.
|
|
2661
|
+
*
|
|
2662
|
+
* @param {string} realmId - Realm document ID
|
|
2663
|
+
* @param {Object[]} users - Array of user data objects
|
|
2664
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2665
|
+
* @returns {Promise<BulkImportResult>} Import results with success/failed arrays
|
|
2666
|
+
*
|
|
2667
|
+
* @example
|
|
2668
|
+
* const result = await userService.bulkImport(realmId, [
|
|
2669
|
+
* { username: 'user1', email: 'user1@example.com' },
|
|
2670
|
+
* { username: 'user2', email: 'user2@example.com' }
|
|
2671
|
+
* ], ctx.state.user);
|
|
2672
|
+
* console.log(`Created ${result.success.length}, Failed ${result.failed.length}`);
|
|
2673
|
+
*/
|
|
2674
|
+
async bulkImport(realmId, users, strapiUser) {
|
|
2675
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canCreate");
|
|
2676
|
+
const results = {
|
|
2677
|
+
success: [],
|
|
2678
|
+
failed: []
|
|
2679
|
+
};
|
|
2680
|
+
for (const userData of users) {
|
|
2681
|
+
try {
|
|
2682
|
+
const result = await this.keycloakClient.createUser(realm, userData);
|
|
2683
|
+
results.success.push({
|
|
2684
|
+
username: userData.username,
|
|
2685
|
+
email: userData.email,
|
|
2686
|
+
userId: result.userId
|
|
2687
|
+
});
|
|
2688
|
+
} catch (err) {
|
|
2689
|
+
results.failed.push({
|
|
2690
|
+
username: userData.username,
|
|
2691
|
+
email: userData.email,
|
|
2692
|
+
error: err.sanitizedMessage || err.message
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
await this.auditLogService.log({
|
|
2697
|
+
realmName: realm.name,
|
|
2698
|
+
realmDisplayName: realm.displayName,
|
|
2699
|
+
action: AUDIT_ACTIONS.BULK_IMPORT,
|
|
2700
|
+
details: {
|
|
2701
|
+
totalAttempted: users.length,
|
|
2702
|
+
successCount: results.success.length,
|
|
2703
|
+
failedCount: results.failed.length
|
|
2704
|
+
},
|
|
2705
|
+
user: strapiUser
|
|
2706
|
+
});
|
|
2707
|
+
return results;
|
|
2708
|
+
},
|
|
2709
|
+
/**
|
|
2710
|
+
* Exports all users from a realm.
|
|
2711
|
+
* Fetches users in batches for performance.
|
|
2712
|
+
*
|
|
2713
|
+
* @param {string} realmId - Realm document ID
|
|
2714
|
+
* @param {StrapiUser} strapiUser - User making the request
|
|
2715
|
+
* @returns {Promise<KeycloakUser[]>} All users in the realm
|
|
2716
|
+
*
|
|
2717
|
+
* @example
|
|
2718
|
+
* const users = await userService.exportUsers(realmId, ctx.state.user);
|
|
2719
|
+
* // Convert to CSV or JSON for download
|
|
2720
|
+
*/
|
|
2721
|
+
async exportUsers(realmId, strapiUser) {
|
|
2722
|
+
const realm = await this.getRealmWithPermission(realmId, strapiUser, "canRead");
|
|
2723
|
+
const allUsers = [];
|
|
2724
|
+
let first = 0;
|
|
2725
|
+
const max = PAGINATION.EXPORT_BATCH_SIZE;
|
|
2726
|
+
let hasMore = true;
|
|
2727
|
+
while (hasMore) {
|
|
2728
|
+
const users = await this.keycloakClient.getUsers(realm, {
|
|
2729
|
+
first,
|
|
2730
|
+
max,
|
|
2731
|
+
briefRepresentation: false
|
|
2732
|
+
});
|
|
2733
|
+
allUsers.push(...users);
|
|
2734
|
+
first += max;
|
|
2735
|
+
hasMore = users.length === max;
|
|
2736
|
+
}
|
|
2737
|
+
return allUsers;
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
const services = {
|
|
2741
|
+
"keycloak-client": keycloakClientService,
|
|
2742
|
+
"audit-log": auditLogService,
|
|
2743
|
+
permission: permissionService,
|
|
2744
|
+
realm: realmService,
|
|
2745
|
+
user: userService
|
|
2746
|
+
};
|
|
2747
|
+
const isAuthenticated = (policyContext) => {
|
|
2748
|
+
return Boolean(policyContext.state.user);
|
|
2749
|
+
};
|
|
2750
|
+
const canAccessRealm = async (policyContext, config2, { strapi }) => {
|
|
2751
|
+
const { realmId, id } = policyContext.params;
|
|
2752
|
+
const user = policyContext.state.user;
|
|
2753
|
+
const targetRealmId = realmId || id;
|
|
2754
|
+
if (!user || !targetRealmId) {
|
|
2755
|
+
return false;
|
|
2756
|
+
}
|
|
2757
|
+
const permissionService2 = strapi.plugin(PLUGIN_ID).service(SERVICES.PERMISSION);
|
|
2758
|
+
if (permissionService2.isSuperAdmin(user)) {
|
|
2759
|
+
return true;
|
|
2760
|
+
}
|
|
2761
|
+
const permission = config2?.permission || "canRead";
|
|
2762
|
+
return permissionService2.canAccessRealm(user, targetRealmId, permission);
|
|
2763
|
+
};
|
|
2764
|
+
const policies = {
|
|
2765
|
+
"is-authenticated": isAuthenticated,
|
|
2766
|
+
"can-access-realm": canAccessRealm
|
|
2767
|
+
};
|
|
2768
|
+
const admin = [
|
|
2769
|
+
// ==================== REALM MANAGEMENT ====================
|
|
2770
|
+
// Get all realms (filtered by user access)
|
|
2771
|
+
{
|
|
2772
|
+
method: "GET",
|
|
2773
|
+
path: "/realms",
|
|
2774
|
+
handler: "realm.find",
|
|
2775
|
+
config: { policies: [] }
|
|
2776
|
+
},
|
|
2777
|
+
// Test connection with raw config (before saving)
|
|
2778
|
+
{
|
|
2779
|
+
method: "POST",
|
|
2780
|
+
path: "/realms/test-connection",
|
|
2781
|
+
handler: "realm.testConnectionRaw",
|
|
2782
|
+
config: { policies: [] }
|
|
2783
|
+
},
|
|
2784
|
+
// Get a single realm
|
|
2785
|
+
{
|
|
2786
|
+
method: "GET",
|
|
2787
|
+
path: "/realms/:id",
|
|
2788
|
+
handler: "realm.findOne",
|
|
2789
|
+
config: { policies: [] }
|
|
2790
|
+
},
|
|
2791
|
+
// Create a new realm
|
|
2792
|
+
{
|
|
2793
|
+
method: "POST",
|
|
2794
|
+
path: "/realms",
|
|
2795
|
+
handler: "realm.create",
|
|
2796
|
+
config: { policies: [] }
|
|
2797
|
+
},
|
|
2798
|
+
// Update a realm
|
|
2799
|
+
{
|
|
2800
|
+
method: "PUT",
|
|
2801
|
+
path: "/realms/:id",
|
|
2802
|
+
handler: "realm.update",
|
|
2803
|
+
config: { policies: [] }
|
|
2804
|
+
},
|
|
2805
|
+
// Delete a realm
|
|
2806
|
+
{
|
|
2807
|
+
method: "DELETE",
|
|
2808
|
+
path: "/realms/:id",
|
|
2809
|
+
handler: "realm.delete",
|
|
2810
|
+
config: { policies: [] }
|
|
2811
|
+
},
|
|
2812
|
+
// Test realm connection
|
|
2813
|
+
{
|
|
2814
|
+
method: "POST",
|
|
2815
|
+
path: "/realms/:id/test",
|
|
2816
|
+
handler: "realm.testConnection",
|
|
2817
|
+
config: { policies: [] }
|
|
2818
|
+
},
|
|
2819
|
+
// ==================== REALM ADMIN ASSIGNMENTS ====================
|
|
2820
|
+
// Get admins for a realm
|
|
2821
|
+
{
|
|
2822
|
+
method: "GET",
|
|
2823
|
+
path: "/realms/:id/admins",
|
|
2824
|
+
handler: "realm.getAdmins",
|
|
2825
|
+
config: { policies: [] }
|
|
2826
|
+
},
|
|
2827
|
+
// Add admin to a realm
|
|
2828
|
+
{
|
|
2829
|
+
method: "POST",
|
|
2830
|
+
path: "/realms/:id/admins",
|
|
2831
|
+
handler: "realm.addAdmin",
|
|
2832
|
+
config: { policies: [] }
|
|
2833
|
+
},
|
|
2834
|
+
// Update admin permissions
|
|
2835
|
+
{
|
|
2836
|
+
method: "PUT",
|
|
2837
|
+
path: "/realms/:id/admins/:adminId",
|
|
2838
|
+
handler: "realm.updateAdmin",
|
|
2839
|
+
config: { policies: [] }
|
|
2840
|
+
},
|
|
2841
|
+
// Remove admin from realm
|
|
2842
|
+
{
|
|
2843
|
+
method: "DELETE",
|
|
2844
|
+
path: "/realms/:id/admins/:adminId",
|
|
2845
|
+
handler: "realm.removeAdmin",
|
|
2846
|
+
config: { policies: [] }
|
|
2847
|
+
},
|
|
2848
|
+
// ==================== KEYCLOAK USERS ====================
|
|
2849
|
+
// List users in a realm
|
|
2850
|
+
{
|
|
2851
|
+
method: "GET",
|
|
2852
|
+
path: "/realms/:realmId/users",
|
|
2853
|
+
handler: "user.find",
|
|
2854
|
+
config: { policies: [] }
|
|
2855
|
+
},
|
|
2856
|
+
// Export users
|
|
2857
|
+
{
|
|
2858
|
+
method: "GET",
|
|
2859
|
+
path: "/realms/:realmId/users/export",
|
|
2860
|
+
handler: "user.exportUsers",
|
|
2861
|
+
config: { policies: [] }
|
|
2862
|
+
},
|
|
2863
|
+
// Bulk import users
|
|
2864
|
+
{
|
|
2865
|
+
method: "POST",
|
|
2866
|
+
path: "/realms/:realmId/users/import",
|
|
2867
|
+
handler: "user.bulkImport",
|
|
2868
|
+
config: { policies: [] }
|
|
2869
|
+
},
|
|
2870
|
+
// Get a single user
|
|
2871
|
+
{
|
|
2872
|
+
method: "GET",
|
|
2873
|
+
path: "/realms/:realmId/users/:id",
|
|
2874
|
+
handler: "user.findOne",
|
|
2875
|
+
config: { policies: [] }
|
|
2876
|
+
},
|
|
2877
|
+
// Create a user
|
|
2878
|
+
{
|
|
2879
|
+
method: "POST",
|
|
2880
|
+
path: "/realms/:realmId/users",
|
|
2881
|
+
handler: "user.create",
|
|
2882
|
+
config: { policies: [] }
|
|
2883
|
+
},
|
|
2884
|
+
// Update a user
|
|
2885
|
+
{
|
|
2886
|
+
method: "PUT",
|
|
2887
|
+
path: "/realms/:realmId/users/:id",
|
|
2888
|
+
handler: "user.update",
|
|
2889
|
+
config: { policies: [] }
|
|
2890
|
+
},
|
|
2891
|
+
// Delete a user
|
|
2892
|
+
{
|
|
2893
|
+
method: "DELETE",
|
|
2894
|
+
path: "/realms/:realmId/users/:id",
|
|
2895
|
+
handler: "user.delete",
|
|
2896
|
+
config: { policies: [] }
|
|
2897
|
+
},
|
|
2898
|
+
// ==================== USER ACTIONS ====================
|
|
2899
|
+
// Reset password
|
|
2900
|
+
{
|
|
2901
|
+
method: "POST",
|
|
2902
|
+
path: "/realms/:realmId/users/:id/reset-password",
|
|
2903
|
+
handler: "user.resetPassword",
|
|
2904
|
+
config: { policies: [] }
|
|
2905
|
+
},
|
|
2906
|
+
// Enable user
|
|
2907
|
+
{
|
|
2908
|
+
method: "POST",
|
|
2909
|
+
path: "/realms/:realmId/users/:id/enable",
|
|
2910
|
+
handler: "user.enable",
|
|
2911
|
+
config: { policies: [] }
|
|
2912
|
+
},
|
|
2913
|
+
// Disable user
|
|
2914
|
+
{
|
|
2915
|
+
method: "POST",
|
|
2916
|
+
path: "/realms/:realmId/users/:id/disable",
|
|
2917
|
+
handler: "user.disable",
|
|
2918
|
+
config: { policies: [] }
|
|
2919
|
+
},
|
|
2920
|
+
// Send verification email
|
|
2921
|
+
{
|
|
2922
|
+
method: "POST",
|
|
2923
|
+
path: "/realms/:realmId/users/:id/send-verify-email",
|
|
2924
|
+
handler: "user.sendVerifyEmail",
|
|
2925
|
+
config: { policies: [] }
|
|
2926
|
+
},
|
|
2927
|
+
// Send password reset email
|
|
2928
|
+
{
|
|
2929
|
+
method: "POST",
|
|
2930
|
+
path: "/realms/:realmId/users/:id/send-reset-password-email",
|
|
2931
|
+
handler: "user.sendResetPasswordEmail",
|
|
2932
|
+
config: { policies: [] }
|
|
2933
|
+
},
|
|
2934
|
+
// ==================== ROLES ====================
|
|
2935
|
+
// Get realm roles
|
|
2936
|
+
{
|
|
2937
|
+
method: "GET",
|
|
2938
|
+
path: "/realms/:realmId/roles",
|
|
2939
|
+
handler: "user.getRoles",
|
|
2940
|
+
config: { policies: [] }
|
|
2941
|
+
},
|
|
2942
|
+
// Get user's roles
|
|
2943
|
+
{
|
|
2944
|
+
method: "GET",
|
|
2945
|
+
path: "/realms/:realmId/users/:id/roles",
|
|
2946
|
+
handler: "user.getUserRoles",
|
|
2947
|
+
config: { policies: [] }
|
|
2948
|
+
},
|
|
2949
|
+
// Assign roles to user
|
|
2950
|
+
{
|
|
2951
|
+
method: "POST",
|
|
2952
|
+
path: "/realms/:realmId/users/:id/roles",
|
|
2953
|
+
handler: "user.assignRoles",
|
|
2954
|
+
config: { policies: [] }
|
|
2955
|
+
},
|
|
2956
|
+
// Remove roles from user
|
|
2957
|
+
{
|
|
2958
|
+
method: "DELETE",
|
|
2959
|
+
path: "/realms/:realmId/users/:id/roles",
|
|
2960
|
+
handler: "user.removeRoles",
|
|
2961
|
+
config: { policies: [] }
|
|
2962
|
+
},
|
|
2963
|
+
// ==================== AUDIT LOGS ====================
|
|
2964
|
+
// Get all audit logs
|
|
2965
|
+
{
|
|
2966
|
+
method: "GET",
|
|
2967
|
+
path: "/audit-logs",
|
|
2968
|
+
handler: "audit.find",
|
|
2969
|
+
config: { policies: [] }
|
|
2970
|
+
},
|
|
2971
|
+
// Get audit logs by realm
|
|
2972
|
+
{
|
|
2973
|
+
method: "GET",
|
|
2974
|
+
path: "/audit-logs/realm/:realmId",
|
|
2975
|
+
handler: "audit.findByRealm",
|
|
2976
|
+
config: { policies: [] }
|
|
2977
|
+
},
|
|
2978
|
+
// Get audit logs by Keycloak user
|
|
2979
|
+
{
|
|
2980
|
+
method: "GET",
|
|
2981
|
+
path: "/audit-logs/user/:keycloakUserId",
|
|
2982
|
+
handler: "audit.findByUser",
|
|
2983
|
+
config: { policies: [] }
|
|
2984
|
+
}
|
|
2985
|
+
];
|
|
2986
|
+
const routes = {
|
|
2987
|
+
admin: {
|
|
2988
|
+
type: "admin",
|
|
2989
|
+
routes: admin
|
|
2990
|
+
}
|
|
2991
|
+
};
|
|
2992
|
+
const index = {
|
|
2993
|
+
bootstrap,
|
|
2994
|
+
register,
|
|
2995
|
+
destroy,
|
|
2996
|
+
config,
|
|
2997
|
+
contentTypes,
|
|
2998
|
+
controllers,
|
|
2999
|
+
services,
|
|
3000
|
+
policies,
|
|
3001
|
+
routes
|
|
3002
|
+
};
|
|
3003
|
+
module.exports = index;
|