├── .dockerignore
├── .github
└── workflows
│ ├── test-console.yml
│ ├── test-frontend.yml
│ └── test-server.yml
├── .gitignore
├── Dockerfile
├── Dockerfile.prebuilt
├── LICENSE
├── README.md
├── console
├── .eslintrc.json
├── .gitignore
├── .idea
│ ├── codeStyles
│ │ ├── Project.xml
│ │ └── codeStyleConfig.xml
│ ├── console.iml
│ ├── inspectionProfiles
│ │ └── Project_Default.xml
│ ├── modules.xml
│ └── vcs.xml
├── .nvmrc
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components.js
│ ├── components
│ │ ├── BackButton.vue
│ │ ├── ConnectionBanner.vue
│ │ ├── DateTimePicker.vue
│ │ ├── DisplayIcon.vue
│ │ ├── EditableText.vue
│ │ ├── ErrorMessage.vue
│ │ ├── Navbar.vue
│ │ ├── admin
│ │ │ ├── ApiKeyEditor.vue
│ │ │ ├── UserEditor.vue
│ │ │ └── settings
│ │ │ │ ├── CoordinateValue.vue
│ │ │ │ ├── IntegerValue.vue
│ │ │ │ └── StringValue.vue
│ │ ├── content
│ │ │ ├── AnnouncementCard.vue
│ │ │ ├── AnnouncementEditor.vue
│ │ │ └── CalendarFeedForm.vue
│ │ ├── displays
│ │ │ ├── DisplayCard.vue
│ │ │ └── DisplayEditor.vue
│ │ └── views
│ │ │ ├── PreviewContentSlot.vue
│ │ │ ├── ViewEditForm.vue
│ │ │ ├── ViewList.vue
│ │ │ ├── ViewListItem.vue
│ │ │ ├── ViewPreview.vue
│ │ │ └── editor
│ │ │ ├── AnnouncementListOptions.vue
│ │ │ ├── ContentSlot.vue
│ │ │ ├── DWDWarningMapOptions.vue
│ │ │ ├── GridEditor.vue
│ │ │ └── RemoteImageOptions.vue
│ ├── feathers-client.js
│ ├── font-awesome.js
│ ├── main.js
│ ├── router
│ │ └── index.js
│ ├── store
│ │ ├── index.js
│ │ ├── services
│ │ │ ├── announcements.js
│ │ │ ├── api-keys.js
│ │ │ ├── calendar-feeds.js
│ │ │ ├── content-slots.js
│ │ │ ├── displays.js
│ │ │ ├── incidents.js
│ │ │ ├── key-requests.js
│ │ │ ├── locations.js
│ │ │ ├── settings.js
│ │ │ ├── users.js
│ │ │ └── views.js
│ │ ├── socket.js
│ │ └── store.auth.js
│ └── views
│ │ ├── About.vue
│ │ ├── Login.vue
│ │ ├── Overview.vue
│ │ ├── Setup.vue
│ │ ├── admin
│ │ ├── ApiKeyForm.vue
│ │ ├── ApiKeyList.vue
│ │ ├── Settings.vue
│ │ ├── UserForm.vue
│ │ └── UserList.vue
│ │ ├── content
│ │ ├── AnnouncementForm.vue
│ │ ├── AnnouncementList.vue
│ │ ├── CalendarFeedEditor.vue
│ │ └── CalendarsView.vue
│ │ └── displays
│ │ ├── DisplayForm.vue
│ │ ├── DisplayList.vue
│ │ ├── KeyRequestCard.vue
│ │ └── ViewForm.vue
└── vue.config.js
├── frontend
├── .eslintrc.json
├── .gitignore
├── .idea
│ ├── codeStyles
│ │ └── codeStyleConfig.xml
│ ├── frontend.iml
│ ├── inspectionProfiles
│ │ └── Project_Default.xml
│ ├── modules.xml
│ └── vcs.xml
├── .nvmrc
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ ├── AlertBanner.vue
│ │ ├── AlertScreen.vue
│ │ ├── BrightSky.vue
│ │ ├── Clock.vue
│ │ ├── ConnectionBanner.vue
│ │ ├── DWDWarningMap.vue
│ │ ├── DefaultAlertView.vue
│ │ ├── DisplayApp.vue
│ │ ├── DisplaySetup.vue
│ │ ├── GridView.vue
│ │ ├── GridViewComponent.vue
│ │ ├── IdleScreen.vue
│ │ ├── NextUpList.vue
│ │ ├── NextUpListItem.vue
│ │ ├── RemoteImage.vue
│ │ ├── SplashScreen.vue
│ │ └── announcements
│ │ │ ├── AnnouncementList.vue
│ │ │ └── Item.vue
│ ├── feathers-client.js
│ ├── font-awesome.js
│ ├── main.js
│ └── store
│ │ ├── index.js
│ │ ├── services
│ │ ├── announcements.js
│ │ ├── calendar-items.js
│ │ ├── content-slots.js
│ │ ├── displays.js
│ │ ├── incidents.js
│ │ ├── key-requests.js
│ │ ├── locations.js
│ │ ├── settings.js
│ │ └── views.js
│ │ └── socket.js
└── vue.config.js
├── renovate.json
├── scripts
└── build.sh
├── server
├── .codeclimate.yml
├── .editorconfig
├── .env.example
├── .gitignore
├── .idea
│ ├── codeStyles
│ │ ├── Project.xml
│ │ └── codeStyleConfig.xml
│ ├── inspectionProfiles
│ │ └── Project_Default.xml
│ ├── jsLibraryMappings.xml
│ ├── modules.xml
│ ├── server.iml
│ └── vcs.xml
├── .nvmrc
├── README.md
├── config
│ ├── ci.json
│ ├── default.json
│ ├── development.json
│ ├── docker.json
│ ├── production.json
│ └── test.json
├── eslint.config.mjs
├── jest.config.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── style.css
├── src
│ ├── app.hooks.ts
│ ├── app.ts
│ ├── auth-strategies
│ │ └── api-key.strategy.ts
│ ├── authentication.ts
│ ├── channels.ts
│ ├── declarations.d.ts
│ ├── hooks
│ │ ├── allowApiKey.ts
│ │ └── unserializeJson.ts
│ ├── index.ts
│ ├── logger.ts
│ ├── middleware
│ │ └── index.ts
│ ├── migrations
│ │ ├── 00000_create-users.ts
│ │ ├── 00001_create-displays.ts
│ │ ├── 00002_create-views.ts
│ │ ├── 00003_create-content-slots.ts
│ │ ├── 00004_create-content-slot-options.ts
│ │ ├── 00005_create-api-keys.ts
│ │ ├── 00006_create-incidents.ts
│ │ ├── 00007_create-locations.ts
│ │ ├── 00008_create-announcements.ts
│ │ ├── 00009_fix-locations-constraint.ts
│ │ ├── 00010_extend_incident_description.ts
│ │ ├── 00011_create-settings.ts
│ │ ├── 00012_move_content-slot-options.ts
│ │ ├── 00013_add-display-type.ts
│ │ ├── 00014_split-locality.ts
│ │ ├── 00015_create-calendar-feeds.ts
│ │ ├── 00016_add-view-state.ts
│ │ └── 00017_add-view-pinned.ts
│ ├── models
│ │ ├── announcements.model.ts
│ │ ├── api-keys.model.ts
│ │ ├── calendar-feeds.model.ts
│ │ ├── content-slots.model.ts
│ │ ├── displays.model.ts
│ │ ├── incidents.model.ts
│ │ ├── locations.model.ts
│ │ ├── settings.model.ts
│ │ ├── users.model.ts
│ │ └── views.model.ts
│ ├── sequelize.ts
│ └── services
│ │ ├── announcements
│ │ ├── announcements.class.ts
│ │ ├── announcements.hooks.ts
│ │ └── announcements.service.ts
│ │ ├── api-keys
│ │ ├── api-keys.class.ts
│ │ ├── api-keys.hooks.ts
│ │ └── api-keys.service.ts
│ │ ├── calendar-feeds
│ │ ├── calendar-feeds.class.ts
│ │ ├── calendar-feeds.hooks.ts
│ │ └── calendar-feeds.service.ts
│ │ ├── calendar-items
│ │ ├── calendar-items.class.ts
│ │ ├── calendar-items.hooks.ts
│ │ └── calendar-items.service.ts
│ │ ├── content-slots
│ │ ├── content-slots.class.ts
│ │ ├── content-slots.hooks.ts
│ │ └── content-slots.service.ts
│ │ ├── displays
│ │ ├── displays.class.ts
│ │ ├── displays.hooks.ts
│ │ └── displays.service.ts
│ │ ├── hub-connector
│ │ ├── hub-connector.class.ts
│ │ ├── hub-connector.hooks.ts
│ │ ├── hub-connector.service.ts
│ │ └── services
│ │ │ ├── incidents.class.ts
│ │ │ └── locations.class.ts
│ │ ├── incidents
│ │ ├── incidents.class.ts
│ │ ├── incidents.hooks.ts
│ │ └── incidents.service.ts
│ │ ├── index.ts
│ │ ├── key-requests
│ │ ├── key-requests.class.ts
│ │ ├── key-requests.hooks.ts
│ │ └── key-requests.service.ts
│ │ ├── locations
│ │ ├── locations.class.ts
│ │ ├── locations.hooks.ts
│ │ └── locations.service.ts
│ │ ├── settings
│ │ ├── settings.class.ts
│ │ ├── settings.hooks.ts
│ │ └── settings.service.ts
│ │ ├── status
│ │ ├── status.class.ts
│ │ ├── status.hooks.ts
│ │ └── status.service.ts
│ │ ├── users
│ │ ├── users.class.ts
│ │ ├── users.hooks.ts
│ │ └── users.service.ts
│ │ └── views
│ │ ├── views.class.ts
│ │ ├── views.hooks.ts
│ │ └── views.service.ts
├── test
│ ├── app.test.ts
│ ├── authentication.test.ts
│ ├── eslint.config.mjs
│ ├── feed.ics
│ ├── services
│ │ ├── announcements.test.ts
│ │ ├── api-keys.test.ts
│ │ ├── calendar-feeds.test.ts
│ │ ├── calendar-items.test.ts
│ │ ├── content-slots.test.ts
│ │ ├── displays.test.ts
│ │ ├── hub-connector.test.ts
│ │ ├── incidents.test.ts
│ │ ├── key-requests.test.ts
│ │ ├── locations.test.ts
│ │ ├── settings.test.ts
│ │ ├── status.test.ts
│ │ ├── users.test.ts
│ │ └── views.test.ts
│ └── testSequencer.js
├── tsconfig.json
└── typings
│ ├── feathers-shallow-populate
│ └── index.d.ts
│ └── ical.js
│ └── index.d.ts
└── test-api
├── .gitignore
├── .idea
├── .gitignore
├── modules.xml
├── test-api.iml
└── vcs.xml
├── .mocharc.json
├── fixtures.mjs
├── package-lock.json
├── package.json
└── spec
└── 001-authentication
├── common.js
├── key-requests.js
└── users.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/node_modules/
2 | server/config/local*.json
--------------------------------------------------------------------------------
/.github/workflows/test-console.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Node.js CI for Console
5 |
6 | on:
7 | push:
8 | branches: [ "develop" ]
9 | pull_request:
10 | branches: [ "develop" ]
11 |
12 | defaults:
13 | run:
14 | working-directory: ./console
15 |
16 | jobs:
17 | build:
18 |
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
23 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
24 | with:
25 | node-version-file: 'console/.nvmrc'
26 | cache: 'npm'
27 | cache-dependency-path: console/package-lock.json
28 | - run: npm ci
29 | - run: npm run lint
30 | - run: npm run build
31 |
--------------------------------------------------------------------------------
/.github/workflows/test-frontend.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Node.js CI for Frontend
5 |
6 | on:
7 | push:
8 | branches: [ "develop" ]
9 | pull_request:
10 | branches: [ "develop" ]
11 |
12 | defaults:
13 | run:
14 | working-directory: ./frontend
15 |
16 | jobs:
17 | build:
18 |
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
23 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
24 | with:
25 | node-version-file: 'frontend/.nvmrc'
26 | cache: 'npm'
27 | cache-dependency-path: frontend/package-lock.json
28 | - run: npm ci
29 | - run: npm run lint
30 | - run: npm run build
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20.19.2@sha256:34ebf3e5d4a51d4b1af4b78188f9b0ad0daf1d763395e2390ab935f910350b78 as build-console
2 |
3 | WORKDIR /home/node/app/console
4 | COPY ./console/package.json ./console/package-lock.json /home/node/app/console/
5 | RUN npm ci --no-audit
6 | COPY ./console /home/node/app/console
7 | RUN npm run build
8 |
9 | FROM node:20.19.2@sha256:34ebf3e5d4a51d4b1af4b78188f9b0ad0daf1d763395e2390ab935f910350b78 as build-frontend
10 |
11 | WORKDIR /home/node/app/frontend
12 | COPY ./frontend/package.json ./frontend/package-lock.json /home/node/app/frontend/
13 | RUN npm ci --no-audit
14 | COPY ./frontend /home/node/app/frontend
15 | RUN npm run build
16 |
17 | FROM node:20.19.2@sha256:34ebf3e5d4a51d4b1af4b78188f9b0ad0daf1d763395e2390ab935f910350b78 as build-server
18 |
19 | WORKDIR /home/node/app/server
20 | COPY ./server/package.json ./server/package-lock.json /home/node/app/server/
21 | RUN npm ci --no-audit
22 | COPY ./server /home/node/app/server
23 | RUN npm run compile
24 |
25 | FROM node:20.19.2@sha256:34ebf3e5d4a51d4b1af4b78188f9b0ad0daf1d763395e2390ab935f910350b78
26 | WORKDIR /home/node/app/
27 | COPY ./server/package.json ./server/package-lock.json /home/node/app/
28 | RUN npm ci --only=production --no-audit
29 | COPY --from=build-server /home/node/app/server/lib /home/node/app/
30 | COPY ./server/config /home/node/app/config
31 | COPY ./server/public /home/node/app/public
32 | COPY --from=build-console /home/node/app/console/dist /home/node/app/ext-console
33 | COPY --from=build-frontend /home/node/app/frontend/dist /home/node/app/ext-display
34 |
35 | EXPOSE 3031
36 | ENV NODE_ENV production
37 | ENV NODE_CONFIG_ENV docker
38 | USER node
39 | CMD [ "node", "index.js" ]
40 |
--------------------------------------------------------------------------------
/Dockerfile.prebuilt:
--------------------------------------------------------------------------------
1 | FROM node:20.19.2@sha256:34ebf3e5d4a51d4b1af4b78188f9b0ad0daf1d763395e2390ab935f910350b78
2 |
3 | WORKDIR /home/node/app
4 | COPY ./build/package.json ./build/package-lock.json /home/node/app/
5 | RUN npm ci --only=production --no-audit
6 | COPY ./build /home/node/app
7 |
8 | EXPOSE 3031
9 | ENV NODE_ENV production
10 | ENV NODE_CONFIG_ENV docker
11 | USER node
12 | CMD [ "node", "index.js" ]
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Alarmdisplay Display
2 |
3 | This component takes care of displaying incident data in case of an emergency, and shows general information otherwise. The incidents can be created via API, or synced over from a [Hub](https://github.com/alarmdisplay/hub) instance.
4 |
5 | This repository contains backend and frontend code.
6 | For more info on how to run those parts, check out the README in the respective sub folder.
7 |
8 | ## Build
9 | You can use the [build script](./scripts/build.sh) to create a runnable version of this project.
10 | During this process, the single components in the sub folders should not be running in development mode.
11 |
12 | ### Docker
13 | There are two options to build a Docker image:
14 | * The default [Dockerfile](./Dockerfile) uses multi-stage builds to build everything inside Docker. This might require more resources, but removes the need to have Node.js set up.
15 | * If you built the application with the build script above, you can use the file [Dockerfile.prebuilt](./Dockerfile.prebuilt) to build the image with the contents of the build folder.
16 |
17 | ## Deployment
18 | At the moment, this project is not recommended for deployment outside a development or test environment.
19 |
--------------------------------------------------------------------------------
/console/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "node": true
5 | },
6 | "extends": [
7 | "plugin:vue/essential",
8 | "eslint:recommended"
9 | ],
10 | "parserOptions": {
11 | "parser": "@babel/eslint-parser"
12 | },
13 | "reportUnusedDisableDirectives": true,
14 | "rules": {
15 | "vue/multi-word-component-names": "off",
16 | "vue/no-mutating-props": "off"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/console/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea/workspace.xml
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
--------------------------------------------------------------------------------
/console/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/console/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/console/.idea/console.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/console/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/console/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/console/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/console/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.19.2
2 |
--------------------------------------------------------------------------------
/console/README.md:
--------------------------------------------------------------------------------
1 | # Display Console
2 |
3 | This web application offers a user interface to manage the [Display Server](../server).
4 |
5 | ## Development
6 | In order to run a development version on your local system, you need a [Node.js](https://nodejs.org/) environment.
7 | Clone the repository and run `npm install` in this folder to install all the dependencies.
8 |
9 | Start the development server by running `npm run serve`, it will automatically reload when files have changed.
10 | Now you can access the server on http://localhost:8080 (may be a different port on your system, check the console output).
11 | If you run a development Display Server on http://localhost:3031, the requests are automatically proxied there.
12 | This allows for parallel development of the Console and the backend.
13 |
14 | ### Libraries and frameworks
15 | This project uses the following libraries or frameworks, please refer to their documentation as well.
16 | - [Vue.js](https://vuejs.org/)
17 | - [Vue Router](https://router.vuejs.org/)
18 | - [Vuex](https://vuex.vuejs.org/)
19 | - [FeathersVuex](https://vuex.feathersjs.com/)
20 | - [Font Awesome](https://fontawesome.com/)
21 |
22 | ## Build & Deploy
23 | Run `npm run build`, which compiles and minifies the app for production.
24 | You find the result of the build process in a folder called `dist`.
25 | This folder only contains HTML, CSS, and JS files, which means they can be hosted as static files.
26 | By default, the app expects to be accessible under the path `/console/`.
27 | You can change this behaviour by adapting the `publicPath` in [vue.config.js](vue.config.js).
28 |
--------------------------------------------------------------------------------
/console/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/console/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@alarmdisplay/display-console",
3 | "version": "1.0.0-beta.5",
4 | "private": false,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "license": "AGPL-3.0-only",
11 | "dependencies": {
12 | "@feathersjs/authentication-client": "4.5.18",
13 | "@feathersjs/feathers": "4.5.17",
14 | "@feathersjs/socketio-client": "4.5.18",
15 | "@fortawesome/fontawesome-svg-core": "6.7.2",
16 | "@fortawesome/free-solid-svg-icons": "6.7.2",
17 | "@fortawesome/vue-fontawesome": "2.0.10",
18 | "@vue/composition-api": "1.7.2",
19 | "bulma": "0.9.4",
20 | "core-js": "3.41.0",
21 | "feathers-hooks-common": "6.1.5",
22 | "feathers-vuex": "3.16.0",
23 | "moment": "2.30.1",
24 | "socket.io-client": "2.5.0",
25 | "vue": "2.6.14",
26 | "vue-moment": "4.1.0",
27 | "vue-router": "3.6.5",
28 | "vuex": "3.6.2"
29 | },
30 | "devDependencies": {
31 | "@babel/eslint-parser": "7.27.1",
32 | "@vue/cli-plugin-babel": "5.0.8",
33 | "@vue/cli-plugin-eslint": "5.0.8",
34 | "@vue/cli-plugin-router": "5.0.8",
35 | "@vue/cli-service": "5.0.8",
36 | "eslint": "8.57.1",
37 | "eslint-plugin-vue": "9.33.0",
38 | "vue-template-compiler": "2.6.14"
39 | },
40 | "overrides": {
41 | "feathers-vuex": {
42 | "serialize-error": "11.0.3"
43 | }
44 | },
45 | "browserslist": [
46 | "> 1%",
47 | "last 2 versions"
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/console/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alarmdisplay/display/833f4b4d27e8292df1f873d1142eba9cad80182d/console/public/favicon.ico
--------------------------------------------------------------------------------
/console/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Anzeige – Console
9 |
10 |
11 |
12 | We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/console/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
67 |
68 |
73 |
--------------------------------------------------------------------------------
/console/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alarmdisplay/display/833f4b4d27e8292df1f873d1142eba9cad80182d/console/src/assets/logo.png
--------------------------------------------------------------------------------
/console/src/components.js:
--------------------------------------------------------------------------------
1 | const components = {
2 | AnnouncementList: {
3 | name: 'Bekanntmachungen',
4 | icon: 'bullhorn',
5 | options: true,
6 | },
7 | BrightSky: {
8 | name: 'Bright Sky',
9 | icon: 'cloud-sun',
10 | options: false,
11 | },
12 | Clock: {
13 | name: 'Uhr',
14 | icon: 'clock',
15 | options: false,
16 | },
17 | DWDWarningMap: {
18 | name: 'DWD-Warnkarte',
19 | icon: 'cloud-showers-heavy',
20 | options: true,
21 | },
22 | NextUpList: {
23 | name: 'Termine',
24 | icon: 'calendar',
25 | options: false,
26 | },
27 | RemoteImage: {
28 | name: 'Bild',
29 | icon: 'image',
30 | options: true,
31 | },
32 | }
33 |
34 | export function getAvailableComponentTypes() {
35 | return Object.keys(components);
36 | }
37 |
38 | export function getComponentName(componentType) {
39 | return components[componentType]?.name || componentType;
40 | }
41 |
42 | export function getComponentIcon(componentType) {
43 | return components[componentType]?.icon || 'cube';
44 | }
45 |
46 | export function isComponentConfigurable(componentType) {
47 | return !!(components[componentType]?.options);
48 | }
49 |
50 | export function isValidComponentType(componentType) {
51 | return getAvailableComponentTypes().includes(componentType);
52 | }
53 |
--------------------------------------------------------------------------------
/console/src/components/BackButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Zurück
7 |
8 |
9 |
19 |
--------------------------------------------------------------------------------
/console/src/components/ConnectionBanner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Verbunden
5 |
6 |
7 |
8 | Keine Verbindung zum Server
9 |
10 |
11 |
12 |
13 |
24 |
25 |
28 |
--------------------------------------------------------------------------------
/console/src/components/DisplayIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
34 |
35 |
38 |
--------------------------------------------------------------------------------
/console/src/components/ErrorMessage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ messages[0] || 'Fehler' }}
4 |
5 |
6 |
9 |
10 |
11 |
12 | {{ message }}
13 |
14 |
15 |
16 |
17 |
18 |
40 |
--------------------------------------------------------------------------------
/console/src/components/admin/ApiKeyEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
44 |
45 |
48 |
--------------------------------------------------------------------------------
/console/src/components/views/PreviewContentSlot.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
27 |
28 |
43 |
--------------------------------------------------------------------------------
/console/src/components/views/ViewPreview.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
29 |
30 |
39 |
--------------------------------------------------------------------------------
/console/src/components/views/editor/AnnouncementListOptions.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
22 |
23 |
26 |
--------------------------------------------------------------------------------
/console/src/components/views/editor/DWDWarningMapOptions.vue:
--------------------------------------------------------------------------------
1 |
2 |
46 |
47 |
48 |
56 |
57 |
60 |
--------------------------------------------------------------------------------
/console/src/components/views/editor/RemoteImageOptions.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
22 |
23 |
26 |
--------------------------------------------------------------------------------
/console/src/feathers-client.js:
--------------------------------------------------------------------------------
1 | import feathers from '@feathersjs/feathers'
2 | import socketio from '@feathersjs/socketio-client'
3 | import auth from '@feathersjs/authentication-client'
4 | import io from 'socket.io-client'
5 | import { iff, discard } from 'feathers-hooks-common'
6 | import feathersVuex from 'feathers-vuex'
7 |
8 | const socket = io({ transports: ['websocket'] })
9 |
10 | const feathersClient = feathers()
11 | .configure(socketio(socket))
12 | .configure(auth({ storage: window.localStorage, storageKey: 'display-console-jwt' }))
13 | .hooks({
14 | before: {
15 | all: [
16 | // Prevent sending local-only properties to the server
17 | iff(
18 | context => ['create', 'update', 'patch'].includes(context.method),
19 | discard('__id', '__isTemp')
20 | )
21 | ]
22 | }
23 | })
24 |
25 | export default feathersClient
26 |
27 | // Setting up feathers-vuex
28 | const { makeServicePlugin, makeAuthPlugin, BaseModel, models, FeathersVuex } = feathersVuex(
29 | feathersClient,
30 | {
31 | serverAlias: 'api',
32 | idField: 'id',
33 | whitelist: ['$regex', '$options']
34 | }
35 | )
36 |
37 | export { makeAuthPlugin, makeServicePlugin, BaseModel, models, FeathersVuex }
38 |
--------------------------------------------------------------------------------
/console/src/font-awesome.js:
--------------------------------------------------------------------------------
1 | import { library } from '@fortawesome/fontawesome-svg-core'
2 | import {
3 | faArrowsAlt,
4 | faBars,
5 | faBullhorn,
6 | faCalendar,
7 | faChevronLeft,
8 | faChevronRight,
9 | faClock,
10 | faCloudShowersHeavy,
11 | faCloudSun,
12 | faCog,
13 | faColumns,
14 | faCube,
15 | faCubes,
16 | faDesktop,
17 | faEdit,
18 | faEnvelope,
19 | faExpandAlt,
20 | faFolderTree,
21 | faHome,
22 | faInfoCircle,
23 | faImage,
24 | faKey,
25 | faLock,
26 | faPause,
27 | faPencilAlt,
28 | faPlay,
29 | faPlus,
30 | faQuestionCircle,
31 | faSignOutAlt,
32 | faSpinner,
33 | faStopwatch,
34 | faTabletAlt,
35 | faTimes,
36 | faThumbtack,
37 | faThumbtackSlash,
38 | faTrashAlt,
39 | faUser,
40 | faUserEdit,
41 | faUserMinus,
42 | faUserPlus,
43 | faWrench
44 | } from '@fortawesome/free-solid-svg-icons'
45 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
46 |
47 | library.add(
48 | faArrowsAlt,
49 | faBars,
50 | faBullhorn,
51 | faCalendar,
52 | faChevronLeft,
53 | faChevronRight,
54 | faClock,
55 | faCloudShowersHeavy,
56 | faCloudSun,
57 | faCog,
58 | faColumns,
59 | faCube,
60 | faCubes,
61 | faDesktop,
62 | faEdit,
63 | faEnvelope,
64 | faExpandAlt,
65 | faFolderTree,
66 | faHome,
67 | faImage,
68 | faInfoCircle,
69 | faKey,
70 | faLock,
71 | faPause,
72 | faPencilAlt,
73 | faPlay,
74 | faPlus,
75 | faQuestionCircle,
76 | faSignOutAlt,
77 | faSpinner,
78 | faStopwatch,
79 | faTabletAlt,
80 | faTimes,
81 | faThumbtack,
82 | faThumbtackSlash,
83 | faTrashAlt,
84 | faUser,
85 | faUserEdit,
86 | faUserMinus,
87 | faUserPlus,
88 | faWrench
89 | )
90 |
91 | export default FontAwesomeIcon
92 |
--------------------------------------------------------------------------------
/console/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueCompositionApi from '@vue/composition-api'
3 | Vue.use(VueCompositionApi)
4 |
5 | import App from './App.vue'
6 | import router from './router'
7 | import store from './store'
8 |
9 | import moment from 'moment'
10 | import VueMoment from 'vue-moment'
11 | require('moment/locale/de')
12 | Vue.use(VueMoment, { moment })
13 |
14 | // Load Font Awesome
15 | import FontAwesomeIcon from './font-awesome'
16 | Vue.component('font-awesome-icon', FontAwesomeIcon)
17 |
18 | Vue.config.productionTip = false
19 |
20 | Vue.filter('durationAsDigits', function (seconds) {
21 | function twoDigits (value) {
22 | let strValue = String(value)
23 | if (strValue.length === 1) {
24 | strValue = `0${value}`
25 | }
26 | return strValue
27 | }
28 |
29 | if (seconds < 3600) {
30 | return `${twoDigits(Math.trunc(seconds / 60))}:${twoDigits(seconds % 60)}`
31 | } else {
32 | const secondsOfHour = seconds % 3600
33 | return `${twoDigits(Math.trunc(seconds / 3600))}:${twoDigits(Math.trunc(secondsOfHour / 60))}:${twoDigits(secondsOfHour % 60)}`
34 | }
35 | })
36 |
37 | // Set up the Vue root instance
38 | new Vue({
39 | store,
40 | render: h => h(App),
41 | data: {
42 | seconds: Math.floor(Date.now() / 1000)
43 | },
44 | mounted () {
45 | setInterval(this.updateSeconds, 1000)
46 | },
47 | methods: {
48 | updateSeconds () {
49 | this.seconds = Math.floor(Date.now() / 1000)
50 | }
51 | },
52 | router: router
53 | }).$mount('#app')
54 |
--------------------------------------------------------------------------------
/console/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import feathersClient, { FeathersVuex } from '@/feathers-client'
4 | import auth from './store.auth'
5 |
6 | import announcements from './services/announcements'
7 | import apiKeys from './services/api-keys'
8 | import calendarFeeds from './services/calendar-feeds'
9 | import contentSlots from './services/content-slots'
10 | import displays from './services/displays'
11 | import incidents from './services/incidents'
12 | import keyRequests from './services/key-requests'
13 | import locations from './services/locations'
14 | import settings from './services/settings'
15 | import users from './services/users'
16 | import views from './services/views'
17 | import socket, { createSocketPlugin } from '@/store/socket'
18 |
19 | Vue.use(Vuex)
20 | Vue.use(FeathersVuex)
21 |
22 | export default new Vuex.Store({
23 | state: {
24 | showSetup: false
25 | },
26 | mutations: {
27 | setShowSetup (state, value) {
28 | state.showSetup = value === true
29 | }
30 | },
31 | actions: {
32 | },
33 | modules: {
34 | socket
35 | },
36 | plugins: [
37 | auth,
38 | createSocketPlugin(feathersClient.io),
39 | announcements,
40 | apiKeys,
41 | calendarFeeds,
42 | contentSlots,
43 | displays,
44 | incidents,
45 | keyRequests,
46 | locations,
47 | settings,
48 | users,
49 | views
50 | ]
51 | })
52 |
--------------------------------------------------------------------------------
/console/src/store/services/announcements.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class Announcement extends BaseModel {
4 | static modelName = 'Announcement'
5 |
6 | static instanceDefaults () {
7 | return {
8 | title: '',
9 | body: '',
10 | important: false,
11 | validFrom: null,
12 | validTo: null
13 | }
14 | }
15 |
16 | static setupInstance (data) {
17 | // Convert date strings into Date objects
18 | for (const prop of ['validFrom', 'validTo', 'updatedAt']) {
19 | if (data[prop]) {
20 | data[prop] = new Date(data[prop])
21 | }
22 | }
23 |
24 | return data
25 | }
26 | }
27 |
28 | const servicePath = 'api/v1/announcements'
29 | const servicePlugin = makeServicePlugin({
30 | Model: Announcement,
31 | service: feathersClient.service(servicePath),
32 | servicePath
33 | })
34 |
35 | // Setup the client-side Feathers hooks.
36 | feathersClient.service(servicePath).hooks({
37 | before: {
38 | all: [],
39 | find: [],
40 | get: [],
41 | create: [],
42 | update: [],
43 | patch: [],
44 | remove: []
45 | },
46 | after: {
47 | all: [],
48 | find: [],
49 | get: [],
50 | create: [],
51 | update: [],
52 | patch: [],
53 | remove: []
54 | },
55 | error: {
56 | all: [],
57 | find: [],
58 | get: [],
59 | create: [],
60 | update: [],
61 | patch: [],
62 | remove: []
63 | }
64 | })
65 |
66 | export default servicePlugin
67 |
--------------------------------------------------------------------------------
/console/src/store/services/api-keys.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class ApiKey extends BaseModel {
4 | static modelName = 'ApiKey'
5 |
6 | static instanceDefaults () {
7 | return {
8 | name: '',
9 | displayId: null
10 | }
11 | }
12 | }
13 |
14 | const servicePath = 'api/v1/api-keys'
15 | const servicePlugin = makeServicePlugin({
16 | Model: ApiKey,
17 | service: feathersClient.service(servicePath),
18 | servicePath,
19 | state: {
20 | createdApiKey: null
21 | },
22 | mutations: {
23 | clearCreatedApiKey: function (state) {
24 | state.createdApiKey = null
25 | },
26 | setCreatedApiKey: function (state, apiKey) {
27 | // Make a copy of the API key for one-time display
28 | state.createdApiKey = `${apiKey.id}:${apiKey.token}`
29 | }
30 | },
31 | setupInstance: (data, { store }) => {
32 | // If this is a newly created API key that includes the token, copy the token and prevent it from being stored
33 | if (data.token) {
34 | store.commit('api-keys/setCreatedApiKey', data)
35 | delete data.token
36 | }
37 | return data
38 | }
39 | })
40 |
41 | // Setup the client-side Feathers hooks.
42 | feathersClient.service(servicePath).hooks({
43 | before: {
44 | all: [],
45 | find: [],
46 | get: [],
47 | create: [],
48 | update: [],
49 | patch: [],
50 | remove: []
51 | },
52 | after: {
53 | all: [],
54 | find: [],
55 | get: [],
56 | create: [],
57 | update: [],
58 | patch: [],
59 | remove: []
60 | },
61 | error: {
62 | all: [],
63 | find: [],
64 | get: [],
65 | create: [],
66 | update: [],
67 | patch: [],
68 | remove: []
69 | }
70 | })
71 |
72 | export default servicePlugin
73 |
--------------------------------------------------------------------------------
/console/src/store/services/calendar-feeds.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class CalendarFeed extends BaseModel {
4 | static modelName = 'CalendarFeed'
5 |
6 | static instanceDefaults () {
7 | return {
8 | name: '',
9 | url: '',
10 | }
11 | }
12 |
13 | static setupInstance (data) {
14 | // Convert date strings into Date objects
15 | for (const prop of ['createdAt', 'updatedAt']) {
16 | if (data[prop]) {
17 | data[prop] = new Date(data[prop])
18 | }
19 | }
20 |
21 | return data
22 | }
23 | }
24 |
25 | const servicePath = 'calendar-feeds'
26 | const servicePlugin = makeServicePlugin({
27 | Model: CalendarFeed,
28 | service: feathersClient.service(servicePath),
29 | servicePath
30 | })
31 |
32 | // Setup the client-side Feathers hooks.
33 | feathersClient.service(servicePath).hooks({
34 | before: {
35 | all: [],
36 | find: [],
37 | get: [],
38 | create: [],
39 | update: [],
40 | patch: [],
41 | remove: []
42 | },
43 | after: {
44 | all: [],
45 | find: [],
46 | get: [],
47 | create: [],
48 | update: [],
49 | patch: [],
50 | remove: []
51 | },
52 | error: {
53 | all: [],
54 | find: [],
55 | get: [],
56 | create: [],
57 | update: [],
58 | patch: [],
59 | remove: []
60 | }
61 | })
62 |
63 | export default servicePlugin
64 |
--------------------------------------------------------------------------------
/console/src/store/services/content-slots.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class ContentSlot extends BaseModel {
4 | static modelName = 'ContentSlot'
5 |
6 | static instanceDefaults () {
7 | return {
8 | component: 'Clock',
9 | columnStart: 1,
10 | columnEnd: 1,
11 | rowStart: 2,
12 | rowEnd: 2,
13 | options: {},
14 | viewId: null
15 | }
16 | }
17 | }
18 |
19 | const servicePath = 'api/v1/content-slots'
20 | const servicePlugin = makeServicePlugin({
21 | Model: ContentSlot,
22 | service: feathersClient.service(servicePath),
23 | servicePath
24 | })
25 |
26 | // Setup the client-side Feathers hooks.
27 | feathersClient.service(servicePath).hooks({
28 | before: {
29 | all: [],
30 | find: [],
31 | get: [],
32 | create: [],
33 | update: [],
34 | patch: [],
35 | remove: []
36 | },
37 | after: {
38 | all: [],
39 | find: [],
40 | get: [],
41 | create: [],
42 | update: [],
43 | patch: [],
44 | remove: []
45 | },
46 | error: {
47 | all: [],
48 | find: [],
49 | get: [],
50 | create: [],
51 | update: [],
52 | patch: [],
53 | remove: []
54 | }
55 | })
56 |
57 | export default servicePlugin
58 |
--------------------------------------------------------------------------------
/console/src/store/services/displays.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class Display extends BaseModel {
4 | static modelName = 'Display'
5 |
6 | static instanceDefaults () {
7 | return {
8 | name: '',
9 | active: false,
10 | description: '',
11 | type: 'monitor'
12 | }
13 | }
14 |
15 | static setupInstance (data, { models }) {
16 | // Add nested view objects to storage
17 | if (data.views && Array.isArray(data.views)) {
18 | data.views.forEach(view => new models.api.View(view))
19 | }
20 |
21 | // Replace the nested views with a getter
22 | Object.defineProperty(data, 'views', {
23 | get: function () {
24 | const views = models.api.View.findInStore({
25 | query: {
26 | displayId: data.id,
27 | $sort: {
28 | order: 1
29 | }
30 | }
31 | })
32 | return views.data
33 | },
34 | configurable: true,
35 | enumerable: true
36 | })
37 |
38 | return data
39 | }
40 | }
41 |
42 | const servicePath = 'api/v1/displays'
43 | const servicePlugin = makeServicePlugin({
44 | Model: Display,
45 | service: feathersClient.service(servicePath),
46 | servicePath
47 | })
48 |
49 | // Setup the client-side Feathers hooks.
50 | feathersClient.service(servicePath).hooks({
51 | before: {
52 | all: [],
53 | find: [],
54 | get: [],
55 | create: [],
56 | update: [],
57 | patch: [],
58 | remove: []
59 | },
60 | after: {
61 | all: [],
62 | find: [],
63 | get: [setOwnDisplayId],
64 | create: [],
65 | update: [],
66 | patch: [],
67 | remove: []
68 | },
69 | error: {
70 | all: [],
71 | find: [],
72 | get: [],
73 | create: [],
74 | update: [],
75 | patch: [],
76 | remove: []
77 | }
78 | })
79 |
80 | function setOwnDisplayId (context) {
81 | if (context.id === 'self') {
82 | console.log('Own Display ID is', context.result.id)
83 | context.service.FeathersVuexModel.store.commit('setOwnDisplayId', context.result.id)
84 | }
85 | }
86 |
87 | export default servicePlugin
88 |
--------------------------------------------------------------------------------
/console/src/store/services/incidents.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class Incident extends BaseModel {
4 | static modelName = 'Incident'
5 |
6 | static instanceDefaults () {
7 | return {
8 | time: new Date(),
9 | sender: '',
10 | ref: '',
11 | caller_name: '',
12 | caller_number: '',
13 | reason: '',
14 | keyword: '',
15 | description: '',
16 | status: 'Actual',
17 | category: 'Other'
18 | }
19 | }
20 |
21 | static setupInstance (data) {
22 | // Convert date strings into Date objects
23 | for (const prop of ['time']) {
24 | if (data[prop]) {
25 | data[prop] = new Date(data[prop])
26 | }
27 | }
28 |
29 | return data
30 | }
31 | }
32 |
33 | const servicePath = 'api/v1/incidents'
34 | const servicePlugin = makeServicePlugin({
35 | Model: Incident,
36 | service: feathersClient.service(servicePath),
37 | servicePath
38 | })
39 |
40 | // Setup the client-side Feathers hooks.
41 | feathersClient.service(servicePath).hooks({
42 | before: {
43 | all: [],
44 | find: [],
45 | get: [],
46 | create: [],
47 | update: [],
48 | patch: [],
49 | remove: []
50 | },
51 | after: {
52 | all: [],
53 | find: [],
54 | get: [],
55 | create: [],
56 | update: [],
57 | patch: [],
58 | remove: []
59 | },
60 | error: {
61 | all: [],
62 | find: [],
63 | get: [],
64 | create: [],
65 | update: [],
66 | patch: [],
67 | remove: []
68 | }
69 | })
70 |
71 | export default servicePlugin
72 |
--------------------------------------------------------------------------------
/console/src/store/services/key-requests.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class KeyRequest extends BaseModel {
4 | static modelName = 'KeyRequest'
5 |
6 | static instanceDefaults () {
7 | return {
8 | requestId: '',
9 | name: '',
10 | granted: false,
11 | apiKey: ''
12 | }
13 | }
14 | }
15 |
16 | const servicePath = 'api/v1/key-requests'
17 | const servicePlugin = makeServicePlugin({
18 | Model: KeyRequest,
19 | service: feathersClient.service(servicePath),
20 | servicePath
21 | })
22 |
23 | // Setup the client-side Feathers hooks.
24 | feathersClient.service(servicePath).hooks({
25 | before: {
26 | all: [],
27 | find: [],
28 | get: [],
29 | create: [],
30 | update: [],
31 | patch: [],
32 | remove: []
33 | },
34 | after: {
35 | all: [],
36 | find: [],
37 | get: [],
38 | create: [],
39 | update: [],
40 | patch: [],
41 | remove: []
42 | },
43 | error: {
44 | all: [],
45 | find: [],
46 | get: [],
47 | create: [],
48 | update: [],
49 | patch: [],
50 | remove: []
51 | }
52 | })
53 |
54 | export default servicePlugin
55 |
--------------------------------------------------------------------------------
/console/src/store/services/locations.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class Location extends BaseModel {
4 | static modelName = 'Location'
5 |
6 | static instanceDefaults () {
7 | return {
8 | rawText: '',
9 | latitude: undefined,
10 | longitude: undefined,
11 | name: '',
12 | street: '',
13 | number: '',
14 | detail: '',
15 | municipality: '',
16 | district: '',
17 | incidentId: undefined
18 | }
19 | }
20 | }
21 |
22 | const servicePath = 'api/v1/locations'
23 | const servicePlugin = makeServicePlugin({
24 | Model: Location,
25 | service: feathersClient.service(servicePath),
26 | servicePath
27 | })
28 |
29 | // Setup the client-side Feathers hooks.
30 | feathersClient.service(servicePath).hooks({
31 | before: {
32 | all: [],
33 | find: [],
34 | get: [],
35 | create: [],
36 | update: [],
37 | patch: [],
38 | remove: []
39 | },
40 | after: {
41 | all: [],
42 | find: [],
43 | get: [],
44 | create: [],
45 | update: [],
46 | patch: [],
47 | remove: []
48 | },
49 | error: {
50 | all: [],
51 | find: [],
52 | get: [],
53 | create: [],
54 | update: [],
55 | patch: [],
56 | remove: []
57 | }
58 | })
59 |
60 | export default servicePlugin
61 |
--------------------------------------------------------------------------------
/console/src/store/services/users.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class User extends BaseModel {
4 | static modelName = 'User'
5 |
6 | static instanceDefaults () {
7 | return {
8 | email: '',
9 | name: ''
10 | }
11 | }
12 |
13 | get displayName () {
14 | return this.name || this.email
15 | }
16 | }
17 |
18 | const servicePath = 'users'
19 | const servicePlugin = makeServicePlugin({
20 | Model: User,
21 | service: feathersClient.service(servicePath),
22 | servicePath
23 | })
24 |
25 | // Setup the client-side Feathers hooks.
26 | feathersClient.service(servicePath).hooks({
27 | before: {
28 | all: [],
29 | find: [],
30 | get: [],
31 | create: [],
32 | update: [],
33 | patch: [],
34 | remove: []
35 | },
36 | after: {
37 | all: [],
38 | find: [],
39 | get: [],
40 | create: [],
41 | update: [],
42 | patch: [],
43 | remove: []
44 | },
45 | error: {
46 | all: [],
47 | find: [],
48 | get: [],
49 | create: [],
50 | update: [],
51 | patch: [],
52 | remove: []
53 | }
54 | })
55 |
56 | export default servicePlugin
57 |
--------------------------------------------------------------------------------
/console/src/store/services/views.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class View extends BaseModel {
4 | static modelName = 'View'
5 |
6 | static instanceDefaults () {
7 | return {
8 | type: 'idle',
9 | order: 999,
10 | columns: 3,
11 | rows: 3,
12 | displayId: null,
13 | active: true,
14 | pinned: false,
15 | }
16 | }
17 |
18 | static setupInstance (data, { models }) {
19 | // Add nested content slot objects to storage
20 | if (data.contentSlots && Array.isArray(data.contentSlots)) {
21 | data.contentSlots.forEach(contentSlot => new models.api.ContentSlot(contentSlot))
22 | }
23 |
24 | data.active = data.active === 1;
25 | data.pinned = data.pinned === 1;
26 |
27 | // Replace the nested content slots with a getter
28 | Object.defineProperty(data, 'contentSlots', {
29 | get: function () {
30 | const contentSlots = models.api.ContentSlot.findInStore({
31 | query: {
32 | viewId: data.id
33 | }
34 | })
35 | return contentSlots.data
36 | },
37 | configurable: true,
38 | enumerable: true
39 | })
40 |
41 | return data
42 | }
43 | }
44 |
45 | const servicePath = 'api/v1/views'
46 | const servicePlugin = makeServicePlugin({
47 | Model: View,
48 | service: feathersClient.service(servicePath),
49 | servicePath
50 | })
51 |
52 | // Setup the client-side Feathers hooks.
53 | feathersClient.service(servicePath).hooks({
54 | before: {
55 | all: [],
56 | find: [],
57 | get: [],
58 | create: [],
59 | update: [],
60 | patch: [],
61 | remove: []
62 | },
63 | after: {
64 | all: [],
65 | find: [],
66 | get: [],
67 | create: [],
68 | update: [],
69 | patch: [],
70 | remove: []
71 | },
72 | error: {
73 | all: [],
74 | find: [],
75 | get: [],
76 | create: [],
77 | update: [],
78 | patch: [],
79 | remove: []
80 | }
81 | })
82 |
83 | export default servicePlugin
84 |
--------------------------------------------------------------------------------
/console/src/store/socket.js:
--------------------------------------------------------------------------------
1 | // initial state
2 | const state = () => ({
3 | connected: false
4 | })
5 |
6 | const getters = {}
7 |
8 | const actions = {}
9 |
10 | const mutations = {
11 | setConnected (state, connected) {
12 | state.connected = connected === true
13 | }
14 | }
15 |
16 | export default {
17 | namespaced: true,
18 | state,
19 | getters,
20 | actions,
21 | mutations
22 | }
23 |
24 | export function createSocketPlugin (socket) {
25 | return store => {
26 | socket.on('connect', () => {
27 | store.commit('socket/setConnected', true)
28 | })
29 | socket.on('disconnect', () => {
30 | store.commit('socket/setConnected', false)
31 |
32 | // Clear all key requests, because the server only stores them in memory and IDs could be reused after a restart
33 | store.commit('key-requests/clearAll')
34 | })
35 | socket.on('connect_error', (err) => {
36 | console.error('Socket connect error', err)
37 | store.commit('socket/setConnected', false)
38 | })
39 | socket.on('connect_timeout', (timeout) => {
40 | console.error('Socket connect timeout', timeout)
41 | })
42 | socket.on('error', (err) => {
43 | console.error('Socket error', err)
44 | })
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/console/src/store/store.auth.js:
--------------------------------------------------------------------------------
1 | import { makeAuthPlugin } from '@/feathers-client'
2 |
3 | function getExpiresAt (state) {
4 | const expiresAt = state.payload?.authentication?.payload?.exp
5 | if (!expiresAt) {
6 | return false
7 | }
8 |
9 | return expiresAt
10 | }
11 |
12 | export default makeAuthPlugin({ userService: 'users', entityIdField: 'id', getters: { expiresAt: getExpiresAt } })
13 |
--------------------------------------------------------------------------------
/console/src/views/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
--------------------------------------------------------------------------------
/console/src/views/Overview.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Übersicht
5 |
6 | Diese Übersichtsseite soll in einer späteren Version einen schnellen Überblick über den Zustand des Systems und direkten Zugriff auf wichtige Funktionen bieten.
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
20 |
--------------------------------------------------------------------------------
/console/src/views/admin/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Einstellungen
5 |
6 |
7 |
8 |
9 | Einstellung
10 | Wert
11 |
12 |
13 |
14 |
15 | Anzeigedauer für Einsätze
16 |
17 |
18 |
19 | Anzeigedauer für Probealarme
20 |
21 |
22 |
23 | Anzeigefilter für Einsatzgrund
24 |
25 |
26 |
27 | Koordinaten des Standorts
28 |
29 |
30 |
31 | Text für Alarmbanner
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
59 |
60 |
63 |
--------------------------------------------------------------------------------
/console/src/views/content/AnnouncementList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Bekanntmachungen
5 |
6 |
7 | Bekanntmachungen können auf den Displays im Ruhemodus angezeigt werden, um aktuelle Informationen zu verbreiten.
8 | Ein optionaler Gültigkeitszeitraum erlaubt ein zeitgesteuertes Ein- und Ausblenden von einzelnen Einträgen.
9 |
10 |
11 |
12 |
13 |
14 | Bekanntmachung anlegen
15 |
16 |
17 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
36 |
37 |
40 |
--------------------------------------------------------------------------------
/console/src/views/content/CalendarFeedEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Kalender-Abonnement {{ feedId === 'new' ? 'anlegen' : 'bearbeiten' }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {
18 | $data.formError = null
19 | save()
20 | .then(() => $router.push({name: 'calendars'}))
21 | .catch(reason => { $data.formError = reason })
22 | }"
23 | @reset="reset"
24 | >
25 |
26 |
27 |
28 |
29 |
30 |
31 |
61 |
62 |
65 |
--------------------------------------------------------------------------------
/console/src/views/displays/KeyRequestCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
59 |
--------------------------------------------------------------------------------
/console/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | devServer: {
3 | proxy: {
4 | '^/socket\\.io/': {
5 | target: 'http://localhost:3031/',
6 | ws: true
7 | }
8 | }
9 | },
10 | publicPath: process.env.NODE_ENV === 'production' ? '/console/' : '/'
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "node": true
5 | },
6 | "extends": [
7 | "plugin:vue/essential",
8 | "eslint:recommended"
9 | ],
10 | "parserOptions": {
11 | "parser": "@babel/eslint-parser"
12 | },
13 | "reportUnusedDisableDirectives": true,
14 | "rules": {
15 | "vue/multi-word-component-names": "off"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea/workspace.xml
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
--------------------------------------------------------------------------------
/frontend/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/frontend/.idea/frontend.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/frontend/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/frontend/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/frontend/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.19.2
2 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Vue.js Display Frontend
2 |
3 | This is the web application, that runs on each Display unit.
4 | The app maintains a [Socket.IO](https://socket.io/) connection to the [Display Server](../server) to get its configuration and data updates.
5 |
6 | ## Development
7 | In order to run a development version on your local system, you need a [Node.js](https://nodejs.org/) environment.
8 | Clone the repository and run `npm install` inside the project folder to install all the dependencies.
9 |
10 | Start the development server by running `npm run serve`, it will automatically restart when files have changed.
11 | Now you can access the server on http://localhost:8080 (may be a different port on your system, check the console output).
12 | If you run a development Display Server on http://localhost:3031, the requests are automatically proxied there.
13 | This allows for parallel development of the frontend and the backend.
14 |
15 | ### Libraries and frameworks
16 | This project uses the following libraries or frameworks, please refer to their documentation as well.
17 | - [Vue.js](https://vuejs.org/)
18 | - [Vuex](https://vuex.vuejs.org/)
19 | - [FeathersVuex](https://vuex.feathersjs.com/)
20 | - [Font Awesome](https://fontawesome.com/)
21 |
22 | ## Deployment
23 | Run `npm run build`, which compiles and minifies the app for production.
24 | You find the result of the build process in a folder called `dist`.
25 | This folder only contains HTML, CSS, and JS files, which means they can be hosted as static files.
26 | By default, the app expects to be accessible under the path `/display/`.
27 | You can change this behaviour by adapting the `publicPath` in [vue.config.js](vue.config.js).
28 |
--------------------------------------------------------------------------------
/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@alarmdisplay/display-frontend",
3 | "version": "1.0.0-beta.5",
4 | "private": false,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "author": "Andreas Brain",
11 | "license": "AGPL-3.0-only",
12 | "dependencies": {
13 | "@feathersjs/feathers": "4.5.17",
14 | "@feathersjs/socketio-client": "4.5.18",
15 | "@fortawesome/fontawesome-svg-core": "6.7.2",
16 | "@fortawesome/free-solid-svg-icons": "6.7.2",
17 | "@fortawesome/vue-fontawesome": "2.0.10",
18 | "@vue/composition-api": "1.7.2",
19 | "axios": "1.8.4",
20 | "core-js": "3.41.0",
21 | "feathers-vuex": "3.16.0",
22 | "leaflet": "1.9.4",
23 | "moment": "2.30.1",
24 | "socket.io-client": "2.5.0",
25 | "vue": "2.6.14",
26 | "vue-moment": "4.1.0",
27 | "vue2-leaflet": "2.7.1",
28 | "vuex": "3.6.2"
29 | },
30 | "devDependencies": {
31 | "@babel/eslint-parser": "7.27.1",
32 | "@vue/cli-plugin-babel": "5.0.8",
33 | "@vue/cli-plugin-eslint": "5.0.8",
34 | "@vue/cli-service": "5.0.8",
35 | "eslint": "8.57.1",
36 | "eslint-plugin-vue": "9.33.0",
37 | "vue-template-compiler": "2.6.14"
38 | },
39 | "overrides": {
40 | "feathers-vuex": {
41 | "serialize-error": "11.0.3"
42 | }
43 | },
44 | "browserslist": [
45 | "> 1%",
46 | "last 2 versions"
47 | ]
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alarmdisplay/display/833f4b4d27e8292df1f873d1142eba9cad80182d/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Alarmdisplay
9 |
10 |
11 |
12 | We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alarmdisplay/display/833f4b4d27e8292df1f873d1142eba9cad80182d/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/frontend/src/components/AlertBanner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ text }}
5 |
6 |
7 |
8 |
9 |
30 |
31 |
46 |
--------------------------------------------------------------------------------
/frontend/src/components/AlertScreen.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
25 |
26 |
28 |
--------------------------------------------------------------------------------
/frontend/src/components/Clock.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ time }}
4 |
{{ date }}
5 |
6 |
7 |
8 |
41 |
42 |
61 |
--------------------------------------------------------------------------------
/frontend/src/components/ConnectionBanner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Verbunden
5 |
6 |
7 |
8 | Keine Verbindung zum Server
9 |
10 |
11 |
12 |
13 |
24 |
25 |
67 |
--------------------------------------------------------------------------------
/frontend/src/components/DisplaySetup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ socketIsConnected ? 'Display einrichten' : 'Keine Verbindung' }}
4 |
5 |
Dieses Display hat keine Verbindung zum Server.
6 |
Überprüfe bitte, ob der Server läuft.
7 |
Falls das Display und der Server auf zwei verschiedenen Geräten laufen, überprüfe die Netzwerkverbindung.
8 |
9 |
10 |
Die Verbindung mit dem Server wurde erfolgreich hergestellt.
11 |
Als nächstes muss das Display freigeschaltet werden.
12 |
Bearbeite in der Anzeige-Console die Anfrage mit der folgenden ID:
13 |
{{ keyRequestId }}
14 |
15 |
16 |
Die Verbindung mit dem Server wurde erfolgreich hergestellt.
17 |
Allerdings scheint es ein anderes Problem zu geben. Bitte überprüfe die Logdateien.
18 |
19 |
20 |
21 |
22 |
35 |
36 |
49 |
--------------------------------------------------------------------------------
/frontend/src/components/GridView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
38 |
39 |
47 |
--------------------------------------------------------------------------------
/frontend/src/components/GridViewComponent.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
35 |
--------------------------------------------------------------------------------
/frontend/src/components/IdleScreen.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
49 |
50 |
57 |
--------------------------------------------------------------------------------
/frontend/src/components/RemoteImage.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
27 |
28 |
38 |
--------------------------------------------------------------------------------
/frontend/src/components/SplashScreen.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Alarmdisplay
4 |
5 |
6 |
7 |
12 |
13 |
16 |
--------------------------------------------------------------------------------
/frontend/src/components/announcements/Item.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ theDate }}
4 | {{ announcement.title }}
5 | {{ announcement.body }}
6 |
7 |
8 |
9 |
31 |
32 |
60 |
--------------------------------------------------------------------------------
/frontend/src/feathers-client.js:
--------------------------------------------------------------------------------
1 | import feathers from '@feathersjs/feathers'
2 | import socketio from '@feathersjs/socketio-client'
3 | import io from 'socket.io-client'
4 | import feathersVuex from 'feathers-vuex'
5 |
6 | let apiKey = localStorage.getItem('display-api-key');
7 | if (!apiKey || apiKey === '') {
8 | // Make sure the header is not empty, so it gets sent and the authentication process kicks in
9 | apiKey = 'none'
10 | }
11 | let options = {
12 | transportOptions: {
13 | polling: {
14 | extraHeaders: {
15 | 'x-api-key': apiKey
16 | }
17 | }
18 | }
19 | };
20 |
21 | const socket = io(options)
22 |
23 | const feathersClient = feathers()
24 | .configure(socketio(socket))
25 |
26 | export default feathersClient
27 |
28 | // Setting up feathers-vuex
29 | const { makeServicePlugin, BaseModel, models, FeathersVuex } = feathersVuex(
30 | feathersClient,
31 | {
32 | serverAlias: 'api',
33 | idField: 'id',
34 | whitelist: ['$regex', '$options']
35 | }
36 | )
37 |
38 | export { makeServicePlugin, BaseModel, models, FeathersVuex }
39 |
--------------------------------------------------------------------------------
/frontend/src/font-awesome.js:
--------------------------------------------------------------------------------
1 | import { library } from '@fortawesome/fontawesome-svg-core'
2 |
3 | import {
4 | faAnglesUp,
5 | faArrowUp,
6 | faBullhorn,
7 | faCalendar,
8 | faCircleQuestion,
9 | faClock,
10 | faCloud,
11 | faCloudBolt,
12 | faCloudMoon,
13 | faCloudRain,
14 | faCloudSun,
15 | faMoon,
16 | faSnowflake,
17 | faSpinner,
18 | faStopwatch,
19 | faSun,
20 | faTemperatureArrowDown,
21 | faTemperatureArrowUp,
22 | faTemperatureThreeQuarters,
23 | faWind,
24 | } from '@fortawesome/free-solid-svg-icons'
25 |
26 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
27 |
28 | library.add(
29 | faAnglesUp,
30 | faArrowUp,
31 | faBullhorn,
32 | faCalendar,
33 | faCircleQuestion,
34 | faClock,
35 | faCloud,
36 | faCloudBolt,
37 | faCloudMoon,
38 | faCloudRain,
39 | faCloudSun,
40 | faMoon,
41 | faSnowflake,
42 | faSpinner,
43 | faStopwatch,
44 | faSun,
45 | faTemperatureArrowDown,
46 | faTemperatureArrowUp,
47 | faTemperatureThreeQuarters,
48 | faWind,
49 | );
50 |
51 | export default FontAwesomeIcon
52 |
--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueCompositionApi from '@vue/composition-api'
3 | Vue.use(VueCompositionApi)
4 |
5 | import App from './App.vue'
6 | import store from './store'
7 |
8 | import moment from 'moment'
9 | import VueMoment from 'vue-moment';
10 |
11 | // Load Font Awesome
12 | import FontAwesomeIcon from './font-awesome'
13 | Vue.component('font-awesome-icon', FontAwesomeIcon)
14 |
15 | require('moment/locale/de');
16 | Vue.use(VueMoment, { moment });
17 |
18 | import 'leaflet/dist/leaflet.css';
19 |
20 | Vue.config.productionTip = false;
21 |
22 | new Vue({
23 | render: h => h(App),
24 | store,
25 | data: {
26 | minutes: Math.floor(Date.now() / 60000),
27 | seconds: Math.floor(Date.now() / 1000)
28 | },
29 | created: function () {
30 | this.$moment.locale('de');
31 | },
32 | mounted: function () {
33 | setTimeout(this.hideSplashScreen, 3000);
34 | setInterval(this.updateSeconds, 1000);
35 | },
36 | methods: {
37 | updateSeconds: function () {
38 | this.seconds = Math.floor(Date.now() / 1000)
39 | },
40 | hideSplashScreen: function () {
41 | this.$store.commit('setShowSplashScreen', false)
42 | }
43 | },
44 | watch: {
45 | seconds(newValue) {
46 | this.minutes = Math.floor(newValue / 60);
47 | }
48 | }
49 | }).$mount('#app');
50 |
51 |
--------------------------------------------------------------------------------
/frontend/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import feathersClient, { FeathersVuex } from '@/feathers-client'
4 |
5 | import announcements from './services/announcements'
6 | import displays from './services/displays'
7 | import calendarItems from './services/calendar-items'
8 | import contentSlots from './services/content-slots'
9 | import incidents from './services/incidents'
10 | import keyRequests from './services/key-requests'
11 | import locations from './services/locations'
12 | import settings from './services/settings'
13 | import views from "./services/views";
14 | import socket, { createSocketPlugin } from "./socket";
15 |
16 | Vue.use(Vuex)
17 | Vue.use(FeathersVuex)
18 |
19 | export default new Vuex.Store({
20 | state: {
21 | ownDisplayId: undefined,
22 | showSplashScreen: true
23 | },
24 | mutations: {
25 | setOwnDisplayId: (state, payload) => {
26 | state.ownDisplayId = payload
27 | },
28 | setShowSplashScreen: (state, payload) => {
29 | state.showSplashScreen = payload
30 | }
31 | },
32 | actions: {
33 | },
34 | modules: {
35 | socket
36 | },
37 | plugins: [
38 | createSocketPlugin(feathersClient.io),
39 | announcements,
40 | calendarItems,
41 | contentSlots,
42 | displays,
43 | incidents,
44 | keyRequests,
45 | locations,
46 | settings,
47 | views
48 | ]
49 | })
50 |
--------------------------------------------------------------------------------
/frontend/src/store/services/announcements.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class Announcement extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'Announcement'
9 |
10 | static instanceDefaults() {
11 | return {
12 | title: '',
13 | body: '',
14 | important: false,
15 | validFrom: null,
16 | validTo: null
17 | }
18 | }
19 |
20 | static setupInstance(data) {
21 | // Convert date strings into Date objects
22 | for (const prop of ['validFrom', 'validTo', 'updatedAt']) {
23 | if (data[prop]) {
24 | data[prop] = new Date(data[prop])
25 | }
26 | }
27 |
28 | return data
29 | }
30 | }
31 |
32 | const servicePath = 'api/v1/announcements'
33 | const servicePlugin = makeServicePlugin({
34 | Model: Announcement,
35 | service: feathersClient.service(servicePath),
36 | servicePath
37 | })
38 |
39 | // Setup the client-side Feathers hooks.
40 | feathersClient.service(servicePath).hooks({
41 | before: {
42 | all: [],
43 | find: [],
44 | get: [],
45 | create: [],
46 | update: [],
47 | patch: [],
48 | remove: []
49 | },
50 | after: {
51 | all: [],
52 | find: [],
53 | get: [],
54 | create: [],
55 | update: [],
56 | patch: [],
57 | remove: []
58 | },
59 | error: {
60 | all: [],
61 | find: [],
62 | get: [],
63 | create: [],
64 | update: [],
65 | patch: [],
66 | remove: []
67 | }
68 | })
69 |
70 | export default servicePlugin
71 |
--------------------------------------------------------------------------------
/frontend/src/store/services/calendar-items.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class CalendarItem extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'CalendarItem'
9 |
10 | static instanceDefaults() {
11 | return {
12 | uid: '',
13 | summary: '',
14 | startDate: null,
15 | endDate: null,
16 | description: '',
17 | status: 'confirmed',
18 | allDayEvent: false,
19 | feedId: 0,
20 | }
21 | }
22 |
23 | static setupInstance(data) {
24 | // Convert date strings into Date objects
25 | for (const prop of ['startDate', 'endDate']) {
26 | if (data[prop]) {
27 | data[prop] = new Date(data[prop])
28 | }
29 | }
30 |
31 | return data
32 | }
33 | }
34 |
35 | const servicePath = 'calendar-items'
36 | const servicePlugin = makeServicePlugin({
37 | Model: CalendarItem,
38 | idField: 'uid',
39 | service: feathersClient.service(servicePath),
40 | servicePath
41 | })
42 |
43 | // Setup the client-side Feathers hooks.
44 | feathersClient.service(servicePath).hooks({
45 | before: {
46 | all: [],
47 | find: [],
48 | get: [],
49 | create: [],
50 | update: [],
51 | patch: [],
52 | remove: []
53 | },
54 | after: {
55 | all: [],
56 | find: [],
57 | get: [],
58 | create: [],
59 | update: [],
60 | patch: [],
61 | remove: []
62 | },
63 | error: {
64 | all: [],
65 | find: [],
66 | get: [],
67 | create: [],
68 | update: [],
69 | patch: [],
70 | remove: []
71 | }
72 | })
73 |
74 | export default servicePlugin
75 |
--------------------------------------------------------------------------------
/frontend/src/store/services/content-slots.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class ContentSlot extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'ContentSlot'
9 |
10 | static instanceDefaults() {
11 | return {
12 | component: 'Clock',
13 | columnStart: 1,
14 | columnEnd: 1,
15 | rowStart: 2,
16 | rowEnd: 2,
17 | options: {}
18 | }
19 | }
20 | }
21 |
22 | const servicePath = 'api/v1/content-slots'
23 | const servicePlugin = makeServicePlugin({
24 | Model: ContentSlot,
25 | service: feathersClient.service(servicePath),
26 | servicePath
27 | })
28 |
29 | // Setup the client-side Feathers hooks.
30 | feathersClient.service(servicePath).hooks({
31 | before: {
32 | all: [],
33 | find: [],
34 | get: [],
35 | create: [],
36 | update: [],
37 | patch: [],
38 | remove: []
39 | },
40 | after: {
41 | all: [],
42 | find: [],
43 | get: [],
44 | create: [],
45 | update: [],
46 | patch: [],
47 | remove: []
48 | },
49 | error: {
50 | all: [],
51 | find: [],
52 | get: [],
53 | create: [],
54 | update: [],
55 | patch: [],
56 | remove: []
57 | }
58 | })
59 |
60 | export default servicePlugin
61 |
--------------------------------------------------------------------------------
/frontend/src/store/services/key-requests.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class KeyRequest extends BaseModel {
4 | static modelName = 'KeyRequest'
5 |
6 | static instanceDefaults () {
7 | return {
8 | requestId: '',
9 | name: '',
10 | granted: false,
11 | apiKey: ''
12 | }
13 | }
14 | }
15 |
16 | const servicePath = 'api/v1/key-requests'
17 | const servicePlugin = makeServicePlugin({
18 | Model: KeyRequest,
19 | service: feathersClient.service(servicePath),
20 | servicePath
21 | })
22 |
23 | // Setup the client-side Feathers hooks.
24 | feathersClient.service(servicePath).hooks({
25 | before: {
26 | all: [],
27 | find: [],
28 | get: [],
29 | create: [],
30 | update: [],
31 | patch: [],
32 | remove: []
33 | },
34 | after: {
35 | all: [],
36 | find: [],
37 | get: [],
38 | create: [],
39 | update: [],
40 | patch: [],
41 | remove: []
42 | },
43 | error: {
44 | all: [],
45 | find: [],
46 | get: [],
47 | create: [],
48 | update: [],
49 | patch: [],
50 | remove: []
51 | }
52 | })
53 |
54 | export default servicePlugin
55 |
--------------------------------------------------------------------------------
/frontend/src/store/services/locations.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class Location extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'Location'
9 |
10 | static instanceDefaults() {
11 | return {
12 | rawText: '',
13 | latitude: undefined,
14 | longitude: undefined,
15 | name: '',
16 | street: '',
17 | number: '',
18 | detail: '',
19 | municipality: '',
20 | district: '',
21 | incidentId: undefined
22 | }
23 | }
24 | }
25 |
26 | const servicePath = 'api/v1/locations'
27 | const servicePlugin = makeServicePlugin({
28 | Model: Location,
29 | service: feathersClient.service(servicePath),
30 | servicePath
31 | })
32 |
33 | // Setup the client-side Feathers hooks.
34 | feathersClient.service(servicePath).hooks({
35 | before: {
36 | all: [],
37 | find: [],
38 | get: [],
39 | create: [],
40 | update: [],
41 | patch: [],
42 | remove: []
43 | },
44 | after: {
45 | all: [],
46 | find: [],
47 | get: [],
48 | create: [],
49 | update: [],
50 | patch: [],
51 | remove: []
52 | },
53 | error: {
54 | all: [],
55 | find: [],
56 | get: [],
57 | create: [],
58 | update: [],
59 | patch: [],
60 | remove: []
61 | }
62 | })
63 |
64 | export default servicePlugin
65 |
--------------------------------------------------------------------------------
/frontend/src/store/services/settings.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { BaseModel, makeServicePlugin } from '../../feathers-client'
2 |
3 | class Setting extends BaseModel {
4 | static modelName = 'Setting'
5 |
6 | static instanceDefaults () {
7 | return {
8 | key: '',
9 | value: null
10 | }
11 | }
12 | }
13 |
14 | const servicePath = 'api/v1/settings'
15 | const servicePlugin = makeServicePlugin({
16 | Model: Setting,
17 | idField: 'key',
18 | getters: {
19 | getIntegerValue: (state, getters) => (id, params) => {
20 | const value = getters.getValue(id, params)
21 | return Number.parseInt(value)
22 | },
23 | getLeafletCoords: (state, getters) => (id, params) => {
24 | const value = getters.getValue(id, params)
25 | if (!value || !value.latitude || !value.longitude) {
26 | return null
27 | }
28 |
29 | return [value.latitude, value.longitude]
30 | },
31 | getRegExp: (state, getters) => (id, params) => {
32 | const value = getters.getValue(id, params)
33 | try {
34 | return RegExp(value)
35 | } catch (e) {
36 | return null
37 | }
38 | },
39 | getStringValue: (state, getters) => (id, params) => {
40 | const value = getters.getValue(id, params)
41 | return value ? String(value) : ''
42 | },
43 | getValue: (state, getters) => (id, params) => {
44 | const setting = getters.get(id, params)
45 | return setting?.value
46 | }
47 | },
48 | service: feathersClient.service(servicePath),
49 | servicePath
50 | })
51 |
52 | // Setup the client-side Feathers hooks.
53 | feathersClient.service(servicePath).hooks({
54 | before: {
55 | all: [],
56 | find: [],
57 | get: [],
58 | create: [],
59 | update: [],
60 | patch: [],
61 | remove: []
62 | },
63 | after: {
64 | all: [],
65 | find: [],
66 | get: [],
67 | create: [],
68 | update: [],
69 | patch: [],
70 | remove: []
71 | },
72 | error: {
73 | all: [],
74 | find: [],
75 | get: [],
76 | create: [],
77 | update: [],
78 | patch: [],
79 | remove: []
80 | }
81 | })
82 |
83 | export default servicePlugin
84 |
--------------------------------------------------------------------------------
/frontend/src/store/services/views.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class View extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'View'
9 |
10 | static instanceDefaults() {
11 | return {
12 | type: 'idle',
13 | order: 999,
14 | columns: 3,
15 | rows: 3,
16 | active: true,
17 | pinned: false,
18 | contentSlots: []
19 | }
20 | }
21 |
22 | static setupInstance(data, { models }) {
23 | // Add nested content slot objects to storage
24 | if (data.contentSlots && Array.isArray(data.contentSlots)) {
25 | data.contentSlots.forEach(contentSlot => new models.api.ContentSlot(contentSlot))
26 | }
27 |
28 | data.active = data.active === 1;
29 | data.pinned = data.pinned === 1;
30 |
31 | // Replace the nested content slots with a getter
32 | Object.defineProperty(data, 'contentSlots', {
33 | get: function () {
34 | const contentSlots = models.api.ContentSlot.findInStore({
35 | query: {
36 | viewId: data.id
37 | }
38 | })
39 | return contentSlots.data
40 | },
41 | configurable: true,
42 | enumerable: true
43 | })
44 |
45 | return data
46 | }
47 | }
48 |
49 | const servicePath = 'api/v1/views'
50 | const servicePlugin = makeServicePlugin({
51 | Model: View,
52 | service: feathersClient.service(servicePath),
53 | servicePath
54 | })
55 |
56 | // Setup the client-side Feathers hooks.
57 | feathersClient.service(servicePath).hooks({
58 | before: {
59 | all: [],
60 | find: [],
61 | get: [],
62 | create: [],
63 | update: [],
64 | patch: [],
65 | remove: []
66 | },
67 | after: {
68 | all: [],
69 | find: [],
70 | get: [],
71 | create: [],
72 | update: [],
73 | patch: [],
74 | remove: []
75 | },
76 | error: {
77 | all: [],
78 | find: [],
79 | get: [],
80 | create: [],
81 | update: [],
82 | patch: [],
83 | remove: []
84 | }
85 | })
86 |
87 | export default servicePlugin
88 |
--------------------------------------------------------------------------------
/frontend/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | devServer: {
3 | proxy: {
4 | '^/socket\\.io/': {
5 | target: 'http://localhost:3031/',
6 | ws: true
7 | }
8 | }
9 | },
10 | publicPath: process.env.NODE_ENV === 'production' ? '/display/' : '/'
11 | };
12 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:best-practices",
5 | "group:linters",
6 | "group:test",
7 | "npm:unpublishSafe",
8 | "schedule:automergeNonOfficeHours",
9 | ":approveMajorUpdates",
10 | ":automergeLinters",
11 | ":automergeTesters",
12 | ":maintainLockFilesWeekly",
13 | ":pinAllExceptPeerDependencies",
14 | ":semanticCommits",
15 | ":separateMultipleMajorReleases"
16 | ],
17 | "packageRules": [
18 | {
19 | "matchPackageNames": [
20 | "typescript"
21 | ],
22 | "groupName": "TypeScript",
23 | "separateMultipleMinor": true
24 | },
25 | {
26 | "matchPackageNames": [
27 | "vue",
28 | "vue-template-compiler"
29 | ],
30 | "allowedVersions": "< 2.7"
31 | }
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # The root directory of the project is one up
4 | cd "$(dirname $0)/.."
5 | PROJECT_DIR=$PWD
6 | echo "Project root is $PROJECT_DIR"
7 |
8 | if [[ -d build ]]; then
9 | echo "Removing old build folder..."
10 | rm -r build
11 | fi
12 |
13 | echo "Building server ..."
14 | cd "$PROJECT_DIR/server"
15 | npm ci
16 | npm run compile || exit
17 | cp package.json lib/
18 | cp package-lock.json lib/
19 | cp -r public lib/
20 |
21 | mkdir lib/config
22 | cp config/default.json lib/config/
23 | cp config/docker.json lib/config/
24 | cp config/production.json lib/config/
25 |
26 | mv lib "$PROJECT_DIR/build"
27 |
28 | echo "Building console ..."
29 | cd "$PROJECT_DIR/console"
30 | npm ci
31 | npm run build || exit
32 | mv dist "$PROJECT_DIR/build/ext-console"
33 |
34 | echo "Building frontend ..."
35 | cd "$PROJECT_DIR/frontend"
36 | npm ci
37 | npm run build || exit
38 | mv dist "$PROJECT_DIR/build/ext-display"
39 |
40 | cd "$PROJECT_DIR"
41 | cp LICENSE build/
42 |
--------------------------------------------------------------------------------
/server/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | plugins:
3 | eslint:
4 | enabled: true
5 | channel: "eslint-6"
6 | exclude_patterns:
7 | - ".idea/"
8 | - ".*.json"
9 | - ".*.yml"
10 | - ".*ignore"
11 | - "node_modules/"
12 | - "__tests__/"
13 | - "*.js"
14 | - "*.json"
15 |
--------------------------------------------------------------------------------
/server/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/server/.env.example:
--------------------------------------------------------------------------------
1 | DEBUG=1
2 | DB_HOST=localhost
3 | DB_USER=ad_display
4 | DB_PASSWORD=
5 | DB_NAME=ad_display
6 | #PORT=3000
7 |
--------------------------------------------------------------------------------
/server/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/server/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/server/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/server/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/server/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/server/.idea/server.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/server/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/server/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.19.2
2 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # Display Server
2 |
3 | The backend provides a REST API as well as WebSocket connections to notify clients about updates.
4 | In production, it also serves the [Console](../console) and [Display](../frontend) frontends.
5 |
6 | ## Development
7 | In order to run a development version on your local system, you need a [Node.js](https://nodejs.org/) environment, and a MariaDB instance.
8 | - Clone the repository and run `npm install` in this folder to install all the dependencies.
9 | - In the `config/` folder, copy the file `development.json` to `local-development.json`.
10 | - At least set the `mysql` property of `local-development.json` to set up the database connection (e.g. `mysql://user:password@localhost:3306/database`).
11 |
12 | Start the development server by running `npm run dev`, it will automatically restart when files have changed.
13 | Now you can access the server on http://localhost:3031.
14 |
15 | Before the first run, and whenever you work on database migration scripts, you'll have to run `npm run start` once.
16 | Unfortunately, running `npm run dev` does not take the migration scripts into account.
17 |
18 | ### Libraries and frameworks
19 | This project uses the following libraries or frameworks, please refer to their documentation as well.
20 | - [FeathersJS](https://feathersjs.com/)
21 |
22 | ## Deployment
23 | At the moment, this project is not ready for deployment outside of a development environment.
24 |
--------------------------------------------------------------------------------
/server/config/ci.json:
--------------------------------------------------------------------------------
1 | {
2 | "dbConfig": {
3 | "dialect": "mysql",
4 | "connection": "MYSQL_URI"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/server/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "host": "localhost",
3 | "port": 3031,
4 | "public": "../public/",
5 | "paginate": {
6 | "default": 10,
7 | "max": 50
8 | },
9 | "authentication": {
10 | "entity": "user",
11 | "service": "users",
12 | "secret": "L6dLJEWc23hFtMrxXJJjxgbD37o=",
13 | "authStrategies": [
14 | "jwt",
15 | "local"
16 | ],
17 | "jwtOptions": {
18 | "header": {
19 | "typ": "access"
20 | },
21 | "issuer": "Alarmdisplay Display",
22 | "algorithm": "HS256",
23 | "expiresIn": "1d"
24 | },
25 | "local": {
26 | "usernameField": "email",
27 | "passwordField": "password"
28 | }
29 | },
30 | "dbConfig": {
31 | "dialect": "mysql",
32 | "connection": "MYSQL_URI"
33 | },
34 | "logging": {
35 | "level": "info"
36 | },
37 | "dbMaxRetries": 5,
38 | "hub_host": "",
39 | "hub_api_key": ""
40 | }
41 |
--------------------------------------------------------------------------------
/server/config/development.json:
--------------------------------------------------------------------------------
1 | {
2 | "logging": {
3 | "level": "debug"
4 | },
5 | "dbConfig": {
6 | "dialect": "sqlite3",
7 | "connection": {
8 | "filename": "src/db.sqlite"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/server/config/docker.json:
--------------------------------------------------------------------------------
1 | {
2 | "dbConfig": {
3 | "dialect": "mysql",
4 | "connection": "MYSQL_URI"
5 | },
6 | "hub_host": "HUB_HOST",
7 | "hub_api_key": "HUB_API_KEY",
8 | "logging": {
9 | "level": "LOG_LEVEL"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/server/config/production.json:
--------------------------------------------------------------------------------
1 | {
2 | }
3 |
--------------------------------------------------------------------------------
/server/config/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "logging": {
3 | "level": "error"
4 | },
5 | "port": 8998,
6 | "dbConfig": {
7 | "dialect": "sqlite",
8 | "connection": "sqlite::memory:"
9 | },
10 | "dbMaxRetries": 10
11 | }
12 |
--------------------------------------------------------------------------------
/server/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from 'globals';
2 | import eslint from '@eslint/js';
3 | import typescriptEslint from 'typescript-eslint';
4 |
5 | export default typescriptEslint.config(
6 | eslint.configs.recommended,
7 | ...typescriptEslint.configs.recommended,
8 | {
9 | files: [ '**/*.js', '**/*.ts' ],
10 | ignores: ['**/.idea/', '**/jest.config.js'],
11 | },
12 | {
13 | languageOptions: {
14 | globals: {
15 | ...globals.node,
16 | },
17 | ecmaVersion: 2018,
18 | sourceType: 'module',
19 | },
20 | rules: {
21 | indent: ['error', 2],
22 | 'linebreak-style': ['error', 'unix'],
23 | quotes: ['error', 'single'],
24 | semi: ['error', 'always'],
25 | '@typescript-eslint/no-explicit-any': 'off',
26 | '@typescript-eslint/no-empty-interface': 'off',
27 | },
28 | }
29 | );
30 |
--------------------------------------------------------------------------------
/server/jest.config.js:
--------------------------------------------------------------------------------
1 | const { dirname } = require('path');
2 |
3 | module.exports = {
4 | preset: 'ts-jest',
5 | testEnvironment: 'node',
6 | coverageDirectory: "coverage",
7 | coverageReporters: [["lcovonly", {"projectRoot": dirname(__dirname)}], ["text", {"skipFull": true}]],
8 | testSequencer: "./test/testSequencer.js",
9 | transform: {
10 | '^.+\\.tsx?$': [
11 | 'ts-jest',
12 | {
13 | diagnostics: false,
14 | },
15 | ],
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alarmdisplay/display/833f4b4d27e8292df1f873d1142eba9cad80182d/server/public/favicon.ico
--------------------------------------------------------------------------------
/server/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Alarmdisplay
7 |
8 |
9 |
10 | Alarmdisplay
11 | Es gibt zwei Anwendungen, die von diesem Server bereitgestellt werden:
12 | Display
13 | Alarmanzeige
14 | Zum Display
15 |
16 | Console
17 | Verwaltung von Displays und deren Inhalten
18 | Zur Console
19 |
20 |
21 |
--------------------------------------------------------------------------------
/server/public/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Open Sans", sans-serif;
3 | }
4 |
--------------------------------------------------------------------------------
/server/src/app.hooks.ts:
--------------------------------------------------------------------------------
1 | // Application hooks that run for every service
2 | // Don't remove this comment. It's needed to format import lines nicely.
3 |
4 | export default {
5 | before: {
6 | all: [],
7 | find: [],
8 | get: [],
9 | create: [],
10 | update: [],
11 | patch: [],
12 | remove: []
13 | },
14 |
15 | after: {
16 | all: [],
17 | find: [],
18 | get: [],
19 | create: [],
20 | update: [],
21 | patch: [],
22 | remove: []
23 | },
24 |
25 | error: {
26 | all: [],
27 | find: [],
28 | get: [],
29 | create: [],
30 | update: [],
31 | patch: [],
32 | remove: []
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/server/src/authentication.ts:
--------------------------------------------------------------------------------
1 | import { ServiceAddons } from '@feathersjs/feathers';
2 | import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication';
3 | import { LocalStrategy } from '@feathersjs/authentication-local';
4 | import { ApiKeyStrategy } from './auth-strategies/api-key.strategy';
5 |
6 | import { Application } from './declarations';
7 |
8 | declare module './declarations' {
9 | interface ServiceTypes {
10 | 'authentication': AuthenticationService & ServiceAddons;
11 | }
12 | }
13 |
14 | export default function(app: Application): void {
15 | const authentication = new AuthenticationService(app);
16 |
17 | authentication.register('jwt', new JWTStrategy());
18 | authentication.register('local', new LocalStrategy());
19 | authentication.register('api-key', new ApiKeyStrategy());
20 |
21 | app.use('/authentication', authentication);
22 | }
23 |
--------------------------------------------------------------------------------
/server/src/declarations.d.ts:
--------------------------------------------------------------------------------
1 | import { Application as ExpressFeathers } from '@feathersjs/express';
2 |
3 | // A mapping of service names to types. Will be extended in service files.
4 | export interface ServiceTypes {} // eslint-disable-line @typescript-eslint/no-empty-object-type
5 | // The application instance type that will be used everywhere else
6 | export type Application = ExpressFeathers;
7 |
--------------------------------------------------------------------------------
/server/src/hooks/allowApiKey.ts:
--------------------------------------------------------------------------------
1 | import {HookContext} from '@feathersjs/feathers';
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
4 | export function allowApiKey(options = {}) {
5 | return async (context: HookContext): Promise => {
6 | const { params } = context;
7 |
8 | // Stop, if it is an internal call or another authentication has been performed already
9 | if (!params.provider || params.authentication) {
10 | return context;
11 | }
12 |
13 | // Extract the API key from the request
14 | if(params.headers && params.headers['x-api-key']) {
15 | context.params = {
16 | ...params,
17 | authentication: {
18 | strategy: 'api-key',
19 | 'api-key': params.headers['x-api-key']
20 | }
21 | };
22 | }
23 |
24 | return context;
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/server/src/hooks/unserializeJson.ts:
--------------------------------------------------------------------------------
1 | import { HookContext } from '@feathersjs/feathers';
2 | import { getItems, replaceItems } from 'feathers-hooks-common';
3 |
4 | export function unserializeJson (property: string) {
5 | return (context: HookContext): HookContext => {
6 | const items = getItems(context);
7 | if (Array.isArray(items)) {
8 | items.forEach(item => {
9 | if (typeof item[property] === 'string') {
10 | item[property] = item[property].length === 0 ? null : JSON.parse(item[property]);
11 | }
12 | });
13 | } else {
14 | if (typeof items[property] === 'string') {
15 | items[property] = items[property].length === 0 ? null : JSON.parse(items[property]);
16 | }
17 | }
18 |
19 | replaceItems(context, items);
20 | return context;
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/server/src/index.ts:
--------------------------------------------------------------------------------
1 | import app from './app';
2 | import logger from './logger';
3 |
4 | const port = app.get('port');
5 | const server = app.listen(port);
6 |
7 | process.on('unhandledRejection', (reason, p) =>
8 | logger.error('Unhandled Rejection at: Promise ', p, reason)
9 | );
10 |
11 | server.on('listening', () =>
12 | logger.info('Display Backend started on http://%s:%d', app.get('host'), port)
13 | );
14 |
--------------------------------------------------------------------------------
/server/src/logger.ts:
--------------------------------------------------------------------------------
1 | import { getLogger as getLog4JsLogger } from 'log4js';
2 |
3 | const logger = getLogger();
4 |
5 | // Set fallback level, should be overridden by config
6 | logger.level = 'info';
7 |
8 | export default logger;
9 |
10 | export function getLogger(category?: string) {
11 | return getLog4JsLogger(category);
12 | }
13 |
--------------------------------------------------------------------------------
/server/src/middleware/index.ts:
--------------------------------------------------------------------------------
1 | import { Application } from '../declarations';
2 | // Don't remove this comment. It's needed to format import lines nicely.
3 |
4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
5 | export default function (app: Application): void {
6 | }
7 |
--------------------------------------------------------------------------------
/server/src/migrations/00000_create-users.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.createTable('users', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true
11 | },
12 | email: {
13 | type: Sequelize.STRING,
14 | allowNull: false,
15 | unique: true
16 | },
17 | name: {
18 | type: Sequelize.STRING,
19 | allowNull: false,
20 | defaultValue: ''
21 | },
22 | password: {
23 | type: Sequelize.STRING,
24 | allowNull: false
25 | },
26 | createdAt: {
27 | type: Sequelize.DATE,
28 | allowNull: false
29 | },
30 | updatedAt: {
31 | type: Sequelize.DATE,
32 | allowNull: false
33 | }
34 | });
35 | };
36 |
37 | export const down: Migration = async ({context: {query}}) => {
38 | await query.dropTable('users');
39 | };
40 |
--------------------------------------------------------------------------------
/server/src/migrations/00001_create-displays.ts:
--------------------------------------------------------------------------------
1 | import Sequelize, {DataTypes} from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.createTable('displays', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true
11 | },
12 | name: {
13 | type: DataTypes.STRING,
14 | allowNull: false
15 | },
16 | active: {
17 | type: DataTypes.BOOLEAN,
18 | allowNull: false,
19 | defaultValue: false
20 | },
21 | description: {
22 | type: DataTypes.STRING,
23 | allowNull: false,
24 | defaultValue: ''
25 | },
26 | createdAt: {
27 | type: Sequelize.DATE,
28 | allowNull: false
29 | },
30 | updatedAt: {
31 | type: Sequelize.DATE,
32 | allowNull: false
33 | }
34 | });
35 | };
36 | export const down: Migration = async ({context: {query}}) => {
37 | await query.dropTable('displays');
38 | };
39 |
--------------------------------------------------------------------------------
/server/src/migrations/00002_create-views.ts:
--------------------------------------------------------------------------------
1 | import Sequelize, {DataTypes} from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'views';
6 |
7 | try {
8 | await query.describeTable(tableName);
9 | // Exit early if the table exists
10 | return;
11 | } catch {
12 | // The table does not exist, so we just continue
13 | }
14 |
15 | await query.createTable(tableName, {
16 | id: {
17 | type: Sequelize.INTEGER,
18 | allowNull: false,
19 | autoIncrement: true,
20 | primaryKey: true
21 | },
22 | type: {
23 | type: DataTypes.STRING,
24 | allowNull: false
25 | },
26 | order: {
27 | type: DataTypes.MEDIUMINT,
28 | allowNull: false
29 | },
30 | columns: {
31 | type: DataTypes.TINYINT,
32 | allowNull: false
33 | },
34 | rows: {
35 | type: DataTypes.TINYINT,
36 | allowNull: false
37 | },
38 | displayId: {
39 | type: Sequelize.INTEGER,
40 | allowNull: false
41 | },
42 | createdAt: {
43 | type: Sequelize.DATE,
44 | allowNull: false
45 | },
46 | updatedAt: {
47 | type: Sequelize.DATE,
48 | allowNull: false
49 | }
50 | });
51 |
52 | await query.addIndex(tableName, {
53 | name: 'displayId',
54 | fields: ['displayId']
55 | });
56 |
57 | await query.addConstraint(tableName, {
58 | name: `${tableName}_ibfk_1`,
59 | type: 'foreign key',
60 | fields: ['displayId'],
61 | references: { table: 'displays', field: 'id' },
62 | onDelete: 'CASCADE',
63 | onUpdate: 'CASCADE'
64 | });
65 | };
66 | export const down: Migration = async ({context: {query}}) => {
67 | await query.dropTable('views');
68 | };
69 |
--------------------------------------------------------------------------------
/server/src/migrations/00003_create-content-slots.ts:
--------------------------------------------------------------------------------
1 | import Sequelize, {DataTypes} from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'content_slots';
6 |
7 | try {
8 | await query.describeTable(tableName);
9 | // Exit early if the table exists
10 | return;
11 | } catch {
12 | // The table does not exist, so we just continue
13 | }
14 |
15 | await query.createTable(tableName, {
16 | id: {
17 | type: Sequelize.INTEGER,
18 | allowNull: false,
19 | autoIncrement: true,
20 | primaryKey: true
21 | },
22 | component: {
23 | type: DataTypes.STRING,
24 | allowNull: false
25 | },
26 | columnStart: {
27 | type: DataTypes.TINYINT,
28 | allowNull: false
29 | },
30 | columnEnd: {
31 | type: DataTypes.TINYINT,
32 | allowNull: false
33 | },
34 | rowStart: {
35 | type: DataTypes.TINYINT,
36 | allowNull: false
37 | },
38 | rowEnd: {
39 | type: DataTypes.TINYINT,
40 | allowNull: false
41 | },
42 | createdAt: {
43 | type: Sequelize.DATE,
44 | allowNull: false
45 | },
46 | updatedAt: {
47 | type: Sequelize.DATE,
48 | allowNull: false
49 | },
50 | viewId: {
51 | type: Sequelize.INTEGER,
52 | allowNull: false
53 | }
54 | });
55 |
56 | await query.addIndex(tableName, {
57 | name: 'viewId',
58 | fields: ['viewId']
59 | });
60 |
61 | await query.addConstraint(tableName, {
62 | name: `${tableName}_ibfk_1`,
63 | type: 'foreign key',
64 | fields: ['viewId'],
65 | references: { table: 'views', field: 'id' },
66 | onDelete: 'CASCADE',
67 | onUpdate: 'CASCADE'
68 | });
69 | };
70 | export const down: Migration = async ({context: {query}}) => {
71 | await query.dropTable('content_slots');
72 | };
73 |
--------------------------------------------------------------------------------
/server/src/migrations/00004_create-content-slot-options.ts:
--------------------------------------------------------------------------------
1 | import Sequelize, {DataTypes} from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'content_slot_options';
6 |
7 | try {
8 | await query.describeTable(tableName);
9 | // Exit early if the table exists
10 | return;
11 | } catch {
12 | // The table does not exist, so we just continue
13 | }
14 |
15 | await query.createTable(tableName, {
16 | id: {
17 | type: Sequelize.INTEGER,
18 | allowNull: false,
19 | autoIncrement: true,
20 | primaryKey: true
21 | },
22 | key: {
23 | type: DataTypes.STRING,
24 | allowNull: false
25 | },
26 | value: {
27 | type: DataTypes.STRING,
28 | allowNull: false
29 | },
30 | createdAt: {
31 | type: Sequelize.DATE,
32 | allowNull: false
33 | },
34 | updatedAt: {
35 | type: Sequelize.DATE,
36 | allowNull: false
37 | },
38 | contentSlotId: {
39 | type: Sequelize.INTEGER,
40 | allowNull: false
41 | }
42 | });
43 |
44 | await query.addIndex(tableName, {
45 | name: 'contentSlotId',
46 | fields: ['contentSlotId']
47 | });
48 |
49 | await query.addConstraint(tableName, {
50 | name: `${tableName}_ibfk_1`,
51 | type: 'foreign key',
52 | fields: ['contentSlotId'],
53 | references: { table: 'content_slots', field: 'id' },
54 | onDelete: 'CASCADE',
55 | onUpdate: 'CASCADE'
56 | });
57 | };
58 | export const down: Migration = async ({context: {query}}) => {
59 | await query.dropTable('content_slot_options');
60 | };
61 |
--------------------------------------------------------------------------------
/server/src/migrations/00005_create-api-keys.ts:
--------------------------------------------------------------------------------
1 | import Sequelize, {DataTypes} from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'api_keys';
6 |
7 | try {
8 | await query.describeTable(tableName);
9 | // Exit early if the table exists
10 | return;
11 | } catch {
12 | // The table does not exist, so we just continue
13 | }
14 |
15 | await query.createTable(tableName, {
16 | id: {
17 | type: Sequelize.INTEGER,
18 | allowNull: false,
19 | autoIncrement: true,
20 | primaryKey: true
21 | },
22 | name: {
23 | type: DataTypes.STRING,
24 | allowNull: false
25 | },
26 | tokenHash: {
27 | type: DataTypes.STRING,
28 | allowNull: false,
29 | unique: true
30 | },
31 | createdAt: {
32 | type: Sequelize.DATE,
33 | allowNull: false
34 | },
35 | updatedAt: {
36 | type: Sequelize.DATE,
37 | allowNull: false
38 | },
39 | displayId: {
40 | type: Sequelize.INTEGER,
41 | allowNull: true
42 | }
43 | });
44 |
45 | await query.addIndex(tableName, {
46 | name: 'displayId',
47 | fields: ['displayId']
48 | });
49 |
50 | await query.addConstraint(tableName, {
51 | name: `${tableName}_ibfk_1`,
52 | type: 'foreign key',
53 | fields: ['displayId'],
54 | references: { table: 'displays', field: 'id' },
55 | onDelete: 'CASCADE',
56 | onUpdate: 'CASCADE'
57 | });
58 | };
59 | export const down: Migration = async ({context: {query}}) => {
60 | await query.dropTable('api_keys');
61 | };
62 |
--------------------------------------------------------------------------------
/server/src/migrations/00006_create-incidents.ts:
--------------------------------------------------------------------------------
1 | import Sequelize, {DataTypes} from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.createTable('incidents', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true
11 | },
12 | time: {
13 | type: DataTypes.DATE,
14 | allowNull: false,
15 | defaultValue: DataTypes.NOW
16 | },
17 | sender: {
18 | type: DataTypes.STRING,
19 | allowNull: false,
20 | defaultValue: ''
21 | },
22 | ref: {
23 | type: DataTypes.STRING,
24 | allowNull: false,
25 | defaultValue: ''
26 | },
27 | caller_name: {
28 | type: DataTypes.STRING,
29 | allowNull: false,
30 | defaultValue: ''
31 | },
32 | caller_number: {
33 | type: DataTypes.STRING,
34 | allowNull: false,
35 | defaultValue: ''
36 | },
37 | reason: {
38 | type: DataTypes.STRING,
39 | allowNull: false,
40 | defaultValue: ''
41 | },
42 | keyword: {
43 | type: DataTypes.STRING,
44 | allowNull: false,
45 | defaultValue: ''
46 | },
47 | description: {
48 | type: DataTypes.STRING,
49 | allowNull: false,
50 | defaultValue: ''
51 | },
52 | status: {
53 | type: DataTypes.ENUM,
54 | values: ['Actual', 'Exercise', 'Test'],
55 | allowNull: false,
56 | defaultValue: 'Actual'
57 | },
58 | category: {
59 | type: DataTypes.ENUM,
60 | values: ['Geo', 'Met', 'Safety', 'Security', 'Rescue', 'Fire', 'Health', 'Env', 'Transport', 'Infra', 'CBRNE', 'Other'],
61 | allowNull: false,
62 | defaultValue: 'Other'
63 | },
64 | hubIncidentId: {
65 | type: DataTypes.INTEGER,
66 | allowNull: true
67 | },
68 | createdAt: {
69 | type: Sequelize.DATE,
70 | allowNull: false
71 | },
72 | updatedAt: {
73 | type: Sequelize.DATE,
74 | allowNull: false
75 | }
76 | });
77 | };
78 | export const down: Migration = async ({context: {query}}) => {
79 | await query.dropTable('incidents');
80 | };
81 |
--------------------------------------------------------------------------------
/server/src/migrations/00008_create-announcements.ts:
--------------------------------------------------------------------------------
1 | import Sequelize, {DataTypes} from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.createTable('announcements', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true
11 | },
12 | title: {
13 | type: DataTypes.STRING,
14 | allowNull: false
15 | },
16 | body: {
17 | type: DataTypes.STRING,
18 | allowNull: true
19 | },
20 | important: {
21 | type: DataTypes.BOOLEAN,
22 | allowNull: false,
23 | defaultValue: false
24 | },
25 | validFrom: {
26 | type: DataTypes.DATE,
27 | allowNull: true
28 | },
29 | validTo: {
30 | type: DataTypes.DATE,
31 | allowNull: true
32 | },
33 | createdAt: {
34 | type: Sequelize.DATE,
35 | allowNull: false
36 | },
37 | updatedAt: {
38 | type: Sequelize.DATE,
39 | allowNull: false
40 | }
41 | });
42 | };
43 | export const down: Migration = async ({context: {query}}) => {
44 | await query.dropTable('announcements');
45 | };
46 |
--------------------------------------------------------------------------------
/server/src/migrations/00009_fix-locations-constraint.ts:
--------------------------------------------------------------------------------
1 | import { Migration } from '../sequelize';
2 |
3 | export const up: Migration = async ({context: {query}}) => {
4 | const tableName = 'locations';
5 |
6 | // Remove the incorrect constraint
7 | await query.removeConstraint(tableName, `${tableName}_ibfk_1`);
8 |
9 | // Add it again with the correct parameters
10 | await query.addConstraint(tableName, {
11 | name: `${tableName}_ibfk_1`,
12 | type: 'foreign key',
13 | fields: ['incidentId'],
14 | references: { table: 'incidents', field: 'id' },
15 | onDelete: 'CASCADE',
16 | onUpdate: 'CASCADE'
17 | });
18 | };
19 | export const down: Migration = async ({context: {query}}) => {
20 | const tableName = 'locations';
21 |
22 | // Remove the correct constraint
23 | await query.removeConstraint(tableName, `${tableName}_ibfk_1`);
24 |
25 | // Add it again with the old, but incorrect parameters
26 | await query.addConstraint(tableName, {
27 | name: `${tableName}_ibfk_1`,
28 | type: 'foreign key',
29 | fields: ['incidentId'],
30 | references: { table: 'incidents', field: 'id' },
31 | onDelete: 'SET NULL',
32 | onUpdate: 'CASCADE'
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/server/src/migrations/00010_extend_incident_description.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.changeColumn('incidents', 'description', {
6 | type: Sequelize.TEXT,
7 | allowNull: false,
8 | defaultValue: ''
9 | });
10 | };
11 | export const down: Migration = async ({context: {query}}) => {
12 | await query.changeColumn('incidents', 'description', {
13 | type: Sequelize.STRING,
14 | allowNull: false,
15 | defaultValue: ''
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/server/src/migrations/00011_create-settings.ts:
--------------------------------------------------------------------------------
1 | import Sequelize, {DataTypes} from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'settings';
6 |
7 | try {
8 | await query.describeTable(tableName);
9 | // Exit early if the table exists
10 | return;
11 | } catch {
12 | // The table does not exist, so we just continue
13 | }
14 |
15 | await query.createTable(tableName, {
16 | key: {
17 | type: DataTypes.STRING,
18 | allowNull: false,
19 | primaryKey: true
20 | },
21 | value: {
22 | type: DataTypes.JSON,
23 | allowNull: true
24 | },
25 | createdAt: {
26 | type: Sequelize.DATE,
27 | allowNull: false
28 | },
29 | updatedAt: {
30 | type: Sequelize.DATE,
31 | allowNull: false
32 | }
33 | });
34 | };
35 | export const down: Migration = async ({context: {query}}) => {
36 | await query.dropTable('settings');
37 | };
38 |
--------------------------------------------------------------------------------
/server/src/migrations/00013_add-display-type.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.addColumn('displays', 'type', {
6 | type: Sequelize.STRING(20),
7 | allowNull: false,
8 | defaultValue: 'monitor'
9 | });
10 | };
11 | export const down: Migration = async ({context: {query}}) => {
12 | await query.removeColumn('displays', 'type');
13 | };
14 |
--------------------------------------------------------------------------------
/server/src/migrations/00014_split-locality.ts:
--------------------------------------------------------------------------------
1 | import {DataTypes} from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'locations';
6 |
7 | await query.renameColumn(tableName, 'locality', 'municipality');
8 | await query.addColumn(tableName, 'district', {
9 | type: DataTypes.STRING,
10 | allowNull: false,
11 | defaultValue: ''
12 | });
13 | };
14 | export const down: Migration = async ({context: {query}}) => {
15 | const tableName = 'locations';
16 | await query.removeColumn(tableName, 'district');
17 | await query.renameColumn(tableName, 'municipality', 'locality');
18 | };
19 |
--------------------------------------------------------------------------------
/server/src/migrations/00015_create-calendar-feeds.ts:
--------------------------------------------------------------------------------
1 | import Sequelize, {DataTypes} from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'calendar_feeds';
6 |
7 | try {
8 | await query.describeTable(tableName);
9 | // Exit early if the table exists
10 | return;
11 | } catch {
12 | // The table does not exist, so we just continue
13 | }
14 |
15 | await query.createTable(tableName, {
16 | id: {
17 | type: Sequelize.INTEGER,
18 | allowNull: false,
19 | autoIncrement: true,
20 | primaryKey: true
21 | },
22 | name: {
23 | type: DataTypes.STRING,
24 | allowNull: false
25 | },
26 | url: {
27 | type: DataTypes.STRING,
28 | allowNull: false
29 | },
30 | createdAt: {
31 | type: Sequelize.DATE,
32 | allowNull: false
33 | },
34 | updatedAt: {
35 | type: Sequelize.DATE,
36 | allowNull: false
37 | }
38 | });
39 |
40 | await query.addConstraint(tableName, {
41 | name: 'name',
42 | type: 'unique',
43 | fields: ['name']
44 | });
45 | };
46 | export const down: Migration = async ({context: {query}}) => {
47 | await query.dropTable('calendar_feeds');
48 | };
49 |
--------------------------------------------------------------------------------
/server/src/migrations/00016_add-view-state.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.addColumn('views', 'active', {
6 | type: Sequelize.BOOLEAN,
7 | allowNull: false,
8 | defaultValue: true
9 | });
10 | };
11 | export const down: Migration = async ({context: {query}}) => {
12 | await query.removeColumn('views', 'active');
13 | };
14 |
--------------------------------------------------------------------------------
/server/src/migrations/00017_add-view-pinned.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.addColumn('views', 'pinned', {
6 | type: Sequelize.BOOLEAN,
7 | allowNull: false,
8 | defaultValue: false
9 | });
10 | };
11 | export const down: Migration = async ({context: {query}}) => {
12 | await query.removeColumn('views', 'pinned');
13 | };
14 |
--------------------------------------------------------------------------------
/server/src/models/announcements.model.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes, Model, Sequelize } from 'sequelize';
2 | import { HookReturn } from 'sequelize/types/hooks';
3 | import { Application } from '../declarations';
4 |
5 | export default function (app: Application): typeof Model {
6 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
7 | const Announcement = sequelizeClient.define('announcement', {
8 | title: {
9 | type: DataTypes.STRING,
10 | allowNull: false
11 | },
12 | body: {
13 | type: DataTypes.STRING,
14 | allowNull: true
15 | },
16 | important: {
17 | type: DataTypes.BOOLEAN,
18 | allowNull: false,
19 | defaultValue: false
20 | },
21 | validFrom: {
22 | type: DataTypes.DATE,
23 | allowNull: true
24 | },
25 | validTo: {
26 | type: DataTypes.DATE,
27 | allowNull: true
28 | }
29 | }, {
30 | hooks: {
31 | beforeCount(options: any): HookReturn {
32 | options.raw = true;
33 | }
34 | },
35 | tableName: 'announcements'
36 | });
37 | return Announcement;
38 | }
39 |
--------------------------------------------------------------------------------
/server/src/models/api-keys.model.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize, DataTypes, Model } from 'sequelize';
2 | import { HookReturn } from 'sequelize/types/hooks';
3 | import { Application } from '../declarations';
4 |
5 | export default function (app: Application): typeof Model {
6 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
7 | const ApiKey = sequelizeClient.define('api_key', {
8 | name: {
9 | type: DataTypes.STRING,
10 | allowNull: false
11 | },
12 | tokenHash: {
13 | type: DataTypes.STRING,
14 | allowNull: false,
15 | unique: true
16 | }
17 | }, {
18 | hooks: {
19 | beforeCount(options: any): HookReturn {
20 | options.raw = true;
21 | }
22 | },
23 | tableName: 'api_keys'
24 | });
25 |
26 | (ApiKey as any).associate = function (models: any): void {
27 | models.api_key.belongsTo(models.display);
28 | };
29 |
30 | return ApiKey;
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/models/calendar-feeds.model.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize, DataTypes, Model } from 'sequelize';
2 | import { Application } from '../declarations';
3 | import { HookReturn } from 'sequelize/types/hooks';
4 |
5 | export default function (app: Application): typeof Model {
6 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
7 | const CalendarFeed = sequelizeClient.define('calendar_feed', {
8 | name: {
9 | type: DataTypes.STRING,
10 | allowNull: false,
11 | unique: true,
12 | },
13 | url: {
14 | type: DataTypes.STRING,
15 | allowNull: false,
16 | validate: {
17 | isUrl: true
18 | },
19 | },
20 | }, {
21 | hooks: {
22 | beforeCount(options: any): HookReturn {
23 | options.raw = true;
24 | }
25 | },
26 | tableName: 'calendar_feeds'
27 | });
28 |
29 | return CalendarFeed;
30 | }
31 |
--------------------------------------------------------------------------------
/server/src/models/content-slots.model.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize, DataTypes, Model } from 'sequelize';
2 | import { HookReturn } from 'sequelize/types/hooks';
3 | import { Application } from '../declarations';
4 |
5 | export default function (app: Application): typeof Model {
6 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
7 | const ContentSlot = sequelizeClient.define('content_slot', {
8 | component: {
9 | type: DataTypes.STRING,
10 | allowNull: false
11 | },
12 | columnStart: {
13 | type: DataTypes.TINYINT,
14 | allowNull: false,
15 | validate: {
16 | min: 1
17 | }
18 | },
19 | columnEnd: {
20 | type: DataTypes.TINYINT,
21 | allowNull: false,
22 | validate: {
23 | min: 2
24 | }
25 | },
26 | rowStart: {
27 | type: DataTypes.TINYINT,
28 | allowNull: false,
29 | validate: {
30 | min: 1
31 | }
32 | },
33 | rowEnd: {
34 | type: DataTypes.TINYINT,
35 | allowNull: false,
36 | validate: {
37 | min: 2
38 | }
39 | },
40 | options: {
41 | type: DataTypes.JSON,
42 | allowNull: true
43 | }
44 | }, {
45 | hooks: {
46 | beforeCount(options: any): HookReturn {
47 | options.raw = true;
48 | }
49 | },
50 | tableName: 'content_slots'
51 | });
52 |
53 | (ContentSlot as any).associate = function (models: any): void {
54 | models.content_slot.belongsTo(models.view, {
55 | as: 'view'
56 | });
57 | };
58 |
59 | return ContentSlot;
60 | }
61 |
--------------------------------------------------------------------------------
/server/src/models/displays.model.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize, DataTypes, Model } from 'sequelize';
2 | import { HookReturn } from 'sequelize/types/hooks';
3 | import { Application } from '../declarations';
4 |
5 | export default function (app: Application): typeof Model {
6 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
7 | const Display = sequelizeClient.define('display', {
8 | name: {
9 | type: DataTypes.STRING,
10 | allowNull: false
11 | },
12 | active: {
13 | type: DataTypes.BOOLEAN,
14 | allowNull: false,
15 | defaultValue: false
16 | },
17 | description: {
18 | type: DataTypes.STRING,
19 | allowNull: false,
20 | defaultValue: ''
21 | },
22 | type: {
23 | type: DataTypes.STRING(20),
24 | allowNull: false,
25 | defaultValue: 'monitor'
26 | }
27 | }, {
28 | hooks: {
29 | beforeCount(options: any): HookReturn {
30 | options.raw = true;
31 | }
32 | },
33 | tableName: 'displays'
34 | });
35 |
36 | (Display as any).associate = function (models: any): void {
37 | models.display.hasMany(models.view, {
38 | foreignKey: { allowNull: false },
39 | as: 'views'
40 | });
41 | models.display.hasOne(models.api_key, {
42 | foreignKey: { allowNull: true },
43 | onDelete: 'CASCADE',
44 | onUpdate: 'CASCADE'
45 | });
46 | };
47 |
48 | return Display;
49 | }
50 |
--------------------------------------------------------------------------------
/server/src/models/locations.model.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize, DataTypes, Model } from 'sequelize';
2 | import { HookReturn } from 'sequelize/types/hooks';
3 | import { Application } from '../declarations';
4 |
5 | export default function (app: Application): typeof Model {
6 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
7 | const locations = sequelizeClient.define('locations', {
8 | rawText: {
9 | type: DataTypes.STRING,
10 | allowNull: false
11 | },
12 | latitude: {
13 | type: DataTypes.DOUBLE,
14 | allowNull: true
15 | },
16 | longitude: {
17 | type: DataTypes.DOUBLE,
18 | allowNull: true
19 | },
20 | name: {
21 | type: DataTypes.STRING,
22 | allowNull: false,
23 | defaultValue: ''
24 | },
25 | street: {
26 | type: DataTypes.STRING,
27 | allowNull: false,
28 | defaultValue: ''
29 | },
30 | number: {
31 | type: DataTypes.STRING,
32 | allowNull: false,
33 | defaultValue: ''
34 | },
35 | detail: {
36 | type: DataTypes.STRING,
37 | allowNull: false,
38 | defaultValue: ''
39 | },
40 | municipality: {
41 | type: DataTypes.STRING,
42 | allowNull: false,
43 | defaultValue: ''
44 | },
45 | district: {
46 | type: DataTypes.STRING,
47 | allowNull: false,
48 | defaultValue: ''
49 | },
50 | hubLocationId: {
51 | type: DataTypes.INTEGER,
52 | allowNull: true
53 | }
54 | }, {
55 | hooks: {
56 | beforeCount(options: any): HookReturn {
57 | options.raw = true;
58 | }
59 | },
60 | tableName: 'locations'
61 | });
62 |
63 | (locations as any).associate = function (models: any): void {
64 | models.locations.belongsTo(models.incident);
65 | };
66 |
67 | return locations;
68 | }
69 |
--------------------------------------------------------------------------------
/server/src/models/settings.model.ts:
--------------------------------------------------------------------------------
1 | // See http://docs.sequelizejs.com/en/latest/docs/models-definition/
2 | // for more of what you can do here.
3 | import { Sequelize, DataTypes, Model } from 'sequelize';
4 | import { HookReturn } from 'sequelize/types/hooks';
5 | import { Application } from '../declarations';
6 |
7 | export default function (app: Application): typeof Model {
8 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
9 | const Setting = sequelizeClient.define('setting', {
10 | key: {
11 | type: DataTypes.STRING,
12 | allowNull: false,
13 | primaryKey: true
14 | },
15 | value: {
16 | type: DataTypes.JSON,
17 | allowNull: true
18 | }
19 | }, {
20 | hooks: {
21 | beforeCount(options: any): HookReturn {
22 | options.raw = true;
23 | }
24 | },
25 | tableName: 'settings'
26 | });
27 | return Setting;
28 | }
29 |
--------------------------------------------------------------------------------
/server/src/models/users.model.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes, Model, Sequelize } from 'sequelize';
2 | import { HookReturn } from 'sequelize/types/hooks';
3 | import { Application } from '../declarations';
4 |
5 | export default function (app: Application): typeof Model {
6 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
7 | const User = sequelizeClient.define('user', {
8 | email: {
9 | type: DataTypes.STRING,
10 | allowNull: false,
11 | unique: true
12 | },
13 | name: {
14 | type: DataTypes.STRING,
15 | allowNull: false,
16 | defaultValue: ''
17 | },
18 | password: {
19 | type: DataTypes.STRING,
20 | allowNull: false
21 | }
22 | }, {
23 | hooks: {
24 | beforeCount(options: any): HookReturn {
25 | options.raw = true;
26 | }
27 | },
28 | tableName: 'users'
29 | });
30 | return User;
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/models/views.model.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize, DataTypes, Model } from 'sequelize';
2 | import { HookReturn } from 'sequelize/types/hooks';
3 | import { Application } from '../declarations';
4 |
5 | export default function (app: Application): typeof Model {
6 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
7 | const View = sequelizeClient.define('view', {
8 | type: {
9 | type: DataTypes.STRING,
10 | allowNull: false
11 | },
12 | order: {
13 | type: DataTypes.MEDIUMINT,
14 | allowNull: false,
15 | validate: {
16 | min: 0
17 | }
18 | },
19 | columns: {
20 | type: DataTypes.TINYINT,
21 | allowNull: false,
22 | validate: {
23 | min: 1
24 | }
25 | },
26 | rows: {
27 | type: DataTypes.TINYINT,
28 | allowNull: false,
29 | validate: {
30 | min: 1
31 | }
32 | },
33 | active: {
34 | type: DataTypes.BOOLEAN,
35 | allowNull: false,
36 | defaultValue: true,
37 | },
38 | pinned: {
39 | type: DataTypes.BOOLEAN,
40 | allowNull: false,
41 | defaultValue: false
42 | }
43 | }, {
44 | hooks: {
45 | beforeCount(options: any): HookReturn {
46 | options.raw = true;
47 | }
48 | },
49 | tableName: 'views'
50 | });
51 |
52 | (View as any).associate = function (models: any): void {
53 | models.view.belongsTo(models.display, {
54 | as: 'display'
55 | });
56 | models.view.hasMany(models.content_slot, {
57 | foreignKey: { allowNull: false },
58 | as: 'contentSlots'
59 | });
60 | };
61 |
62 | return View;
63 | }
64 |
--------------------------------------------------------------------------------
/server/src/services/announcements/announcements.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application } from '../../declarations';
3 |
4 | export class Announcements extends Service {
5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars
6 | constructor(options: Partial, app: Application) {
7 | super(options);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/services/announcements/announcements.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { allowApiKey } from '../../hooks/allowApiKey';
3 | // Don't remove this comment. It's needed to format import lines nicely.
4 |
5 | const { authenticate } = authentication.hooks;
6 |
7 | export default {
8 | before: {
9 | all: [ allowApiKey(), authenticate('jwt', 'api-key') ],
10 | find: [],
11 | get: [],
12 | create: [],
13 | update: [],
14 | patch: [],
15 | remove: []
16 | },
17 |
18 | after: {
19 | all: [],
20 | find: [],
21 | get: [],
22 | create: [],
23 | update: [],
24 | patch: [],
25 | remove: []
26 | },
27 |
28 | error: {
29 | all: [],
30 | find: [],
31 | get: [],
32 | create: [],
33 | update: [],
34 | patch: [],
35 | remove: []
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/server/src/services/announcements/announcements.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `announcements` service on path `/api/v1/announcements`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { Announcements } from './announcements.class';
5 | import createModel from '../../models/announcements.model';
6 | import hooks from './announcements.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'api/v1/announcements': Announcements & ServiceAddons;
12 | }
13 |
14 | interface AnnouncementData {
15 | id: number
16 | title: string
17 | body?: string
18 | important: boolean
19 | validFrom?: Date
20 | validTo?: Date
21 | }
22 | }
23 |
24 | export default function (app: Application): void {
25 | const options = {
26 | Model: createModel(app),
27 | paginate: app.get('paginate')
28 | };
29 |
30 | // Initialize our service with any options it requires
31 | app.use('/api/v1/announcements', new Announcements(options, app));
32 |
33 | // Get our initialized service so that we can register hooks
34 | const service = app.service('api/v1/announcements');
35 |
36 | service.hooks(hooks);
37 | }
38 |
--------------------------------------------------------------------------------
/server/src/services/api-keys/api-keys.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application } from '../../declarations';
3 |
4 | export class ApiKeys extends Service {
5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars
6 | constructor(options: Partial, app: Application) {
7 | super(options);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/services/api-keys/api-keys.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `api-keys` service on path `/api/v1/api-keys`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { ApiKeys } from './api-keys.class';
5 | import createModel from '../../models/api-keys.model';
6 | import hooks from './api-keys.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'api/v1/api-keys': ApiKeys & ServiceAddons;
12 | }
13 | }
14 |
15 | export default function (app: Application): void {
16 | const options = {
17 | Model: createModel(app),
18 | paginate: app.get('paginate')
19 | };
20 |
21 | // Initialize our service with any options it requires
22 | app.use('/api/v1/api-keys', new ApiKeys(options, app));
23 |
24 | // Get our initialized service so that we can register hooks
25 | const service = app.service('api/v1/api-keys');
26 |
27 | service.hooks(hooks);
28 | }
29 |
--------------------------------------------------------------------------------
/server/src/services/calendar-feeds/calendar-feeds.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application } from '../../declarations';
3 |
4 | export interface CalendarFeedData {
5 | id: number
6 | name: string
7 | url: string
8 | }
9 |
10 | export class CalendarFeeds extends Service {
11 | //eslint-disable-next-line @typescript-eslint/no-unused-vars
12 | constructor(options: Partial, app: Application) {
13 | super(options);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/src/services/calendar-feeds/calendar-feeds.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { allowApiKey } from '../../hooks/allowApiKey';
3 | // Don't remove this comment. It's needed to format import lines nicely.
4 |
5 | const { authenticate } = authentication.hooks;
6 |
7 | export default {
8 | before: {
9 | all: [ allowApiKey(), authenticate('jwt', 'api-key') ],
10 | find: [],
11 | get: [],
12 | create: [],
13 | update: [],
14 | patch: [],
15 | remove: []
16 | },
17 |
18 | after: {
19 | all: [],
20 | find: [],
21 | get: [],
22 | create: [],
23 | update: [],
24 | patch: [],
25 | remove: []
26 | },
27 |
28 | error: {
29 | all: [],
30 | find: [],
31 | get: [],
32 | create: [],
33 | update: [],
34 | patch: [],
35 | remove: []
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/server/src/services/calendar-feeds/calendar-feeds.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `Calendar Feeds` service on path `/calendar-feeds`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { CalendarFeeds } from './calendar-feeds.class';
5 | import createModel from '../../models/calendar-feeds.model';
6 | import hooks from './calendar-feeds.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'calendar-feeds': CalendarFeeds & ServiceAddons;
12 | }
13 | }
14 |
15 | export default function (app: Application): void {
16 | const options = {
17 | Model: createModel(app),
18 | paginate: app.get('paginate')
19 | };
20 |
21 | // Initialize our service with any options it requires
22 | app.use('/calendar-feeds', new CalendarFeeds(options, app));
23 |
24 | // Get our initialized service so that we can register hooks
25 | const service = app.service('calendar-feeds');
26 |
27 | service.hooks(hooks);
28 | }
29 |
--------------------------------------------------------------------------------
/server/src/services/calendar-items/calendar-items.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { disallow } from 'feathers-hooks-common';
3 | import { allowApiKey } from '../../hooks/allowApiKey';
4 | // Don't remove this comment. It's needed to format import lines nicely.
5 |
6 | const { authenticate } = authentication.hooks;
7 |
8 | export default {
9 | before: {
10 | all: [ allowApiKey(), authenticate('jwt', 'api-key') ],
11 | find: [],
12 | get: [],
13 | create: [ disallow('external') ],
14 | update: [ disallow('external') ],
15 | patch: [ disallow('external') ],
16 | remove: [ disallow('external') ]
17 | },
18 |
19 | after: {
20 | all: [],
21 | find: [],
22 | get: [],
23 | create: [],
24 | update: [],
25 | patch: [],
26 | remove: []
27 | },
28 |
29 | error: {
30 | all: [],
31 | find: [],
32 | get: [],
33 | create: [],
34 | update: [],
35 | patch: [],
36 | remove: []
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/server/src/services/calendar-items/calendar-items.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `Calendar Items` service on path `/calendar-items`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { CalendarItems } from './calendar-items.class';
5 | import hooks from './calendar-items.hooks';
6 | import { MemoryServiceOptions } from 'feathers-memory';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'calendar-items': CalendarItems & ServiceAddons;
12 | }
13 | }
14 |
15 | export default function (app: Application): void {
16 | const options: Partial = {
17 | id: 'uid',
18 | paginate: app.get('paginate'),
19 | events: ['bulk-change'],
20 | };
21 |
22 | // Initialize our service with any options it requires
23 | app.use('/calendar-items', new CalendarItems(options, app));
24 |
25 | // Get our initialized service so that we can register hooks
26 | const service = app.service('calendar-items');
27 |
28 | service.hooks(hooks);
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/services/content-slots/content-slots.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application, ContentSlotData } from '../../declarations';
3 |
4 | export class ContentSlots extends Service {
5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars
6 | constructor(options: Partial, app: Application) {
7 | super(options);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/services/content-slots/content-slots.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { allowApiKey } from '../../hooks/allowApiKey';
3 | import { unserializeJson } from '../../hooks/unserializeJson';
4 | import { HookContext } from '@feathersjs/feathers';
5 | import { getItems, replaceItems } from 'feathers-hooks-common';
6 | import { ContentSlotData } from '../../declarations';
7 | // Don't remove this comment. It's needed to format import lines nicely.
8 |
9 | const { authenticate } = authentication.hooks;
10 |
11 | export default {
12 | before: {
13 | all: [ allowApiKey(), authenticate('jwt', 'api-key') ],
14 | find: [],
15 | get: [],
16 | create: [],
17 | update: [],
18 | patch: [],
19 | remove: []
20 | },
21 |
22 | after: {
23 | all: [ unserializeJson('options'), ensureDefaultOptions ],
24 | find: [],
25 | get: [],
26 | create: [],
27 | update: [],
28 | patch: [],
29 | remove: []
30 | },
31 |
32 | error: {
33 | all: [],
34 | find: [],
35 | get: [],
36 | create: [],
37 | update: [],
38 | patch: [],
39 | remove: []
40 | }
41 | };
42 |
43 | const defaultOptions = new Map([
44 | ['AnnouncementList', { title: '' }],
45 | ['DWDWarningMap', { areaCode: 'DE', mapType: 'area' }]
46 | ]);
47 |
48 | function ensureDefaultOptions(context: HookContext): HookContext {
49 | const items = getItems(context) as ContentSlotData|ContentSlotData[];
50 | if (Array.isArray(items)) {
51 | items.forEach(item => {
52 | item.options = Object.assign({}, defaultOptions.get(item.component) || {}, item.options);
53 | });
54 | } else {
55 | items.options = Object.assign({}, defaultOptions.get(items.component) || {}, items.options);
56 | }
57 |
58 | replaceItems(context, items);
59 | return context;
60 | }
61 |
--------------------------------------------------------------------------------
/server/src/services/content-slots/content-slots.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `content-slots` service on path `/api/v1/content-slots`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { ContentSlots } from './content-slots.class';
5 | import createModel from '../../models/content-slots.model';
6 | import hooks from './content-slots.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'api/v1/content-slots': ContentSlots & ServiceAddons;
12 | }
13 |
14 | interface ContentSlotData {
15 | id: number
16 | component: string
17 | columnStart: number
18 | columnEnd: number
19 | rowStart: number
20 | rowEnd: number
21 | options: any
22 | viewId: number
23 | }
24 | }
25 |
26 | export default function (app: Application): void {
27 | const options = {
28 | Model: createModel(app),
29 | multi: ['remove'],
30 | paginate: app.get('paginate')
31 | };
32 |
33 | // Initialize our service with any options it requires
34 | app.use('/api/v1/content-slots', new ContentSlots(options, app));
35 |
36 | // Get our initialized service so that we can register hooks
37 | const service = app.service('api/v1/content-slots');
38 |
39 | service.hooks(hooks);
40 | }
41 |
--------------------------------------------------------------------------------
/server/src/services/displays/displays.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { allowApiKey } from '../../hooks/allowApiKey';
3 | import { shallowPopulate } from 'feathers-shallow-populate';
4 | import { HookContext } from '@feathersjs/feathers';
5 | // Don't remove this comment. It's needed to format import lines nicely.
6 |
7 | const { authenticate } = authentication.hooks;
8 |
9 | const populateOptions = {
10 | include: {
11 | service: 'api/v1/views',
12 | nameAs: 'views',
13 | keyHere: 'id',
14 | keyThere: 'displayId',
15 | }
16 | };
17 |
18 | export default {
19 | before: {
20 | all: [ allowApiKey(), authenticate('jwt', 'api-key') ],
21 | find: [],
22 | get: [],
23 | create: [ includeViews ],
24 | update: [],
25 | patch: [],
26 | remove: []
27 | },
28 |
29 | after: {
30 | all: [ shallowPopulate(populateOptions) ],
31 | find: [],
32 | get: [],
33 | create: [],
34 | update: [],
35 | patch: [],
36 | remove: []
37 | },
38 |
39 | error: {
40 | all: [],
41 | find: [],
42 | get: [],
43 | create: [],
44 | update: [],
45 | patch: [],
46 | remove: []
47 | }
48 | };
49 |
50 | /**
51 | * Automatically create nested views when creating a display
52 | * @param context
53 | */
54 | function includeViews(context: HookContext): HookContext {
55 | const sequelize = context.app.get('sequelizeClient');
56 | context.params.sequelize = { include: [ { model: sequelize.models.view, as: 'views' } ] };
57 | return context;
58 | }
59 |
--------------------------------------------------------------------------------
/server/src/services/displays/displays.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `displays` service on path `/api/v1/displays`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { Displays } from './displays.class';
5 | import createModel from '../../models/displays.model';
6 | import hooks from './displays.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'api/v1/displays': Displays & ServiceAddons;
12 | }
13 |
14 | interface DisplayData {
15 | id: number
16 | name: string
17 | active: boolean
18 | description: string,
19 | type: string
20 | views: ViewData[]
21 | }
22 | }
23 |
24 | export default function (app: Application): void {
25 | const options = {
26 | Model: createModel(app),
27 | paginate: app.get('paginate')
28 | };
29 |
30 | // Initialize our service with any options it requires
31 | app.use('/api/v1/displays', new Displays(options, app));
32 |
33 | // Get our initialized service so that we can register hooks
34 | const service = app.service('api/v1/displays');
35 |
36 | service.hooks(hooks);
37 | }
38 |
--------------------------------------------------------------------------------
/server/src/services/hub-connector/hub-connector.class.ts:
--------------------------------------------------------------------------------
1 | import { SetupMethod } from '@feathersjs/feathers';
2 | import { Application } from '../../declarations';
3 | import io from 'socket.io-client';
4 | import logger from '../../logger';
5 | import IncidentsWatcher from './services/incidents.class';
6 | import LocationsWatcher from './services/locations.class';
7 |
8 | export class HubConnector implements SetupMethod {
9 | app: Application;
10 |
11 | constructor (app: Application) {
12 | this.app = app;
13 | }
14 |
15 | setup(app: Application): void {
16 | const hubHost = app.get('hub_host');
17 | const hubApiKey = app.get('hub_api_key');
18 |
19 | // If no host is set, there is no intention to connect to the Hub
20 | if (!hubHost || hubHost === '' || hubHost === 'HUB_HOST') {
21 | return;
22 | }
23 |
24 | let hubUrl: URL;
25 | try {
26 | hubUrl = new URL(hubHost);
27 | } catch {
28 | logger.error('hub_host is not a valid URL');
29 | return;
30 | }
31 |
32 | if (!hubApiKey || hubApiKey === '' || hubApiKey === 'HUB_API_KEY') {
33 | logger.warn('hub_host is set, but hub_api_key is empty');
34 | return;
35 | }
36 |
37 | (app.get('databaseReady') as Promise).then(() => {
38 | logger.info('Connecting to Hub at %s...', hubUrl.toString());
39 | const socket = io(hubUrl.toString(), {
40 | transportOptions: {
41 | polling: {
42 | extraHeaders: {
43 | 'x-api-key': hubApiKey
44 | }
45 | }
46 | }
47 | });
48 | socket.on('connect', () => {
49 | logger.info('Connected to Hub');
50 | });
51 | socket.on('disconnect', (reason: Error) => {
52 | logger.error('Disconnected from Hub:', reason);
53 | });
54 | socket.on('connect_error', (reason: Error) => {
55 | logger.error('Error connecting to Hub:', reason.message);
56 | });
57 | socket.on('connect_timeout', (reason: Error) => {
58 | logger.error('Timeout connecting to Hub:', reason);
59 | });
60 |
61 | // Start watching services on the Hub
62 | new IncidentsWatcher(app, socket);
63 | new LocationsWatcher(app, socket);
64 | });
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/server/src/services/hub-connector/hub-connector.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { disallow } from 'feathers-hooks-common';
3 | // Don't remove this comment. It's needed to format import lines nicely.
4 |
5 | const { authenticate } = authentication.hooks;
6 |
7 | export default {
8 | before: {
9 | all: [ disallow('external'), authenticate('jwt') ],
10 | find: [],
11 | get: [],
12 | create: [],
13 | update: [],
14 | patch: [],
15 | remove: []
16 | },
17 |
18 | after: {
19 | all: [],
20 | find: [],
21 | get: [],
22 | create: [],
23 | update: [],
24 | patch: [],
25 | remove: []
26 | },
27 |
28 | error: {
29 | all: [],
30 | find: [],
31 | get: [],
32 | create: [],
33 | update: [],
34 | patch: [],
35 | remove: []
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/server/src/services/hub-connector/hub-connector.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `hub-connector` service on path `/hub-connector`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { HubConnector } from './hub-connector.class';
5 | import hooks from './hub-connector.hooks';
6 |
7 | // Add this service to the service type index
8 | declare module '../../declarations' {
9 | interface ServiceTypes {
10 | 'hub-connector': HubConnector & ServiceAddons;
11 | }
12 | }
13 |
14 | export default function (app: Application): void {
15 | app.use('/hub-connector', new HubConnector(app));
16 |
17 | // Get our initialized service so that we can register hooks
18 | const service = app.service('hub-connector');
19 |
20 | service.hooks(hooks);
21 | }
22 |
--------------------------------------------------------------------------------
/server/src/services/incidents/incidents.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application, IncidentData } from '../../declarations';
3 |
4 | export class Incidents extends Service {
5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars
6 | constructor(options: Partial, app: Application) {
7 | super(options);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/services/incidents/incidents.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { allowApiKey } from '../../hooks/allowApiKey';
3 | import { HookContext } from '@feathersjs/feathers';
4 | import { shallowPopulate } from 'feathers-shallow-populate';
5 | // Don't remove this comment. It's needed to format import lines nicely.
6 |
7 | const { authenticate } = authentication.hooks;
8 |
9 | // TODO make sure that incidents that are mirrored from the Hub do not get modified
10 | // TODO make sure that local incidents cannot be assigned a hubIncidentId
11 |
12 | const populateOptions = {
13 | include: {
14 | service: 'api/v1/locations',
15 | nameAs: 'location',
16 | keyHere: 'id',
17 | keyThere: 'incidentId',
18 | asArray: false
19 | }
20 | };
21 |
22 | export default {
23 | before: {
24 | all: [ allowApiKey(), authenticate('jwt', 'api-key') ],
25 | find: [],
26 | get: [],
27 | create: [ includeAssociations ],
28 | update: [],
29 | patch: [],
30 | remove: []
31 | },
32 |
33 | after: {
34 | all: [ shallowPopulate(populateOptions) ],
35 | find: [],
36 | get: [],
37 | create: [],
38 | update: [],
39 | patch: [],
40 | remove: []
41 | },
42 |
43 | error: {
44 | all: [],
45 | find: [],
46 | get: [],
47 | create: [],
48 | update: [],
49 | patch: [],
50 | remove: []
51 | }
52 | };
53 |
54 | async function includeAssociations(context: HookContext): Promise {
55 | if (context.data.location) {
56 | const LocationService = context.app.service('api/v1/locations');
57 | context.params.sequelize = {
58 | include: [{ model: LocationService.Model }]
59 | };
60 | }
61 | return context;
62 | }
63 |
--------------------------------------------------------------------------------
/server/src/services/incidents/incidents.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `incidents` service on path `/api/v1/incidents`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { Incidents } from './incidents.class';
5 | import createModel from '../../models/incidents.model';
6 | import hooks from './incidents.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'api/v1/incidents': Incidents & ServiceAddons;
12 | }
13 |
14 | interface IncidentData {
15 | id: number
16 | time: Date
17 | sender: string
18 | ref: string
19 | caller_name: string
20 | caller_number: string
21 | location?: LocationData
22 | reason: string
23 | keyword: string
24 | description: string
25 | status: 'Actual'|'Exercise'|'Test'
26 | category: 'Geo'|'Met'|'Safety'|'Security'|'Rescue'|'Fire'|'Health'|'Env'|'Transport'|'Infra'|'CBRNE'|'Other'
27 | hubIncidentId?: number
28 | }
29 | }
30 |
31 | export default function (app: Application): void {
32 | const options = {
33 | Model: createModel(app),
34 | paginate: app.get('paginate'),
35 | multi: ['remove']
36 | };
37 |
38 | // Initialize our service with any options it requires
39 | app.use('/api/v1/incidents', new Incidents(options, app));
40 |
41 | // Get our initialized service so that we can register hooks
42 | const service = app.service('api/v1/incidents');
43 |
44 | service.hooks(hooks);
45 | }
46 |
--------------------------------------------------------------------------------
/server/src/services/index.ts:
--------------------------------------------------------------------------------
1 | import { Application } from '../declarations';
2 | import users from './users/users.service';
3 | import incidents from './incidents/incidents.service';
4 | import displays from './displays/displays.service';
5 | import apiKeys from './api-keys/api-keys.service';
6 | import views from './views/views.service';
7 | import contentSlots from './content-slots/content-slots.service';
8 | import locations from './locations/locations.service';
9 | import announcements from './announcements/announcements.service';
10 | import hubConnector from './hub-connector/hub-connector.service';
11 | import keyRequests from './key-requests/key-requests.service';
12 | import settings from './settings/settings.service';
13 | import status from './status/status.service';
14 | import calendarFeeds from './calendar-feeds/calendar-feeds.service';
15 | import calendarItems from './calendar-items/calendar-items.service';
16 | // Don't remove this comment. It's needed to format import lines nicely.
17 |
18 | export default function (app: Application): void {
19 | app.configure(users);
20 | app.configure(incidents);
21 | app.configure(displays);
22 | app.configure(apiKeys);
23 | app.configure(views);
24 | app.configure(contentSlots);
25 | app.configure(locations);
26 | app.configure(announcements);
27 | app.configure(hubConnector);
28 | app.configure(keyRequests);
29 | app.configure(settings);
30 | app.configure(status);
31 | app.configure(calendarFeeds);
32 | app.configure(calendarItems);
33 | }
34 |
--------------------------------------------------------------------------------
/server/src/services/key-requests/key-requests.class.ts:
--------------------------------------------------------------------------------
1 | import { MemoryServiceOptions, Service } from 'feathers-memory';
2 | import { Application, DisplayData, KeyRequestData } from '../../declarations';
3 | import { NullableId, Params } from '@feathersjs/feathers';
4 | import { getIdentifierForConnection } from '../../channels';
5 | import { MethodNotAllowed } from '@feathersjs/errors';
6 |
7 | export class KeyRequests extends Service {
8 | private app: Application;
9 | constructor(options: Partial, app: Application) {
10 | super(options);
11 | this.app = app;
12 | }
13 |
14 | _create(data: Partial, params?: Params): Promise {
15 | if (!params?.connection) {
16 | throw new MethodNotAllowed('A key request can only be created by realtime connections');
17 | }
18 |
19 | // Every unauthenticated connection gets an identifier that can be used to identify its key request
20 | const identifier = getIdentifierForConnection(params.connection);
21 | if (!identifier) {
22 | throw new Error('Unknown connection');
23 | }
24 | data.requestId = identifier;
25 |
26 | // No request may be granted from the beginning
27 | data.granted = false;
28 |
29 | return super._create(data, params);
30 | }
31 |
32 |
33 | async _patch(id: NullableId, data: Partial, params?: Params): Promise {
34 | if (id === null) {
35 | throw new MethodNotAllowed('Can not patch multiple entries');
36 | }
37 |
38 | const existingRecord = await this.get(id);
39 | // If the key request gets granted, create an API key, connected to a new Display
40 | if (!existingRecord.granted && data.granted === true) {
41 | const newDisplay = await this.app.service('api/v1/displays').create({
42 | name: data.name || 'Neues Display',
43 | active: true
44 | }) as DisplayData;
45 | const newApiKey = await this.app.service('api/v1/api-keys').create({
46 | name: newDisplay.name,
47 | displayId: newDisplay.id
48 | });
49 | data.apiKey = `${newApiKey.id}:${newApiKey.token}`;
50 | }
51 |
52 | return await super._patch(id, data, params);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/server/src/services/key-requests/key-requests.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { disallow } from 'feathers-hooks-common';
3 |
4 | const { authenticate } = authentication.hooks;
5 |
6 | export default {
7 | before: {
8 | all: [],
9 | find: [ authenticate('jwt') ],
10 | get: [ authenticate('jwt') ],
11 | create: [ disallow('rest')],
12 | update: [ authenticate('jwt') ],
13 | patch: [ authenticate('jwt') ],
14 | remove: [ authenticate('jwt') ]
15 | },
16 |
17 | after: {
18 | all: [],
19 | find: [],
20 | get: [],
21 | create: [],
22 | update: [],
23 | patch: [],
24 | remove: []
25 | },
26 |
27 | error: {
28 | all: [],
29 | find: [],
30 | get: [],
31 | create: [],
32 | update: [],
33 | patch: [],
34 | remove: []
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/server/src/services/key-requests/key-requests.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `key-requests` service on path `/api/v1/key-requests`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { KeyRequests } from './key-requests.class';
5 | import hooks from './key-requests.hooks';
6 |
7 | // Add this service to the service type index
8 | declare module '../../declarations' {
9 | interface ServiceTypes {
10 | 'api/v1/key-requests': KeyRequests & ServiceAddons;
11 | }
12 |
13 | interface KeyRequestData {
14 | name: string
15 | granted: boolean
16 | requestId: string
17 | apiKey: string
18 | }
19 | }
20 |
21 | export default function (app: Application): void {
22 | const options = {
23 | paginate: app.get('paginate'),
24 | multi: ['remove']
25 | };
26 |
27 | // Initialize our service with any options it requires
28 | app.use('/api/v1/key-requests', new KeyRequests(options, app));
29 |
30 | // Get our initialized service so that we can register hooks
31 | const service = app.service('api/v1/key-requests');
32 |
33 | service.hooks(hooks);
34 | }
35 |
--------------------------------------------------------------------------------
/server/src/services/locations/locations.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application, LocationData } from '../../declarations';
3 |
4 | export class Locations extends Service {
5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars
6 | constructor(options: Partial, app: Application) {
7 | super(options);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/services/locations/locations.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { allowApiKey } from '../../hooks/allowApiKey';
3 | // Don't remove this comment. It's needed to format import lines nicely.
4 |
5 | const { authenticate } = authentication.hooks;
6 |
7 | // TODO make sure that locations that are mirrored from the Hub do not get modified
8 | // TODO make sure that local locations cannot be assigned a hubLocationId
9 |
10 | export default {
11 | before: {
12 | all: [ allowApiKey(), authenticate('jwt', 'api-key') ],
13 | find: [],
14 | get: [],
15 | create: [],
16 | update: [],
17 | patch: [],
18 | remove: []
19 | },
20 |
21 | after: {
22 | all: [],
23 | find: [],
24 | get: [],
25 | create: [],
26 | update: [],
27 | patch: [],
28 | remove: []
29 | },
30 |
31 | error: {
32 | all: [],
33 | find: [],
34 | get: [],
35 | create: [],
36 | update: [],
37 | patch: [],
38 | remove: []
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/server/src/services/locations/locations.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `locations` service on path `/api/v1/locations`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { Locations } from './locations.class';
5 | import createModel from '../../models/locations.model';
6 | import hooks from './locations.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'api/v1/locations': Locations & ServiceAddons;
12 | }
13 |
14 | interface LocationData {
15 | id?: number
16 | rawText: string
17 | latitude?: number
18 | longitude?: number
19 | name: string
20 | street: string
21 | number: string
22 | detail: string
23 | municipality: string
24 | district: string
25 | incidentId?: number
26 | hubLocationId?: number
27 | }
28 | }
29 |
30 | export default function (app: Application): void {
31 | const options = {
32 | Model: createModel(app),
33 | multi: ['remove'],
34 | paginate: app.get('paginate')
35 | };
36 |
37 | // Initialize our service with any options it requires
38 | app.use('/api/v1/locations', new Locations(options, app));
39 |
40 | // Get our initialized service so that we can register hooks
41 | const service = app.service('api/v1/locations');
42 |
43 | service.hooks(hooks);
44 | }
45 |
--------------------------------------------------------------------------------
/server/src/services/settings/settings.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application, SettingsData, SettingsValue } from '../../declarations';
3 | import logger from '../../logger';
4 |
5 | export class Settings extends Service {
6 | //eslint-disable-next-line @typescript-eslint/no-unused-vars
7 | constructor(options: Partial, app: Application) {
8 | super(options);
9 | }
10 |
11 | setup(app: Application): void {
12 | const settingDefaults = new Map([
13 | ['alert_banner_message', ''],
14 | ['incident_display_minutes', 60],
15 | ['incident_test_display_minutes', 1],
16 | ['reason_display_regex', null],
17 | ['station_coordinates', null],
18 | ]);
19 |
20 | (app.get('databaseReady') as Promise).then(async () => {
21 | logger.debug('Checking the settings table');
22 | for (const [settingKey, settingDefault] of settingDefaults) {
23 | try {
24 | await this.get(settingKey);
25 | } catch {
26 | // If the setting cannot be found, create it with the default value
27 | await this.create({ key: settingKey, value: settingDefault });
28 | }
29 | }
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/server/src/services/settings/settings.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { allowApiKey } from '../../hooks/allowApiKey';
3 | import { disallow } from 'feathers-hooks-common';
4 | import { unserializeJson } from '../../hooks/unserializeJson';
5 | // Don't remove this comment. It's needed to format import lines nicely.
6 |
7 | const { authenticate } = authentication.hooks;
8 |
9 | export default {
10 | before: {
11 | all: [ allowApiKey(), authenticate('jwt', 'api-key') ],
12 | find: [],
13 | get: [],
14 | create: [ disallow('external') ],
15 | update: [],
16 | patch: [],
17 | remove: [ disallow('external') ]
18 | },
19 |
20 | after: {
21 | all: [ unserializeJson('value') ],
22 | find: [],
23 | get: [],
24 | create: [],
25 | update: [],
26 | patch: [],
27 | remove: []
28 | },
29 |
30 | error: {
31 | all: [],
32 | find: [],
33 | get: [],
34 | create: [],
35 | update: [],
36 | patch: [],
37 | remove: []
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/server/src/services/settings/settings.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `settings` service on path `/api/v1/settings`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { Settings } from './settings.class';
5 | import createModel from '../../models/settings.model';
6 | import hooks from './settings.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'api/v1/settings': Settings & ServiceAddons;
12 | }
13 |
14 | interface SettingsData {
15 | key: string
16 | value: SettingsValue
17 | }
18 |
19 | interface CoordinateValue {
20 | latitude: number
21 | longitude: number
22 | }
23 |
24 | type SettingsValue = null | string | number | CoordinateValue
25 | }
26 |
27 | export default function (app: Application): void {
28 | const options = {
29 | Model: createModel(app),
30 | id: 'key',
31 | paginate: app.get('paginate')
32 | };
33 |
34 | // Initialize our service with any options it requires
35 | app.use('/api/v1/settings', new Settings(options, app));
36 |
37 | // Get our initialized service so that we can register hooks
38 | const service = app.service('api/v1/settings');
39 |
40 | service.hooks(hooks);
41 | }
42 |
--------------------------------------------------------------------------------
/server/src/services/status/status.class.ts:
--------------------------------------------------------------------------------
1 | import { Params, SetupMethod } from '@feathersjs/feathers';
2 | import { Application } from '../../declarations';
3 |
4 | interface StatusData {
5 | ready: boolean
6 | }
7 |
8 | export class Status implements SetupMethod {
9 | app: Application;
10 | databaseReady = false;
11 |
12 | constructor (app: Application) {
13 | this.app = app;
14 | }
15 |
16 | setup(app: Application) {
17 | // Since we cannot get the Promise's status, we have to set a local variable upon fulfilment
18 | (app.get('databaseReady') as Promise).then(() => {
19 | this.databaseReady = true;
20 | });
21 | }
22 |
23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
24 | async find (params?: Params): Promise {
25 | return {
26 | ready: this.databaseReady
27 | };
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/services/status/status.hooks.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | before: {
3 | all: [],
4 | find: [],
5 | get: [],
6 | create: [],
7 | update: [],
8 | patch: [],
9 | remove: []
10 | },
11 |
12 | after: {
13 | all: [],
14 | find: [],
15 | get: [],
16 | create: [],
17 | update: [],
18 | patch: [],
19 | remove: []
20 | },
21 |
22 | error: {
23 | all: [],
24 | find: [],
25 | get: [],
26 | create: [],
27 | update: [],
28 | patch: [],
29 | remove: []
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/server/src/services/status/status.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `status` service on path `/status`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { Status } from './status.class';
5 | import hooks from './status.hooks';
6 |
7 | // Add this service to the service type index
8 | declare module '../../declarations' {
9 | interface ServiceTypes {
10 | 'status': Status & ServiceAddons;
11 | }
12 | }
13 |
14 | export default function (app: Application): void {
15 | // Initialize our service with any options it requires
16 | app.use('/status', new Status(app));
17 |
18 | // Get our initialized service so that we can register hooks
19 | const service = app.service('status');
20 |
21 | service.hooks(hooks);
22 | }
23 |
--------------------------------------------------------------------------------
/server/src/services/users/users.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application } from '../../declarations';
3 |
4 | export class Users extends Service {
5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars
6 | constructor(options: Partial, app: Application) {
7 | super(options);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/src/services/users/users.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as feathersAuthentication from '@feathersjs/authentication';
2 | import * as local from '@feathersjs/authentication-local';
3 | import { HookContext } from '@feathersjs/feathers';
4 | import { BadRequest } from '@feathersjs/errors';
5 | // Don't remove this comment. It's needed to format import lines nicely.
6 |
7 | const { authenticate } = feathersAuthentication.hooks;
8 | const { hashPassword, protect } = local.hooks;
9 |
10 | function preventEmptyPassword (context: HookContext): HookContext {
11 | if ((context.method === 'create' || context.method === 'update') && !context.data.password) {
12 | throw new BadRequest('Password must not be empty');
13 | }
14 |
15 | if (context.data.password && context.data.password === '') {
16 | throw new BadRequest('Password must not be empty');
17 | }
18 |
19 | return context;
20 | }
21 |
22 | /**
23 | * Require the request to be authenticated, except when creating the first user
24 | *
25 | * @param context
26 | */
27 | async function maybeAuthenticate(this: any, context: HookContext): Promise {
28 | const existingUsers = await context.service.find({ query: { $limit: 0 } });
29 |
30 | if (existingUsers.total > 0) {
31 | return authenticate('jwt').call(this, context);
32 | }
33 |
34 | return context;
35 | }
36 |
37 | export default {
38 | before: {
39 | all: [],
40 | find: [ authenticate('jwt') ],
41 | get: [ authenticate('jwt') ],
42 | create: [ preventEmptyPassword, hashPassword('password'), maybeAuthenticate ],
43 | update: [ preventEmptyPassword, hashPassword('password'), authenticate('jwt') ],
44 | patch: [ preventEmptyPassword, hashPassword('password'), authenticate('jwt') ],
45 | remove: [ authenticate('jwt') ]
46 | },
47 |
48 | after: {
49 | all: [
50 | // Make sure the password field is never sent to the client
51 | // Always must be the last hook
52 | protect('password')
53 | ],
54 | find: [],
55 | get: [],
56 | create: [],
57 | update: [],
58 | patch: [],
59 | remove: []
60 | },
61 |
62 | error: {
63 | all: [],
64 | find: [],
65 | get: [],
66 | create: [],
67 | update: [],
68 | patch: [],
69 | remove: []
70 | }
71 | };
72 |
--------------------------------------------------------------------------------
/server/src/services/users/users.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `users` service on path `/users`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { Users } from './users.class';
5 | import createModel from '../../models/users.model';
6 | import hooks from './users.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'users': Users & ServiceAddons;
12 | }
13 | }
14 |
15 | export default function (app: Application): void {
16 | const options = {
17 | Model: createModel(app),
18 | paginate: app.get('paginate')
19 | };
20 |
21 | // Initialize our service with any options it requires
22 | app.use('/users', new Users(options, app));
23 |
24 | // Get our initialized service so that we can register hooks
25 | const service = app.service('users');
26 |
27 | service.hooks(hooks);
28 | }
29 |
--------------------------------------------------------------------------------
/server/src/services/views/views.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { shallowPopulate } from 'feathers-shallow-populate';
3 | import { allowApiKey } from '../../hooks/allowApiKey';
4 | import { HookContext } from '@feathersjs/feathers';
5 | // Don't remove this comment. It's needed to format import lines nicely.
6 |
7 | const { authenticate } = authentication.hooks;
8 |
9 | const populateOptions = {
10 | include: {
11 | service: 'api/v1/content-slots',
12 | nameAs: 'contentSlots',
13 | keyHere: 'id',
14 | keyThere: 'viewId',
15 | }
16 | };
17 |
18 | export default {
19 | before: {
20 | all: [ allowApiKey(), authenticate('jwt', 'api-key') ],
21 | find: [],
22 | get: [],
23 | create: [ includeContentSlots ],
24 | update: [],
25 | patch: [],
26 | remove: []
27 | },
28 |
29 | after: {
30 | all: [ shallowPopulate(populateOptions) ],
31 | find: [],
32 | get: [],
33 | create: [],
34 | update: [],
35 | patch: [],
36 | remove: []
37 | },
38 |
39 | error: {
40 | all: [],
41 | find: [],
42 | get: [],
43 | create: [],
44 | update: [],
45 | patch: [],
46 | remove: []
47 | }
48 | };
49 |
50 | /**
51 | * Automatically create nested content slots when creating a view
52 | * @param context
53 | */
54 | function includeContentSlots(context: HookContext): HookContext {
55 | const sequelize = context.app.get('sequelizeClient');
56 | context.params.sequelize = { include: [ { model: sequelize.models.content_slot, as: 'contentSlots' } ] };
57 | return context;
58 | }
59 |
--------------------------------------------------------------------------------
/server/src/services/views/views.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `views` service on path `/api/v1/views`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { Views } from './views.class';
5 | import createModel from '../../models/views.model';
6 | import hooks from './views.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'api/v1/views': Views & ServiceAddons;
12 | }
13 |
14 | interface ViewData {
15 | id: number
16 | type: string
17 | order: number
18 | columns: number
19 | rows: number
20 | displayId: number
21 | active: boolean
22 | pinned: boolean
23 | contentSlots: ContentSlotData[]
24 | }
25 | }
26 |
27 | export default function (app: Application): void {
28 | const options = {
29 | Model: createModel(app),
30 | multi: ['remove'],
31 | paginate: app.get('paginate')
32 | };
33 |
34 | // Initialize our service with any options it requires
35 | app.use('/api/v1/views', new Views(options, app));
36 |
37 | // Get our initialized service so that we can register hooks
38 | const service = app.service('api/v1/views');
39 |
40 | service.hooks(hooks);
41 | }
42 |
--------------------------------------------------------------------------------
/server/test/app.test.ts:
--------------------------------------------------------------------------------
1 | import { Server } from 'http';
2 | import url from 'url';
3 | import axios, { AxiosError } from 'axios';
4 |
5 | import app from '../src/app';
6 |
7 | const port = app.get('port') || 8998;
8 | const getUrl = (pathname?: string): string => url.format({
9 | hostname: app.get('host') || 'localhost',
10 | protocol: 'http',
11 | port,
12 | pathname
13 | });
14 |
15 | describe('Feathers application tests (with jest)', () => {
16 | let server: Server;
17 |
18 | beforeAll(done => {
19 | server = app.listen(port);
20 | (app.get('databaseReady') as Promise).then(done);
21 | }, 60000);
22 |
23 | afterAll(done => {
24 | server.close(done);
25 | });
26 |
27 | it('starts and shows the index page', async () => {
28 | expect.assertions(1);
29 |
30 | const { data } = await axios.get(getUrl());
31 |
32 | expect(data.indexOf('')).not.toBe(-1);
33 | });
34 |
35 | describe('404', () => {
36 | it('shows a 404 HTML page', async () => {
37 | expect.assertions(4);
38 |
39 | try {
40 | await axios.get(getUrl('path/to/nowhere'), {
41 | headers: {
42 | 'Accept': 'text/html'
43 | }
44 | });
45 | } catch (error) {
46 | expect(error).toBeInstanceOf(AxiosError);
47 | const { response } = error as AxiosError;
48 |
49 | expect(response?.status).toBe(404);
50 | expect(typeof response?.data).toBe('string');
51 | expect((response?.data as string).startsWith('')).toBeTruthy();
52 | }
53 | });
54 |
55 | it('shows a 404 JSON error without stack trace', async () => {
56 | expect.assertions(3);
57 |
58 | try {
59 | await axios.get(getUrl('path/to/nowhere'));
60 | } catch (error) {
61 | expect(error).toBeInstanceOf(AxiosError);
62 | const { response } = error as AxiosError;
63 |
64 | expect(response?.status).toBe(404);
65 | expect(response?.data).toMatchObject({
66 | code: 404,
67 | message: 'Page not found',
68 | name: 'NotFound'
69 | });
70 | }
71 | });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/server/test/authentication.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../src/app';
2 |
3 | describe('authentication', () => {
4 | beforeAll(done => {
5 | app.setup();
6 | // Wait for the database to be migrated / synced
7 | (app.get('databaseReady') as Promise).then(done);
8 | });
9 |
10 | it('registered the authentication service', () => {
11 | expect(app.service('authentication')).toBeTruthy();
12 | });
13 |
14 | describe('local strategy', () => {
15 | const userInfo = {
16 | email: 'someone@example.com',
17 | password: 'supersecret'
18 | };
19 |
20 | beforeAll((done) => {
21 | // Wait for the database to be migrated / synced
22 | app.service('users').create(userInfo)
23 | .then(() => done(), () => {
24 | // Do nothing, it just means the user already exists and can be tested
25 | done();
26 | });
27 | });
28 |
29 | it('authenticates user and creates accessToken', async () => {
30 | const { user, accessToken } = await app.service('authentication').create({
31 | strategy: 'local',
32 | ...userInfo
33 | }, {});
34 |
35 | expect(accessToken).toBeTruthy();
36 | expect(user).toBeTruthy();
37 | });
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/server/test/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from 'globals';
2 | import mainConfig from '../eslint.config.mjs';
3 |
4 | export default [
5 | ...mainConfig,
6 | {
7 | languageOptions: {
8 | globals: {
9 | ...globals.jest,
10 | },
11 | },
12 | }];
13 |
--------------------------------------------------------------------------------
/server/test/services/announcements.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'announcements\' service', () => {
4 | beforeAll(done => {
5 | app.setup();
6 | (app.get('databaseReady') as Promise).then(done);
7 | });
8 |
9 | it('registered the service', () => {
10 | const service = app.service('api/v1/announcements');
11 | expect(service).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/server/test/services/api-keys.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'api-keys\' service', () => {
4 | beforeAll(done => {
5 | app.setup();
6 | (app.get('databaseReady') as Promise).then(done);
7 | });
8 |
9 | it('registered the service', () => {
10 | const service = app.service('api/v1/api-keys');
11 | expect(service).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/server/test/services/calendar-feeds.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'Calendar Feeds\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('calendar-feeds');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/server/test/services/calendar-items.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, jest, it } from '@jest/globals';
2 | import app from '../../src/app';
3 | import axios from 'axios';
4 | import { CalendarItemData } from '../../src/services/calendar-items/calendar-items.class';
5 | import fs from 'fs/promises';
6 | import path from 'path';
7 |
8 | jest.mock('axios');
9 |
10 | describe('\'Calendar Items\' service', () => {
11 | beforeAll(done => {
12 | app.setup();
13 | (app.get('databaseReady') as Promise).then(done);
14 | });
15 |
16 | it('registered the service', () => {
17 | const service = app.service('calendar-items');
18 | expect(service).toBeTruthy();
19 | });
20 |
21 | it('should fetch a calendar feed', async () => {
22 | const testFeed = await fs.readFile(path.resolve(__dirname, '../feed.ics'), { encoding: 'utf-8' });
23 | axios.get.mockResolvedValue({ data: testFeed });
24 |
25 | jest.useFakeTimers();
26 | jest.setSystemTime(1723149778415); // simulate Thu Aug 08 2024 20:42:58 GMT+0000
27 |
28 | const calendarFeedsService = app.service('calendar-feeds');
29 | await calendarFeedsService.create({ name: 'Test', url: 'https://example.org/testfeed.ics' });
30 |
31 | const service = app.service('calendar-items');
32 | const items = await service.find({ paginate: false }) as CalendarItemData[];
33 |
34 | expect(axios.get).toHaveBeenCalledWith('https://example.org/testfeed.ics');
35 | expect(items.length).toBe(9);
36 | expect(items.filter(item => item.summary === 'Recurring event weekly').length).toBe(5);
37 | expect(items.filter(item => item.summary === 'Recurring event weekly only twice').length).toBe(2);
38 | expect(items.filter(item => item.summary === 'Future Event').length).toBe(1);
39 | expect(items.filter(item => item.summary === 'Past Event').length).toBe(0);
40 | expect(items.filter(item => item.summary === 'Whole day').length).toBe(1);
41 |
42 | jest.useRealTimers();
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/server/test/services/content-slots.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'content-slots\' service', () => {
4 | beforeAll(done => {
5 | app.setup();
6 | (app.get('databaseReady') as Promise).then(done);
7 | });
8 |
9 | it('registered the service', () => {
10 | const service = app.service('api/v1/content-slots');
11 | expect(service).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/server/test/services/displays.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | const servicePath = 'api/v1/displays';
4 |
5 | describe('\'displays\' service', () => {
6 | beforeAll(done => {
7 | app.setup();
8 | (app.get('databaseReady') as Promise).then(done);
9 |
10 | });
11 | it('registered the service', () => {
12 | const service = app.service(servicePath);
13 | expect(service).toBeTruthy();
14 | });
15 |
16 | it('creates a Display with just a name', async () => {
17 | const service = app.service(servicePath);
18 | const display = await service.create({ name: 'Test-Display' });
19 | expect(display).toHaveProperty('id');
20 | expect(display).toHaveProperty('name', 'Test-Display');
21 | expect(display).toHaveProperty('description', '');
22 | expect(display).toHaveProperty('active', false);
23 | expect(display).toHaveProperty('views', []);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/server/test/services/hub-connector.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'hub-connector\' service', () => {
4 | beforeAll(done => {
5 | app.setup();
6 | (app.get('databaseReady') as Promise).then(done);
7 | });
8 |
9 | it('registered the service', () => {
10 | const service = app.service('hub-connector');
11 | expect(service).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/server/test/services/incidents.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'incidents\' service', () => {
4 | beforeAll(done => {
5 | app.setup();
6 | (app.get('databaseReady') as Promise).then(done);
7 | });
8 |
9 | it('registered the service', () => {
10 | const service = app.service('api/v1/incidents');
11 | expect(service).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/server/test/services/key-requests.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'key-requests\' service', () => {
4 | beforeAll(done => {
5 | app.setup();
6 | (app.get('databaseReady') as Promise).then(done);
7 | });
8 |
9 | it('registered the service', () => {
10 | const service = app.service('api/v1/key-requests');
11 | expect(service).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/server/test/services/locations.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'locations\' service', () => {
4 | beforeAll(done => {
5 | app.setup();
6 | (app.get('databaseReady') as Promise).then(done);
7 | });
8 |
9 | it('registered the service', () => {
10 | const service = app.service('api/v1/locations');
11 | expect(service).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/server/test/services/settings.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'settings\' service', () => {
4 | beforeAll(done => {
5 | app.setup();
6 | (app.get('databaseReady') as Promise).then(done);
7 | });
8 |
9 | it('registered the service', () => {
10 | const service = app.service('api/v1/settings');
11 | expect(service).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/server/test/services/status.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'status\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('status');
6 | expect(service).toBeTruthy();
7 | });
8 |
9 | it('should return the ready status', async () => {
10 | const statusData = await app.service('status').find();
11 | expect(statusData).toEqual(expect.objectContaining({ ready: expect.any(Boolean) }));
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/server/test/services/users.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'users\' service', () => {
4 | beforeAll(done => {
5 | app.setup();
6 | (app.get('databaseReady') as Promise).then(done);
7 | });
8 |
9 | it('registered the service', () => {
10 | const service = app.service('users');
11 | expect(service).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/server/test/services/views.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'views\' service', () => {
4 | beforeAll(done => {
5 | app.setup();
6 | (app.get('databaseReady') as Promise).then(done);
7 | });
8 |
9 | it('registered the service', () => {
10 | const service = app.service('api/v1/views');
11 | expect(service).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/server/test/testSequencer.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-require-imports
2 | const Sequencer = require('@jest/test-sequencer').default;
3 |
4 | class CustomSequencer extends Sequencer {
5 | sort(tests) {
6 | const copyTests = Array.from(tests);
7 | return copyTests.sort((testA, testB) => (testA.path > testB.path ? 1 : -1));
8 | }
9 | }
10 |
11 | module.exports = CustomSequencer;
12 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2018",
4 | "module": "commonjs",
5 | "outDir": "./lib",
6 | "rootDir": "./src",
7 | "strict": true,
8 | "typeRoots" : ["./node_modules/@types", "./typings"],
9 | "esModuleInterop": true
10 | },
11 | "exclude": [
12 | "test"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/server/typings/feathers-shallow-populate/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for feathers-shallow-populate
2 | declare module 'feathers-shallow-populate' {
3 | import { Hook, HookContext, Params } from '@feathersjs/feathers';
4 |
5 | interface Include {
6 | service: string
7 | nameAs: string
8 | keyHere?: string
9 | keyThere?: string
10 | asArray?: boolean
11 | requestPerItem?: boolean
12 | catchOnError?: boolean
13 | params?: Params | typeof ParamsFunction
14 | }
15 |
16 | function ParamsFunction(params?: Params, context?: HookContext): Params | Promise | undefined;
17 |
18 | export interface PopulateOptions {
19 | include: (Include | Include[])
20 | catchOnError?: boolean
21 | }
22 |
23 | export function shallowPopulate(options: PopulateOptions): Hook
24 | }
25 |
--------------------------------------------------------------------------------
/test-api/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/test-api/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/test-api/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/test-api/.idea/test-api.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/test-api/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test-api/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec": ["spec/**/*.js"],
3 | "require": ["fixtures.mjs"],
4 | "sort": true
5 | }
6 |
--------------------------------------------------------------------------------
/test-api/fixtures.mjs:
--------------------------------------------------------------------------------
1 | import chai from 'chai'
2 | let should = chai.should();
3 |
4 | const serverAddress = process.env.SERVER_URL ? process.env.SERVER_URL : 'http://localhost:3031'
5 | const url = new URL(serverAddress)
6 | url.protocol.should.be.oneOf(['http:', 'https:'], 'The URL must begin with http or https')
7 | console.log(`Using ${url.origin} as base for all requests`);
8 |
9 | /**
10 | * Preparations that need to happen, before the tests can start.
11 | *
12 | * @return {Promise}
13 | */
14 | export async function mochaGlobalSetup() {
15 | const maxAttempts = 40
16 | console.log(`Trying to connect to ${url.origin} and determine the ready state ...`);
17 | for (let i = 0; i < maxAttempts; i++) {
18 | if (i > 0) {
19 | // Wait a bit between attempts
20 | await sleep(3000)
21 | }
22 |
23 | try {
24 | const res = await chai.request(url.origin).get('/status')
25 | res.should.have.status(200);
26 | res.body.should.include({ ready: true });
27 | console.log('[OK] The server is reachable and ready');
28 | break
29 | } catch (e) {
30 | // Only throw, if maxAttempts is reached
31 | if (i === maxAttempts - 1) {
32 | throw e
33 | }
34 | }
35 | }
36 | }
37 |
38 | export const mochaHooks = {
39 | beforeAll(done) {
40 | this.server = {
41 | base: url.origin
42 | }
43 | done();
44 | }
45 | };
46 |
47 | async function sleep (duration) {
48 | return new Promise(resolve => {
49 | setTimeout(resolve, duration)
50 | })
51 | }
52 |
--------------------------------------------------------------------------------
/test-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@alarmdisplay/display-api-test",
3 | "version": "1.0.0",
4 | "description": "Tests the REST API from a client's perspective",
5 | "scripts": {
6 | "start": "mocha"
7 | },
8 | "author": "Andreas Brain",
9 | "devDependencies": {
10 | "chai": "5.2.0",
11 | "chai-http": "5.1.2",
12 | "mocha": "11.3.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------