diff --git a/apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.html b/apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.html index 537b3860..00327859 100644 --- a/apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.html +++ b/apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.html @@ -4,19 +4,19 @@
- +
- + @if (form().get('username')?.errors?.['required'] && form().get('username')?.touched) { -

Username is required

+

Username or email is required

}
diff --git a/apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.ts b/apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.ts index daac58e4..bd4468f8 100644 --- a/apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.ts +++ b/apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.ts @@ -1,6 +1,7 @@ // Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT +import { HttpErrorResponse } from '@angular/common/http'; import { Component, computed, inject, signal } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { ButtonComponent } from '@components/button/button.component'; @@ -92,13 +93,29 @@ export class UserFormComponent { }); this.dialogRef.close(true); }, - error: (error: any) => { + error: (error: HttpErrorResponse) => { console.error('Error saving user:', error); - this.messageService.add({ - severity: 'error', - summary: 'Error', - detail: `Failed to ${this.isEditing() ? 'update' : 'add'} user. Please try again.`, - }); + + // Check if it's a 404 error for email not found + if (error.status === 404 && error.error?.code === 'NOT_FOUND') { + const usernameValue = formValue.username; + const isEmail = usernameValue.includes('@'); + + this.messageService.add({ + severity: 'error', + summary: 'User Not Found', + detail: isEmail + ? `No user found with email address "${usernameValue}". Please verify the email address and try again.` + : error.error?.message || 'User not found', + }); + } else { + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: error.error?.message || `Failed to ${this.isEditing() ? 'update' : 'add'} user. Please try again.`, + }); + } + this.submitting.set(false); }, }); diff --git a/apps/lfx-one/src/server/controllers/project.controller.ts b/apps/lfx-one/src/server/controllers/project.controller.ts index 19aaba02..a05b7346 100644 --- a/apps/lfx-one/src/server/controllers/project.controller.ts +++ b/apps/lfx-one/src/server/controllers/project.controller.ts @@ -259,12 +259,39 @@ export class ProjectController { return; } - const result = await this.projectService.updateProjectPermissions(req, uid, 'add', userData.username, userData.role); + // Detect if input is email or username + const isEmail = userData.username.includes('@'); + let username = userData.username; + + if (isEmail) { + req.log.info( + { + email: userData.username, + operation: 'add_user_project_permissions', + }, + 'Email detected, resolving to username via NATS' + ); + + // Resolve email to username via NATS + username = await this.projectService.resolveEmailToUsername(req, userData.username); + + req.log.info( + { + email: userData.username, + username, + operation: 'add_user_project_permissions', + }, + 'Successfully resolved email to username' + ); + } + + const result = await this.projectService.updateProjectPermissions(req, uid, 'add', username, userData.role); Logger.success(req, 'add_user_project_permissions', startTime, { uid, - username: userData.username, + username, role: userData.role, + resolved_from_email: isEmail, }); res.status(201).json(result); diff --git a/apps/lfx-one/src/server/services/project.service.ts b/apps/lfx-one/src/server/services/project.service.ts index 7ee3e5df..7df4153c 100644 --- a/apps/lfx-one/src/server/services/project.service.ts +++ b/apps/lfx-one/src/server/services/project.service.ts @@ -207,6 +207,91 @@ export class ProjectService { return result; } + /** + * Resolve email address to username using NATS request-reply pattern + * @param req - Express request object for logging + * @param email - Email address to lookup + * @returns Username associated with the email + * @throws ResourceNotFoundError if user not found + */ + public async resolveEmailToUsername(req: Request, email: string): Promise { + const codec = this.natsService.getCodec(); + + // Normalize email input + const normalizedEmail = email.trim().toLowerCase(); + + try { + req.log.info({ email: normalizedEmail }, 'Resolving email to username via NATS'); + + const response = await this.natsService.request(NatsSubjects.EMAIL_TO_USERNAME, codec.encode(normalizedEmail), { timeout: NATS_CONFIG.REQUEST_TIMEOUT }); + + const responseText = codec.decode(response.data); + + // Parse once and branch on the result shape + let username: string; + try { + const parsed = JSON.parse(responseText); + + // Check if it's an error response + if (typeof parsed === 'object' && parsed !== null && parsed.success === false) { + req.log.info({ email: normalizedEmail, error: parsed.error }, 'User email not found via NATS'); + + throw new ResourceNotFoundError('User', normalizedEmail, { + operation: 'resolve_email_to_username', + service: 'project_service', + path: '/nats/email-to-username', + }); + } + + // Extract username from JSON success response or JSON string + username = typeof parsed === 'string' ? parsed : parsed.username; + } catch (parseError) { + // Re-throw ResourceNotFoundError as-is + if (parseError instanceof ResourceNotFoundError) { + throw parseError; + } + + // JSON parsing failed - use raw text as username + username = responseText; + } + + // Trim and validate username + username = username.trim(); + + if (!username || username === '') { + req.log.info({ email: normalizedEmail }, 'Empty username returned from NATS'); + + throw new ResourceNotFoundError('User', normalizedEmail, { + operation: 'resolve_email_to_username', + service: 'project_service', + path: '/nats/email-to-username', + }); + } + + req.log.info({ email: normalizedEmail, username }, 'Successfully resolved email to username'); + + return username; + } catch (error) { + // Re-throw ResourceNotFoundError as-is + if (error instanceof ResourceNotFoundError) { + throw error; + } + + req.log.error({ error: error instanceof Error ? error.message : error, email: normalizedEmail }, 'Failed to resolve email to username via NATS'); + + // If it's a timeout or no responder error, treat as not found + if (error instanceof Error && (error.message.includes('timeout') || error.message.includes('503'))) { + throw new ResourceNotFoundError('User', normalizedEmail, { + operation: 'resolve_email_to_username', + service: 'project_service', + path: '/nats/email-to-username', + }); + } + + throw error; + } + } + /** * Get project ID by slug using NATS request-reply pattern * @private diff --git a/packages/shared/src/enums/nats.enum.ts b/packages/shared/src/enums/nats.enum.ts index 5cd1f2d4..e4435e67 100644 --- a/packages/shared/src/enums/nats.enum.ts +++ b/packages/shared/src/enums/nats.enum.ts @@ -7,4 +7,5 @@ export enum NatsSubjects { PROJECT_SLUG_TO_UID = 'lfx.projects-api.slug_to_uid', USER_METADATA_UPDATE = 'lfx.auth-service.user_metadata.update', + EMAIL_TO_USERNAME = 'lfx.auth-service.email_to_username', } diff --git a/packages/shared/src/interfaces/auth.interface.ts b/packages/shared/src/interfaces/auth.interface.ts index c9b0ec82..efa5e823 100644 --- a/packages/shared/src/interfaces/auth.interface.ts +++ b/packages/shared/src/interfaces/auth.interface.ts @@ -161,3 +161,14 @@ export interface AuthConfig { /** Default route type for unmatched routes */ defaultType: RouteType; } + +/** + * Error response from email to username NATS lookup + * @description Response structure when user email is not found + */ +export interface EmailToUsernameErrorResponse { + /** Success flag - always false for error responses */ + success: false; + /** Error message describing the failure */ + error: string; +}