Skip to content

Commit aaf5be3

Browse files
authored
feat(permissions): add email-to-username lookup for users (#115)
1 parent 8fc47f2 commit aaf5be3

File tree

6 files changed

+153
-12
lines changed

6 files changed

+153
-12
lines changed

apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
<form [formGroup]="form()" (ngSubmit)="onSubmit()" class="space-y-6">
55
<!-- User Information Section -->
66
<div class="flex flex-col gap-3">
7-
<!-- Username -->
7+
<!-- Username or Email -->
88
<div>
9-
<label for="username" class="block text-sm font-medium text-gray-700 mb-1"> Username <span class="text-red-500">*</span> </label>
9+
<label for="username" class="block text-sm font-medium text-gray-700 mb-1"> Username or Email <span class="text-red-500">*</span> </label>
1010
<lfx-input-text
1111
size="small"
1212
[form]="form()"
1313
control="username"
1414
id="username"
15-
placeholder="Enter username"
15+
placeholder="Enter username or email address"
1616
styleClass="w-full"
1717
data-testid="settings-user-form-username"></lfx-input-text>
1818
@if (form().get('username')?.errors?.['required'] && form().get('username')?.touched) {
19-
<p class="mt-1 text-xs text-red-600">Username is required</p>
19+
<p class="mt-1 text-xs text-red-600">Username or email is required</p>
2020
}
2121
</div>
2222
</div>

apps/lfx-one/src/app/modules/project/settings/components/user-form/user-form.component.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright The Linux Foundation and each contributor to LFX.
22
// SPDX-License-Identifier: MIT
33

4+
import { HttpErrorResponse } from '@angular/common/http';
45
import { Component, computed, inject, signal } from '@angular/core';
56
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
67
import { ButtonComponent } from '@components/button/button.component';
@@ -92,13 +93,29 @@ export class UserFormComponent {
9293
});
9394
this.dialogRef.close(true);
9495
},
95-
error: (error: any) => {
96+
error: (error: HttpErrorResponse) => {
9697
console.error('Error saving user:', error);
97-
this.messageService.add({
98-
severity: 'error',
99-
summary: 'Error',
100-
detail: `Failed to ${this.isEditing() ? 'update' : 'add'} user. Please try again.`,
101-
});
98+
99+
// Check if it's a 404 error for email not found
100+
if (error.status === 404 && error.error?.code === 'NOT_FOUND') {
101+
const usernameValue = formValue.username;
102+
const isEmail = usernameValue.includes('@');
103+
104+
this.messageService.add({
105+
severity: 'error',
106+
summary: 'User Not Found',
107+
detail: isEmail
108+
? `No user found with email address "${usernameValue}". Please verify the email address and try again.`
109+
: error.error?.message || 'User not found',
110+
});
111+
} else {
112+
this.messageService.add({
113+
severity: 'error',
114+
summary: 'Error',
115+
detail: error.error?.message || `Failed to ${this.isEditing() ? 'update' : 'add'} user. Please try again.`,
116+
});
117+
}
118+
102119
this.submitting.set(false);
103120
},
104121
});

apps/lfx-one/src/server/controllers/project.controller.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,39 @@ export class ProjectController {
259259
return;
260260
}
261261

262-
const result = await this.projectService.updateProjectPermissions(req, uid, 'add', userData.username, userData.role);
262+
// Detect if input is email or username
263+
const isEmail = userData.username.includes('@');
264+
let username = userData.username;
265+
266+
if (isEmail) {
267+
req.log.info(
268+
{
269+
email: userData.username,
270+
operation: 'add_user_project_permissions',
271+
},
272+
'Email detected, resolving to username via NATS'
273+
);
274+
275+
// Resolve email to username via NATS
276+
username = await this.projectService.resolveEmailToUsername(req, userData.username);
277+
278+
req.log.info(
279+
{
280+
email: userData.username,
281+
username,
282+
operation: 'add_user_project_permissions',
283+
},
284+
'Successfully resolved email to username'
285+
);
286+
}
287+
288+
const result = await this.projectService.updateProjectPermissions(req, uid, 'add', username, userData.role);
263289

264290
Logger.success(req, 'add_user_project_permissions', startTime, {
265291
uid,
266-
username: userData.username,
292+
username,
267293
role: userData.role,
294+
resolved_from_email: isEmail,
268295
});
269296

270297
res.status(201).json(result);

apps/lfx-one/src/server/services/project.service.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,91 @@ export class ProjectService {
207207
return result;
208208
}
209209

210+
/**
211+
* Resolve email address to username using NATS request-reply pattern
212+
* @param req - Express request object for logging
213+
* @param email - Email address to lookup
214+
* @returns Username associated with the email
215+
* @throws ResourceNotFoundError if user not found
216+
*/
217+
public async resolveEmailToUsername(req: Request, email: string): Promise<string> {
218+
const codec = this.natsService.getCodec();
219+
220+
// Normalize email input
221+
const normalizedEmail = email.trim().toLowerCase();
222+
223+
try {
224+
req.log.info({ email: normalizedEmail }, 'Resolving email to username via NATS');
225+
226+
const response = await this.natsService.request(NatsSubjects.EMAIL_TO_USERNAME, codec.encode(normalizedEmail), { timeout: NATS_CONFIG.REQUEST_TIMEOUT });
227+
228+
const responseText = codec.decode(response.data);
229+
230+
// Parse once and branch on the result shape
231+
let username: string;
232+
try {
233+
const parsed = JSON.parse(responseText);
234+
235+
// Check if it's an error response
236+
if (typeof parsed === 'object' && parsed !== null && parsed.success === false) {
237+
req.log.info({ email: normalizedEmail, error: parsed.error }, 'User email not found via NATS');
238+
239+
throw new ResourceNotFoundError('User', normalizedEmail, {
240+
operation: 'resolve_email_to_username',
241+
service: 'project_service',
242+
path: '/nats/email-to-username',
243+
});
244+
}
245+
246+
// Extract username from JSON success response or JSON string
247+
username = typeof parsed === 'string' ? parsed : parsed.username;
248+
} catch (parseError) {
249+
// Re-throw ResourceNotFoundError as-is
250+
if (parseError instanceof ResourceNotFoundError) {
251+
throw parseError;
252+
}
253+
254+
// JSON parsing failed - use raw text as username
255+
username = responseText;
256+
}
257+
258+
// Trim and validate username
259+
username = username.trim();
260+
261+
if (!username || username === '') {
262+
req.log.info({ email: normalizedEmail }, 'Empty username returned from NATS');
263+
264+
throw new ResourceNotFoundError('User', normalizedEmail, {
265+
operation: 'resolve_email_to_username',
266+
service: 'project_service',
267+
path: '/nats/email-to-username',
268+
});
269+
}
270+
271+
req.log.info({ email: normalizedEmail, username }, 'Successfully resolved email to username');
272+
273+
return username;
274+
} catch (error) {
275+
// Re-throw ResourceNotFoundError as-is
276+
if (error instanceof ResourceNotFoundError) {
277+
throw error;
278+
}
279+
280+
req.log.error({ error: error instanceof Error ? error.message : error, email: normalizedEmail }, 'Failed to resolve email to username via NATS');
281+
282+
// If it's a timeout or no responder error, treat as not found
283+
if (error instanceof Error && (error.message.includes('timeout') || error.message.includes('503'))) {
284+
throw new ResourceNotFoundError('User', normalizedEmail, {
285+
operation: 'resolve_email_to_username',
286+
service: 'project_service',
287+
path: '/nats/email-to-username',
288+
});
289+
}
290+
291+
throw error;
292+
}
293+
}
294+
210295
/**
211296
* Get project ID by slug using NATS request-reply pattern
212297
* @private

packages/shared/src/enums/nats.enum.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
export enum NatsSubjects {
88
PROJECT_SLUG_TO_UID = 'lfx.projects-api.slug_to_uid',
99
USER_METADATA_UPDATE = 'lfx.auth-service.user_metadata.update',
10+
EMAIL_TO_USERNAME = 'lfx.auth-service.email_to_username',
1011
}

packages/shared/src/interfaces/auth.interface.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,14 @@ export interface AuthConfig {
161161
/** Default route type for unmatched routes */
162162
defaultType: RouteType;
163163
}
164+
165+
/**
166+
* Error response from email to username NATS lookup
167+
* @description Response structure when user email is not found
168+
*/
169+
export interface EmailToUsernameErrorResponse {
170+
/** Success flag - always false for error responses */
171+
success: false;
172+
/** Error message describing the failure */
173+
error: string;
174+
}

0 commit comments

Comments
 (0)