From 5cdf4370bf181fb7bcbf631c088c1d4507b502fa Mon Sep 17 00:00:00 2001 From: juancarmore Date: Fri, 19 Sep 2025 11:33:50 +0200 Subject: [PATCH] backend: implement rooms appearance configuration endpoints and validation --- .../appearance-config.controller.ts | 50 ++++++++++++++++--- .../config-validator.middleware.ts | 16 ++++++ .../room-validator.middleware.ts | 19 +++++++ backend/src/models/error.model.ts | 32 +++++++----- backend/src/routes/global-config.routes.ts | 10 ++-- 5 files changed, 104 insertions(+), 23 deletions(-) diff --git a/backend/src/controllers/global-config/appearance-config.controller.ts b/backend/src/controllers/global-config/appearance-config.controller.ts index 1993d2e..8766988 100644 --- a/backend/src/controllers/global-config/appearance-config.controller.ts +++ b/backend/src/controllers/global-config/appearance-config.controller.ts @@ -1,12 +1,48 @@ +import { MeetAppearanceConfig } from '@typings-ce'; import { Request, Response } from 'express'; -import { errorProFeature, rejectRequestFromMeetError } from '../../models/error.model.js'; +import { container } from '../../config/index.js'; +import { + errorRoomsAppearanceConfigNotDefined, + handleError, + rejectRequestFromMeetError +} from '../../models/error.model.js'; +import { LoggerService, MeetStorageService } from '../../services/index.js'; -export const updateAppearanceConfig = async (_req: Request, res: Response) => { - const error = errorProFeature('update appearance config'); - rejectRequestFromMeetError(res, error); +export const updateRoomsAppearanceConfig = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const storageService = container.get(MeetStorageService); + + logger.verbose(`Updating rooms appearance config: ${JSON.stringify(req.body)}`); + const appearanceConfig = req.body as { appearance: MeetAppearanceConfig }; + + try { + const globalConfig = await storageService.getGlobalConfig(); + globalConfig.roomsConfig = appearanceConfig; + await storageService.saveGlobalConfig(globalConfig); + + return res.status(200).json({ message: 'Rooms appearance config updated successfully' }); + } catch (error) { + handleError(res, error, 'updating rooms appearance config'); + } }; -export const getAppearanceConfig = async (_req: Request, res: Response) => { - const error = errorProFeature('get appearance config'); - rejectRequestFromMeetError(res, error); +export const getRoomsAppearanceConfig = async (_req: Request, res: Response) => { + const logger = container.get(LoggerService); + const storageService = container.get(MeetStorageService); + + logger.verbose(`Getting rooms appearance config`); + + try { + const globalConfig = await storageService.getGlobalConfig(); + const appearanceConfig = globalConfig.roomsConfig?.appearance; + + if (!appearanceConfig) { + const error = errorRoomsAppearanceConfigNotDefined(); + return rejectRequestFromMeetError(res, error); + } + + return res.status(200).json({ appearance: appearanceConfig }); + } catch (error) { + handleError(res, error, 'getting rooms appearance config'); + } }; diff --git a/backend/src/middlewares/request-validators/config-validator.middleware.ts b/backend/src/middlewares/request-validators/config-validator.middleware.ts index 43d7a9b..627cce9 100644 --- a/backend/src/middlewares/request-validators/config-validator.middleware.ts +++ b/backend/src/middlewares/request-validators/config-validator.middleware.ts @@ -10,6 +10,7 @@ import { import { NextFunction, Request, Response } from 'express'; import { z } from 'zod'; import { rejectUnprocessableRequest } from '../../models/error.model.js'; +import { AppearanceConfigSchema } from './room-validator.middleware.js'; const WebhookConfigSchema: z.ZodType = z .object({ @@ -57,6 +58,10 @@ const SecurityConfigSchema: z.ZodType = z.object({ authentication: AuthenticationConfigSchema }); +const RoomsAppearanceConfigSchema = z.object({ + appearance: AppearanceConfigSchema +}); + export const validateWebhookConfig = (req: Request, res: Response, next: NextFunction) => { const { success, error, data } = WebhookConfigSchema.safeParse(req.body); @@ -89,3 +94,14 @@ export const validateSecurityConfig = (req: Request, res: Response, next: NextFu req.body = data; next(); }; + +export const validateRoomsAppearanceConfig = (req: Request, res: Response, next: NextFunction) => { + const { success, error, data } = RoomsAppearanceConfigSchema.safeParse(req.body); + + if (!success) { + return rejectUnprocessableRequest(res, error); + } + + req.body = data; + next(); +}; diff --git a/backend/src/middlewares/request-validators/room-validator.middleware.ts b/backend/src/middlewares/request-validators/room-validator.middleware.ts index 1d42409..9036dd8 100644 --- a/backend/src/middlewares/request-validators/room-validator.middleware.ts +++ b/backend/src/middlewares/request-validators/room-validator.middleware.ts @@ -9,6 +9,7 @@ import { MeetRoomFilters, MeetRoomOptions, MeetRoomStatus, + MeetRoomThemeMode, MeetVirtualBackgroundConfig, ParticipantRole, RecordingPermissions @@ -89,10 +90,28 @@ const VirtualBackgroundConfigSchema: z.ZodType = z. enabled: z.boolean() }); +const ThemeModeSchema: z.ZodType = z.enum([MeetRoomThemeMode.LIGHT, MeetRoomThemeMode.DARK]); + +const hexColorSchema = z.string().regex(/^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$/, 'Must be a valid hex color code'); + +const RoomThemeSchema = z.object({ + name: z.string().min(1, 'Theme name cannot be empty').max(50, 'Theme name cannot exceed 50 characters'), + baseTheme: ThemeModeSchema, + backgroundColor: hexColorSchema.optional(), + primaryColor: hexColorSchema.optional(), + secondaryColor: hexColorSchema.optional(), + surfaceColor: hexColorSchema.optional() +}); + +export const AppearanceConfigSchema = z.object({ + themes: z.array(RoomThemeSchema).length(1, 'There must be exactly one theme defined') +}); + const RoomConfigSchema: z.ZodType = z.object({ recording: RecordingConfigSchema, chat: ChatConfigSchema, virtualBackground: VirtualBackgroundConfigSchema + // appearance: AppearanceConfigSchema, }); const RoomDeletionPolicyWithMeetingSchema: z.ZodType = z.enum([ diff --git a/backend/src/models/error.model.ts b/backend/src/models/error.model.ts index 3652c91..7f5c868 100644 --- a/backend/src/models/error.model.ts +++ b/backend/src/models/error.model.ts @@ -63,18 +63,6 @@ export const errorAzureNotAvailable = (error: any): OpenViduMeetError => { return new OpenViduMeetError('ABS Error', `Azure Blob Storage is not available ${error}`, 503); }; -export const errorInvalidWebhookUrl = (url: string, reason: string): OpenViduMeetError => { - return new OpenViduMeetError('Webhook Error', `Webhook URL '${url}' is invalid: ${reason}`, 400); -}; - -export const errorApiKeyNotConfiguredForWebhooks = (): OpenViduMeetError => { - return new OpenViduMeetError( - 'Webhook Error', - 'There are no API keys configured yet. Please, create one to use webhooks.', - 400 - ); -}; - // Auth errors export const errorInvalidCredentials = (): OpenViduMeetError => { @@ -259,6 +247,26 @@ export const errorParticipantIdentityNotProvided = (): OpenViduMeetError => { return new OpenViduMeetError('Participant Error', 'No participant identity provided', 400); }; +// Global config errors + +export const errorRoomsAppearanceConfigNotDefined = (): OpenViduMeetError => { + return new OpenViduMeetError('Global Config Error', 'Rooms appearance config not defined', 404); +}; + +// Webhook errors + +export const errorInvalidWebhookUrl = (url: string, reason: string): OpenViduMeetError => { + return new OpenViduMeetError('Webhook Error', `Webhook URL '${url}' is invalid: ${reason}`, 400); +}; + +export const errorApiKeyNotConfiguredForWebhooks = (): OpenViduMeetError => { + return new OpenViduMeetError( + 'Webhook Error', + 'There are no API keys configured yet. Please, create one to use webhooks.', + 400 + ); +}; + // Handlers export const handleError = (res: Response, error: OpenViduMeetError | unknown, operationDescription: string) => { diff --git a/backend/src/routes/global-config.routes.ts b/backend/src/routes/global-config.routes.ts index 5ac9947..d289a5c 100644 --- a/backend/src/routes/global-config.routes.ts +++ b/backend/src/routes/global-config.routes.ts @@ -6,6 +6,7 @@ import * as securityConfigCtrl from '../controllers/global-config/security-confi import * as webhookConfigCtrl from '../controllers/global-config/webhook-config.controller.js'; import { tokenAndRoleValidator, + validateRoomsAppearanceConfig, validateSecurityConfig, validateWebhookConfig, withAuth, @@ -37,12 +38,13 @@ configRouter.get('/security', securityConfigCtrl.getSecurityConfig); // Appearance config configRouter.put( - '/appearance', + '/rooms/appearance', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), - appearanceConfigCtrl.updateAppearanceConfig + validateRoomsAppearanceConfig, + appearanceConfigCtrl.updateRoomsAppearanceConfig ); configRouter.get( - '/appearance', + '/rooms/appearance', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), - appearanceConfigCtrl.getAppearanceConfig + appearanceConfigCtrl.getRoomsAppearanceConfig );