diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index f9ea3f8..5f4bcda 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -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); diff --git a/backend/src/controllers/global-preferences/appearance-preferences.controller.ts b/backend/src/controllers/global-preferences/appearance-preferences.controller.ts index 5c08ec4..c5de6ad 100644 --- a/backend/src/controllers/global-preferences/appearance-preferences.controller.ts +++ b/backend/src/controllers/global-preferences/appearance-preferences.controller.ts @@ -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); }; diff --git a/backend/src/controllers/global-preferences/security-preferences.controller.ts b/backend/src/controllers/global-preferences/security-preferences.controller.ts index e03b83f..1ab7f2a 100644 --- a/backend/src/controllers/global-preferences/security-preferences.controller.ts +++ b/backend/src/controllers/global-preferences/security-preferences.controller.ts @@ -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'); } }; diff --git a/backend/src/controllers/global-preferences/webhook-preferences.controller.ts b/backend/src/controllers/global-preferences/webhook-preferences.controller.ts index 5f65051..21c02dc 100644 --- a/backend/src/controllers/global-preferences/webhook-preferences.controller.ts +++ b/backend/src/controllers/global-preferences/webhook-preferences.controller.ts @@ -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'); } }; diff --git a/backend/src/controllers/meeting.controller.ts b/backend/src/controllers/meeting.controller.ts index acbd64f..5fe248d 100644 --- a/backend/src/controllers/meeting.controller.ts +++ b/backend/src/controllers/meeting.controller.ts @@ -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}'`); } }; diff --git a/backend/src/controllers/participant.controller.ts b/backend/src/controllers/participant.controller.ts index 6ac61f4..118fbc9 100644 --- a/backend/src/controllers/participant.controller.ts +++ b/backend/src/controllers/participant.controller.ts @@ -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}'`); } }; diff --git a/backend/src/controllers/recording.controller.ts b/backend/src/controllers/recording.controller.ts index 369a6ae..e434be0 100644 --- a/backend/src/controllers/recording.controller.ts +++ b/backend/src/controllers/recording.controller.ts @@ -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}'`); } }; diff --git a/backend/src/controllers/room.controller.ts b/backend/src/controllers/room.controller.ts index bd89f1a..943260b 100644 --- a/backend/src/controllers/room.controller.ts +++ b/backend/src/controllers/room.controller.ts @@ -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}'`); } }; diff --git a/backend/src/middlewares/auth.middleware.ts b/backend/src/middlewares/auth.middleware.ts index 3e14912..e22162a 100644 --- a/backend/src/middlewares/auth.middleware.ts +++ b/backend/src/middlewares/auth.middleware.ts @@ -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)[]): 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) { diff --git a/backend/src/middlewares/content-type.middleware.ts b/backend/src/middlewares/content-type.middleware.ts index 0ac032a..63bc2eb 100644 --- a/backend/src/middlewares/content-type.middleware.ts +++ b/backend/src/middlewares/content-type.middleware.ts @@ -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); } diff --git a/backend/src/middlewares/participant.middleware.ts b/backend/src/middlewares/participant.middleware.ts index 4be74f8..1c93281 100644 --- a/backend/src/middlewares/participant.middleware.ts +++ b/backend/src/middlewares/participant.middleware.ts @@ -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(); diff --git a/backend/src/middlewares/recording.middleware.ts b/backend/src/middlewares/recording.middleware.ts index f731757..9c1d8b0 100644 --- a/backend/src/middlewares/recording.middleware.ts +++ b/backend/src/middlewares/recording.middleware.ts @@ -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]; diff --git a/backend/src/middlewares/request-validators/auth-validator.middleware.ts b/backend/src/middlewares/request-validators/auth-validator.middleware.ts index 8e6f0d6..5dcaceb 100644 --- a/backend/src/middlewares/request-validators/auth-validator.middleware.ts +++ b/backend/src/middlewares/request-validators/auth-validator.middleware.ts @@ -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; diff --git a/backend/src/middlewares/request-validators/participant-validator.middleware.ts b/backend/src/middlewares/request-validators/participant-validator.middleware.ts index aaeb120..c268d9d 100644 --- a/backend/src/middlewares/request-validators/participant-validator.middleware.ts +++ b/backend/src/middlewares/request-validators/participant-validator.middleware.ts @@ -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 = 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 - }); -}; diff --git a/backend/src/middlewares/request-validators/preferences-validator.middleware.ts b/backend/src/middlewares/request-validators/preferences-validator.middleware.ts index c41cf4b..3bc8fa7 100644 --- a/backend/src/middlewares/request-validators/preferences-validator.middleware.ts +++ b/backend/src/middlewares/request-validators/preferences-validator.middleware.ts @@ -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 = 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 - }); -}; diff --git a/backend/src/middlewares/request-validators/recording-validator.middleware.ts b/backend/src/middlewares/request-validators/recording-validator.middleware.ts index 7f96d23..8093aab 100644 --- a/backend/src/middlewares/request-validators/recording-validator.middleware.ts +++ b/backend/src/middlewares/request-validators/recording-validator.middleware.ts @@ -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 - }); -}; diff --git a/backend/src/middlewares/request-validators/room-validator.middleware.ts b/backend/src/middlewares/request-validators/room-validator.middleware.ts index 6bc139d..3f0b7a3 100644 --- a/backend/src/middlewares/request-validators/room-validator.middleware.ts +++ b/backend/src/middlewares/request-validators/room-validator.middleware.ts @@ -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 - }); -}; diff --git a/backend/src/middlewares/room.middleware.ts b/backend/src/middlewares/room.middleware.ts index dec9e89..d225b4f 100644 --- a/backend/src/middlewares/room.middleware.ts +++ b/backend/src/middlewares/room.middleware.ts @@ -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 = []; diff --git a/backend/src/models/error.model.ts b/backend/src/models/error.model.ts index 77cb58c..7446463 100644 --- a/backend/src/models/error.model.ts +++ b/backend/src/models/error.model.ts @@ -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); }; diff --git a/backend/src/server.ts b/backend/src/server.ts index d5b9d9a..485f21e 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -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; }; diff --git a/backend/src/services/livekit.service.ts b/backend/src/services/livekit.service.ts index 690146e..88e6ca4 100644 --- a/backend/src/services/livekit.service.ts +++ b/backend/src/services/livekit.service.ts @@ -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, options: SendDataOptions): Promise { 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; diff --git a/backend/src/services/recording.service.ts b/backend/src/services/recording.service.ts index 80290db..2444a15 100644 --- a/backend/src/services/recording.service.ts +++ b/backend/src/services/recording.service.ts @@ -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}`; diff --git a/backend/src/services/redis.service.ts b/backend/src/services/redis.service.ts index d2565cc..4113d74 100644 --- a/backend/src/services/redis.service.ts +++ b/backend/src/services/redis.service.ts @@ -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`); } } diff --git a/backend/src/services/room.service.ts b/backend/src/services/room.service.ts index 3a70bf8..a4bc70e 100644 --- a/backend/src/services/room.service.ts +++ b/backend/src/services/room.service.ts @@ -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); diff --git a/backend/src/services/s3.service.ts b/backend/src/services/s3.service.ts index 648be45..3edc440 100644 --- a/backend/src/services/s3.service.ts +++ b/backend/src/services/s3.service.ts @@ -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'); } } diff --git a/backend/src/services/storage/storage.service.ts b/backend/src/services/storage/storage.service.ts index 3fd6754..5151dcf 100644 --- a/backend/src/services/storage/storage.service.ts +++ b/backend/src/services/storage/storage.service.ts @@ -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} */ async getGlobalPreferences(): Promise { - 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; + if (!preferences) { + this.logger.error('Global preferences not found after initialization'); + throw internalError('getting global preferences'); + } + + return preferences as G; } /**