From 2d82d6a96dd480916572ec9ce9b34a39e24e907c Mon Sep 17 00:00:00 2001 From: juancarmore Date: Fri, 28 Mar 2025 18:55:39 +0100 Subject: [PATCH] backend: Add security and webhook preferences controllers and validation middleware --- .../security-preferences.controller.ts | 76 +++++++++++++++++ .../webhook-preferences.controller.ts | 53 ++++++++++++ backend/src/controllers/index.ts | 6 +- .../preferences-validator.middleware.ts | 81 +++++++++++++++++++ .../src/routes/global-preferences.routes.ts | 39 +++++++-- .../preferences/global-preferences.service.ts | 10 +++ 6 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 backend/src/controllers/global-preferences/security-preferences.controller.ts create mode 100644 backend/src/controllers/global-preferences/webhook-preferences.controller.ts create mode 100644 backend/src/middlewares/request-validators/preferences-validator.middleware.ts diff --git a/backend/src/controllers/global-preferences/security-preferences.controller.ts b/backend/src/controllers/global-preferences/security-preferences.controller.ts new file mode 100644 index 0000000..8db8b59 --- /dev/null +++ b/backend/src/controllers/global-preferences/security-preferences.controller.ts @@ -0,0 +1,76 @@ +import { container } from '../../config/dependency-injector.config.js'; +import { Request, Response } from 'express'; +import { LoggerService } from '../../services/logger.service.js'; +import { GlobalPreferencesService } from '../../services/preferences/index.js'; +import { OpenViduMeetError } from '../../models/error.model.js'; +import { SecurityPreferencesDTO, UpdateSecurityPreferencesDTO } from '@typings-ce'; + +export const updateSecurityPreferences = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const globalPrefService = container.get(GlobalPreferencesService); + + logger.verbose(`Updating security preferences: ${JSON.stringify(req.body)}`); + const securityPreferences = req.body as UpdateSecurityPreferencesDTO; + + try { + const globalPreferences = await globalPrefService.getGlobalPreferences(); + + if (securityPreferences.roomCreationPolicy) { + globalPreferences.securityPreferences.roomCreationPolicy = securityPreferences.roomCreationPolicy; + } + + if (securityPreferences.authentication) { + const currentAuth = globalPreferences.securityPreferences.authentication; + const newAuth = securityPreferences.authentication; + + currentAuth.authMode = newAuth.authMode; + currentAuth.method.type = newAuth.method.type; + } + + await globalPrefService.saveGlobalPreferences(globalPreferences); + + 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' }); + } +}; + +export const getSecurityPreferences = async (_req: Request, res: Response) => { + const logger = container.get(LoggerService); + const preferenceService = container.get(GlobalPreferencesService); + + try { + const preferences = await preferenceService.getGlobalPreferences(); + + if (!preferences) { + 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 = { + roomCreationPolicy: securityPreferences.roomCreationPolicy, + authentication: { + authMode: securityPreferences.authentication.authMode, + method: { + type: securityPreferences.authentication.method.type + } + } + }; + 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' }); + } +}; diff --git a/backend/src/controllers/global-preferences/webhook-preferences.controller.ts b/backend/src/controllers/global-preferences/webhook-preferences.controller.ts new file mode 100644 index 0000000..4b6d112 --- /dev/null +++ b/backend/src/controllers/global-preferences/webhook-preferences.controller.ts @@ -0,0 +1,53 @@ +import { container } from '../../config/dependency-injector.config.js'; +import { Request, Response } from 'express'; +import { LoggerService } from '../../services/logger.service.js'; +import { GlobalPreferencesService } from '../../services/preferences/index.js'; +import { OpenViduMeetError } from '../../models/error.model.js'; +import { WebhookPreferences } from '@typings-ce'; + +export const updateWebhookPreferences = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const globalPrefService = container.get(GlobalPreferencesService); + + logger.verbose(`Updating webhooks preferences: ${JSON.stringify(req.body)}`); + const webhookPreferences = req.body as WebhookPreferences; + + try { + const globalPreferences = await globalPrefService.getGlobalPreferences(); + globalPreferences.webhooksPreferences = webhookPreferences; + await globalPrefService.saveGlobalPreferences(globalPreferences); + + 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' }); + } +}; + +export const getWebhookPreferences = async (_req: Request, res: Response) => { + const logger = container.get(LoggerService); + const preferenceService = container.get(GlobalPreferencesService); + + try { + const preferences = await preferenceService.getGlobalPreferences(); + + if (!preferences) { + 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' }); + } +}; diff --git a/backend/src/controllers/index.ts b/backend/src/controllers/index.ts index 70f3fe2..904cff8 100644 --- a/backend/src/controllers/index.ts +++ b/backend/src/controllers/index.ts @@ -2,4 +2,8 @@ export * from './auth.controller.js'; export * from './recording.controller.js'; export * from './room.controller.js'; export * from './participant.controller.js'; -export * from './livekit-webhook.controller.js'; \ No newline at end of file +export * from './livekit-webhook.controller.js'; + +export * from './global-preferences/appearance-preferences.controller.js'; +export * from './global-preferences/webhook-preferences.controller.js'; +export * from './global-preferences/security-preferences.controller.js'; diff --git a/backend/src/middlewares/request-validators/preferences-validator.middleware.ts b/backend/src/middlewares/request-validators/preferences-validator.middleware.ts new file mode 100644 index 0000000..7b28149 --- /dev/null +++ b/backend/src/middlewares/request-validators/preferences-validator.middleware.ts @@ -0,0 +1,81 @@ +import { + AuthenticationPreferencesDTO, + AuthMode, + AuthType, + RoomCreationPolicy, + SingleUserAuthDTO, + UpdateSecurityPreferencesDTO, + ValidAuthMethodDTO, + WebhookPreferences +} from '@typings-ce'; +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; + +const WebhookPreferencesSchema: z.ZodType = z.object({ + enabled: z.boolean(), + url: z.string().url() +}); + +const AuthModeSchema: z.ZodType = z.enum([AuthMode.NONE, AuthMode.MODERATORS_ONLY, AuthMode.ALL_USERS]); + +const AuthTypeSchema: z.ZodType = z.enum([AuthType.SINGLE_USER]); + +const SingleUserAuthDTOSchema: z.ZodType = z.object({ + type: AuthTypeSchema +}); + +const ValidAuthMethodDTOSchema: z.ZodType = SingleUserAuthDTOSchema; + +const AuthenticationPreferencesDTOSchema: z.ZodType = z.object({ + authMode: AuthModeSchema, + method: ValidAuthMethodDTOSchema +}); + +const RoomCreationPolicySchema: z.ZodType = z.object({ + allowRoomCreation: z.boolean(), + requireAuthentication: z.boolean() +}); + +const UpdateSecurityPreferencesDTOSchema: z.ZodType = z + .object({ + authentication: AuthenticationPreferencesDTOSchema.optional(), + roomCreationPolicy: RoomCreationPolicySchema.optional() + }) + .refine((data) => Object.keys(data).length > 0, { + message: 'At least one field must be provided for the update' + }); + +export const validateWebhookPreferences = (req: Request, res: Response, next: NextFunction) => { + const { success, error, data } = WebhookPreferencesSchema.safeParse(req.body); + + if (!success) { + return rejectRequest(res, error); + } + + req.body = data; + next(); +}; + +export const validateSecurityPreferences = (req: Request, res: Response, next: NextFunction) => { + const { success, error, data } = UpdateSecurityPreferencesDTOSchema.safeParse(req.body); + + if (!success) { + return rejectRequest(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 body', + details: errors + }); +}; diff --git a/backend/src/routes/global-preferences.routes.ts b/backend/src/routes/global-preferences.routes.ts index a47fa58..45873d2 100644 --- a/backend/src/routes/global-preferences.routes.ts +++ b/backend/src/routes/global-preferences.routes.ts @@ -1,24 +1,49 @@ import { Router } from 'express'; import bodyParser from 'body-parser'; -import { - getAppearancePreferences, - updateAppearancePreferences -} from '../controllers/global-preferences/appearance-preferences.controller.js'; +import * as appearancePrefCtrl from '../controllers/global-preferences/appearance-preferences.controller.js'; +import * as webhookPrefCtrl from '../controllers/global-preferences/webhook-preferences.controller.js'; +import * as securityPrefCtrl from '../controllers/global-preferences/security-preferences.controller.js'; import { withAuth, tokenAndRoleValidator, apiKeyValidator } from '../middlewares/auth.middleware.js'; import { UserRole } from '@typings-ce'; +import { + validateSecurityPreferences, + validateWebhookPreferences +} from '../middlewares/request-validators/preferences-validator.middleware.js'; export const preferencesRouter = Router(); - preferencesRouter.use(bodyParser.urlencoded({ extended: true })); preferencesRouter.use(bodyParser.json()); +// Webhook preferences +preferencesRouter.put( + '/webhooks', + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + validateWebhookPreferences, + webhookPrefCtrl.updateWebhookPreferences +); +preferencesRouter.get( + '/webhooks', + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + webhookPrefCtrl.getWebhookPreferences +); + +// Security preferences +preferencesRouter.put( + '/security', + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + validateSecurityPreferences, + securityPrefCtrl.updateSecurityPreferences +); +preferencesRouter.get('/security', securityPrefCtrl.getSecurityPreferences); + +// Appearance preferences preferencesRouter.put( '/appearance', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), - updateAppearancePreferences + appearancePrefCtrl.updateAppearancePreferences ); preferencesRouter.get( '/appearance', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), - getAppearancePreferences + appearancePrefCtrl.getAppearancePreferences ); diff --git a/backend/src/services/preferences/global-preferences.service.ts b/backend/src/services/preferences/global-preferences.service.ts index 3d4117b..efda5b6 100644 --- a/backend/src/services/preferences/global-preferences.service.ts +++ b/backend/src/services/preferences/global-preferences.service.ts @@ -53,6 +53,16 @@ export class GlobalPreferencesService< return await this.ensurePreferencesInitialized(); } + /** + * Saves the global preferences. + * @param {GlobalPreferences} preferences + * @returns {Promise} + */ + async saveGlobalPreferences(preferences: G): Promise { + this.logger.info('Saving global preferences'); + return this.storage.saveGlobalPreferences(preferences) as Promise; + } + async saveOpenViduRoom(ovRoom: R): Promise { this.logger.info(`Saving OpenVidu room ${ovRoom.roomName}`); return this.storage.saveOpenViduRoom(ovRoom) as Promise;