diff --git a/apps/client/src/common/hooks-query/useSettings.ts b/apps/client/src/common/hooks-query/useSettings.ts index 5477eb9cf..3d1aa985f 100644 --- a/apps/client/src/common/hooks-query/useSettings.ts +++ b/apps/client/src/common/hooks-query/useSettings.ts @@ -1,7 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { unobfuscate } from 'ontime-utils'; -import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { APP_SETTINGS } from '../api/constants'; import { getSettings } from '../api/settings'; import { ontimePlaceholderSettings } from '../models/OntimeSettings'; @@ -11,10 +10,6 @@ export default function useSettings() { queryKey: APP_SETTINGS, queryFn: getSettings, placeholderData: (previousData, _previousQuery) => previousData, - retry: 5, - retryDelay: (attempt) => attempt * 2500, - refetchInterval: queryRefetchIntervalSlow, - networkMode: 'always', select: (data) => { const unobfuscated = { ...data }; if (data.editorKey) { diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index 9c05be079..df8fc5b67 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -11,6 +11,7 @@ import { import { isProduction, websocketUrl } from '../../externals'; import { + APP_SETTINGS, CLIENT_LIST, CUSTOM_FIELDS, PROJECT_DATA, @@ -177,6 +178,9 @@ export const connectSocket = () => { case RefetchKey.Translation: ontimeQueryClient.invalidateQueries({ queryKey: TRANSLATION }); break; + case RefetchKey.Settings: + ontimeQueryClient.invalidateQueries({ queryKey: APP_SETTINGS }); + break; default: { target satisfies never; break; diff --git a/apps/server/src/api-data/settings/settings.controller.ts b/apps/server/src/api-data/settings/settings.controller.ts deleted file mode 100644 index 384aaa00a..000000000 --- a/apps/server/src/api-data/settings/settings.controller.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ErrorResponse, Settings } from 'ontime-types'; -import { getErrorMessage, obfuscate } from 'ontime-utils'; - -import type { Request, Response } from 'express'; - -import { isDocker } from '../../setup/environment.js'; -import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; -import * as appState from '../../services/app-state-service/AppStateService.js'; - -import { extractPin } from './settings.utils.js'; - -export async function getSettings(_req: Request, res: Response) { - const settings = getDataProvider().getSettings(); - const obfuscatedSettings = { ...settings }; - if (settings.editorKey) { - obfuscatedSettings.editorKey = obfuscate(settings.editorKey); - } - - if (settings.operatorKey) { - obfuscatedSettings.operatorKey = obfuscate(settings.operatorKey); - } - - res.status(200).send(obfuscatedSettings); -} - -export async function postSettings(req: Request, res: Response) { - try { - const settings = getDataProvider().getSettings(); - const editorKey = extractPin(req.body?.editorKey, settings.editorKey); - const operatorKey = extractPin(req.body?.operatorKey, settings.operatorKey); - const serverPort = Number(req.body?.serverPort); - //TODO: should this not be part of the validator? - if (isNaN(serverPort)) { - res.status(400).send({ message: `Invalid value found for server port: ${req.body?.serverPort}` }); - return; - } - - const hasChangedPort = settings.serverPort !== serverPort; - - if (isDocker && hasChangedPort) { - res.status(403).json({ message: 'Can`t change port when running inside docker' }); - return; - } - - let timeFormat = settings.timeFormat; - if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') { - timeFormat = req.body.timeFormat; - } - - const language = req.body?.language || 'en'; - - const newData = { - ...settings, - editorKey, - operatorKey, - timeFormat, - language, - serverPort, - }; - await getDataProvider().setSettings(newData); - res.status(200).send(newData); - } catch (error) { - const message = getErrorMessage(error); - res.status(400).send({ message }); - } -} - -export async function postWelcomeDialog(req: Request, res: Response) { - const show = await appState.setShowWelcomeDialog(req.body.show); - res.status(200).send({ show }); -} diff --git a/apps/server/src/api-data/settings/settings.router.ts b/apps/server/src/api-data/settings/settings.router.ts index a96af3a91..df3eb9ade 100644 --- a/apps/server/src/api-data/settings/settings.router.ts +++ b/apps/server/src/api-data/settings/settings.router.ts @@ -1,10 +1,58 @@ import express from 'express'; -import { getSettings, postSettings, postWelcomeDialog } from './settings.controller.js'; +import { matchedData } from 'express-validator'; +import type { Request, Response } from 'express'; +import { deepEqual } from 'fast-equals'; + +import { ErrorResponse, RefetchKey, Settings } from 'ontime-types'; +import { getErrorMessage, obfuscate } from 'ontime-utils'; + import { validateSettings, validateWelcomeDialog } from './settings.validation.js'; +import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; +import * as appState from '../../services/app-state-service/AppStateService.js'; +import { isDocker } from '../../setup/environment.js'; +import { sendRefetch } from '../../adapters/WebsocketAdapter.js'; export const router = express.Router(); -router.post('/welcomedialog', validateWelcomeDialog, postWelcomeDialog); +router.post('/welcomedialog', validateWelcomeDialog, async (req: Request, res: Response) => { + const show = await appState.setShowWelcomeDialog(req.body.show); + res.status(200).json({ show }); +}); -router.get('/', getSettings); -router.post('/', validateSettings, postSettings); +router.get('/', (_req: Request, res: Response) => { + const settings = getDataProvider().getSettings(); + const obfuscatedSettings = { ...settings }; + if (settings.editorKey) { + obfuscatedSettings.editorKey = obfuscate(settings.editorKey); + } + + if (settings.operatorKey) { + obfuscatedSettings.operatorKey = obfuscate(settings.operatorKey); + } + + res.status(200).json(obfuscatedSettings); +}); + +router.post('/', validateSettings, async (req: Request, res: Response) => { + try { + const data = matchedData(req); + const settings = getDataProvider().getSettings(); + + if (isDocker && settings.serverPort !== data.serverPort) { + res.status(403).json({ message: 'Can`t change port when running inside docker' }); + return; + } + + data.version = settings.version; + + if (!deepEqual(data, settings)) { + await getDataProvider().setSettings(data); + sendRefetch(RefetchKey.Settings); + } + + res.status(200).json(data); + } catch (error) { + const message = getErrorMessage(error); + res.status(400).json({ message }); + } +}); diff --git a/apps/server/src/api-data/settings/settings.utils.ts b/apps/server/src/api-data/settings/settings.utils.ts deleted file mode 100644 index 614a34aa9..000000000 --- a/apps/server/src/api-data/settings/settings.utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Business logic for resolving a string - */ -export function extractPin(value: string | undefined | null, fallback: string | null): string | null { - if (value === null) { - return value; - } - if (typeof value === 'undefined') { - return fallback; - } - if (value.length === 0) { - return null; - } - return value; -} diff --git a/apps/server/src/api-data/settings/settings.validation.ts b/apps/server/src/api-data/settings/settings.validation.ts index 40ab67cb1..1ac5d1178 100644 --- a/apps/server/src/api-data/settings/settings.validation.ts +++ b/apps/server/src/api-data/settings/settings.validation.ts @@ -1,4 +1,4 @@ -import { body } from 'express-validator'; +import { body, checkExact } from 'express-validator'; import { requestValidationFunction } from '../validation-utils/validationFunction.js'; /** @@ -6,16 +6,30 @@ import { requestValidationFunction } from '../validation-utils/validationFunctio */ export const validateWelcomeDialog = [body('show').isBoolean(), requestValidationFunction]; +const pinValidator = (key: string) => { + return body(key) + .isString() + .trim() + .isLength({ min: 0, max: 4 }) + .customSanitizer((input) => { + if (input.length === 0) { + return null; + } + return input; + }); +}; + /** * @description Validates object for POST /ontime/settings */ export const validateSettings = [ - body().notEmpty().withMessage('No object found in request'), - body('editorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }), - body('operatorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }), - body('timeFormat').isString().isIn(['12', '24']), - body('language').isString(), - body('serverPort').isPort().optional(), + pinValidator('editorKey'), + pinValidator('operatorKey'), + body('timeFormat').isString().isIn(['12', '24']).withMessage('Time format can only be "12" or "24"'), + body('language').isString().trim().notEmpty(), + body('serverPort').isPort().withMessage('Invalid value found for server port').toInt(), + checkExact(), requestValidationFunction, ]; +// TODO: dont allow other keys diff --git a/packages/types/src/api/websocket/refetch.type.ts b/packages/types/src/api/websocket/refetch.type.ts index e891ffcd2..fdaac6b75 100644 --- a/packages/types/src/api/websocket/refetch.type.ts +++ b/packages/types/src/api/websocket/refetch.type.ts @@ -7,4 +7,5 @@ export enum RefetchKey { UrlPresets = 'url-presets', ViewSettings = 'view-settings', Translation = 'translation', + Settings = 'settings', }