backend: Enhance room management logic
This commit is contained in:
parent
c598530918
commit
4ff00aad96
@ -3,7 +3,7 @@ import { Request, Response } from 'express';
|
|||||||
import { LoggerService } from '../services/logger.service.js';
|
import { LoggerService } from '../services/logger.service.js';
|
||||||
import { OpenViduMeetError } from '../models/error.model.js';
|
import { OpenViduMeetError } from '../models/error.model.js';
|
||||||
import { RoomService } from '../services/room.service.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) => {
|
export const createRoom = async (req: Request, res: Response) => {
|
||||||
const logger = container.get(LoggerService);
|
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) => {
|
export const getRooms = async (req: Request, res: Response) => {
|
||||||
const logger = container.get(LoggerService);
|
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 {
|
try {
|
||||||
logger.verbose('Getting rooms');
|
const response = await roomService.getAllMeetRooms(queryParams);
|
||||||
|
|
||||||
const roomService = container.get(RoomService);
|
return res.status(200).json(response);
|
||||||
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);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting rooms');
|
logger.error('Error getting rooms');
|
||||||
handleError(res, error);
|
handleError(res, error);
|
||||||
@ -48,18 +43,13 @@ export const getRoom = async (req: Request, res: Response) => {
|
|||||||
const logger = container.get(LoggerService);
|
const logger = container.get(LoggerService);
|
||||||
|
|
||||||
const { roomId } = req.params;
|
const { roomId } = req.params;
|
||||||
const fields = req.query.fields as string[] | undefined;
|
const fields = req.query.fields as string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.verbose(`Getting room with id '${roomId}'`);
|
logger.verbose(`Getting room with id '${roomId}'`);
|
||||||
|
|
||||||
const roomService = container.get(RoomService);
|
const roomService = container.get(RoomService);
|
||||||
const room = await roomService.getMeetRoom(roomId);
|
const room = await roomService.getMeetRoom(roomId, fields);
|
||||||
|
|
||||||
if (fields && fields.length > 0) {
|
|
||||||
const filteredRoom = filterObjectFields(room, fields);
|
|
||||||
return res.status(200).json(filteredRoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json(room);
|
return res.status(200).json(room);
|
||||||
} catch (error) {
|
} 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 logger = container.get(LoggerService);
|
||||||
const roomService = container.get(RoomService);
|
const roomService = container.get(RoomService);
|
||||||
|
|
||||||
const { roomId } = req.params;
|
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 {
|
try {
|
||||||
logger.verbose(`Deleting rooms: ${roomsToDelete.join(', ')}`);
|
logger.verbose(`Deleting room: ${roomId}`);
|
||||||
|
|
||||||
await roomService.deleteRooms(roomsToDelete);
|
await roomService.bulkDeleteRooms([roomId]);
|
||||||
logger.info(`Rooms deleted: ${roomsToDelete.join(', ')}`);
|
logger.info(`Room deleted: ${roomId}`);
|
||||||
return res.status(200).json({ message: 'Rooms deleted', deletedRooms: roomsToDelete });
|
return res.status(204).json();
|
||||||
} catch (error) {
|
} 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);
|
handleError(res, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -114,41 +114,19 @@ export const getParticipantRole = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const updateRoomPreferences = async (req: Request, res: Response) => {
|
export const updateRoomPreferences = async (req: Request, res: Response) => {
|
||||||
const logger = container.get(LoggerService);
|
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)}`);
|
logger.verbose(`Updating room preferences`);
|
||||||
// const { roomName, roomPreferences } = req.body;
|
|
||||||
|
|
||||||
// try {
|
try {
|
||||||
// const preferenceService = container.get(GlobalPreferencesService);
|
const room = await roomService.updateMeetRoomPreferences(roomId, roomPreferences);
|
||||||
// preferenceService.validateRoomPreferences(roomPreferences);
|
return res.status(200).json(room);
|
||||||
|
} catch (error) {
|
||||||
// const savedPreferences = await preferenceService.updateOpenViduRoomPreferences(roomName, roomPreferences);
|
logger.error(`Error saving room preferences: ${error}`);
|
||||||
|
handleError(res, error);
|
||||||
// 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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (res: Response, error: OpenViduMeetError | unknown) => {
|
const handleError = (res: Response, error: OpenViduMeetError | unknown) => {
|
||||||
|
|||||||
18
backend/src/helpers/utils.helper.ts
Normal file
18
backend/src/helpers/utils.helper.ts
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -3,11 +3,28 @@ import {
|
|||||||
MeetRoomOptions,
|
MeetRoomOptions,
|
||||||
MeetRecordingPreferences,
|
MeetRecordingPreferences,
|
||||||
MeetRoomPreferences,
|
MeetRoomPreferences,
|
||||||
MeetVirtualBackgroundPreferences
|
MeetVirtualBackgroundPreferences,
|
||||||
|
MeetRoomFilters
|
||||||
} 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';
|
||||||
|
|
||||||
|
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({
|
const RecordingPreferencesSchema: z.ZodType<MeetRecordingPreferences> = z.object({
|
||||||
enabled: z.boolean()
|
enabled: z.boolean()
|
||||||
});
|
});
|
||||||
@ -61,6 +78,33 @@ const GetParticipantRoleSchema = z.object({
|
|||||||
secret: z.string()
|
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) => {
|
export const withValidRoomOptions = (req: Request, res: Response, next: NextFunction) => {
|
||||||
const { success, error, data } = RoomRequestOptionsSchema.safeParse(req.body);
|
const { success, error, data } = RoomRequestOptionsSchema.safeParse(req.body);
|
||||||
|
|
||||||
@ -72,14 +116,40 @@ export const withValidRoomOptions = (req: Request, res: Response, next: NextFunc
|
|||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateGetRoomQueryParams = (req: Request, res: Response, next: NextFunction) => {
|
export const withValidRoomFiltersRequest = (req: Request, res: Response, next: NextFunction) => {
|
||||||
const fieldsQuery = req.query.fields as string | undefined;
|
const { success, error, data } = GetRoomFiltersSchema.safeParse(req.query);
|
||||||
|
|
||||||
if (fieldsQuery) {
|
if (!success) {
|
||||||
const fields = fieldsQuery.split(',').map((f) => f.trim());
|
return rejectRequest(res, error);
|
||||||
req.query.fields = fields;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -87,16 +157,7 @@ export const validateGetParticipantRoleRequest = (req: Request, res: Response, n
|
|||||||
const { success, error, data } = GetParticipantRoleSchema.safeParse(req.query);
|
const { success, error, data } = GetParticipantRoleSchema.safeParse(req.query);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
const errors = error.errors.map((error) => ({
|
return rejectRequest(res, error);
|
||||||
field: error.path.join('.'),
|
|
||||||
message: error.message
|
|
||||||
}));
|
|
||||||
|
|
||||||
return res.status(422).json({
|
|
||||||
error: 'Unprocessable Entity',
|
|
||||||
message: 'Invalid request query',
|
|
||||||
details: errors
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
req.query = data;
|
req.query = data;
|
||||||
|
|||||||
@ -3,7 +3,8 @@ export const enum RedisKeyPrefix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const enum RedisKeyName {
|
export const enum RedisKeyName {
|
||||||
GLOBAL_PREFERENCES = `${RedisKeyPrefix.BASE}global_preferences`
|
GLOBAL_PREFERENCES = `${RedisKeyPrefix.BASE}global_preferences`,
|
||||||
|
ROOM = `${RedisKeyPrefix.BASE}room:`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum RedisLockPrefix {
|
export const enum RedisLockPrefix {
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import {
|
|||||||
apiKeyValidator,
|
apiKeyValidator,
|
||||||
participantTokenValidator,
|
participantTokenValidator,
|
||||||
validateGetParticipantRoleRequest,
|
validateGetParticipantRoleRequest,
|
||||||
validateGetRoomQueryParams,
|
withValidRoomFiltersRequest,
|
||||||
withValidRoomOptions,
|
withValidRoomOptions,
|
||||||
configureCreateRoomAuth,
|
configureCreateRoomAuth,
|
||||||
configureRoomAuthorization
|
configureRoomAuthorization,
|
||||||
|
withValidRoomPreferences,
|
||||||
|
withValidRoomBulkDeleteRequest
|
||||||
} from '../middlewares/index.js';
|
} from '../middlewares/index.js';
|
||||||
|
|
||||||
import { UserRole } from '@typings-ce';
|
import { UserRole } from '@typings-ce';
|
||||||
@ -21,31 +23,37 @@ roomRouter.use(bodyParser.json());
|
|||||||
|
|
||||||
// Room Routes
|
// Room Routes
|
||||||
roomRouter.post('/', configureCreateRoomAuth, withValidRoomOptions, roomCtrl.createRoom);
|
roomRouter.post('/', configureCreateRoomAuth, withValidRoomOptions, roomCtrl.createRoom);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
roomRouter.get(
|
roomRouter.get(
|
||||||
'/',
|
'/',
|
||||||
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
|
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
|
||||||
validateGetRoomQueryParams,
|
withValidRoomFiltersRequest,
|
||||||
roomCtrl.getRooms
|
roomCtrl.getRooms
|
||||||
);
|
);
|
||||||
|
roomRouter.delete(
|
||||||
|
'/',
|
||||||
|
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
|
||||||
|
withValidRoomBulkDeleteRequest,
|
||||||
|
roomCtrl.bulkDeleteRooms
|
||||||
|
);
|
||||||
roomRouter.get(
|
roomRouter.get(
|
||||||
'/:roomId',
|
'/:roomId',
|
||||||
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), participantTokenValidator),
|
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), participantTokenValidator),
|
||||||
configureRoomAuthorization,
|
configureRoomAuthorization,
|
||||||
validateGetRoomQueryParams,
|
|
||||||
roomCtrl.getRoom
|
roomCtrl.getRoom
|
||||||
);
|
);
|
||||||
roomRouter.delete('/:roomId', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.deleteRooms);
|
roomRouter.delete('/:roomId', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.deleteRoom);
|
||||||
|
|
||||||
// Room preferences
|
|
||||||
roomRouter.put('/', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.updateRoomPreferences);
|
|
||||||
|
|
||||||
// Internal room routes
|
// Internal room routes
|
||||||
export const internalRoomRouter = Router();
|
export const internalRoomRouter = Router();
|
||||||
internalRoomRouter.use(bodyParser.urlencoded({ extended: true }));
|
internalRoomRouter.use(bodyParser.urlencoded({ extended: true }));
|
||||||
internalRoomRouter.use(bodyParser.json());
|
internalRoomRouter.use(bodyParser.json());
|
||||||
|
|
||||||
|
// Room preferences
|
||||||
|
internalRoomRouter.put(
|
||||||
|
'/:roomId',
|
||||||
|
withAuth(tokenAndRoleValidator(UserRole.ADMIN)),
|
||||||
|
withValidRoomPreferences,
|
||||||
|
roomCtrl.updateRoomPreferences
|
||||||
|
);
|
||||||
|
|
||||||
internalRoomRouter.get('/:roomId/participant-role', validateGetParticipantRoleRequest, roomCtrl.getParticipantRole);
|
internalRoomRouter.get('/:roomId/participant-role', validateGetParticipantRoleRequest, roomCtrl.getParticipantRole);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user