Skip to content

Commit a257f4a

Browse files
committed
feat(meetings): add join URL endpoint and improve authentication flow
LFXV2-458: Implement meeting join URL functionality with selective authentication - Add /public/api/meetings/:id/join-url endpoint - Enhance auth middleware for public route handling - Improve meeting components for join URL management - Add M2M token support for server-side API calls - Create custom authentication error handling Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent 3fbee9f commit a257f4a

File tree

19 files changed

+426
-137
lines changed

19 files changed

+426
-137
lines changed

apps/lfx-pcc/src/app/modules/meeting/meeting.component.html

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22
<!-- SPDX-License-Identifier: MIT -->
33

44
@if (meeting()) {
5-
<div class="min-h-screen bg-gray-50">
5+
<div class="bg-gray-50">
66
<!-- Main Content -->
77
<div class="container mx-auto py-6 px-8">
88
<div class="max-w-4xl mx-auto">
9+
<!-- Project Logo -->
10+
@if (project()?.logo_url) {
11+
<img src="{{ project()?.logo_url }}" alt="{{ project()?.name }}" class="w-full h-20 mb-6" />
12+
}
13+
914
<!-- Meeting Information Card -->
1015
<lfx-card class="mb-6" data-testid="meeting-info-card">
1116
<!-- Header with badges -->
@@ -40,8 +45,8 @@
4045
</div>
4146

4247
<!-- Meeting Title and Date -->
43-
<div class="flex items-start justify-between mb-4" data-testid="meeting-title-section">
44-
<div class="flex-1 pr-4">
48+
<div class="flex sm:flex-row flex-col items-start justify-between mb-4" data-testid="meeting-title-section">
49+
<div class="flex-1 pr-4 flex-grow">
4550
<h2 class="text-xl font-display font-semibold text-gray-900 mb-1 leading-tight" data-testid="meeting-title">
4651
{{ meeting().title }}
4752
</h2>
@@ -178,7 +183,11 @@ <h4 class="font-medium text-gray-900 font-sans">{{ user.name }}</h4>
178183
<i class="fa-light fa-check-circle"></i>
179184
<span class="font-sans">Ready to join as {{ user.name }}</span>
180185
</div>
181-
<lfx-button size="small" [href]="meeting().join_url" severity="primary" label="Join Meeting" icon="fa-light fa-sign-in"></lfx-button>
186+
@if (meeting().join_url) {
187+
<lfx-button size="small" [href]="meeting().join_url" severity="primary" label="Join Meeting" icon="fa-light fa-sign-in"></lfx-button>
188+
} @else {
189+
<lfx-button size="small" severity="primary" label="Join Meeting" icon="fa-light fa-sign-in" (click)="onJoinMeeting()"></lfx-button>
190+
}
182191
</div>
183192

184193
<div class="text-center">
@@ -228,13 +237,12 @@ <h4 class="font-medium text-gray-900 font-sans">Enter your information</h4>
228237
<form [formGroup]="joinForm" class="space-y-3">
229238
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
230239
<div>
231-
<label for="fullName" class="flex items-center gap-1 text-sm font-medium text-gray-700 font-sans mb-1">
240+
<label for="name" class="flex items-center gap-1 text-sm font-medium text-gray-700 font-sans mb-1">
232241
<i class="fa-light fa-user text-xs"></i>
233242
Full Name
234243
<span class="text-red-500">*</span>
235244
</label>
236-
<lfx-input-text id="fullName" [form]="joinForm" control="fullName" placeholder="John Doe" styleClass="w-full" size="small">
237-
</lfx-input-text>
245+
<lfx-input-text id="name" [form]="joinForm" control="name" placeholder="John Doe" styleClass="w-full" size="small"> </lfx-input-text>
238246
</div>
239247

240248
<div>
@@ -258,13 +266,23 @@ <h4 class="font-medium text-gray-900 font-sans">Enter your information</h4>
258266
</div>
259267

260268
<div class="flex items-center justify-end pt-3">
261-
<lfx-button
262-
size="small"
263-
[href]="meeting().join_url"
264-
severity="primary"
265-
label="Join Meeting"
266-
icon="fa-light fa-sign-in"
267-
[disabled]="joinForm.invalid"></lfx-button>
269+
@if (meeting().join_url) {
270+
<lfx-button
271+
size="small"
272+
[href]="joinForm.invalid ? undefined : meeting().join_url"
273+
severity="primary"
274+
label="Join Meeting"
275+
icon="fa-light fa-sign-in"
276+
[disabled]="joinForm.invalid"></lfx-button>
277+
} @else {
278+
<lfx-button
279+
size="small"
280+
severity="primary"
281+
label="Join Meeting"
282+
[disabled]="joinForm.invalid"
283+
icon="fa-light fa-sign-in"
284+
(click)="onJoinMeeting()"></lfx-button>
285+
}
268286
</div>
269287
</form>
270288
</div>

apps/lfx-pcc/src/app/modules/meeting/meeting.component.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { UserService } from '@services/user.service';
1919
import { MessageService } from 'primeng/api';
2020
import { ToastModule } from 'primeng/toast';
2121
import { TooltipModule } from 'primeng/tooltip';
22-
import { combineLatest, map, of, switchMap } from 'rxjs';
22+
import { combineLatest, map, of, switchMap, tap } from 'rxjs';
2323

2424
@Component({
2525
selector: 'lfx-meeting',
@@ -51,7 +51,7 @@ export class MeetingComponent {
5151
public authenticated: WritableSignal<boolean>;
5252
public user: Signal<User | null> = this.userService.user;
5353
public joinForm: FormGroup;
54-
public project: Signal<Project | null> = signal<Project | null>(null);
54+
public project: WritableSignal<Project | null> = signal<Project | null>(null);
5555
public meeting: Signal<Meeting & { project: Project }>;
5656
public meetingTypeBadge: Signal<{ badgeClass: string; icon?: string; text: string } | null>;
5757
public importantLinks: Signal<{ url: string; domain: string }[]>;
@@ -68,28 +68,52 @@ export class MeetingComponent {
6868
this.returnTo = this.initializeReturnTo();
6969
}
7070

71+
public onJoinMeeting(): void {
72+
this.isJoining.set(true);
73+
74+
this.meetingService
75+
.getPublicMeetingJoinUrl(this.meeting().uid, this.meeting().password, {
76+
email: this.authenticated() ? this.user()?.email : this.joinForm.get('email')?.value,
77+
})
78+
.subscribe({
79+
next: (res) => {
80+
this.meeting().join_url = res.join_url;
81+
this.isJoining.set(false);
82+
window.open(this.meeting().join_url as string, '_blank');
83+
},
84+
error: ({ error }) => {
85+
this.isJoining.set(false);
86+
this.messageService.add({ severity: 'error', summary: 'Error', detail: error.error });
87+
},
88+
});
89+
}
90+
7191
private initializeMeeting() {
7292
return toSignal<Meeting & { project: Project }>(
7393
combineLatest([this.activatedRoute.paramMap, this.activatedRoute.queryParamMap]).pipe(
74-
switchMap(([params]) => {
94+
switchMap(([params, queryParams]) => {
7595
const meetingId = params.get('id');
96+
const password = queryParams.get('password');
7697
if (meetingId) {
77-
return this.meetingService.getPublicMeeting(meetingId);
98+
return this.meetingService.getPublicMeeting(meetingId, password);
7899
}
79100

80101
// TODO: If no meeting ID, redirect to 404
81102
return of({} as { meeting: Meeting; project: Project });
82103
}),
83-
map((res) => ({ ...res.meeting, project: res.project }))
104+
map((res) => ({ ...res.meeting, project: res.project })),
105+
tap((res) => {
106+
this.project.set(res.project);
107+
})
84108
)
85109
) as Signal<Meeting & { project: Project }>;
86110
}
87111

88112
// Private initialization methods
89113
private initializeJoinForm(): FormGroup {
90114
return new FormGroup({
91-
fullName: new FormControl<string>('', [Validators.required]),
92-
email: new FormControl<string>('', [Validators.required, Validators.email]),
115+
name: new FormControl<string>(this.user()?.name || '', [Validators.required]),
116+
email: new FormControl<string>(this.user()?.email || '', [Validators.required, Validators.email]),
93117
organization: new FormControl<string>(''),
94118
});
95119
}
@@ -154,7 +178,7 @@ export class MeetingComponent {
154178

155179
private initializeReturnTo(): Signal<string | undefined> {
156180
return computed(() => {
157-
return `${environment.urls.home}/meetings/${this.meeting().uid}`;
181+
return `${environment.urls.home}/meetings/${this.meeting().uid}?password=${this.meeting().password}`;
158182
});
159183
}
160184
}

apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.html

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,28 +80,29 @@
8080
<!-- Meeting Title and Date -->
8181
<div class="flex items-start justify-between mb-4" data-testid="meeting-title-section">
8282
<div class="flex-1 pr-4">
83-
@if (meeting().title) {
83+
@if (occurrence()?.title || meeting().title) {
8484
<h3 class="text-xl font-display font-semibold text-gray-900 mb-1 leading-tight" data-testid="meeting-title">
85-
{{ meeting().title }}
85+
{{ occurrence()?.title || meeting().title }}
8686
</h3>
8787
}
8888
</div>
89-
@if (meeting().start_time) {
89+
@if (occurrence()?.start_time || meeting().start_time) {
9090
<div class="flex items-center gap-1 text-xs text-gray-600 bg-gray-100 px-2 py-1 rounded flex-shrink-0" data-testid="meeting-datetime">
9191
<i class="fa-light fa-calendar-days text-gray-400"></i>
9292
<span
93-
>{{ meeting().start_time | meetingTime: meeting().duration : 'date' }} • {{ meeting().start_time | meetingTime: meeting().duration : 'time' }}</span
93+
>{{ occurrence()?.start_time || meeting().start_time | meetingTime: meeting().duration : 'date' }} •
94+
{{ occurrence()?.start_time || meeting().start_time | meetingTime: meeting().duration : 'time' }}</span
9495
>
9596
</div>
9697
}
9798
</div>
9899

99100
<!-- Description Section -->
100-
@if (meeting().description) {
101+
@if (occurrence()?.description || meeting().description) {
101102
<div class="mb-4" data-testid="meeting-description">
102103
<lfx-expandable-text [maxHeight]="85">
103104
<div class="text-sm text-gray-600 leading-relaxed">
104-
<div [innerHTML]="meeting().description | linkify"></div>
105+
<div [innerHTML]="occurrence()?.description || meeting().description | linkify"></div>
105106
</div>
106107
</lfx-expandable-text>
107108
</div>
@@ -171,8 +172,8 @@ <h3 class="text-xl font-display font-semibold text-gray-900 mb-1 leading-tight"
171172
<span>Details</span>
172173
</div>
173174
<div class="space-y-1">
174-
@if (meeting().duration) {
175-
<div class="text-sm">{{ meeting().duration }}m duration</div>
175+
@if (occurrence()?.duration || meeting().duration) {
176+
<div class="text-sm">{{ occurrence()?.duration || meeting().duration }}m duration</div>
176177
}
177178
<div class="text-xs text-gray-500">{{ enabledFeaturesCount() }} features enabled</div>
178179
<div class="text-xs text-gray-500">Updated {{ meeting().created_at | date: 'MMM d, y' }}</div>

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ import { AvatarComponent } from '@components/avatar/avatar.component';
1212
import { ButtonComponent } from '@components/button/button.component';
1313
import { ExpandableTextComponent } from '@components/expandable-text/expandable-text.component';
1414
import { MenuComponent } from '@components/menu/menu.component';
15-
import { extractUrlsWithDomains, Meeting, MeetingAttachment, MeetingRegistrant } from '@lfx-pcc/shared';
15+
import { extractUrlsWithDomains, Meeting, MeetingAttachment, MeetingOccurrence, MeetingRegistrant } from '@lfx-pcc/shared';
1616
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
17-
import { CommitteeService } from '@services/committee.service';
1817
import { MeetingService } from '@services/meeting.service';
1918
import { ProjectService } from '@services/project.service';
2019
import { AnimateOnScrollModule } from 'primeng/animateonscroll';
@@ -52,19 +51,20 @@ import { RegistrantModalComponent } from '../registrant-modal/registrant-modal.c
5251
export class MeetingCardComponent implements OnInit {
5352
private readonly projectService = inject(ProjectService);
5453
private readonly meetingService = inject(MeetingService);
55-
private readonly committeeService = inject(CommitteeService);
5654
private readonly dialogService = inject(DialogService);
5755
private readonly messageService = inject(MessageService);
5856
private readonly injector = inject(Injector);
5957

6058
public readonly meetingInput = input.required<Meeting>();
59+
public readonly occurrenceInput = input<MeetingOccurrence | null>(null);
6160
public readonly pastMeeting = input<boolean>(false);
6261
public readonly loading = input<boolean>(false);
6362
public readonly showBorder = input<boolean>(false);
6463
public readonly meetingRegistrantCount: Signal<number> = this.initMeetingRegistrantCount();
6564
public readonly registrantResponseBreakdown: Signal<string> = this.initRegistrantResponseBreakdown();
6665
public showRegistrants: WritableSignal<boolean> = signal(false);
6766
public meeting: WritableSignal<Meeting> = signal({} as Meeting);
67+
public occurrence: WritableSignal<MeetingOccurrence | null> = signal(null);
6868
public registrantsLoading: WritableSignal<boolean> = signal(true);
6969
private refresh$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
7070
public registrants = this.initRegistrantsList();
@@ -92,6 +92,9 @@ export class MeetingCardComponent implements OnInit {
9292
public constructor() {
9393
effect(() => {
9494
this.meeting.set(this.meetingInput());
95+
if (this.occurrenceInput()) {
96+
this.occurrence.set(this.occurrenceInput()!);
97+
}
9598
});
9699
}
97100

apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-delete-confirmation/meeting-delete-confirmation.component.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ <h3 class="text-lg font-semibold text-gray-900 mb-3">Meeting Details</h3>
4141
</div>
4242
</div>
4343

44+
<!-- TODO: Reenable when we have support for deleting series -->
4445
<!-- Recurrence Options -->
45-
@if (isRecurring && !isPastMeeting) {
46+
<!-- @if (isRecurring && !isPastMeeting) {
4647
<div class="mb-6">
4748
<h4 class="text-md font-semibold text-gray-900 mb-3">Delete Options</h4>
4849
<div class="space-y-4">
@@ -71,7 +72,7 @@ <h4 class="text-md font-semibold text-gray-900 mb-3">Delete Options</h4>
7172
</div>
7273
</div>
7374
</div>
74-
}
75+
} -->
7576

7677
<!-- Warning Message -->
7778
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">

apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-delete-confirmation/meeting-delete-confirmation.component.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { CommonModule } from '@angular/common';
55
import { Component, inject, signal, WritableSignal } from '@angular/core';
66
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
77
import { ButtonComponent } from '@components/button/button.component';
8-
import { RadioButtonComponent } from '@components/radio-button/radio-button.component';
98
import { Meeting } from '@lfx-pcc/shared/interfaces';
109
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
1110
import { MeetingService } from '@services/meeting.service';
@@ -21,7 +20,7 @@ export interface MeetingDeleteResult {
2120
@Component({
2221
selector: 'lfx-meeting-delete-confirmation',
2322
standalone: true,
24-
imports: [CommonModule, ReactiveFormsModule, ButtonComponent, RadioButtonComponent, MeetingTimePipe],
23+
imports: [CommonModule, ReactiveFormsModule, ButtonComponent, MeetingTimePipe],
2524
templateUrl: './meeting-delete-confirmation.component.html',
2625
styleUrl: './meeting-delete-confirmation.component.scss',
2726
})

apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-modal/meeting-modal.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
@if (meeting) {
55
<div class="p-0">
66
<!-- Show meeting card for view mode -->
7-
<lfx-meeting-card [meetingInput]="meeting" [pastMeeting]="false" (meetingDeleted)="onDelete()"></lfx-meeting-card>
7+
<lfx-meeting-card [meetingInput]="meeting" [occurrenceInput]="occurrence" [pastMeeting]="false" (meetingDeleted)="onDelete()"></lfx-meeting-card>
88
</div>
99
}

apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-modal/meeting-modal.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class MeetingModalComponent {
1717
private readonly dialogRef = inject(DynamicDialogRef);
1818

1919
public readonly meeting = this.config.data?.meeting;
20+
public readonly occurrence = this.config.data?.occurrence;
2021

2122
public onDelete(): void {
2223
this.dialogRef.close(true);

0 commit comments

Comments
 (0)