backend: Enhance room deletion functionality with force delete option and improved response handling
This commit is contained in:
parent
14d5637151
commit
ac12841418
@ -63,13 +63,19 @@ export const deleteRoom = async (req: Request, res: Response) => {
|
||||
const roomService = container.get(RoomService);
|
||||
|
||||
const { roomId } = req.params;
|
||||
const { force } = req.query;
|
||||
const forceDelete = force === 'true';
|
||||
|
||||
try {
|
||||
logger.verbose(`Deleting room: ${roomId}`);
|
||||
|
||||
await roomService.bulkDeleteRooms([roomId]);
|
||||
logger.info(`Room deleted: ${roomId}`);
|
||||
return res.status(204).json();
|
||||
const { deleted } = await roomService.bulkDeleteRooms([roomId], forceDelete);
|
||||
|
||||
if (deleted.length > 0) {
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
return res.status(202).json({ message: `Room ${roomId} marked as deleted` });
|
||||
} catch (error) {
|
||||
logger.error(`Error deleting room: ${roomId}`);
|
||||
handleError(res, error);
|
||||
@ -79,15 +85,26 @@ export const deleteRoom = async (req: Request, res: Response) => {
|
||||
export const bulkDeleteRooms = async (req: Request, res: Response) => {
|
||||
const logger = container.get(LoggerService);
|
||||
const roomService = container.get(RoomService);
|
||||
const { roomIds } = req.query;
|
||||
|
||||
const { roomIds, force } = req.query;
|
||||
const forceDelete = force === 'true';
|
||||
logger.info(`Deleting rooms: ${roomIds}`);
|
||||
|
||||
try {
|
||||
const roomIdsArray = (roomIds as string).split(',');
|
||||
await roomService.bulkDeleteRooms(roomIdsArray);
|
||||
const { deleted, markedAsDeleted } = await roomService.bulkDeleteRooms(roomIdsArray, forceDelete);
|
||||
|
||||
return res.status(204).send();
|
||||
if (roomIdsArray.length === 1) {
|
||||
if (deleted.length > 0) {
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
return res.status(202).json({ message: `Room ${roomIds} marked as deleted` });
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
deleted,
|
||||
markedAsDeleted
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error deleting rooms: ${error}`);
|
||||
handleError(res, error);
|
||||
|
||||
@ -25,6 +25,17 @@ const nonEmptySanitizedString = (fieldName: string) =>
|
||||
message: `${fieldName} cannot be empty after sanitization`
|
||||
});
|
||||
|
||||
const validForceQueryParam = () =>
|
||||
z
|
||||
.preprocess((val) => {
|
||||
if (typeof val === 'string') {
|
||||
return val.toLowerCase() === 'true';
|
||||
}
|
||||
|
||||
return val;
|
||||
}, z.boolean())
|
||||
.default(false);
|
||||
|
||||
const RecordingPreferencesSchema: z.ZodType<MeetRecordingPreferences> = z.object({
|
||||
enabled: z.boolean()
|
||||
});
|
||||
@ -46,8 +57,8 @@ const RoomPreferencesSchema: z.ZodType<MeetRoomPreferences> = z.object({
|
||||
const RoomRequestOptionsSchema: z.ZodType<MeetRoomOptions> = z.object({
|
||||
autoDeletionDate: z
|
||||
.number()
|
||||
.positive('Expiration date must be a positive integer')
|
||||
.refine((date) => date >= Date.now() + 60 * 60 * 1000, 'Expiration date must be at least 1 hour in the future')
|
||||
.positive('autoDeletionDate must be a positive integer')
|
||||
.refine((date) => date >= Date.now() + 60 * 60 * 1000, 'autoDeletionDate must be at least 1 hour in the future')
|
||||
.optional(),
|
||||
roomIdPrefix: z
|
||||
.string()
|
||||
@ -103,7 +114,8 @@ const BulkDeleteRoomsSchema = z.object({
|
||||
return arg;
|
||||
},
|
||||
z.array(nonEmptySanitizedString('roomId')).default([])
|
||||
)
|
||||
),
|
||||
force: validForceQueryParam()
|
||||
});
|
||||
|
||||
export const withValidRoomOptions = (req: Request, res: Response, next: NextFunction) => {
|
||||
@ -162,6 +174,27 @@ export const withValidRoomBulkDeleteRequest = (req: Request, res: Response, next
|
||||
}
|
||||
|
||||
req.query.roomIds = data.roomIds.join(',');
|
||||
req.query.force = data.force ? 'true' : 'false';
|
||||
next();
|
||||
};
|
||||
|
||||
export const withValidRoomDeleteRequest = (req: Request, res: Response, next: NextFunction) => {
|
||||
const roomIdResult = nonEmptySanitizedString('roomId').safeParse(req.params.roomId);
|
||||
|
||||
if (!roomIdResult.success) {
|
||||
return rejectRequest(res, roomIdResult.error);
|
||||
}
|
||||
|
||||
req.params.roomId = roomIdResult.data;
|
||||
|
||||
const forceResult = validForceQueryParam().safeParse(req.query.force);
|
||||
|
||||
if (!forceResult.success) {
|
||||
return rejectRequest(res, forceResult.error);
|
||||
}
|
||||
|
||||
req.query.force = forceResult.data ? 'true' : 'false';
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
|
||||
@ -13,7 +13,8 @@ import {
|
||||
configureRoomAuthorization,
|
||||
withValidRoomPreferences,
|
||||
withValidRoomBulkDeleteRequest,
|
||||
withValidRoomId
|
||||
withValidRoomId,
|
||||
withValidRoomDeleteRequest
|
||||
} from '../middlewares/index.js';
|
||||
|
||||
import { UserRole } from '@typings-ce';
|
||||
@ -43,7 +44,12 @@ roomRouter.get(
|
||||
withValidRoomId,
|
||||
roomCtrl.getRoom
|
||||
);
|
||||
roomRouter.delete('/:roomId', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.deleteRoom);
|
||||
roomRouter.delete(
|
||||
'/:roomId',
|
||||
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
|
||||
withValidRoomDeleteRequest,
|
||||
roomCtrl.deleteRoom
|
||||
);
|
||||
|
||||
// Internal room routes
|
||||
export const internalRoomRouter = Router();
|
||||
|
||||
@ -146,10 +146,17 @@ export class LivekitWebhookService {
|
||||
*/
|
||||
async handleMeetingFinished(room: Room): Promise<void> {
|
||||
try {
|
||||
await Promise.all([
|
||||
const [meetRoom] = await Promise.all([
|
||||
this.roomService.getMeetRoom(room.name),
|
||||
this.recordingService.releaseRoomRecordingActiveLock(room.name),
|
||||
this.openViduWebhookService.sendRoomFinishedWebhook(room)
|
||||
]);
|
||||
|
||||
if (meetRoom.markedForDeletion) {
|
||||
// If the room is marked for deletion, we need to delete it
|
||||
this.logger.info(`Deleting room ${room.name} after meeting finished because it was marked for deletion`);
|
||||
this.roomService.bulkDeleteRooms([room.name], true);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error handling room finished event: ${error}`);
|
||||
}
|
||||
|
||||
@ -75,6 +75,22 @@ export class LiveKitService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a LiveKit room has at least one participant.
|
||||
*
|
||||
* @param roomName - The name of the room to check
|
||||
* @returns A promise that resolves to true if the room has at least one participant,
|
||||
* or false if the room has no participants or if an error occurs
|
||||
*/
|
||||
async roomHasParticipants(roomName: string): Promise<boolean> {
|
||||
try {
|
||||
const participants = await this.roomClient.listParticipants(roomName);
|
||||
return participants.length > 0;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getRoom(roomName: string): Promise<Room> {
|
||||
let rooms: Room[] = [];
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import { MeetRoom, MeetRoomFilters, MeetRoomOptions, MeetRoomPreferences, Partic
|
||||
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
||||
import { SystemEventService } from './system-event.service.js';
|
||||
import { IScheduledTask, TaskSchedulerService } from './task-scheduler.service.js';
|
||||
import { errorParticipantUnauthorized } from '../models/error.model.js';
|
||||
import { errorParticipantUnauthorized, internalError } from '../models/error.model.js';
|
||||
import { OpenViduComponentsAdapterHelper } from '../helpers/index.js';
|
||||
import { uid } from 'uid/single';
|
||||
import { MEET_NAME_ID, MEET_ROOM_GC_INTERVAL } from '../environment.js';
|
||||
@ -159,27 +159,63 @@ export class RoomService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk deletes rooms from both storage (e.g., S3) and LiveKit.
|
||||
* Deletes a room by its ID.
|
||||
*
|
||||
* @param roomIds - An array with the IDs of the rooms to delete.
|
||||
* @param roomId - The unique identifier of the room to delete.
|
||||
* @param forceDelete - Whether to force delete the room even if it has participants.
|
||||
* @returns A promise that resolves to an object containing the deleted and marked rooms.
|
||||
*/
|
||||
async bulkDeleteRooms(roomIds: string[]): Promise<void> {
|
||||
async bulkDeleteRooms(
|
||||
roomIds: string[],
|
||||
forceDelete: boolean
|
||||
): Promise<{ deleted: string[]; markedAsDeleted: string[] }> {
|
||||
try {
|
||||
// Delete rooms from storage and LiveKit in parallel
|
||||
const meetRoomPromise = this.storageService.deleteMeetRooms(roomIds);
|
||||
const livekitResults = await Promise.allSettled(
|
||||
roomIds.map((roomId) => this.livekitService.deleteRoom(roomId))
|
||||
const results = await Promise.allSettled(
|
||||
roomIds.map(async (roomId) => {
|
||||
const hasParticipants = await this.livekitService.roomHasParticipants(roomId);
|
||||
const shouldDelete = forceDelete || !hasParticipants;
|
||||
|
||||
if (shouldDelete) {
|
||||
this.logger.verbose(`Deleting room ${roomId}.`);
|
||||
|
||||
await Promise.all([
|
||||
this.storageService.deleteMeetRooms([roomId]),
|
||||
this.livekitService.deleteRoom(roomId)
|
||||
]);
|
||||
|
||||
return { roomId, status: 'deleted' } as const;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Room ${roomId} has participants. Marking as deleted (graceful deletion).`);
|
||||
// Mark room as deleted
|
||||
const room = await this.storageService.getMeetRoom(roomId);
|
||||
room.markedForDeletion = true;
|
||||
await this.storageService.saveMeetRoom(room);
|
||||
return { roomId, status: 'marked' } as const;
|
||||
})
|
||||
);
|
||||
|
||||
// Wait for the meet rooms deletion to complete
|
||||
await meetRoomPromise;
|
||||
const deleted: string[] = [];
|
||||
const markedAsDeleted: string[] = [];
|
||||
|
||||
// Log results of LiveKit room deletions
|
||||
livekitResults.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
this.logger.error(`Failed to delete LiveKit room "${roomIds[index]}": ${result.reason}`);
|
||||
results.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
if (result.value.status === 'deleted') {
|
||||
deleted.push(result.value.roomId);
|
||||
} else if (result.value.status === 'marked') {
|
||||
markedAsDeleted.push(result.value.roomId);
|
||||
}
|
||||
} else {
|
||||
this.logger.error(`Failed to process deletion for a room: ${result.reason}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (deleted.length === 0 && markedAsDeleted.length === 0) {
|
||||
this.logger.error('No rooms were deleted or marked as deleted.');
|
||||
throw internalError('No rooms were deleted or marked as deleted.');
|
||||
}
|
||||
|
||||
return { deleted, markedAsDeleted };
|
||||
} catch (error) {
|
||||
this.logger.error('Error deleting rooms:', error);
|
||||
throw error;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user