backend: implement rooms appearance configuration endpoints and validation

This commit is contained in:
juancarmore 2025-09-19 11:33:50 +02:00
parent bb5a86da2c
commit 5cdf4370bf
5 changed files with 104 additions and 23 deletions

View File

@ -1,12 +1,48 @@
import { MeetAppearanceConfig } from '@typings-ce';
import { Request, Response } from 'express'; 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) => { export const updateRoomsAppearanceConfig = async (req: Request, res: Response) => {
const error = errorProFeature('update appearance config'); const logger = container.get(LoggerService);
rejectRequestFromMeetError(res, error); 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) => { export const getRoomsAppearanceConfig = async (_req: Request, res: Response) => {
const error = errorProFeature('get appearance config'); const logger = container.get(LoggerService);
rejectRequestFromMeetError(res, error); 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');
}
}; };

View File

@ -10,6 +10,7 @@ import {
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import { z } from 'zod'; import { z } from 'zod';
import { rejectUnprocessableRequest } from '../../models/error.model.js'; import { rejectUnprocessableRequest } from '../../models/error.model.js';
import { AppearanceConfigSchema } from './room-validator.middleware.js';
const WebhookConfigSchema: z.ZodType<WebhookConfig> = z const WebhookConfigSchema: z.ZodType<WebhookConfig> = z
.object({ .object({
@ -57,6 +58,10 @@ const SecurityConfigSchema: z.ZodType<SecurityConfig> = z.object({
authentication: AuthenticationConfigSchema authentication: AuthenticationConfigSchema
}); });
const RoomsAppearanceConfigSchema = z.object({
appearance: AppearanceConfigSchema
});
export const validateWebhookConfig = (req: Request, res: Response, next: NextFunction) => { export const validateWebhookConfig = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = WebhookConfigSchema.safeParse(req.body); const { success, error, data } = WebhookConfigSchema.safeParse(req.body);
@ -89,3 +94,14 @@ export const validateSecurityConfig = (req: Request, res: Response, next: NextFu
req.body = data; req.body = data;
next(); 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();
};

View File

@ -9,6 +9,7 @@ import {
MeetRoomFilters, MeetRoomFilters,
MeetRoomOptions, MeetRoomOptions,
MeetRoomStatus, MeetRoomStatus,
MeetRoomThemeMode,
MeetVirtualBackgroundConfig, MeetVirtualBackgroundConfig,
ParticipantRole, ParticipantRole,
RecordingPermissions RecordingPermissions
@ -89,10 +90,28 @@ const VirtualBackgroundConfigSchema: z.ZodType<MeetVirtualBackgroundConfig> = z.
enabled: z.boolean() enabled: z.boolean()
}); });
const ThemeModeSchema: z.ZodType<MeetRoomThemeMode> = 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<MeetRoomConfig> = z.object({ const RoomConfigSchema: z.ZodType<MeetRoomConfig> = z.object({
recording: RecordingConfigSchema, recording: RecordingConfigSchema,
chat: ChatConfigSchema, chat: ChatConfigSchema,
virtualBackground: VirtualBackgroundConfigSchema virtualBackground: VirtualBackgroundConfigSchema
// appearance: AppearanceConfigSchema,
}); });
const RoomDeletionPolicyWithMeetingSchema: z.ZodType<MeetRoomDeletionPolicyWithMeeting> = z.enum([ const RoomDeletionPolicyWithMeetingSchema: z.ZodType<MeetRoomDeletionPolicyWithMeeting> = z.enum([

View File

@ -63,18 +63,6 @@ export const errorAzureNotAvailable = (error: any): OpenViduMeetError => {
return new OpenViduMeetError('ABS Error', `Azure Blob Storage is not available ${error}`, 503); 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 // Auth errors
export const errorInvalidCredentials = (): OpenViduMeetError => { export const errorInvalidCredentials = (): OpenViduMeetError => {
@ -259,6 +247,26 @@ export const errorParticipantIdentityNotProvided = (): OpenViduMeetError => {
return new OpenViduMeetError('Participant Error', 'No participant identity provided', 400); 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 // Handlers
export const handleError = (res: Response, error: OpenViduMeetError | unknown, operationDescription: string) => { export const handleError = (res: Response, error: OpenViduMeetError | unknown, operationDescription: string) => {

View File

@ -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 * as webhookConfigCtrl from '../controllers/global-config/webhook-config.controller.js';
import { import {
tokenAndRoleValidator, tokenAndRoleValidator,
validateRoomsAppearanceConfig,
validateSecurityConfig, validateSecurityConfig,
validateWebhookConfig, validateWebhookConfig,
withAuth, withAuth,
@ -37,12 +38,13 @@ configRouter.get('/security', securityConfigCtrl.getSecurityConfig);
// Appearance config // Appearance config
configRouter.put( configRouter.put(
'/appearance', '/rooms/appearance',
withAuth(tokenAndRoleValidator(UserRole.ADMIN)), withAuth(tokenAndRoleValidator(UserRole.ADMIN)),
appearanceConfigCtrl.updateAppearanceConfig validateRoomsAppearanceConfig,
appearanceConfigCtrl.updateRoomsAppearanceConfig
); );
configRouter.get( configRouter.get(
'/appearance', '/rooms/appearance',
withAuth(tokenAndRoleValidator(UserRole.ADMIN)), withAuth(tokenAndRoleValidator(UserRole.ADMIN)),
appearanceConfigCtrl.getAppearanceConfig appearanceConfigCtrl.getRoomsAppearanceConfig
); );