@@ -148,19 +148,27 @@ export class ProjectService {
148148 req : Request ,
149149 projectId : string ,
150150 operation : 'add' | 'update' | 'remove' ,
151- username : string ,
151+ usernameOrEmail : string ,
152152 role ?: 'view' | 'manage' ,
153153 manualUserInfo ?: { name : string ; email : string ; username : string ; avatar ?: string }
154154 ) : Promise < ProjectSettings > {
155- // Step 1: Fetch current settings with ETag
155+ // Step 1: Determine the appropriate identifier for backend operations
156+ // For emails, use the sub; for usernames, use the username directly
157+ let backendIdentifier = usernameOrEmail . trim ( ) ;
158+ if ( usernameOrEmail . includes ( '@' ) ) {
159+ // For emails, get the sub for backend operations
160+ backendIdentifier = await this . resolveEmailToSub ( req , usernameOrEmail ) ;
161+ }
162+
163+ // Step 2: Fetch current settings with ETag
156164 const { data : settings , etag } = await this . etagService . fetchWithETag < ProjectSettings > (
157165 req ,
158166 'LFX_V2_SERVICE' ,
159167 `/projects/${ projectId } /settings` ,
160168 `${ operation } _user_project_permissions`
161169 ) ;
162170
163- // Step 2 : Update the settings based on operation
171+ // Step 3 : Update the settings based on operation
164172 const updatedSettings = { ...settings } ;
165173
166174 // Initialize arrays if they don't exist
@@ -169,8 +177,9 @@ export class ProjectService {
169177
170178 // Remove user from both arrays first (for all operations)
171179 // Compare by username property since writers/auditors are now UserInfo objects
172- updatedSettings . writers = updatedSettings . writers . filter ( ( u ) => u . username !== username ) ;
173- updatedSettings . auditors = updatedSettings . auditors . filter ( ( u ) => u . username !== username ) ;
180+ // Use backendIdentifier (sub) for comparison to ensure proper removal
181+ updatedSettings . writers = updatedSettings . writers . filter ( ( u ) => u . username !== backendIdentifier ) ;
182+ updatedSettings . auditors = updatedSettings . auditors . filter ( ( u ) => u . username !== backendIdentifier ) ;
174183
175184 // For 'add' or 'update', we need to add the user back with full UserInfo
176185 if ( operation === 'add' || operation === 'update' ) {
@@ -183,23 +192,28 @@ export class ProjectService {
183192 if ( manualUserInfo ) {
184193 req . log . info (
185194 {
186- username,
195+ username : backendIdentifier ,
187196 operation : `${ operation } _user_project_permissions` ,
188197 } ,
189198 'Using manually provided user info'
190199 ) ;
191200 userInfo = {
192201 name : manualUserInfo . name ,
193202 email : manualUserInfo . email ,
194- username : manualUserInfo . username ,
203+ username : backendIdentifier , // Use the sub for backend consistency
195204 } ;
196205 // Only include avatar if it's provided and not empty
197206 if ( manualUserInfo . avatar ) {
198207 userInfo . avatar = manualUserInfo . avatar ;
199208 }
200209 } else {
201- // Fetch user info from user service via NATS
202- userInfo = await this . getUserInfo ( req , username ) ;
210+ // Fetch user info from user service via NATS using the original input
211+ const fetchedUserInfo = await this . getUserInfo ( req , usernameOrEmail ) ;
212+ // Use the backend identifier (sub) for the username in the stored UserInfo for backend consistency
213+ userInfo = {
214+ ...fetchedUserInfo ,
215+ username : backendIdentifier , // Use the sub for backend consistency
216+ } ;
203217 }
204218
205219 if ( role === 'manage' ) {
@@ -250,7 +264,7 @@ export class ProjectService {
250264 {
251265 operation : `${ operation } _user_project_permissions` ,
252266 project_id : projectId ,
253- username,
267+ username : backendIdentifier ,
254268 role : role || 'N/A' ,
255269 } ,
256270 `User ${ operation } operation completed successfully`
@@ -260,13 +274,13 @@ export class ProjectService {
260274 }
261275
262276 /**
263- * Resolve email address to username using NATS request-reply pattern
277+ * Resolve email address to sub using NATS request-reply pattern
264278 * @param req - Express request object for logging
265279 * @param email - Email address to lookup
266- * @returns Username (sub) associated with the email
280+ * @returns Sub associated with the email (used for backend auditors/writers)
267281 * @throws ResourceNotFoundError if user not found
268282 */
269- public async resolveEmailToUsername ( req : Request , email : string ) : Promise < string > {
283+ public async resolveEmailToSub ( req : Request , email : string ) : Promise < string > {
270284 const codec = this . natsService . getCodec ( ) ;
271285
272286 // Normalize email input
@@ -345,8 +359,93 @@ export class ProjectService {
345359 }
346360
347361 /**
348- * Fetch user information by username using NATS request-reply pattern
349- * The username can be an email or actual username - if email, it will be resolved first
362+ * Resolve email address to username using NATS request-reply pattern
363+ * @param req - Express request object for logging
364+ * @param email - Email address to lookup
365+ * @returns Username associated with the email (used for display purposes)
366+ * @throws ResourceNotFoundError if user not found
367+ */
368+ public async resolveEmailToUsername ( req : Request , email : string ) : Promise < string > {
369+ const codec = this . natsService . getCodec ( ) ;
370+
371+ // Normalize email input
372+ const normalizedEmail = email . trim ( ) . toLowerCase ( ) ;
373+
374+ try {
375+ req . log . info ( { email : normalizedEmail } , 'Resolving email to username via NATS' ) ;
376+
377+ const response = await this . natsService . request ( NatsSubjects . EMAIL_TO_USERNAME , codec . encode ( normalizedEmail ) , { timeout : NATS_CONFIG . REQUEST_TIMEOUT } ) ;
378+
379+ const responseText = codec . decode ( response . data ) ;
380+
381+ // Parse once and branch on the result shape
382+ let username : string ;
383+ try {
384+ const parsed = JSON . parse ( responseText ) ;
385+
386+ // Check if it's an error response
387+ if ( typeof parsed === 'object' && parsed !== null && parsed . success === false ) {
388+ req . log . info ( { email : normalizedEmail , error : parsed . error } , 'User email not found via NATS' ) ;
389+
390+ throw new ResourceNotFoundError ( 'User' , normalizedEmail , {
391+ operation : 'resolve_email_to_username' ,
392+ service : 'project_service' ,
393+ path : '/nats/email-to-username' ,
394+ } ) ;
395+ }
396+
397+ // Extract username from JSON success response or JSON string
398+ username = typeof parsed === 'string' ? parsed : parsed . username ;
399+ } catch ( parseError ) {
400+ // Re-throw ResourceNotFoundError as-is
401+ if ( parseError instanceof ResourceNotFoundError ) {
402+ throw parseError ;
403+ }
404+
405+ // JSON parsing failed - use raw text as username
406+ username = responseText ;
407+ }
408+
409+ // Trim and validate username
410+ username = username . trim ( ) ;
411+
412+ if ( ! username || username === '' ) {
413+ req . log . info ( { email : normalizedEmail } , 'Empty username returned from NATS' ) ;
414+
415+ throw new ResourceNotFoundError ( 'User' , normalizedEmail , {
416+ operation : 'resolve_email_to_username' ,
417+ service : 'project_service' ,
418+ path : '/nats/email-to-username' ,
419+ } ) ;
420+ }
421+
422+ req . log . info ( { email : normalizedEmail , username } , 'Successfully resolved email to username' ) ;
423+
424+ return username ;
425+ } catch ( error ) {
426+ // Re-throw ResourceNotFoundError as-is
427+ if ( error instanceof ResourceNotFoundError ) {
428+ throw error ;
429+ }
430+
431+ req . log . error ( { error : error instanceof Error ? error . message : error , email : normalizedEmail } , 'Failed to resolve email to username via NATS' ) ;
432+
433+ // If it's a timeout or no responder error, treat as not found
434+ if ( error instanceof Error && ( error . message . includes ( 'timeout' ) || error . message . includes ( '503' ) ) ) {
435+ throw new ResourceNotFoundError ( 'User' , normalizedEmail , {
436+ operation : 'resolve_email_to_username' ,
437+ service : 'project_service' ,
438+ path : '/nats/email-to-username' ,
439+ } ) ;
440+ }
441+
442+ throw error ;
443+ }
444+ }
445+
446+ /**
447+ * Fetch user information by username or email using NATS request-reply pattern
448+ * For emails, it resolves to username first, then uses the username for user metadata lookup
350449 * @param req - Express request object for logging
351450 * @param usernameOrEmail - Username or email to lookup
352451 * @returns UserInfo object with name, email, username, and optional avatar
@@ -355,51 +454,65 @@ export class ProjectService {
355454 public async getUserInfo ( req : Request , usernameOrEmail : string ) : Promise < { name : string ; email : string ; username : string ; avatar ?: string } > {
356455 const codec = this . natsService . getCodec ( ) ;
357456
358- // First, check if input is an email and resolve to username if needed
359- let username = usernameOrEmail . trim ( ) ;
457+ // For emails, resolve to username for the NATS lookup
458+ // For usernames, use them directly
459+ let usernameForLookup = usernameOrEmail . trim ( ) ;
460+ let originalEmail = '' ;
461+
360462 if ( usernameOrEmail . includes ( '@' ) ) {
361- username = await this . resolveEmailToUsername ( req , usernameOrEmail ) ;
463+ originalEmail = usernameOrEmail ;
464+ // First confirm the user exists with email_to_sub
465+ await this . resolveEmailToSub ( req , usernameOrEmail ) ;
466+ // Then get the username for user metadata lookup
467+ usernameForLookup = await this . resolveEmailToUsername ( req , usernameOrEmail ) ;
468+ req . log . info ( { email : originalEmail , resolvedUsername : usernameForLookup } , 'Resolved email to username for user metadata lookup' ) ;
362469 }
363470
364471 try {
365- req . log . info ( { username } , 'Fetching user info via NATS' ) ;
472+ req . log . info ( { username : usernameForLookup } , 'Fetching user metadata via NATS' ) ;
366473
367- const response = await this . natsService . request ( NatsSubjects . USERNAME_TO_USER_INFO , codec . encode ( username ) , { timeout : NATS_CONFIG . REQUEST_TIMEOUT } ) ;
474+ const response = await this . natsService . request ( NatsSubjects . USER_METADATA_READ , codec . encode ( usernameForLookup ) , {
475+ timeout : NATS_CONFIG . REQUEST_TIMEOUT ,
476+ } ) ;
368477
369478 const responseText = codec . decode ( response . data ) ;
370- const userInfo = JSON . parse ( responseText ) ;
479+ const userMetadata = JSON . parse ( responseText ) ;
371480
372481 // Validate response structure
373- if ( ! userInfo || typeof userInfo !== 'object' ) {
374- throw new ResourceNotFoundError ( 'User' , username , {
482+ if ( ! userMetadata || typeof userMetadata !== 'object' ) {
483+ throw new ResourceNotFoundError ( 'User' , usernameForLookup , {
375484 operation : 'get_user_info' ,
376485 service : 'project_service' ,
377- path : '/nats/username-to- user-info ' ,
486+ path : '/nats/user-metadata-read ' ,
378487 } ) ;
379488 }
380489
381490 // Check if it's an error response
382- if ( userInfo . success === false ) {
383- req . log . info ( { username, error : userInfo . error } , 'User not found via NATS' ) ;
491+ if ( userMetadata . success === false ) {
492+ req . log . info ( { username : usernameForLookup , error : userMetadata . error } , 'User metadata not found via NATS' ) ;
384493
385- throw new ResourceNotFoundError ( 'User' , username , {
494+ throw new ResourceNotFoundError ( 'User' , usernameForLookup , {
386495 operation : 'get_user_info' ,
387496 service : 'project_service' ,
388- path : '/nats/username-to- user-info ' ,
497+ path : '/nats/user-metadata-read ' ,
389498 } ) ;
390499 }
391500
392- req . log . info ( { username } , 'Successfully fetched user info' ) ;
501+ req . log . info ( { username : usernameForLookup } , 'Successfully fetched user metadata' ) ;
502+
503+ const userData = userMetadata . data || { } ;
393504
394505 const result : { name : string ; email : string ; username : string ; avatar ?: string } = {
395- name : userInfo . name || '' ,
396- email : userInfo . email || '' ,
397- username : userInfo . username || username ,
506+ // Use the name from metadata, fallback to constructed name from given_name/family_name
507+ name : userData . name || `${ userData . given_name || '' } ${ userData . family_name || '' } ` . trim ( ) || usernameForLookup ,
508+ // Use the original email if we had one, otherwise leave empty
509+ email : originalEmail || '' ,
510+ username : usernameForLookup ,
398511 } ;
399512
400- // Only include avatar if it exists and is not empty
401- if ( userInfo . avatar && userInfo . avatar . trim ( ) !== '' ) {
402- result . avatar = userInfo . avatar ;
513+ // Use picture field as avatar if available
514+ if ( userData . picture && userData . picture . trim ( ) !== '' ) {
515+ result . avatar = userData . picture ;
403516 }
404517
405518 return result ;
@@ -409,14 +522,14 @@ export class ProjectService {
409522 throw error ;
410523 }
411524
412- req . log . error ( { error : error instanceof Error ? error . message : error , username } , 'Failed to fetch user info via NATS' ) ;
525+ req . log . error ( { error : error instanceof Error ? error . message : error , username : usernameForLookup } , 'Failed to fetch user metadata via NATS' ) ;
413526
414527 // If it's a timeout or no responder error, treat as not found
415528 if ( error instanceof Error && ( error . message . includes ( 'timeout' ) || error . message . includes ( '503' ) ) ) {
416- throw new ResourceNotFoundError ( 'User' , username , {
529+ throw new ResourceNotFoundError ( 'User' , usernameForLookup , {
417530 operation : 'get_user_info' ,
418531 service : 'project_service' ,
419- path : '/nats/username-to- user-info ' ,
532+ path : '/nats/user-metadata-read ' ,
420533 } ) ;
421534 }
422535
0 commit comments