├── .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 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /console/src/App.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 81 | 82 | 87 | -------------------------------------------------------------------------------- /console/src/components/ApiKeyEditor.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 64 | 65 | 68 | -------------------------------------------------------------------------------- /console/src/components/BackButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 18 | -------------------------------------------------------------------------------- /console/src/components/DateTimePicker.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 60 | 61 | 64 | -------------------------------------------------------------------------------- /console/src/components/EditableText.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 77 | 78 | 87 | -------------------------------------------------------------------------------- /console/src/components/ErrorMessage.vue: -------------------------------------------------------------------------------- 1 | 27 | 52 | -------------------------------------------------------------------------------- /console/src/components/IncidentDetails.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 53 | 54 | 57 | -------------------------------------------------------------------------------- /console/src/components/IncidentListRow.vue: -------------------------------------------------------------------------------- 1 | 27 | 51 | 56 | -------------------------------------------------------------------------------- /console/src/components/IncidentMediaObject.vue: -------------------------------------------------------------------------------- 1 | 38 | 57 | -------------------------------------------------------------------------------- /console/src/components/IncidentSummary.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /console/src/components/PaginationUI.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 52 | -------------------------------------------------------------------------------- /console/src/components/ResourceIcon.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 40 | 41 | 44 | -------------------------------------------------------------------------------- /console/src/components/ScheduledAlertCard.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 77 | 78 | 84 | -------------------------------------------------------------------------------- /console/src/components/admin/settings/BooleanValue.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 66 | 67 | 70 | -------------------------------------------------------------------------------- /console/src/components/input/InputStepFormModal.vue: -------------------------------------------------------------------------------- 1 | 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 | 13 | -------------------------------------------------------------------------------- /console/src/views/OverviewPage.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 65 | -------------------------------------------------------------------------------- /console/src/views/admin/ApiKeyForm.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 77 | -------------------------------------------------------------------------------- /console/src/views/admin/SettingsPage.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 50 | 51 | 54 | -------------------------------------------------------------------------------- /console/src/views/admin/UserForm.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 78 | -------------------------------------------------------------------------------- /console/src/views/output/DisplayPage.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /console/src/views/processing/ResourceList.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 81 | 82 | 85 | -------------------------------------------------------------------------------- /console/src/views/processing/ScheduledAlertsList.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------