@culturefy/shared 1.0.28 → 1.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/build/cjs/dto/index.js +8 -0
  2. package/build/cjs/dto/index.js.map +1 -1
  3. package/build/cjs/index.js +6 -0
  4. package/build/cjs/index.js.map +1 -1
  5. package/build/cjs/interfaces/index.js +16 -0
  6. package/build/cjs/interfaces/index.js.map +1 -0
  7. package/build/cjs/interfaces/keycloak.js +2 -0
  8. package/build/cjs/interfaces/keycloak.js.map +1 -0
  9. package/build/cjs/interfaces/user.js +2 -0
  10. package/build/cjs/interfaces/user.js.map +1 -0
  11. package/build/cjs/middlewares/index.js +10 -0
  12. package/build/cjs/middlewares/index.js.map +1 -0
  13. package/build/cjs/middlewares/token-validation.js +485 -0
  14. package/build/cjs/middlewares/token-validation.js.map +1 -0
  15. package/build/cjs/models/user.model.js +507 -0
  16. package/build/cjs/models/user.model.js.map +1 -0
  17. package/build/cjs/service/keycloak.service.js +1003 -0
  18. package/build/cjs/service/keycloak.service.js.map +1 -0
  19. package/build/cjs/service/user.service.js +95 -0
  20. package/build/cjs/service/user.service.js.map +1 -0
  21. package/build/cjs/utils/index.js +6 -0
  22. package/build/cjs/utils/index.js.map +1 -1
  23. package/build/cjs/utils/jwt.js +14 -0
  24. package/build/cjs/utils/jwt.js.map +1 -0
  25. package/build/cjs/utils/response.js +19 -0
  26. package/build/cjs/utils/response.js.map +1 -1
  27. package/build/esm/dto/index.js +1 -1
  28. package/build/esm/dto/index.js.map +1 -1
  29. package/build/esm/index.js +1 -0
  30. package/build/esm/index.js.map +1 -1
  31. package/build/esm/interfaces/index.js +3 -0
  32. package/build/esm/interfaces/index.js.map +1 -0
  33. package/build/esm/interfaces/keycloak.js +2 -0
  34. package/build/esm/interfaces/keycloak.js.map +1 -0
  35. package/build/esm/interfaces/user.js +2 -0
  36. package/build/esm/interfaces/user.js.map +1 -0
  37. package/build/esm/middlewares/index.js +2 -0
  38. package/build/esm/middlewares/index.js.map +1 -0
  39. package/build/esm/middlewares/token-validation.js +481 -0
  40. package/build/esm/middlewares/token-validation.js.map +1 -0
  41. package/build/esm/models/user.model.js +497 -0
  42. package/build/esm/models/user.model.js.map +1 -0
  43. package/build/esm/service/keycloak.service.js +996 -0
  44. package/build/esm/service/keycloak.service.js.map +1 -0
  45. package/build/esm/service/user.service.js +91 -0
  46. package/build/esm/service/user.service.js.map +1 -0
  47. package/build/esm/utils/index.js +1 -0
  48. package/build/esm/utils/index.js.map +1 -1
  49. package/build/esm/utils/jwt.js +10 -0
  50. package/build/esm/utils/jwt.js.map +1 -0
  51. package/build/esm/utils/response.js +18 -0
  52. package/build/esm/utils/response.js.map +1 -1
  53. package/build/package.json +13 -2
  54. package/build/src/dto/index.d.ts +1 -0
  55. package/build/src/dto/index.js +3 -0
  56. package/build/src/dto/index.js.map +1 -1
  57. package/build/src/index.d.ts +1 -0
  58. package/build/src/index.js +1 -0
  59. package/build/src/index.js.map +1 -1
  60. package/build/src/interfaces/index.d.ts +2 -0
  61. package/build/src/interfaces/index.js +6 -0
  62. package/build/src/interfaces/index.js.map +1 -0
  63. package/build/src/interfaces/keycloak.d.ts +151 -0
  64. package/build/src/interfaces/keycloak.js +3 -0
  65. package/build/src/interfaces/keycloak.js.map +1 -0
  66. package/build/src/interfaces/user.d.ts +5 -0
  67. package/build/src/interfaces/user.js +3 -0
  68. package/build/src/interfaces/user.js.map +1 -0
  69. package/build/src/middlewares/index.d.ts +1 -0
  70. package/build/src/middlewares/index.js +5 -0
  71. package/build/src/middlewares/index.js.map +1 -0
  72. package/build/src/middlewares/token-validation.d.ts +17 -0
  73. package/build/src/middlewares/token-validation.js +329 -0
  74. package/build/src/middlewares/token-validation.js.map +1 -0
  75. package/build/src/models/user.model.d.ts +193 -0
  76. package/build/src/models/user.model.js +401 -0
  77. package/build/src/models/user.model.js.map +1 -0
  78. package/build/src/service/keycloak.service.d.ts +88 -0
  79. package/build/src/service/keycloak.service.js +1059 -0
  80. package/build/src/service/keycloak.service.js.map +1 -0
  81. package/build/src/service/user.service.d.ts +11 -0
  82. package/build/src/service/user.service.js +118 -0
  83. package/build/src/service/user.service.js.map +1 -0
  84. package/build/src/utils/index.d.ts +1 -0
  85. package/build/src/utils/index.js +1 -0
  86. package/build/src/utils/index.js.map +1 -1
  87. package/build/src/utils/jwt.d.ts +1 -0
  88. package/build/src/utils/jwt.js +14 -0
  89. package/build/src/utils/jwt.js.map +1 -0
  90. package/build/src/utils/response.d.ts +1 -0
  91. package/build/src/utils/response.js +19 -0
  92. package/build/src/utils/response.js.map +1 -1
  93. package/package.json +13 -2
  94. package/src/dto/index.ts +1 -0
  95. package/src/index.ts +2 -1
  96. package/src/interfaces/index.ts +2 -0
  97. package/src/interfaces/keycloak.ts +161 -0
  98. package/src/interfaces/user.ts +5 -0
  99. package/src/middlewares/index.ts +1 -0
  100. package/src/middlewares/token-validation.ts +456 -0
  101. package/src/models/user.model.ts +488 -0
  102. package/src/service/keycloak.service.ts +1104 -0
  103. package/src/service/user.service.ts +99 -0
  104. package/src/utils/index.ts +2 -1
  105. package/src/utils/jwt.ts +10 -0
  106. package/src/utils/response.ts +19 -0
@@ -0,0 +1,996 @@
1
+ // keycloak-admin.service.ts
2
+ import axios from 'axios';
3
+ export class KeycloakAdminService {
4
+ constructor(context, config) {
5
+ this.context = void 0;
6
+ this.baseUrl = void 0;
7
+ this.adminClientId = void 0;
8
+ this.adminClientSecret = void 0;
9
+ this.axiosInstance = void 0;
10
+ this.accessToken = null;
11
+ this.context = context;
12
+ if (!config.baseUrl || !config.adminClientId || !config.adminClientSecret) {
13
+ this.context.error("Missing required configuration");
14
+ throw new Error('Missing required configuration');
15
+ }
16
+ this.baseUrl = config.baseUrl;
17
+ this.adminClientId = config.adminClientId;
18
+ this.adminClientSecret = config.adminClientSecret;
19
+ try {
20
+ this.axiosInstance = axios.create({
21
+ baseURL: this.baseUrl
22
+ });
23
+ } catch (error) {
24
+ this.context.error("Error in createAxiosInstance", error);
25
+ throw error;
26
+ }
27
+ }
28
+ async authenticate() {
29
+ try {
30
+ const params = new URLSearchParams();
31
+ params.append('grant_type', 'client_credentials');
32
+ params.append('client_id', this.adminClientId);
33
+ params.append('client_secret', this.adminClientSecret);
34
+ this.context.log(`Params: ${JSON.stringify(params)}`);
35
+ const response = await this.axiosInstance.post('/realms/master/protocol/openid-connect/token', params, {
36
+ headers: {
37
+ 'Content-Type': 'application/x-www-form-urlencoded'
38
+ }
39
+ });
40
+ if (response.status !== 200) {
41
+ throw new Error(`Failed to authenticate: ${JSON.stringify(response.data)}`);
42
+ }
43
+ // this.context.log(`Response: ${JSON.stringify(response.data)}`);
44
+
45
+ const data = response.data;
46
+ this.accessToken = data.access_token;
47
+ this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${this.accessToken}`;
48
+ return data;
49
+ } catch (error) {
50
+ this.context.error("Error in authenticate", error);
51
+ throw error;
52
+ }
53
+ }
54
+ async authenticateRelmClient(realm, clientId) {
55
+ try {
56
+ const clientUUID = await this.getClientUUID(realm, clientId);
57
+ if (!clientUUID) {
58
+ throw new Error(`Client UUID for "${clientId}" not found`);
59
+ }
60
+ const clientSecret = await this.getClientSecret(realm, clientId);
61
+ if (!clientSecret) {
62
+ throw new Error(`Client secret for "${clientId}" not found`);
63
+ }
64
+ const params = new URLSearchParams();
65
+ params.append('grant_type', 'client_credentials');
66
+ params.append('client_id', clientId);
67
+ params.append('client_secret', clientSecret);
68
+ const response = await this.axiosInstance.post(`/realms/${realm}/protocol/openid-connect/token`, params, {
69
+ headers: {
70
+ 'Content-Type': 'application/x-www-form-urlencoded'
71
+ }
72
+ });
73
+ if (response.status !== 200) {
74
+ throw new Error(`Failed to authenticate realm client: ${JSON.stringify(response.data)}`);
75
+ }
76
+ // this.context.log(`Response: ${JSON.stringify(response.data)}`);
77
+
78
+ const data = response.data;
79
+ this.accessToken = data.access_token;
80
+ this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${this.accessToken}`;
81
+ return data;
82
+ } catch (error) {
83
+ this.context.error("Error in authenticateRelmClient", error);
84
+ throw error;
85
+ }
86
+ }
87
+ async createRealm(realm) {
88
+ try {
89
+ var _realm$enabled;
90
+ this.context.log(`Realm: ${JSON.stringify(realm)}`);
91
+ await this.authenticate();
92
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
93
+ const response = await this.axiosInstance.post('/admin/realms', {
94
+ realm: realm.realm,
95
+ id: realm.realm,
96
+ displayName: realm.displayName || realm.realm,
97
+ enabled: (_realm$enabled = realm.enabled) != null ? _realm$enabled : true,
98
+ sslRequired: 'external',
99
+ bruteForceProtected: true,
100
+ eventsEnabled: false
101
+ });
102
+ if (response.status !== 201) {
103
+ throw new Error(`Failed to create realm: ${JSON.stringify(response.data)}`);
104
+ }
105
+ this.context.log(`Response: Successfully created realm`);
106
+ return;
107
+ } catch (error) {
108
+ this.context.error("Error in createRealm", error);
109
+ throw error;
110
+ }
111
+ }
112
+ async createClient(realm, client) {
113
+ try {
114
+ var _client$publicClient, _client$directAccessG, _client$standardFlowE, _client$implicitFlowE, _client$serviceAccoun, _client$authorization, _client$bearerOnly, _client$consentRequir, _client$fullScopeAllo;
115
+ this.context.log(`Realm name: ${realm}`);
116
+ this.context.log(`Creating client: ${JSON.stringify(client)}`);
117
+ await this.authenticate();
118
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
119
+ const response = await this.axiosInstance.post(`/admin/realms/${realm}/clients`, {
120
+ clientId: client.clientId,
121
+ name: client.name,
122
+ description: client.description,
123
+ enabled: client.enabled,
124
+ protocol: client.protocol || 'openid-connect',
125
+ publicClient: (_client$publicClient = client.publicClient) != null ? _client$publicClient : false,
126
+ secret: client.secret || null,
127
+ directAccessGrantsEnabled: (_client$directAccessG = client.directAccessGrantsEnabled) != null ? _client$directAccessG : true,
128
+ standardFlowEnabled: (_client$standardFlowE = client.standardFlowEnabled) != null ? _client$standardFlowE : true,
129
+ implicitFlowEnabled: (_client$implicitFlowE = client.implicitFlowEnabled) != null ? _client$implicitFlowE : false,
130
+ serviceAccountsEnabled: (_client$serviceAccoun = client.serviceAccountsEnabled) != null ? _client$serviceAccoun : true,
131
+ authorizationServicesEnabled: (_client$authorization = client.authorizationServicesEnabled) != null ? _client$authorization : true,
132
+ redirectUris: client.redirectUris || [],
133
+ webOrigins: client.webOrigins || [],
134
+ bearerOnly: (_client$bearerOnly = client.bearerOnly) != null ? _client$bearerOnly : false,
135
+ consentRequired: (_client$consentRequir = client.consentRequired) != null ? _client$consentRequir : false,
136
+ fullScopeAllowed: (_client$fullScopeAllo = client.fullScopeAllowed) != null ? _client$fullScopeAllo : true,
137
+ attributes: client.attributes || {
138
+ tls_client_certificate_bound_access_tokens: "false",
139
+ client_credentials_use_refresh_token: "true"
140
+ }
141
+ });
142
+ if (response.status !== 201) {
143
+ throw new Error(`Failed to create client: ${JSON.stringify(response.data)}`);
144
+ }
145
+ this.context.log(`Response: ${JSON.stringify(response.data)}`);
146
+ return;
147
+ } catch (error) {
148
+ this.context.error("Error in createClient", error);
149
+ throw error;
150
+ }
151
+ }
152
+ async getClient(realm, clientId, clientUUID) {
153
+ try {
154
+ this.context.log(`Realm: ${realm}`);
155
+ this.context.log(`Client ID: ${clientId}`);
156
+ await this.authenticateRelmClient(realm, clientId);
157
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
158
+ const response = await this.axiosInstance.get(`/admin/realms/${realm}/clients/${clientUUID}`);
159
+ if (response.status !== 200) {
160
+ throw new Error(`Failed to get client: ${JSON.stringify(response.data)}`);
161
+ }
162
+ this.context.log(`(getClient) Client: ${JSON.stringify(response.data)}`);
163
+ const data = response.data;
164
+ return data;
165
+ } catch (error) {
166
+ this.context.error("Error in getClient", error);
167
+ throw error;
168
+ }
169
+ }
170
+ async getClientUUID(realm, clientId) {
171
+ try {
172
+ this.context.log(`Realm: ${realm}`);
173
+ this.context.log(`Client ID: ${clientId}`);
174
+ await this.authenticate();
175
+ // this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
176
+
177
+ const response = await this.axiosInstance.get(`/admin/realms/${realm}/clients`, {
178
+ params: {
179
+ clientId
180
+ }
181
+ });
182
+ if (response.status !== 200) {
183
+ throw new Error(`Failed to get clients: ${JSON.stringify(response.data)}`);
184
+ }
185
+ this.context.log(`Clients: ${JSON.stringify(response.data)}`);
186
+ const data = response.data;
187
+ const client = data.find(c => c.clientId == clientId);
188
+ this.context.log(`(getClientUUID) Client: ${JSON.stringify(client)}`);
189
+ if (!client) {
190
+ throw new Error(`Client "${clientId}" not found`);
191
+ }
192
+ return client.id;
193
+ } catch (error) {
194
+ this.context.error("Error in getClientUUID", error);
195
+ throw error;
196
+ }
197
+ }
198
+ async getServiceAccountUserId(realm, clientUUID) {
199
+ try {
200
+ await this.authenticate();
201
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
202
+ const response = await this.axiosInstance.get(`/admin/realms/${realm}/clients/${clientUUID}/service-account-user`);
203
+ if (response.status !== 200) {
204
+ throw new Error(`Failed to get service account user: ${JSON.stringify(response.data)}`);
205
+ }
206
+ this.context.log(`Service account user: ${JSON.stringify(response.data)}`);
207
+ return response.data.id;
208
+ } catch (error) {
209
+ this.context.error("Error in getServiceAccountUserId", error);
210
+ throw error;
211
+ }
212
+ }
213
+ async getAllRoles(realm, clientUUID) {
214
+ try {
215
+ await this.authenticate();
216
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
217
+ const response = await this.axiosInstance.get(`/admin/realms/${realm}/clients/${clientUUID}/roles`);
218
+ if (response.status !== 200) {
219
+ throw new Error(`Failed to get all roles: ${JSON.stringify(response.data)}`);
220
+ }
221
+ this.context.log(`All roles: ${JSON.stringify(response.data)}`);
222
+ const data = response.data;
223
+ return data;
224
+ } catch (error) {
225
+ this.context.error("Error in getAllRoles", error);
226
+ throw error;
227
+ }
228
+ }
229
+ async getAssignedRoles(realm, userId, clientUUID) {
230
+ try {
231
+ await this.authenticate();
232
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
233
+ const response = await this.axiosInstance.get(`/admin/realms/${realm}/users/${userId}/role-mappings/clients/${clientUUID}`);
234
+ if (response.status !== 200) {
235
+ throw new Error(`Failed to get assigned roles: ${JSON.stringify(response.data)}`);
236
+ }
237
+ this.context.log(`Assigned roles: ${JSON.stringify(response.data)}`);
238
+ const data = response.data;
239
+ return data;
240
+ } catch (error) {
241
+ this.context.error("Error in getAssignedRoles", error);
242
+ throw error;
243
+ }
244
+ }
245
+ async assignRolesToServiceAccount(realm, userId, clientUUID, roles) {
246
+ try {
247
+ await this.authenticate();
248
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
249
+
250
+ // According to Keycloak REST API documentation, we need to send role objects, not just IDs
251
+ const roleMappings = roles.map(role => ({
252
+ id: role.id,
253
+ name: role.name,
254
+ description: role.description,
255
+ composite: role.composite,
256
+ clientRole: role.clientRole,
257
+ containerId: role.containerId
258
+ }));
259
+ const response = await this.axiosInstance.post(`/admin/realms/${realm}/users/${userId}/role-mappings/clients/${clientUUID}`, roleMappings);
260
+ if (response.status !== 204) {
261
+ throw new Error(`Failed to assign roles to service account: ${JSON.stringify(response.data)}`);
262
+ }
263
+ this.context.log(`Response: ${JSON.stringify(response.data)}`);
264
+ return;
265
+ } catch (error) {
266
+ this.context.error("Error in assignRolesToServiceAccount", error);
267
+ throw error;
268
+ }
269
+ }
270
+ async assignAllRealmMgmtRolesToClient(realm, clientId) {
271
+ try {
272
+ this.context.log(`🔍 Getting UUID for clientId = ${clientId}`);
273
+ const clientUUID = await this.getClientUUID(realm, clientId);
274
+ this.context.log(`🔍 Getting UUID for realm-management`);
275
+ const realmMgmtUUID = await this.getClientUUID(realm, "realm-management");
276
+ this.context.log(`🔍 Getting service account user for ${clientId}`);
277
+ const serviceUserId = await this.getServiceAccountUserId(realm, clientUUID);
278
+ this.context.log(`🔍 Fetching all roles from realm-management`);
279
+ const allRoles = await this.getAllRoles(realm, realmMgmtUUID);
280
+ this.context.log(`🔍 Fetching already assigned roles`);
281
+ const assignedRoles = await this.getAssignedRoles(realm, serviceUserId, realmMgmtUUID);
282
+ const assignedNames = new Set(assignedRoles.map(r => r.name));
283
+ const unassignedRoles = allRoles.filter(r => !assignedNames.has(r.name));
284
+ this.context.log(`✅ Found ${unassignedRoles.length} unassigned roles`);
285
+ if (unassignedRoles.length) {
286
+ this.context.log(`🚀 Assigning roles to service account`);
287
+ await this.assignRolesToServiceAccount(realm, serviceUserId, realmMgmtUUID, unassignedRoles);
288
+ this.context.log("✅ Roles successfully assigned!");
289
+ } else {
290
+ this.context.log("ℹ️ All roles are already assigned.");
291
+ }
292
+ } catch (error) {
293
+ this.context.error("Error in assignAllRealmMgmtRolesToClient", error);
294
+ throw error;
295
+ }
296
+ }
297
+ async assignAllAccountMgmtRolesToClient(realm, clientId) {
298
+ try {
299
+ this.context.log(`🔍 Getting UUID for clientId = ${clientId}`);
300
+ const clientUUID = await this.getClientUUID(realm, clientId);
301
+ this.context.log(`🔍 Getting UUID for account`);
302
+ const accountMgmtUUID = await this.getClientUUID(realm, "account");
303
+ this.context.log(`🔍 Getting service account user for ${clientId}`);
304
+ const serviceUserId = await this.getServiceAccountUserId(realm, clientUUID);
305
+ this.context.log(`🔍 Fetching all roles from account`);
306
+ const allRoles = await this.getAllRoles(realm, accountMgmtUUID);
307
+ this.context.log(`🔍 Fetching already assigned roles`);
308
+ const assignedRoles = await this.getAssignedRoles(realm, serviceUserId, accountMgmtUUID);
309
+ const assignedNames = new Set(assignedRoles.map(r => r.name));
310
+ const unassignedRoles = allRoles.filter(r => !assignedNames.has(r.name));
311
+ this.context.log(`✅ Found ${unassignedRoles.length} unassigned roles`);
312
+ if (unassignedRoles.length) {
313
+ this.context.log(`🚀 Assigning roles to service account`);
314
+ await this.assignRolesToServiceAccount(realm, serviceUserId, accountMgmtUUID, unassignedRoles);
315
+ this.context.log("✅ Roles successfully assigned!");
316
+ } else {
317
+ this.context.log("ℹ️ All roles are already assigned.");
318
+ }
319
+ } catch (error) {
320
+ this.context.error("Error in assignAllAccountMgmtRolesToClient", error);
321
+ throw error;
322
+ }
323
+ }
324
+ async assignAllBrokerMgmtRolesToClient(realm, clientId) {
325
+ try {
326
+ this.context.log(`🔍 Getting UUID for clientId = ${clientId}`);
327
+ const clientUUID = await this.getClientUUID(realm, clientId);
328
+ this.context.log(`🔍 Getting UUID for broker`);
329
+ const brokerMgmtUUID = await this.getClientUUID(realm, "broker");
330
+ this.context.log(`🔍 Getting service broker user for ${clientId}`);
331
+ const serviceUserId = await this.getServiceAccountUserId(realm, clientUUID);
332
+ this.context.log(`🔍 Fetching all roles from broker`);
333
+ const allRoles = await this.getAllRoles(realm, brokerMgmtUUID);
334
+ this.context.log(`🔍 Fetching already assigned roles`);
335
+ const assignedRoles = await this.getAssignedRoles(realm, serviceUserId, brokerMgmtUUID);
336
+ const assignedNames = new Set(assignedRoles.map(r => r.name));
337
+ const unassignedRoles = allRoles.filter(r => !assignedNames.has(r.name));
338
+ this.context.log(`✅ Found ${unassignedRoles.length} unassigned roles`);
339
+ if (unassignedRoles.length) {
340
+ this.context.log(`🚀 Assigning roles to service account`);
341
+ await this.assignRolesToServiceAccount(realm, serviceUserId, brokerMgmtUUID, unassignedRoles);
342
+ this.context.log("✅ Roles successfully assigned!");
343
+ } else {
344
+ this.context.log("ℹ️ All roles are already assigned.");
345
+ }
346
+ } catch (error) {
347
+ this.context.error("Error in assignAllBrokerMgmtRolesToClient", error);
348
+ throw error;
349
+ }
350
+ }
351
+ async getClientSecret(realm, clientId) {
352
+ try {
353
+ this.context.log(`Realm: ${realm}`);
354
+ this.context.log(`Client ID: ${clientId}`);
355
+ await this.authenticate();
356
+ // this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
357
+
358
+ const clientUUID = await this.getClientUUID(realm, clientId);
359
+ if (!clientUUID) {
360
+ throw new Error(`Failed to get UUID for client "${clientId}"`);
361
+ }
362
+ this.context.log(`Client UUID: ${clientUUID}`);
363
+ const response = await this.axiosInstance.get(`/admin/realms/${realm}/clients/${clientUUID}/client-secret`);
364
+ if (response.status !== 200) {
365
+ throw new Error(`Failed to get client secret: ${JSON.stringify(response.data)}`);
366
+ }
367
+ this.context.log(`Client secret: ${JSON.stringify(response.data)}`);
368
+ const data = response.data;
369
+ return data.value;
370
+ } catch (error) {
371
+ this.context.error("Error in getClientSecret", error);
372
+ throw error;
373
+ }
374
+ }
375
+ async createUser(realm, clientId, user) {
376
+ try {
377
+ this.context.log(`Realm name: ${realm}`);
378
+ this.context.log(`Client ID: ${clientId}`);
379
+ this.context.log(`User: ${JSON.stringify(user)}`);
380
+ await this.authenticateRelmClient(realm, clientId);
381
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
382
+ const response = await this.axiosInstance.post(`/admin/realms/${realm}/users`, {
383
+ username: user.username,
384
+ email: user.email,
385
+ firstName: user.firstName,
386
+ lastName: user.lastName,
387
+ enabled: true,
388
+ emailVerified: true,
389
+ credentials: [{
390
+ type: "password",
391
+ value: user.password,
392
+ temporary: true // true if user must reset on first login
393
+ }],
394
+ requiredActions: [
395
+ // Options: "VERIFY_EMAIL", "UPDATE_PROFILE", "UPDATE_PASSWORD", "CONFIGURE_TOTP"
396
+ "UPDATE_PASSWORD"],
397
+ realmRoles: [
398
+ // Optional: assign realm-level roles
399
+ "offline_access", "uma_authorization"],
400
+ groups: [
401
+ // Optional: assign to realm groups
402
+ // "/devs",
403
+ // "/admins"
404
+ ],
405
+ federationLink: null,
406
+ // for federated identity provider
407
+ totp: false,
408
+ // TOTP is not configured initially
409
+ disableableCredentialTypes: [],
410
+ // Optional advanced fields
411
+ notBefore: 0,
412
+ access: {
413
+ manage: true
414
+ }
415
+ });
416
+ if (response.status !== 201) {
417
+ throw new Error(`Failed to create user: ${JSON.stringify(response.data)}`);
418
+ }
419
+ this.context.log(`Response: ${JSON.stringify(response.data)}`);
420
+ return;
421
+ } catch (error) {
422
+ this.context.error("Error in createUser", error);
423
+ throw error;
424
+ }
425
+ }
426
+ async listUsers(realm) {
427
+ try {
428
+ await this.authenticate();
429
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
430
+ const response = await this.axiosInstance.get(`/admin/realms/${realm}/users`);
431
+ if (response.status !== 200) {
432
+ throw new Error(`Failed to list users: ${JSON.stringify(response.data)}`);
433
+ }
434
+ this.context.log(`Users: ${JSON.stringify(response.data)}`);
435
+ const data = response.data;
436
+ return data;
437
+ } catch (error) {
438
+ this.context.error("Error in listUsers", error);
439
+ throw error;
440
+ }
441
+ }
442
+ async getUser(realm, userId) {
443
+ try {
444
+ await this.authenticate();
445
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
446
+ const response = await this.axiosInstance.get(`/admin/realms/${realm}/users/${userId}`);
447
+ if (response.status !== 200) {
448
+ throw new Error(`Failed to get user: ${JSON.stringify(response.data)}`);
449
+ }
450
+ this.context.log(`User: ${JSON.stringify(response.data)}`);
451
+ const data = response.data;
452
+ return data;
453
+ } catch (error) {
454
+ this.context.error("Error in getUser", error);
455
+ throw error;
456
+ }
457
+ }
458
+ async getUserByUsername(realm, username) {
459
+ try {
460
+ await this.authenticate();
461
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
462
+ const response = await this.axiosInstance.get(`/admin/realms/${realm}/users`, {
463
+ params: {
464
+ username
465
+ }
466
+ });
467
+ if (response.status !== 200) {
468
+ throw new Error(`Failed to get user by username: ${JSON.stringify(response.data)}`);
469
+ }
470
+ this.context.log(`User: ${JSON.stringify(response.data)}`);
471
+ if (response.data.length === 0) {
472
+ throw new Error(`User "${username}" not found`);
473
+ }
474
+ const data = response.data[0];
475
+ return data;
476
+ } catch (error) {
477
+ this.context.error("Error in getUserByUsername", error);
478
+ throw error;
479
+ }
480
+ }
481
+ async getUserByEmail(realm, email) {
482
+ try {
483
+ await this.authenticate();
484
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
485
+ const response = await this.axiosInstance.get(`/admin/realms/${realm}/users`, {
486
+ params: {
487
+ email
488
+ }
489
+ });
490
+ if (response.status !== 200) {
491
+ throw new Error(`Failed to get user by email: ${JSON.stringify(response.data)}`);
492
+ }
493
+ this.context.log(`User: ${JSON.stringify(response.data)}`);
494
+ if (response.data.length === 0) {
495
+ throw new Error(`User "${email}" not found`);
496
+ }
497
+ const data = response.data[0];
498
+ return data;
499
+ } catch (error) {
500
+ this.context.error("Error in getUserByEmail", error);
501
+ throw error;
502
+ }
503
+ }
504
+ async login(realm, clientId, user) {
505
+ try {
506
+ this.context.log(`Realm name: ${realm}`);
507
+ this.context.log(`Client ID: ${clientId}`);
508
+ this.context.log(`User: ${JSON.stringify(user)}`);
509
+ await this.authenticateRelmClient(realm, clientId);
510
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
511
+ const clientSecret = await this.getClientSecret(realm, clientId);
512
+ if (!clientSecret) {
513
+ throw new Error(`Client secret for "${clientId}" not found`);
514
+ }
515
+ const loginIdentifier = user.username || user.email;
516
+ const params = new URLSearchParams();
517
+ params.append('grant_type', 'password');
518
+ params.append('username', loginIdentifier);
519
+ params.append('password', user.password);
520
+ params.append('client_id', clientId);
521
+ params.append('client_secret', clientSecret);
522
+ this.context.log(`Params: ${JSON.stringify(params)}`);
523
+ const response = await this.axiosInstance.post(`/realms/${realm}/protocol/openid-connect/token`, params, {
524
+ headers: {
525
+ 'Content-Type': 'application/x-www-form-urlencoded'
526
+ }
527
+ });
528
+ if (response.status !== 200) {
529
+ this.context.error(`Response: ${JSON.stringify(response.data)}`);
530
+ if (response.data.error === 'invalid_grant') {
531
+ if (response.data.error_description === 'Account is not fully set up') {
532
+ throw new Error(`Please reset your password`);
533
+ }
534
+ if (response.data.error_description === 'Invalid user credentials') {
535
+ throw new Error(`Invalid username or password`);
536
+ }
537
+ } else if (response.data.error === 'invalid_token') {
538
+ throw new Error(`Invalid token`);
539
+ } else if (response.data.error === 'invalid_client') {
540
+ throw new Error(`Invalid client`);
541
+ } else if (response.data.error === 'invalid_request') {
542
+ throw new Error(`Invalid request`);
543
+ }
544
+ throw new Error(`Failed to login user using keycloak`);
545
+ }
546
+ this.context.log(`Response: ${JSON.stringify(response.data)}`);
547
+ const data = response.data;
548
+ return data;
549
+ } catch (error) {
550
+ this.context.error("Error in login", error);
551
+ if ((error == null ? void 0 : error.response.data.error) === 'invalid_grant') {
552
+ if ((error == null ? void 0 : error.response.data.error_description) === 'Account is not fully set up') {
553
+ throw new Error(`Please reset your password`);
554
+ }
555
+ if ((error == null ? void 0 : error.response.data.error_description) === 'Invalid user credentials') {
556
+ throw new Error(`Invalid username or password`);
557
+ }
558
+ } else if ((error == null ? void 0 : error.response.data.error) === 'invalid_token') {
559
+ throw new Error(`Invalid token`);
560
+ } else if ((error == null ? void 0 : error.response.data.error) === 'invalid_client') {
561
+ throw new Error(`Invalid client`);
562
+ } else if ((error == null ? void 0 : error.response.data.error) === 'invalid_request') {
563
+ throw new Error(`Invalid request`);
564
+ }
565
+ throw new Error(`Failed to login user using keycloak`);
566
+ }
567
+ }
568
+ async resetPassword(realm, userId, password) {
569
+ try {
570
+ await this.authenticate();
571
+ this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
572
+ const response = await this.axiosInstance.put(`/admin/realms/${realm}/users/${userId}/reset-password`, {
573
+ type: "password",
574
+ value: password,
575
+ temporary: false
576
+ });
577
+ if (response.status !== 204) {
578
+ throw new Error(`Failed to reset password: ${JSON.stringify(response.data)}`);
579
+ }
580
+ return;
581
+ } catch (error) {
582
+ var _error$response;
583
+ this.context.error("Error in resetPassword", error);
584
+ if (error != null && (_error$response = error.response) != null && (_error$response = _error$response.data) != null && _error$response.error_description) {
585
+ var _error$response2;
586
+ throw new Error(error == null || (_error$response2 = error.response) == null || (_error$response2 = _error$response2.data) == null ? void 0 : _error$response2.error_description);
587
+ }
588
+ if ((error == null ? void 0 : error.response.data.error) === 'invalid_grant') {
589
+ throw new Error(`Invalid grant`);
590
+ } else if ((error == null ? void 0 : error.response.data.error) === 'invalid_token') {
591
+ throw new Error(`Invalid token`);
592
+ } else if ((error == null ? void 0 : error.response.data.error) === 'invalid_client') {
593
+ throw new Error(`Invalid client`);
594
+ } else if ((error == null ? void 0 : error.response.data.error) === 'invalid_request') {
595
+ throw new Error(`Invalid request`);
596
+ }
597
+ throw error;
598
+ }
599
+ }
600
+
601
+ /**
602
+ * Logout culturefy and revoke refresh token
603
+ * This method uses Keycloak's standard OpenID Connect logout endpoint
604
+ * which both logs out the culturefy and invalidates the refresh token
605
+ *
606
+ * @param realm - The Keycloak realm name
607
+ * @param clientId - The client ID
608
+ * @param refreshToken - The refresh token to revoke
609
+ * @returns Promise<void>
610
+ */
611
+ async logout(realm, clientId, refreshToken) {
612
+ try {
613
+ this.context.log(`Culturefy Logout - Realm: ${realm}, Client: ${clientId}`);
614
+ this.context.log(`Culturefy Logout - Refresh Token: ${refreshToken}`);
615
+ await this.authenticateRelmClient(realm, clientId);
616
+ // this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
617
+
618
+ // Try to get client secret, but handle the case where it might not exist
619
+ let clientSecret = null;
620
+ try {
621
+ clientSecret = await this.getClientSecret(realm, clientId);
622
+ } catch (error) {
623
+ this.context.warn(`Could not retrieve client secret for ${clientId}:`, error);
624
+ // For admin-cli, we might not need a client secret
625
+ clientSecret = null;
626
+ }
627
+ const params = new URLSearchParams();
628
+ params.append('refresh_token', refreshToken || '');
629
+ params.append('client_id', clientId || '');
630
+
631
+ // Only add client_secret if it exists
632
+ if (clientSecret) {
633
+ params.append('client_secret', clientSecret || '');
634
+ }
635
+ this.context.log(`Culturefy Logout Params: ${JSON.stringify(params)}`);
636
+ const response = await this.axiosInstance.post(`/realms/${realm}/protocol/openid-connect/logout`, params, {
637
+ headers: {
638
+ 'Content-Type': 'application/x-www-form-urlencoded'
639
+ }
640
+ });
641
+ if (response.status !== 200) {
642
+ this.context.error(`Culturefy Logout Response: ${JSON.stringify(response.data)}`);
643
+
644
+ // Handle specific error cases
645
+ if (response.data.error === 'invalid_grant') {
646
+ throw new Error(`Invalid refresh token`);
647
+ } else if (response.data.error === 'invalid_token') {
648
+ throw new Error(`Invalid token`);
649
+ } else if (response.data.error === 'invalid_client') {
650
+ throw new Error(`Invalid client configuration - admin-cli client may not be properly configured`);
651
+ } else if (response.data.error === 'invalid_request') {
652
+ throw new Error(`Invalid request`);
653
+ }
654
+ throw new Error(`Failed to logout culturefy - check admin-cli client configuration`);
655
+ }
656
+ this.context.log(`Culturefy Logout Response: ${JSON.stringify(response.data)}`);
657
+ return;
658
+ } catch (error) {
659
+ var _error$response3, _error$response4, _error$response5, _error$response6, _error$response7;
660
+ this.context.error("Error in logoutCulturefy", error);
661
+
662
+ // Handle specific error cases
663
+ if ((error == null || (_error$response3 = error.response) == null || (_error$response3 = _error$response3.data) == null ? void 0 : _error$response3.error) === 'invalid_grant') {
664
+ throw new Error(`Invalid refresh token`);
665
+ } else if ((error == null || (_error$response4 = error.response) == null || (_error$response4 = _error$response4.data) == null ? void 0 : _error$response4.error) === 'invalid_token') {
666
+ throw new Error(`Invalid token`);
667
+ } else if ((error == null || (_error$response5 = error.response) == null || (_error$response5 = _error$response5.data) == null ? void 0 : _error$response5.error) === 'invalid_client') {
668
+ throw new Error(`Invalid client configuration - admin-cli client may not be properly configured`);
669
+ } else if ((error == null || (_error$response6 = error.response) == null || (_error$response6 = _error$response6.data) == null ? void 0 : _error$response6.error) === 'invalid_request') {
670
+ throw new Error(`Invalid request`);
671
+ }
672
+ if (error != null && (_error$response7 = error.response) != null && (_error$response7 = _error$response7.data) != null && _error$response7.error_description) {
673
+ var _error$response8;
674
+ throw new Error(error == null || (_error$response8 = error.response) == null || (_error$response8 = _error$response8.data) == null ? void 0 : _error$response8.error_description);
675
+ }
676
+ throw new Error(`Failed to logout culturefy - check admin-cli client configuration`);
677
+ }
678
+ }
679
+
680
+ /**
681
+ * Refresh culturefy access token using refresh token
682
+ * This method uses Keycloak's standard OpenID Connect token endpoint
683
+ * to refresh the access token for culturefy users
684
+ *
685
+ * @param realm - The Keycloak realm name
686
+ * @param clientId - The client ID
687
+ * @param refreshToken - The refresh token to use for getting new access token
688
+ * @returns Promise<KeycloakUserLoginResponse> - New access and refresh tokens
689
+ */
690
+ async refreshToken(realm, clientId, refreshToken) {
691
+ try {
692
+ this.context.log(`Culturefy Token Refresh - Realm: ${realm}, Client: ${clientId}`);
693
+ await this.authenticate();
694
+ // this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
695
+
696
+ // Try to get client secret, but handle the case where it might not exist
697
+ let clientSecret = null;
698
+ try {
699
+ clientSecret = await this.getClientSecret(realm, clientId);
700
+ } catch (error) {
701
+ this.context.warn(`Could not retrieve client secret for ${clientId}:`, error);
702
+ // For admin-cli, we might not need a client secret
703
+ clientSecret = null;
704
+ }
705
+ const params = new URLSearchParams();
706
+ params.append('grant_type', 'refresh_token');
707
+ params.append('refresh_token', refreshToken);
708
+ params.append('client_id', clientId);
709
+
710
+ // Only add client_secret if it exists
711
+ if (clientSecret) {
712
+ params.append('client_secret', clientSecret);
713
+ }
714
+ this.context.log(`Culturefy Token Refresh Params: ${JSON.stringify(params)}`);
715
+ const response = await this.axiosInstance.post(`/realms/${realm}/protocol/openid-connect/token`, params, {
716
+ headers: {
717
+ 'Content-Type': 'application/x-www-form-urlencoded'
718
+ }
719
+ });
720
+ if (response.status !== 200) {
721
+ this.context.error(`Culturefy Token Refresh Response: ${JSON.stringify(response.data)}`);
722
+
723
+ // Handle specific error cases
724
+ if (response.data.error === 'invalid_grant') {
725
+ if (response.data.error_description === 'Token is not active') {
726
+ throw new Error(`Refresh token has expired, please login again`);
727
+ }
728
+ if (response.data.error_description === 'Invalid refresh token') {
729
+ throw new Error(`Invalid refresh token`);
730
+ }
731
+ throw new Error(`Invalid refresh token`);
732
+ } else if (response.data.error === 'invalid_token') {
733
+ throw new Error(`Invalid token`);
734
+ } else if (response.data.error === 'invalid_client') {
735
+ throw new Error(`Invalid client configuration - admin-cli client may not be properly configured`);
736
+ } else if (response.data.error === 'invalid_request') {
737
+ throw new Error(`Invalid request`);
738
+ }
739
+ throw new Error(`Failed to refresh culturefy token - check admin-cli client configuration`);
740
+ }
741
+ this.context.log(`Culturefy Token Refresh Response: ${JSON.stringify(response.data)}`);
742
+ const data = response.data;
743
+ return data;
744
+ } catch (error) {
745
+ var _error$response9, _error$response10, _error$response11, _error$response12, _error$response13;
746
+ this.context.error("Error in refreshCulturefyToken", error);
747
+
748
+ // Handle specific error cases
749
+ if ((error == null || (_error$response9 = error.response) == null || (_error$response9 = _error$response9.data) == null ? void 0 : _error$response9.error) === 'invalid_grant') {
750
+ var _error$response0, _error$response1;
751
+ if ((error == null || (_error$response0 = error.response) == null || (_error$response0 = _error$response0.data) == null ? void 0 : _error$response0.error_description) === 'Token is not active') {
752
+ throw new Error(`Refresh token has expired, please login again`);
753
+ }
754
+ if ((error == null || (_error$response1 = error.response) == null || (_error$response1 = _error$response1.data) == null ? void 0 : _error$response1.error_description) === 'Invalid refresh token') {
755
+ throw new Error(`Invalid refresh token`);
756
+ }
757
+ throw new Error(`Invalid refresh token`);
758
+ } else if ((error == null || (_error$response10 = error.response) == null || (_error$response10 = _error$response10.data) == null ? void 0 : _error$response10.error) === 'invalid_token') {
759
+ throw new Error(`Invalid token`);
760
+ } else if ((error == null || (_error$response11 = error.response) == null || (_error$response11 = _error$response11.data) == null ? void 0 : _error$response11.error) === 'invalid_client') {
761
+ throw new Error(`Invalid client configuration - admin-cli client may not be properly configured`);
762
+ } else if ((error == null || (_error$response12 = error.response) == null || (_error$response12 = _error$response12.data) == null ? void 0 : _error$response12.error) === 'invalid_request') {
763
+ throw new Error(`Invalid request`);
764
+ }
765
+ if (error != null && (_error$response13 = error.response) != null && (_error$response13 = _error$response13.data) != null && _error$response13.error_description) {
766
+ var _error$response14;
767
+ throw new Error(error == null || (_error$response14 = error.response) == null || (_error$response14 = _error$response14.data) == null ? void 0 : _error$response14.error_description);
768
+ }
769
+ throw new Error(`Failed to refresh culturefy token - check admin-cli client configuration`);
770
+ }
771
+ }
772
+
773
+ /**
774
+ * Introspect culturefy token to check if it's active
775
+ * This method uses Keycloak's standard OpenID Connect token introspection endpoint
776
+ * to verify the active state of a culturefy token
777
+ *
778
+ * @param realm - The Keycloak realm name
779
+ * @param clientId - The client ID
780
+ * @param token - The token to introspect
781
+ * @returns Promise<KeycloakIntrospectTokenResponse> - Token introspection result
782
+ */
783
+ async introspectToken(realm, clientId, token) {
784
+ try {
785
+ this.context.log(`Culturefy Token Introspection - Realm: ${realm}, Client: ${clientId}`);
786
+ await this.authenticateRelmClient(realm, clientId);
787
+ // this.context.log(`Authenticated`, JSON.stringify(this.accessToken));
788
+
789
+ // Try to get client secret, but handle the case where it might not exist
790
+ let clientSecret = null;
791
+ try {
792
+ clientSecret = await this.getClientSecret(realm, clientId);
793
+ } catch (error) {
794
+ this.context.warn(`Could not retrieve client secret for ${clientId}:`, error);
795
+ // For admin-cli, we might not need a client secret
796
+ clientSecret = null;
797
+ }
798
+
799
+ // Create introspection parameters
800
+ const params = new URLSearchParams();
801
+ params.append('token', token);
802
+ this.context.log(`Culturefy Token Introspection Params: ${JSON.stringify(params)}`);
803
+
804
+ // Perform the introspection request
805
+ const response = await this.axiosInstance.post(`/realms/${realm}/protocol/openid-connect/token/introspect`, params, {
806
+ headers: {
807
+ 'Content-Type': 'application/x-www-form-urlencoded'
808
+ },
809
+ auth: {
810
+ username: clientId,
811
+ password: clientSecret || '' // Use empty string if no client secret
812
+ }
813
+ });
814
+ if (response.status !== 200) {
815
+ this.context.error(`Culturefy Token Introspection Response: ${JSON.stringify(response.data)}`);
816
+
817
+ // Handle specific error cases
818
+ if (response.data.error === 'invalid_token') {
819
+ throw new Error(`Invalid token`);
820
+ } else if (response.data.error === 'invalid_client') {
821
+ throw new Error(`Invalid client configuration - admin-cli client may not be properly configured`);
822
+ } else if (response.data.error === 'invalid_request') {
823
+ throw new Error(`Invalid request`);
824
+ }
825
+ throw new Error(`Failed to introspect culturefy token - check admin-cli client configuration`);
826
+ }
827
+ this.context.log(`Culturefy Token Introspection Response: ${JSON.stringify(response.data)}`);
828
+ const data = response.data;
829
+
830
+ // Check if token is active
831
+ if (!data.active) {
832
+ throw new Error(`Token is not active or has expired`);
833
+ }
834
+ return data;
835
+ } catch (error) {
836
+ var _error$response15, _error$response16, _error$response17, _error$response18;
837
+ this.context.error("Error in introspectCulturefyToken", error);
838
+
839
+ // Handle specific error cases
840
+ if ((error == null || (_error$response15 = error.response) == null || (_error$response15 = _error$response15.data) == null ? void 0 : _error$response15.error) === 'invalid_token') {
841
+ throw new Error(`Invalid token`);
842
+ } else if ((error == null || (_error$response16 = error.response) == null || (_error$response16 = _error$response16.data) == null ? void 0 : _error$response16.error) === 'invalid_client') {
843
+ throw new Error(`Invalid client configuration - admin-cli client may not be properly configured`);
844
+ } else if ((error == null || (_error$response17 = error.response) == null || (_error$response17 = _error$response17.data) == null ? void 0 : _error$response17.error) === 'invalid_request') {
845
+ throw new Error(`Invalid request`);
846
+ }
847
+ if (error != null && (_error$response18 = error.response) != null && (_error$response18 = _error$response18.data) != null && _error$response18.error_description) {
848
+ var _error$response19;
849
+ throw new Error(error == null || (_error$response19 = error.response) == null || (_error$response19 = _error$response19.data) == null ? void 0 : _error$response19.error_description);
850
+ }
851
+ throw new Error(`Failed to introspect culturefy token - check admin-cli client configuration`);
852
+ }
853
+ }
854
+
855
+ /**
856
+ * Get user details from a token using Keycloak's userinfo endpoint
857
+ * This method uses Keycloak's standard OpenID Connect userinfo endpoint
858
+ * to get the user details associated with the token
859
+ *
860
+ * @param realm - The Keycloak realm name
861
+ * @param clientId - The client ID
862
+ * @param token - The access token to get user info for
863
+ * @returns Promise<KeycloakUserResponse> - User details from the token
864
+ */
865
+ async getUserByToken(realm, token) {
866
+ try {
867
+ try {
868
+ const response = await this.axiosInstance.get(`/realms/${realm}/protocol/openid-connect/userinfo`, {
869
+ headers: {
870
+ 'Authorization': `Bearer ${token}`,
871
+ 'Content-Type': 'application/json'
872
+ }
873
+ });
874
+ if (response.status === 200) {
875
+ this.context.log(`User info from userinfo endpoint: ${JSON.stringify(response.data)}`);
876
+
877
+ // Transform the userinfo response to match KeycloakUserResponse format
878
+ const userInfo = response.data;
879
+ const userResponse = {
880
+ id: userInfo.sub || userInfo.user_id,
881
+ username: userInfo.preferred_username || userInfo.username,
882
+ email: userInfo.email,
883
+ firstName: userInfo.given_name || userInfo.first_name,
884
+ lastName: userInfo.family_name || userInfo.last_name,
885
+ emailVerified: userInfo.email_verified || false,
886
+ enabled: true,
887
+ // userinfo doesn't provide this, assume true if token is valid
888
+ createdTimestamp: userInfo.iat ? userInfo.iat * 1000 : 0,
889
+ totp: false,
890
+ disableableCredentialTypes: [],
891
+ requiredActions: [],
892
+ notBefore: 0,
893
+ access: {
894
+ manage: true
895
+ }
896
+ };
897
+ return userResponse;
898
+ }
899
+ } catch (userinfoError) {
900
+ this.context.warn(`Userinfo endpoint failed, trying alternative method:`, userinfoError);
901
+ }
902
+ // Method 2: Decode JWT token and extract user ID, then get user details
903
+ try {
904
+ // Decode the JWT token to get the user ID
905
+ const decoded = this.decodeJwtToken(token);
906
+ if (!decoded || !decoded.sub) {
907
+ throw new Error('Invalid token or missing user ID');
908
+ }
909
+ const userId = decoded.sub;
910
+ this.context.log(`Extracted user ID from token: ${userId}`);
911
+
912
+ // Get user details using the admin API
913
+ await this.authenticate();
914
+ const response = await this.axiosInstance.get(`/admin/realms/${realm}/users/${userId}`);
915
+ if (response.status === 200) {
916
+ this.context.log(`User details from admin API: ${JSON.stringify(response.data)}`);
917
+ const userResponse = response.data;
918
+ return userResponse;
919
+ }
920
+ } catch (adminApiError) {
921
+ this.context.warn(`Admin API method failed:`, adminApiError);
922
+ }
923
+
924
+ // Method 3: Extract user details directly from JWT token claims
925
+ try {
926
+ const decoded = this.decodeJwtToken(token);
927
+ if (decoded) {
928
+ this.context.log(`Extracting user details from JWT claims: ${JSON.stringify(decoded)}`);
929
+ const userResponse = {
930
+ id: decoded.sub,
931
+ username: decoded.preferred_username || decoded.username,
932
+ email: decoded.email,
933
+ firstName: decoded.given_name || decoded.first_name,
934
+ lastName: decoded.family_name || decoded.last_name,
935
+ emailVerified: decoded.email_verified || false,
936
+ enabled: true,
937
+ createdTimestamp: decoded.iat ? decoded.iat * 1000 : 0,
938
+ totp: false,
939
+ disableableCredentialTypes: [],
940
+ requiredActions: [],
941
+ notBefore: 0,
942
+ access: {
943
+ manage: true
944
+ }
945
+ // Note: Additional user attributes are available in decoded token but not in KeycloakUserResponse interface
946
+ };
947
+ return userResponse;
948
+ }
949
+ } catch (jwtError) {
950
+ this.context.error(`JWT decoding failed:`, jwtError);
951
+ }
952
+ throw new Error('Failed to get user details from culturefy token');
953
+ } catch (error) {
954
+ var _error$response20, _error$response21, _error$response22, _error$response23;
955
+ this.context.error("Error in getUserByCulturefyToken", error);
956
+ if ((error == null || (_error$response20 = error.response) == null || (_error$response20 = _error$response20.data) == null ? void 0 : _error$response20.error) === 'invalid_token') {
957
+ throw new Error(`Invalid token`);
958
+ } else if ((error == null || (_error$response21 = error.response) == null || (_error$response21 = _error$response21.data) == null ? void 0 : _error$response21.error) === 'insufficient_scope') {
959
+ throw new Error(`Token does not have required scope to access user info`);
960
+ } else if ((error == null || (_error$response22 = error.response) == null || (_error$response22 = _error$response22.data) == null ? void 0 : _error$response22.error) === 'invalid_request') {
961
+ throw new Error(`Invalid request`);
962
+ }
963
+ if (error != null && (_error$response23 = error.response) != null && (_error$response23 = _error$response23.data) != null && _error$response23.error_description) {
964
+ var _error$response24;
965
+ throw new Error(error == null || (_error$response24 = error.response) == null || (_error$response24 = _error$response24.data) == null ? void 0 : _error$response24.error_description);
966
+ }
967
+ throw new Error(`Failed to get user details from culturefy token`);
968
+ }
969
+ }
970
+
971
+ /**
972
+ * Decode JWT token without verification (for extracting claims)
973
+ * This is used to extract user information from the token
974
+ *
975
+ * @param token - The JWT token to decode
976
+ * @returns any - Decoded token payload
977
+ */
978
+ decodeJwtToken(token) {
979
+ try {
980
+ // Split the token and get the payload part
981
+ const parts = token.split('.');
982
+ if (parts.length !== 3) {
983
+ throw new Error('Invalid JWT token format');
984
+ }
985
+
986
+ // Decode the payload (second part)
987
+ const payload = parts[1];
988
+ const decodedPayload = Buffer.from(payload, 'base64').toString('utf-8');
989
+ return JSON.parse(decodedPayload);
990
+ } catch (error) {
991
+ this.context.error("Error decoding JWT token:", error);
992
+ throw new Error('Failed to decode JWT token');
993
+ }
994
+ }
995
+ }
996
+ //# sourceMappingURL=keycloak.service.js.map