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
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,5 @@ Before starting any work or commits:
2. **Create JIRA ticket if needed** for untracked work
3. **Include JIRA ticket in commit message** (e.g., LFXV2-XXX)
4. **Link PR to JIRA ticket** when creating pull requests

- Always use sequential thinking mcp for planning before doing any changes
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export class MeetingManageComponent {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Project information is required to create a meeting.',
detail: `Project information is required to ${this.isEditMode() ? 'update' : 'create'} a meeting.`,
});
return;
}
Expand Down Expand Up @@ -401,6 +401,13 @@ export class MeetingManageComponent {
// Map recurrence object back to form value
const recurrenceValue = mapRecurrenceToFormValue(meeting.recurrence);

// If recording_enabled is true, enable controls for transcript_enabled and youtube_upload_enabled
if (meeting.recording_enabled) {
this.form().get('transcript_enabled')?.enable();
this.form().get('youtube_upload_enabled')?.enable();
this.form().get('zoom_ai_enabled')?.enable();
}

this.form().patchValue({
title: meeting.title,
description: meeting.description,
Expand Down
91 changes: 90 additions & 1 deletion apps/lfx-pcc/src/server/controllers/meeting.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { CreateMeetingRequest } from '@lfx-pcc/shared/interfaces';
import { CreateMeetingRequest, UpdateMeetingRequest } from '@lfx-pcc/shared/interfaces';
import { Request, Response } from 'express';

import { Logger } from '../helpers/logger';
Expand Down Expand Up @@ -134,6 +134,95 @@ export class MeetingController {
}
}

/**
* PUT /meetings/:id
*/
public async updateMeeting(req: Request, res: Response): Promise<void> {
const { id } = req.params;
const meetingData: UpdateMeetingRequest = req.body;
const { editType } = req.query;
const startTime = Logger.start(req, 'update_meeting', {
meeting_id: id,
project_uid: meetingData?.project_uid,
title: meetingData?.title,
start_time: meetingData?.start_time,
duration: meetingData?.duration,
timezone: meetingData?.timezone,
edit_type: editType,
body_size: JSON.stringify(req.body).length,
});

try {
if (!id) {
Logger.error(req, 'update_meeting', startTime, new Error('Missing meeting ID parameter'));

Responder.badRequest(res, 'Meeting ID is required', {
code: 'MISSING_MEETING_ID',
});
return;
}

// Basic validation
if (!meetingData.title || !meetingData.start_time || !meetingData.project_uid || !meetingData.duration || !meetingData.timezone) {
Logger.error(req, 'update_meeting', startTime, new Error('Missing required fields for meeting update'), {
provided_fields: {
has_title: !!meetingData.title,
has_start_time: !!meetingData.start_time,
has_project_uid: !!meetingData.project_uid,
has_duration: !!meetingData.duration,
has_timezone: !!meetingData.timezone,
},
});

Responder.badRequest(res, 'Title, start_time, duration, timezone, and project_uid are required fields', {
code: 'MISSING_REQUIRED_FIELDS',
});
return;
}

// Validate duration range
if (meetingData.duration < 0 || meetingData.duration > 600) {
Logger.error(req, 'update_meeting', startTime, new Error('Invalid duration for meeting update'), {
provided_duration: meetingData.duration,
});

Responder.badRequest(res, 'Duration must be between 0 and 600 minutes', {
code: 'INVALID_DURATION',
});
return;
}

// Validate editType for recurring meetings
if (editType && !['single', 'future'].includes(editType as string)) {
Logger.error(req, 'update_meeting', startTime, new Error('Invalid edit type for meeting update'), {
provided_edit_type: editType,
});

Responder.badRequest(res, 'Edit type must be "single" or "future"', {
code: 'INVALID_EDIT_TYPE',
});
return;
}

const meeting = await this.meetingService.updateMeeting(req, id, meetingData, editType as 'single' | 'future');

Logger.success(req, 'update_meeting', startTime, {
meeting_id: id,
project_uid: meeting.project_uid,
title: meeting.title,
edit_type: editType || 'single',
});

res.json(meeting);
} catch (error) {
Logger.error(req, 'update_meeting', startTime, error, {
meeting_id: id,
edit_type: editType,
});
Responder.handle(res, error, 'update_meeting');
}
}

/**
* DELETE /meetings/:id
*/
Expand Down
42 changes: 2 additions & 40 deletions apps/lfx-pcc/src/server/routes/meetings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,46 +168,8 @@ router.get('/:id', (req, res) => meetingController.getMeetingById(req, res));
// POST /meetings - using new controller pattern
router.post('/', (req, res) => meetingController.createMeeting(req, res));

router.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
try {
const meetingId = req.params['id'];
const meetingData = req.body;
const { editType } = req.query;

if (!meetingId) {
return res.status(400).json({
error: 'Meeting ID is required',
code: 'MISSING_MEETING_ID',
});
}

// Validate editType for recurring meetings
if (editType && !['single', 'future'].includes(editType as string)) {
return res.status(400).json({
error: 'Edit type must be "single" or "future"',
code: 'INVALID_EDIT_TYPE',
});
}

// Remove fields that shouldn't be updated directly
delete meetingData.id;
delete meetingData.created_at;

const meeting = await supabaseService.updateMeeting(meetingId, meetingData, editType as 'single' | 'future');

return res.json(meeting);
} catch (error) {
req.log.error(
{
error: error instanceof Error ? error.message : error,
meeting_id: req.params['id'],
edit_type: req.query['editType'],
},
'Failed to update meeting'
);
return next(error);
}
});
// PUT /meetings/:id - using new controller pattern
router.put('/:id', (req, res) => meetingController.updateMeeting(req, res));

router.delete('/:id', (req, res) => meetingController.deleteMeeting(req, res));

Expand Down
2 changes: 1 addition & 1 deletion apps/lfx-pcc/src/server/services/etag.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { ETagError, ETagResult, extractErrorDetails } from '@lfx-pcc/shared/interfaces';
import { HTTP_HEADERS } from '@lfx-pcc/shared/constants';
import { ETagError, ETagResult, extractErrorDetails } from '@lfx-pcc/shared/interfaces';
import { Request } from 'express';

import { MicroserviceProxyService } from './microservice-proxy.service';
Expand Down
55 changes: 53 additions & 2 deletions apps/lfx-pcc/src/server/services/meeting.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { CreateMeetingRequest, ETagError, Meeting, QueryServiceResponse } from '@lfx-pcc/shared/interfaces';
import { CreateMeetingRequest, ETagError, Meeting, QueryServiceResponse, UpdateMeetingRequest } from '@lfx-pcc/shared/interfaces';
import { Request } from 'express';

import { getUsernameFromAuth } from '../utils/auth-helper';
import { Logger } from '../helpers/logger';
import { getUsernameFromAuth } from '../utils/auth-helper';
import { ApiClientService } from './api-client.service';
import { ETagService } from './etag.service';
import { MicroserviceProxyService } from './microservice-proxy.service';
Expand Down Expand Up @@ -101,6 +101,57 @@ export class MeetingService {
return newMeeting;
}

/**
* Updates a meeting using ETag for concurrency control
*/
public async updateMeeting(req: Request, meetingId: string, meetingData: UpdateMeetingRequest, editType?: 'single' | 'future'): Promise<Meeting> {
// Step 1: Fetch meeting with ETag
const { etag, data } = await this.etagService.fetchWithETag<Meeting>(req, 'LFX_V2_SERVICE', `/meetings/${meetingId}`, 'update_meeting');

// Get the logged-in user's username to maintain organizer if not provided
const username = await getUsernameFromAuth(req);

// Create organizers array ensuring no duplicates or null values
const existingOrganizers = data.organizers || [];
const organizersSet = new Set(existingOrganizers.filter((organizer) => organizer != null));

// Add current user as organizer if username exists and not already included
if (username) {
organizersSet.add(username);
}

// Include organizers in the update payload
const updatePayload = {
...meetingData,
organizers: Array.from(organizersSet),
};

const sanitizedPayload = Logger.sanitize({ updatePayload, editType });
req.log.info(sanitizedPayload, 'Updating meeting payload');

// Step 2: Update meeting with ETag, including editType query parameter if provided
let path = `/meetings/${meetingId}`;
if (editType) {
path += `?editType=${editType}`;
}

const updatedMeeting = await this.etagService.updateWithETag<Meeting>(req, 'LFX_V2_SERVICE', path, etag, updatePayload, 'update_meeting');

req.log.info(
{
operation: 'update_meeting',
meeting_id: meetingId,
project_uid: updatedMeeting.project_uid,
title: updatedMeeting.title,
edit_type: editType || 'single',
organizer: username || 'none',
},
'Meeting updated successfully'
);

return updatedMeeting;
}

/**
* Deletes a meeting using ETag for concurrency control
*/
Expand Down
Loading