├── .dockerignore
├── .github
└── workflows
│ ├── docker-nightly.yml
│ ├── test-console.yml
│ └── test-server.yml
├── .gitignore
├── Dockerfile
├── Dockerfile.prebuilt
├── LICENSE
├── README.md
├── console
├── .eslintrc.json
├── .gitignore
├── .npmrc
├── .nvmrc
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── components
│ │ ├── ApiKeyEditor.vue
│ │ ├── BackButton.vue
│ │ ├── DateTimePicker.vue
│ │ ├── EditableText.vue
│ │ ├── ErrorMessage.vue
│ │ ├── IncidentDetails.vue
│ │ ├── IncidentEditor.vue
│ │ ├── IncidentListRow.vue
│ │ ├── IncidentMediaObject.vue
│ │ ├── IncidentSummary.vue
│ │ ├── PaginationUI.vue
│ │ ├── PrintTaskEditor.vue
│ │ ├── ResourceCard.vue
│ │ ├── ResourceEditor.vue
│ │ ├── ResourceIcon.vue
│ │ ├── ScheduledAlertCard.vue
│ │ ├── ScheduledAlertEditor.vue
│ │ ├── SerialMonitorEditor.vue
│ │ ├── TheNavbar.vue
│ │ ├── UserEditor.vue
│ │ ├── WatchedFolderEditor.vue
│ │ ├── admin
│ │ │ └── settings
│ │ │ │ └── BooleanValue.vue
│ │ └── input
│ │ │ ├── InputStepFormModal.vue
│ │ │ ├── SerialMonitor.vue
│ │ │ └── WatchedFolder.vue
│ ├── feathers-client.js
│ ├── font-awesome.js
│ ├── main.js
│ ├── router
│ │ └── index.js
│ ├── store
│ │ ├── index.js
│ │ ├── services
│ │ │ ├── api-keys.js
│ │ │ ├── incidents.js
│ │ │ ├── locations.js
│ │ │ ├── print-tasks.js
│ │ │ ├── resource-identifiers.js
│ │ │ ├── resources.js
│ │ │ ├── scheduled-alerts.js
│ │ │ ├── serial-monitors.js
│ │ │ ├── settings.js
│ │ │ ├── text-analysis.js
│ │ │ ├── users.js
│ │ │ └── watched-folders.js
│ │ ├── socket.js
│ │ └── store.auth.js
│ └── views
│ │ ├── AboutPage.vue
│ │ ├── IncidentForm.vue
│ │ ├── IncidentList.vue
│ │ ├── InputPage.vue
│ │ ├── LoginPage.vue
│ │ ├── OverviewPage.vue
│ │ ├── SetupPage.vue
│ │ ├── admin
│ │ ├── ApiKeyForm.vue
│ │ ├── ApiKeyList.vue
│ │ ├── SettingsPage.vue
│ │ ├── UserForm.vue
│ │ └── UserList.vue
│ │ ├── input
│ │ ├── SerialMonitorForm.vue
│ │ └── WatchedFolderForm.vue
│ │ ├── output
│ │ └── DisplayPage.vue
│ │ └── processing
│ │ ├── ResourceForm.vue
│ │ ├── ResourceList.vue
│ │ ├── ScheduledAlertsForm.vue
│ │ └── ScheduledAlertsList.vue
└── vue.config.js
├── renovate.json
├── scripts
└── build.sh
├── server
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .npmrc
├── .nvmrc
├── README.md
├── config
│ ├── ci.json
│ ├── default.json
│ ├── development.json
│ ├── docker.json
│ ├── production.json
│ └── test.json
├── 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
│ ├── feathers-factories.ts
│ ├── hooks
│ │ ├── allowApiKey.ts
│ │ ├── unserializeJson.ts
│ │ └── validateJoiSchema.ts
│ ├── index.ts
│ ├── logger.ts
│ ├── middleware
│ │ └── index.ts
│ ├── migrations
│ │ ├── 00000_create-users.ts
│ │ ├── 00001_create-api-keys.ts
│ │ ├── 00002_create-incidents.ts
│ │ ├── 00003_create-locations.ts
│ │ ├── 00004_create-resources.ts
│ │ ├── 00005_create-resource-identifiers.ts
│ │ ├── 00006_create-dispatched-resources.ts
│ │ ├── 00007_create-watched-folders.ts
│ │ ├── 00008_extend_incident_description.ts
│ │ ├── 00009_create-textanalysis.ts
│ │ ├── 00010_create-print-tasks.ts
│ │ ├── 00011_change_textanalysis_events.ts
│ │ ├── 00012_create-serial-monitors.ts
│ │ ├── 00013_create-settings.ts
│ │ ├── 00014_split-locality.ts
│ │ ├── 00015_create-processed_files.ts
│ │ └── 00016_create-scheduled-alerts.ts
│ ├── models
│ │ ├── api-keys.model.ts
│ │ ├── incidents.model.ts
│ │ ├── locations.model.ts
│ │ ├── print-tasks.model.ts
│ │ ├── processed-files.model.ts
│ │ ├── resource-identifiers.model.ts
│ │ ├── resources.model.ts
│ │ ├── scheduled-alerts.model.ts
│ │ ├── serial-monitors.model.ts
│ │ ├── settings.model.ts
│ │ ├── textanalysis.model.ts
│ │ ├── users.model.ts
│ │ └── watchedfolders.model.ts
│ ├── sequelize.ts
│ └── services
│ │ ├── alerts
│ │ ├── alerts.class.ts
│ │ ├── alerts.hooks.ts
│ │ └── alerts.service.ts
│ │ ├── api-keys
│ │ ├── api-keys.class.ts
│ │ ├── api-keys.hooks.ts
│ │ └── api-keys.service.ts
│ │ ├── incidents
│ │ ├── incidents.class.ts
│ │ ├── incidents.hooks.ts
│ │ └── incidents.service.ts
│ │ ├── index.ts
│ │ ├── input
│ │ └── pager
│ │ │ ├── pager.class.ts
│ │ │ ├── pager.hooks.ts
│ │ │ ├── pager.schemas.ts
│ │ │ └── pager.service.ts
│ │ ├── locations
│ │ ├── locations.class.ts
│ │ ├── locations.hooks.ts
│ │ ├── locations.service.ts
│ │ └── nominatim.class.ts
│ │ ├── print-tasks
│ │ ├── print-tasks.class.ts
│ │ ├── print-tasks.hooks.ts
│ │ └── print-tasks.service.ts
│ │ ├── processed-files
│ │ ├── processed-files.class.ts
│ │ ├── processed-files.hooks.ts
│ │ └── processed-files.service.ts
│ │ ├── resource-identifiers
│ │ ├── resource-identifiers.class.ts
│ │ ├── resource-identifiers.hooks.ts
│ │ └── resource-identifiers.service.ts
│ │ ├── resources
│ │ ├── resources.class.ts
│ │ ├── resources.hooks.ts
│ │ ├── resources.schemas.ts
│ │ └── resources.service.ts
│ │ ├── scheduled-alerts
│ │ ├── scheduled-alerts.class.ts
│ │ ├── scheduled-alerts.hooks.ts
│ │ └── scheduled-alerts.service.ts
│ │ ├── serial-monitors
│ │ ├── serial-monitors.class.ts
│ │ ├── serial-monitors.hooks.ts
│ │ └── serial-monitors.service.ts
│ │ ├── settings
│ │ ├── settings.class.ts
│ │ ├── settings.hooks.ts
│ │ └── settings.service.ts
│ │ ├── status
│ │ ├── status.class.ts
│ │ ├── status.hooks.ts
│ │ └── status.service.ts
│ │ ├── textanalysis
│ │ ├── analyser.class.ts
│ │ ├── configs
│ │ │ ├── ILS_Augsburg.ts
│ │ │ ├── ILS_Bamberg.ts
│ │ │ ├── ILS_Biberach.ts
│ │ │ ├── ILS_Rosenheim.ts
│ │ │ ├── LS_Bodenseekreis.ts
│ │ │ └── index.ts
│ │ ├── extractor.ts
│ │ ├── ocr.class.ts
│ │ ├── textanalysis.class.ts
│ │ ├── textanalysis.hooks.ts
│ │ └── textanalysis.service.ts
│ │ ├── users
│ │ ├── users.class.ts
│ │ ├── users.hooks.ts
│ │ └── users.service.ts
│ │ └── watchedfolders
│ │ ├── watchedfolders.class.ts
│ │ ├── watchedfolders.hooks.ts
│ │ └── watchedfolders.service.ts
├── test
│ ├── app.test.ts
│ ├── authentication.test.ts
│ ├── services
│ │ ├── alerts.test.ts
│ │ ├── api-keys.test.ts
│ │ ├── incidents.test.ts
│ │ ├── input
│ │ │ └── pager.test.ts
│ │ ├── locations.test.ts
│ │ ├── pocessed-files.test.ts
│ │ ├── print-tasks.test.ts
│ │ ├── resource-identifiers.test.ts
│ │ ├── resources.test.ts
│ │ ├── scheduled-alerts.test.ts
│ │ ├── serial-monitors.test.ts
│ │ ├── settings.test.ts
│ │ ├── status.test.ts
│ │ ├── textanalysis.test.ts
│ │ ├── users.test.ts
│ │ └── watchedfolders.test.ts
│ └── testSequencer.js
├── tsconfig.json
└── typings
│ ├── feathers-shallow-populate
│ └── index.d.ts
│ └── gauss-krueger
│ └── index.d.ts
└── test-api
├── .gitignore
├── .mocharc.json
├── fixtures.mjs
├── package-lock.json
├── package.json
└── spec
└── 001-authentication
├── common.js
├── input_pager.js
└── users.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/node_modules/
2 | server/config/local*.json
3 |
--------------------------------------------------------------------------------
/.github/workflows/docker-nightly.yml:
--------------------------------------------------------------------------------
1 | name: Docker Nightly
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '25 3 * * *'
7 |
8 | env:
9 | IMAGE_NAME: ${{ github.repository }}
10 |
11 | jobs:
12 | build:
13 |
14 | runs-on: ubuntu-24.04
15 | permissions:
16 | contents: read
17 | packages: write
18 |
19 | steps:
20 | - name: Checkout repository
21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
22 |
23 | - name: Set up QEMU
24 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
25 |
26 | - name: Set up Docker Buildx
27 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
28 | with:
29 | platforms: linux/arm/v7,linux/arm64,linux/amd64
30 |
31 | - name: Log into registry ghcr.io
32 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
33 | with:
34 | registry: ghcr.io
35 | username: ${{ github.actor }}
36 | password: ${{ secrets.GITHUB_TOKEN }}
37 |
38 | - name: Log into Docker Hub
39 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
40 | with:
41 | registry: docker.io
42 | username: ${{ secrets.DOCKER_HUB_USER }}
43 | password: ${{ secrets.DOCKER_HUB_TOKEN }}
44 |
45 | - name: Extract Docker metadata
46 | id: meta
47 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
48 | with:
49 | images: |
50 | ${{ env.IMAGE_NAME }}
51 | ghcr.io/${{ env.IMAGE_NAME }}
52 |
53 | - name: Build and push Docker image
54 | id: build-and-push
55 | uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5.4.0
56 | with:
57 | context: .
58 | platforms: linux/arm/v7,linux/arm64,linux/amd64
59 | push: true
60 | tags: ${{ steps.meta.outputs.tags }}
61 | labels: ${{ steps.meta.outputs.labels }}
62 | cache-from: type=gha
63 | cache-to: type=gha,mode=max
64 |
--------------------------------------------------------------------------------
/.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-server.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 Server
5 |
6 | on:
7 | push:
8 | branches: [ "develop" ]
9 | pull_request:
10 | branches: [ "develop" ]
11 |
12 | defaults:
13 | run:
14 | working-directory: ./server
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-latest
19 | services:
20 | mysql:
21 | image: mariadb:11.8.2@sha256:1e669024fc94f626b9dc48bf47b29b5339cec203c28e61a3dc372991a345daf5
22 | env:
23 | MARIADB_USER: hub
24 | MARIADB_PASSWORD: hub
25 | MARIADB_ROOT_PASSWORD: doesntmatter
26 | MARIADB_DATABASE: hub
27 | TZ: Europe/Berlin
28 | ports:
29 | - 3306/tcp
30 | options: --health-cmd="mariadb-admin -u root --password=$MARIADB_ROOT_PASSWORD ping" --health-interval=10s --health-timeout=5s --health-retries=3
31 |
32 | steps:
33 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
34 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
35 | with:
36 | node-version-file: 'server/.nvmrc'
37 | cache: 'npm'
38 | cache-dependency-path: server/package-lock.json
39 | - run: npm ci
40 | - run: npm run lint
41 | - run: npm run compile
42 | - run: npm run jest -- --coverage
43 | env:
44 | NODE_CONFIG_ENV: ci
45 | MYSQL_URI: mysql://hub:hub@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/hub
46 | - name: Upload code coverage to Code Climate
47 | uses: paambaati/codeclimate-action@7c100bd1ed15de0bdee476b38ca759d8c94207b5 # v8.0.0
48 | env:
49 | CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}}
50 | with:
51 | coverageLocations: |
52 | ${{github.workspace}}/server/coverage/lcov.info:lcov
53 |
54 | - name: Upload coverage reports to Codecov
55 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
56 | with:
57 | fail_ci_if_error: true
58 | token: ${{ secrets.CODECOV_TOKEN }}
59 | directory: ${{github.workspace}}/server/coverage
60 | flags: server
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20.19.3@sha256:691ef3fccb415741c5f5ecb39cc5f5a9b8122b84c5ffda53cf68f4a4963f45ff 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.3@sha256:691ef3fccb415741c5f5ecb39cc5f5a9b8122b84c5ffda53cf68f4a4963f45ff as build-server
10 |
11 | WORKDIR /home/node/app
12 | COPY ./server/package.json ./server/package-lock.json /home/node/app/
13 | RUN npm ci --no-audit
14 | COPY ./server /home/node/app
15 | RUN npm run compile
16 |
17 | FROM node:20.19.3-bookworm@sha256:691ef3fccb415741c5f5ecb39cc5f5a9b8122b84c5ffda53cf68f4a4963f45ff
18 |
19 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y -o "APT::Acquire::Retries=3" \
20 | git \
21 | imagemagick \
22 | poppler-utils \
23 | tesseract-ocr-deu \
24 | && rm -rf /var/lib/apt/lists/*
25 |
26 | # allow ImageMagick to process PDFs
27 | RUN sed -i '$i\ \ ' /etc/ImageMagick-6/policy.xml
28 |
29 | RUN adduser node dialout
30 |
31 | WORKDIR /home/node/app
32 | COPY ./server/package.json ./server/package-lock.json /home/node/app/
33 | RUN npm ci --omit=dev --no-audit
34 |
35 | COPY --from=build-server /home/node/app/lib /home/node/app
36 | COPY ./server/config /home/node/app/config
37 | COPY ./server/public /home/node/app/public
38 | COPY --from=build-console /home/node/app/console/dist /home/node/app/ext-console
39 |
40 | EXPOSE 3030
41 | ENV NODE_ENV production
42 | ENV NODE_CONFIG_ENV docker
43 | USER node
44 | CMD [ "node", "index.js" ]
45 |
--------------------------------------------------------------------------------
/Dockerfile.prebuilt:
--------------------------------------------------------------------------------
1 | FROM node:20.19.3-bookworm@sha256:691ef3fccb415741c5f5ecb39cc5f5a9b8122b84c5ffda53cf68f4a4963f45ff
2 |
3 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y -o "APT::Acquire::Retries=3" \
4 | git \
5 | imagemagick \
6 | poppler-utils \
7 | tesseract-ocr-deu \
8 | && rm -rf /var/lib/apt/lists/*
9 |
10 | # allow ImageMagick to process PDFs
11 | RUN sed -i '$i\ \ ' /etc/ImageMagick-6/policy.xml
12 |
13 | RUN adduser node dialout
14 |
15 | WORKDIR /home/node/app
16 | COPY ./build/package.json ./build/package-lock.json /home/node/app/
17 | RUN npm ci --omit=dev --no-audit
18 |
19 | COPY ./build /home/node/app
20 |
21 | EXPOSE 3030
22 | ENV NODE_ENV production
23 | ENV NODE_CONFIG_ENV docker
24 | USER node
25 | CMD [ "node", "index.js" ]
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Alarmdisplay Hub
2 |
3 | This component takes care of collecting, receiving, and processing alerts.
4 | It combines them to incidents and forwards them to other systems like the [Display](https://github.com/alarmdisplay/display).
5 |
6 | This repository contains backend and frontend code.
7 | For more info on how to run those parts, check out the README in the respective sub folder.
8 |
9 | ## Build from source
10 |
11 | ```
12 | git clone https://github.com/alarmdisplay/hub.git
13 | cd hub
14 |
15 | # Skip this step, if you want to build the development version
16 | git checkout main
17 |
18 | ./scripts/build.sh
19 | ```
20 | Now you have the runnable version in the _build_ directory.
21 |
22 | ## Deployment
23 | At the moment, this project is not recommended for deployment outside a development or test environment.
24 |
--------------------------------------------------------------------------------
/console/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "node": true
5 | },
6 | "extends": [
7 | "plugin:vue/recommended",
8 | "eslint:recommended"
9 | ],
10 | "parserOptions": {
11 | "parser": "@babel/eslint-parser"
12 | },
13 | "reportUnusedDisableDirectives": true,
14 | "rules": {
15 | "vue/html-indent": "error",
16 | "vue/html-comment-indent": "error",
17 | "vue/no-mutating-props": "off",
18 | "vue/script-indent": "error"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/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 | pnpm-debug.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 |
--------------------------------------------------------------------------------
/console/.npmrc:
--------------------------------------------------------------------------------
1 | fetch-retries=5
2 |
--------------------------------------------------------------------------------
/console/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.19.3
2 |
--------------------------------------------------------------------------------
/console/README.md:
--------------------------------------------------------------------------------
1 | # Hub Console
2 |
3 | This web application offers a user interface to manage the [Hub 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 Hub Server on http://localhost:3030, 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/hub-console",
3 | "version": "1.0.0-beta.5",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint --no-fix",
9 | "lint-fix": "vue-cli-service lint"
10 | },
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.43.0",
21 | "feathers-hooks-common": "5.0.6",
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.5",
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-plugin-vuex": "5.0.8",
36 | "@vue/cli-service": "5.0.8",
37 | "eslint": "8.57.1",
38 | "eslint-plugin-vue": "9.33.0",
39 | "vue-template-compiler": "2.6.14"
40 | },
41 | "overrides": {
42 | "feathers-vuex": {
43 | "serialize-error": "6.0.0"
44 | }
45 | },
46 | "browserslist": [
47 | "> 1%",
48 | "last 2 versions",
49 | "not dead"
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/console/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alarmdisplay/hub/d639920fe48cb943760ebc688626ee94a45b3a87/console/public/favicon.ico
--------------------------------------------------------------------------------
/console/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Zentrale – 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 |
7 |
8 | Verbunden
9 |
10 |
14 |
15 |
19 |
20 | Keine Verbindung zum Server …
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
81 |
82 |
87 |
--------------------------------------------------------------------------------
/console/src/components/ApiKeyEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
38 |
64 |
65 |
68 |
--------------------------------------------------------------------------------
/console/src/components/BackButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 | Zurück
11 |
12 |
13 |
18 |
--------------------------------------------------------------------------------
/console/src/components/DateTimePicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
60 |
61 |
64 |
--------------------------------------------------------------------------------
/console/src/components/EditableText.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ item[prop] || '' }}
9 |
13 |
14 | handler(e, save)"
22 | >
23 |
24 |
25 | {{ error.message || error.toString() }}
29 |
30 |
31 |
32 |
77 |
78 |
87 |
--------------------------------------------------------------------------------
/console/src/components/ErrorMessage.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | {{ messages[0] || 'Fehler' }}
7 |
8 |
12 |
15 |
16 |
17 |
21 | {{ message }}
22 |
23 |
24 |
25 |
26 |
27 |
52 |
--------------------------------------------------------------------------------
/console/src/components/IncidentDetails.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ incident.keyword || '?' }}
6 |
7 |
8 | {{ incident.reason || 'Grund unbekannt' }}
9 |
10 |
11 |
12 |
13 | Alarmzeit: {{ incident.time | moment('LLL') }} Uhr
14 |
15 |
16 | Gemeldet von: {{ incident.caller_name }}
17 | ({{ incident.caller_number }})
18 |
19 |
20 | Absender: {{ incident.sender }}
21 | Referenz: {{ incident.ref }}
22 |
23 |
24 |
25 | Einsatzort: {{ locationText || '-/-' }}
26 |
27 |
28 | Freitext: {{ incident.description || '-/-' }}
29 |
30 |
31 |
32 |
33 |
53 |
54 |
57 |
--------------------------------------------------------------------------------
/console/src/components/IncidentListRow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ reasonText }}
4 | {{ incident.keyword }}
5 |
6 | {{ incident.location ? [incident.location.district || incident.location.municipality, incident.location.street].join('\n').trim() : '' }}
7 |
8 |
9 | {{ incident.time | moment('LLL') }}
10 |
11 |
12 |
13 |
14 |
19 |
20 | Bearbeiten
21 |
22 |
23 |
24 |
25 |
26 |
27 |
51 |
56 |
--------------------------------------------------------------------------------
/console/src/components/IncidentMediaObject.vue:
--------------------------------------------------------------------------------
1 |
2 |
37 |
38 |
57 |
--------------------------------------------------------------------------------
/console/src/components/IncidentSummary.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ incident.keyword || '?' }}
6 |
7 |
8 | {{ incident.reason || 'Grund unbekannt' }}
9 |
10 |
11 |
12 | {{ incident.description }}
13 |
14 |
15 | {{ incident.time | moment('from', 'now') }}
16 |
17 |
18 |
19 |
20 |
37 |
38 |
41 |
--------------------------------------------------------------------------------
/console/src/components/PaginationUI.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | $emit('prev', e)"
12 | >
13 | Vorherige Seite
14 |
15 |
16 | Seite {{ currentPage }} von {{ pageCount }}
17 | $emit('next', e)"
21 | >
22 | Nächste Seite
23 |
24 |
25 |
26 |
27 |
28 |
29 |
52 |
--------------------------------------------------------------------------------
/console/src/components/ResourceIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
40 |
41 |
44 |
--------------------------------------------------------------------------------
/console/src/components/ScheduledAlertCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
14 |
18 | {{ scheduledAlert.keyword || '?' }}
19 |
20 | {{ title }}
21 |
22 | (Einsatzgrund: {{ scheduledAlert. reason }})
23 |
24 |
25 |
26 | {{ validityInfo }}
27 |
28 |
29 |
30 |
31 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
77 |
78 |
84 |
--------------------------------------------------------------------------------
/console/src/components/admin/settings/BooleanValue.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
14 | {
21 | $data.formError = null
22 | save().catch(reason => { $data.formError = reason })
23 | }"
24 | >
25 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
66 |
67 |
70 |
--------------------------------------------------------------------------------
/console/src/components/input/InputStepFormModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ modalTitle }}
8 |
9 |
14 |
15 |
16 |
17 |
18 |
22 |
23 | {
27 | $data.formError = null
28 | save()
29 | .then(() => { $emit('close-request') })
30 | .catch(reason => { $data.formError = reason })
31 | }"
32 | @reset="reset"
33 | @remove="
34 | () => {
35 | $data.formError = null
36 | remove()
37 | .then(() => { $emit('close-request') })
38 | .catch(reason => { $data.formError = reason })
39 | }"
40 | />
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
87 |
--------------------------------------------------------------------------------
/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: 'hub-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 | faAngleDown,
4 | faBell,
5 | faBellSlash,
6 | faBuilding,
7 | faCheck,
8 | faChevronDown,
9 | faChevronLeft,
10 | faChevronRight,
11 | faChevronUp,
12 | faCircle,
13 | faCog,
14 | faCogs,
15 | faDesktop,
16 | faEdit,
17 | faEnvelope,
18 | faFileAlt,
19 | faFolder,
20 | faHome,
21 | faInbox,
22 | faInfoCircle,
23 | faKey,
24 | faLock,
25 | faPager,
26 | faPaperPlane,
27 | faPause,
28 | faPlay,
29 | faPlus,
30 | faPrint,
31 | faQuestionCircle,
32 | faSignOutAlt,
33 | faSpinner,
34 | faStop,
35 | faStopwatch,
36 | faTag,
37 | faTimes,
38 | faTools,
39 | faTrashAlt,
40 | faTruck,
41 | faUser,
42 | faUserEdit,
43 | faUserMinus,
44 | faUserPlus,
45 | faUsers,
46 | faUserTag,
47 | faWaveSquare,
48 | faWrench,
49 | } from '@fortawesome/free-solid-svg-icons'
50 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
51 |
52 | library.add(
53 | faAngleDown,
54 | faBell,
55 | faBellSlash,
56 | faBuilding,
57 | faCheck,
58 | faChevronDown,
59 | faChevronLeft,
60 | faChevronRight,
61 | faChevronUp,
62 | faCircle,
63 | faCog,
64 | faCogs,
65 | faDesktop,
66 | faEdit,
67 | faEnvelope,
68 | faFileAlt,
69 | faFolder,
70 | faHome,
71 | faInbox,
72 | faInfoCircle,
73 | faKey,
74 | faLock,
75 | faPager,
76 | faPaperPlane,
77 | faPause,
78 | faPlay,
79 | faPlus,
80 | faPrint,
81 | faQuestionCircle,
82 | faSignOutAlt,
83 | faSpinner,
84 | faStop,
85 | faStopwatch,
86 | faTag,
87 | faTimes,
88 | faTools,
89 | faTrashAlt,
90 | faTruck,
91 | faUser,
92 | faUserEdit,
93 | faUserMinus,
94 | faUserPlus,
95 | faUsers,
96 | faUserTag,
97 | faWaveSquare,
98 | faWrench,
99 | )
100 |
101 | export default FontAwesomeIcon
102 |
--------------------------------------------------------------------------------
/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('FontAwesomeIcon', 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 | new Vue({
38 | router,
39 | store,
40 | data: {
41 | seconds: Math.floor(Date.now() / 1000)
42 | },
43 | mounted () {
44 | setInterval(this.updateSeconds, 1000)
45 | },
46 | methods: {
47 | updateSeconds () {
48 | this.seconds = Math.floor(Date.now() / 1000)
49 | }
50 | },
51 | render: h => h(App)
52 | }).$mount('#app')
53 |
--------------------------------------------------------------------------------
/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 apiKeys from './services/api-keys'
7 | import incidents from './services/incidents'
8 | import locations from './services/locations'
9 | import printTasks from './services/print-tasks'
10 | import resourceIdentifiers from './services/resource-identifiers'
11 | import resources from './services/resources'
12 | import scheduledAlerts from './services/scheduled-alerts'
13 | import serialMonitors from './services/serial-monitors'
14 | import settings from './services/settings'
15 | import textAnalysis from './services/text-analysis'
16 | import users from './services/users'
17 | import watchedFolders from './services/watched-folders'
18 | import socket, { createSocketPlugin } from '@/store/socket'
19 |
20 | Vue.use(Vuex)
21 | Vue.use(FeathersVuex)
22 |
23 | export default new Vuex.Store({
24 | state: {
25 | showSetup: false
26 | },
27 | mutations: {
28 | setShowSetup (state, value) {
29 | state.showSetup = value === true
30 | }
31 | },
32 | actions: {
33 | },
34 | modules: {
35 | socket
36 | },
37 | plugins: [
38 | auth,
39 | createSocketPlugin(feathersClient.io),
40 | apiKeys,
41 | incidents,
42 | locations,
43 | printTasks,
44 | resourceIdentifiers,
45 | resources,
46 | scheduledAlerts,
47 | serialMonitors,
48 | settings,
49 | textAnalysis,
50 | users,
51 | watchedFolders
52 | ]
53 | })
54 |
--------------------------------------------------------------------------------
/console/src/store/services/api-keys.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class ApiKey extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'ApiKey'
9 |
10 | static instanceDefaults() {
11 | return {
12 | name: ''
13 | }
14 | }
15 | }
16 |
17 | const servicePath = 'api-keys'
18 | const servicePlugin = makeServicePlugin({
19 | Model: ApiKey,
20 | service: feathersClient.service(servicePath),
21 | servicePath,
22 | state: {
23 | createdApiKey: null
24 | },
25 | mutations: {
26 | clearCreatedApiKey: function (state) {
27 | state.createdApiKey = null
28 | },
29 | setCreatedApiKey: function (state, apiKey) {
30 | // Make a copy of the API key for one-time display
31 | state.createdApiKey = `${apiKey.id}:${apiKey.token}`
32 | }
33 | },
34 | setupInstance: (data, { store }) => {
35 | // If this is a newly created API key that includes the token, copy the token and prevent it from being stored
36 | if (data.token) {
37 | store.commit('api-keys/setCreatedApiKey', data)
38 | delete data.token
39 | }
40 | return data
41 | }
42 | })
43 |
44 | // Setup the client-side Feathers hooks.
45 | feathersClient.service(servicePath).hooks({
46 | before: {
47 | all: [],
48 | find: [],
49 | get: [],
50 | create: [],
51 | update: [],
52 | patch: [],
53 | remove: []
54 | },
55 | after: {
56 | all: [],
57 | find: [],
58 | get: [],
59 | create: [],
60 | update: [],
61 | patch: [],
62 | remove: []
63 | },
64 | error: {
65 | all: [],
66 | find: [],
67 | get: [],
68 | create: [],
69 | update: [],
70 | patch: [],
71 | remove: []
72 | }
73 | })
74 |
75 | export default servicePlugin
76 |
--------------------------------------------------------------------------------
/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 | postCode: '',
16 | municipality: '',
17 | district: '',
18 | country: '',
19 | incidentId: undefined
20 | }
21 | }
22 | }
23 |
24 | const servicePath = 'locations'
25 | const servicePlugin = makeServicePlugin({
26 | Model: Location,
27 | service: feathersClient.service(servicePath),
28 | servicePath
29 | })
30 |
31 | // Setup the client-side Feathers hooks.
32 | feathersClient.service(servicePath).hooks({
33 | before: {
34 | all: [],
35 | find: [],
36 | get: [],
37 | create: [],
38 | update: [],
39 | patch: [],
40 | remove: []
41 | },
42 | after: {
43 | all: [],
44 | find: [],
45 | get: [],
46 | create: [],
47 | update: [],
48 | patch: [],
49 | remove: []
50 | },
51 | error: {
52 | all: [],
53 | find: [],
54 | get: [],
55 | create: [],
56 | update: [],
57 | patch: [],
58 | remove: []
59 | }
60 | })
61 |
62 | export default servicePlugin
63 |
--------------------------------------------------------------------------------
/console/src/store/services/print-tasks.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class PrintTask extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'PrintTask'
9 |
10 | static instanceDefaults() {
11 | return {
12 | event: '',
13 | sourceId: null,
14 | printerName: '',
15 | numberCopies: 1,
16 | }
17 | }
18 | }
19 |
20 | const servicePath = 'print-tasks'
21 | const servicePlugin = makeServicePlugin({
22 | Model: PrintTask,
23 | service: feathersClient.service(servicePath),
24 | servicePath
25 | })
26 |
27 | // Setup the client-side Feathers hooks.
28 | feathersClient.service(servicePath).hooks({
29 | before: {
30 | all: [],
31 | find: [],
32 | get: [],
33 | create: [],
34 | update: [],
35 | patch: [],
36 | remove: []
37 | },
38 | after: {
39 | all: [],
40 | find: [],
41 | get: [],
42 | create: [],
43 | update: [],
44 | patch: [],
45 | remove: []
46 | },
47 | error: {
48 | all: [],
49 | find: [],
50 | get: [],
51 | create: [],
52 | update: [],
53 | patch: [],
54 | remove: []
55 | }
56 | })
57 |
58 | export default servicePlugin
59 |
--------------------------------------------------------------------------------
/console/src/store/services/resource-identifiers.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class ResourceIdentifier extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'ResourceIdentifier'
9 |
10 | static instanceDefaults() {
11 | return {
12 | type: 'name',
13 | value: ''
14 | }
15 | }
16 | }
17 |
18 | const servicePath = 'resource-identifiers'
19 | const servicePlugin = makeServicePlugin({
20 | Model: ResourceIdentifier,
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/resources.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class Resource extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'Resource'
9 |
10 | static instanceDefaults() {
11 | return {
12 | identifiers: [],
13 | name: '',
14 | type: 'vehicle'
15 | }
16 | }
17 |
18 | static setupInstance(data, { models }) {
19 | if (data.identifiers && Array.isArray(data.identifiers)) {
20 | data.identifiers = data.identifiers.map(identifier => new models.api.ResourceIdentifier(identifier))
21 | }
22 | return data
23 | }
24 |
25 | clone (data) {
26 | let clone = super.clone(data)
27 | clone.identifiers = clone.identifiers.map(identifier => identifier.clone())
28 | return clone
29 | }
30 | }
31 |
32 | const servicePath = 'resources'
33 | const servicePlugin = makeServicePlugin({
34 | Model: Resource,
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 |
--------------------------------------------------------------------------------
/console/src/store/services/scheduled-alerts.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class ScheduledAlert extends BaseModel {
4 | static modelName = 'ScheduledAlert'
5 |
6 | static instanceDefaults () {
7 | const date = new Date();
8 | return {
9 | begin: date.toJSON(),
10 | end: new Date(date.valueOf() + 15 * 60 * 1000).toJSON(),
11 | reason: '',
12 | keyword: '',
13 | status: 'Test',
14 | incidentId: undefined
15 | }
16 | }
17 |
18 | static setupInstance(data) {
19 | // Convert date strings into Date objects
20 | for (const prop of ['begin', 'end', 'createdAt', 'updatedAt']) {
21 | if (data[prop]) {
22 | data[prop] = new Date(data[prop])
23 | }
24 | }
25 |
26 | return data;
27 | }
28 | }
29 |
30 | const servicePath = 'scheduled-alerts'
31 | const servicePlugin = makeServicePlugin({
32 | Model: ScheduledAlert,
33 | service: feathersClient.service(servicePath),
34 | servicePath
35 | })
36 |
37 | // Setup the client-side Feathers hooks.
38 | feathersClient.service(servicePath).hooks({
39 | before: {
40 | all: [],
41 | find: [],
42 | get: [],
43 | create: [],
44 | update: [],
45 | patch: [],
46 | remove: []
47 | },
48 | after: {
49 | all: [],
50 | find: [],
51 | get: [],
52 | create: [],
53 | update: [],
54 | patch: [],
55 | remove: []
56 | },
57 | error: {
58 | all: [],
59 | find: [],
60 | get: [],
61 | create: [],
62 | update: [],
63 | patch: [],
64 | remove: []
65 | }
66 | })
67 |
68 | export default servicePlugin
69 |
--------------------------------------------------------------------------------
/console/src/store/services/serial-monitors.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class SerialMonitor extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'SerialMonitor'
9 |
10 | static instanceDefaults() {
11 | return {
12 | port: '',
13 | baudRate: 9600,
14 | active: true,
15 | timeout: 1000
16 | }
17 | }
18 | }
19 |
20 | const servicePath = 'serial-monitors'
21 | const servicePlugin = makeServicePlugin({
22 | Model: SerialMonitor,
23 | service: feathersClient.service(servicePath),
24 | servicePath
25 | })
26 |
27 | // Setup the client-side Feathers hooks.
28 | feathersClient.service(servicePath).hooks({
29 | before: {
30 | all: [],
31 | find: [],
32 | get: [],
33 | create: [],
34 | update: [],
35 | patch: [],
36 | remove: []
37 | },
38 | after: {
39 | all: [],
40 | find: [],
41 | get: [],
42 | create: [],
43 | update: [],
44 | patch: [],
45 | remove: []
46 | },
47 | error: {
48 | all: [],
49 | find: [],
50 | get: [],
51 | create: [],
52 | update: [],
53 | patch: [],
54 | remove: []
55 | }
56 | })
57 |
58 | export default servicePlugin
59 |
--------------------------------------------------------------------------------
/console/src/store/services/settings.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class Setting extends BaseModel {
4 | static modelName = 'Setting'
5 | static idField = 'key' // Workaround, see https://github.com/feathersjs-ecosystem/feathers-vuex/issues/542
6 |
7 | static instanceDefaults () {
8 | return {
9 | key: '',
10 | value: null
11 | }
12 | }
13 | }
14 |
15 | const servicePath = 'settings'
16 | const servicePlugin = makeServicePlugin({
17 | Model: Setting,
18 | idField: 'key',
19 | getters: {
20 | getBooleanValue: (state, getters) => (id, params) => {
21 | const value = getters.getValue(id, params)
22 | return Boolean(value)
23 | },
24 | getValue: (state, getters) => (id, params) => {
25 | const setting = getters.get(id, params)
26 | return setting?.value
27 | }
28 | },
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 |
--------------------------------------------------------------------------------
/console/src/store/services/text-analysis.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class TextAnalysis extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'TextAnalysis'
9 |
10 | static instanceDefaults() {
11 | return {
12 | config: '',
13 | event: '',
14 | sourceId: null,
15 | }
16 | }
17 | }
18 |
19 | const servicePath = 'textanalysis'
20 | const servicePlugin = makeServicePlugin({
21 | Model: TextAnalysis,
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/users.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class User extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'User'
9 |
10 | static instanceDefaults() {
11 | return {
12 | name: '',
13 | email: '',
14 | }
15 | }
16 |
17 | get displayName() {
18 | return this.name || this.email
19 | }
20 | }
21 |
22 | const servicePath = 'users'
23 | const servicePlugin = makeServicePlugin({
24 | Model: User,
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/watched-folders.js:
--------------------------------------------------------------------------------
1 | import feathersClient, { makeServicePlugin, BaseModel } from '../../feathers-client'
2 |
3 | class WatchedFolder extends BaseModel {
4 | constructor(data, options) {
5 | super(data, options)
6 | }
7 |
8 | static modelName = 'WatchedFolder'
9 |
10 | static instanceDefaults() {
11 | return {
12 | path: '',
13 | active: true,
14 | polling: false
15 | }
16 | }
17 | }
18 |
19 | const servicePath = 'watchedfolders'
20 | const servicePlugin = makeServicePlugin({
21 | Model: WatchedFolder,
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/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 | socket.on('connect_error', (err) => {
33 | console.error('Socket connect error', err)
34 | store.commit('socket/setConnected', false)
35 | })
36 | socket.on('connect_timeout', (timeout) => {
37 | console.error('Socket connect timeout', timeout)
38 | })
39 | socket.on('error', (err) => {
40 | console.error('Socket error', err)
41 | })
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/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/AboutPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/console/src/views/OverviewPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Letzte Einsätze
8 |
9 |
10 |
11 |
12 |
16 |
17 | Einsatz anlegen
18 |
19 |
23 | Alle Einsätze
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
38 |
39 |
44 |
45 |
46 |
47 |
48 | Keine Einsätze gefunden
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
65 |
--------------------------------------------------------------------------------
/console/src/views/admin/ApiKeyForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | API-Key {{ id === 'new' ? 'anlegen' : 'bearbeiten' }}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 | {
23 | $data.formError = null
24 | save()
25 | .then(() => $router.push({name: 'api-key-list'}))
26 | .catch(reason => { $data.formError = reason })
27 | }"
28 | @reset="reset"
29 | />
30 |
31 |
32 |
33 |
34 |
35 |
36 |
77 |
--------------------------------------------------------------------------------
/console/src/views/admin/SettingsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Einstellungen
6 |
7 |
8 |
9 | Überwachte Ordner
10 |
11 |
12 |
13 |
14 |
15 | Einstellung
16 |
17 |
18 | Wert
19 |
20 |
21 |
22 |
23 |
24 |
25 | Bereits verarbeitete Dateien ignorieren
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
50 |
51 |
54 |
--------------------------------------------------------------------------------
/console/src/views/admin/UserForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Konto {{ id === 'new' ? 'anlegen' : 'bearbeiten' }}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 | {
23 | $data.formError = null
24 | save()
25 | .then(() => $router.push({name: 'user-list'}))
26 | .catch(reason => { $data.formError = reason })
27 | }"
28 | @reset="reset"
29 | />
30 |
31 |
32 |
33 |
34 |
35 |
36 |
78 |
--------------------------------------------------------------------------------
/console/src/views/output/DisplayPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Anzeige
6 |
7 |
8 | Die Einsätze können an die Anzeige weitergeleitet werden. Dazu benötigt der Server der Anzeige
9 | einen
10 | API-Key
11 | . Das weitere Vorgehen ist in der
12 | Dokumentation beschrieben.
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/console/src/views/processing/ResourceList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Einsatzmittel
6 |
7 |
8 |
9 | Hier konfigurierte Einsatzmittel werden in den eingehenden Alarmen identifiziert und den Einsätzen zugeordnet.
10 |
11 |
12 |
13 |
18 |
19 | Einsatzmittel anlegen
20 |
21 |
22 |
23 |
27 |
28 |
36 |
37 |
38 |
39 |
47 |
48 |
49 |
50 |
51 |
81 |
82 |
85 |
--------------------------------------------------------------------------------
/console/src/views/processing/ScheduledAlertsList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Geplante Alarmierungen
6 |
7 |
8 |
9 | Im Voraus bekannte Alarmierungen wie Probealarme oder Übungen können hier eingetragen werden. Dadurch werden neue Einsätze in diesem Zeitraum nicht als tatsächliche Einsätze behandelt.
10 |
11 |
12 |
13 |
18 |
19 | Geplante Alarmierung anlegen
20 |
21 |
22 |
23 |
27 |
28 |
36 |
37 |
38 |
39 |
47 |
48 |
49 |
50 |
51 |
81 |
82 |
85 |
--------------------------------------------------------------------------------
/console/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | devServer: {
3 | proxy: {
4 | '^/socket\\.io/': {
5 | 'target': 'http://localhost:3030/',
6 | ws: true
7 | }
8 | }
9 | },
10 | publicPath: process.env.NODE_ENV === 'production' ? '/console/' : '/'
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 | cd "$PROJECT_DIR"
35 | cp LICENSE build/
36 |
--------------------------------------------------------------------------------
/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/.eslintignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | jest.config.js
3 |
--------------------------------------------------------------------------------
/server/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "node": true
5 | },
6 | "parserOptions": {
7 | "parser": "@typescript-eslint/parser",
8 | "ecmaVersion": 2018,
9 | "sourceType": "module"
10 | },
11 | "plugins": [
12 | "@typescript-eslint"
13 | ],
14 | "extends": [
15 | "plugin:@typescript-eslint/recommended"
16 | ],
17 | "rules": {
18 | "indent": [
19 | "error",
20 | 2
21 | ],
22 | "linebreak-style": [
23 | "error",
24 | "unix"
25 | ],
26 | "quotes": [
27 | "error",
28 | "single"
29 | ],
30 | "semi": [
31 | "error",
32 | "always"
33 | ],
34 | "@typescript-eslint/no-explicit-any": "off",
35 | "@typescript-eslint/no-empty-interface": "off"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Commenting this out is preferred by some people, see
24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
25 | node_modules
26 |
27 | # Users Environment Variables
28 | .lock-wscript
29 |
30 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore)
31 | /.idea
32 | .project
33 | .classpath
34 | .c9/
35 | *.launch
36 | .settings/
37 | *.sublime-workspace
38 |
39 | # IDE - VSCode
40 | .vscode/*
41 | !.vscode/settings.json
42 | !.vscode/tasks.json
43 | !.vscode/launch.json
44 | !.vscode/extensions.json
45 |
46 | ### Linux ###
47 | *~
48 |
49 | # temporary files which can be created if a process still has a handle open of a deleted file
50 | .fuse_hidden*
51 |
52 | # KDE directory preferences
53 | .directory
54 |
55 | # Linux trash folder which might appear on any partition or disk
56 | .Trash-*
57 |
58 | # .nfs files are created when an open file is removed but is still being accessed
59 | .nfs*
60 |
61 | ### OSX ###
62 | *.DS_Store
63 | .AppleDouble
64 | .LSOverride
65 |
66 | # Icon must end with two \r
67 | Icon
68 |
69 |
70 | # Thumbnails
71 | ._*
72 |
73 | # Files that might appear in the root of a volume
74 | .DocumentRevisions-V100
75 | .fseventsd
76 | .Spotlight-V100
77 | .TemporaryItems
78 | .Trashes
79 | .VolumeIcon.icns
80 | .com.apple.timemachine.donotpresent
81 |
82 | # Directories potentially created on remote AFP share
83 | .AppleDB
84 | .AppleDesktop
85 | Network Trash Folder
86 | Temporary Items
87 | .apdisk
88 |
89 | ### Windows ###
90 | # Windows thumbnail cache files
91 | Thumbs.db
92 | ehthumbs.db
93 | ehthumbs_vista.db
94 |
95 | # Folder config file
96 | Desktop.ini
97 |
98 | # Recycle Bin used on file shares
99 | $RECYCLE.BIN/
100 |
101 | # Windows Installer files
102 | *.cab
103 | *.msi
104 | *.msm
105 | *.msp
106 |
107 | # Windows shortcuts
108 | *.lnk
109 |
110 | # Others
111 | /lib/
112 | /data/
113 | /config/local-*.json
114 | /inbox/
115 | /ext-console
116 |
--------------------------------------------------------------------------------
/server/.npmrc:
--------------------------------------------------------------------------------
1 | fetch-retries=5
2 |
--------------------------------------------------------------------------------
/server/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.19.3
2 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # Hub 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) frontend.
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:3030.
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 |
--------------------------------------------------------------------------------
/server/config/ci.json:
--------------------------------------------------------------------------------
1 | {
2 | "logging": {
3 | "level": "info"
4 | },
5 | "port": 8999,
6 | "dbConfig": {
7 | "dialect": "mysql",
8 | "connection": "MYSQL_URI"
9 | },
10 | "dbMaxRetries": 10
11 | }
12 |
--------------------------------------------------------------------------------
/server/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "host": "localhost",
3 | "port": 3030,
4 | "public": "../public/",
5 | "paginate": {
6 | "default": 10,
7 | "max": 50
8 | },
9 | "authentication": {
10 | "entity": "user",
11 | "service": "users",
12 | "secret": "sOOFQU3NnlW/+NO8v4ltQaA6Rr4=",
13 | "authStrategies": [
14 | "jwt",
15 | "local"
16 | ],
17 | "jwtOptions": {
18 | "header": {
19 | "typ": "access"
20 | },
21 | "issuer": "Alarmdisplay Hub",
22 | "algorithm": "HS256",
23 | "expiresIn": "1d"
24 | },
25 | "local": {
26 | "usernameField": "email",
27 | "passwordField": "password"
28 | }
29 | },
30 | "incidents": {
31 | "minutesBeforeNewIncident": 15
32 | },
33 | "dbConfig": {
34 | "dialect": "mysql",
35 | "connection": "MYSQL_URI"
36 | },
37 | "logging": {
38 | "level": "info"
39 | },
40 | "dbMaxRetries": 5,
41 | "validate_location": false
42 | }
43 |
--------------------------------------------------------------------------------
/server/config/development.json:
--------------------------------------------------------------------------------
1 | {
2 | "incidents": {
3 | "minutesBeforeNewIncident": 1
4 | },
5 | "logging": {
6 | "level": "debug"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/server/config/docker.json:
--------------------------------------------------------------------------------
1 | {
2 | "dbConfig": {
3 | "dialect": "mysql",
4 | "connection": "MYSQL_URI"
5 | },
6 | "validate_location": false,
7 | "logging": {
8 | "level": "LOG_LEVEL"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/server/config/production.json:
--------------------------------------------------------------------------------
1 | {
2 | "validate_location": false
3 | }
4 |
--------------------------------------------------------------------------------
/server/config/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "logging": {
3 | "level": "error"
4 | },
5 | "port": 8999,
6 | "dbConfig": {
7 | "dialect": "sqlite",
8 | "connection": "sqlite::memory:"
9 | },
10 | "dbMaxRetries": 10
11 | }
12 |
--------------------------------------------------------------------------------
/server/jest.config.js:
--------------------------------------------------------------------------------
1 | const { dirname } = require('path');
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'node',
5 | coverageDirectory: "coverage",
6 | coverageReporters: [["lcovonly", {"projectRoot": dirname(__dirname)}], ["text", {"skipFull": true}]],
7 | testSequencer: "./test/testSequencer.js",
8 | transform: {
9 | '^.+\\.tsx?$': [
10 | 'ts-jest',
11 | {
12 | diagnostics: false,
13 | },
14 | ],
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/server/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alarmdisplay/hub/d639920fe48cb943760ebc688626ee94a45b3a87/server/public/favicon.ico
--------------------------------------------------------------------------------
/server/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Alarmdisplay
7 |
8 |
9 |
10 | Alarmdisplay Zentrale
11 |
12 | Console
13 | Verwaltung von Alarmquellen, Einsätzen, usw.
14 | Zur Console
15 |
16 |
17 |
--------------------------------------------------------------------------------
/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/auth-strategies/api-key.strategy.ts:
--------------------------------------------------------------------------------
1 | import {AuthenticationBaseStrategy, AuthenticationRequest, AuthenticationResult} from '@feathersjs/authentication';
2 | import { FeathersError, NotAuthenticated } from '@feathersjs/errors';
3 | import bcrypt from 'bcryptjs';
4 | import logger from '../logger';
5 |
6 | export class ApiKeyStrategy extends AuthenticationBaseStrategy {
7 | /**
8 | * Authenticate
9 | * @param authentication
10 | */
11 | async authenticate(authentication: AuthenticationRequest): Promise {
12 | if (authentication.strategy !== 'api-key') {
13 | throw new Error(`Cannot handle authentication strategy ${authentication.strategy}`);
14 | }
15 |
16 | const apiKey = authentication['api-key'];
17 | if (!apiKey || apiKey === '') {
18 | throw new NotAuthenticated('No API key provided');
19 | }
20 |
21 | const match = apiKey.match(/^(\d+):(\w{64})$/);
22 | if (!match) {
23 | // The API key does not have the expected format
24 | throw new NotAuthenticated('API key invalid');
25 | }
26 | const [,id,token] = match;
27 |
28 | // Get the API key object for the given ID
29 | if (!this.app) {
30 | throw new Error('Cannot access main application');
31 | }
32 | const ApiKeyService = this.app.service('api-keys');
33 | let storedApiKey;
34 | try {
35 | storedApiKey = await ApiKeyService.get(id);
36 | } catch (e) {
37 | if (e instanceof FeathersError) {
38 | logger.debug(`No API key with ID ${id} found: ${e.message}`);
39 | } else {
40 | logger.debug(`No API key with ID ${id} found: ${e}`);
41 | }
42 | throw new NotAuthenticated('API key invalid');
43 | }
44 |
45 | // Compare the submitted token to its hashed version in the database
46 | if (!await bcrypt.compare(token, storedApiKey.tokenHash)) {
47 | throw new NotAuthenticated('API key invalid');
48 | }
49 |
50 | return {
51 | 'api-key': true
52 | };
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/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 { expressOauth } from '@feathersjs/authentication-oauth';
5 | import { ApiKeyStrategy } from './auth-strategies/api-key.strategy';
6 |
7 | import { Application } from './declarations';
8 |
9 | declare module './declarations' {
10 | interface ServiceTypes {
11 | 'authentication': AuthenticationService & ServiceAddons;
12 | }
13 | }
14 |
15 | export default function(app: Application) {
16 | const authentication = new AuthenticationService(app);
17 |
18 | authentication.register('jwt', new JWTStrategy());
19 | authentication.register('local', new LocalStrategy());
20 | authentication.register('api-key', new ApiKeyStrategy());
21 |
22 | app.use('/authentication', authentication);
23 | app.configure(expressOauth());
24 | }
25 |
--------------------------------------------------------------------------------
/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 {
5 | 'dummy': any; // just here to please @typescript-eslint/no-empty-object-type
6 | }
7 | // The application instance type that will be used everywhere else
8 | export type Application = ExpressFeathers;
9 |
10 |
--------------------------------------------------------------------------------
/server/src/feathers-factories.ts:
--------------------------------------------------------------------------------
1 | import FeathersApp from './app';
2 | import { Factory } from 'feathers-factory';
3 | import { fakerDE as faker } from '@faker-js/faker';
4 |
5 | export const IncidentFactory = new Factory(FeathersApp.service('incidents'), {
6 | id: () => faker.number.int({ min:1, max: 2147483647 }),
7 | time: () => new Date(),
8 | status: () => faker.helpers.arrayElement<'Actual' | 'Exercise' | 'Test'>(['Actual', 'Exercise', 'Test']),
9 | category: () => faker.helpers.arrayElement<'Geo' | 'Met' | 'Safety' | 'Security' | 'Rescue' | 'Fire' | 'Health' | 'Env' | 'Transport' | 'Infra' | 'CBRNE' | 'Other'>(['Geo', 'Met', 'Safety', 'Security', 'Rescue', 'Fire', 'Health', 'Env', 'Transport', 'Infra', 'CBRNE', 'Other']),
10 | caller_name: () => faker.person.fullName(),
11 | caller_number: () => faker.phone.number(),
12 | description: () => faker.lorem.paragraphs(2),
13 | keyword: () => faker.helpers.replaceSymbols('??? #'),
14 | location: () => LocationFactory.get(),
15 | reason: () => faker.lorem.words(3),
16 | ref: () => faker.string.uuid(),
17 | resources: [],
18 | sender: () => `Leitstelle ${faker.location.city()}`
19 | });
20 |
21 | export const LocationFactory = new Factory(FeathersApp.service('locations'), {
22 | name: () => faker.company.name(),
23 | street: () => faker.location.street(),
24 | number: () => faker.string.numeric(),
25 | detail: () => faker.location.secondaryAddress(),
26 | postCode: () => faker.location.zipCode(),
27 | municipality: () => faker.location.city(),
28 | district: () => faker.location.county(),
29 | country: () => faker.location.country(),
30 | rawText: () => faker.location.streetAddress(true)
31 | });
32 |
--------------------------------------------------------------------------------
/server/src/hooks/allowApiKey.ts:
--------------------------------------------------------------------------------
1 | import {HookContext} from '@feathersjs/feathers';
2 |
3 | export function allowApiKey() {
4 | return async (context: HookContext): Promise => {
5 | const { params } = context;
6 |
7 | // Stop, if it is an internal call or another authentication has been performed already
8 | if (!params.provider || params.authentication) {
9 | return context;
10 | }
11 |
12 | // Extract the API key from the request
13 | if(params.headers && params.headers['x-api-key']) {
14 | context.params = {
15 | ...params,
16 | authentication: {
17 | strategy: 'api-key',
18 | 'api-key': params.headers['x-api-key']
19 | }
20 | };
21 | }
22 |
23 | return context;
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/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/hooks/validateJoiSchema.ts:
--------------------------------------------------------------------------------
1 | import {Schema} from 'joi';
2 | import {HookContext} from '@feathersjs/feathers';
3 | import {BadRequest} from '@feathersjs/errors';
4 |
5 | export function validateJoiSchema(schema: Schema) {
6 | return (context: HookContext) => {
7 | const { error, value } = schema.validate(context.data);
8 | if (error) {
9 | throw new BadRequest(error);
10 | }
11 |
12 | context.data = value;
13 |
14 | return context;
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/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 | process.on('exit', () => {
12 | logger.info('Shutting down ...');
13 | const SerialMonitorsService = app.service('serial-monitors');
14 | if (SerialMonitorsService) {
15 | SerialMonitorsService.onExit();
16 | }
17 | });
18 |
19 | server.on('listening', () =>
20 | logger.info('Hub Backend started on http://%s:%d', app.get('host'), port)
21 | );
22 |
--------------------------------------------------------------------------------
/server/src/logger.ts:
--------------------------------------------------------------------------------
1 | import { getLogger } 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 |
--------------------------------------------------------------------------------
/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) {
6 | // Placeholder for middleware setup
7 | }
8 |
--------------------------------------------------------------------------------
/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-api-keys.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.createTable('api_keys', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true
11 | },
12 | name: {
13 | type: Sequelize.STRING,
14 | allowNull: false
15 | },
16 | tokenHash: {
17 | type: Sequelize.STRING,
18 | allowNull: false,
19 | unique: true,
20 | field: 'token_hash'
21 | },
22 | createdAt: {
23 | type: Sequelize.DATE,
24 | allowNull: false
25 | },
26 | updatedAt: {
27 | type: Sequelize.DATE,
28 | allowNull: false
29 | }
30 | });
31 | };
32 |
33 | export const down: Migration = async ({context: {query}}) => {
34 | await query.dropTable('api_keys');
35 | };
36 |
--------------------------------------------------------------------------------
/server/src/migrations/00002_create-incidents.ts:
--------------------------------------------------------------------------------
1 | import Sequelize 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: Sequelize.DATE,
14 | allowNull: false,
15 | defaultValue: Sequelize.NOW
16 | },
17 | sender: {
18 | type: Sequelize.STRING,
19 | allowNull: false,
20 | defaultValue: ''
21 | },
22 | ref: {
23 | type: Sequelize.STRING,
24 | allowNull: false,
25 | defaultValue: ''
26 | },
27 | caller_name: {
28 | type: Sequelize.STRING,
29 | allowNull: false,
30 | defaultValue: ''
31 | },
32 | caller_number: {
33 | type: Sequelize.STRING,
34 | allowNull: false,
35 | defaultValue: ''
36 | },
37 | reason: {
38 | type: Sequelize.STRING,
39 | allowNull: false,
40 | defaultValue: ''
41 | },
42 | keyword: {
43 | type: Sequelize.STRING,
44 | allowNull: false,
45 | defaultValue: ''
46 | },
47 | description: {
48 | type: Sequelize.STRING,
49 | allowNull: false,
50 | defaultValue: ''
51 | },
52 | status: {
53 | type: Sequelize.ENUM,
54 | values: ['Actual', 'Exercise', 'Test'],
55 | allowNull: false,
56 | defaultValue: 'Actual'
57 | },
58 | category: {
59 | type: Sequelize.ENUM,
60 | values: ['Geo', 'Met', 'Safety', 'Security', 'Rescue', 'Fire', 'Health', 'Env', 'Transport', 'Infra', 'CBRNE', 'Other'],
61 | allowNull: false,
62 | defaultValue: 'Other'
63 | },
64 | createdAt: {
65 | type: Sequelize.DATE,
66 | allowNull: false
67 | },
68 | updatedAt: {
69 | type: Sequelize.DATE,
70 | allowNull: false
71 | }
72 | });
73 | };
74 |
75 | export const down: Migration = async ({context: {query}}) => {
76 | await query.dropTable('incidents');
77 | };
78 |
--------------------------------------------------------------------------------
/server/src/migrations/00003_create-locations.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'locations';
6 |
7 | const tableExists = await query.tableExists(tableName);
8 | if (tableExists) {
9 | // Exit early if the table exists
10 | return;
11 | }
12 |
13 | await query.createTable(tableName, {
14 | id: {
15 | type: Sequelize.INTEGER,
16 | allowNull: false,
17 | autoIncrement: true,
18 | primaryKey: true
19 | },
20 | rawText: {
21 | type: Sequelize.STRING,
22 | allowNull: false
23 | },
24 | latitude: {
25 | type: Sequelize.DOUBLE,
26 | allowNull: true
27 | },
28 | longitude: {
29 | type: Sequelize.DOUBLE,
30 | allowNull: true
31 | },
32 | name: {
33 | type: Sequelize.STRING,
34 | allowNull: false,
35 | defaultValue: ''
36 | },
37 | street: {
38 | type: Sequelize.STRING,
39 | allowNull: false,
40 | defaultValue: ''
41 | },
42 | number: {
43 | type: Sequelize.STRING,
44 | allowNull: false,
45 | defaultValue: ''
46 | },
47 | detail: {
48 | type: Sequelize.STRING,
49 | allowNull: false,
50 | defaultValue: ''
51 | },
52 | postCode: {
53 | type: Sequelize.STRING,
54 | allowNull: false,
55 | defaultValue: ''
56 | },
57 | locality: {
58 | type: Sequelize.STRING,
59 | allowNull: false,
60 | defaultValue: ''
61 | },
62 | country: {
63 | type: Sequelize.STRING,
64 | allowNull: false,
65 | defaultValue: ''
66 | },
67 | createdAt: {
68 | type: Sequelize.DATE,
69 | allowNull: false
70 | },
71 | updatedAt: {
72 | type: Sequelize.DATE,
73 | allowNull: false
74 | },
75 | incidentId: {
76 | type: Sequelize.INTEGER,
77 | allowNull: true
78 | }
79 | });
80 |
81 | await query.addIndex(tableName, {
82 | name: 'incidentId',
83 | fields: ['incidentId']
84 | });
85 |
86 | await query.addConstraint(tableName, {
87 | name: `${tableName}_ibfk_1`,
88 | type: 'foreign key',
89 | fields: ['incidentId'],
90 | references: { table: 'incidents', field: 'id' },
91 | onDelete: 'CASCADE',
92 | onUpdate: 'CASCADE'
93 | });
94 | };
95 |
96 | export const down: Migration = async ({context: {query}}) => {
97 | await query.dropTable('locations');
98 | };
99 |
--------------------------------------------------------------------------------
/server/src/migrations/00004_create-resources.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.createTable('resources', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true
11 | },
12 | name: {
13 | type: Sequelize.STRING,
14 | allowNull: false
15 | },
16 | type: {
17 | type: Sequelize.ENUM,
18 | values: ['organization', 'group', 'vehicle', 'role', 'other'],
19 | allowNull: false
20 | },
21 | createdAt: {
22 | type: Sequelize.DATE,
23 | allowNull: false
24 | },
25 | updatedAt: {
26 | type: Sequelize.DATE,
27 | allowNull: false
28 | }
29 | });
30 | };
31 |
32 | export const down: Migration = async ({context: {query}}) => {
33 | await query.dropTable('resources');
34 | };
35 |
--------------------------------------------------------------------------------
/server/src/migrations/00005_create-resource-identifiers.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'resource_identifiers';
6 |
7 | const tableExists = await query.tableExists(tableName);
8 | if (tableExists) {
9 | // Exit early if the table exists
10 | return;
11 | }
12 |
13 | await query.createTable(tableName, {
14 | id: {
15 | type: Sequelize.INTEGER,
16 | allowNull: false,
17 | autoIncrement: true,
18 | primaryKey: true
19 | },
20 | type: {
21 | type: Sequelize.ENUM,
22 | values: ['name', 'selcall'],
23 | allowNull: false
24 | },
25 | value: {
26 | type: Sequelize.STRING,
27 | allowNull: false
28 | },
29 | createdAt: {
30 | type: Sequelize.DATE,
31 | allowNull: false
32 | },
33 | updatedAt: {
34 | type: Sequelize.DATE,
35 | allowNull: false
36 | },
37 | resourceId: {
38 | type: Sequelize.INTEGER,
39 | allowNull: false
40 | }
41 | });
42 |
43 | await query.addIndex(tableName, {
44 | name: 'resourceId',
45 | fields: ['resourceId']
46 | });
47 |
48 | await query.addConstraint(tableName, {
49 | name: `${tableName}_ibfk_1`,
50 | type: 'foreign key',
51 | fields: ['resourceId'],
52 | references: { table: 'resources', field: 'id' },
53 | onDelete: 'CASCADE',
54 | onUpdate: 'CASCADE'
55 | });
56 | };
57 |
58 | export const down: Migration = async ({context: {query}}) => {
59 | await query.dropTable('resource_identifiers');
60 | };
61 |
--------------------------------------------------------------------------------
/server/src/migrations/00006_create-dispatched-resources.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'dispatched_resources';
6 |
7 | const tableExists = await query.tableExists(tableName);
8 | if (tableExists) {
9 | // Exit early if the table exists
10 | return;
11 | }
12 |
13 | await query.createTable(tableName, {
14 | createdAt: {
15 | type: Sequelize.DATE,
16 | allowNull: false
17 | },
18 | updatedAt: {
19 | type: Sequelize.DATE,
20 | allowNull: false
21 | },
22 | resourceId: {
23 | type: Sequelize.INTEGER,
24 | allowNull: false,
25 | primaryKey: true
26 | },
27 | incidentId: {
28 | type: Sequelize.INTEGER,
29 | allowNull: false,
30 | primaryKey: true
31 | }
32 | });
33 |
34 | await query.addIndex(tableName, {
35 | name: 'incidentId',
36 | fields: ['incidentId']
37 | });
38 |
39 | await query.addConstraint(tableName, {
40 | name: `${tableName}_ibfk_1`,
41 | type: 'foreign key',
42 | fields: ['resourceId'],
43 | references: { table: 'resources', field: 'id' },
44 | onDelete: 'CASCADE',
45 | onUpdate: 'CASCADE'
46 | });
47 |
48 | await query.addConstraint(tableName, {
49 | name: `${tableName}_ibfk_2`,
50 | type: 'foreign key',
51 | fields: ['incidentId'],
52 | references: { table: 'incidents', field: 'id' },
53 | onDelete: 'CASCADE',
54 | onUpdate: 'CASCADE'
55 | });
56 | };
57 |
58 | export const down: Migration = async ({context: {query}}) => {
59 | await query.dropTable('dispatched_resources');
60 | };
61 |
--------------------------------------------------------------------------------
/server/src/migrations/00007_create-watched-folders.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.createTable('watched_folders', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true
11 | },
12 | path: {
13 | type: Sequelize.STRING,
14 | allowNull: false
15 | },
16 | active: {
17 | type: Sequelize.BOOLEAN,
18 | allowNull: false,
19 | defaultValue: true
20 | },
21 | polling: {
22 | type: Sequelize.BOOLEAN,
23 | allowNull: false,
24 | defaultValue: 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('watched_folders');
39 | };
40 |
--------------------------------------------------------------------------------
/server/src/migrations/00008_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 |
12 | export const down: Migration = async ({context: {query}}) => {
13 | await query.changeColumn('incidents', 'description', {
14 | type: Sequelize.STRING,
15 | allowNull: false,
16 | defaultValue: ''
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/server/src/migrations/00009_create-textanalysis.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'textanalysis';
6 |
7 | const tableExists = await query.tableExists(tableName);
8 | if (tableExists) {
9 | // Exit early if the table exists
10 | return;
11 | }
12 |
13 | await query.createTable(tableName, {
14 | id: {
15 | type: Sequelize.INTEGER,
16 | allowNull: false,
17 | autoIncrement: true,
18 | primaryKey: true
19 | },
20 | config: {
21 | type: Sequelize.STRING,
22 | allowNull: false
23 | },
24 | watchedFolderId: {
25 | type: Sequelize.INTEGER,
26 | allowNull: false
27 | },
28 | createdAt: {
29 | type: Sequelize.DATE,
30 | allowNull: false
31 | },
32 | updatedAt: {
33 | type: Sequelize.DATE,
34 | allowNull: false
35 | }
36 | });
37 |
38 | await query.addConstraint(tableName, {
39 | name: 'watchedFolderId',
40 | type: 'unique',
41 | fields: ['watchedFolderId']
42 | });
43 |
44 | await query.addConstraint(tableName, {
45 | name: `${tableName}_ibfk_1`,
46 | type: 'foreign key',
47 | fields: ['watchedFolderId'],
48 | references: { table: 'watched_folders', field: 'id' },
49 | onDelete: 'CASCADE',
50 | onUpdate: 'CASCADE'
51 | });
52 | };
53 |
54 | export const down: Migration = async ({context: {query}}) => {
55 | await query.dropTable('textanalysis');
56 | };
57 |
--------------------------------------------------------------------------------
/server/src/migrations/00010_create-print-tasks.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'print_tasks';
6 |
7 | const tableExists = await query.tableExists(tableName);
8 | if (tableExists) {
9 | // Exit early if the table exists
10 | return;
11 | }
12 |
13 | await query.createTable(tableName, {
14 | id: {
15 | type: Sequelize.INTEGER,
16 | allowNull: false,
17 | autoIncrement: true,
18 | primaryKey: true
19 | },
20 | event: {
21 | type: Sequelize.STRING,
22 | allowNull: false
23 | },
24 | sourceId: {
25 | type: Sequelize.INTEGER,
26 | allowNull: true
27 | },
28 | printerName: {
29 | type: Sequelize.STRING,
30 | allowNull: false,
31 | defaultValue: '',
32 | },
33 | numberCopies: {
34 | type: Sequelize.TINYINT,
35 | allowNull: false,
36 | defaultValue: 1,
37 | },
38 | createdAt: {
39 | type: Sequelize.DATE,
40 | allowNull: false
41 | },
42 | updatedAt: {
43 | type: Sequelize.DATE,
44 | allowNull: false
45 | }
46 | });
47 | };
48 |
49 | export const down: Migration = async ({context: {query}}) => {
50 | await query.dropTable('print_tasks');
51 | };
52 |
--------------------------------------------------------------------------------
/server/src/migrations/00011_change_textanalysis_events.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'textanalysis';
6 |
7 | // Add the new event column and automatically fill it with the default value
8 | await query.addColumn(tableName, 'event', {
9 | type: Sequelize.STRING,
10 | allowNull: false,
11 | defaultValue: 'found_file'
12 | });
13 |
14 | // Transform watchedFolderId column to more general sourceId column
15 | await query.removeConstraint(tableName, `${tableName}_ibfk_1`);
16 | await query.removeConstraint(tableName, 'watchedFolderId');
17 | await query.changeColumn(tableName, 'watchedFolderId', {
18 | type: Sequelize.INTEGER,
19 | allowNull: true
20 | });
21 | await query.renameColumn(tableName, 'watchedFolderId', 'sourceId');
22 | };
23 |
24 | export const down: Migration = async ({context: {query}}) => {
25 | const tableName = 'textanalysis';
26 |
27 | // Transform general sourceId column back to watchedFolderId column
28 | await query.renameColumn(tableName, 'sourceId', 'watchedFolderId');
29 | await query.changeColumn(tableName, 'watchedFolderId', {
30 | type: Sequelize.INTEGER,
31 | allowNull: false,
32 | unique: true
33 | });
34 | await query.addConstraint(tableName, {
35 | name: `${tableName}_ibfk_1`,
36 | type: 'foreign key',
37 | fields: ['watchedFolderId'],
38 | references: { table: 'watched_folders', field: 'id' },
39 | onDelete: 'CASCADE',
40 | onUpdate: 'CASCADE'
41 | });
42 |
43 | await query.removeColumn(tableName, 'event');
44 | };
45 |
--------------------------------------------------------------------------------
/server/src/migrations/00012_create-serial-monitors.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | await query.createTable('serial_monitors', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | allowNull: false,
9 | autoIncrement: true,
10 | primaryKey: true
11 | },
12 | port: {
13 | type: Sequelize.STRING,
14 | allowNull: false
15 | },
16 | baudRate: {
17 | type: Sequelize.INTEGER,
18 | allowNull: false,
19 | defaultValue: 9600
20 | },
21 | active: {
22 | type: Sequelize.BOOLEAN,
23 | allowNull: false,
24 | defaultValue: true
25 | },
26 | timeout: {
27 | type: Sequelize.INTEGER,
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 |
41 | export const down: Migration = async ({context: {query}}) => {
42 | await query.dropTable('serial_monitors');
43 | };
44 |
--------------------------------------------------------------------------------
/server/src/migrations/00013_create-settings.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'settings';
6 |
7 | const tableExists = await query.tableExists(tableName);
8 | if (tableExists) {
9 | // Exit early if the table exists
10 | return;
11 | }
12 |
13 | await query.createTable(tableName, {
14 | key: {
15 | type: Sequelize.STRING,
16 | allowNull: false,
17 | primaryKey: true
18 | },
19 | value: {
20 | type: Sequelize.JSON,
21 | allowNull: true
22 | },
23 | createdAt: {
24 | type: Sequelize.DATE,
25 | allowNull: false
26 | },
27 | updatedAt: {
28 | type: Sequelize.DATE,
29 | allowNull: false
30 | }
31 | });
32 | };
33 |
34 | export const down: Migration = async ({context: {query}}) => {
35 | await query.dropTable('settings');
36 | };
37 |
--------------------------------------------------------------------------------
/server/src/migrations/00014_split-locality.ts:
--------------------------------------------------------------------------------
1 | import Sequelize 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: Sequelize.STRING,
10 | allowNull: false,
11 | defaultValue: ''
12 | });
13 | };
14 |
15 | export const down: Migration = async ({context: {query}}) => {
16 | const tableName = 'locations';
17 | await query.removeColumn(tableName, 'district');
18 | await query.renameColumn(tableName, 'municipality', 'locality');
19 | };
20 |
--------------------------------------------------------------------------------
/server/src/migrations/00015_create-processed_files.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'processed_files';
6 |
7 | const tableExists = await query.tableExists(tableName);
8 | if (tableExists) {
9 | // Exit early if the table exists
10 | return;
11 | }
12 |
13 | await query.createTable(tableName, {
14 | hash: {
15 | type: Sequelize.STRING,
16 | allowNull: false,
17 | primaryKey: true
18 | },
19 | createdAt: {
20 | type: Sequelize.DATE,
21 | allowNull: false
22 | }
23 | });
24 | };
25 |
26 | export const down: Migration = async ({context: {query}}) => {
27 | await query.dropTable('processed_files');
28 | };
29 |
--------------------------------------------------------------------------------
/server/src/migrations/00016_create-scheduled-alerts.ts:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import { Migration } from '../sequelize';
3 |
4 | export const up: Migration = async ({context: {query}}) => {
5 | const tableName = 'scheduled_alerts';
6 |
7 | await query.createTable(tableName, {
8 | id: { type: Sequelize.INTEGER, allowNull: false, autoIncrement: true, primaryKey: true },
9 | begin: { type: Sequelize.DATE, allowNull: false },
10 | end: { type: Sequelize.DATE, allowNull: false },
11 | reason: { type: Sequelize.STRING, allowNull: false, defaultValue: '' },
12 | keyword: { type: Sequelize.STRING, allowNull: false, defaultValue: '' },
13 | status: { type: Sequelize.ENUM, values: ['Exercise', 'Test'], allowNull: false, defaultValue: 'Test' },
14 | incidentId: { type: Sequelize.INTEGER, allowNull: true },
15 | createdAt: { type: Sequelize.DATE, allowNull: false },
16 | updatedAt: { type: Sequelize.DATE, allowNull: false }
17 | });
18 |
19 | await query.addIndex(tableName, { name: 'incidentId', fields: ['incidentId'] });
20 | await query.addConstraint(tableName, {
21 | name: `${tableName}_ibfk_1`,
22 | type: 'foreign key',
23 | fields: ['incidentId'],
24 | references: { table: 'incidents', field: 'id' },
25 | onDelete: 'SET NULL',
26 | onUpdate: 'CASCADE'
27 | });
28 | };
29 |
30 | export const down: Migration = async ({context: {query}}) => {
31 | await query.dropTable('scheduled_alerts');
32 | };
33 |
--------------------------------------------------------------------------------
/server/src/models/api-keys.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 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 | field: 'token_hash'
17 | }
18 | }, {
19 | hooks: {
20 | beforeCount(options: any): HookReturn {
21 | options.raw = true;
22 | }
23 | },
24 | tableName: 'api_keys'
25 | });
26 |
27 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
28 | (ApiKey as any).associate = function (models: any): void {
29 | // Define associations here
30 | // See http://docs.sequelizejs.com/en/latest/docs/associations/
31 | };
32 |
33 | return ApiKey;
34 | }
35 |
--------------------------------------------------------------------------------
/server/src/models/incidents.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 Incident = sequelizeClient.define('incident', {
8 | time: {
9 | type: DataTypes.DATE,
10 | allowNull: false,
11 | defaultValue: DataTypes.NOW
12 | },
13 | sender: {
14 | type: DataTypes.STRING,
15 | allowNull: false,
16 | defaultValue: ''
17 | },
18 | ref: {
19 | type: DataTypes.STRING,
20 | allowNull: false,
21 | defaultValue: ''
22 | },
23 | caller_name: {
24 | type: DataTypes.STRING,
25 | allowNull: false,
26 | defaultValue: ''
27 | },
28 | caller_number: {
29 | type: DataTypes.STRING,
30 | allowNull: false,
31 | defaultValue: ''
32 | },
33 | reason: {
34 | type: DataTypes.STRING,
35 | allowNull: false,
36 | defaultValue: ''
37 | },
38 | keyword: {
39 | type: DataTypes.STRING,
40 | allowNull: false,
41 | defaultValue: ''
42 | },
43 | description: {
44 | type: DataTypes.TEXT,
45 | allowNull: false,
46 | defaultValue: ''
47 | },
48 | status: {
49 | type: DataTypes.ENUM,
50 | values: ['Actual', 'Exercise', 'Test'],
51 | allowNull: false,
52 | defaultValue: 'Actual'
53 | },
54 | category: {
55 | type: DataTypes.ENUM,
56 | values: ['Geo', 'Met', 'Safety', 'Security', 'Rescue', 'Fire', 'Health', 'Env', 'Transport', 'Infra', 'CBRNE', 'Other'],
57 | allowNull: false,
58 | defaultValue: 'Other'
59 | }
60 | }, {
61 | hooks: {
62 | beforeCount(options: any): HookReturn {
63 | options.raw = true;
64 | }
65 | },
66 | tableName: 'incidents'
67 | });
68 |
69 | (Incident as any).associate = function (models: any): void {
70 | models.incident.belongsToMany(models.resource, {
71 | through: 'dispatched_resources',
72 | as: 'resources'
73 | });
74 | models.incident.hasOne(models.locations);
75 | models.incident.hasOne(models.scheduled_alert);
76 | };
77 |
78 | return Incident;
79 | }
80 |
--------------------------------------------------------------------------------
/server/src/models/locations.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 locations = sequelizeClient.define('locations', {
10 | rawText: {
11 | type: DataTypes.STRING,
12 | allowNull: false
13 | },
14 | latitude: {
15 | type: DataTypes.DOUBLE,
16 | allowNull: true
17 | },
18 | longitude: {
19 | type: DataTypes.DOUBLE,
20 | allowNull: true
21 | },
22 | name: {
23 | type: DataTypes.STRING,
24 | allowNull: false,
25 | defaultValue: ''
26 | },
27 | street: {
28 | type: DataTypes.STRING,
29 | allowNull: false,
30 | defaultValue: ''
31 | },
32 | number: {
33 | type: DataTypes.STRING,
34 | allowNull: false,
35 | defaultValue: ''
36 | },
37 | detail: {
38 | type: DataTypes.STRING,
39 | allowNull: false,
40 | defaultValue: ''
41 | },
42 | postCode: {
43 | type: DataTypes.STRING,
44 | allowNull: false,
45 | defaultValue: ''
46 | },
47 | municipality: {
48 | type: DataTypes.STRING,
49 | allowNull: false,
50 | defaultValue: ''
51 | },
52 | district: {
53 | type: DataTypes.STRING,
54 | allowNull: false,
55 | defaultValue: ''
56 | },
57 | country: {
58 | type: DataTypes.STRING,
59 | allowNull: false,
60 | defaultValue: ''
61 | }
62 | }, {
63 | hooks: {
64 | beforeCount(options: any): HookReturn {
65 | options.raw = true;
66 | }
67 | },
68 | tableName: 'locations'
69 | });
70 |
71 | (locations as any).associate = function (models: any): void {
72 | models.locations.belongsTo(models.incident, {
73 | onDelete: 'CASCADE',
74 | onUpdate: 'CASCADE'
75 | });
76 | };
77 |
78 | return locations;
79 | }
80 |
--------------------------------------------------------------------------------
/server/src/models/print-tasks.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 PrintTask = sequelizeClient.define('print_task', {
8 | event: {
9 | type: DataTypes.STRING,
10 | allowNull: false
11 | },
12 | sourceId: {
13 | type: DataTypes.INTEGER,
14 | allowNull: true
15 | },
16 | printerName: {
17 | type: DataTypes.STRING,
18 | allowNull: false,
19 | defaultValue: '',
20 | },
21 | numberCopies: {
22 | type: DataTypes.TINYINT,
23 | allowNull: false,
24 | defaultValue: 1,
25 | },
26 | }, {
27 | hooks: {
28 | beforeCount(options: any): HookReturn {
29 | options.raw = true;
30 | }
31 | },
32 | tableName: 'print_tasks',
33 | });
34 | return PrintTask;
35 | }
36 |
--------------------------------------------------------------------------------
/server/src/models/processed-files.model.ts:
--------------------------------------------------------------------------------
1 | // See https://sequelize.org/master/manual/model-basics.html
2 | // for more of what you can do here.
3 | import { Sequelize, DataTypes, Model } from 'sequelize';
4 | import { Application } from '../declarations';
5 | import { HookReturn } from 'sequelize/types/hooks';
6 |
7 | export default function (app: Application): typeof Model {
8 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
9 | const ProcessedFile = sequelizeClient.define('processed_file', {
10 | hash: {
11 | type: DataTypes.STRING,
12 | allowNull: false,
13 | primaryKey: true
14 | }
15 | }, {
16 | hooks: {
17 | beforeCount(options: any): HookReturn {
18 | options.raw = true;
19 | }
20 | },
21 | tableName: 'processed_files',
22 | updatedAt: false
23 | });
24 |
25 | return ProcessedFile;
26 | }
27 |
--------------------------------------------------------------------------------
/server/src/models/resource-identifiers.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 ResourceIdentifier = sequelizeClient.define('resource_identifier', {
8 | type: {
9 | type: DataTypes.ENUM,
10 | values: ['name', 'selcall'],
11 | allowNull: false
12 | },
13 | value: {
14 | type: DataTypes.STRING,
15 | allowNull: false
16 | }
17 | }, {
18 | hooks: {
19 | beforeCount(options: any): HookReturn {
20 | options.raw = true;
21 | }
22 | },
23 | tableName: 'resource_identifiers'
24 | });
25 |
26 | (ResourceIdentifier as any).associate = function (models: any): void {
27 | models.resource_identifier.belongsTo(models.resource, { as: 'resource' });
28 | };
29 |
30 | return ResourceIdentifier;
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/models/resources.model.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize, DataTypes } from 'sequelize';
2 | import { Application } from '../declarations';
3 |
4 | export default function (app: Application) {
5 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
6 | const resources = sequelizeClient.define('resource', {
7 | name: {
8 | type: DataTypes.STRING,
9 | allowNull: false
10 | },
11 | type: {
12 | type: DataTypes.ENUM,
13 | values: ['organization', 'group', 'vehicle', 'role', 'other'],
14 | allowNull: false
15 | }
16 | }, {
17 | hooks: {
18 | beforeCount(options: any) {
19 | options.raw = true;
20 | }
21 | },
22 | tableName: 'resources'
23 | });
24 |
25 | (resources as any).associate = function (models: any) {
26 | models.resource.hasMany(models.resource_identifier, {
27 | foreignKey: { allowNull: false },
28 | as: 'identifiers'
29 | });
30 | models.resource.belongsToMany(models.incident, { through: 'dispatched_resources' });
31 | };
32 |
33 | return resources;
34 | }
35 |
--------------------------------------------------------------------------------
/server/src/models/scheduled-alerts.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 ScheduledAlert = sequelizeClient.define('scheduled_alert', {
8 | begin: { type: DataTypes.DATE, allowNull: false },
9 | end: { type: DataTypes.DATE, allowNull: false },
10 | reason: { type: DataTypes.STRING, allowNull: false, defaultValue: '' },
11 | keyword: { type: DataTypes.STRING, allowNull: false, defaultValue: '' },
12 | status: { type: DataTypes.ENUM, values: ['Exercise', 'Test'], allowNull: false, defaultValue: 'Test' },
13 | }, {
14 | hooks: {
15 | beforeCount(options: any): HookReturn {
16 | options.raw = true;
17 | }
18 | },
19 | tableName: 'scheduled_alerts'
20 | });
21 |
22 | (ScheduledAlert as any).associate = function (models: any): void {
23 | models.scheduled_alert.belongsTo(models.incident);
24 | };
25 |
26 | return ScheduledAlert;
27 | }
28 |
--------------------------------------------------------------------------------
/server/src/models/serial-monitors.model.ts:
--------------------------------------------------------------------------------
1 | // See http://docs.sequelizejs.com/en/latest/docs/models-definition/
2 | // for more of what you can do here.
3 | import { DataTypes, Model, Sequelize } 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 SerialMonitor = sequelizeClient.define('serial_monitor', {
10 | port: {
11 | type: DataTypes.STRING,
12 | allowNull: false
13 | },
14 | baudRate: {
15 | type: DataTypes.INTEGER,
16 | allowNull: false,
17 | defaultValue: 9600
18 | },
19 | active: {
20 | type: DataTypes.BOOLEAN,
21 | allowNull: false,
22 | defaultValue: true
23 | },
24 | timeout: {
25 | type: DataTypes.INTEGER,
26 | allowNull: false,
27 | defaultValue: 1000
28 | }
29 | }, {
30 | hooks: {
31 | beforeCount(options: any): HookReturn {
32 | options.raw = true;
33 | }
34 | },
35 | tableName: 'serial_monitors'
36 | });
37 | return SerialMonitor;
38 | }
39 |
--------------------------------------------------------------------------------
/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 { DataTypes, Model, Sequelize } 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/textanalysis.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, Model, DataTypes } 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 TextAnalysis = sequelizeClient.define('textanalysis', {
10 | config: {
11 | type: DataTypes.STRING,
12 | allowNull: false
13 | },
14 | event: {
15 | type: DataTypes.STRING,
16 | allowNull: false
17 | },
18 | sourceId: {
19 | type: DataTypes.INTEGER,
20 | allowNull: true
21 | }
22 | }, {
23 | hooks: {
24 | beforeCount(options: any): HookReturn {
25 | options.raw = true;
26 | }
27 | },
28 | tableName: 'textanalysis'
29 | });
30 | return TextAnalysis;
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/models/users.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 } from 'sequelize';
4 | import { Application } from '../declarations';
5 |
6 | export default function (app: Application) {
7 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
8 | const users = sequelizeClient.define('users', {
9 | email: {
10 | type: DataTypes.STRING,
11 | validate: {
12 | isEmail: true
13 | },
14 | allowNull: false,
15 | unique: true
16 | },
17 | name: {
18 | type: DataTypes.STRING,
19 | allowNull: false,
20 | defaultValue: ''
21 | },
22 | password: {
23 | type: DataTypes.STRING,
24 | allowNull: false
25 | },
26 | }, {
27 | hooks: {
28 | beforeCount(options: any) {
29 | options.raw = true;
30 | }
31 | },
32 | tableName: 'users'
33 | });
34 |
35 | return users;
36 | }
37 |
--------------------------------------------------------------------------------
/server/src/models/watchedfolders.model.ts:
--------------------------------------------------------------------------------
1 | // See http://docs.sequelizejs.com/en/latest/docs/models-definition/
2 | // for more of what you can do here.
3 | import { DataTypes, Sequelize } from 'sequelize';
4 | import { Application } from '../declarations';
5 |
6 | export default function (app: Application) {
7 | const sequelizeClient: Sequelize = app.get('sequelizeClient');
8 | return sequelizeClient.define('watched_folder', {
9 | path: {
10 | type: DataTypes.STRING,
11 | allowNull: false
12 | },
13 | active: {
14 | type: DataTypes.BOOLEAN,
15 | allowNull: false,
16 | defaultValue: true
17 | },
18 | polling: {
19 | type: DataTypes.BOOLEAN,
20 | allowNull: false,
21 | defaultValue: false
22 | }
23 | }, {
24 | hooks: {
25 | beforeCount(options: any) {
26 | options.raw = true;
27 | }
28 | },
29 | tableName: 'watched_folders'
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/services/alerts/alerts.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | // Don't remove this comment. It's needed to format import lines nicely.
3 |
4 | const { authenticate } = authentication.hooks;
5 |
6 | export default {
7 | before: {
8 | all: [ authenticate('jwt') ],
9 | find: [],
10 | get: [],
11 | create: [],
12 | update: [],
13 | patch: [],
14 | remove: []
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/alerts/alerts.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `Alerts` service on path `/alerts`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { Alerts } from './alerts.class';
5 | import hooks from './alerts.hooks';
6 |
7 | // Add this service to the service type index
8 | declare module '../../declarations' {
9 | interface ServiceTypes {
10 | 'alerts': Alerts & ServiceAddons;
11 | }
12 | }
13 |
14 | export default function (app: Application): void {
15 | const options = {
16 | paginate: app.get('paginate')
17 | };
18 |
19 | // Initialize our service with any options it requires
20 | app.use('/alerts', new Alerts(options, app));
21 |
22 | // Get our initialized service so that we can register hooks
23 | const service = app.service('alerts');
24 |
25 | service.hooks(hooks);
26 | }
27 |
--------------------------------------------------------------------------------
/server/src/services/api-keys/api-keys.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application, ApiKeyData } 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-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-keys': ApiKeys & ServiceAddons;
12 | }
13 |
14 | interface ApiKeyData {
15 | name: string
16 | tokenHash: string
17 | }
18 | }
19 |
20 | export default function (app: Application): void {
21 | const options = {
22 | Model: createModel(app),
23 | paginate: app.get('paginate')
24 | };
25 |
26 | // Initialize our service with any options it requires
27 | app.use('/api-keys', new ApiKeys(options, app));
28 |
29 | // Get our initialized service so that we can register hooks
30 | const service = app.service('api-keys');
31 |
32 | service.hooks(hooks);
33 |
34 | // Prevent publishing of any newly created token
35 | service.publish('created', () => {
36 | // Do not return a channel to publish to
37 | return;
38 | });
39 | }
40 |
--------------------------------------------------------------------------------
/server/src/services/incidents/incidents.class.ts:
--------------------------------------------------------------------------------
1 | import { SequelizeServiceOptions, Service } from 'feathers-sequelize';
2 | import { Application, IncidentData } from '../../declarations';
3 | import { NullableId, Params } from '@feathersjs/feathers';
4 |
5 | export class Incidents extends Service {
6 | private app: Application;
7 |
8 | constructor(options: Partial, app: Application) {
9 | super(options);
10 | this.app = app;
11 | }
12 |
13 | async _patch(id: NullableId, data: Partial, params?: Params): Promise {
14 | if (Object.keys(data).includes('location')) {
15 | const LocationsService = this.app.service('locations');
16 | if (data.location && data.location.id) {
17 | // Existing location submitted, try to patch it
18 | data.location.incidentId = id as number;
19 | await LocationsService.patch(data.location.id, data.location);
20 | } else if (data.location) {
21 | // New location submitted, create it and remove any locations that might belong to this incident beforehand
22 | await LocationsService.remove(null, { query: { incidentId: id } });
23 | data.location.incidentId = id as number;
24 | await LocationsService.create(data.location);
25 | } else {
26 | // The location is intentionally left empty, remove all existing ones
27 | await LocationsService.remove(null, { query: { incidentId: id } });
28 | }
29 | }
30 | return await super._patch(id, data, params);
31 | }
32 |
33 | async _update(id: NullableId, data: IncidentData, params?: Params): Promise {
34 | if (data.location && data.location.id) {
35 | await this.app.service('locations').update(data.location.id, data.location);
36 | } else {
37 | // The location is intentionally left empty, remove all existing ones
38 | await this.app.service('locations').remove(null, { query: { incidentId: id } });
39 | }
40 | return super._update(id, data, params);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/server/src/services/index.ts:
--------------------------------------------------------------------------------
1 | import { Application } from '../declarations';
2 | import users from './users/users.service';
3 | import watchedfolders from './watchedfolders/watchedfolders.service';
4 | import textanalysis from './textanalysis/textanalysis.service';
5 | import resources from './resources/resources.service';
6 | import resourceIdentifiers from './resource-identifiers/resource-identifiers.service';
7 | import locations from './locations/locations.service';
8 | import incidents from './incidents/incidents.service';
9 | import apiKeys from './api-keys/api-keys.service';
10 | import inputPager from './input/pager/pager.service';
11 | import printTasks from './print-tasks/print-tasks.service';
12 | import serialMonitors from './serial-monitors/serial-monitors.service';
13 | import settings from './settings/settings.service';
14 | import processedFiles from './processed-files/processed-files.service';
15 | import status from './status/status.service';
16 | import scheduledAlerts from './scheduled-alerts/scheduled-alerts.service';
17 | import alerts from './alerts/alerts.service';
18 | // Don't remove this comment. It's needed to format import lines nicely.
19 |
20 | export default function (app: Application) {
21 | app.configure(users);
22 | app.configure(watchedfolders);
23 | app.configure(textanalysis);
24 | app.configure(resources);
25 | app.configure(resourceIdentifiers);
26 | app.configure(locations);
27 | app.configure(incidents);
28 | app.configure(apiKeys);
29 | app.configure(inputPager);
30 | app.configure(printTasks);
31 | app.configure(serialMonitors);
32 | app.configure(settings);
33 | app.configure(processedFiles);
34 | app.configure(status);
35 | app.configure(scheduledAlerts);
36 | app.configure(alerts);
37 | }
38 |
--------------------------------------------------------------------------------
/server/src/services/input/pager/pager.class.ts:
--------------------------------------------------------------------------------
1 | import { Application, ResourceData, ResourceIdentifierData } from '../../../declarations';
2 | import { NotFound } from '@feathersjs/errors';
3 | import { AlertSourceType } from '../../incidents/incidents.service';
4 | import { PaginationOptions } from '@feathersjs/feathers';
5 |
6 | interface PagerData {
7 | selcall: string
8 | }
9 |
10 | interface PagerResponse {
11 | incidentId: number
12 | }
13 |
14 | interface ServiceOptions {
15 | paginate: PaginationOptions
16 | }
17 |
18 | export class Pager {
19 | app: Application;
20 | options: ServiceOptions;
21 |
22 | constructor (options: ServiceOptions, app: Application) {
23 | this.options = options;
24 | this.app = app;
25 | }
26 |
27 | async create (data: PagerData): Promise {
28 | // Find the resources associated with this selcall
29 | const resourceIdentifierService = this.app.service('resource-identifiers');
30 | const resourceIdentifiers = await resourceIdentifierService.find({ query: { type: 'selcall', value: data.selcall }, paginate: false }) as ResourceIdentifierData[];
31 | const resourceIds = resourceIdentifiers.map(identifier => identifier.resourceId);
32 | const resourceService = this.app.service('resources');
33 | const resources = await resourceService.find({ query: { id: resourceIds }, paginate: false }) as ResourceData[];
34 |
35 | // Do not process the alert if there is no resource associated with this selcall
36 | if (resources.length === 0) {
37 | throw new NotFound('No resources are associated with this selcall');
38 | }
39 |
40 | // Forward the alert
41 | const incidentData = await this.app.service('alerts').create({
42 | resources: resources,
43 | context: {
44 | processingStarted: new Date(),
45 | rawContent: data.selcall,
46 | source: {
47 | type: AlertSourceType.PLAIN
48 | }
49 | }
50 | });
51 |
52 | return {
53 | incidentId: incidentData.id
54 | };
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/server/src/services/input/pager/pager.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import {allowApiKey} from '../../../hooks/allowApiKey';
3 | import { createSchema } from './pager.schemas';
4 | import { validateJoiSchema } from '../../../hooks/validateJoiSchema';
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: [ validateJoiSchema(createSchema) ],
15 | update: [],
16 | patch: [],
17 | remove: []
18 | },
19 |
20 | after: {
21 | all: [],
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/input/pager/pager.schemas.ts:
--------------------------------------------------------------------------------
1 | import Joi from 'joi';
2 |
3 | export const createSchema = Joi.object({
4 | selcall: Joi.string().regex(/^\d{5}$/).required()
5 | });
6 |
--------------------------------------------------------------------------------
/server/src/services/input/pager/pager.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `input/pager` service on path `/input/pager`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../../declarations';
4 | import { Pager } from './pager.class';
5 | import hooks from './pager.hooks';
6 |
7 | // Add this service to the service type index
8 | declare module '../../../declarations' {
9 | interface ServiceTypes {
10 | 'input/pager': Pager & ServiceAddons;
11 | }
12 | }
13 |
14 | export default function (app: Application): void {
15 | const options = {
16 | paginate: app.get('paginate')
17 | };
18 |
19 | // Initialize our service with any options it requires
20 | app.use('/input/pager', new Pager(options, app));
21 |
22 | // Get our initialized service so that we can register hooks
23 | const service = app.service('input/pager');
24 |
25 | service.hooks(hooks);
26 | }
27 |
--------------------------------------------------------------------------------
/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 | 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/locations/locations.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `locations` service on path `/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 | '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 | postCode: string,
24 | municipality: string,
25 | district: string
26 | country: string
27 | incidentId?: 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('/locations', new Locations(options, app));
40 |
41 | // Get our initialized service so that we can register hooks
42 | const service = app.service('locations');
43 |
44 | service.hooks(hooks);
45 | }
46 |
--------------------------------------------------------------------------------
/server/src/services/locations/nominatim.class.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import logger from '../../logger';
3 |
4 | export interface NominatimResult {
5 | place_id: bigint,
6 | licence: string,
7 | osm_type: string,
8 | osm_id: bigint,
9 | boundingbox: [],
10 | lat: string,
11 | lon: string,
12 | display_name: string,
13 | class: string,
14 | type: string,
15 | importance: number,
16 | address: {
17 | house_name?: string,
18 | house_number: string,
19 | road: string,
20 | isolated_dwelling?: string,
21 | croft?: string,
22 | hamlet?: string,
23 | subdivision?: string,
24 | suburb?: string,
25 | borough?: string,
26 | district?: string,
27 | city_district?: string,
28 | village?: string,
29 | town?: string,
30 | city?: string,
31 | municipality?: string,
32 | county: string,
33 | state: string,
34 | postcode: string,
35 | country: string,
36 | country_code: string
37 | }
38 | }
39 |
40 | export class Nominatim {
41 | private baseUrl: URL;
42 |
43 | constructor(baseUrl?: URL) {
44 | this.baseUrl = baseUrl || new URL('https://nominatim.openstreetmap.org');
45 | }
46 |
47 | geocode(street: string, city: string, postalCode: string): Promise {
48 | return axios.get('/search', {
49 | baseURL: this.baseUrl.toString(),
50 | headers: {
51 | 'User-Agent': 'Alarmdisplay Hub/1.0.0'
52 | },
53 | params: {
54 | 'accept-language': 'de',
55 | addressdetails: 1,
56 | countrycodes: 'de',
57 | format: 'json',
58 | street: street,
59 | city: city,
60 | postalcode: postalCode
61 | },
62 | responseType: 'json'
63 | })
64 | .then(result => {
65 | logger.debug(result.data);
66 | return result.data as NominatimResult[];
67 | });
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/server/src/services/print-tasks/print-tasks.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application, PrintTaskData, FoundFileContext } from '../../declarations';
3 | import logger from '../../logger';
4 | import cp from 'child_process';
5 |
6 | export class PrintTasks extends Service {
7 | constructor(options: Partial) {
8 | super(options);
9 | }
10 |
11 | setup(app: Application): void {
12 | // Register to be notified of new files
13 | const watchedFoldersService = app.service('watchedfolders');
14 | watchedFoldersService.on('found_file', (context: FoundFileContext) => this.onNewFile(context.path, context.watchedFolderId));
15 | }
16 |
17 | private async onNewFile(filePath: string, watchedFolderId: number) {
18 | const printTasks = await this.find({
19 | query: {
20 | event: 'found_file',
21 | sourceId: watchedFolderId,
22 | },
23 | paginate: false
24 | }) as PrintTaskData[];
25 |
26 | for (const printTask of printTasks) {
27 | let command = 'lp ';
28 |
29 | // Add the printer name, if it is set and doesn't look suspicious
30 | if (printTask.printerName && printTask.printerName !== '') {
31 | if (/^\w+$/.test(printTask.printerName)) {
32 | command += `-d ${printTask.printerName} `;
33 | } else {
34 | logger.warn('The printer name contains illegal characters (e.g. space)');
35 | }
36 | }
37 |
38 | command += '-o fit-to-page ';
39 |
40 | // If the number of copies is a valid parameter, add it to the command
41 | if (Number.isInteger(printTask.numberCopies) && printTask.numberCopies > 0 && printTask.numberCopies < 128) {
42 | command += `-n ${printTask.numberCopies} `;
43 | }
44 |
45 | // Add the file name
46 | command += filePath;
47 |
48 | // Execute the command
49 | try {
50 | cp.execSync(command, { stdio: 'ignore' });
51 | } catch (error: any) {
52 | logger.error(error.message);
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/server/src/services/print-tasks/print-tasks.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/print-tasks/print-tasks.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `print-tasks` service on path `/print-tasks`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { PrintTasks } from './print-tasks.class';
5 | import createModel from '../../models/print-tasks.model';
6 | import hooks from './print-tasks.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'print-tasks': PrintTasks & ServiceAddons;
12 | }
13 |
14 | interface PrintTaskData {
15 | id: number
16 | event: string
17 | sourceId: number
18 | printerName: string
19 | numberCopies: number
20 | }
21 | }
22 |
23 | export default function (app: Application): void {
24 | const options = {
25 | Model: createModel(app),
26 | paginate: app.get('paginate')
27 | };
28 |
29 | // Initialize our service with any options it requires
30 | app.use('/print-tasks', new PrintTasks(options));
31 |
32 | // Get our initialized service so that we can register hooks
33 | const service = app.service('print-tasks');
34 |
35 | service.hooks(hooks);
36 | }
37 |
--------------------------------------------------------------------------------
/server/src/services/processed-files/processed-files.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application, ProcessedFilesData } from '../../declarations';
3 |
4 | export class ProcessedFiles 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/processed-files/processed-files.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: [ authenticate('jwt') ],
10 | find: [],
11 | get: [],
12 | create: [ disallow('external') ],
13 | update: [ disallow() ],
14 | patch: [ disallow() ],
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/processed-files/processed-files.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `processed files` service on path `/processed-files`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { ProcessedFiles } from './processed-files.class';
5 | import createModel from '../../models/processed-files.model';
6 | import hooks from './processed-files.hooks';
7 | import { SequelizeServiceOptions } from 'feathers-sequelize';
8 |
9 | // Add this service to the service type index
10 | declare module '../../declarations' {
11 | interface ServiceTypes {
12 | 'processed-files': ProcessedFiles & ServiceAddons;
13 | }
14 |
15 | interface ProcessedFilesData {
16 | hash: string
17 | createdAt: Date
18 | }
19 | }
20 |
21 | export default function (app: Application): void {
22 | const options: Partial = {
23 | Model: createModel(app),
24 | paginate: app.get('paginate'),
25 | id: 'hash'
26 | };
27 |
28 | // Initialize our service with any options it requires
29 | app.use('/processed-files', new ProcessedFiles(options, app));
30 |
31 | // Get our initialized service so that we can register hooks
32 | const service = app.service('processed-files');
33 |
34 | service.hooks(hooks);
35 | }
36 |
--------------------------------------------------------------------------------
/server/src/services/resource-identifiers/resource-identifiers.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application, ResourceIdentifierData } from '../../declarations';
3 |
4 | export class ResourceIdentifiers 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/resource-identifiers/resource-identifiers.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/resource-identifiers/resource-identifiers.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `resource-identifiers` service on path `/resource-identifiers`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { ResourceIdentifiers } from './resource-identifiers.class';
5 | import createModel from '../../models/resource-identifiers.model';
6 | import hooks from './resource-identifiers.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'resource-identifiers': ResourceIdentifiers & ServiceAddons;
12 | }
13 |
14 | interface ResourceIdentifierData {
15 | id: number
16 | type: 'name' | 'selcall'
17 | value: string
18 | createdAt: Date
19 | updatedAt: Date
20 | resourceId: number
21 | }
22 | }
23 |
24 | export default function (app: Application): void {
25 | const options = {
26 | Model: createModel(app),
27 | multi: ['remove'],
28 | paginate: app.get('paginate')
29 | };
30 |
31 | // Initialize our service with any options it requires
32 | app.use('/resource-identifiers', new ResourceIdentifiers(options, app));
33 |
34 | // Get our initialized service so that we can register hooks
35 | const service = app.service('resource-identifiers');
36 |
37 | service.hooks(hooks);
38 | }
39 |
--------------------------------------------------------------------------------
/server/src/services/resources/resources.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 | import { iff } from 'feathers-hooks-common';
6 | // Don't remove this comment. It's needed to format import lines nicely.
7 |
8 | const { authenticate } = authentication.hooks;
9 |
10 | const populateOptions = {
11 | include: {
12 | service: 'resource-identifiers',
13 | nameAs: 'identifiers',
14 | keyHere: 'id',
15 | keyThere: 'resourceId',
16 | }
17 | };
18 |
19 | export default {
20 | before: {
21 | all: [ allowApiKey(), authenticate('jwt', 'api-key') ],
22 | find: [],
23 | get: [],
24 | create: [ includeIdentifiers ],
25 | update: [],
26 | patch: [],
27 | remove: []
28 | },
29 |
30 | after: {
31 | all: [ iff(shouldPopulate, shallowPopulate(populateOptions)) ],
32 | find: [],
33 | get: [],
34 | create: [],
35 | update: [],
36 | patch: [],
37 | remove: []
38 | },
39 |
40 | error: {
41 | all: [],
42 | find: [],
43 | get: [],
44 | create: [],
45 | update: [],
46 | patch: [],
47 | remove: []
48 | }
49 | };
50 |
51 | /**
52 | * Automatically create nested resource identifiers when creating a resource
53 | * @param context
54 | */
55 | function includeIdentifiers(context: HookContext): HookContext {
56 | const sequelize = context.app.get('sequelizeClient');
57 | const ResourceIdentifier = sequelize.models.resource_identifier;
58 | context.params.sequelize = { include: [ { model: ResourceIdentifier, as: 'identifiers' } ] };
59 | return context;
60 | }
61 |
62 | /**
63 | * Returns if the nested resource identifiers should be populated
64 | * @param context
65 | */
66 | function shouldPopulate(context: HookContext): boolean {
67 | return !context.params.skipPopulate;
68 | }
69 |
--------------------------------------------------------------------------------
/server/src/services/resources/resources.schemas.ts:
--------------------------------------------------------------------------------
1 | import Joi from 'joi';
2 |
3 | export const createUpdateSchema = Joi.object({
4 | name: Joi.string().min(1).required(),
5 | type: Joi.string().valid('organization', 'group', 'vehicle', 'role', 'other').insensitive()
6 | });
7 |
8 | export const patchSchema = Joi.object({
9 | name: Joi.string().min(1),
10 | type: Joi.string().valid('organization', 'group', 'vehicle', 'role', 'other').insensitive().optional()
11 | });
12 |
--------------------------------------------------------------------------------
/server/src/services/resources/resources.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `resources` service on path `/resources`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application, ResourceIdentifierData } from '../../declarations';
4 | import { Resources } from './resources.class';
5 | import createModel from '../../models/resources.model';
6 | import hooks from './resources.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'resources': Resources & ServiceAddons;
12 | }
13 |
14 | interface ResourceData {
15 | id: number
16 | name: string
17 | type: 'organization' | 'group' | 'vehicle' | 'role' | 'other'
18 | createdAt: Date
19 | updatedAt: Date
20 | identifiers: ResourceIdentifierData[]
21 | }
22 | }
23 |
24 | export default function (app: Application) {
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('/resources', new Resources(options, app));
32 |
33 | // Get our initialized service so that we can register hooks
34 | const service = app.service('resources');
35 |
36 | service.hooks(hooks);
37 | }
38 |
--------------------------------------------------------------------------------
/server/src/services/scheduled-alerts/scheduled-alerts.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application } from '../../declarations';
3 |
4 | export interface ScheduledAlertData {
5 | id: number
6 | begin: Date
7 | end: Date
8 | reason: string
9 | keyword: string
10 | status: 'Exercise' | 'Test'
11 | createdAt: Date
12 | updatedAt: Date
13 | incidentId?: number
14 | }
15 |
16 | export class ScheduledAlerts extends Service {
17 | //eslint-disable-next-line @typescript-eslint/no-unused-vars
18 | constructor(options: Partial, app: Application) {
19 | super(options);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/server/src/services/scheduled-alerts/scheduled-alerts.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/scheduled-alerts/scheduled-alerts.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `Scheduled alerts` service on path `/scheduled-alerts`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { ScheduledAlerts } from './scheduled-alerts.class';
5 | import createModel from '../../models/scheduled-alerts.model';
6 | import hooks from './scheduled-alerts.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'scheduled-alerts': ScheduledAlerts & 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('/scheduled-alerts', new ScheduledAlerts(options, app));
23 |
24 | // Get our initialized service so that we can register hooks
25 | const service = app.service('scheduled-alerts');
26 |
27 | service.hooks(hooks);
28 | }
29 |
--------------------------------------------------------------------------------
/server/src/services/serial-monitors/serial-monitors.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/serial-monitors/serial-monitors.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `serial-monitors` service on path `/serial-monitors`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { SerialMonitors } from './serial-monitors.class';
5 | import createModel from '../../models/serial-monitors.model';
6 | import hooks from './serial-monitors.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'serial-monitors': SerialMonitors & ServiceAddons;
12 | }
13 |
14 | interface SerialDataContext {
15 | serialMonitorId: number
16 | data: Buffer
17 | }
18 | }
19 |
20 | export default function (app: Application): void {
21 | const options = {
22 | Model: createModel(app),
23 | paginate: app.get('paginate')
24 | };
25 |
26 | // Initialize our service with any options it requires
27 | app.use('/serial-monitors', new SerialMonitors(options, app));
28 |
29 | // Get our initialized service so that we can register hooks
30 | const service = app.service('serial-monitors');
31 |
32 | service.hooks(hooks);
33 | }
34 |
--------------------------------------------------------------------------------
/server/src/services/settings/settings.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 | import { Application, SettingsValue, SettingsData } 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 | ['ignore_processed_files', true],
14 | ]);
15 |
16 | (app.get('databaseReady') as Promise).then(async () => {
17 | logger.debug('Checking the settings table');
18 | for (const [settingKey, settingDefault] of settingDefaults) {
19 | try {
20 | await this.get(settingKey);
21 | } catch {
22 | // If the setting cannot be found, create it with the default value
23 | await this.create({ key: settingKey, value: settingDefault });
24 | }
25 | }
26 | });
27 | }
28 |
29 | async getBooleanValue(key: string): Promise {
30 | const settingsData = await this.get(key);
31 | return settingsData.value == true;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/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 `/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 | 'settings': Settings & ServiceAddons;
12 | }
13 |
14 | interface SettingsData {
15 | key: string
16 | value: SettingsValue
17 | }
18 |
19 | type SettingsValue = null | string | number | boolean
20 | }
21 |
22 | export default function (app: Application): void {
23 | const options = {
24 | Model: createModel(app),
25 | id: 'key',
26 | paginate: app.get('paginate')
27 | };
28 |
29 | // Initialize our service with any options it requires
30 | app.use('/settings', new Settings(options, app));
31 |
32 | // Get our initialized service so that we can register hooks
33 | const service = app.service('settings');
34 |
35 | service.hooks(hooks);
36 | }
37 |
--------------------------------------------------------------------------------
/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/textanalysis/configs/ILS_Augsburg.ts:
--------------------------------------------------------------------------------
1 | import {TextAnalysisConfig} from '../../../declarations';
2 |
3 | export default {
4 | name: 'ILS Augsburg',
5 | beginningMark: /Alarmfax der ILS Augsburg/,
6 | endMark: /\n.*ENDE FAX.*\n/,
7 | importantWords: [
8 | 'MITTEILER',
9 | 'EINSATZORT',
10 | 'ZIELORT',
11 | 'EINSATZGRUND',
12 | 'EINSATZMITTEL',
13 | 'BEMERKUNG',
14 | 'ENDE',
15 | 'FAX',
16 | 'Absender',
17 | 'Einsatznummer',
18 | 'Rückrufnummer',
19 | 'Straße',
20 | 'Ort',
21 | 'Koordinate',
22 | 'Einsatzplan',
23 | ],
24 | sections: [
25 | {
26 | beginningMark: /Alarmfax der ILS Augsburg/,
27 | regexps: [
28 | /Absender [:;=] (?.*) Tel/,
29 | /Einsatznummer (?:.*)[:;=] (?[.*)/
30 | ]
31 | },
32 | {
33 | beginningMark: /MITTEILER/,
34 | regexps: [
35 | /Name\s*[:;=](?].*)Rückrufnummer[:;=](?.*)/
36 | ]
37 | },
38 | {
39 | beginningMark: /EINSATZORT/,
40 | regexps: [
41 | /Straße\s*[:;=](?.*)Haus-Nr\.[:;=]\s*(?\d+(?:\s?[a-z])?)(?\s+.*)?$/,
42 | /Ort\s*[:;=]\s*(?\d{5}) (?.+)\s+-\s+(?.+)\s+\k/,
43 | /Objekt\s*[:;=]\s*(?.+)$/,
44 | /Koordinate\s*[:;=]\s(?\d+[,.]\d+) \/ (?\d+[,.]\d+)$/
45 | ]
46 | },
47 | {
48 | beginningMark: /ZIELORT/,
49 | regexps: []
50 | },
51 | {
52 | beginningMark: /EINSATZGRUND/,
53 | regexps: [
54 | /Schlagw\.[:;=]\s(?.*)$/,
55 | /Stichwort[:;=]\s(?.*)$/
56 | ]
57 | },
58 | {
59 | beginningMark: /EINSATZMITTEL/,
60 | regexps: [
61 | /(?.*) \(Ausger/
62 | ]
63 | },
64 | {
65 | beginningMark: /BEMERKUNG/,
66 | regexps: [
67 | /Einsatzplan[:;=](?(?:.|\n)*)/m
68 | ]
69 | }
70 | ],
71 | triggerWords: ['Alarmfax']
72 | } as TextAnalysisConfig;
73 |
--------------------------------------------------------------------------------
/server/src/services/textanalysis/configs/ILS_Bamberg.ts:
--------------------------------------------------------------------------------
1 | import {TextAnalysisConfig} from '../../../declarations';
2 |
3 | export default {
4 | name: 'ILS Bamberg',
5 | beginningMark: /Alarmfax der ILS Bamberg/,
6 | endMark: /\n.*ENDE FAX.*\n/,
7 | importantWords: [
8 | 'MITTEILER',
9 | 'EINSATZORT',
10 | 'ZIELORT',
11 | 'EINSATZGRUND',
12 | 'EINSATZMITTEL',
13 | 'BEMERKUNG',
14 | 'ENDE',
15 | 'FAX',
16 | 'Absender',
17 | 'Einsatznummer',
18 | 'Rufnummer',
19 | 'Straße',
20 | 'Ort',
21 | 'Koordinaten',
22 | 'Einsatzplan',
23 | ],
24 | sections: [
25 | {
26 | beginningMark: /Alarmfax der ILS Bamberg/,
27 | regexps: [
28 | /Absender [:;=] (?.*) Tel/,
29 | /Einsatznummer (?:.*)[:;=] (?[.*)/
30 | ]
31 | },
32 | {
33 | beginningMark: /MITTEILER/,
34 | regexps: [
35 | /Name\s*[:;=](?].*)/,
36 | /Rufnummer[:;=](?.*)/
37 | ]
38 | },
39 | {
40 | beginningMark: /EINSATZORT/,
41 | regexps: [
42 | /Straße\s*[:;=](?.*)Haus-Nr\.[:;=]\s*(?\d+(?:\s?[a-z])?)(?\s+.*)?$/,
43 | /Ort\s*[:;=]\s*(?\d{5}) (?\w+)/,
44 | /Koordinaten\s*[:;=]\sx\s*[:;=]\s(?\d+[,.]\d+)\s\/\sy\s*[:;=]\s(?\d+[,.]\d+)/
45 | ]
46 | },
47 | {
48 | beginningMark: /EINSATZGRUND/,
49 | regexps: [
50 | /Schlagwort[:;=]\s(?.*)$/
51 | ]
52 | },
53 | {
54 | beginningMark: /EINSATZMITTEL/,
55 | regexps: [
56 | /Einsatzmittelname:\s*(?.*)/
57 | ]
58 | },
59 | {
60 | beginningMark: /SPRECHGRUPPE/,
61 | regexps: []
62 | },
63 | {
64 | beginningMark: /BEMERKUNG/,
65 | regexps: [
66 | /(?(?:.|\n)*)/m
67 | ]
68 | },
69 | {
70 | beginningMark: /OBJEKTINFO/, // Wahrscheinlich optional
71 | regexps: []
72 | }
73 | ],
74 | triggerWords: ['Alarmfax']
75 | } as TextAnalysisConfig;
76 |
--------------------------------------------------------------------------------
/server/src/services/textanalysis/configs/ILS_Biberach.ts:
--------------------------------------------------------------------------------
1 | import {TextAnalysisConfig} from '../../../declarations';
2 |
3 | export default {
4 | name: 'ILS Biberach',
5 | sections: [
6 | {
7 | beginningMark: /Alarmdruck/,
8 | regexps: [
9 | /^\s*(?[\d+)/,
10 | /(?]Integrierte Leitstelle Biberach)/
11 | ]
12 | },
13 | {
14 | beginningMark: /Einsatzanlass/,
15 | regexps: [
16 | /Meldebild\s+(?.*?)\s+Datum/,
17 | /Bemerkung\s+(?!GEBIET_UND_BEZEICHNUNG)(?[^\s].*?)(?:^\s+Mit Sondersignal|^\w)/ms,
18 | /Stichwort\s.+\sStichwort 2\s+(?[^-\s].*)/,
19 | /Stichwort\s+(?[^-\s].*?)\s+Stichwort 2\s/
20 | ]
21 | },
22 | {
23 | beginningMark: /Einsatzort/,
24 | regexps: [
25 | /Objekt\s+(?.+)$/,
26 | /Ort\s+(?.+?)\s+\[(?\d{5})]/,
27 | /Ortsteil\s+(?.+)$/,
28 | /Straße\s+(?\D+)(?.*)/,
29 | /Ortszusatz\s+(?.+)$/,
30 | /Bemerkung\s+(?.+)$/,
31 | ]
32 | },
33 | {
34 | beginningMark: /Einsatzstatus/,
35 | regexps: []
36 | },
37 | {
38 | beginningMark: /EM\s+alarmiert.*\n/,
39 | regexps: [
40 | /(?.*?)\s{5,}/
41 | ]
42 | },
43 | {
44 | beginningMark: /Eskalationsstufe/,
45 | regexps: []
46 | }
47 | ],
48 | triggerWords: ['Alarmdruck']
49 | } as TextAnalysisConfig;
50 |
--------------------------------------------------------------------------------
/server/src/services/textanalysis/configs/ILS_Rosenheim.ts:
--------------------------------------------------------------------------------
1 | import {TextAnalysisConfig} from '../../../declarations';
2 |
3 | export default {
4 | name: 'ILS Rosenheim',
5 | beginningMark: /Alarmfax der ILS Rosenheim/,
6 | endMark: /\n.*ENDE ALARMFAX.*\n/,
7 | importantWords: [
8 | 'MITTEILER',
9 | 'EINSATZORT',
10 | 'PATIENT',
11 | 'EINSATZGRUND',
12 | 'TETRA',
13 | 'EINSATZMITTEL',
14 | 'BEMERKUNG',
15 | 'EINSATZHINWEIS',
16 | 'ENDE',
17 | 'ALARMFAX',
18 | ],
19 | sections: [
20 | {
21 | beginningMark: /Alarmfax der ILS Rosenheim/,
22 | regexps: [
23 | /Einsatz-Nr\.[:;=] (?[.*)/
24 | ]
25 | },
26 | {
27 | beginningMark: /MITTEILER/,
28 | regexps: [
29 | /Name\s+[:;=](?].*)/,
30 | /Rufnummer\s+[:;=](?.*)/
31 | ]
32 | },
33 | {
34 | beginningMark: /EINSATZORT/,
35 | regexps: [
36 | /--\s*(?\d+)\s+(?\d+)$/,
37 | /Straße\s+[:;=](?.*?)(?\d+.*)?$/,
38 | /Ortsteil\s+[:;=](?.+)\s-\s/,
39 | /Gemeinde\s+[:;=](?.+)/,
40 | /Objekt\s+[:;=](?.+)/
41 | ]
42 | },
43 | {
44 | beginningMark: /PATIENT/,
45 | regexps: []
46 | },
47 | {
48 | beginningMark: /EINSATZGRUND/,
49 | regexps: [
50 | /Schlagw[-.]\s+[:;=](?.+)/,
51 | /Stichwort\s+[:;=](?.+)/
52 | ]
53 | },
54 | {
55 | beginningMark: /TETRA/,
56 | regexps: []
57 | },
58 | {
59 | beginningMark: /EINSATZMITTEL/,
60 | regexps: [
61 | /Name\s+[:;=](?.*)/
62 | ]
63 | },
64 | {
65 | beginningMark: /BEMERKUNG/,
66 | regexps: [
67 | /\n(?.*)\n(?\d+) (?\S+\s\S+) [^,]+, (?[^,]+), (?[^,]+), (?[^,]+)/,
11 | /, L=(?\d+)\?(?\d+)’(?\d+\.\d+)",\sB=(?\d+)\?(?\d+)’(?\d+\.\d+)"/
12 | ]
13 | },
14 | {
15 | beginningMark: /MSG:/,
16 | regexps: [
17 | /(?(?:.|\n)*)/m
18 | ]
19 | }
20 | ],
21 | triggerWords: ['ENR']
22 | } as TextAnalysisConfig;
23 |
--------------------------------------------------------------------------------
/server/src/services/textanalysis/configs/index.ts:
--------------------------------------------------------------------------------
1 | import ILS_Augsburg from './ILS_Augsburg';
2 | import ILS_Bamberg from './ILS_Bamberg';
3 | import ILS_Biberach from './ILS_Biberach';
4 | import ILS_Rosenheim from './ILS_Rosenheim';
5 | import LS_Bodenseekreis from './LS_Bodenseekreis';
6 |
7 | export default {
8 | 'ils_augsburg': ILS_Augsburg,
9 | 'ils_bamberg': ILS_Bamberg,
10 | 'ils_biberach': ILS_Biberach,
11 | 'ils_rosenheim': ILS_Rosenheim,
12 | 'ls_bodenseekreis': LS_Bodenseekreis
13 | };
14 |
--------------------------------------------------------------------------------
/server/src/services/textanalysis/extractor.ts:
--------------------------------------------------------------------------------
1 | import { Application, TextAnalysisConfig, TextExtractionResult } from '../../declarations';
2 | import logger from '../../logger';
3 | import util from 'util';
4 | import cp from 'child_process';
5 | import { Ocr } from './ocr.class';
6 |
7 | export class Extractor {
8 | private app: Application;
9 | private ocr: Ocr;
10 |
11 | constructor (app: Application) {
12 | this.app = app;
13 | this.ocr = new Ocr(app);
14 | }
15 |
16 | public async getTextFromFile(filePath: string, textAnalysisConfig: TextAnalysisConfig): Promise {
17 | // Initialize the return value
18 | const result: TextExtractionResult = {
19 | method: 'plain',
20 | content: ''
21 | };
22 |
23 | try {
24 | result.content = await Extractor.getEmbeddedText(filePath);
25 | } catch (error: any) {
26 | logger.debug('Could not extract embedded text from file:', error.message);
27 | }
28 |
29 | // If text got extracted, return it right away
30 | if (result.content && result.content !== '') {
31 | return result;
32 | }
33 |
34 | logger.debug('No embedded text found, continue with OCR...');
35 | result.method = 'ocr';
36 | const userWords = new Array().concat(
37 | textAnalysisConfig.triggerWords || [],
38 | textAnalysisConfig.importantWords || []
39 | );
40 | result.content = await this.ocr.getTextFromFile(filePath, userWords);
41 |
42 | return result;
43 | }
44 |
45 | /**
46 | * Determines if the file is a PDF.
47 | * Currently, only the file name is taken into account
48 | *
49 | * @param filePath
50 | * @private
51 | */
52 | private static isFileOfTypePDF(filePath: string): boolean {
53 | return /\.pdf$/i.test(filePath);
54 | }
55 |
56 | /**
57 | * Try to extract embedded text from a file.
58 | *
59 | * @param filePath
60 | * @private
61 | */
62 | private static async getEmbeddedText(filePath: string): Promise {
63 | if (Extractor.isFileOfTypePDF(filePath)) {
64 | const exec = util.promisify(cp.exec);
65 |
66 | // Check if pdftotext is installed, will throw on error
67 | await exec('pdftotext -v');
68 |
69 | // Try to extract the text while preserving the layout
70 | const { stdout } = await exec(`pdftotext -layout "${filePath}" -`);
71 |
72 | return (stdout || '').trim();
73 | }
74 |
75 | // Return nothing for unknown file types
76 | return '';
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/server/src/services/textanalysis/textanalysis.hooks.ts:
--------------------------------------------------------------------------------
1 | import * as authentication from '@feathersjs/authentication';
2 | import { allowApiKey } from '../../hooks/allowApiKey';
3 | import { HookContext } from '@feathersjs/feathers';
4 | import { BadRequest } from '@feathersjs/errors';
5 | import configs from './configs';
6 | // Don't remove this comment. It's needed to format import lines nicely.
7 |
8 | const { authenticate } = authentication.hooks;
9 |
10 | export default {
11 | before: {
12 | all: [ allowApiKey(), authenticate('jwt', 'api-key') ],
13 | find: [],
14 | get: [],
15 | create: [ validateConfigName ],
16 | update: [ validateConfigName ],
17 | patch: [ validateConfigName ],
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 |
42 | function validateConfigName(context: HookContext): HookContext {
43 | if (context.method === 'create' && !context.data.config) {
44 | throw new BadRequest('config not provided');
45 | }
46 |
47 | if (context.method === 'create' || context.method === 'update' || (context.method === 'patch' && context.data.config)) {
48 | if (!Object.keys(configs).includes(context.data.config)) {
49 | throw new BadRequest('config name invalid');
50 | }
51 | }
52 |
53 | return context;
54 | }
55 |
--------------------------------------------------------------------------------
/server/src/services/users/users.class.ts:
--------------------------------------------------------------------------------
1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize';
2 |
3 | export class Users extends Service {
4 | constructor(options: Partial) {
5 | super(options);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/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) {
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 |
20 | /**
21 | * Require the request to be authenticated, except when creating the first user
22 | *
23 | * @param context
24 | */
25 | async function maybeAuthenticate(this: any, context: HookContext): Promise {
26 | const existingUsers = await context.service.find({ query: { $limit: 0 } });
27 |
28 | if (existingUsers.total > 0) {
29 | return authenticate('jwt').call(this, context);
30 | }
31 |
32 | return context;
33 | }
34 |
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) {
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));
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/watchedfolders/watchedfolders.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/watchedfolders/watchedfolders.service.ts:
--------------------------------------------------------------------------------
1 | // Initializes the `watchedfolders` service on path `/watchedfolders`
2 | import { ServiceAddons } from '@feathersjs/feathers';
3 | import { Application } from '../../declarations';
4 | import { WatchedFolders } from './watchedfolders.class';
5 | import createModel from '../../models/watchedfolders.model';
6 | import hooks from './watchedfolders.hooks';
7 |
8 | // Add this service to the service type index
9 | declare module '../../declarations' {
10 | interface ServiceTypes {
11 | 'watchedfolders': WatchedFolders & ServiceAddons;
12 | }
13 |
14 | interface FoundFileContext {
15 | watchedFolderId: number
16 | path: string
17 | }
18 | }
19 |
20 | export default function (app: Application) {
21 | const options = {
22 | Model: createModel(app),
23 | paginate: app.get('paginate')
24 | };
25 |
26 | // Initialize our service with any options it requires
27 | app.use('/watchedfolders', new WatchedFolders(options, app));
28 |
29 | // Get our initialized service so that we can register hooks
30 | const service = app.service('watchedfolders');
31 |
32 | service.hooks(hooks);
33 | }
34 |
--------------------------------------------------------------------------------
/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) => 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/services/api-keys.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'api-keys\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('api-keys');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/server/test/services/incidents.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 | import { IncidentData } from '../../src/declarations';
3 | import { IncidentFactory, LocationFactory } from '../../src/feathers-factories';
4 |
5 | describe('\'incidents\' service', () => {
6 | beforeAll(async () => {
7 | app.setup();
8 | await (app.get('databaseReady') as Promise);
9 | });
10 |
11 | afterEach(async () => {
12 | await app.service('locations')._remove(null);
13 | await app.service('incidents')._remove(null);
14 | });
15 |
16 | it('registered the service', () => {
17 | const service = app.service('incidents');
18 | expect(service).toBeTruthy();
19 | });
20 |
21 | it('creates an incident', async () => {
22 | const data: Partial = await IncidentFactory.get();
23 | delete data.id;
24 | const incident = await app.service('incidents').create(data) as IncidentData;
25 | expect(incident).toMatchObject(data);
26 | expect(incident.id).toBeGreaterThan(0);
27 | });
28 |
29 | it('adds a location to an existing incident', async () => {
30 | const data: Partial = await IncidentFactory.get();
31 | delete data.location;
32 | const incident = await app.service('incidents').create(data) as IncidentData;
33 | expect(incident.location).toBeUndefined();
34 | const location = await LocationFactory.get();
35 | const patchedIncident = await app.service('incidents').patch(incident.id, {
36 | location: location
37 | }) as IncidentData;
38 | expect(patchedIncident.location).toMatchObject(location);
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/server/test/services/input/pager.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../../src/app';
2 | import { BadRequest, NotFound } from '@feathersjs/errors';
3 | import { ResourceData } from '../../../src/declarations';
4 |
5 | describe('\'input/pager\' service', () => {
6 | beforeAll(async () => {
7 | app.setup();
8 | await (app.get('databaseReady') as Promise);
9 | });
10 |
11 | afterEach(async () => {
12 | await app.service('incidents')._remove(null);
13 | await app.service('resources')._remove(null);
14 | });
15 |
16 | it('registered the service', () => {
17 | const service = app.service('input/pager');
18 | expect(service).toBeTruthy();
19 | });
20 |
21 | it('rejects wrong selcall formats', async () => {
22 | const service = app.service('input/pager');
23 | await expect(service.create({ selcall: 'some text' })).rejects.toBeInstanceOf(BadRequest);
24 | await expect(service.create({ selcall: 'ABCDE' })).rejects.toBeInstanceOf(BadRequest);
25 | await expect(service.create({ selcall: '123' })).rejects.toBeInstanceOf(BadRequest);
26 | await expect(service.create({ selcall: '123456' })).rejects.toBeInstanceOf(BadRequest);
27 | await expect(service.create({ selcall: '' })).rejects.toBeInstanceOf(BadRequest);
28 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
29 | // @ts-ignore Allow invalid data format
30 | await expect(service.create({})).rejects.toBeInstanceOf(BadRequest);
31 | });
32 |
33 | it('rejects unknown selcalls', async () => {
34 | const service = app.service('input/pager');
35 | await expect(service.create({ selcall: '99999' })).rejects.toBeInstanceOf(NotFound);
36 | });
37 |
38 | it('creates an incident for a known selcall', async () => {
39 | const resource = await app.service('resources').create({ name: 'foo', type: 'other' }) as ResourceData;
40 | await app.service('resource-identifiers').create({ type: 'selcall', value: '99999', resourceId: resource.id });
41 |
42 | const response = await app.service('input/pager').create({ selcall: '99999' });
43 |
44 | expect(response).toHaveProperty('incidentId');
45 | const incident = await app.service('incidents').get(response.incidentId);
46 | expect(incident).toHaveProperty('resources');
47 | expect(incident.resources).toHaveLength(1);
48 | expect(incident.resources[0].id).toStrictEqual(resource.id);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/server/test/services/locations.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'locations\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('locations');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/server/test/services/pocessed-files.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'processed files\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('processed-files');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/server/test/services/print-tasks.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'print-tasks\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('print-tasks');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/server/test/services/resource-identifiers.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'resource-identifiers\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('resource-identifiers');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/server/test/services/resources.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'resources\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('resources');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/server/test/services/scheduled-alerts.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'Scheduled alerts\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('scheduled-alerts');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/server/test/services/settings.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'settings\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('settings');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/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/textanalysis.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'textanalysis\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('textanalysis');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/server/test/services/users.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'users\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('users');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/server/test/services/watchedfolders.test.ts:
--------------------------------------------------------------------------------
1 | import app from '../../src/app';
2 |
3 | describe('\'watchedfolders\' service', () => {
4 | it('registered the service', () => {
5 | const service = app.service('watchedfolders');
6 | expect(service).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/server/test/testSequencer.js:
--------------------------------------------------------------------------------
1 | const Sequencer = require('@jest/test-sequencer').default;
2 |
3 | class CustomSequencer extends Sequencer {
4 | sort(tests) {
5 | const copyTests = Array.from(tests);
6 | return copyTests.sort((testA, testB) => (testA.path > testB.path ? 1 : -1));
7 | }
8 | }
9 |
10 | module.exports = CustomSequencer;
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/server/typings/gauss-krueger/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for gauss-krueger
2 | declare module 'gauss-krueger' {
3 | export interface WGSCoordinates {
4 | latitude: number
5 | longitude: number
6 | }
7 |
8 | export interface GKCoordinates {
9 | x: number
10 | y: number
11 | }
12 |
13 | export function toGK(coordinates: WGSCoordinates, zone?: number): WGSCoordinates
14 | export function toWGS(coordinates: GKCoordinates): WGSCoordinates
15 | }
16 |
--------------------------------------------------------------------------------
/test-api/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules
3 |
--------------------------------------------------------------------------------
/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:3030'
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/hub-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": "4.5.0",
11 | "chai-http": "4.4.0",
12 | "mocha": "11.7.1"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/test-api/spec/001-authentication/input_pager.js:
--------------------------------------------------------------------------------
1 | let chai = require('chai');
2 | let chaiHttp = require('chai-http');
3 | let should = chai.should();
4 |
5 | chai.use(chaiHttp);
6 |
7 | const basePath = '/input/pager'
8 | describe(basePath, () => {
9 | describe('Authentication', () => {
10 | it(`GET ${basePath} should not work`, function (done) {
11 | chai.request(this.server.base)
12 | .get(basePath)
13 | .end((err, res) => {
14 | if (err) {
15 | return done(err)
16 | }
17 | res.should.have.status(405);
18 | done();
19 | });
20 | });
21 |
22 | it(`POST ${basePath} should require authentication`, function (done) {
23 | chai.request(this.server.base)
24 | .post(basePath)
25 | .send({})
26 | .end((err, res) => {
27 | if (err) {
28 | return done(err)
29 | }
30 | res.should.have.status(401);
31 | done();
32 | });
33 | });
34 |
35 | it(`POST ${basePath} should reject invalid JWT`, function (done) {
36 | chai.request(this.server.base)
37 | .post(basePath)
38 | .auth('invalid-token', { type: 'bearer' })
39 | .send({})
40 | .end((err, res) => {
41 | if (err) {
42 | return done(err)
43 | }
44 | res.should.have.status(401);
45 | done();
46 | });
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------