Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
},
"angular.enable-strict-mode-prompt": false
}
4 changes: 4 additions & 0 deletions apps/lfx-pcc/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@

import { Routes } from '@angular/router';

import { authGuard } from './shared/guards/auth.guard';

export const routes: Routes = [
{
path: '',
loadComponent: () => import('./modules/pages/home/home.component').then((m) => m.HomeComponent),
canActivate: [authGuard],
},
{
path: 'meetings/:id',
Expand All @@ -16,6 +19,7 @@ export const routes: Routes = [
path: 'project/:slug',
loadComponent: () => import('./layouts/project-layout/project-layout.component').then((m) => m.ProjectLayoutComponent),
loadChildren: () => import('./modules/project/project.routes').then((m) => m.PROJECT_ROUTES),
canActivate: [authGuard],
data: { preload: true, preloadDelay: 1000 }, // Preload after 1 second for likely navigation
},
];
11 changes: 8 additions & 3 deletions apps/lfx-pcc/src/app/modules/meeting/meeting.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,14 @@ <h4 class="font-medium text-gray-900 font-sans">Enter your information</h4>
</div>
</div>

<div class="flex items-center justify-between pt-3">
<p class="text-sm text-gray-500 font-sans">Meeting invites and updates will be sent to your email</p>
<lfx-button size="small" [href]="meeting().join_url" severity="primary" label="Join Meeting" icon="fa-light fa-sign-in"></lfx-button>
<div class="flex items-center justify-end pt-3">
<lfx-button
size="small"
[href]="meeting().join_url"
severity="primary"
label="Join Meeting"
icon="fa-light fa-sign-in"
[disabled]="joinForm.invalid"></lfx-button>
</div>
</form>
</div>
Expand Down
32 changes: 32 additions & 0 deletions apps/lfx-pcc/src/app/shared/guards/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { isPlatformBrowser } from '@angular/common';
import { inject, PLATFORM_ID } from '@angular/core';
import { CanActivateFn } from '@angular/router';

import { UserService } from '../services/user.service';

/**
* Authentication guard that protects routes requiring user authentication.
*
* Redirects unauthenticated users to the login page with a returnTo parameter
* containing the originally requested URL.
*/
export const authGuard: CanActivateFn = (_route, state) => {
const userService = inject(UserService);
const platformId = inject(PLATFORM_ID);

// Check if user is authenticated using the signal
if (userService.authenticated()) {
return true;
}

// Redirect to login with returnTo parameter using Router for SSR compatibility
const returnTo = encodeURIComponent(state.url);
if (isPlatformBrowser(platformId)) {
window.location.href = `/login?returnTo=${returnTo}`;
}

return false;
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const authenticationInterceptor: HttpInterceptorFn = (req, next) => {
const cookieService = inject(SsrCookieService);
const userService = inject(UserService);

if (req.url.startsWith('/api/') && userService.authenticated()) {
if ((req.url.startsWith('/api/') || req.url.startsWith('/public/api/')) && userService.authenticated()) {
const authenticatedReq = req.clone({
withCredentials: true,
headers: req.headers.append(
Expand Down
43 changes: 43 additions & 0 deletions apps/lfx-pcc/src/server/controllers/meeting.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,49 @@ export class MeetingController {
}
}

/**
* GET /meetings/:uid/join-url
*/
public async getMeetingJoinUrl(req: Request, res: Response, next: NextFunction): Promise<void> {
const { uid } = req.params;
const startTime = Logger.start(req, 'get_meeting_join_url', {
meeting_uid: uid,
});

try {
// Check if the meeting UID is provided
if (
!validateUidParameter(uid, req, next, {
operation: 'get_meeting_join_url',
service: 'meeting_controller',
logStartTime: startTime,
})
) {
return;
}

// Get the meeting join URL
const joinUrlData = await this.meetingService.getMeetingJoinUrl(req, uid);

// Log the success
Logger.success(req, 'get_meeting_join_url', startTime, {
meeting_uid: uid,
has_join_url: !!joinUrlData.join_url,
});

// Send the join URL data to the client
res.json(joinUrlData);
} catch (error) {
// Log the error
Logger.error(req, 'get_meeting_join_url', startTime, error, {
meeting_uid: uid,
});

// Send the error to the next middleware
next(error);
}
}

/**
* Private helper to process registrant operations with fail-fast for 403 errors
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Logger } from '../helpers/logger';
import { validateUidParameter } from '../helpers/validation.helper';
import { MeetingService } from '../services/meeting.service';
import { ProjectService } from '../services/project.service';
import { validatePasscode } from '../utils/security.util';
import { validatePassword } from '../utils/security.util';

/**
* Controller for handling public meeting HTTP requests (no authentication required)
Expand Down Expand Up @@ -42,8 +42,8 @@ export class PublicMeetingController {
// TODO: Generate an M2M token

// Get the meeting by ID using the existing meeting service
const meeting = await this.meetingService.getMeetingById(req, id);
const project = await this.projectService.getProjectById(req, meeting.project_uid);
const meeting = await this.meetingService.getMeetingById(req, id, 'meeting', false);
const project = await this.projectService.getProjectById(req, meeting.project_uid, false);

if (!project) {
throw new ResourceNotFoundError('Project', meeting.project_uid, {
Expand All @@ -60,22 +60,83 @@ export class PublicMeetingController {
title: meeting.title,
});

// Check if the meeting visibility is public, if so, return the meeting and project
// Check if the meeting visibility is public, if so, get join URL and return the meeting and project
if (meeting.visibility === MeetingVisibility.PUBLIC) {
res.json({ meeting, project });
try {
// Get the meeting join URL for public meetings
req.log.debug(
Logger.sanitize({
meeting_uid: id,
isAuthenticated: req.oidc?.isAuthenticated(),
hasOidc: !!req.oidc,
user: req.oidc?.user,
accessToken: req.oidc?.accessToken ? 'present' : 'missing',
cookies: Object.keys(req.cookies || {}),
headers: {
cookie: req.headers.cookie ? 'present' : 'missing',
},
}),
'OIDC Authentication Debug - Getting join URL for public meeting'
);
const joinUrlData = await this.meetingService.getMeetingJoinUrl(req, id);
meeting.join_url = joinUrlData.join_url;

req.log.debug(
Logger.sanitize({
meeting_uid: id,
has_join_url: !!joinUrlData.join_url,
}),
'Fetched join URL for public meeting'
);
} catch (error) {
// Log the error but don't fail the request - join URL is optional
req.log.warn(
{
error: error instanceof Error ? error.message : error,
meeting_uid: id,
has_token: !!req.bearerToken,
},
'Failed to fetch join URL for public meeting, continuing without it'
);
}

res.json({ meeting, project: { name: project.name, slug: project.slug, logo_url: project.logo_url } });
return;
}

// Check if the user has passed in a password, if so, check if it's correct
const { password } = req.query;
if (!password || !validatePasscode(password as string, meeting.password)) {
if (!password || !validatePassword(password as string, meeting.password)) {
throw new AuthenticationError('Invalid password', {
operation: 'get_public_meeting_by_id',
service: 'public_meeting_controller',
path: `/meetings/${id}`,
});
}

// Get the meeting join URL for password-protected meetings
try {
const joinUrlData = await this.meetingService.getMeetingJoinUrl(req, id);
meeting.join_url = joinUrlData.join_url;

req.log.debug(
{
meeting_uid: id,
has_join_url: !!joinUrlData.join_url,
},
'Fetched join URL for password-protected meeting'
);
} catch (error) {
// Log the error but don't fail the request - join URL is optional
req.log.warn(
{
error: error instanceof Error ? error.message : error,
meeting_uid: id,
},
'Failed to fetch join URL for password-protected meeting, continuing without it'
);
}

// Send the meeting and project data to the client
res.json({ meeting, project: { name: project.name, slug: project.slug, logo_url: project.logo_url } });
} catch (error) {
Expand Down
55 changes: 0 additions & 55 deletions apps/lfx-pcc/src/server/middleware/auth-token.middleware.ts

This file was deleted.

Loading