Skip to content

Commit 3de93ed

Browse files
authored
feat: llmo-1539 new endpoint to get user details (#1585)
1 parent 7810fe1 commit 3de93ed

File tree

6 files changed

+707
-2
lines changed

6 files changed

+707
-2
lines changed

docs/index.html

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

src/controllers/user-details.js

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Copyright 2025 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {
14+
badRequest,
15+
notFound,
16+
ok,
17+
forbidden,
18+
internalServerError,
19+
} from '@adobe/spacecat-shared-http-utils';
20+
import {
21+
hasText,
22+
isNonEmptyObject,
23+
isValidUUID,
24+
} from '@adobe/spacecat-shared-utils';
25+
26+
import AccessControlUtil from '../support/access-control-util.js';
27+
28+
/**
29+
* UserDetails controller. Provides methods to fetch user details by external user ID.
30+
* @param {object} ctx - Context of the request.
31+
* @returns {object} UserDetails controller.
32+
* @constructor
33+
*/
34+
function UserDetailsController(ctx) {
35+
if (!isNonEmptyObject(ctx)) {
36+
throw new Error('Context required');
37+
}
38+
39+
const { dataAccess, imsClient, log } = ctx;
40+
if (!isNonEmptyObject(dataAccess)) {
41+
throw new Error('Data access required');
42+
}
43+
44+
const { TrialUser, Organization } = dataAccess;
45+
46+
const accessControlUtil = AccessControlUtil.fromContext(ctx);
47+
48+
/**
49+
* Helper function to fetch user details from IMS if user is admin.
50+
* @param {string} externalUserId - The external user ID to fetch from IMS.
51+
* @param {string} organizationId - The organization ID for fallback.
52+
* @returns {Promise<Object>} User details object.
53+
*/
54+
const fetchFromImsIfAdmin = async (externalUserId, organizationId) => {
55+
// Check if requestor has admin access
56+
if (!accessControlUtil.hasAdminAccess()) {
57+
log.debug(`User is not admin, returning system defaults for ${externalUserId}`);
58+
return {
59+
firstName: 'system',
60+
lastName: '-',
61+
email: '',
62+
organizationId,
63+
};
64+
}
65+
66+
// Try to fetch from IMS for admin users
67+
try {
68+
log.debug(`Admin user requesting details for ${externalUserId}, attempting IMS fallback`);
69+
const imsProfile = await imsClient.getImsAdminProfile(externalUserId);
70+
return {
71+
firstName: imsProfile.first_name || '-',
72+
lastName: imsProfile.last_name || '-',
73+
email: imsProfile.email || '',
74+
organizationId,
75+
};
76+
} catch (error) {
77+
log.warn(`Failed to fetch user details from IMS for ${externalUserId}: ${error.message}`);
78+
return {
79+
firstName: '-',
80+
lastName: '-',
81+
email: '',
82+
organizationId,
83+
};
84+
}
85+
};
86+
87+
/**
88+
* Gets user details by external user ID.
89+
* @param {object} context - Context of the request.
90+
* @returns {Promise<Response>} User details response.
91+
*/
92+
const getUserDetailsByExternalUserId = async (context) => {
93+
const { organizationId, externalUserId } = context.params;
94+
95+
if (!isValidUUID(organizationId)) {
96+
return badRequest('Organization ID required');
97+
}
98+
99+
if (!hasText(externalUserId)) {
100+
return badRequest('External user ID is required');
101+
}
102+
103+
try {
104+
// Check if user has access to the organization
105+
const organization = await Organization.findById(organizationId);
106+
if (!organization) {
107+
return notFound('Organization not found');
108+
}
109+
110+
if (!await accessControlUtil.hasAccess(organization)) {
111+
return forbidden('Access denied to this organization');
112+
}
113+
114+
// Find trial user by external user ID and organization ID
115+
const trialUsers = await TrialUser.allByOrganizationId(organizationId);
116+
const trialUser = trialUsers.find(
117+
(user) => user.getExternalUserId() === externalUserId,
118+
);
119+
120+
let userDetails;
121+
if (trialUser) {
122+
userDetails = {
123+
firstName: trialUser.getFirstName(),
124+
lastName: trialUser.getLastName(),
125+
email: trialUser.getEmailId(),
126+
organizationId: trialUser.getOrganizationId(),
127+
};
128+
} else {
129+
// User not found in trial users - try IMS if admin
130+
userDetails = await fetchFromImsIfAdmin(externalUserId, organizationId);
131+
}
132+
133+
return ok(userDetails);
134+
} catch (e) {
135+
context.log.error(`Error getting user details for external user ID ${externalUserId}: ${e.message}`);
136+
return internalServerError(e.message);
137+
}
138+
};
139+
140+
/**
141+
* Gets user details for multiple users in bulk.
142+
* @param {object} context - Context of the request.
143+
* @returns {Promise<Response>} Bulk user details response.
144+
*/
145+
const getUserDetailsInBulk = async (context) => {
146+
const { organizationId } = context.params;
147+
const { userIds } = context.data;
148+
149+
if (!isValidUUID(organizationId)) {
150+
return badRequest('Organization ID required');
151+
}
152+
153+
if (!Array.isArray(userIds) || userIds.length === 0) {
154+
return badRequest('userIds array is required and must not be empty');
155+
}
156+
157+
try {
158+
// Check if user has access to the organization
159+
const organization = await Organization.findById(organizationId);
160+
if (!organization) {
161+
return notFound('Organization not found');
162+
}
163+
164+
if (!await accessControlUtil.hasAccess(organization)) {
165+
return forbidden('Access denied to this organization');
166+
}
167+
168+
// Fetch all trial users for the organization
169+
const trialUsers = await TrialUser.allByOrganizationId(organizationId);
170+
171+
// Create a map of externalUserId to user details
172+
const userDetailsMap = {};
173+
let imsCallCount = 0;
174+
175+
for (const externalUserId of userIds) {
176+
const trialUser = trialUsers.find(
177+
(user) => user.getExternalUserId() === externalUserId,
178+
);
179+
180+
if (trialUser) {
181+
userDetailsMap[externalUserId] = {
182+
firstName: trialUser.getFirstName(),
183+
lastName: trialUser.getLastName(),
184+
email: trialUser.getEmailId(),
185+
organizationId: trialUser.getOrganizationId(),
186+
};
187+
} else {
188+
// User not found in trial users - try IMS if admin
189+
imsCallCount += 1;
190+
// eslint-disable-next-line no-await-in-loop
191+
const details = await fetchFromImsIfAdmin(externalUserId, organizationId);
192+
userDetailsMap[externalUserId] = details;
193+
}
194+
}
195+
196+
// Log IMS fallback count
197+
if (imsCallCount > 0) {
198+
context.log.info(`Fetched user details from IMS ${imsCallCount} times for organization ${organizationId}`);
199+
}
200+
201+
return ok(userDetailsMap);
202+
} catch (e) {
203+
context.log.error(`Error getting bulk user details for organization ${organizationId}: ${e.message}`);
204+
return internalServerError(e.message);
205+
}
206+
};
207+
208+
return {
209+
getUserDetailsByExternalUserId,
210+
getUserDetailsInBulk,
211+
};
212+
}
213+
214+
export default UserDetailsController;

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import LlmoController from './controllers/llmo/llmo.js';
7272
import UserActivitiesController from './controllers/user-activities.js';
7373
import SiteEnrollmentsController from './controllers/site-enrollments.js';
7474
import TrialUsersController from './controllers/trial-users.js';
75+
import UserDetailsController from './controllers/user-details.js';
7576
import EntitlementsController from './controllers/entitlements.js';
7677
import SandboxAuditController from './controllers/sandbox-audit.js';
7778
import PTA2Controller from './controllers/paid/pta2.js';
@@ -136,6 +137,7 @@ async function run(request, context) {
136137
const userActivitiesController = UserActivitiesController(context);
137138
const siteEnrollmentsController = SiteEnrollmentsController(context);
138139
const trialUsersController = TrialUsersController(context);
140+
const userDetailsController = UserDetailsController(context);
139141
const entitlementsController = EntitlementsController(context);
140142
const sandboxAuditController = SandboxAuditController(context);
141143
const pta2Controller = PTA2Controller(context, log, context.env);
@@ -169,6 +171,7 @@ async function run(request, context) {
169171
userActivitiesController,
170172
siteEnrollmentsController,
171173
trialUsersController,
174+
userDetailsController,
172175
entitlementsController,
173176
sandboxAuditController,
174177
reportsController,

src/routes/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ function isStaticRoute(routePattern) {
7575
* @param {Object} userActivityController - The user activity controller.
7676
* @param {Object} siteEnrollmentController - The site enrollment controller.
7777
* @param {Object} trialUserController - The trial user controller.
78+
* @param {Object} userDetailsController - The user details controller.
7879
* @param {Object} entitlementController - The entitlement controller.
7980
* @param {Object} sandboxAuditController - The sandbox audit controller.
8081
* @param {Object} reportsController - The reports controller.
@@ -110,6 +111,7 @@ export default function getRouteHandlers(
110111
userActivityController,
111112
siteEnrollmentController,
112113
trialUserController,
114+
userDetailsController,
113115
entitlementController,
114116
sandboxAuditController,
115117
reportsController,
@@ -340,6 +342,8 @@ export default function getRouteHandlers(
340342
'POST /sites/:siteId/user-activities': userActivityController.createTrialUserActivity,
341343
'GET /sites/:siteId/site-enrollments': siteEnrollmentController.getBySiteID,
342344
'GET /organizations/:organizationId/trial-users': trialUserController.getByOrganizationID,
345+
'GET /organizations/:organizationId/userDetails/:externalUserId': userDetailsController.getUserDetailsByExternalUserId,
346+
'POST /organizations/:organizationId/userDetails': userDetailsController.getUserDetailsInBulk,
343347
'POST /organizations/:organizationId/trial-user-invite': trialUserController.createTrialUserForEmailInvite,
344348
'GET /organizations/:organizationId/entitlements': entitlementController.getByOrganizationID,
345349
'POST /organizations/:organizationId/entitlements': entitlementController.createEntitlement,

0 commit comments

Comments
 (0)