backend: Add security and webhook preferences controllers and validation middleware

This commit is contained in:
juancarmore 2025-03-28 18:55:39 +01:00
parent 166389b606
commit 2d82d6a96d
6 changed files with 257 additions and 8 deletions

View File

@ -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' });
}
};

View File

@ -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' });
}
};

View File

@ -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';
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';

View File

@ -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<WebhookPreferences> = z.object({
enabled: z.boolean(),
url: z.string().url()
});
const AuthModeSchema: z.ZodType<AuthMode> = z.enum([AuthMode.NONE, AuthMode.MODERATORS_ONLY, AuthMode.ALL_USERS]);
const AuthTypeSchema: z.ZodType<AuthType> = z.enum([AuthType.SINGLE_USER]);
const SingleUserAuthDTOSchema: z.ZodType<SingleUserAuthDTO> = z.object({
type: AuthTypeSchema
});
const ValidAuthMethodDTOSchema: z.ZodType<ValidAuthMethodDTO> = SingleUserAuthDTOSchema;
const AuthenticationPreferencesDTOSchema: z.ZodType<AuthenticationPreferencesDTO> = z.object({
authMode: AuthModeSchema,
method: ValidAuthMethodDTOSchema
});
const RoomCreationPolicySchema: z.ZodType<RoomCreationPolicy> = z.object({
allowRoomCreation: z.boolean(),
requireAuthentication: z.boolean()
});
const UpdateSecurityPreferencesDTOSchema: z.ZodType<UpdateSecurityPreferencesDTO> = 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
});
};

View File

@ -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
);

View File

@ -53,6 +53,16 @@ export class GlobalPreferencesService<
return await this.ensurePreferencesInitialized();
}
/**
* Saves the global preferences.
* @param {GlobalPreferences} preferences
* @returns {Promise<GlobalPreferences>}
*/
async saveGlobalPreferences(preferences: G): Promise<G> {
this.logger.info('Saving global preferences');
return this.storage.saveGlobalPreferences(preferences) as Promise<G>;
}
async saveOpenViduRoom(ovRoom: R): Promise<R> {
this.logger.info(`Saving OpenVidu room ${ovRoom.roomName}`);
return this.storage.saveOpenViduRoom(ovRoom) as Promise<R>;