backend: Improve error handling and logging, and refactor code

This commit is contained in:
juancarmore 2025-04-30 14:00:38 +02:00
parent 963db44b55
commit 8357a54597
26 changed files with 343 additions and 437 deletions

View File

@ -3,6 +3,15 @@ import { ClaimGrants } from 'livekit-server-sdk';
import { container } from '../config/index.js';
import INTERNAL_CONFIG from '../config/internal-config.js';
import { MEET_ACCESS_TOKEN_EXPIRATION, MEET_REFRESH_TOKEN_EXPIRATION } from '../environment.js';
import {
errorInvalidCredentials,
errorInvalidRefreshToken,
errorInvalidTokenSubject,
errorRefreshTokenNotPresent,
errorUnauthorized,
handleError,
rejectRequestFromMeetError
} from '../models/error.model.js';
import { AuthService, LoggerService, TokenService, UserService } from '../services/index.js';
import { getCookieOptions } from '../utils/cookie-utils.js';
@ -16,7 +25,8 @@ export const login = async (req: Request, res: Response) => {
if (!user) {
logger.warn('Login failed');
return res.status(404).json({ message: 'Login failed. Invalid username or password' });
const error = errorInvalidCredentials();
return rejectRequestFromMeetError(res, error);
}
try {
@ -36,8 +46,7 @@ export const login = async (req: Request, res: Response) => {
logger.info(`Login succeeded for user '${username}'`);
return res.status(200).json({ message: 'Login succeeded' });
} catch (error) {
logger.error('Error generating token' + error);
return res.status(500).json({ message: 'Internal server error' });
handleError(res, error, 'generating token');
}
};
@ -56,7 +65,8 @@ export const refreshToken = async (req: Request, res: Response) => {
if (!refreshToken) {
logger.warn('No refresh token provided');
return res.status(400).json({ message: 'No refresh token provided' });
const error = errorRefreshTokenNotPresent();
return rejectRequestFromMeetError(res, error);
}
const tokenService = container.get(TokenService);
@ -65,8 +75,9 @@ export const refreshToken = async (req: Request, res: Response) => {
try {
payload = await tokenService.verifyToken(refreshToken);
} catch (error) {
logger.error('Error verifying refresh token' + error);
return res.status(400).json({ message: 'Invalid refresh token' });
logger.error('Error verifying refresh token:', error);
const meetError = errorInvalidRefreshToken();
return rejectRequestFromMeetError(res, meetError);
}
const username = payload.sub;
@ -75,7 +86,8 @@ export const refreshToken = async (req: Request, res: Response) => {
if (!user) {
logger.warn('Invalid refresh token subject');
return res.status(403).json({ message: 'Invalid refresh token subject' });
const error = errorInvalidTokenSubject();
return rejectRequestFromMeetError(res, error);
}
try {
@ -88,8 +100,7 @@ export const refreshToken = async (req: Request, res: Response) => {
logger.info(`Token refreshed for user ${username}`);
return res.status(200).json({ message: 'Token refreshed' });
} catch (error) {
logger.error('Error refreshing token' + error);
return res.status(500).json({ message: 'Internal server error' });
handleError(res, error, 'refreshing token');
}
};
@ -97,7 +108,8 @@ export const getProfile = (req: Request, res: Response) => {
const user = req.session?.user;
if (!user) {
return res.status(401).json({ message: 'Unauthorized' });
const error = errorUnauthorized();
return rejectRequestFromMeetError(res, error);
}
return res.status(200).json(user);

View File

@ -1,13 +1,12 @@
import { Request, Response } from 'express';
import { errorProFeature, rejectRequestFromMeetError } from '../../models/error.model.js';
export const updateAppearancePreferences = async (req: Request, res: Response) => {
return res
.status(402)
.json({ message: 'Storing appearance preference is a PRO feature. Please, Updrade to OpenVidu PRO' });
export const updateAppearancePreferences = async (_req: Request, res: Response) => {
const error = errorProFeature('update appearance preferences');
rejectRequestFromMeetError(res, error);
};
export const getAppearancePreferences = async (req: Request, res: Response) => {
return res
.status(402)
.json({ message: 'Getting appearance preference is a PRO feature. Please, Updrade to OpenVidu PRO' });
export const getAppearancePreferences = async (_req: Request, res: Response) => {
const error = errorProFeature('get appearance preferences');
rejectRequestFromMeetError(res, error);
};

View File

@ -1,7 +1,7 @@
import { SecurityPreferencesDTO, UpdateSecurityPreferencesDTO } from '@typings-ce';
import { Request, Response } from 'express';
import { container } from '../../config/index.js';
import { OpenViduMeetError } from '../../models/error.model.js';
import { handleError } from '../../models/error.model.js';
import { LoggerService, MeetStorageService } from '../../services/index.js';
export const updateSecurityPreferences = async (req: Request, res: Response) => {
@ -30,13 +30,7 @@ export const updateSecurityPreferences = async (req: Request, res: Response) =>
return res.status(200).json({ message: 'Security preferences updated successfully' });
} catch (error) {
if (error instanceof OpenViduMeetError) {
logger.error(`Error updating security preferences: ${error.message}`);
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
logger.error('Error updating security preferences:' + error);
return res.status(500).json({ message: 'Error updating security preferences' });
handleError(res, error, 'updating security preferences');
}
};
@ -44,15 +38,11 @@ export const getSecurityPreferences = async (_req: Request, res: Response) => {
const logger = container.get(LoggerService);
const preferenceService = container.get(MeetStorageService);
logger.verbose('Getting security preferences');
try {
const preferences = await preferenceService.getGlobalPreferences();
if (!preferences) {
//TODO: Create OpenViduMeetError for this case
logger.error('Security preferences not found');
return res.status(404).json({ message: 'Security preferences not found' });
}
// Convert the preferences to the DTO format by removing credentials
const securityPreferences = preferences.securityPreferences;
const securityPreferencesDTO: SecurityPreferencesDTO = {
@ -66,12 +56,6 @@ export const getSecurityPreferences = async (_req: Request, res: Response) => {
};
return res.status(200).json(securityPreferencesDTO);
} catch (error) {
if (error instanceof OpenViduMeetError) {
logger.error(`Error getting security preferences: ${error.message}`);
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
logger.error('Error getting security preferences:' + error);
return res.status(500).json({ message: 'Error fetching security preferences from database' });
handleError(res, error, 'getting security preferences');
}
};

View File

@ -1,7 +1,7 @@
import { WebhookPreferences } from '@typings-ce';
import { Request, Response } from 'express';
import { container } from '../../config/index.js';
import { OpenViduMeetError } from '../../models/error.model.js';
import { handleError } from '../../models/error.model.js';
import { LoggerService, MeetStorageService } from '../../services/index.js';
export const updateWebhookPreferences = async (req: Request, res: Response) => {
@ -18,13 +18,7 @@ export const updateWebhookPreferences = async (req: Request, res: Response) => {
return res.status(200).json({ message: 'Webhooks preferences updated successfully' });
} catch (error) {
if (error instanceof OpenViduMeetError) {
logger.error(`Error updating webhooks preferences: ${error.message}`);
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
logger.error('Error updating webhooks preferences:' + error);
return res.status(500).json({ message: 'Error updating webhooks preferences' });
handleError(res, error, 'updating webhooks preferences');
}
};
@ -32,23 +26,12 @@ export const getWebhookPreferences = async (_req: Request, res: Response) => {
const logger = container.get(LoggerService);
const preferenceService = container.get(MeetStorageService);
logger.verbose('Getting webhooks preferences');
try {
const preferences = await preferenceService.getGlobalPreferences();
if (!preferences) {
//TODO: Creare OpenViduMeetError for this case
logger.error('Webhooks preferences not found');
return res.status(404).json({ message: 'Webhooks preferences not found' });
}
return res.status(200).json(preferences.webhooksPreferences);
} catch (error) {
if (error instanceof OpenViduMeetError) {
logger.error(`Error getting webhooks preferences: ${error.message}`);
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
logger.error('Error getting webhooks preferences:' + error);
return res.status(500).json({ message: 'Error fetching webhooks preferences from database' });
handleError(res, error, 'getting webhooks preferences');
}
};

View File

@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import { container } from '../config/index.js';
import { OpenViduMeetError } from '../models/error.model.js';
import { handleError } from '../models/error.model.js';
import { LiveKitService, LoggerService, RoomService } from '../services/index.js';
export const endMeeting = async (req: Request, res: Response) => {
@ -14,30 +14,15 @@ export const endMeeting = async (req: Request, res: Response) => {
try {
await roomService.getMeetRoom(roomId);
} catch (error) {
logger.error(`Error getting room '${roomId}'`);
return handleError(res, error);
return handleError(res, error, `getting room '${roomId}'`);
}
try {
logger.info(`Ending meeting from room '${roomId}'`);
// To end a meeting, we need to delete the room from LiveKit
await livekitService.deleteRoom(roomId);
res.status(200).json({ message: 'Meeting ended successfully' });
} catch (error) {
logger.error(`Error ending meeting from room: ${roomId}`);
return handleError(res, error);
}
};
const handleError = (res: Response, error: OpenViduMeetError | unknown) => {
const logger = container.get(LoggerService);
logger.error(String(error));
if (error instanceof OpenViduMeetError) {
res.status(error.statusCode).json({ name: error.name, message: error.message });
} else {
res.status(500).json({
name: 'Meeting Error',
message: 'Internal server error. Meeting operation failed'
});
handleError(res, error, `ending meeting from room '${roomId}'`);
}
};

View File

@ -3,7 +3,7 @@ import { Request, Response } from 'express';
import { container } from '../config/index.js';
import INTERNAL_CONFIG from '../config/internal-config.js';
import { MEET_PARTICIPANT_TOKEN_EXPIRATION } from '../environment.js';
import { OpenViduMeetError } from '../models/error.model.js';
import { errorParticipantTokenStillValid, handleError, rejectRequestFromMeetError } from '../models/error.model.js';
import { LoggerService, ParticipantService, RoomService, TokenService } from '../services/index.js';
import { getCookieOptions } from '../utils/cookie-utils.js';
@ -15,7 +15,7 @@ export const generateParticipantToken = async (req: Request, res: Response) => {
const { roomId } = participantOptions;
try {
logger.verbose(`Generating participant token for room ${roomId}`);
logger.verbose(`Generating participant token for room '${roomId}'`);
await roomService.createLivekitRoom(roomId);
const token = await participantService.generateOrRefreshParticipantToken(participantOptions);
@ -26,8 +26,7 @@ export const generateParticipantToken = async (req: Request, res: Response) => {
);
return res.status(200).json({ token });
} catch (error) {
logger.error(`Error generating participant token for room: ${roomId}`);
return handleError(res, error);
handleError(res, error, `generating participant token for room '${roomId}'`);
}
};
@ -44,7 +43,9 @@ export const refreshParticipantToken = async (req: Request, res: Response) => {
try {
await tokenService.verifyToken(previousToken);
logger.verbose('Previous participant token is valid. No need to refresh');
return res.status(409).json({ message: 'Participant token is still valid' });
const error = errorParticipantTokenStillValid();
return rejectRequestFromMeetError(res, error);
} catch (error) {
logger.verbose('Previous participant token is invalid');
}
@ -63,11 +64,10 @@ export const refreshParticipantToken = async (req: Request, res: Response) => {
token,
getCookieOptions('/', MEET_PARTICIPANT_TOKEN_EXPIRATION)
);
logger.verbose(`Participant token refreshed for room ${roomId}`);
logger.verbose(`Participant token refreshed for room '${roomId}'`);
return res.status(200).json({ token });
} catch (error) {
logger.error(`Error refreshing participant token for room: ${roomId}`);
return handleError(res, error);
handleError(res, error, `refreshing participant token for room '${roomId}'`);
}
};
@ -77,24 +77,10 @@ export const deleteParticipant = async (req: Request, res: Response) => {
const { roomId, participantName } = req.params;
try {
logger.verbose(`Deleting participant '${participantName}' from room '${roomId}'`);
await participantService.deleteParticipant(participantName, roomId);
res.status(200).json({ message: 'Participant deleted' });
} catch (error) {
logger.error(`Error deleting participant from room: ${roomId}`);
return handleError(res, error);
}
};
const handleError = (res: Response, error: OpenViduMeetError | unknown) => {
const logger = container.get(LoggerService);
logger.error(String(error));
if (error instanceof OpenViduMeetError) {
res.status(error.statusCode).json({ name: error.name, message: error.message });
} else {
res.status(500).json({
name: 'Participant Error',
message: 'Internal server error. Participant operation failed'
});
handleError(res, error, `deleting participant '${participantName}' from room '${roomId}'`);
}
};

View File

@ -1,15 +1,15 @@
import { Request, Response } from 'express';
import { Readable } from 'stream';
import { container } from '../config/index.js';
import INTERNAL_CONFIG from '../config/internal-config.js';
import { internalError, OpenViduMeetError } from '../models/error.model.js';
import { handleError, internalError, rejectRequestFromMeetError } from '../models/error.model.js';
import { LoggerService, RecordingService } from '../services/index.js';
import { Readable } from 'stream';
export const startRecording = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const recordingService = container.get(RecordingService);
const { roomId } = req.body;
logger.info(`Initiating recording for room ${roomId}`);
logger.info(`Starting recording in room '${roomId}'`);
try {
const recordingInfo = await recordingService.startRecording(roomId);
@ -20,12 +20,7 @@ export const startRecording = async (req: Request, res: Response) => {
return res.status(201).json(recordingInfo);
} catch (error) {
if (error instanceof OpenViduMeetError) {
logger.error(`Error starting recording: ${error.message}`);
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
return res.status(500).json({ name: 'Recording Error', message: 'Failed to start recording' });
handleError(res, error, `starting recording in room '${roomId}'`);
}
};
@ -40,9 +35,9 @@ export const getRecordings = async (req: Request, res: Response) => {
if (payload && payload.video) {
const roomId = payload.video.room;
queryParams.roomId = roomId;
logger.debug(`Getting recordings for room ${roomId}`);
logger.info(`Getting recordings for room '${roomId}'`);
} else {
logger.verbose('Getting all recordings');
logger.info('Getting all recordings');
}
try {
@ -58,12 +53,7 @@ export const getRecordings = async (req: Request, res: Response) => {
}
});
} catch (error) {
if (error instanceof OpenViduMeetError) {
logger.error(`Error getting all recordings: ${error.message}`);
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
return res.status(500).json({ name: 'Recording Error', message: 'Unexpected error getting recordings' });
handleError(res, error, 'getting recordings');
}
};
@ -88,12 +78,7 @@ export const bulkDeleteRecordings = async (req: Request, res: Response) => {
// Some or all recordings could not be deleted
return res.status(200).json({ deleted, notDeleted });
} catch (error) {
if (error instanceof OpenViduMeetError) {
logger.error(`Error deleting recordings: ${error.message}`);
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
return res.status(500).json({ name: 'Recording Error', message: 'Unexpected error deleting recordings' });
handleError(res, error, 'deleting recordings');
}
};
@ -103,18 +88,13 @@ export const getRecording = async (req: Request, res: Response) => {
const recordingId = req.params.recordingId;
const fields = req.query.fields as string | undefined;
logger.info(`Getting recording ${recordingId}`);
logger.info(`Getting recording '${recordingId}'`);
try {
const recordingInfo = await recordingService.getRecording(recordingId, fields);
return res.status(200).json(recordingInfo);
} catch (error) {
if (error instanceof OpenViduMeetError) {
logger.error(`Error getting recording: ${error.message}`);
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
return res.status(500).json({ name: 'Recording Error', message: 'Unexpected error getting recording' });
handleError(res, error, `getting recording '${recordingId}'`);
}
};
@ -123,19 +103,14 @@ export const stopRecording = async (req: Request, res: Response) => {
const recordingId = req.params.recordingId;
try {
logger.info(`Initiating stop for recording ${recordingId}`);
logger.info(`Stopping recording '${recordingId}'`);
const recordingService = container.get(RecordingService);
const recordingInfo = await recordingService.stopRecording(recordingId);
res.setHeader('Location', `${req.protocol}://${req.get('host')}${req.originalUrl}`);
return res.status(202).json(recordingInfo);
} catch (error) {
if (error instanceof OpenViduMeetError) {
logger.error(`Error stopping recording: ${error.message}`);
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
return res.status(500).json({ name: 'Recording Error', message: 'Unexpected error stopping recording' });
handleError(res, error, `stopping recording '${recordingId}'`);
}
};
@ -143,19 +118,14 @@ export const deleteRecording = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const recordingService = container.get(RecordingService);
const recordingId = req.params.recordingId;
logger.info(`Deleting recording ${recordingId}`);
logger.info(`Deleting recording '${recordingId}'`);
try {
// TODO: Check role to determine if the request is from an admin or a participant
await recordingService.deleteRecording(recordingId);
return res.status(204).send();
} catch (error) {
if (error instanceof OpenViduMeetError) {
logger.error(`Error deleting recording: ${error.message}`);
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
return res.status(500).json({ name: 'Recording Error', message: 'Unexpected error deleting recording' });
handleError(res, error, `deleting recording '${recordingId}'`);
}
};
@ -174,7 +144,7 @@ export const getRecordingMedia = async (req: Request, res: Response) => {
let fileStream: Readable | undefined;
try {
logger.info(`Streaming recording ${recordingId}`);
logger.info(`Streaming recording '${recordingId}'`);
const recordingService = container.get(RecordingService);
const result = await recordingService.getRecordingAsStream(recordingId, range);
@ -182,11 +152,11 @@ export const getRecordingMedia = async (req: Request, res: Response) => {
fileStream = result.fileStream;
fileStream.on('error', (streamError) => {
logger.error(`Error streaming recording ${recordingId}: ${streamError.message}`);
logger.error(`Error streaming recording '${recordingId}': ${streamError.message}`);
if (!res.headersSent) {
const error = internalError(streamError);
res.status(error.statusCode).json({ name: 'Recording Error', message: error.message });
const error = internalError(`streaming recording '${recordingId}'`);
rejectRequestFromMeetError(res, error);
}
res.end();
@ -195,7 +165,7 @@ export const getRecordingMedia = async (req: Request, res: Response) => {
// Handle client disconnection
req.on('close', () => {
if (fileStream && !fileStream.destroyed) {
logger.debug(`Client closed connection for recording media ${recordingId}`);
logger.debug(`Client closed connection for recording media '${recordingId}'`);
fileStream.destroy();
}
});
@ -225,12 +195,11 @@ export const getRecordingMedia = async (req: Request, res: Response) => {
fileStream
.pipe(res)
.on('finish', () => {
logger.debug(`Finished streaming recording ${recordingId}`);
logger.debug(`Finished streaming recording '${recordingId}'`);
res.end();
})
.on('error', (err) => {
logger.error(`Error in response stream for ${recordingId}: ${err.message}`);
logger.error(`Error in response stream for recording '${recordingId}': ${err.message}`);
if (!res.headersSent) {
res.status(500).end();
@ -241,14 +210,6 @@ export const getRecordingMedia = async (req: Request, res: Response) => {
fileStream.destroy();
}
if (error instanceof OpenViduMeetError) {
logger.error(`Error streaming recording: ${error.message}`);
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
logger.error(`Unexpected error streaming recording ${recordingId}: ${error}`);
return res
.status(500)
.json({ name: 'Recording Error', message: 'An unexpected error occurred while processing the recording' });
handleError(res, error, `streaming recording '${recordingId}'`);
}
};

View File

@ -3,7 +3,7 @@ import { Request, Response } from 'express';
import { container } from '../config/index.js';
import INTERNAL_CONFIG from '../config/internal-config.js';
import { MEET_RECORDING_TOKEN_EXPIRATION } from '../environment.js';
import { OpenViduMeetError } from '../models/error.model.js';
import { handleError } from '../models/error.model.js';
import { LoggerService, ParticipantService, RoomService } from '../services/index.js';
import { getCookieOptions } from '../utils/cookie-utils.js';
@ -20,8 +20,7 @@ export const createRoom = async (req: Request, res: Response) => {
res.set('Location', `${baseUrl}${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${room.roomId}`);
return res.status(201).json(room);
} catch (error) {
logger.error(`Error creating room with options '${JSON.stringify(options)}'`);
handleError(res, error);
handleError(res, error, 'creating room');
}
};
@ -37,8 +36,7 @@ export const getRooms = async (req: Request, res: Response) => {
const maxItems = Number(queryParams.maxItems);
return res.status(200).json({ rooms, pagination: { isTruncated, nextPageToken, maxItems } });
} catch (error) {
logger.error('Error getting rooms');
handleError(res, error);
handleError(res, error, 'getting rooms');
}
};
@ -49,15 +47,14 @@ export const getRoom = async (req: Request, res: Response) => {
const fields = req.query.fields as string | undefined;
try {
logger.verbose(`Getting room with id '${roomId}'`);
logger.verbose(`Getting room '${roomId}'`);
const roomService = container.get(RoomService);
const room = await roomService.getMeetRoom(roomId, fields);
return res.status(200).json(room);
} catch (error) {
logger.error(`Error getting room with id '${roomId}'`);
handleError(res, error);
handleError(res, error, `getting room '${roomId}'`);
}
};
@ -70,7 +67,7 @@ export const deleteRoom = async (req: Request, res: Response) => {
const forceDelete = force === 'true';
try {
logger.verbose(`Deleting room: ${roomId}`);
logger.verbose(`Deleting room '${roomId}'`);
const { deleted } = await roomService.bulkDeleteRooms([roomId], forceDelete);
@ -80,10 +77,9 @@ export const deleteRoom = async (req: Request, res: Response) => {
}
// Room was marked as deleted
return res.status(202).json({ message: `Room ${roomId} marked as deleted` });
return res.status(202).json({ message: `Room '${roomId}' marked as deleted` });
} catch (error) {
logger.error(`Error deleting room: ${roomId}`);
handleError(res, error);
handleError(res, error, `deleting room '${roomId}'`);
}
};
@ -110,8 +106,8 @@ export const bulkDeleteRooms = async (req: Request, res: Response) => {
if (deleted.length === 0 && markedForDeletion.length > 0) {
const message =
markedForDeletion.length === 1
? `Room ${markedForDeletion[0]} marked for deletion`
: `Rooms ${markedForDeletion.join(', ')} marked for deletion`;
? `Room '${markedForDeletion[0]}' marked for deletion`
: `Rooms '${markedForDeletion.join(', ')}' marked for deletion`;
return res.status(202).json({ message });
}
@ -119,8 +115,7 @@ export const bulkDeleteRooms = async (req: Request, res: Response) => {
// Mixed result (some rooms deleted, some marked for deletion)
return res.status(200).json({ deleted, markedForDeletion });
} catch (error) {
logger.error(`Error deleting rooms: ${error}`);
handleError(res, error);
handleError(res, error, `deleting rooms`);
}
};
@ -130,14 +125,13 @@ export const updateRoomPreferences = async (req: Request, res: Response) => {
const roomPreferences = req.body;
const { roomId } = req.params;
logger.verbose(`Updating room preferences`);
logger.verbose(`Updating room preferences for room '${roomId}'`);
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);
handleError(res, error, `updating room preferences for room '${roomId}'`);
}
};
@ -159,8 +153,7 @@ export const generateRecordingToken = async (req: Request, res: Response) => {
);
return res.status(200).json({ token });
} catch (error) {
logger.error(`Error generating recording token for room '${roomId}'`);
handleError(res, error);
handleError(res, error, `generating recording token for room '${roomId}'`);
}
};
@ -175,8 +168,7 @@ export const getRoomRolesAndPermissions = async (req: Request, res: Response) =>
try {
await roomService.getMeetRoom(roomId);
} catch (error) {
logger.error(`Error getting room '${roomId}'`);
return handleError(res, error);
return handleError(res, error, `getting room '${roomId}'`);
}
logger.verbose(`Getting roles and associated permissions for room '${roomId}'`);
@ -214,18 +206,6 @@ export const getRoomRoleAndPermissions = async (req: Request, res: Response) =>
};
return res.status(200).json(roleAndPermissions);
} catch (error) {
logger.error(`Error getting room role and permissions for room '${roomId}' and secret '${secret}'`);
handleError(res, error);
}
};
const handleError = (res: Response, error: OpenViduMeetError | unknown) => {
const logger = container.get(LoggerService);
logger.error(String(error));
if (error instanceof OpenViduMeetError) {
res.status(error.statusCode).json({ name: error.name, message: error.message });
} else {
res.status(500).json({ name: 'Room Error', message: 'Internal server error. Room operation failed' });
handleError(res, error, `getting room role and permissions for room '${roomId}' and secret '${secret}'`);
}
};

View File

@ -12,7 +12,9 @@ import {
errorInvalidToken,
errorInvalidTokenSubject,
errorUnauthorized,
OpenViduMeetError
internalError,
OpenViduMeetError,
rejectRequestFromMeetError
} from '../models/index.js';
import { LoggerService, TokenService, UserService } from '../services/index.js';
@ -42,10 +44,11 @@ export const withAuth = (...validators: ((req: Request) => Promise<void>)[]): Re
}
if (lastError) {
return res.status(lastError.statusCode).json({ message: lastError.message });
return rejectRequestFromMeetError(res, lastError);
}
return res.status(500).json({ message: 'Internal server error' });
const error = internalError('authenticating user');
return rejectRequestFromMeetError(res, error);
};
};
@ -94,10 +97,7 @@ export const recordingTokenValidator = async (req: Request) => {
await validateTokenAndSetSession(req, INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME);
};
const validateTokenAndSetSession = async (
req: Request,
cookieName: string
) => {
const validateTokenAndSetSession = async (req: Request, cookieName: string) => {
const token = req.cookies[cookieName];
if (!token) {

View File

@ -1,4 +1,5 @@
import { NextFunction, Request, Response } from 'express';
import { errorMalformedBody, errorUnsupportedMediaType, rejectRequestFromMeetError } from '../models/error.model.js';
export const mediaTypeValidatorMiddleware = (req: Request, res: Response, next: NextFunction) => {
if (req.method === 'GET') {
@ -9,9 +10,8 @@ export const mediaTypeValidatorMiddleware = (req: Request, res: Response, next:
const contentType = req.headers['content-type'];
if (!contentType || !supportedMediaTypes.includes(contentType)) {
return res.status(415).json({
error: `Unsupported Media Type. Supported types: ${supportedMediaTypes.join(', ')}`
});
const error = errorUnsupportedMediaType(supportedMediaTypes);
return rejectRequestFromMeetError(res, error);
}
next();
@ -29,12 +29,9 @@ export const mediaTypeValidatorMiddleware = (req: Request, res: Response, next:
* @param next - Express next function to continue to the next middleware
*/
export const jsonSyntaxErrorHandler = (err: any, req: Request, res: Response, next: NextFunction): void => {
// This middleware handles JSON syntax errors
if (err instanceof SyntaxError && (err as any).status === 400 && 'body' in err) {
res.status(400).json({
error: 'Bad Request',
message: 'Malformed Body'
});
const error = errorMalformedBody();
rejectRequestFromMeetError(res, error);
} else {
next(err);
}

View File

@ -1,8 +1,8 @@
import { AuthMode, ParticipantOptions, ParticipantRole, UserRole } from '@typings-ce';
import { NextFunction, Request, Response } from 'express';
import { container } from '../config/index.js';
import { OpenViduMeetError } from '../models/error.model.js';
import { LoggerService, MeetStorageService, RoomService } from '../services/index.js';
import { errorInsufficientPermissions, handleError, rejectRequestFromMeetError } from '../models/error.model.js';
import { MeetStorageService, RoomService } from '../services/index.js';
import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
/**
@ -13,7 +13,6 @@ import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middlewa
* - Otherwise, allow anonymous access.
*/
export const configureParticipantTokenAuth = async (req: Request, res: Response, next: NextFunction) => {
const logger = container.get(LoggerService);
const globalPrefService = container.get(MeetStorageService);
const roomService = container.get(RoomService);
@ -23,16 +22,7 @@ export const configureParticipantTokenAuth = async (req: Request, res: Response,
const { roomId, secret } = req.body as ParticipantOptions;
role = await roomService.getRoomRoleBySecret(roomId, secret);
} catch (error) {
logger.error('Error getting room role by secret', error);
if (error instanceof OpenViduMeetError) {
return res.status(error.statusCode).json({ name: error.name, message: error.message });
} else {
return res.status(500).json({
name: 'Participant Error',
message: 'Internal server error. Participant operation failed'
});
}
return handleError(res, error, 'getting room role by secret');
}
let authMode: AuthMode;
@ -41,8 +31,7 @@ export const configureParticipantTokenAuth = async (req: Request, res: Response,
const { securityPreferences } = await globalPrefService.getGlobalPreferences();
authMode = securityPreferences.authentication.authMode;
} catch (error) {
logger.error('Error checking authentication preferences', error);
return res.status(500).json({ message: 'Internal server error' });
return handleError(res, error, 'checking authentication preferences');
}
const authValidators = [];
@ -68,7 +57,8 @@ export const withModeratorPermissions = async (req: Request, res: Response, next
const payload = req.session?.tokenClaims;
if (!payload) {
return res.status(403).json({ message: 'Insufficient permissions to access this resource' });
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
const sameRoom = payload.video?.room === roomId;
@ -76,7 +66,8 @@ export const withModeratorPermissions = async (req: Request, res: Response, next
const role = metadata.role as ParticipantRole;
if (!sameRoom || role !== ParticipantRole.MODERATOR) {
return res.status(403).json({ message: 'Insufficient permissions to access this resource' });
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
return next();

View File

@ -2,9 +2,21 @@ import { MeetRecordingAccess, MeetRoom, OpenViduMeetPermissions, RecordingPermis
import { NextFunction, Request, Response } from 'express';
import { container } from '../config/index.js';
import { RecordingHelper } from '../helpers/index.js';
import { OpenViduMeetError } from '../models/error.model.js';
import {
errorInsufficientPermissions,
errorRecordingDisabled,
errorRoomMetadataNotFound,
handleError,
rejectRequestFromMeetError
} from '../models/error.model.js';
import { LoggerService, MeetStorageService, RoomService } from '../services/index.js';
import { allowAnonymous, apiKeyValidator, recordingTokenValidator, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
import {
allowAnonymous,
apiKeyValidator,
recordingTokenValidator,
tokenAndRoleValidator,
withAuth
} from './auth.middleware.js';
export const withRecordingEnabled = async (req: Request, res: Response, next: NextFunction) => {
const logger = container.get(LoggerService);
@ -15,23 +27,14 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne
const room: MeetRoom = await roomService.getMeetRoom(roomId!);
if (!room.preferences?.recordingPreferences?.enabled) {
logger.debug(`Recording is disabled for room ${roomId}`);
return res.status(403).json({
message: 'Recording is disabled in this room'
});
logger.debug(`Recording is disabled for room '${roomId}'`);
const error = errorRecordingDisabled(roomId!);
return rejectRequestFromMeetError(res, error);
}
return next();
} catch (error) {
logger.error(`Error checking recording preferences: ${error}`);
if (error instanceof OpenViduMeetError) {
return res.status(error.statusCode).json({ name: error.name, message: error.message });
}
return res.status(500).json({
message: 'Unexpected error checking recording preferences'
});
handleError(res, error, 'checking recording preferences');
}
};
@ -40,7 +43,8 @@ export const withCanRecordPermission = async (req: Request, res: Response, next:
const payload = req.session?.tokenClaims;
if (!payload) {
return res.status(403).json({ message: 'Insufficient permissions to access this resource' });
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
const sameRoom = payload.video?.room === roomId;
@ -49,7 +53,8 @@ export const withCanRecordPermission = async (req: Request, res: Response, next:
const canRecord = permissions?.canRecord;
if (!sameRoom || !canRecord) {
return res.status(403).json({ message: 'Insufficient permissions to access this resource' });
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
return next();
@ -72,7 +77,8 @@ export const withCanRetrieveRecordingsPermission = async (req: Request, res: Res
const canRetrieveRecordings = permissions?.canRetrieveRecordings;
if (!sameRoom || !canRetrieveRecordings) {
return res.status(403).json({ message: 'Insufficient permissions to access this resource' });
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
return next();
@ -94,7 +100,8 @@ export const withCanDeleteRecordingsPermission = async (req: Request, res: Respo
const canDeleteRecordings = permissions?.canDeleteRecordings;
if (!sameRoom || !canDeleteRecordings) {
return res.status(403).json({ message: 'Insufficient permissions to access this resource' });
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
return next();
@ -107,7 +114,6 @@ export const withCanDeleteRecordingsPermission = async (req: Request, res: Respo
* - If recording access is public, anonymous users are allowed
*/
export const configureRecordingMediaAuth = async (req: Request, res: Response, next: NextFunction) => {
const logger = container.get(LoggerService);
const storageService = container.get(MeetStorageService);
let recordingAccess: MeetRecordingAccess;
@ -117,17 +123,13 @@ export const configureRecordingMediaAuth = async (req: Request, res: Response, n
const room = await storageService.getArchivedRoomMetadata(roomId!);
if (!room) {
return res.status(404).json({
message: 'Room metadata associated with the recording not found'
});
const error = errorRoomMetadataNotFound(roomId!);
return rejectRequestFromMeetError(res, error);
}
recordingAccess = room.preferences!.recordingPreferences.allowAccessTo;
} catch (error) {
logger.error(`Error checking recording permissions: ${error}`);
return res.status(500).json({
message: 'Unexpected error checking recording permissions'
});
return handleError(res, error, 'checking recording permissions');
}
const authValidators = [apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator];

View File

@ -1,5 +1,6 @@
import { NextFunction, Request, Response } from 'express';
import { z } from 'zod';
import { rejectUnprocessableRequest } from '../../models/error.model.js';
const LoginRequestSchema = z.object({
username: z.string().min(4, 'Username must be at least 4 characters long'),
@ -10,16 +11,7 @@ export const validateLoginRequest = (req: Request, res: Response, next: NextFunc
const { success, error, data } = LoginRequestSchema.safeParse(req.body);
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',
details: errors
});
return rejectUnprocessableRequest(res, error);
}
req.body = data;

View File

@ -1,6 +1,7 @@
import { ParticipantOptions } from '@typings-ce';
import { NextFunction, Request, Response } from 'express';
import { z } from 'zod';
import { rejectUnprocessableRequest } from '../../models/error.model.js';
import { nonEmptySanitizedRoomId } from './room-validator.middleware.js';
const ParticipantTokenRequestSchema: z.ZodType<ParticipantOptions> = z.object({
@ -13,24 +14,9 @@ export const validateParticipantTokenRequest = (req: Request, res: Response, nex
const { success, error, data } = ParticipantTokenRequestSchema.safeParse(req.body);
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.body = data;
next();
};
const rejectRequest = (res: Response, error: z.ZodError) => {
const errors = error.errors.map((error) => ({
field: error.path.join('.'),
message: error.message
}));
console.log(errors);
return res.status(422).json({
error: 'Unprocessable Entity',
message: 'Invalid request',
details: errors
});
};

View File

@ -10,6 +10,7 @@ import {
} from '@typings-ce';
import { NextFunction, Request, Response } from 'express';
import { z } from 'zod';
import { rejectUnprocessableRequest } from '../../models/error.model.js';
const WebhookPreferencesSchema: z.ZodType<WebhookPreferences> = z.object({
enabled: z.boolean(),
@ -49,7 +50,7 @@ export const validateWebhookPreferences = (req: Request, res: Response, next: Ne
const { success, error, data } = WebhookPreferencesSchema.safeParse(req.body);
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.body = data;
@ -60,22 +61,9 @@ export const validateSecurityPreferences = (req: Request, res: Response, next: N
const { success, error, data } = UpdateSecurityPreferencesDTOSchema.safeParse(req.body);
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.body = data;
next();
};
const rejectRequest = (res: Response, error: z.ZodError) => {
const errors = error.errors.map((error) => ({
field: error.path.join('.'),
message: error.message
}));
return res.status(422).json({
error: 'Unprocessable Entity',
message: 'Invalid request',
details: errors
});
};

View File

@ -1,6 +1,7 @@
import { MeetRecordingFilters } from '@typings-ce';
import { NextFunction, Request, Response } from 'express';
import { z } from 'zod';
import { rejectUnprocessableRequest } from '../../models/error.model.js';
import { nonEmptySanitizedRoomId } from './room-validator.middleware.js';
const nonEmptySanitizedRecordingId = (fieldName: string) =>
@ -114,7 +115,7 @@ export const withValidStartRecordingRequest = (req: Request, res: Response, next
const { success, error, data } = StartRecordingRequestSchema.safeParse(req.body);
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.body = data;
@ -125,7 +126,7 @@ export const withValidRecordingId = (req: Request, res: Response, next: NextFunc
const { success, error, data } = GetRecordingSchema.safeParse({ recordingId: req.params.recordingId });
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.params.recordingId = data.recordingId;
@ -136,7 +137,7 @@ export const withValidRecordingFiltersRequest = (req: Request, res: Response, ne
const { success, error, data } = GetRecordingsFiltersSchema.safeParse(req.query);
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.query = {
@ -150,7 +151,7 @@ export const withValidRecordingBulkDeleteRequest = (req: Request, res: Response,
const { success, error, data } = BulkDeleteRecordingsSchema.safeParse(req.query);
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.query.recordingIds = data.recordingIds.join(',');
@ -164,23 +165,10 @@ export const withValidGetMediaRequest = (req: Request, res: Response, next: Next
});
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.params.recordingId = data.params.recordingId;
req.headers.range = data.headers.range;
next();
};
const rejectRequest = (res: Response, error: z.ZodError) => {
const errors = error.errors.map((error) => ({
field: error.path.join('.'),
message: error.message
}));
return res.status(422).json({
error: 'Unprocessable Entity',
message: 'Invalid request',
details: errors
});
};

View File

@ -11,6 +11,7 @@ import { NextFunction, Request, Response } from 'express';
import ms from 'ms';
import { z } from 'zod';
import INTERNAL_CONFIG from '../../config/internal-config.js';
import { rejectUnprocessableRequest } from '../../models/error.model.js';
/**
* Sanitizes an identifier by removing/replacing invalid characters
@ -171,7 +172,7 @@ export const withValidRoomOptions = (req: Request, res: Response, next: NextFunc
const { success, error, data } = RoomRequestOptionsSchema.safeParse(req.body);
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.body = data;
@ -182,14 +183,13 @@ export const withValidRoomFiltersRequest = (req: Request, res: Response, next: N
const { success, error, data } = GetRoomFiltersSchema.safeParse(req.query);
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.query = {
...data,
maxItems: data.maxItems?.toString()
};
next();
};
@ -197,7 +197,7 @@ export const withValidRoomPreferences = (req: Request, res: Response, next: Next
const { success, error, data } = RoomPreferencesSchema.safeParse(req.body);
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.body = data;
@ -209,7 +209,7 @@ export const withValidRoomId = (req: Request, res: Response, next: NextFunction)
if (!success) {
error.errors[0].path = ['roomId'];
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.params.roomId = data;
@ -220,7 +220,7 @@ export const withValidRoomBulkDeleteRequest = (req: Request, res: Response, next
const { success, error, data } = BulkDeleteRoomsSchema.safeParse(req.query);
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.query.roomIds = data.roomIds as any;
@ -232,7 +232,7 @@ export const withValidRoomDeleteRequest = (req: Request, res: Response, next: Ne
const roomIdResult = nonEmptySanitizedRoomId('roomId').safeParse(req.params.roomId);
if (!roomIdResult.success) {
return rejectRequest(res, roomIdResult.error);
return rejectUnprocessableRequest(res, roomIdResult.error);
}
req.params.roomId = roomIdResult.data;
@ -240,11 +240,10 @@ export const withValidRoomDeleteRequest = (req: Request, res: Response, next: Ne
const forceResult = validForceQueryParam().safeParse(req.query.force);
if (!forceResult.success) {
return rejectRequest(res, forceResult.error);
return rejectUnprocessableRequest(res, forceResult.error);
}
req.query.force = forceResult.data ? 'true' : 'false';
next();
};
@ -252,22 +251,9 @@ export const withValidRoomSecret = (req: Request, res: Response, next: NextFunct
const { success, error, data } = RecordingTokenRequestSchema.safeParse(req.body);
if (!success) {
return rejectRequest(res, error);
return rejectUnprocessableRequest(res, error);
}
req.body = data;
next();
};
const rejectRequest = (res: Response, error: z.ZodError) => {
const errors = error.errors.map((error) => ({
field: error.path.join('.'),
message: error.message
}));
return res.status(422).json({
error: 'Unprocessable Entity',
message: 'Invalid request',
details: errors
});
};

View File

@ -3,10 +3,11 @@ import { NextFunction, Request, Response } from 'express';
import { container } from '../config/index.js';
import {
errorInsufficientPermissions,
errorRoomNotFoundOrEmptyRecordings,
OpenViduMeetError
errorRoomMetadataNotFound,
handleError,
rejectRequestFromMeetError
} from '../models/error.model.js';
import { LoggerService, MeetStorageService, RoomService } from '../services/index.js';
import { MeetStorageService, RoomService } from '../services/index.js';
import { allowAnonymous, apiKeyValidator, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
/**
@ -17,7 +18,6 @@ import { allowAnonymous, apiKeyValidator, tokenAndRoleValidator, withAuth } from
* - If room creation is allowed and does not require authentication, anonymous users are allowed.
*/
export const configureCreateRoomAuth = async (req: Request, res: Response, next: NextFunction) => {
const logger = container.get(LoggerService);
const globalPrefService = container.get(MeetStorageService);
let allowRoomCreation: boolean;
let requireAuthentication: boolean;
@ -26,8 +26,7 @@ export const configureCreateRoomAuth = async (req: Request, res: Response, next:
const { securityPreferences } = await globalPrefService.getGlobalPreferences();
({ allowRoomCreation, requireAuthentication } = securityPreferences.roomCreationPolicy);
} catch (error) {
logger.error('Error checking room creation policy:' + error);
return res.status(500).json({ message: 'Internal server error' });
return handleError(res, error, 'checking room creation policy');
}
const authValidators = [apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)];
@ -70,10 +69,10 @@ export const configureRoomAuthorization = async (req: Request, res: Response, ne
const role = metadata.role as ParticipantRole;
if (!sameRoom) {
return res.status(403).json({ message: 'Insufficient permissions to access this resource' });
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
const logger = container.get(LoggerService);
const globalPrefService = container.get(MeetStorageService);
let authMode: AuthMode;
@ -81,8 +80,7 @@ export const configureRoomAuthorization = async (req: Request, res: Response, ne
const { securityPreferences } = await globalPrefService.getGlobalPreferences();
authMode = securityPreferences.authentication.authMode;
} catch (error) {
logger.error('Error checking authentication preferences', error);
return res.status(500).json({ message: 'Internal server error' });
return handleError(res, error, 'checking authentication preferences');
}
// If the user is a moderator, it is necessary to add the user role validator
@ -96,7 +94,8 @@ export const configureRoomAuthorization = async (req: Request, res: Response, ne
}
// If the user is not a moderator, it is not allowed to access the resource
return res.status(403).json({ message: 'Insufficient permissions to access this resource' });
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
};
/**
@ -108,7 +107,6 @@ export const configureRoomAuthorization = async (req: Request, res: Response, ne
* - Otherwise, allow anonymous access.
*/
export const configureRecordingTokenAuth = async (req: Request, res: Response, next: NextFunction) => {
const logger = container.get(LoggerService);
const storageService = container.get(MeetStorageService);
const roomService = container.get(RoomService);
@ -121,7 +119,7 @@ export const configureRecordingTokenAuth = async (req: Request, res: Response, n
if (!room) {
// If the room is not found, it means that there are no recordings for that room or the room doesn't exist
throw errorRoomNotFoundOrEmptyRecordings(roomId);
throw errorRoomMetadataNotFound(roomId);
}
const recordingAccess = room.preferences!.recordingPreferences.allowAccessTo;
@ -133,16 +131,7 @@ export const configureRecordingTokenAuth = async (req: Request, res: Response, n
role = roomService.getRoomRoleBySecretFromRoom(room as MeetRoom, secret);
} catch (error) {
logger.error('Error getting room role by secret', error);
if (error instanceof OpenViduMeetError) {
return res.status(error.statusCode).json({ name: error.name, message: error.message });
} else {
return res.status(500).json({
name: 'Room Error',
message: 'Internal server error. Room operation failed'
});
}
return handleError(res, error, 'getting room role by secret');
}
let authMode: AuthMode;
@ -151,8 +140,7 @@ export const configureRecordingTokenAuth = async (req: Request, res: Response, n
const { securityPreferences } = await storageService.getGlobalPreferences();
authMode = securityPreferences.authentication.authMode;
} catch (error) {
logger.error('Error checking authentication preferences', error);
return res.status(500).json({ message: 'Internal server error' });
return handleError(res, error, 'checking authentication preferences');
}
const authValidators = [];

View File

@ -1,7 +1,13 @@
type StatusError = 400 | 401 | 403 | 404 | 406 | 409 | 416 | 422 | 500 | 503;
import { Response } from 'express';
import { container } from '../config/index.js';
import { LoggerService } from '../services/index.js';
import { z } from 'zod';
type StatusError = 400 | 401 | 402 | 403 | 404 | 406 | 409 | 415 | 416 | 422 | 500 | 503;
export class OpenViduMeetError extends Error {
name: string;
statusCode: StatusError;
constructor(error: string, message: string, statusCode: StatusError) {
super(message);
this.name = error;
@ -9,9 +15,42 @@ export class OpenViduMeetError extends Error {
}
}
interface ErrorResponse {
error: string;
message: string;
details?: {
field: string;
message: string;
}[];
}
// General errors
export const errorLivekitIsNotAvailable = (): OpenViduMeetError => {
export const errorMalformedBody = (): OpenViduMeetError => {
return new OpenViduMeetError('Bad Request', 'Malformed body', 400);
};
export const errorProFeature = (operation: string): OpenViduMeetError => {
return new OpenViduMeetError(
'Pro Feature Error',
`The operation '${operation}' is a PRO feature. Please, upgrade to OpenVidu PRO`,
402
);
};
export const errorUnsupportedMediaType = (supportedTypes: string[]): OpenViduMeetError => {
return new OpenViduMeetError(
'Unsupported Media Type',
`Unsupported media type. Supported types: ${supportedTypes.join(', ')}`,
415
);
};
export const internalError = (operationDescription: string): OpenViduMeetError => {
return new OpenViduMeetError('Interal Server Error', `Unexpected error while ${operationDescription}`, 500);
};
export const errorLivekitNotAvailable = (): OpenViduMeetError => {
return new OpenViduMeetError('LiveKit Error', 'LiveKit is not available', 503);
};
@ -19,42 +58,46 @@ export const errorS3NotAvailable = (error: any): OpenViduMeetError => {
return new OpenViduMeetError('S3 Error', `S3 is not available ${error}`, 503);
};
export const internalError = (error: any): OpenViduMeetError => {
return new OpenViduMeetError('Unexpected error', `Something went wrong ${error}`, 500);
};
export const errorRequest = (error: string): OpenViduMeetError => {
return new OpenViduMeetError('Wrong request', `Problem with some body parameter. ${error}`, 400);
};
export const errorUnprocessableParams = (error: string): OpenViduMeetError => {
return new OpenViduMeetError('Wrong request', `Some parameters are not valid. ${error}`, 422);
};
// Auth errors
export const errorInvalidCredentials = (): OpenViduMeetError => {
return new OpenViduMeetError('Login Error', 'Invalid username or password', 404);
};
export const errorUnauthorized = (): OpenViduMeetError => {
return new OpenViduMeetError('Authentication error', 'Unauthorized', 401);
return new OpenViduMeetError('Authentication Error', 'Unauthorized', 401);
};
export const errorInvalidToken = (): OpenViduMeetError => {
return new OpenViduMeetError('Authentication error', 'Invalid token', 401);
return new OpenViduMeetError('Authentication Error', 'Invalid token', 401);
};
export const errorInvalidTokenSubject = (): OpenViduMeetError => {
return new OpenViduMeetError('Authentication error', 'Invalid token subject', 403);
return new OpenViduMeetError('Authorization Error', 'Invalid token subject', 403);
};
export const errorRefreshTokenNotPresent = (): OpenViduMeetError => {
return new OpenViduMeetError('Refresh Token Error', 'No refresh token provided', 400);
};
export const errorInvalidRefreshToken = (): OpenViduMeetError => {
return new OpenViduMeetError('Refresh Token Error', 'Invalid refresh token', 400);
};
export const errorInsufficientPermissions = (): OpenViduMeetError => {
return new OpenViduMeetError('Authentication error', 'You do not have permission to access this resource', 403);
return new OpenViduMeetError('Authorization Error', 'You do not have permission to access this resource', 403);
};
export const errorInvalidApiKey = (): OpenViduMeetError => {
return new OpenViduMeetError('Authentication error', 'Invalid API key', 401);
return new OpenViduMeetError('Authentication Error', 'Invalid API key', 401);
};
// Recording errors
export const errorRecordingDisabled = (roomId: string): OpenViduMeetError => {
return new OpenViduMeetError('Recording Error', `Recording is disabled for room '${roomId}'`, 403);
};
export const errorRecordingNotFound = (recordingId: string): OpenViduMeetError => {
return new OpenViduMeetError('Recording Error', `Recording '${recordingId}' not found`, 404);
};
@ -72,7 +115,7 @@ export const errorRecordingCannotBeStoppedWhileStarting = (recordingId: string):
};
export const errorRecordingAlreadyStarted = (roomId: string): OpenViduMeetError => {
return new OpenViduMeetError('Recording Error', `The room '${roomId}' is already being recorded`, 409);
return new OpenViduMeetError('Recording Error', `Room '${roomId}' is already being recorded`, 409);
};
export const errorRecordingStartTimeout = (roomId: string): OpenViduMeetError => {
@ -88,7 +131,7 @@ export const errorRecordingRangeNotSatisfiable = (recordingId: string, fileSize:
};
export const errorRoomHasNoParticipants = (roomId: string): OpenViduMeetError => {
return new OpenViduMeetError('Recording Error', `The room '${roomId}' has no participants`, 409);
return new OpenViduMeetError('Recording Error', `Room '${roomId}' has no participants`, 409);
};
const isMatchingError = (error: OpenViduMeetError, originalError: OpenViduMeetError): boolean => {
@ -118,23 +161,74 @@ export const isErrorRecordingCannotBeStoppedWhileStarting = (
// Room errors
export const errorRoomNotFound = (roomId: string): OpenViduMeetError => {
return new OpenViduMeetError('Room Error', `The room '${roomId}' does not exist`, 404);
return new OpenViduMeetError('Room Error', `Room '${roomId}' does not exist`, 404);
};
export const errorRoomNotFoundOrEmptyRecordings = (roomId: string): OpenViduMeetError => {
return new OpenViduMeetError('Room Error', `The room '${roomId}' does not exist or has no recordings`, 404);
export const errorRoomMetadataNotFound = (roomId: string): OpenViduMeetError => {
return new OpenViduMeetError(
'Room Error',
`Room metadata for '${roomId}' not found. Room '${roomId}' does not exist or has no recordings associated`,
404
);
};
export const errorInvalidRoomSecret = (roomId: string, secret: string): OpenViduMeetError => {
return new OpenViduMeetError('Room Error', `The secret '${secret}' is not recognized for room '${roomId}'`, 400);
return new OpenViduMeetError('Room Error', `Secret '${secret}' is not recognized for room '${roomId}'`, 400);
};
// Participant errors
export const errorParticipantNotFound = (participantName: string, roomId: string): OpenViduMeetError => {
return new OpenViduMeetError('Participant Error', `'${participantName}' not found in room '${roomId}'`, 404);
return new OpenViduMeetError(
'Participant Error',
`Participant '${participantName}' not found in room '${roomId}'`,
404
);
};
export const errorParticipantAlreadyExists = (participantName: string, roomId: string): OpenViduMeetError => {
return new OpenViduMeetError('Room Error', `'${participantName}' already exists in room in ${roomId}`, 409);
return new OpenViduMeetError(
'Participant Error',
`Participant '${participantName}' already exists in room '${roomId}'`,
409
);
};
export const errorParticipantTokenStillValid = (): OpenViduMeetError => {
return new OpenViduMeetError('Participant Error', 'Participant token is still valid', 409);
};
// Handlers
export const handleError = (res: Response, error: OpenViduMeetError | unknown, operationDescription: string) => {
const logger = container.get(LoggerService);
logger.error(`Error while ${operationDescription}: ${error}`);
if (!(error instanceof OpenViduMeetError)) {
error = internalError(operationDescription);
}
return rejectRequestFromMeetError(res, error as OpenViduMeetError);
};
export const rejectRequestFromMeetError = (res: Response, error: OpenViduMeetError) => {
const errorResponse: ErrorResponse = {
error: error.name,
message: error.message
};
return res.status(error.statusCode).json(errorResponse);
};
export const rejectUnprocessableRequest = (res: Response, error: z.ZodError) => {
const errorDetails = error.errors.map((error) => ({
field: error.path.join('.'),
message: error.message
}));
const errorResponse: ErrorResponse = {
error: 'Unprocessable Entity',
message: 'Invalid request',
details: errorDetails
};
return res.status(422).json(errorResponse);
};

View File

@ -69,9 +69,11 @@ const createApp = () => {
// Serve OpenVidu Meet webcomponent bundle file
app.get('/meet/v1/openvidu-meet.js', (_req: Request, res: Response) => res.sendFile(webcomponentBundlePath));
// Serve OpenVidu Meet index.html file for all non-API routes
app.get(/^(?!\/api).*$/, (_req: Request, res: Response) => res.sendFile(indexHtmlPath));
app.get(/^(?!.*\/(api|internal-api)\/).*$/, (_req: Request, res: Response) => res.sendFile(indexHtmlPath));
// Catch all other routes and return 404
app.use((_req: Request, res: Response) => res.status(404).json({ error: 'Not found' }));
app.use((_req: Request, res: Response) =>
res.status(404).json({ error: 'Path Not Found', message: 'API path not implemented' })
);
return app;
};

View File

@ -17,7 +17,7 @@ import {
import { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL_PRIVATE } from '../environment.js';
import { RecordingHelper } from '../helpers/index.js';
import {
errorLivekitIsNotAvailable,
errorLivekitNotAvailable,
errorParticipantNotFound,
errorRoomNotFound,
internalError,
@ -41,7 +41,7 @@ export class LiveKitService {
return await this.roomClient.createRoom(options);
} catch (error) {
this.logger.error('Error creating LiveKit room:', error);
throw internalError(`Error creating room: ${error}`);
throw internalError('creating LiveKit room');
}
}
@ -89,8 +89,8 @@ export class LiveKitService {
try {
rooms = await this.roomClient.listRooms([roomName]);
} catch (error) {
this.logger.error(`Error getting room ${error}`);
throw internalError(`Error getting room: ${error}`);
this.logger.error(`Error getting room: ${error}`);
throw internalError(`getting LiveKit room '${roomName}'`);
}
if (rooms.length === 0) {
@ -104,8 +104,8 @@ export class LiveKitService {
try {
return await this.roomClient.listRooms();
} catch (error) {
this.logger.error(`Error getting LiveKit rooms ${error}`);
throw internalError(`Error getting rooms: ${error}`);
this.logger.error(`Error getting LiveKit rooms: ${error}`);
throw internalError('getting LiveKit rooms');
}
}
@ -120,8 +120,8 @@ export class LiveKitService {
await this.roomClient.deleteRoom(roomName);
} catch (error) {
this.logger.error(`Error deleting LiveKit room ${error}`);
throw internalError(`Error deleting room: ${error}`);
this.logger.error(`Error deleting LiveKit room: ${error}`);
throw internalError(`deleting LiveKit room '${roomName}'`);
}
}
@ -137,8 +137,8 @@ export class LiveKitService {
try {
return await this.roomClient.getParticipant(roomName, participantName);
} catch (error) {
this.logger.warn(`Participant ${participantName} not found in room ${roomName} ${error}`);
throw internalError(`Error getting participant: ${error}`);
this.logger.warn(`Participant ${participantName} not found in room ${roomName}: ${error}`);
throw internalError(`getting participant '${participantName}' in room '${roomName}'`);
}
}
@ -154,15 +154,11 @@ export class LiveKitService {
async sendData(roomName: string, rawData: Record<string, any>, options: SendDataOptions): Promise<void> {
try {
if (this.roomClient) {
const data: Uint8Array = new TextEncoder().encode(JSON.stringify(rawData));
await this.roomClient.sendData(roomName, data, DataPacket_Kind.RELIABLE, options);
} else {
throw internalError(`No RoomServiceClient available`);
}
const data: Uint8Array = new TextEncoder().encode(JSON.stringify(rawData));
await this.roomClient.sendData(roomName, data, DataPacket_Kind.RELIABLE, options);
} catch (error) {
this.logger.error(`Error sending data ${error}`);
throw internalError(`Error sending data: ${error}`);
this.logger.error(`Error sending data: ${error}`);
throw internalError(`sending data to LiveKit room '${roomName}'`);
}
}
@ -174,8 +170,8 @@ export class LiveKitService {
try {
return await this.egressClient.startRoomCompositeEgress(roomName, output, options);
} catch (error: any) {
this.logger.error('Error starting Room Composite Egress');
throw internalError(`Error starting Room Composite Egress: ${JSON.stringify(error)}`);
this.logger.error('Error starting Room Composite Egress:', error);
throw internalError(`starting Room Composite Egress for room '${roomName}'`);
}
}
@ -184,8 +180,8 @@ export class LiveKitService {
this.logger.info(`Stopping ${egressId} egress`);
return await this.egressClient.stopEgress(egressId);
} catch (error: any) {
this.logger.error(`Error stopping egress: JSON.stringify(error)`);
throw internalError(`Error stopping egress: ${error}`);
this.logger.error(`Error stopping egress: ${JSON.stringify(error)}`);
throw internalError(`stopping egress '${egressId}'`);
}
}
@ -210,7 +206,7 @@ export class LiveKitService {
}
this.logger.error(`Error getting egress: ${JSON.stringify(error)}`);
throw internalError(`Error getting egress: ${error}`);
throw internalError(`getting egress '${egressId}'`);
}
}
@ -292,7 +288,7 @@ export class LiveKitService {
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`Error getting in-progress recordings: ${errorMessage}`);
throw internalError(`Error getting in-progress recordings: ${errorMessage}`);
throw internalError(`getting in-progress egress for room '${roomName}'`);
}
}
@ -309,7 +305,7 @@ export class LiveKitService {
this.logger.error(error);
if (error?.cause?.code === 'ECONNREFUSED') {
throw errorLivekitIsNotAvailable();
throw errorLivekitNotAvailable();
}
return false;

View File

@ -377,7 +377,7 @@ export class RecordingService {
if (!fileSize) {
this.logger.error(`Error getting file size for recording ${recordingId}`);
throw internalError(`Error getting file size for recording ${recordingId}`);
throw internalError(`getting file size for recording '${recordingId}'`);
}
if (range) {
@ -509,7 +509,7 @@ export class RecordingService {
const filename = RecordingHelper.extractFilename(recordingInfo);
if (!filename) {
throw internalError(`Error extracting path from recording ${recordingId}`);
throw internalError(`extracting path from recording '${recordingId}'`);
}
const recordingPath = `${INTERNAL_CONFIG.S3_RECORDINGS_PREFIX}/${filename}`;

View File

@ -183,7 +183,7 @@ export class RedisService extends EventEmitter {
}
} catch (error) {
this.logger.error('Error getting value from Redis', error);
throw internalError(error);
throw internalError('getting value from Redis');
}
}
@ -234,7 +234,7 @@ export class RedisService extends EventEmitter {
return this.redisPublisher.del(keys);
} catch (error) {
throw internalError(`Error deleting key from Redis ${error}`);
throw internalError(`deleting key from Redis`);
}
}

View File

@ -14,7 +14,7 @@ import { uid } from 'uid/single';
import INTERNAL_CONFIG from '../config/internal-config.js';
import { MEET_NAME_ID } from '../environment.js';
import { MeetRoomHelper, OpenViduComponentsAdapterHelper, UtilsHelper } from '../helpers/index.js';
import { errorInvalidRoomSecret, errorRoomNotFoundOrEmptyRecordings, internalError } from '../models/error.model.js';
import { errorInvalidRoomSecret, errorRoomMetadataNotFound, internalError } from '../models/error.model.js';
import {
IScheduledTask,
LiveKitService,
@ -212,7 +212,7 @@ export class RoomService {
if (deleted.length === 0 && markedForDeletion.length === 0) {
this.logger.error('No rooms were deleted or marked as deleted.');
throw internalError('No rooms were deleted or marked as deleted.');
throw internalError('while deleting rooms. No rooms were deleted or marked as deleted.');
}
return { deleted, markedForDeletion };
@ -275,7 +275,7 @@ export class RoomService {
if (!room) {
// If the room is not found, it means that there are no recordings for that room or the room doesn't exist
throw errorRoomNotFoundOrEmptyRecordings(roomId);
throw errorRoomMetadataNotFound(roomId);
}
const role = this.getRoomRoleBySecretFromRoom(room as MeetRoom, secret);

View File

@ -84,7 +84,7 @@ export class S3Service {
throw errorS3NotAvailable(error);
}
throw internalError(error);
throw internalError('saving object to S3');
}
}
@ -109,7 +109,7 @@ export class S3Service {
return result;
} catch (error: any) {
this.logger.error(`S3 bulk delete: error deleting objects in bucket ${bucket}: ${error}`);
throw internalError(error);
throw internalError('deleting objects from S3');
}
}
@ -158,7 +158,7 @@ export class S3Service {
return response;
} catch (error: any) {
this.logger.error(`S3 listObjectsPaginated: error listing objects with prefix "${basePrefix}": ${error}`);
throw internalError(error);
throw internalError('listing objects from S3');
}
}
@ -182,7 +182,7 @@ export class S3Service {
}
this.logger.error(`S3 getObjectAsJson: error retrieving object ${name} from bucket ${bucket}: ${error}`);
throw internalError(error);
throw internalError('getting object as JSON from S3');
}
}
@ -212,7 +212,7 @@ export class S3Service {
throw errorS3NotAvailable(error);
}
throw internalError(error);
throw internalError('getting object as stream from S3');
}
}
@ -230,7 +230,7 @@ export class S3Service {
`S3 getHeaderObject: error getting header for object ${this.getFullKey(name)} in bucket ${bucket}: ${error}`
);
throw internalError(error);
throw internalError('getting header for object from S3');
}
}

View File

@ -3,7 +3,7 @@ import { inject, injectable } from 'inversify';
import ms from 'ms';
import { MEET_NAME_ID, MEET_SECRET, MEET_USER, MEET_WEBHOOK_ENABLED, MEET_WEBHOOK_URL } from '../../environment.js';
import { MeetLock, PasswordHelper } from '../../helpers/index.js';
import { errorRoomNotFound, OpenViduMeetError } from '../../models/error.model.js';
import { errorRoomNotFound, internalError, OpenViduMeetError } from '../../models/error.model.js';
import { LoggerService, MutexService, StorageFactory, StorageProvider } from '../index.js';
/**
@ -56,13 +56,19 @@ export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences,
* @returns {Promise<GlobalPreferences>}
*/
async getGlobalPreferences(): Promise<G> {
const preferences = await this.storageProvider.getGlobalPreferences();
let preferences = await this.storageProvider.getGlobalPreferences();
if (preferences) return preferences as G;
await this.initializeGlobalPreferences();
preferences = await this.storageProvider.getGlobalPreferences();
return this.storageProvider.getGlobalPreferences() as Promise<G>;
if (!preferences) {
this.logger.error('Global preferences not found after initialization');
throw internalError('getting global preferences');
}
return preferences as G;
}
/**