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 roomService = container.get(RoomService);
|
||||||
|
|
||||||
const { roomId } = req.params;
|
const { roomId } = req.params;
|
||||||
|
const { force } = req.query;
|
||||||
|
const forceDelete = force === 'true';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.verbose(`Deleting room: ${roomId}`);
|
logger.verbose(`Deleting room: ${roomId}`);
|
||||||
|
|
||||||
await roomService.bulkDeleteRooms([roomId]);
|
const { deleted } = await roomService.bulkDeleteRooms([roomId], forceDelete);
|
||||||
logger.info(`Room deleted: ${roomId}`);
|
|
||||||
return res.status(204).json();
|
if (deleted.length > 0) {
|
||||||
|
return res.status(204).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(202).json({ message: `Room ${roomId} marked as deleted` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting room: ${roomId}`);
|
logger.error(`Error deleting room: ${roomId}`);
|
||||||
handleError(res, error);
|
handleError(res, error);
|
||||||
@ -79,15 +85,26 @@ export const deleteRoom = async (req: Request, res: Response) => {
|
|||||||
export const bulkDeleteRooms = async (req: Request, res: Response) => {
|
export const bulkDeleteRooms = async (req: Request, res: Response) => {
|
||||||
const logger = container.get(LoggerService);
|
const logger = container.get(LoggerService);
|
||||||
const roomService = container.get(RoomService);
|
const roomService = container.get(RoomService);
|
||||||
const { roomIds } = req.query;
|
const { roomIds, force } = req.query;
|
||||||
|
const forceDelete = force === 'true';
|
||||||
logger.info(`Deleting rooms: ${roomIds}`);
|
logger.info(`Deleting rooms: ${roomIds}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const roomIdsArray = (roomIds as string).split(',');
|
const roomIdsArray = (roomIds as string).split(',');
|
||||||
await roomService.bulkDeleteRooms(roomIdsArray);
|
const { deleted, markedAsDeleted } = await roomService.bulkDeleteRooms(roomIdsArray, forceDelete);
|
||||||
|
|
||||||
|
if (roomIdsArray.length === 1) {
|
||||||
|
if (deleted.length > 0) {
|
||||||
return res.status(204).send();
|
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) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting rooms: ${error}`);
|
logger.error(`Error deleting rooms: ${error}`);
|
||||||
handleError(res, error);
|
handleError(res, error);
|
||||||
|
|||||||
@ -25,6 +25,17 @@ const nonEmptySanitizedString = (fieldName: string) =>
|
|||||||
message: `${fieldName} cannot be empty after sanitization`
|
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({
|
const RecordingPreferencesSchema: z.ZodType<MeetRecordingPreferences> = z.object({
|
||||||
enabled: z.boolean()
|
enabled: z.boolean()
|
||||||
});
|
});
|
||||||
@ -46,8 +57,8 @@ const RoomPreferencesSchema: z.ZodType<MeetRoomPreferences> = z.object({
|
|||||||
const RoomRequestOptionsSchema: z.ZodType<MeetRoomOptions> = z.object({
|
const RoomRequestOptionsSchema: z.ZodType<MeetRoomOptions> = z.object({
|
||||||
autoDeletionDate: z
|
autoDeletionDate: z
|
||||||
.number()
|
.number()
|
||||||
.positive('Expiration date must be a positive integer')
|
.positive('autoDeletionDate must be a positive integer')
|
||||||
.refine((date) => date >= Date.now() + 60 * 60 * 1000, 'Expiration date must be at least 1 hour in the future')
|
.refine((date) => date >= Date.now() + 60 * 60 * 1000, 'autoDeletionDate must be at least 1 hour in the future')
|
||||||
.optional(),
|
.optional(),
|
||||||
roomIdPrefix: z
|
roomIdPrefix: z
|
||||||
.string()
|
.string()
|
||||||
@ -103,7 +114,8 @@ const BulkDeleteRoomsSchema = z.object({
|
|||||||
return arg;
|
return arg;
|
||||||
},
|
},
|
||||||
z.array(nonEmptySanitizedString('roomId')).default([])
|
z.array(nonEmptySanitizedString('roomId')).default([])
|
||||||
)
|
),
|
||||||
|
force: validForceQueryParam()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const withValidRoomOptions = (req: Request, res: Response, next: NextFunction) => {
|
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.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();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import {
|
|||||||
configureRoomAuthorization,
|
configureRoomAuthorization,
|
||||||
withValidRoomPreferences,
|
withValidRoomPreferences,
|
||||||
withValidRoomBulkDeleteRequest,
|
withValidRoomBulkDeleteRequest,
|
||||||
withValidRoomId
|
withValidRoomId,
|
||||||
|
withValidRoomDeleteRequest
|
||||||
} from '../middlewares/index.js';
|
} from '../middlewares/index.js';
|
||||||
|
|
||||||
import { UserRole } from '@typings-ce';
|
import { UserRole } from '@typings-ce';
|
||||||
@ -43,7 +44,12 @@ roomRouter.get(
|
|||||||
withValidRoomId,
|
withValidRoomId,
|
||||||
roomCtrl.getRoom
|
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
|
// Internal room routes
|
||||||
export const internalRoomRouter = Router();
|
export const internalRoomRouter = Router();
|
||||||
|
|||||||
@ -146,10 +146,17 @@ export class LivekitWebhookService {
|
|||||||
*/
|
*/
|
||||||
async handleMeetingFinished(room: Room): Promise<void> {
|
async handleMeetingFinished(room: Room): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
const [meetRoom] = await Promise.all([
|
||||||
|
this.roomService.getMeetRoom(room.name),
|
||||||
this.recordingService.releaseRoomRecordingActiveLock(room.name),
|
this.recordingService.releaseRoomRecordingActiveLock(room.name),
|
||||||
this.openViduWebhookService.sendRoomFinishedWebhook(room)
|
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) {
|
} catch (error) {
|
||||||
this.logger.error(`Error handling room finished event: ${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> {
|
async getRoom(roomName: string): Promise<Room> {
|
||||||
let rooms: Room[] = [];
|
let rooms: Room[] = [];
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { MeetRoom, MeetRoomFilters, MeetRoomOptions, MeetRoomPreferences, Partic
|
|||||||
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
||||||
import { SystemEventService } from './system-event.service.js';
|
import { SystemEventService } from './system-event.service.js';
|
||||||
import { IScheduledTask, TaskSchedulerService } from './task-scheduler.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 { OpenViduComponentsAdapterHelper } from '../helpers/index.js';
|
||||||
import { uid } from 'uid/single';
|
import { uid } from 'uid/single';
|
||||||
import { MEET_NAME_ID, MEET_ROOM_GC_INTERVAL } from '../environment.js';
|
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 {
|
try {
|
||||||
// Delete rooms from storage and LiveKit in parallel
|
const results = await Promise.allSettled(
|
||||||
const meetRoomPromise = this.storageService.deleteMeetRooms(roomIds);
|
roomIds.map(async (roomId) => {
|
||||||
const livekitResults = await Promise.allSettled(
|
const hasParticipants = await this.livekitService.roomHasParticipants(roomId);
|
||||||
roomIds.map((roomId) => this.livekitService.deleteRoom(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
|
const deleted: string[] = [];
|
||||||
await meetRoomPromise;
|
const markedAsDeleted: string[] = [];
|
||||||
|
|
||||||
// Log results of LiveKit room deletions
|
results.forEach((result) => {
|
||||||
livekitResults.forEach((result, index) => {
|
if (result.status === 'fulfilled') {
|
||||||
if (result.status === 'rejected') {
|
if (result.value.status === 'deleted') {
|
||||||
this.logger.error(`Failed to delete LiveKit room "${roomIds[index]}": ${result.reason}`);
|
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) {
|
} catch (error) {
|
||||||
this.logger.error('Error deleting rooms:', error);
|
this.logger.error('Error deleting rooms:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user