From 644c2951cbbfd3741def398c3d43174aa47ddd5c Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Tue, 22 Oct 2024 13:09:01 +0200 Subject: [PATCH] backend: Added preferences feature - Added preferences API routes - Added room preferences database models - Added preferences controllers - Updated sequelize config --- backend/index.ts | 1 - backend/src/config/sequelize.ts | 33 ++++++-- .../global-preferences.controller.ts | 60 -------------- .../appearance-preferences.controller.ts | 37 +++++++++ .../global-preferences.controller.ts | 42 ++++++++++ .../room-preferences.controller.ts | 62 +++++++++++++++ backend/src/controllers/index.ts | 2 +- .../src/models/global-preferences.model.ts | 19 ----- .../appearance-preference.model.ts | 10 +++ .../room-preference.model.ts | 46 +++++++++++ backend/src/routes/api.routes.ts | 12 ++- .../services/global-preferences.service.ts | 78 +++++++++++++++++++ 12 files changed, 313 insertions(+), 89 deletions(-) delete mode 100644 backend/src/controllers/global-preferences.controller.ts create mode 100644 backend/src/controllers/global-preferences/appearance-preferences.controller.ts create mode 100644 backend/src/controllers/global-preferences/global-preferences.controller.ts create mode 100644 backend/src/controllers/global-preferences/room-preferences.controller.ts delete mode 100644 backend/src/models/global-preferences.model.ts create mode 100644 backend/src/models/global-preferences/appearance-preference.model.ts create mode 100644 backend/src/models/global-preferences/room-preference.model.ts create mode 100644 backend/src/services/global-preferences.service.ts diff --git a/backend/index.ts b/backend/index.ts index c358f813..949ccd0a 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -5,4 +5,3 @@ export * from './src/models/index.js'; export * from './src/helpers/index.js'; export * from './src/server.js'; export * from './src/config/index.js'; -export * from './src/utils/path-utils.js'; diff --git a/backend/src/config/sequelize.ts b/backend/src/config/sequelize.ts index 24e223e6..4d571e42 100644 --- a/backend/src/config/sequelize.ts +++ b/backend/src/config/sequelize.ts @@ -1,11 +1,12 @@ import { Dialect } from 'sequelize'; import { Sequelize, SequelizeOptions } from 'sequelize-typescript'; import { DB_DIALECT, DB_HOST, DB_NAME, DB_PASSWORD, DB_USER } from '../config.js'; -import { GlobalPreferences } from '../models/global-preferences.model.js'; -import { LoggerService } from '../services/logger.service.js'; +import { LoggerService } from '../services/logger.service.js'; +import { RoomPreferencesModel } from '../models/global-preferences/room-preference.model.js'; +import { AppearancePreferencesModel } from '../models/global-preferences/appearance-preference.model.js'; -const models = [GlobalPreferences]; +const models = [RoomPreferencesModel, AppearancePreferencesModel]; const options: SequelizeOptions = { database: DB_NAME, dialect: DB_DIALECT as Dialect, @@ -18,16 +19,36 @@ const options: SequelizeOptions = { const sequelize = new Sequelize(options); sequelize.addModels(models); - const sequelizeSync = async () => { const logger = LoggerService.getInstance(); try { await sequelize.sync(); + await initializeDefaultPreferences(); logger.verbose('Database connected and models synced.'); } catch (error) { - logger.error(`Error connecting to the ${DB_DIALECT} database: ${error}`); + logger.error(`Error initializing the ${DB_DIALECT} database: ${error}`); + console.error(error); } -} +}; + +const initializeDefaultPreferences = async () => { + const logger = LoggerService.getInstance(); + + const [roomPreferences, appearancePreferences] = await Promise.all([ + RoomPreferencesModel.findOne(), + AppearancePreferencesModel.findOne() + ]); + + if (!roomPreferences) { + await RoomPreferencesModel.create(); + } + + if (!appearancePreferences) { + await AppearancePreferencesModel.create(); + } + + logger.verbose('Default preferences initialized.'); +}; export { sequelize, sequelizeSync }; diff --git a/backend/src/controllers/global-preferences.controller.ts b/backend/src/controllers/global-preferences.controller.ts deleted file mode 100644 index e9ae77f9..00000000 --- a/backend/src/controllers/global-preferences.controller.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Request, Response } from 'express'; -import { CALL_PRIVATE_ACCESS } from '../config.js'; -import { LoggerService } from '../services/logger.service.js'; -import { GlobalPreferences, RoomPreferences } from '../models/global-preferences.model.js'; -import { OpenViduCallError } from '../models/error.model.js'; - -const logger = LoggerService.getInstance(); - -export const savePreferences = async (req: Request, res: Response) => { - const preferences: RoomPreferences = req.body; - - if (!preferences) { - return res.status(400).json({ status: 'error', message: 'No preferences provided' }); - } - - logger.verbose(`Saving preferences ${preferences}`); - - try { - const roomPreferences: RoomPreferences = { - recordingEnabled: true, - broadcastingEnabled: true, - chatEnabled: true - }; - const preferences = new GlobalPreferences({ roomPreferences }); - await preferences.save(); - - return res.status(200).json({ message: 'Configuration saved successfully.' }); - } catch (error) { - if (error instanceof OpenViduCallError) { - logger.error(`Error saving configuration: ${error.message}`); - return res.status(error.statusCode).json({ name: error.name, message: error.message }); - } - - logger.error(`Unexpected error saving configuration ${error}`); - return res - .status(500) - .json({ name: 'Global Preferences Error', message: 'Unexpected error saving configuration' }); - } -}; - -export const getGlobalPreferences = async (req: Request, res: Response) => { - logger.verbose('Getting global preferences'); - - try { - const preferences = await GlobalPreferences.findOne(); - return res.status(200).json(preferences); - } catch (error) { - logger.error(`Unexpected error getting global preferences ${error}`); - return res - .status(500) - .json({ name: 'Global Preferences Error', message: 'Unexpected error getting global preferences' }); - } -}; - -// TODO: Remove this endpoint -export const getConfig = async (req: Request, res: Response) => { - logger.verbose('Getting config'); - const response = { isPrivateAccess: CALL_PRIVATE_ACCESS === 'true' }; - return res.status(200).json(response); -}; diff --git a/backend/src/controllers/global-preferences/appearance-preferences.controller.ts b/backend/src/controllers/global-preferences/appearance-preferences.controller.ts new file mode 100644 index 00000000..054a2757 --- /dev/null +++ b/backend/src/controllers/global-preferences/appearance-preferences.controller.ts @@ -0,0 +1,37 @@ +import { Request, Response } from 'express'; +import { AppearancePreferencesModel } from '../../models/global-preferences/appearance-preference.model.js'; + +export const updateAppearancePreferences = async (req: Request, res: Response) => { + const { appearancePreferences } = req.body; + + try { + const preferences = await AppearancePreferencesModel.findOne(); + + if (preferences) { + preferences.theme = appearancePreferences.theme; + await preferences.save(); + return res.status(200).json({ message: 'Appearance preferences updated successfully.' }); + } else { + await AppearancePreferencesModel.create({ appearancePreferences }); + return res.status(201).json({ message: 'Appearance preferences created successfully.' }); + } + } catch (error) { + console.error('Error saving appearance preferences:', error); + return res.status(500).json({ message: 'Error saving appearance preferences', error }); + } +}; + +export const getAppearancePreferences = async (req: Request, res: Response) => { + try { + const preferences = await AppearancePreferencesModel.findOne(); + + if (preferences) { + return res.status(200).json(preferences); + } else { + return res.status(404).json({ message: 'Appearance preferences not found' }); + } + } catch (error) { + console.error('Error fetching appearance preferences:', error); + return res.status(500).json({ message: 'Error fetching appearance preferences', error }); + } +}; diff --git a/backend/src/controllers/global-preferences/global-preferences.controller.ts b/backend/src/controllers/global-preferences/global-preferences.controller.ts new file mode 100644 index 00000000..1bc7fecd --- /dev/null +++ b/backend/src/controllers/global-preferences/global-preferences.controller.ts @@ -0,0 +1,42 @@ +import { Request, Response } from 'express'; +import { CALL_PRIVATE_ACCESS } from '../../config.js'; +import { LoggerService } from '../../services/logger.service.js'; +import { GlobalPreferences } from '@openvidu/call-common-types'; +import { RoomPreferencesModel } from '../../models/global-preferences/room-preference.model.js'; +import { AppearancePreferencesModel } from '../../models/global-preferences/appearance-preference.model.js'; + +const logger = LoggerService.getInstance(); + +export const getGlobalPreferences = async (req: Request, res: Response) => { + logger.verbose('Getting global preferences'); + + try { + const [roomPreferences, appearancePreferences] = await Promise.all([ + RoomPreferencesModel.findOne(), + AppearancePreferencesModel.findOne() + ]); + + const globalPreferences: GlobalPreferences = { + roomPreferences: roomPreferences?.dataValues.roomPreferences, + appearancePreferences: appearancePreferences?.dataValues.appearancePreferences + }; + + if (!globalPreferences) { + return res.status(404).json({ message: 'Global preferences not found' }); + } + + return res.status(200).json(globalPreferences); + } catch (error) { + logger.error(`Unexpected error getting global preferences ${error}`); + return res + .status(500) + .json({ name: 'Global Preferences Error', message: 'Unexpected error getting global preferences' }); + } +}; + +// TODO: Remove this endpoint +export const getConfig = async (req: Request, res: Response) => { + logger.verbose('Getting config'); + const response = { isPrivateAccess: CALL_PRIVATE_ACCESS === 'true' }; + return res.status(200).json(response); +}; diff --git a/backend/src/controllers/global-preferences/room-preferences.controller.ts b/backend/src/controllers/global-preferences/room-preferences.controller.ts new file mode 100644 index 00000000..70ece923 --- /dev/null +++ b/backend/src/controllers/global-preferences/room-preferences.controller.ts @@ -0,0 +1,62 @@ +// src/controllers/roomPreferences.controller.ts +import { Request, Response } from 'express'; +import { RoomPreferencesModel } from '../../models/global-preferences/room-preference.model.js'; +import { LoggerService } from '../../services/logger.service.js'; +import { GlobalPreferencseService } from '../../services/global-preferences.service.js'; +import { OpenViduCallError } from '../../models/error.model.js'; + +const logger = LoggerService.getInstance(); +const preferenceService = GlobalPreferencseService.getInstance(); + +export const updateRoomPreferences = async (req: Request, res: Response) => { + logger.verbose('Updating room preferences:' + JSON.stringify(req.body)); + const roomPreferences = req.body; + + try { + preferenceService.validatePreferences(roomPreferences); + + const savedPreferences = await preferenceService.updateRoomPreferences(roomPreferences); + + return res + .status(200) + .json({ message: 'Room preferences updated successfully.', preferences: savedPreferences }); + + // let preferences = await RoomPreferencesModel.findOne(); + + // if (preferences) { + // await RoomPreferencesModel.update({ chatPreferences }, { where: { id: preferences.id } }); + // return res.status(200).json({ message: 'Room preferences updated successfully.', preferences }); + // } else { + // // Crear preferencias si no existen + // preferences = await RoomPreferencesModel.create({ + // recordingPreferences, + // broadcastingPreferences, + // chatPreferences + // }); + // return res.status(201).json({ message: 'Room preferences created successfully.', preferences }); + // } + } catch (error) { + if (error instanceof OpenViduCallError) { + logger.error(`Error saving room preferences: ${error.message}`); + return res.status(error.statusCode).json({ name: error.name, message: error.message }); + } + + console.error('Error saving room preferences:', error); + return res.status(500).json({ message: 'Error saving room preferences', error }); + } +}; + +export const getRoomPreferences = async (req: Request, res: Response) => { + try { + const preferences = await preferenceService.getRoomPreferences(); + + if (!preferences) { + return res.status(404).json({ message: 'Room preferences not found' }); + } + + return res.status(200).json(preferences); + } catch (error) { + console.error('Error fetching room preferences:', error); + return res.status(500).json({ message: 'Error fetching room preferences', error }); + } +}; diff --git a/backend/src/controllers/index.ts b/backend/src/controllers/index.ts index a3843812..36bc3571 100644 --- a/backend/src/controllers/index.ts +++ b/backend/src/controllers/index.ts @@ -1,6 +1,6 @@ export * from './auth.controller.js'; export * from './broadcasting.controller.js'; -export * from './global-preferences.controller.js'; +export * from './global-preferences/global-preferences.controller.js'; export * from './healthcheck.controller.js'; export * from './recording.controller.js'; export * from './room.controller.js'; diff --git a/backend/src/models/global-preferences.model.ts b/backend/src/models/global-preferences.model.ts deleted file mode 100644 index e9819f75..00000000 --- a/backend/src/models/global-preferences.model.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * This file defines the models for the global preferences of the application. - * - * It is used to store the preferences of the application, such as recording enabled, chat enabled, authentication mode, theme, branding, etc. - */ - -import { Column, Model, Table } from 'sequelize-typescript'; - -export interface RoomPreferences { - recordingEnabled: boolean; - broadcastingEnabled: boolean; - chatEnabled: boolean; -} - -@Table -export class GlobalPreferences extends Model { - @Column({ type: 'json' }) - roomPreferences!: RoomPreferences; -} diff --git a/backend/src/models/global-preferences/appearance-preference.model.ts b/backend/src/models/global-preferences/appearance-preference.model.ts new file mode 100644 index 00000000..8f4ff652 --- /dev/null +++ b/backend/src/models/global-preferences/appearance-preference.model.ts @@ -0,0 +1,10 @@ +import { Column, DataType, Model, Table } from 'sequelize-typescript'; + +@Table({ tableName: 'appearance_preferences' }) +export class AppearancePreferencesModel extends Model { + @Column({ + type: DataType.STRING, + defaultValue: 'default' + }) + theme!: string; +} diff --git a/backend/src/models/global-preferences/room-preference.model.ts b/backend/src/models/global-preferences/room-preference.model.ts new file mode 100644 index 00000000..71f8c114 --- /dev/null +++ b/backend/src/models/global-preferences/room-preference.model.ts @@ -0,0 +1,46 @@ +import { Column, DataType, Model, Table } from 'sequelize-typescript'; +import { + BroadcastingPreferences, + ChatPreferences, + RecordingPreferences, + VirtualBackgroundPreferences +} from '@openvidu/call-common-types'; + +@Table({ tableName: 'room_preferences' }) +export class RoomPreferencesModel extends Model { + @Column({ + type: DataType.JSON, + allowNull: false, + defaultValue: { + enabled: false + } + }) + recordingPreferences!: RecordingPreferences; + + @Column({ + type: DataType.JSON, + allowNull: false, + defaultValue: { + enabled: false + } + }) + broadcastingPreferences!: BroadcastingPreferences; + + @Column({ + type: DataType.JSON, + allowNull: false, + defaultValue: { + enabled: false + } + }) + chatPreferences!: ChatPreferences; + + @Column({ + type: DataType.JSON, + allowNull: false, + defaultValue: { + enabled: false + } + }) + virtualBackgroundPreferences!: VirtualBackgroundPreferences; +} diff --git a/backend/src/routes/api.routes.ts b/backend/src/routes/api.routes.ts index 791fce94..2fca7a12 100644 --- a/backend/src/routes/api.routes.ts +++ b/backend/src/routes/api.routes.ts @@ -4,9 +4,11 @@ import * as roomCtrl from '../controllers/room.controller.js'; import * as recordingCtrl from '../controllers/recording.controller.js'; import * as broadcastCtrl from '../controllers/broadcasting.controller.js'; import * as authCtrl from '../controllers/auth.controller.js'; -import { getConfig, getGlobalPreferences, savePreferences } from '../controllers/global-preferences.controller.js'; +import { getConfig, getGlobalPreferences } from '../controllers/global-preferences/global-preferences.controller.js'; import { healthCheck } from '../controllers/healthcheck.controller.js'; import { withAdminAndUserBasicAuth, withAdminBasicAuth, withUserBasicAuth } from '../services/auth.service.js'; +import { getRoomPreferences, updateRoomPreferences } from '../controllers/global-preferences/room-preferences.controller.js'; +import { getAppearancePreferences, updateAppearancePreferences } from '../controllers/global-preferences/appearance-preferences.controller.js'; const apiRouter = Router(); @@ -36,8 +38,14 @@ apiRouter.post('/admin/login', authCtrl.adminLogin); apiRouter.post('/admin/logout', authCtrl.adminLogout); // Global Preferences Routes -apiRouter.post('/preferences', /*withAdminBasicAuth,*/ savePreferences); apiRouter.get('/preferences', /*withAdminBasicAuth,*/ getGlobalPreferences); + +apiRouter.put('/preferences/room', /*withAdminBasicAuth,*/ updateRoomPreferences); +apiRouter.get('/preferences/room', /*withAdminBasicAuth,*/ getRoomPreferences); + +apiRouter.put('/preferences/appearance', /*withAdminAndUserBasicAuth*/ updateAppearancePreferences); +apiRouter.get('/preferences/appearance', /*withAdminAndUserBasicAuth*/ getAppearancePreferences); + apiRouter.get('/config', getConfig); // TODO: remove this route // Health Check Route diff --git a/backend/src/services/global-preferences.service.ts b/backend/src/services/global-preferences.service.ts new file mode 100644 index 00000000..38d26861 --- /dev/null +++ b/backend/src/services/global-preferences.service.ts @@ -0,0 +1,78 @@ +import { RoomPreferences } from '@openvidu/call-common-types'; +import { LoggerService } from './logger.service.js'; +import { RoomPreferencesModel } from '../models/global-preferences/room-preference.model.js'; + +export class GlobalPreferencseService { + private logger = LoggerService.getInstance(); + protected static instance: GlobalPreferencseService; + + private constructor() {} + + static getInstance() { + if (!GlobalPreferencseService.instance) { + GlobalPreferencseService.instance = new GlobalPreferencseService(); + } + + return GlobalPreferencseService.instance; + } + + async createRoomPreferences(roomPreferences: RoomPreferences) { + return await RoomPreferencesModel.create({ roomPreferences }); + } + + async getRoomPreferences() { + return await RoomPreferencesModel.findOne(); + } + + async updateRoomPreferences(roomPreferences: RoomPreferences) { + try { + const preferences = await this.getRoomPreferences(); + + if (preferences) { + await RoomPreferencesModel.update(roomPreferences, { where: { id: preferences.id } }); + return roomPreferences; + } else { + return await this.createRoomPreferences(roomPreferences); + } + } catch (error) { + //TODO: Implement error handling + throw error; + } + } + + async deleteRoomPreferences() { + const preferences = await this.getRoomPreferences(); + + if (preferences) { + await preferences.destroy(); + return { message: 'Room preferences deleted successfully.' }; + } else { + throw new Error('No preferences found to delete.'); + } + } + + validatePreferences(preferences: RoomPreferences) { + const { recordingPreferences, broadcastingPreferences, chatPreferences, virtualBackgroundPreferences } = + preferences; + + if (!recordingPreferences || !broadcastingPreferences || !chatPreferences || !virtualBackgroundPreferences) { + throw new Error('All room preferences must be provided'); + } + + if (typeof preferences.recordingPreferences.enabled !== 'boolean') { + throw new Error('Invalid value for recordingPreferences.enabled'); + } + + if (typeof preferences.broadcastingPreferences.enabled !== 'boolean') { + throw new Error('Invalid value for broadcastingPreferences.enabled'); + } + + if (typeof preferences.chatPreferences.enabled !== 'boolean') { + throw new Error('Invalid value for chatPreferences.enabled'); + } + + if (typeof preferences.virtualBackgroundPreferences.enabled !== 'boolean') { + throw new Error('Invalid value for virtualBackgroundPreferences.enabled'); + } + } +}