backend: Refactor bulkDeleteRooms to improve response handling and update sanitization logic
This commit is contained in:
parent
33a970d1ef
commit
c3fa764534
@ -92,21 +92,35 @@ export const bulkDeleteRooms = async (req: Request, res: Response) => {
|
|||||||
logger.info(`Deleting rooms: ${roomIds}`);
|
logger.info(`Deleting rooms: ${roomIds}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const roomIdsArray = (roomIds as string).split(',');
|
const roomIdsArray = roomIds as string[];
|
||||||
const { deleted, markedAsDeleted } = await roomService.bulkDeleteRooms(roomIdsArray, forceDelete);
|
|
||||||
|
|
||||||
if (roomIdsArray.length === 1) {
|
const { deleted, markedForDeletion } = await roomService.bulkDeleteRooms(roomIdsArray, forceDelete);
|
||||||
|
const isSingleRoom = roomIdsArray.length === 1;
|
||||||
|
|
||||||
|
if (isSingleRoom) {
|
||||||
|
// For a single room, no content is sent if fully deleted.
|
||||||
if (deleted.length > 0) {
|
if (deleted.length > 0) {
|
||||||
return res.status(204).send();
|
return res.sendStatus(204);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(202).json({ message: `Room ${roomIds} marked as deleted` });
|
// For a single room marked as deleted, return a message.
|
||||||
|
return res.status(202).json({ message: `Room ${roomIdsArray[0]} marked as deleted` });
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
// For multiple rooms
|
||||||
deleted,
|
if (deleted.length > 0 && markedForDeletion.length === 0) {
|
||||||
markedAsDeleted
|
// All rooms were deleted
|
||||||
});
|
return res.sendStatus(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleted.length === 0 && markedForDeletion.length > 0) {
|
||||||
|
// All rooms were marked as deleted
|
||||||
|
return res
|
||||||
|
.status(202)
|
||||||
|
.json({ message: `Rooms ${markedForDeletion.join(', ')} marked for deletion`, markedForDeletion });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ deleted, markedForDeletion });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting rooms: ${error}`);
|
logger.error(`Error deleting rooms: ${error}`);
|
||||||
handleError(res, error);
|
handleError(res, error);
|
||||||
|
|||||||
@ -8,25 +8,37 @@ import {
|
|||||||
} from '@typings-ce';
|
} from '@typings-ce';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import ms from 'ms';
|
||||||
|
import INTERNAL_CONFIG from '../../config/internal-config.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes an identifier by removing/replacing invalid characters
|
||||||
|
* and normalizing format.
|
||||||
|
*
|
||||||
|
* @param val The string to sanitize
|
||||||
|
* @returns A sanitized string safe for use as an identifier
|
||||||
|
*/
|
||||||
const sanitizeId = (val: string): string => {
|
const sanitizeId = (val: string): string => {
|
||||||
return val
|
let transformed = val
|
||||||
.trim() // Remove leading and trailing spaces
|
.trim() // Remove leading/trailing spaces
|
||||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
.replace(/\s+/g, '') // Remove all spaces
|
||||||
.replace(/[^a-zA-Z0-9_-]/g, ''); // Remove special characters (allow alphanumeric, hyphens and underscores)
|
.replace(/[^a-zA-Z0-9_-]/g, '') // Allow alphanumeric, underscores and hyphens
|
||||||
|
.replace(/-+/g, '-') // Replace multiple consecutive hyphens
|
||||||
|
.replace(/-+$/, ''); // Remove trailing hyphens
|
||||||
|
|
||||||
|
// Remove leading hyphens
|
||||||
|
if (transformed.startsWith('-')) {
|
||||||
|
transformed = transformed.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const nonEmptySanitizedString = (fieldName: string) =>
|
const nonEmptySanitizedString = (fieldName: string) =>
|
||||||
z
|
z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: `${fieldName} is required and cannot be empty` })
|
.min(1, { message: `${fieldName} is required and cannot be empty` })
|
||||||
.transform((val) => {
|
.transform(sanitizeId)
|
||||||
let transformed = sanitizeId(val);
|
|
||||||
|
|
||||||
if (transformed.startsWith('-')) transformed = transformed.substring(1);
|
|
||||||
|
|
||||||
return transformed;
|
|
||||||
})
|
|
||||||
.refine((data) => data !== '', {
|
.refine((data) => data !== '', {
|
||||||
message: `${fieldName} cannot be empty after sanitization`
|
message: `${fieldName} cannot be empty after sanitization`
|
||||||
});
|
});
|
||||||
@ -64,25 +76,14 @@ const RoomRequestOptionsSchema: z.ZodType<MeetRoomOptions> = z.object({
|
|||||||
autoDeletionDate: z
|
autoDeletionDate: z
|
||||||
.number()
|
.number()
|
||||||
.positive('autoDeletionDate must be a positive integer')
|
.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')
|
.refine(
|
||||||
|
(date) => date >= Date.now() + ms(INTERNAL_CONFIG.MIN_FUTURE_TIME_FOR_ROOM_AUTODELETION_DATE),
|
||||||
|
`autoDeletionDate must be at least ${INTERNAL_CONFIG.MIN_FUTURE_TIME_FOR_ROOM_AUTODELETION_DATE} in the future`
|
||||||
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
roomIdPrefix: z
|
roomIdPrefix: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => {
|
.transform(sanitizeId)
|
||||||
let transformed = val
|
|
||||||
.trim() // Remove leading and trailing spaces
|
|
||||||
.replace(/\s+/g, '') // Remove all whitespace instead of replacing it with hyphens
|
|
||||||
.replace(/[^a-zA-Z0-9-]/g, '') // Remove any character except letters, numbers, and hyphens
|
|
||||||
.replace(/-+/g, '-') // Replace multiple consecutive hyphens with a single one
|
|
||||||
.replace(/-+$/, ''); // Remove trailing hyphens
|
|
||||||
|
|
||||||
// If the transformed string starts with a hyphen, remove it.
|
|
||||||
if (transformed.startsWith('-')) {
|
|
||||||
transformed = transformed.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformed;
|
|
||||||
})
|
|
||||||
.optional()
|
.optional()
|
||||||
.default(''),
|
.default(''),
|
||||||
preferences: RoomPreferencesSchema.optional().default({
|
preferences: RoomPreferencesSchema.optional().default({
|
||||||
@ -116,17 +117,37 @@ const GetRoomFiltersSchema: z.ZodType<MeetRoomFilters> = z.object({
|
|||||||
const BulkDeleteRoomsSchema = z.object({
|
const BulkDeleteRoomsSchema = z.object({
|
||||||
roomIds: z.preprocess(
|
roomIds: z.preprocess(
|
||||||
(arg) => {
|
(arg) => {
|
||||||
|
// First, convert input to array of strings
|
||||||
|
let roomIds: string[] = [];
|
||||||
|
|
||||||
if (typeof arg === 'string') {
|
if (typeof arg === 'string') {
|
||||||
// If the argument is a string, it is expected to be a comma-separated list of recording IDs.
|
roomIds = arg
|
||||||
return arg
|
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter((s) => s !== '');
|
.filter((s) => s !== '');
|
||||||
|
} else if (Array.isArray(arg)) {
|
||||||
|
roomIds = arg.map((item) => String(item)).filter((s) => s !== '');
|
||||||
}
|
}
|
||||||
|
|
||||||
return arg;
|
// Apply sanitization BEFORE validation and deduplicate
|
||||||
|
// This prevents identical IDs from being processed separately
|
||||||
|
const sanitizedIds = new Set();
|
||||||
|
|
||||||
|
// Pre-sanitize to check for duplicates that would become identical
|
||||||
|
for (const id of roomIds) {
|
||||||
|
const transformed = sanitizeId(id);
|
||||||
|
|
||||||
|
// Only add non-empty IDs
|
||||||
|
if (transformed !== '') {
|
||||||
|
sanitizedIds.add(transformed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(sanitizedIds);
|
||||||
},
|
},
|
||||||
z.array(nonEmptySanitizedString('roomId')).default([])
|
z.array(z.string()).min(1, {
|
||||||
|
message: 'At least one valid roomId is required after sanitization'
|
||||||
|
})
|
||||||
),
|
),
|
||||||
force: validForceQueryParam()
|
force: validForceQueryParam()
|
||||||
});
|
});
|
||||||
@ -186,7 +207,7 @@ export const withValidRoomBulkDeleteRequest = (req: Request, res: Response, next
|
|||||||
return rejectRequest(res, error);
|
return rejectRequest(res, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.query.roomIds = data.roomIds.join(',');
|
req.query.roomIds = data.roomIds as any;
|
||||||
req.query.force = data.force ? 'true' : 'false';
|
req.query.force = data.force ? 'true' : 'false';
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -159,7 +159,7 @@ export class RoomService {
|
|||||||
async bulkDeleteRooms(
|
async bulkDeleteRooms(
|
||||||
roomIds: string[],
|
roomIds: string[],
|
||||||
forceDelete: boolean
|
forceDelete: boolean
|
||||||
): Promise<{ deleted: string[]; markedAsDeleted: string[] }> {
|
): Promise<{ deleted: string[]; markedForDeletion: string[] }> {
|
||||||
try {
|
try {
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
roomIds.map(async (roomId) => {
|
roomIds.map(async (roomId) => {
|
||||||
@ -184,26 +184,26 @@ export class RoomService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const deleted: string[] = [];
|
const deleted: string[] = [];
|
||||||
const markedAsDeleted: string[] = [];
|
const markedForDeletion: string[] = [];
|
||||||
|
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
if (result.value.status === 'deleted') {
|
if (result.value.status === 'deleted') {
|
||||||
deleted.push(result.value.roomId);
|
deleted.push(result.value.roomId);
|
||||||
} else if (result.value.status === 'marked') {
|
} else if (result.value.status === 'marked') {
|
||||||
markedAsDeleted.push(result.value.roomId);
|
markedForDeletion.push(result.value.roomId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.logger.error(`Failed to process deletion for a room: ${result.reason}`);
|
this.logger.error(`Failed to process deletion for a room: ${result.reason}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deleted.length === 0 && markedAsDeleted.length === 0) {
|
if (deleted.length === 0 && markedForDeletion.length === 0) {
|
||||||
this.logger.error('No rooms were deleted or marked as deleted.');
|
this.logger.error('No rooms were deleted or marked as deleted.');
|
||||||
throw internalError('No rooms were deleted or marked as deleted.');
|
throw internalError('No rooms were deleted or marked as deleted.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { deleted, markedAsDeleted };
|
return { deleted, markedForDeletion };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error deleting rooms:', error);
|
this.logger.error('Error deleting rooms:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -287,7 +287,7 @@ export class RoomService {
|
|||||||
protected async deleteExpiredRooms(): Promise<void> {
|
protected async deleteExpiredRooms(): Promise<void> {
|
||||||
let nextPageToken: string | undefined;
|
let nextPageToken: string | undefined;
|
||||||
const deletedRooms: string[] = [];
|
const deletedRooms: string[] = [];
|
||||||
const markedAsDeletedRooms: string[] = [];
|
const markedForDeletionRooms: string[] = [];
|
||||||
this.logger.verbose(`Checking expired rooms at ${new Date(Date.now()).toISOString()}`);
|
this.logger.verbose(`Checking expired rooms at ${new Date(Date.now()).toISOString()}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -306,10 +306,10 @@ export class RoomService {
|
|||||||
`Trying to delete ${expiredRoomIds.length} expired Meet rooms: ${expiredRoomIds.join(', ')}`
|
`Trying to delete ${expiredRoomIds.length} expired Meet rooms: ${expiredRoomIds.join(', ')}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const { deleted, markedAsDeleted } = await this.bulkDeleteRooms(expiredRoomIds, false);
|
const { deleted, markedForDeletion } = await this.bulkDeleteRooms(expiredRoomIds, false);
|
||||||
|
|
||||||
deletedRooms.push(...deleted);
|
deletedRooms.push(...deleted);
|
||||||
markedAsDeletedRooms.push(...markedAsDeleted);
|
markedForDeletionRooms.push(...markedForDeletion);
|
||||||
}
|
}
|
||||||
} while (nextPageToken);
|
} while (nextPageToken);
|
||||||
|
|
||||||
@ -317,8 +317,8 @@ export class RoomService {
|
|||||||
this.logger.verbose(`Successfully deleted ${deletedRooms.length} expired rooms`);
|
this.logger.verbose(`Successfully deleted ${deletedRooms.length} expired rooms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (markedAsDeletedRooms.length > 0) {
|
if (markedForDeletionRooms.length > 0) {
|
||||||
this.logger.verbose(`Marked as deleted ${markedAsDeletedRooms.length} expired rooms`);
|
this.logger.verbose(`Marked for deletion ${markedForDeletionRooms.length} expired rooms`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error deleting expired rooms:', error);
|
this.logger.error('Error deleting expired rooms:', error);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user