Skip to content

Commit 6eb913a

Browse files
asithadeclaude
andauthored
feat(profile): nats-based user profile updates with oidc fields (#111)
* feat(profile): nats-based user profile updates with oidc fields - Refactor user profile interfaces to OIDC-compliant field names - Add NATS messaging for user metadata updates via auth-service - Refactor NatsService to pure infrastructure pattern - Consolidate shared interfaces and remove duplicates - Improve error handling with proper error classes - Update frontend forms to use new field names - Minor UI improvements to project layout JIRA: LFXV2-633, LFXV2-634, LFXV2-635, LFXV2-636, LFXV2-637 Signed-off-by: Asitha De Silva <adesilva@linuxfoundation.org> Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix(profile): cleanup type improvements and comments LFXV2-633 LFXV2-636 LFXV2-637 - Remove outdated field mapping comments from profile-edit component - Add proper type imports for ProfileUpdateRequest and UserMetadata - Fix error message accuracy (Username vs User ID) - Replace any type with proper UserMetadata type in validation 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Asitha de Silva <asithade@gmail.com> --------- Signed-off-by: Asitha De Silva <adesilva@linuxfoundation.org> Signed-off-by: Asitha de Silva <asithade@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8aecaec commit 6eb913a

File tree

19 files changed

+574
-328
lines changed

19 files changed

+574
-328
lines changed

apps/lfx-one/src/app/layouts/profile-layout/profile-layout.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export class ProfileLayoutComponent {
145145
const profile = this.profile();
146146
if (!profile?.profile) return '';
147147

148-
return profile.profile.title || '';
148+
return profile.profile.job_title || '';
149149
});
150150
}
151151

@@ -156,7 +156,7 @@ export class ProfileLayoutComponent {
156156

157157
const parts = [];
158158
if (profile.profile.city) parts.push(profile.profile.city);
159-
if (profile.profile.state) parts.push(profile.profile.state);
159+
if (profile.profile.state_province) parts.push(profile.profile.state_province);
160160
if (profile.profile.country) parts.push(profile.profile.country);
161161

162162
return parts.join(', ');

apps/lfx-one/src/app/layouts/project-layout/project-layout.component.html

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,28 @@
1717
</lfx-breadcrumb>
1818
</div>
1919

20-
<div class="flex flex-col md:flex-row md:items-center justify-between">
21-
<div class="flex flex-col items-start gap-3">
20+
<div class="flex flex-col md:flex-row md:items-center justify-between w-full">
21+
<div class="flex justify-between items-start gap-3 w-full">
22+
<div class="flex flex-col gap-1 w-full">
23+
<!-- Project Title -->
24+
<h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
25+
{{ projectTitle() }}
26+
</h1>
27+
28+
<!-- Project Description -->
29+
@if (projectDescription(); as description) {
30+
<p class="text-gray-600 text-sm mb-6 max-w-3xl leading-relaxed">
31+
{{ description }}
32+
</p>
33+
}
34+
</div>
35+
2236
<!-- Category Badge -->
2337
@if (categoryLabel(); as category) {
2438
<div class="flex justify-start">
25-
<img [src]="projectLogo()" class="h-10 w-full" />
39+
<img [src]="projectLogo()" class="h-16 w-full" />
2640
</div>
2741
}
28-
29-
<!-- Project Title -->
30-
<h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
31-
{{ projectTitle() }}
32-
</h1>
33-
34-
<!-- Project Description -->
35-
@if (projectDescription(); as description) {
36-
<p class="text-gray-600 text-sm mb-6 max-w-3xl leading-relaxed">
37-
{{ description }}
38-
</p>
39-
}
4042
</div>
4143
</div>
4244

apps/lfx-one/src/app/modules/profile/edit/profile-edit.component.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ <h3 class="text-base font-medium text-gray-900" data-testid="profile-edit-person
3737
<lfx-input-text
3838
size="small"
3939
[form]="profileForm"
40-
control="first_name"
40+
control="given_name"
4141
id="first-name"
4242
placeholder="Enter your first name"
4343
styleClass="w-full"
@@ -51,7 +51,7 @@ <h3 class="text-base font-medium text-gray-900" data-testid="profile-edit-person
5151
<lfx-input-text
5252
size="small"
5353
[form]="profileForm"
54-
control="last_name"
54+
control="family_name"
5555
id="last-name"
5656
placeholder="Enter your last name"
5757
styleClass="w-full"
@@ -97,7 +97,7 @@ <h3 class="text-base font-medium text-gray-900" data-testid="profile-edit-profes
9797
<lfx-input-text
9898
size="small"
9999
[form]="profileForm"
100-
control="title"
100+
control="job_title"
101101
id="job-title"
102102
placeholder="Enter your job title"
103103
styleClass="w-full"
@@ -167,7 +167,7 @@ <h3 class="text-base font-medium text-gray-900" data-testid="profile-edit-locati
167167
<lfx-select
168168
size="small"
169169
[form]="profileForm"
170-
control="state"
170+
control="state_province"
171171
[options]="stateOptions"
172172
id="state"
173173
placeholder="Select your state"
@@ -179,7 +179,7 @@ <h3 class="text-base font-medium text-gray-900" data-testid="profile-edit-locati
179179
<lfx-input-text
180180
size="small"
181181
[form]="profileForm"
182-
control="state"
182+
control="state_province"
183183
id="state"
184184
placeholder="Enter your state or province"
185185
styleClass="w-full"
@@ -222,7 +222,7 @@ <h3 class="text-base font-medium text-gray-900" data-testid="profile-edit-locati
222222
<lfx-input-text
223223
size="small"
224224
[form]="profileForm"
225-
control="zipcode"
225+
control="postal_code"
226226
id="zipcode"
227227
placeholder="Enter your postal or zip code"
228228
styleClass="w-full"
@@ -269,7 +269,7 @@ <h3 class="text-base font-medium text-gray-900" data-testid="profile-edit-additi
269269
<lfx-select
270270
size="small"
271271
[form]="profileForm"
272-
control="tshirt_size"
272+
control="t_shirt_size"
273273
[options]="tshirtSizeOptions"
274274
id="tshirt-size"
275275
placeholder="Select your t-shirt size"

apps/lfx-one/src/app/modules/profile/edit/profile-edit.component.ts

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Component, computed, inject, OnInit, Signal, signal } from '@angular/co
66
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
77
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
88
import { COUNTRIES, markFormControlsAsTouched, TSHIRT_SIZES, US_STATES } from '@lfx-one/shared';
9-
import { CombinedProfile, UpdateProfileDetailsRequest, UpdateUserProfileRequest } from '@lfx-one/shared/interfaces';
9+
import { CombinedProfile, ProfileUpdateRequest, UserMetadata } from '@lfx-one/shared/interfaces';
1010
import { UserService } from '@services/user.service';
1111
import { ButtonComponent } from '@shared/components/button/button.component';
1212
import { CardComponent } from '@shared/components/card/card.component';
@@ -16,7 +16,7 @@ import { SelectComponent } from '@shared/components/select/select.component';
1616
import { MessageService } from 'primeng/api';
1717
import { ToastModule } from 'primeng/toast';
1818
import { TooltipModule } from 'primeng/tooltip';
19-
import { BehaviorSubject, catchError, finalize, forkJoin, of, switchMap, tap } from 'rxjs';
19+
import { BehaviorSubject, catchError, finalize, of, switchMap, tap } from 'rxjs';
2020

2121
@Component({
2222
selector: 'lfx-profile-edit',
@@ -78,23 +78,23 @@ export class ProfileEditComponent implements OnInit {
7878
return this.selectedCountrySignal() === 'United States';
7979
});
8080

81-
// Profile form
81+
// Profile form with backend-aligned field names
8282
public profileForm: FormGroup = this.fb.group({
83-
// User table fields
84-
first_name: ['', [Validators.maxLength(50)]],
85-
last_name: ['', [Validators.maxLength(50)]],
83+
// Direct user fields
8684
username: [{ value: '', disabled: true }, [Validators.required, Validators.minLength(3), Validators.maxLength(30)]],
8785

88-
// Profile table fields
89-
title: ['', [Validators.maxLength(100)]],
86+
// User metadata fields
87+
given_name: ['', [Validators.maxLength(50)]],
88+
family_name: ['', [Validators.maxLength(50)]],
89+
job_title: ['', [Validators.maxLength(100)]],
9090
organization: ['', [Validators.maxLength(100)]],
9191
country: ['', [Validators.maxLength(50)]],
92-
state: ['', [Validators.maxLength(50)]],
92+
state_province: ['', [Validators.maxLength(50)]],
9393
city: ['', [Validators.maxLength(50)]],
9494
address: ['', [Validators.maxLength(200)]],
95-
zipcode: ['', [Validators.maxLength(20)]],
95+
postal_code: ['', [Validators.maxLength(20)]],
9696
phone_number: ['', [Validators.maxLength(20)]],
97-
tshirt_size: ['', []],
97+
t_shirt_size: ['', []],
9898
});
9999

100100
public constructor() {
@@ -106,9 +106,9 @@ export class ProfileEditComponent implements OnInit {
106106
.subscribe((country: string) => {
107107
this.selectedCountrySignal.set(country || '');
108108

109-
// Clear state field when country changes to avoid invalid state/country combinations
109+
// Clear state_province field when country changes to avoid invalid state/country combinations
110110
if (country !== 'United States') {
111-
this.profileForm.get('state')?.setValue('');
111+
this.profileForm.get('state_province')?.setValue('');
112112
}
113113
});
114114
}
@@ -137,28 +137,29 @@ export class ProfileEditComponent implements OnInit {
137137

138138
this.savingSignal.set(true);
139139

140-
// Prepare user update data
141-
const userUpdate: UpdateUserProfileRequest = {
142-
first_name: formValue.first_name || null,
143-
last_name: formValue.last_name || null,
144-
username: formValue.username || null,
140+
// Build user_metadata only if there are fields to update
141+
const userMetadata: Partial<UserMetadata> = {
142+
given_name: formValue.given_name || undefined,
143+
family_name: formValue.family_name || undefined,
144+
job_title: formValue.job_title || undefined,
145+
organization: formValue.organization || undefined,
146+
country: formValue.country || undefined,
147+
state_province: formValue.state_province || undefined,
148+
city: formValue.city || undefined,
149+
address: formValue.address || undefined,
150+
postal_code: formValue.postal_code || undefined,
151+
phone_number: formValue.phone_number || undefined,
152+
t_shirt_size: formValue.t_shirt_size || undefined,
145153
};
146154

147-
// Prepare profile update data
148-
const profileUpdate: UpdateProfileDetailsRequest = {
149-
title: formValue.title || null,
150-
organization: formValue.organization || null,
151-
country: formValue.country || null,
152-
state: formValue.state || null,
153-
city: formValue.city || null,
154-
address: formValue.address || null,
155-
zipcode: formValue.zipcode || null,
156-
phone_number: formValue.phone_number || null,
157-
tshirt_size: formValue.tshirt_size || null,
155+
// Prepare update data - only send user_metadata
156+
const updateData: ProfileUpdateRequest = {
157+
user_metadata: userMetadata as UserMetadata,
158158
};
159159

160-
// Update both user and profile data in parallel using forkJoin
161-
forkJoin([this.userService.updateUserInfo(userUpdate), this.userService.updateProfileDetails(profileUpdate)])
160+
// Update profile via unified endpoint
161+
this.userService
162+
.updateUserProfile(updateData)
162163
.pipe(finalize(() => this.savingSignal.set(false)))
163164
.subscribe({
164165
next: () => {
@@ -220,21 +221,18 @@ export class ProfileEditComponent implements OnInit {
220221
const countryValue = profile.profile?.country || '';
221222

222223
this.profileForm.patchValue({
223-
// User fields
224-
first_name: profile.user.first_name || '',
225-
last_name: profile.user.last_name || '',
224+
given_name: profile.user.first_name || '',
225+
family_name: profile.user.last_name || '',
226226
username: profile.user.username || '',
227-
228-
// Profile fields
229-
title: profile.profile?.title || '',
227+
job_title: profile.profile?.job_title || '',
230228
organization: profile.profile?.organization || '',
231229
country: countryValue,
232-
state: profile.profile?.state || '',
230+
state_province: profile.profile?.state_province || '',
233231
city: profile.profile?.city || '',
234232
address: profile.profile?.address || '',
235-
zipcode: profile.profile?.zipcode || '',
233+
postal_code: profile.profile?.postal_code || '',
236234
phone_number: profile.profile?.phone_number || '',
237-
tshirt_size: profile.profile?.tshirt_size || '',
235+
t_shirt_size: profile.profile?.t_shirt_size || '',
238236
});
239237

240238
// Set the initial country signal value

apps/lfx-one/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,9 @@ export class MeetingCardComponent implements OnInit {
239239
}
240240

241241
public copyMeetingLink(): void {
242-
const meetingLink = environment.urls.home + '/meetings/' + this.meeting().uid;
243-
this.clipboard.copy(meetingLink);
242+
const meetingUrl: URL = new URL(environment.urls.home + '/meetings/' + this.meeting().uid);
243+
meetingUrl.searchParams.set('password', this.meeting().password || '');
244+
this.clipboard.copy(meetingUrl.toString());
244245
this.messageService.add({
245246
severity: 'success',
246247
summary: 'Meeting Link Copied',

apps/lfx-one/src/app/shared/services/user.service.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ import {
1010
CreateUserPermissionRequest,
1111
EmailManagementData,
1212
EmailPreferences,
13+
ProfileUpdateRequest,
1314
TwoFactorSettings,
1415
UpdateEmailPreferencesRequest,
15-
UpdateProfileDetailsRequest,
16-
UpdateUserProfileRequest,
1716
User,
1817
UserEmail,
1918
} from '@lfx-one/shared/interfaces';
@@ -43,17 +42,11 @@ export class UserService {
4342
}
4443

4544
/**
46-
* Update user info fields (first_name, last_name, username)
45+
* Update user profile metadata via unified NATS endpoint
46+
* Only sends user_metadata - username and email are extracted from OIDC claim on backend
4747
*/
48-
public updateUserInfo(data: UpdateUserProfileRequest): Observable<User> {
49-
return this.http.patch<User>('/api/profile/user', data).pipe(take(1));
50-
}
51-
52-
/**
53-
* Update profile details fields (title, organization, location, etc.)
54-
*/
55-
public updateProfileDetails(data: UpdateProfileDetailsRequest): Observable<any> {
56-
return this.http.patch('/api/profile/details', data).pipe(take(1));
48+
public updateUserProfile(data: ProfileUpdateRequest): Observable<any> {
49+
return this.http.patch('/api/profile', data).pipe(take(1));
5750
}
5851

5952
// Email management methods

0 commit comments

Comments
 (0)