From 17058c375c33cde5d845b10d5f756a1a9259e9e1 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 24 Jun 2019 00:00:00 +0000 Subject: [PATCH] Add new status message system --- Readme.md | 3 -- package-lock.json | 6 +++ package.json | 2 + src/api/admin.ts | 36 +++++++++++++++- src/api/index.ts | 2 +- src/database/index.ts | 3 ++ src/function/statusmessage/index.ts | 48 +++++++++++++++++++++ src/function/sync/get-server-data-status.ts | 3 +- src/websocket/index.ts | 22 ++++++++++ 9 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 src/function/statusmessage/index.ts diff --git a/Readme.md b/Readme.md index ecaa968..aa90688 100644 --- a/Readme.md +++ b/Readme.md @@ -57,9 +57,6 @@ This fixes the causes of lint warnings (where possible). - MAIL_IMPRINT - a string which is added to the footer of the sent mails - default value: ``not defined`` -- STATUS_MESSAGE - - a message which is shown to all users in the overview screen - - default: null/ no shown message - ADMIN_TOKEN - a password which allows to use some APIs - admin APIs are disabled when this is not set diff --git a/package-lock.json b/package-lock.json index bff20b9..87ece3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,6 +109,12 @@ "integrity": "sha512-4CRCtDogTManXlOcj9ixIcxoHTKcaxpqaJWuMN/Zw9tYL1nqVkI64IWcx+CQ9XT/JnpP12buionHQqgrHcH34Q==", "dev": true }, + "@types/escape-html": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-0.0.20.tgz", + "integrity": "sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw==", + "dev": true + }, "@types/events": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", diff --git a/package.json b/package.json index deea74f..f29f9ad 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@types/basic-auth": "^1.1.2", "@types/body-parser": "^1.17.0", "@types/email-templates": "^3.5.0", + "@types/escape-html": "0.0.20", "@types/express": "^4.16.0", "@types/http-errors": "^1.6.1", "@types/lodash": "^4.14.116", @@ -46,6 +47,7 @@ "body-parser": "^1.18.3", "ejs": "^2.6.1", "email-templates": "^5.0.4", + "escape-html": "^1.0.3", "express": "^4.16.3", "google-auth-library": "^4.2.2", "http-errors": "^1.7.0", diff --git a/src/api/admin.ts b/src/api/admin.ts index 866f992..bcc122d 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -16,9 +16,15 @@ */ import { Router } from 'express' +import { Database } from '../database' import { WebsocketApi } from '../websocket' +import { setStatusMessage, getStatusMessage } from '../function/statusmessage' +import * as escape from 'escape-html' +import { json } from 'body-parser' +import { BadRequest } from 'http-errors' -export const createAdminRouter = ({ websocket }: { +export const createAdminRouter = ({ database, websocket }: { + database: Database websocket: WebsocketApi }) => { const router = Router() @@ -29,5 +35,33 @@ export const createAdminRouter = ({ websocket }: { }) }) + router.get('/status-message', async (_, res, next) => { + try { + const currentStatusMessage = await getStatusMessage({ database }) + + res.send('
') + } catch (ex) { + next(ex) + } + }) + + router.post('/status-message', json(), async (req, res, next) => { + try { + if (typeof req.body !== 'object' || typeof req.body.smessage !== 'string') { + throw new BadRequest() + } + + const newStatusMessage = req.body.smessage as string + + await setStatusMessage({ database, newStatusMessage }) + + websocket.triggerImportantSyncAtAllDevicesInBackground() + + res.json({ok: true}) + } catch (ex) { + next(ex) + } + }) + return router } diff --git a/src/api/index.ts b/src/api/index.ts index 7c3fdd2..a934325 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -62,7 +62,7 @@ export const createApi = ({ database, websocket, connectedDevicesManager }: { res.sendStatus(401) } }, - createAdminRouter({ websocket }) + createAdminRouter({ database, websocket }) ) return app diff --git a/src/database/index.ts b/src/database/index.ts index 5539239..6ff6863 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -22,6 +22,7 @@ import { AppActivityModel, createAppActivityModel } from './appactivity' import { AuthTokenModel, createAuthtokenModel } from './authtoken' import { CategoryModel, createCategoryModel } from './category' import { CategoryAppModel, createCategoryAppModel } from './categoryapp' +import { ConfigModel, createConfigModel } from './config' import { createDeviceModel, DeviceModel } from './device' import { createFamilyModel, FamilyModel } from './family' import { createMailLoginTokenModel, MailLoginTokenModel } from './maillogintoken' @@ -39,6 +40,7 @@ export interface Database { appActivity: AppActivityModel category: CategoryModel categoryApp: CategoryAppModel + config: ConfigModel device: DeviceModel family: FamilyModel mailLoginToken: MailLoginTokenModel @@ -57,6 +59,7 @@ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({ appActivity: createAppActivityModel(sequelize), category: createCategoryModel(sequelize), categoryApp: createCategoryAppModel(sequelize), + config: createConfigModel(sequelize), device: createDeviceModel(sequelize), family: createFamilyModel(sequelize), mailLoginToken: createMailLoginTokenModel(sequelize), diff --git a/src/function/statusmessage/index.ts b/src/function/statusmessage/index.ts new file mode 100644 index 0000000..0bc6eda --- /dev/null +++ b/src/function/statusmessage/index.ts @@ -0,0 +1,48 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Database } from '../../database' +import { configItemIds } from '../../database/config' +import * as Sequelize from 'sequelize' + +export const getStatusMessage = async ({ database, transaction }: { + database: Database + transaction?: Sequelize.Transaction +}) => { + const currentStatusMessageItem = await database.config.findById(configItemIds.statusMessage, { transaction }) + const currentStatusMessage = (currentStatusMessageItem ? currentStatusMessageItem.value : null) || '' + + return currentStatusMessage +} + +export const setStatusMessage = async ({ database, newStatusMessage }: { + database: Database + newStatusMessage: string +}) => { + if (newStatusMessage === '') { + await database.config.destroy({ + where: { + id: configItemIds.statusMessage + } + }) + } else { + await database.config.insertOrUpdate({ + id: configItemIds.statusMessage, + value: newStatusMessage + }) + } +} diff --git a/src/function/sync/get-server-data-status.ts b/src/function/sync/get-server-data-status.ts index 656250a..33ea8e3 100644 --- a/src/function/sync/get-server-data-status.ts +++ b/src/function/sync/get-server-data-status.ts @@ -24,6 +24,7 @@ import { ServerUpdatedCategoryBaseData, ServerUpdatedCategoryUsedTimes, ServerUpdatedTimeLimitRules } from '../../object/serverdatastatus' +import { getStatusMessage } from '../../function/statusmessage' export const generateServerDataStatus = async ({ database, clientStatus, familyId, transaction }: { database: Database, @@ -57,7 +58,7 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI let result: ServerDataStatus = { fullVersion: familyEntry.hasFullVersion ? parseInt(familyEntry.fullVersionUntil, 10) : 0, - message: process.env.STATUS_MESSAGE || undefined + message: await getStatusMessage({ database, transaction }) || undefined } if (familyEntry.deviceListVersion !== clientStatus.devices) { diff --git a/src/websocket/index.ts b/src/websocket/index.ts index df27d69..3d8be30 100644 --- a/src/websocket/index.ts +++ b/src/websocket/index.ts @@ -19,6 +19,7 @@ import * as io from 'socket.io' import { ConnectedDevicesManager, VisibleConnectedDevicesManager } from '../connected-devices' import { Database } from '../database' import { deviceByAuthTokenRoom } from './rooms' +import * as EventEmitter from 'events' export const createWebsocketHandler = ({ connectedDevicesManager, database }: { connectedDevicesManager: VisibleConnectedDevicesManager @@ -27,6 +28,14 @@ export const createWebsocketHandler = ({ connectedDevicesManager, database }: { websocketServer: io.Server websocketApi: WebsocketApi } => { + const events = new EventEmitter() + + // this disables warnings for many listeners + // this is required because very single socket causes listeners + events.setMaxListeners(0) + + const eventTriggerImportantSyncForAll = 'triggerimportantsyncforall' + let socketCounter = 0 const server = io() @@ -43,6 +52,15 @@ export const createWebsocketHandler = ({ connectedDevicesManager, database }: { socket.join(deviceByAuthTokenRoom(deviceAuthToken)) + const importantSyncForAllListener = () => { + setTimeout(() => { + socket.connected && socket.emit('should sync', { isImportant: true }) + }, Math.random() * 1000 * 60 /* wait up to one minute */) + } + + events.on(eventTriggerImportantSyncForAll, importantSyncForAllListener) + socket.on('disconnect', () => events.off(eventTriggerImportantSyncForAll, importantSyncForAllListener)) + ;(async () => { const deviceEntryUnsafe = await database.device.findOne({ where: { @@ -94,6 +112,9 @@ export const createWebsocketHandler = ({ connectedDevicesManager, database }: { .to(deviceByAuthTokenRoom(deviceAuthToken)) .emit('sign out') }, + triggerImportantSyncAtAllDevicesInBackground: () => { + events.emit(eventTriggerImportantSyncForAll) + }, countConnections: () => socketCounter } @@ -106,5 +127,6 @@ export const createWebsocketHandler = ({ connectedDevicesManager, database }: { export interface WebsocketApi { triggerSyncByDeviceAuthToken: (params: {deviceAuthToken: string, isImportant: boolean}) => void triggerLogoutByDeviceAuthToken: (params: {deviceAuthToken: string}) => void + triggerImportantSyncAtAllDevicesInBackground: () => void countConnections: () => number }