backend: Enhance room management logic

This commit is contained in:
Carlos Santos 2025-04-07 17:14:49 +02:00
parent c598530918
commit 4ff00aad96
5 changed files with 161 additions and 95 deletions

View File

@ -3,7 +3,7 @@ import { Request, Response } from 'express';
import { LoggerService } from '../services/logger.service.js';
import { OpenViduMeetError } from '../models/error.model.js';
import { RoomService } from '../services/room.service.js';
import { MeetRoomOptions } from '@typings-ce';
import { MeetRoomFilters, MeetRoomOptions } from '@typings-ce';
export const createRoom = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
@ -24,20 +24,15 @@ export const createRoom = async (req: Request, res: Response) => {
export const getRooms = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const fields = req.query.fields as string[] | undefined;
const roomService = container.get(RoomService);
const queryParams = req.query as unknown as MeetRoomFilters;
logger.verbose('Getting all rooms');
try {
logger.verbose('Getting rooms');
const response = await roomService.getAllMeetRooms(queryParams);
const roomService = container.get(RoomService);
const rooms = await roomService.listOpenViduRooms();
if (fields && fields.length > 0) {
const filteredRooms = rooms.map((room) => filterObjectFields(room, fields));
return res.status(200).json(filteredRooms);
}
return res.status(200).json(rooms);
return res.status(200).json(response);
} catch (error) {
logger.error('Error getting rooms');
handleError(res, error);
@ -48,18 +43,13 @@ export const getRoom = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const { roomId } = req.params;
const fields = req.query.fields as string[] | undefined;
const fields = req.query.fields as string | undefined;
try {
logger.verbose(`Getting room with id '${roomId}'`);
const roomService = container.get(RoomService);
const room = await roomService.getMeetRoom(roomId);
if (fields && fields.length > 0) {
const filteredRoom = filterObjectFields(room, fields);
return res.status(200).json(filteredRoom);
}
const room = await roomService.getMeetRoom(roomId, fields);
return res.status(200).json(room);
} catch (error) {
@ -68,28 +58,38 @@ export const getRoom = async (req: Request, res: Response) => {
}
};
export const deleteRooms = async (req: Request, res: Response) => {
export const deleteRoom = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const roomService = container.get(RoomService);
const { roomId } = req.params;
const { roomIds } = req.body;
const roomsToDelete = roomId ? [roomId] : roomIds;
// TODO: Validate roomIds with ZOD
if (!Array.isArray(roomsToDelete) || roomsToDelete.length === 0) {
return res.status(400).json({ error: 'roomIds must be a non-empty array' });
}
try {
logger.verbose(`Deleting rooms: ${roomsToDelete.join(', ')}`);
logger.verbose(`Deleting room: ${roomId}`);
await roomService.deleteRooms(roomsToDelete);
logger.info(`Rooms deleted: ${roomsToDelete.join(', ')}`);
return res.status(200).json({ message: 'Rooms deleted', deletedRooms: roomsToDelete });
await roomService.bulkDeleteRooms([roomId]);
logger.info(`Room deleted: ${roomId}`);
return res.status(204).json();
} catch (error) {
logger.error(`Error deleting rooms: ${roomsToDelete.join(', ')}`);
logger.error(`Error deleting room: ${roomId}`);
handleError(res, error);
}
};
export const bulkDeleteRooms = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const roomService = container.get(RoomService);
const { roomIds } = req.query;
logger.info(`Deleting rooms: ${roomIds}`);
try {
const roomIdsArray = (roomIds as string).split(',');
await roomService.bulkDeleteRooms(roomIdsArray);
return res.status(204).send();
} catch (error) {
logger.error(`Error deleting rooms: ${error}`);
handleError(res, error);
}
};
@ -114,41 +114,19 @@ export const getParticipantRole = async (req: Request, res: Response) => {
export const updateRoomPreferences = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const roomService = container.get(RoomService);
const roomPreferences = req.body;
const { roomId } = req.params;
logger.verbose(`Updating room preferences: ${JSON.stringify(req.body)}`);
// const { roomName, roomPreferences } = req.body;
logger.verbose(`Updating room preferences`);
// try {
// const preferenceService = container.get(GlobalPreferencesService);
// preferenceService.validateRoomPreferences(roomPreferences);
// const savedPreferences = await preferenceService.updateOpenViduRoomPreferences(roomName, roomPreferences);
// return res
// .status(200)
// .json({ message: 'Room preferences updated successfully.', preferences: savedPreferences });
// } catch (error) {
// if (error instanceof OpenViduCallError) {
// logger.error(`Error saving room preferences: ${error.message}`);
// return res.status(error.statusCode).json({ name: error.name, message: error.message });
// }
// logger.error('Error saving room preferences:' + error);
// return res.status(500).json({ message: 'Error saving room preferences', error });
// }
};
const filterObjectFields = (obj: Record<string, any>, fields: string[]): Record<string, any> => {
return fields.reduce(
(acc, field) => {
if (Object.prototype.hasOwnProperty.call(obj, field)) {
acc[field] = obj[field];
}
return acc;
},
{} as Record<string, any>
);
try {
const room = await roomService.updateMeetRoomPreferences(roomId, roomPreferences);
return res.status(200).json(room);
} catch (error) {
logger.error(`Error saving room preferences: ${error}`);
handleError(res, error);
}
};
const handleError = (res: Response, error: OpenViduMeetError | unknown) => {

View File

@ -0,0 +1,18 @@
export class UtilsHelper {
private constructor() {
// Prevent instantiation of this utility class
}
static filterObjectFields = (obj: Record<string, unknown>, fields: string[]): Record<string, any> => {
return fields.reduce(
(acc, field) => {
if (Object.prototype.hasOwnProperty.call(obj, field)) {
acc[field] = obj[field];
}
return acc;
},
{} as Record<string, unknown>
);
};
}

View File

@ -3,11 +3,28 @@ import {
MeetRoomOptions,
MeetRecordingPreferences,
MeetRoomPreferences,
MeetVirtualBackgroundPreferences
MeetVirtualBackgroundPreferences,
MeetRoomFilters
} from '@typings-ce';
import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
const sanitizeId = (val: string): string => {
return val
.trim() // Remove leading and trailing spaces
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/[^a-zA-Z0-9_-]/g, ''); // Remove special characters (allow alphanumeric, hyphens and underscores)
};
const nonEmptySanitizedString = (fieldName: string) =>
z
.string()
.min(1, { message: `${fieldName} is required and cannot be empty` })
.transform(sanitizeId)
.refine((data) => data !== '', {
message: `${fieldName} cannot be empty after sanitization`
});
const RecordingPreferencesSchema: z.ZodType<MeetRecordingPreferences> = z.object({
enabled: z.boolean()
});
@ -61,6 +78,33 @@ const GetParticipantRoleSchema = z.object({
secret: z.string()
});
const GetRoomFiltersSchema: z.ZodType<MeetRoomFilters> = z.object({
maxItems: z.coerce
.number()
.int()
.transform((val) => (val > 100 ? 100 : val))
.default(10),
nextPageToken: z.string().optional(),
fields: z.string().optional()
});
const BulkDeleteRoomsSchema = z.object({
roomIds: z.preprocess(
(arg) => {
if (typeof arg === 'string') {
// If the argument is a string, it is expected to be a comma-separated list of recording IDs.
return arg
.split(',')
.map((s) => s.trim())
.filter((s) => s !== '');
}
return arg;
},
z.array(nonEmptySanitizedString('recordingId')).default([])
)
});
export const withValidRoomOptions = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = RoomRequestOptionsSchema.safeParse(req.body);
@ -72,14 +116,40 @@ export const withValidRoomOptions = (req: Request, res: Response, next: NextFunc
next();
};
export const validateGetRoomQueryParams = (req: Request, res: Response, next: NextFunction) => {
const fieldsQuery = req.query.fields as string | undefined;
export const withValidRoomFiltersRequest = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = GetRoomFiltersSchema.safeParse(req.query);
if (fieldsQuery) {
const fields = fieldsQuery.split(',').map((f) => f.trim());
req.query.fields = fields;
if (!success) {
return rejectRequest(res, error);
}
req.query = {
...data,
maxItems: data.maxItems?.toString()
};
next();
};
export const withValidRoomPreferences = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = RoomPreferencesSchema.safeParse(req.body);
if (!success) {
return rejectRequest(res, error);
}
req.body = data;
next();
};
export const withValidRoomBulkDeleteRequest = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = BulkDeleteRoomsSchema.safeParse(req.query);
if (!success) {
return rejectRequest(res, error);
}
req.query.roomIds = data.roomIds.join(',');
next();
};
@ -87,16 +157,7 @@ export const validateGetParticipantRoleRequest = (req: Request, res: Response, n
const { success, error, data } = GetParticipantRoleSchema.safeParse(req.query);
if (!success) {
const errors = error.errors.map((error) => ({
field: error.path.join('.'),
message: error.message
}));
return res.status(422).json({
error: 'Unprocessable Entity',
message: 'Invalid request query',
details: errors
});
return rejectRequest(res, error);
}
req.query = data;

View File

@ -3,7 +3,8 @@ export const enum RedisKeyPrefix {
}
export const enum RedisKeyName {
GLOBAL_PREFERENCES = `${RedisKeyPrefix.BASE}global_preferences`
GLOBAL_PREFERENCES = `${RedisKeyPrefix.BASE}global_preferences`,
ROOM = `${RedisKeyPrefix.BASE}room:`,
}
export const enum RedisLockPrefix {

View File

@ -7,10 +7,12 @@ import {
apiKeyValidator,
participantTokenValidator,
validateGetParticipantRoleRequest,
validateGetRoomQueryParams,
withValidRoomFiltersRequest,
withValidRoomOptions,
configureCreateRoomAuth,
configureRoomAuthorization
configureRoomAuthorization,
withValidRoomPreferences,
withValidRoomBulkDeleteRequest
} from '../middlewares/index.js';
import { UserRole } from '@typings-ce';
@ -21,31 +23,37 @@ roomRouter.use(bodyParser.json());
// Room Routes
roomRouter.post('/', configureCreateRoomAuth, withValidRoomOptions, roomCtrl.createRoom);
roomRouter.get(
'/',
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
validateGetRoomQueryParams,
withValidRoomFiltersRequest,
roomCtrl.getRooms
);
roomRouter.delete(
'/',
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
withValidRoomBulkDeleteRequest,
roomCtrl.bulkDeleteRooms
);
roomRouter.get(
'/:roomId',
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), participantTokenValidator),
configureRoomAuthorization,
validateGetRoomQueryParams,
roomCtrl.getRoom
);
roomRouter.delete('/:roomId', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.deleteRooms);
// Room preferences
roomRouter.put('/', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.updateRoomPreferences);
roomRouter.delete('/:roomId', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.deleteRoom);
// Internal room routes
export const internalRoomRouter = Router();
internalRoomRouter.use(bodyParser.urlencoded({ extended: true }));
internalRoomRouter.use(bodyParser.json());
// Room preferences
internalRoomRouter.put(
'/:roomId',
withAuth(tokenAndRoleValidator(UserRole.ADMIN)),
withValidRoomPreferences,
roomCtrl.updateRoomPreferences
);
internalRoomRouter.get('/:roomId/participant-role', validateGetParticipantRoleRequest, roomCtrl.getParticipantRole);