Compare commits

..

No commits in common. "main" and "v0.0.11" have entirely different histories.

207 changed files with 7339 additions and 17973 deletions

View File

@ -1,42 +1,6 @@
---
kind: secret
name: docker_username
get:
path: odit-registry-builder
name: username
---
kind: secret
name: docker_password
get:
path: odit-registry-builder
name: password
---
kind: secret
name: git_ssh
get:
path: odit-git-bot
name: sshkey
---
kind: secret
name: ci_token
get:
path: odit-ci-bot
name: apikey
---
kind: secret
name: npm_url
get:
path: odit-npm-cache
name: url
--- ---
kind: pipeline kind: pipeline
type: kubernetes name: tests:node_latest
name: tests:node
clone: clone:
disable: true disable: true
steps: steps:
@ -45,48 +9,51 @@ steps:
commands: commands:
- git clone $DRONE_REMOTE_URL . - git clone $DRONE_REMOTE_URL .
- git checkout $DRONE_SOURCE_BRANCH - git checkout $DRONE_SOURCE_BRANCH
- mv .env.ci .env
- name: run tests - name: run tests
image: registry.odit.services/hub/library/node:19.5.0-alpine3.16 image: node:14.15.1-alpine3.12
commands: commands:
- npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@8 - yarn
- pnpm i - yarn test:ci
- pnpm test:ci
environment:
NPM_REGISTRY_URL:
from_secret: npm_url
trigger: trigger:
event: event:
- pull_request - pull_request
--- ---
kind: pipeline kind: pipeline
type: kubernetes type: docker
name: build:dev name: build:dev
clone:
disable: true
steps: steps:
- name: clone
image: alpine/git
commands:
- git clone $DRONE_REMOTE_URL .
- git checkout dev
- name: build dev - name: build dev
depends_on: ["clone"] image: plugins/docker
image: registry.odit.services/library/drone-kaniko depends_on: [clone]
settings: settings:
username: username:
from_secret: docker_username from_secret: DOCKER_REGISTRY_USER
password: password:
from_secret: docker_password from_secret: DOCKER_REGISTRY_PASSWORD
build_args: repo: registry.odit.services/lfk/backend
- NPM_REGISTRY_URL:
from_secret: npm_url
repo: lfk/backend
tags: tags:
- dev - dev
cache: true
registry: registry.odit.services registry: registry.odit.services
- name: run full license export
depends_on: ["clone"]
image: node:14.15.1-alpine3.12
commands:
- yarn
- yarn licenses:export
- name: push new licenses file to repo
depends_on: ["run full license export"]
image: appleboy/drone-git-push
settings:
branch: dev
commit: true
commit_message: new license file version [CI SKIP]
author_email: bot@odit.services
remote: git@git.odit.services:lfk/backend.git
ssh_key:
from_secret: GITLAB_SSHKEY
trigger: trigger:
branch: branch:
@ -96,44 +63,22 @@ trigger:
--- ---
kind: pipeline kind: pipeline
type: kubernetes type: docker
name: build:latest name: build:latest
clone:
disable: true
steps: steps:
- name: clone
image: alpine/git
commands:
- git clone $DRONE_REMOTE_URL .
- git checkout dev
- git merge main
- git checkout main
- name: build latest - name: build latest
depends_on: ["clone"] image: plugins/docker
image: registry.odit.services/library/drone-kaniko depends_on: [clone]
settings: settings:
username: username:
from_secret: docker_username from_secret: DOCKER_REGISTRY_USER
password: password:
from_secret: docker_password from_secret: DOCKER_REGISTRY_PASSWORD
build_args: repo: registry.odit.services/lfk/backend
- NPM_REGISTRY_URL:
from_secret: npm_url
repo: lfk/backend
tags: tags:
- latest - latest
cache: true
registry: registry.odit.services registry: registry.odit.services
- name: push merge to repo
depends_on: ["clone"]
image: appleboy/drone-git-push
settings:
branch: dev
commit: false
remote: git@git.odit.services:lfk/backend.git
ssh_key:
from_secret: git_ssh
trigger: trigger:
branch: branch:
@ -143,32 +88,34 @@ trigger:
--- ---
kind: pipeline kind: pipeline
type: kubernetes type: docker
name: build:tags name: build:tags
steps: steps:
- name: build $DRONE_TAG - name: build $DRONE_TAG
depends_on: ["clone"] image: plugins/docker
image: registry.odit.services/library/drone-kaniko depends_on: [clone]
settings: settings:
username: username:
from_secret: docker_username from_secret: DOCKER_REGISTRY_USER
password: password:
from_secret: docker_password from_secret: DOCKER_REGISTRY_PASSWORD
build_args: repo: registry.odit.services/lfk/backend
- NPM_REGISTRY_URL:
from_secret: npm_url
repo: lfk/backend
tags: tags:
- "${DRONE_TAG}" - '${DRONE_TAG}'
cache: true
registry: registry.odit.services registry: registry.odit.services
- name: trigger node lib build
image: idcooldi/drone-webhook
settings:
urls: https://ci.odit.services/api/repos/lfk/lfk-client-node/builds?SOURCE_TAG=${DRONE_TAG}
bearer:
from_secret: BOT_DRONE_KEY
- name: trigger js lib build - name: trigger js lib build
image: idcooldi/drone-webhook image: idcooldi/drone-webhook
settings: settings:
urls: https://ci.odit.services/api/repos/lfk/lfk-client-js/builds?SOURCE_TAG=${DRONE_TAG} urls: https://ci.odit.services/api/repos/lfk/lfk-client-js/builds?SOURCE_TAG=${DRONE_TAG}
bearer: bearer:
from_secret: ci_token from_secret: BOT_DRONE_KEY
trigger: trigger:
event: event:
- tag - tag

View File

@ -6,4 +6,4 @@ DB_USER=unused
DB_PASSWORD=bla DB_PASSWORD=bla
DB_NAME=./test.sqlite DB_NAME=./test.sqlite
NODE_ENV=dev NODE_ENV=dev
POSTALCODE_COUNTRYCODE=DE POSTALCODE_COUNTRYCODE=null

View File

@ -1,10 +1,9 @@
APP_PORT=4010 APP_PORT=4010
DB_TYPE=sqlite DB_TYPE=bla
DB_HOST=bla DB_HOST=bla
DB_PORT=bla DB_PORT=bla
DB_USER=bla DB_USER=bla
DB_PASSWORD=bla DB_PASSWORD=bla
DB_NAME=./test.sqlite DB_NAME=bla
NODE_ENV=production NODE_ENV=production
POSTALCODE_COUNTRYCODE=DE POSTALCODE_COUNTRYCODE=null
SEED_TEST_DATA=false

2
.gitignore vendored
View File

@ -135,4 +135,4 @@ build
/docs /docs
lib lib
/oss-attribution /oss-attribution
*.tmp *.tmp

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,16 @@
# Typescript Build # Typescript Build
FROM registry.odit.services/hub/library/node:21.1.0-alpine3.18 as build FROM node:14.15.1-alpine3.12
ARG NPM_REGISTRY_URL=https://registry.npmjs.org
WORKDIR /app WORKDIR /app
COPY package.json ./ COPY package.json ./
RUN npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@8 RUN npm i -g pnpm
RUN mkdir /pnpm && pnpm config set store-dir /pnpm && pnpm i RUN pnpm i
COPY tsconfig.json ormconfig.js ./ COPY tsconfig.json ormconfig.js ./
COPY src ./src COPY src ./src
RUN pnpm run build \ RUN pnpm run build
&& rm -rf /app/node_modules \
&& pnpm i --production --prefer-offline
# final image # final image
FROM registry.odit.services/hub/library/node:21.1.0-alpine3.18 as final FROM node:14.15.1-alpine3.12
WORKDIR /app COPY package.json ormconfig.js ./
COPY --from=build /app/package.json /app/package.json RUN npm i -g pnpm
COPY --from=build /app/ormconfig.js /app/ormconfig.js RUN pnpm i --prod
COPY --from=build /app/dist /app/dist COPY --from=0 /app/dist dist
COPY --from=build /app/node_modules /app/node_modules ENTRYPOINT ["node", "dist/app.js"]
ENTRYPOINT ["node", "/app/dist/app.js"]

115
README.md
View File

@ -2,73 +2,39 @@
Backend Server Backend Server
## Quickstart 🐳
> Use this to run the backend with a postgresql db in docker
1. Clone the repo or copy the docker-compose
2. Run in toe folder that contains the docker-compose file: `docker-compose up -d`
3. Visit http://127.0.0.1:4010/api/docs to check if the server is running
4. You can now use the default admin user (`demo:demo`)
## Dev Setup 🛠 ## Dev Setup 🛠
> Local dev setup utilizing sqlite3 as the database.
1. Rename the .env.example file to .env (you can adjust app port and other settings, if needed) ### Local w/ sqlite
1. Create a .env file in the project root containing:
```
APP_PORT=4010
DB_TYPE=sqlite
DB_HOST=bla
DB_PORT=bla
DB_USER=bla
DB_PASSWORD=bla
DB_NAME=./test.sqlite
```
2. Install Dependencies 2. Install Dependencies
```bash ```bash
pnpm i yarn
``` ```
3. Start the server 3. Start the server
```bash ```bash
pnpm dev yarn dev
``` ```
### Run Tests
```bash
# Run tests once (server has to run)
pnpm test
# Run test in watch mode (reruns on change)
pnpm test:watch
# Run test in ci mode (automaticly starts the dev server)
pnpm test:ci
```
### Use your own mail templates
> You use your own mail templates by replacing the default ones we provided (either in-code or by mounting them into the /app/static/mail_templates folder).
The mail templates always come in a .html and a .txt variant to provide compatability with legacy mail clients.
Currently the following templates exist:
* pw-reset.(html/txt)
### Generate Docs ### Generate Docs
```bash ```
pnpm docs yarn docs
``` ```
## ENV Vars ### Docker w/ postgres 🐳
> You can provide them via .env file or docker env vars.
> You can use the `test:ci:generate_env` package script to generate a example env (uses bs data as test server and ignores the errors).
| Name | Type | Default | Description |
| ---------------------- | ------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------- |
| APP_PORT | Number | 4010 | The port the backend server listens on. Is optional. |
| DB_TYPE | String | N/A | The type of the db u want to use. It has to be supported by typeorm. Possible: `sqlite`, `mysql`, `postgresql` |
| DB_HOST | String | N/A | The db's host's ip-address/fqdn or file path for sqlite |
| DB_PORT | String | N/A | The db's port |
| DB_USER | String | N/A | The user for accessing the db |
| DB_PASSWORD | String | N/A | The user's password for accessing the db |
| DB_NAME | String | N/A | The db's name |
| NODE_ENV | String | dev | The apps env - influences debug info. Also when the env is set to "test", mailing errors get ignored. |
| POSTALCODE_COUNTRYCODE | String/CountryCode | N/A | The countrycode used to validate address's postal codes |
| PHONE_COUNTRYCODE | String/CountryCode | null (international) | The countrycode used to validate phone numers |
| SEED_TEST_DATA | Boolean | False | If you want the app to seed some example data set this to true |
| MAILER_URL | String(Url) | N/A | The mailer's base url (no trailing slash) |
| MAILER_KEY | String | N/A | The mailer's api key. |
| IMPRINT_URL | String(Url) | /imprint | The link to a imprint page for the system (Defaults to the frontend's imprint) |
| PRIVACY_URL | String(Url) | /privacy | The link to a privacy page for the system (Defaults to the frontend's privacy page) |
```bash
docker-compose up --build
```
## Recommended Editor ## Recommended Editor
@ -76,19 +42,22 @@ pnpm docs
### Recommended Extensions ### Recommended Extensions
* will be automatically recommended via ./vscode/extensions.json - will be automatically recommended via ./vscode/extensions.json
## Staging ## Branches
### Branches & Tags - main: Protected "release" branch
* vX.Y.Z: Release tags created from the main branch - dev: Current dev branch for merging the different features - only push for merges or minor changes!
* The version numbers follow the semver standard - feature/xyz: Feature branches - `feature/issueid-title`
* A new release tag automaticly triggers the release ci pipeline - bugfix/xyz: Branches for bugfixes - `bugfix/issueid-title` (no id for readme changes needed)
* main: Protected "release" branch
* The latest tag of the docker image get's build from this
* dev: Current dev branch for merging the different feature branches and bugfixes ## File Structure
* New releases get created as tags from this
* The dev tag of the docker image get's build from this - src/models/entities\* - database models (typeorm entities)
* Only push minor changes to this branch! - src/models/actions\* - actions models
* To merge a feature branch into this please create a pull request - src/models/responses\* - response models
* feature/xyz: Feature branches - naming scheme: `feature/issueid-title` - src/controllers/\* - routing-controllers
* bugfix/xyz: Branches for bugfixes - naming scheme:`bugfix/issueid-title` - src/loaders/\* - loaders for the different init steps of the api server
- src/middlewares/\* - express middlewares (mainly auth r/n)
- src/errors/* - our custom (http) errors
- src/routes/\* - express routes for everything we don't do via routing-controllers (depreciated)

View File

@ -11,12 +11,8 @@ services:
DB_PORT: bla DB_PORT: bla
DB_USER: bla DB_USER: bla
DB_PASSWORD: bla DB_PASSWORD: bla
DB_NAME: ./db.sqlite DB_NAME: dev.sqlite
NODE_ENV: production NODE_ENV: production
POSTALCODE_COUNTRYCODE: DE
SEED_TEST_DATA: "false"
MAILER_URL: https://dev.lauf-fuer-kaya.de/mailer
MAILER_KEY: asdasd
# APP_PORT: 4010 # APP_PORT: 4010
# DB_TYPE: postgres # DB_TYPE: postgres
# DB_HOST: backend_db # DB_HOST: backend_db

View File

@ -1,32 +1,3 @@
# @odit/class-validator-jsonschema
**Author**: Aleksi Pekkala <aleksipekkala@gmail.com>
**Repo**: git@github.com:epiphone/class-validator-jsonschema.git
**License**: MIT
**Description**: Convert class-validator-decorated classes into JSON schema
## License Text
MIT License
Copyright (c) 2017 Aleksi Pekkala
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# argon2 # argon2
**Author**: Ranieri Althoff <ranisalt+argon2@gmail.com> **Author**: Ranieri Althoff <ranisalt+argon2@gmail.com>
**Repo**: [object Object] **Repo**: [object Object]
@ -57,33 +28,6 @@ SOFTWARE.
# axios
**Author**: Matt Zabriskie
**Repo**: [object Object]
**License**: MIT
**Description**: Promise based HTTP client for the browser and node.js
## License Text
Copyright (c) 2014-present Matt Zabriskie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
# body-parser # body-parser
**Author**: undefined **Author**: undefined
**Repo**: expressjs/body-parser **Repo**: expressjs/body-parser
@ -115,35 +59,6 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# check-password-strength
**Author**: deanilvincent
**Repo**: [object Object]
**License**: MIT
**Description**: A NPM Password strength checker based from Javascript RegExp. Check passphrase if it's "Weak", "Medium" or "Strong"
## License Text
MIT License
Copyright (c) 2020 Mark Deanil Vicente
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# class-transformer # class-transformer
**Author**: [object Object] **Author**: [object Object]
**Repo**: [object Object] **Repo**: [object Object]
@ -173,15 +88,22 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
# class-validator # class-validator
**Author**: TypeStack contributors **Author**: [object Object]
**Repo**: [object Object] **Repo**: [object Object]
**License**: MIT **License**: MIT
**Description**: Decorator-based property validation for classes. **Description**: Class-based validation with Typescript / ES6 / ES5 using decorators or validation schemas. Supports both node.js and browser
## License Text ## License Text
The MIT License # class-validator-jsonschema
**Author**: Aleksi Pekkala <aleksipekkala@gmail.com>
**Repo**: git@github.com:epiphone/class-validator-jsonschema.git
**License**: MIT
**Description**: Convert class-validator-decorated classes into JSON schema
## License Text
MIT License
Copyright (c) 2015-2020 TypeStack Copyright (c) 2017 Aleksi Pekkala
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -190,16 +112,17 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in The above copyright notice and this permission notice shall be included in all
all copies or substantial portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
THE SOFTWARE. SOFTWARE.
# consola # consola
**Author**: undefined **Author**: undefined
@ -409,60 +332,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
# libphonenumber-js
**Author**: catamphetamine <purecatamphetamine@gmail.com>
**Repo**: [object Object]
**License**: MIT
**Description**: A simpler (and smaller) rewrite of Google Android's libphonenumber library in javascript
## License Text
(The MIT License)
Copyright (c) 2016 @catamphetamine <purecatamphetamine@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# mysql # mysql
**Author**: Felix Geisendörfer <felix@debuggable.com> (http://debuggable.com/) **Author**: Felix Geisendörfer <felix@debuggable.com> (http://debuggable.com/)
**Repo**: mysqljs/mysql **Repo**: mysqljs/mysql
**License**: MIT **License**: MIT
**Description**: A node.js driver for mysql. It is written in JavaScript, does not require compiling, and is 100% MIT licensed. **Description**: A node.js driver for mysql. It is written in JavaScript, does not require compiling, and is 100% MIT licensed.
## License Text ## License Text
Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
# pg # pg
@ -715,80 +590,11 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# @faker-js/faker
**Author**: undefined
**Repo**: [object Object]
**License**: MIT
**Description**: Generate massive amounts of fake contextual data
## License Text
Faker - Copyright (c) 2022
This software consists of voluntary contributions made by many individuals.
For exact contribution history, see the revision history
available at https://github.com/faker-js/faker
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
===
From: https://github.com/faker-js/faker/commit/a9f98046c7d5eeaabe12fc587024c06d683800b8
To: https://github.com/faker-js/faker/commit/29234378807c4141588861f69421bf20b5ac635e
Based on faker.js, copyright Marak Squires and contributor, what follows below is the original license.
===
faker.js - Copyright (c) 2020
Marak Squires
http://github.com/marak/faker.js/
faker.js was inspired by and has used data definitions from:
* https://github.com/stympy/faker/ - Copyright (c) 2007-2010 Benjamin Curtis
* http://search.cpan.org/~jasonk/Data-Faker-0.07/ - Copyright 2004-2005 by Jason Kohles
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# @odit/license-exporter # @odit/license-exporter
**Author**: ODIT.Services **Author**: ODIT.Services
**Repo**: [object Object] **Repo**: [object Object]
**License**: MIT **License**: MIT
**Description**: A simple license crawler for crediting open source work **Description**: A simple license crawler
## License Text ## License Text
MIT License Copyright (c) 2020 ODIT.Services (info@odit.services) MIT License Copyright (c) 2020 ODIT.Services (info@odit.services)
@ -1014,15 +820,13 @@ OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
SOFTWARE SOFTWARE
# auto-changelog # axios
**Author**: Pete Cook <pete@cookpete.com> (https://github.com/cookpete) **Author**: Matt Zabriskie
**Repo**: [object Object] **Repo**: [object Object]
**License**: MIT **License**: MIT
**Description**: Command line tool for generating a changelog from git tags and commit history **Description**: Promise based HTTP client for the browser and node.js
## License Text ## License Text
The MIT License Copyright (c) 2014-present Matt Zabriskie
Copyright (c) 2017 Pete Cook https://cookpete.com
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -1130,35 +934,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
# release-it
**Author**: [object Object]
**Repo**: [object Object]
**License**: MIT
**Description**: Generic CLI tool to automate versioning and package publishing related tasks.
## License Text
MIT License
Copyright (c) 2018 Lars Kappert
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# rimraf # rimraf
**Author**: Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/) **Author**: Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)
**Repo**: git://github.com/isaacs/rimraf.git **Repo**: git://github.com/isaacs/rimraf.git

View File

@ -1,117 +1,102 @@
{ {
"name": "@odit/lfk-backend", "name": "@odit/lfk-backend",
"version": "1.1.3", "version": "0.0.11",
"main": "src/app.ts", "main": "src/app.ts",
"repository": "https://git.odit.services/lfk/backend", "repository": "https://git.odit.services/lfk/backend",
"engines": { "author": {
"pnpm": "8" "name": "ODIT.Services",
}, "email": "info@odit.services",
"author": { "url": "https://odit.services"
"name": "ODIT.Services", },
"email": "info@odit.services", "contributors": [
"url": "https://odit.services" {
}, "name": "Philipp Dormann",
"contributors": [ "email": "philipp@philippdormann.de",
{ "url": "https://philippdormann.de"
"name": "Philipp Dormann", },
"email": "philipp@philippdormann.de", {
"url": "https://philippdormann.de" "name": "Nicolai Ort",
}, "email": "info@nicolai-ort.com",
{ "url": "https://nicolai-ort.com"
"name": "Nicolai Ort", }
"email": "info@nicolai-ort.com", ],
"url": "https://nicolai-ort.com" "license": "CC-BY-NC-SA-4.0",
} "dependencies": {
], "argon2": "^0.27.1",
"license": "CC-BY-NC-SA-4.0", "body-parser": "^1.19.0",
"dependencies": { "class-transformer": "^0.3.1",
"@odit/class-validator-jsonschema": "2.1.1", "class-validator": "^0.12.2",
"argon2": "0.27.1", "class-validator-jsonschema": "^2.1.0",
"axios": "0.21.1", "consola": "^2.15.0",
"body-parser": "1.19.0", "cookie": "^0.4.1",
"check-password-strength": "2.0.2", "cookie-parser": "^1.4.5",
"class-transformer": "0.3.1", "cors": "^2.8.5",
"class-validator": "0.13.1", "csvtojson": "^2.0.10",
"consola": "2.15.0", "dotenv": "^8.2.0",
"cookie": "0.4.1", "express": "^4.17.1",
"cookie-parser": "1.4.5", "jsonwebtoken": "^8.5.1",
"cors": "2.8.5", "mysql": "^2.18.1",
"csvtojson": "2.0.10", "pg": "^8.5.1",
"dotenv": "8.2.0", "reflect-metadata": "^0.1.13",
"express": "4.17.1", "routing-controllers": "^0.9.0-alpha.6",
"jsonwebtoken": "8.5.1", "routing-controllers-openapi": "^2.2.0",
"libphonenumber-js": "1.9.9", "sqlite3": "5.0.0",
"mysql": "2.18.1", "typeorm": "^0.2.29",
"pg": "8.5.1", "typeorm-routing-controllers-extensions": "^0.2.0",
"reflect-metadata": "0.1.13", "typeorm-seeding": "^1.6.1",
"routing-controllers": "0.9.0-alpha.6", "uuid": "^8.3.2",
"routing-controllers-openapi": "2.2.0", "validator": "^13.5.2"
"sqlite3": "5.0.0", },
"typeorm": "0.2.30", "devDependencies": {
"typeorm-routing-controllers-extensions": "0.2.0", "@odit/license-exporter": "^0.0.9",
"typeorm-seeding": "1.6.1", "@types/cors": "^2.8.9",
"uuid": "8.3.2", "@types/csvtojson": "^1.1.5",
"validator": "13.5.2" "@types/express": "^4.17.9",
}, "@types/jest": "^26.0.16",
"devDependencies": { "@types/jsonwebtoken": "^8.5.0",
"@faker-js/faker": "7.6.0", "@types/node": "^14.14.20",
"@odit/license-exporter": "0.0.9", "@types/uuid": "^8.3.0",
"@types/cors": "2.8.9", "axios": "^0.21.1",
"@types/csvtojson": "1.1.5", "cp-cli": "^2.0.0",
"@types/express": "4.17.11", "jest": "^26.6.3",
"@types/jest": "26.0.20", "nodemon": "^2.0.7",
"@types/jsonwebtoken": "8.5.0", "release-it": "^14.2.2",
"@types/node": "14.14.22", "rimraf": "^3.0.2",
"@types/uuid": "8.3.0", "start-server-and-test": "^1.11.7",
"auto-changelog": "2.4.0", "ts-jest": "^26.4.4",
"cp-cli": "2.0.0", "ts-node": "^9.1.1",
"jest": "26.6.3", "typedoc": "^0.20.14",
"nodemon": "2.0.7", "typescript": "^4.1.3"
"release-it": "14.2.2", },
"rimraf": "3.0.2", "scripts": {
"start-server-and-test": "1.11.7", "dev": "nodemon src/app.ts",
"ts-jest": "26.5.0", "build": "rimraf ./dist && tsc && cp-cli ./src/static ./dist/static",
"ts-node": "9.1.1", "docs": "typedoc --out docs src",
"typedoc": "0.20.19", "test": "jest",
"typescript": "4.1.3" "test:watch": "jest --watchAll",
}, "test:ci": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test",
"scripts": { "seed": "ts-node ./node_modules/typeorm/cli.js schema:sync && ts-node ./node_modules/typeorm-seeding/dist/cli.js seed",
"dev": "nodemon src/app.ts", "openapi:export": "ts-node scripts/openapi_export.ts",
"build": "rimraf ./dist && tsc && cp-cli ./src/static ./dist/static", "licenses:export": "license-exporter --md",
"docs": "typedoc --out docs src", "release": "release-it"
"test": "jest", },
"test:watch": "jest --watchAll", "release-it": {
"test:ci:generate_env": "ts-node scripts/create_testenv.ts", "git": {
"test:ci:run": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test", "requireCleanWorkingDir": false,
"test:ci": "npm run test:ci:generate_env && npm run test:ci:run", "requireBranch": "main",
"seed": "ts-node ./node_modules/typeorm/cli.js schema:sync && ts-node ./node_modules/typeorm-seeding/dist/cli.js seed", "push": false,
"openapi:export": "ts-node scripts/openapi_export.ts", "tag": true,
"licenses:export": "license-exporter --markdown", "tagName": "v${version}",
"changelog:export": "auto-changelog --commit-limit false -p -u --hide-credit", "tagAnnotation": "v${version}"
"release": "release-it --only-version" },
}, "npm": {
"release-it": { "publish": false
"git": { }
"commit": true, },
"requireCleanWorkingDir": false, "nodemonConfig": {
"commitMessage": "🚀Bumped version to v${version}", "ignore": [
"requireBranch": "dev", "src/tests/*",
"push": true, "docs/*"
"tag": true, ]
"tagName": "v${version}", }
"tagAnnotation": "v${version}"
},
"npm": {
"publish": false
},
"hooks": {
"after:bump": "npm run changelog:export && npm run licenses:export && git add CHANGELOG.md && git add licenses.md"
}
},
"nodemonConfig": {
"ignore": [
"src/tests/*",
"docs/*"
]
}
} }

7803
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
import consola from "consola";
import fs from "fs";
const env = `
APP_PORT=4010
DB_TYPE=sqlite
DB_HOST=bla
DB_PORT=bla
DB_USER=bla
DB_PASSWORD=bla
DB_NAME=./test.sqlite
NODE_ENV=test
POSTALCODE_COUNTRYCODE=DE
SEED_TEST_DATA=true
MAILER_URL=https://dev.lauf-fuer-kaya.de/mailer
MAILER_KEY=asdasd`;
try {
fs.writeFileSync("./.env", env, { encoding: "utf-8" });
consola.success("Exported ci env to .env");
} catch (error) {
consola.error("Couldn't export the ci env");
}

View File

@ -1,4 +1,4 @@
import { validationMetadatasToSchemas } from '@odit/class-validator-jsonschema'; import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
import consola from "consola"; import consola from "consola";
import fs from "fs"; import fs from "fs";
import "reflect-metadata"; import "reflect-metadata";

View File

@ -1,6 +1,5 @@
import { MetadataArgsStorage } from 'routing-controllers'; import { MetadataArgsStorage } from 'routing-controllers';
import { routingControllersToSpec } from 'routing-controllers-openapi'; import { routingControllersToSpec } from 'routing-controllers-openapi';
import { config } from './config';
/** /**
* This function generates a the openapi spec from route metadata and type schemas. * This function generates a the openapi spec from route metadata and type schemas.
@ -42,9 +41,9 @@ export function generateSpec(storage: MetadataArgsStorage, schemas) {
} }
}, },
info: { info: {
description: `The the backend API for the LfK! runner system. <br>[Imprint](${config.imprint_url}) & [Privacy](${config.privacy_url})`, description: "The the backend API for the LfK! runner system.",
title: "LfK! Backend API", title: "LfK! Backend API",
version: config.version version: process.env.npm_package_version
}, },
} }
); );

View File

@ -5,12 +5,10 @@ import { config, e as errors } from './config';
import loaders from "./loaders/index"; import loaders from "./loaders/index";
import authchecker from "./middlewares/authchecker"; import authchecker from "./middlewares/authchecker";
import { ErrorHandler } from './middlewares/ErrorHandler'; import { ErrorHandler } from './middlewares/ErrorHandler';
import UserChecker from './middlewares/UserChecker';
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts'; const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
const app = createExpressServer({ const app = createExpressServer({
authorizationChecker: authchecker, authorizationChecker: authchecker,
currentUserChecker: UserChecker,
middlewares: [ErrorHandler], middlewares: [ErrorHandler],
development: config.development, development: config.development,
cors: true, cors: true,
@ -20,9 +18,6 @@ const app = createExpressServer({
async function main() { async function main() {
await loaders(app); await loaders(app);
if (config.testing) {
consola.info("🛠[config]: Discovered testing env. Mailing errors will get ignored!")
}
app.listen(config.internal_port, () => { app.listen(config.internal_port, () => {
consola.success( consola.success(
`⚡️[server]: Server is running at http://localhost:${config.internal_port}` `⚡️[server]: Server is running at http://localhost:${config.internal_port}`

View File

@ -1,35 +1,26 @@
import { config as configDotenv } from 'dotenv'; import { config as configDotenv } from 'dotenv';
import { CountryCode } from 'libphonenumber-js';
import ValidatorJS from 'validator'; import ValidatorJS from 'validator';
configDotenv(); configDotenv();
export const config = { export const config = {
internal_port: parseInt(process.env.APP_PORT) || 4010, internal_port: parseInt(process.env.APP_PORT) || 4010,
development: process.env.NODE_ENV === "production", development: process.env.NODE_ENV === "production",
testing: process.env.NODE_ENV === "test",
jwt_secret: process.env.JWT_SECRET || "secretjwtsecret", jwt_secret: process.env.JWT_SECRET || "secretjwtsecret",
phone_validation_countrycode: getPhoneCodeLocale(), phone_validation_countrycode: process.env.PHONE_COUNTRYCODE || "ZZ",
postalcode_validation_countrycode: getPostalCodeLocale(), postalcode_validation_countrycode: getPostalCodeLocale()
version: process.env.VERSION || require('../package.json').version,
seedTestData: getDataSeeding(),
app_url: process.env.APP_URL || "http://localhost:8080",
privacy_url: process.env.PRIVACY_URL || "/privacy",
imprint_url: process.env.IMPRINT_URL || "/imprint",
mailer_url: process.env.MAILER_URL || "",
mailer_key: process.env.MAILER_KEY || ""
} }
let errors = 0 let errors = 0
if (typeof config.internal_port !== "number") { if (typeof config.internal_port !== "number") {
errors++ errors++
} }
if (typeof config.development !== "boolean") { if (typeof config.phone_validation_countrycode !== "string") {
errors++ errors++
} }
if (config.mailer_url == "" || config.mailer_key == "") { if (config.phone_validation_countrycode.length !== 2) {
errors++; errors++
} }
function getPhoneCodeLocale(): CountryCode { if (typeof config.development !== "boolean") {
return (process.env.PHONE_COUNTRYCODE as CountryCode); errors++
} }
function getPostalCodeLocale(): any { function getPostalCodeLocale(): any {
try { try {
@ -40,11 +31,4 @@ function getPostalCodeLocale(): any {
return null; return null;
} }
} }
function getDataSeeding(): Boolean {
try {
return JSON.parse(process.env.SEED_TEST_DATA);
} catch (error) {
return false;
}
}
export let e = errors export let e = errors

View File

@ -1,106 +1,103 @@
import { Body, CookieParam, JsonController, Param, Post, QueryParam, Req, Res } from 'routing-controllers'; import { Body, CookieParam, JsonController, Param, Post, Req, Res } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError'; import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError';
import { MailSendingError } from '../errors/MailErrors'; import { UserNotFoundError } from '../errors/UserErrors';
import { UserNotFoundError } from '../errors/UserErrors'; import { CreateAuth } from '../models/actions/create/CreateAuth';
import { Mailer } from '../mailer'; import { CreateResetToken } from '../models/actions/create/CreateResetToken';
import { CreateAuth } from '../models/actions/create/CreateAuth'; import { HandleLogout } from '../models/actions/HandleLogout';
import { CreateResetToken } from '../models/actions/create/CreateResetToken'; import { RefreshAuth } from '../models/actions/RefreshAuth';
import { HandleLogout } from '../models/actions/HandleLogout'; import { ResetPassword } from '../models/actions/ResetPassword';
import { RefreshAuth } from '../models/actions/RefreshAuth'; import { ResponseAuth } from '../models/responses/ResponseAuth';
import { ResetPassword } from '../models/actions/ResetPassword'; import { Logout } from '../models/responses/ResponseLogout';
import { ResponseAuth } from '../models/responses/ResponseAuth';
import { ResponseEmpty } from '../models/responses/ResponseEmpty'; @JsonController('/auth')
import { Logout } from '../models/responses/ResponseLogout'; export class AuthController {
constructor() {
@JsonController('/auth') }
export class AuthController {
@Post("/login")
@Post("/login") @ResponseSchema(ResponseAuth)
@ResponseSchema(ResponseAuth) @ResponseSchema(InvalidCredentialsError)
@ResponseSchema(InvalidCredentialsError) @ResponseSchema(UserNotFoundError)
@ResponseSchema(UserNotFoundError) @ResponseSchema(UsernameOrEmailNeededError)
@ResponseSchema(UsernameOrEmailNeededError) @ResponseSchema(PasswordNeededError)
@ResponseSchema(PasswordNeededError) @ResponseSchema(InvalidCredentialsError)
@ResponseSchema(InvalidCredentialsError) @OpenAPI({ description: 'Login with your username/email and password. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)' })
@OpenAPI({ description: 'Login with your username/email and password. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)' }) async login(@Body({ validate: true }) createAuth: CreateAuth, @Res() response: any) {
async login(@Body({ validate: true }) createAuth: CreateAuth, @Res() response: any) { let auth;
let auth; try {
try { auth = await createAuth.toAuth();
auth = await createAuth.toAuth(); response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true }); response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true }); return response.send(auth)
return response.send(auth) } catch (error) {
} catch (error) { throw error;
throw error; }
} }
}
@Post("/logout")
@Post("/logout") @ResponseSchema(Logout)
@ResponseSchema(Logout) @ResponseSchema(InvalidCredentialsError)
@ResponseSchema(InvalidCredentialsError) @ResponseSchema(UserNotFoundError)
@ResponseSchema(UserNotFoundError) @ResponseSchema(UsernameOrEmailNeededError)
@ResponseSchema(UsernameOrEmailNeededError) @ResponseSchema(PasswordNeededError)
@ResponseSchema(PasswordNeededError) @ResponseSchema(InvalidCredentialsError)
@ResponseSchema(InvalidCredentialsError) @OpenAPI({ description: 'Logout using your refresh token. <br> This instantly invalidates all your access and refresh tokens.', security: [{ "RefreshTokenCookie": [] }] })
@OpenAPI({ description: 'Logout using your refresh token. <br> This instantly invalidates all your access and refresh tokens.', security: [{ "RefreshTokenCookie": [] }] }) async logout(@Body({ validate: true }) handleLogout: HandleLogout, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any) {
async logout(@Body({ validate: true }) handleLogout: HandleLogout, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any) { if (refresh_token && refresh_token.length != 0 && handleLogout.token == undefined) {
if (refresh_token && refresh_token.length != 0 && handleLogout.token == undefined) { handleLogout.token = refresh_token;
handleLogout.token = refresh_token; }
}
let logout;
let logout; try {
try { logout = await handleLogout.logout()
logout = await handleLogout.logout() await response.cookie('lfk_backend__refresh_token', "expired", { expires: new Date(Date.now()), httpOnly: true });
await response.cookie('lfk_backend__refresh_token', "expired", { expires: new Date(Date.now()), httpOnly: true }); response.cookie('lfk_backend__refresh_token_expires_at', "expired", { expires: new Date(Date.now()), httpOnly: true });
response.cookie('lfk_backend__refresh_token_expires_at', "expired", { expires: new Date(Date.now()), httpOnly: true }); } catch (error) {
} catch (error) { throw error;
throw error; }
} return response.send(logout)
return response.send(logout) }
}
@Post("/refresh")
@Post("/refresh") @ResponseSchema(ResponseAuth)
@ResponseSchema(ResponseAuth) @ResponseSchema(JwtNotProvidedError)
@ResponseSchema(JwtNotProvidedError) @ResponseSchema(IllegalJWTError)
@ResponseSchema(IllegalJWTError) @ResponseSchema(UserNotFoundError)
@ResponseSchema(UserNotFoundError) @ResponseSchema(RefreshTokenCountInvalidError)
@ResponseSchema(RefreshTokenCountInvalidError) @OpenAPI({ description: 'Refresh your access and refresh tokens using a valid refresh token. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)', security: [{ "RefreshTokenCookie": [] }] })
@OpenAPI({ description: 'Refresh your access and refresh tokens using a valid refresh token. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)', security: [{ "RefreshTokenCookie": [] }] }) async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any, @Req() req: any) {
async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any, @Req() req: any) { if (refresh_token && refresh_token.length != 0 && refreshAuth.token == undefined) {
if (refresh_token && refresh_token.length != 0 && refreshAuth.token == undefined) { refreshAuth.token = refresh_token;
refreshAuth.token = refresh_token; }
} let auth;
let auth; try {
try { auth = await refreshAuth.toAuth();
auth = await refreshAuth.toAuth(); response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true }); response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true }); } catch (error) {
} catch (error) { throw error;
throw error; }
} return response.send(auth)
return response.send(auth) }
}
@Post("/reset")
@Post("/reset") @ResponseSchema(ResponseAuth)
@ResponseSchema(ResponseEmpty, { statusCode: 200 }) @ResponseSchema(UserNotFoundError)
@ResponseSchema(UserNotFoundError, { statusCode: 404 }) @ResponseSchema(UsernameOrEmailNeededError)
@ResponseSchema(UsernameOrEmailNeededError, { statusCode: 406 }) @OpenAPI({ description: "Request a password reset token. <br> This will provide you with a reset token that you can use by posting to /api/auth/reset/{token}." })
@ResponseSchema(MailSendingError, { statusCode: 500 }) async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken) {
@OpenAPI({ description: "Request a password reset token. <br> This will provide you with a reset token that you can use by posting to /api/auth/reset/{token}." }) //This really shouldn't just get returned, but sent via mail or sth like that. But for dev only this is fine.
async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken, @QueryParam("locale") locale: string = "en") { return { "resetToken": await passwordReset.toResetToken() };
const reset_token: string = await passwordReset.toResetToken(); }
await Mailer.sendResetMail(passwordReset.email, reset_token, locale);
return new ResponseEmpty(); @Post("/reset/:token")
} @ResponseSchema(ResponseAuth)
@ResponseSchema(UserNotFoundError)
@Post("/reset/:token") @ResponseSchema(UsernameOrEmailNeededError)
@ResponseSchema(ResponseAuth) @OpenAPI({ description: "Reset a user's utilising a valid password reset token. <br> This will set the user's password to the one you provided in the body. <br> To get a reset token post to /api/auth/reset with your username." })
@ResponseSchema(UserNotFoundError) async resetPassword(@Param("token") token: string, @Body({ validate: true }) passwordReset: ResetPassword) {
@ResponseSchema(UsernameOrEmailNeededError) passwordReset.resetToken = token;
@OpenAPI({ description: "Reset a user's utilising a valid password reset token. <br> This will set the user's password to the one you provided in the body. <br> To get a reset token post to /api/auth/reset with your username." }) return await passwordReset.resetPassword();
async resetPassword(@Param("token") token: string, @Body({ validate: true }) passwordReset: ResetPassword) { }
passwordReset.resetToken = token; }
return await passwordReset.resetPassword();
}
}

View File

@ -1,152 +0,0 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { DonationIdsNotMatchingError, DonationNotFoundError } from '../errors/DonationErrors';
import { DonorNotFoundError } from '../errors/DonorErrors';
import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { CreateDistanceDonation } from '../models/actions/create/CreateDistanceDonation';
import { CreateFixedDonation } from '../models/actions/create/CreateFixedDonation';
import { UpdateDistanceDonation } from '../models/actions/update/UpdateDistanceDonation';
import { UpdateFixedDonation } from '../models/actions/update/UpdateFixedDonation';
import { DistanceDonation } from '../models/entities/DistanceDonation';
import { Donation } from '../models/entities/Donation';
import { FixedDonation } from '../models/entities/FixedDonation';
import { ResponseDistanceDonation } from '../models/responses/ResponseDistanceDonation';
import { ResponseDonation } from '../models/responses/ResponseDonation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
@JsonController('/donations')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class DonationController {
private donationRepository: Repository<Donation>;
private distanceDonationRepository: Repository<DistanceDonation>;
private fixedDonationRepository: Repository<FixedDonation>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.donationRepository = getConnectionManager().get().getRepository(Donation);
this.distanceDonationRepository = getConnectionManager().get().getRepository(DistanceDonation);
this.fixedDonationRepository = getConnectionManager().get().getRepository(FixedDonation);
}
@Get()
@Authorized("DONATION:GET")
@ResponseSchema(ResponseDonation, { isArray: true })
@ResponseSchema(ResponseDistanceDonation, { isArray: true })
@OpenAPI({ description: 'Lists all donations (fixed or distance based) from all donors. <br> This includes the donations\'s runner\'s distance ran(if distance donation).' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseDonations: ResponseDonation[] = new Array<ResponseDonation>();
let donations: Array<Donation>;
if (page != undefined) {
donations = await this.donationRepository.find({ relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'], skip: page * page_size, take: page_size });
} else {
donations = await this.donationRepository.find({ relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] });
}
donations.forEach(donation => {
responseDonations.push(donation.toResponse());
});
return responseDonations;
}
@Get('/:id')
@Authorized("DONATION:GET")
@ResponseSchema(ResponseDonation)
@ResponseSchema(ResponseDistanceDonation)
@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
@OnUndefined(DonationNotFoundError)
@OpenAPI({ description: 'Lists all information about the donation whose id got provided. This includes the donation\'s runner\'s distance ran (if distance donation).' })
async getOne(@Param('id') id: number) {
let donation = await this.donationRepository.findOne({ id: id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })
if (!donation) { throw new DonationNotFoundError(); }
return donation.toResponse();
}
@Post('/fixed')
@Authorized("DONATION:CREATE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a fixed donation (not distance donation - use /donations/distance instead). <br> Please rmemember to provide the donation\'s donors\'s id and amount.' })
async postFixed(@Body({ validate: true }) createDonation: CreateFixedDonation) {
let donation = await createDonation.toEntity();
donation = await this.fixedDonationRepository.save(donation);
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
}
@Post('/distance')
@Authorized("DONATION:CREATE")
@ResponseSchema(ResponseDistanceDonation)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a distance donation (not fixed donation - use /donations/fixed instead). <br> Please rmemember to provide the donation\'s donors\'s and runner\s ids and amount per distance (kilometer).' })
async postDistance(@Body({ validate: true }) createDonation: CreateDistanceDonation) {
let donation = await createDonation.toEntity();
donation = await this.distanceDonationRepository.save(donation);
return (await this.distanceDonationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
}
@Put('/fixed/:id')
@Authorized("DONATION:UPDATE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the fixed donation (not distance donation - use /donations/distance instead) whose id you provided. <br> Please remember that ids can't be changed and amounts must be positive." })
async putFixed(@Param('id') id: number, @Body({ validate: true }) donation: UpdateFixedDonation) {
let oldDonation = await this.fixedDonationRepository.findOne({ id: id });
if (!oldDonation) {
throw new DonationNotFoundError();
}
if (oldDonation.id != donation.id) {
throw new DonationIdsNotMatchingError();
}
await this.fixedDonationRepository.save(await donation.update(oldDonation));
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
}
@Put('/distance/:id')
@Authorized("DONATION:UPDATE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the distance donation (not fixed donation - use /donations/fixed instead) whose id you provided. <br> Please remember that ids can't be changed and amountPerDistance must be positive." })
async putDistance(@Param('id') id: number, @Body({ validate: true }) donation: UpdateDistanceDonation) {
let oldDonation = await this.distanceDonationRepository.findOne({ id: id });
if (!oldDonation) {
throw new DonationNotFoundError();
}
if (oldDonation.id != donation.id) {
throw new DonationIdsNotMatchingError();
}
await this.distanceDonationRepository.save(await donation.update(oldDonation));
return (await this.distanceDonationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
}
@Delete('/:id')
@Authorized("DONATION:DELETE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(ResponseDistanceDonation)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the donation whose id you provided. <br> If no donation with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let donation = await this.donationRepository.findOne({ id: id });
if (!donation) { return null; }
const responseScan = await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] });
await this.donationRepository.delete(donation);
return responseScan.toResponse();
}
}

View File

@ -1,13 +1,12 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { DonorHasDonationsError, DonorIdsNotMatchingError, DonorNotFoundError } from '../errors/DonorErrors'; import { DonorIdsNotMatchingError, DonorNotFoundError } from '../errors/DonorErrors';
import { CreateDonor } from '../models/actions/create/CreateDonor'; import { CreateDonor } from '../models/actions/create/CreateDonor';
import { UpdateDonor } from '../models/actions/update/UpdateDonor'; import { UpdateDonor } from '../models/actions/update/UpdateDonor';
import { Donor } from '../models/entities/Donor'; import { Donor } from '../models/entities/Donor';
import { ResponseDonor } from '../models/responses/ResponseDonor'; import { ResponseDonor } from '../models/responses/ResponseDonor';
import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { DonationController } from './DonationController';
@JsonController('/donors') @JsonController('/donors')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
@ -24,17 +23,10 @@ export class DonorController {
@Get() @Get()
@Authorized("DONOR:GET") @Authorized("DONOR:GET")
@ResponseSchema(ResponseDonor, { isArray: true }) @ResponseSchema(ResponseDonor, { isArray: true })
@OpenAPI({ description: 'Lists all donor. <br> This includes the donor\'s current donation amount.' }) @OpenAPI({ description: 'Lists all runners from all teams/orgs. <br> This includes the runner\'s group and distance ran.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { async getAll() {
let responseDonors: ResponseDonor[] = new Array<ResponseDonor>(); let responseDonors: ResponseDonor[] = new Array<ResponseDonor>();
let donors: Array<Donor>; const donors = await this.donorRepository.find();
if (page != undefined) {
donors = await this.donorRepository.find({ relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'], skip: page * page_size, take: page_size });
} else {
donors = await this.donorRepository.find({ relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] });
}
donors.forEach(donor => { donors.forEach(donor => {
responseDonors.push(new ResponseDonor(donor)); responseDonors.push(new ResponseDonor(donor));
}); });
@ -46,9 +38,9 @@ export class DonorController {
@ResponseSchema(ResponseDonor) @ResponseSchema(ResponseDonor)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 }) @ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@OnUndefined(DonorNotFoundError) @OnUndefined(DonorNotFoundError)
@OpenAPI({ description: 'Lists all information about the donor whose id got provided. <br> This includes the donor\'s current donation amount.' }) @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' })
async getOne(@Param('id') id: number) { async getOne(@Param('id') id: number) {
let donor = await this.donorRepository.findOne({ id: id }, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] }) let donor = await this.donorRepository.findOne({ id: id })
if (!donor) { throw new DonorNotFoundError(); } if (!donor) { throw new DonorNotFoundError(); }
return new ResponseDonor(donor); return new ResponseDonor(donor);
} }
@ -56,7 +48,7 @@ export class DonorController {
@Post() @Post()
@Authorized("DONOR:CREATE") @Authorized("DONOR:CREATE")
@ResponseSchema(ResponseDonor) @ResponseSchema(ResponseDonor)
@OpenAPI({ description: 'Create a new donor.' }) @OpenAPI({ description: 'Create a new runner. <br> Please remeber to provide the runner\'s group\'s id.' })
async post(@Body({ validate: true }) createRunner: CreateDonor) { async post(@Body({ validate: true }) createRunner: CreateDonor) {
let donor; let donor;
try { try {
@ -66,7 +58,7 @@ export class DonorController {
} }
donor = await this.donorRepository.save(donor) donor = await this.donorRepository.save(donor)
return new ResponseDonor(await this.donorRepository.findOne(donor, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] })); return new ResponseDonor(await this.donorRepository.findOne(donor));
} }
@Put('/:id') @Put('/:id')
@ -74,7 +66,7 @@ export class DonorController {
@ResponseSchema(ResponseDonor) @ResponseSchema(ResponseDonor)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 }) @ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@ResponseSchema(DonorIdsNotMatchingError, { statusCode: 406 }) @ResponseSchema(DonorIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the donor whose id you provided. <br> Please remember that ids can't be changed." }) @OpenAPI({ description: "Update the runner whose id you provided. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) donor: UpdateDonor) { async put(@Param('id') id: number, @Body({ validate: true }) donor: UpdateDonor) {
let oldDonor = await this.donorRepository.findOne({ id: id }); let oldDonor = await this.donorRepository.findOne({ id: id });
@ -87,7 +79,7 @@ export class DonorController {
} }
await this.donorRepository.save(await donor.update(oldDonor)); await this.donorRepository.save(await donor.update(oldDonor));
return new ResponseDonor(await this.donorRepository.findOne({ id: id }, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] })); return new ResponseDonor(await this.donorRepository.findOne({ id: id }));
} }
@Delete('/:id') @Delete('/:id')
@ -95,24 +87,17 @@ export class DonorController {
@ResponseSchema(ResponseDonor) @ResponseSchema(ResponseDonor)
@ResponseSchema(ResponseEmpty, { statusCode: 204 }) @ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204) @OnUndefined(204)
@OpenAPI({ description: 'Delete the donor whose id you provided. <br> If no donor with this id exists it will just return 204(no content). <br> If the donor still has donations associated this will fail, please provide the query param ?force=true to delete the donor with all associated donations.' }) @OpenAPI({ description: 'Delete the runner whose id you provided. <br> If no runner with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let donor = await this.donorRepository.findOne({ id: id }); let donor = await this.donorRepository.findOne({ id: id });
if (!donor) { return null; } if (!donor) { return null; }
const responseDonor = await this.donorRepository.findOne(donor, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] }); const responseDonor = await this.donorRepository.findOne(donor);
if (!donor) { if (!donor) {
throw new DonorNotFoundError(); throw new DonorNotFoundError();
} }
const donorDonations = (await this.donorRepository.findOne({ id: donor.id }, { relations: ["donations"] })).donations; //TODO: DELETE DONATIONS AND WARN FOR FORCE (https://git.odit.services/lfk/backend/issues/66)
if (donorDonations.length > 0 && !force) {
throw new DonorHasDonationsError();
}
const donationController = new DonationController();
for (let donation of donorDonations) {
await donationController.remove(donation.id, force);
}
await this.donorRepository.delete(donor); await this.donorRepository.delete(donor);
return new ResponseDonor(responseDonor); return new ResponseDonor(responseDonor);

View File

@ -1,114 +0,0 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnection, getConnectionManager } from 'typeorm';
import { GroupContactIdsNotMatchingError, GroupContactNotFoundError } from '../errors/GroupContactErrors';
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
import { CreateGroupContact } from '../models/actions/create/CreateGroupContact';
import { UpdateGroupContact } from '../models/actions/update/UpdateGroupContact';
import { GroupContact } from '../models/entities/GroupContact';
import { RunnerGroup } from '../models/entities/RunnerGroup';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseGroupContact } from '../models/responses/ResponseGroupContact';
@JsonController('/contacts')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class GroupContactController {
private contactRepository: Repository<GroupContact>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.contactRepository = getConnectionManager().get().getRepository(GroupContact);
}
@Get()
@Authorized("CONTACT:GET")
@ResponseSchema(ResponseGroupContact, { isArray: true })
@OpenAPI({ description: 'Lists all contacts. <br> This includes the contact\'s associated groups.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseContacts: ResponseGroupContact[] = new Array<ResponseGroupContact>();
let contacts: Array<GroupContact>;
if (page != undefined) {
contacts = await this.contactRepository.find({ relations: ['groups', 'groups.parentGroup'], skip: page * page_size, take: page_size });
} else {
contacts = await this.contactRepository.find({ relations: ['groups', 'groups.parentGroup'] });
}
contacts.forEach(contact => {
responseContacts.push(contact.toResponse());
});
return responseContacts;
}
@Get('/:id')
@Authorized("CONTACT:GET")
@ResponseSchema(ResponseGroupContact)
@ResponseSchema(GroupContactNotFoundError, { statusCode: 404 })
@OnUndefined(GroupContactNotFoundError)
@OpenAPI({ description: 'Lists all information about the contact whose id got provided. <br> This includes the contact\'s associated groups.' })
async getOne(@Param('id') id: number) {
let contact = await this.contactRepository.findOne({ id: id }, { relations: ['groups', 'groups.parentGroup'] })
if (!contact) { throw new GroupContactNotFoundError(); }
return contact.toResponse();
}
@Post()
@Authorized("CONTACT:CREATE")
@ResponseSchema(ResponseGroupContact)
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new contact.' })
async post(@Body({ validate: true }) createContact: CreateGroupContact) {
let contact;
try {
contact = await createContact.toEntity();
} catch (error) {
throw error;
}
contact = await this.contactRepository.save(contact)
return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups', 'groups.parentGroup'] })).toResponse();
}
@Put('/:id')
@Authorized("CONTACT:UPDATE")
@ResponseSchema(ResponseGroupContact)
@ResponseSchema(GroupContactNotFoundError, { statusCode: 404 })
@ResponseSchema(GroupContactIdsNotMatchingError, { statusCode: 406 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@OpenAPI({ description: "Update the contact whose id you provided. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) contact: UpdateGroupContact) {
let oldContact = await this.contactRepository.findOne({ id: id });
if (!oldContact) {
throw new GroupContactNotFoundError();
}
if (oldContact.id != contact.id) {
throw new GroupContactIdsNotMatchingError();
}
await this.contactRepository.save(await contact.update(oldContact));
return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups', 'groups.parentGroup'] })).toResponse();
}
@Delete('/:id')
@Authorized("CONTACT:DELETE")
@ResponseSchema(ResponseGroupContact)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the contact whose id you provided. <br> If no contact with this id exists it will just return 204(no content). <br> This won\'t delete any groups associated with the contact.' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let contact = await this.contactRepository.findOne({ id: id });
if (!contact) { return null; }
const responseContact = await this.contactRepository.findOne(contact, { relations: ['groups', 'groups.parentGroup'] });
for (let group of responseContact.groups) {
group.contact = null;
await getConnection().getRepository(RunnerGroup).save(group);
}
await this.contactRepository.delete(contact);
return responseContact.toResponse();
}
}

View File

@ -36,7 +36,7 @@ export class ImportController {
return responseRunners; return responseRunners;
} }
@Post('/organizations/:id/import') @Post('/organisations/:id/import')
@ContentType("application/json") @ContentType("application/json")
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 }) @ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 }) @ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ -78,7 +78,7 @@ export class ImportController {
return await this.postJSON(importRunners, groupID); return await this.postJSON(importRunners, groupID);
} }
@Post('/organizations/:id/import/csv') @Post('/organisations/:id/import/csv')
@ContentType("application/json") @ContentType("application/json")
@UseBefore(RawBodyMiddleware) @UseBefore(RawBodyMiddleware)
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 }) @ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })

View File

@ -1,90 +0,0 @@
import { Body, CurrentUser, Delete, Get, JsonController, OnUndefined, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors';
import { UpdateUser } from '../models/actions/update/UpdateUser';
import { User } from '../models/entities/User';
import { ResponseUser } from '../models/responses/ResponseUser';
import { ResponseUserPermissions } from '../models/responses/ResponseUserPermissions';
import { PermissionController } from './PermissionController';
@JsonController('/users/me')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class MeController {
private userRepository: Repository<User>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.userRepository = getConnectionManager().get().getRepository(User);
}
@Get('/')
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Lists all information about yourself.' })
async get(@CurrentUser() currentUser: User) {
let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
if (!user) { throw new UserNotFoundError(); }
return new ResponseUser(user);
}
@Get('/permissions')
@ResponseSchema(ResponseUserPermissions)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Lists all permissions granted to the you sorted into directly granted and inherited as permission response objects.' })
async getPermissions(@CurrentUser() currentUser: User) {
let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
if (!user) { throw new UserNotFoundError(); }
return new ResponseUserPermissions(user);
}
@Put('/')
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
@OpenAPI({ description: "Update the yourself. <br> You can't edit your own permissions or group memberships here - Please use the /api/users/:id enpoint instead. <br> Please remember that ids can't be changed." })
async put(@CurrentUser() currentUser: User, @Body({ validate: true }) updateUser: UpdateUser) {
let oldUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['groups'] });
updateUser.groups = oldUser.groups.map(g => g.id);
if (!oldUser) {
throw new UserNotFoundError();
}
if (oldUser.id != updateUser.id) {
throw new UserIdsNotMatchingError();
}
await this.userRepository.save(await updateUser.update(oldUser));
return new ResponseUser(await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] }));
}
@Delete('/')
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 })
@OpenAPI({ description: 'Delete yourself. <br> You have to confirm your decision by providing the ?force=true query param. <br> If there are any permissions directly granted to you they will get deleted as well.' })
async remove(@CurrentUser() currentUser: User, @QueryParam("force") force: boolean) {
if (!force) { throw new UserDeletionNotConfirmedError; }
if (!currentUser) { return UserNotFoundError; }
const responseUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] });;
const permissionControler = new PermissionController();
for (let permission of responseUser.permissions) {
await permissionControler.remove(permission.id, true);
}
await this.userRepository.delete(currentUser);
return new ResponseUser(responseUser);
}
}

View File

@ -1,6 +1,6 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { PermissionIdsNotMatchingError, PermissionNeedsPrincipalError, PermissionNotFoundError } from '../errors/PermissionErrors'; import { PermissionIdsNotMatchingError, PermissionNeedsPrincipalError, PermissionNotFoundError } from '../errors/PermissionErrors';
import { PrincipalNotFoundError } from '../errors/PrincipalErrors'; import { PrincipalNotFoundError } from '../errors/PrincipalErrors';
import { CreatePermission } from '../models/actions/create/CreatePermission'; import { CreatePermission } from '../models/actions/create/CreatePermission';
@ -27,16 +27,9 @@ export class PermissionController {
@Authorized("PERMISSION:GET") @Authorized("PERMISSION:GET")
@ResponseSchema(ResponsePermission, { isArray: true }) @ResponseSchema(ResponsePermission, { isArray: true })
@OpenAPI({ description: 'Lists all permissions for all users and groups.' }) @OpenAPI({ description: 'Lists all permissions for all users and groups.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { async getAll() {
let responsePermissions: ResponsePermission[] = new Array<ResponsePermission>(); let responsePermissions: ResponsePermission[] = new Array<ResponsePermission>();
let permissions: Array<Permission>; const permissions = await this.permissionRepository.find({ relations: ['principal'] });
if (page != undefined) {
permissions = await this.permissionRepository.find({ relations: ['principal'], skip: page * page_size, take: page_size });
} else {
permissions = await this.permissionRepository.find({ relations: ['principal'] });
}
permissions.forEach(permission => { permissions.forEach(permission => {
responsePermissions.push(new ResponsePermission(permission)); responsePermissions.push(new ResponsePermission(permission));
}); });
@ -97,7 +90,7 @@ export class PermissionController {
if (oldPermission.id != permission.id) { if (oldPermission.id != permission.id) {
throw new PermissionIdsNotMatchingError(); throw new PermissionIdsNotMatchingError();
} }
let existingPermission = await this.permissionRepository.findOne({ target: permission.target, action: permission.action, principal: await permission.getPrincipal() }, { relations: ['principal'] }); let existingPermission = await this.permissionRepository.findOne({ target: permission.target, action: permission.action, principal: permission.principal }, { relations: ['principal'] });
if (existingPermission) { if (existingPermission) {
await this.remove(permission.id, true); await this.remove(permission.id, true);
return new ResponsePermission(existingPermission); return new ResponsePermission(existingPermission);

View File

@ -1,138 +1,106 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { RunnerCardHasScansError, RunnerCardIdsNotMatchingError, RunnerCardNotFoundError } from '../errors/RunnerCardErrors'; import { RunnerCardHasScansError, RunnerCardIdsNotMatchingError, RunnerCardNotFoundError } from '../errors/RunnerCardErrors';
import { RunnerNotFoundError } from '../errors/RunnerErrors'; import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { CreateRunnerCard } from '../models/actions/create/CreateRunnerCard'; import { CreateRunnerCard } from '../models/actions/create/CreateRunnerCard';
import { UpdateRunnerCard } from '../models/actions/update/UpdateRunnerCard'; import { UpdateRunnerCard } from '../models/actions/update/UpdateRunnerCard';
import { RunnerCard } from '../models/entities/RunnerCard'; import { RunnerCard } from '../models/entities/RunnerCard';
import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunnerCard } from '../models/responses/ResponseRunnerCard'; import { ResponseRunnerCard } from '../models/responses/ResponseRunnerCard';
import { ScanController } from './ScanController'; import { ScanController } from './ScanController';
@JsonController('/cards') @JsonController('/cards')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class RunnerCardController { export class RunnerCardController {
private cardRepository: Repository<RunnerCard>; private cardRepository: Repository<RunnerCard>;
/** /**
* Gets the repository of this controller's model/entity. * Gets the repository of this controller's model/entity.
*/ */
constructor() { constructor() {
this.cardRepository = getConnectionManager().get().getRepository(RunnerCard); this.cardRepository = getConnectionManager().get().getRepository(RunnerCard);
} }
@Get() @Get()
@Authorized("CARD:GET") @Authorized("CARD:GET")
@ResponseSchema(ResponseRunnerCard, { isArray: true }) @ResponseSchema(ResponseRunnerCard, { isArray: true })
@OpenAPI({ description: 'Lists all card.' }) @OpenAPI({ description: 'Lists all card.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { async getAll() {
let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>(); let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
let cards: Array<RunnerCard>; const cards = await this.cardRepository.find({ relations: ['runner'] });
cards.forEach(card => {
if (page != undefined) { responseCards.push(new ResponseRunnerCard(card));
cards = await this.cardRepository.find({ relations: ['runner', 'runner.group', 'runner.group.parentGroup'], skip: page * page_size, take: page_size }); });
} else { return responseCards;
cards = await this.cardRepository.find({ relations: ['runner', 'runner.group', 'runner.group.parentGroup'] }); }
}
@Get('/:id')
cards.forEach(card => { @Authorized("CARD:GET")
responseCards.push(new ResponseRunnerCard(card)); @ResponseSchema(ResponseRunnerCard)
}); @ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
return responseCards; @OnUndefined(RunnerCardNotFoundError)
} @OpenAPI({ description: "Lists all information about the card whose id got provided." })
async getOne(@Param('id') id: number) {
@Get('/:id') let card = await this.cardRepository.findOne({ id: id }, { relations: ['runner'] });
@Authorized("CARD:GET") if (!card) { throw new RunnerCardNotFoundError(); }
@ResponseSchema(ResponseRunnerCard) return card.toResponse();
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 }) }
@OnUndefined(RunnerCardNotFoundError)
@OpenAPI({ description: "Lists all information about the card whose id got provided." }) @Post()
async getOne(@Param('id') id: number) { @Authorized("CARD:CREATE")
let card = await this.cardRepository.findOne({ id: id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] }); @ResponseSchema(ResponseRunnerCard)
if (!card) { throw new RunnerCardNotFoundError(); } @ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
return card.toResponse(); @OpenAPI({ description: "Create a new card. <br> You can provide a associated runner by id but you don't have to." })
} async post(@Body({ validate: true }) createCard: CreateRunnerCard) {
let card = await createCard.toEntity();
@Post('/bulk') card = await this.cardRepository.save(card);
@Authorized("CARD:CREATE") return (await this.cardRepository.findOne({ id: card.id }, { relations: ['runner'] })).toResponse();
@ResponseSchema(ResponseEmpty, { statusCode: 200 }) }
@OpenAPI({ description: "Create blank cards in bulk. <br> Just provide the count as a query param and wait for the 200 response. <br> You can provide the 'returnCards' query param if you want to receive the RESPONSERUNNERCARD objects in the response." })
async postBlancoBulk(@QueryParam("count") count: number, @QueryParam("returnCards") returnCards: boolean = false) { @Put('/:id')
let createPromises = new Array<any>(); @Authorized("CARD:UPDATE")
for (let index = 0; index < count; index++) { @ResponseSchema(ResponseRunnerCard)
createPromises.push(this.cardRepository.save({ runner: null, enabled: true })) @ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
} @ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerCardIdsNotMatchingError, { statusCode: 406 })
const cards = await Promise.all(createPromises); @OpenAPI({ description: "Update the card whose id you provided. <br> Scans created via this card will still be associated with the old runner. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) card: UpdateRunnerCard) {
if (returnCards) { let oldCard = await this.cardRepository.findOne({ id: id });
let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
for await (let card of cards) { if (!oldCard) {
let dbCard = await this.cardRepository.findOne({ id: card.id }); throw new RunnerCardNotFoundError();
responseCards.push(new ResponseRunnerCard(dbCard)); }
}
return responseCards; if (oldCard.id != card.id) {
} throw new RunnerCardIdsNotMatchingError();
let response = new ResponseEmpty(); }
response.response = `Created ${count} new blanco cards.`
return response; await this.cardRepository.save(await card.update(oldCard));
} return (await this.cardRepository.findOne({ id: id }, { relations: ['runner'] })).toResponse();
}
@Post()
@Authorized("CARD:CREATE") @Delete('/:id')
@ResponseSchema(ResponseRunnerCard) @Authorized("CARD:DELETE")
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) @ResponseSchema(ResponseRunnerCard)
@OpenAPI({ description: "Create a new card. <br> You can provide a associated runner by id but you don't have to." }) @ResponseSchema(ResponseEmpty, { statusCode: 204 })
async post(@Body({ validate: true }) createCard: CreateRunnerCard) { @ResponseSchema(RunnerCardHasScansError, { statusCode: 406 })
let card = await createCard.toEntity(); @OnUndefined(204)
card = await this.cardRepository.save(card); @OpenAPI({ description: "Delete the card whose id you provided. <br> If no card with this id exists it will just return 204(no content). <br> If the card still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with by this card - please disable it instead or just remove the runner association)." })
return (await this.cardRepository.findOne({ id: card.id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] })).toResponse(); async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
} let card = await this.cardRepository.findOne({ id: id });
if (!card) { return null; }
@Put('/:id')
@Authorized("CARD:UPDATE") const cardScans = (await this.cardRepository.findOne({ id: id }, { relations: ["scans"] })).scans;
@ResponseSchema(ResponseRunnerCard) if (cardScans.length != 0 && !force) {
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 }) throw new RunnerCardHasScansError();
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) }
@ResponseSchema(RunnerCardIdsNotMatchingError, { statusCode: 406 }) const scanController = new ScanController;
@OpenAPI({ description: "Update the card whose id you provided. <br> Scans created via this card will still be associated with the old runner. <br> Please remember that ids can't be changed." }) for (let scan of cardScans) {
async put(@Param('id') id: number, @Body({ validate: true }) card: UpdateRunnerCard) { await scanController.remove(scan.id, force);
let oldCard = await this.cardRepository.findOne({ id: id }); }
if (!oldCard) { await this.cardRepository.delete(card);
throw new RunnerCardNotFoundError(); return card.toResponse();
} }
if (oldCard.id != card.id) {
throw new RunnerCardIdsNotMatchingError();
}
await this.cardRepository.save(await card.update(oldCard));
return (await this.cardRepository.findOne({ id: id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] })).toResponse();
}
@Delete('/:id')
@Authorized("CARD:DELETE")
@ResponseSchema(ResponseRunnerCard)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(RunnerCardHasScansError, { statusCode: 406 })
@OnUndefined(204)
@OpenAPI({ description: "Delete the card whose id you provided. <br> If no card with this id exists it will just return 204(no content). <br> If the card still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with by this card - please disable it instead or just remove the runner association)." })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let card = await this.cardRepository.findOne({ id: id });
if (!card) { return null; }
const cardScans = (await this.cardRepository.findOne({ id: id }, { relations: ["scans"] })).scans;
if (cardScans.length != 0 && !force) {
throw new RunnerCardHasScansError();
}
const scanController = new ScanController;
for (let scan of cardScans) {
await scanController.remove(scan.id, force);
}
await this.cardRepository.delete(card);
return card.toResponse();
}
} }

View File

@ -1,16 +1,13 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { RunnerGroupNeededError, RunnerHasDistanceDonationsError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors'; import { RunnerGroupNeededError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors'; import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
import { CreateRunner } from '../models/actions/create/CreateRunner'; import { CreateRunner } from '../models/actions/create/CreateRunner';
import { UpdateRunner } from '../models/actions/update/UpdateRunner'; import { UpdateRunner } from '../models/actions/update/UpdateRunner';
import { Runner } from '../models/entities/Runner'; import { Runner } from '../models/entities/Runner';
import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunner } from '../models/responses/ResponseRunner'; import { ResponseRunner } from '../models/responses/ResponseRunner';
import { ResponseScan } from '../models/responses/ResponseScan';
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
import { DonationController } from './DonationController';
import { RunnerCardController } from './RunnerCardController'; import { RunnerCardController } from './RunnerCardController';
import { ScanController } from './ScanController'; import { ScanController } from './ScanController';
@ -30,16 +27,9 @@ export class RunnerController {
@Authorized("RUNNER:GET") @Authorized("RUNNER:GET")
@ResponseSchema(ResponseRunner, { isArray: true }) @ResponseSchema(ResponseRunner, { isArray: true })
@OpenAPI({ description: 'Lists all runners from all teams/orgs. <br> This includes the runner\'s group and distance ran.' }) @OpenAPI({ description: 'Lists all runners from all teams/orgs. <br> This includes the runner\'s group and distance ran.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { async getAll() {
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>(); let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
let runners: Array<Runner>; const runners = await this.runnerRepository.find({ relations: ['scans', 'group', 'scans.track', 'cards'] });
if (page != undefined) {
runners = await this.runnerRepository.find({ relations: ['scans', 'group', 'group.parentGroup', 'scans.track'], skip: page * page_size, take: page_size });
} else {
runners = await this.runnerRepository.find({ relations: ['scans', 'group', 'group.parentGroup', 'scans.track'] });
}
runners.forEach(runner => { runners.forEach(runner => {
responseRunners.push(new ResponseRunner(runner)); responseRunners.push(new ResponseRunner(runner));
}); });
@ -53,36 +43,11 @@ export class RunnerController {
@OnUndefined(RunnerNotFoundError) @OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' })
async getOne(@Param('id') id: number) { async getOne(@Param('id') id: number) {
let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }) let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'scans.track', 'cards'] })
if (!runner) { throw new RunnerNotFoundError(); } if (!runner) { throw new RunnerNotFoundError(); }
return new ResponseRunner(runner); return new ResponseRunner(runner);
} }
@Get('/:id/scans')
@Authorized(["RUNNER:GET", "SCAN:GET"])
@ResponseSchema(ResponseScan, { isArray: true })
@ResponseSchema(ResponseTrackScan, { isArray: true })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Lists all scans of the runner whose id got provided. <br> If you only want the valid scans just add the ?onlyValid=true query param.' })
async getScans(@Param('id') id: number, onlyValid?: boolean) {
let responseScans: ResponseScan[] = new Array<ResponseScan>();
let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'scans.track', 'scans.station', 'scans.runner'] })
if (!runner) { throw new RunnerNotFoundError(); }
if (!onlyValid) {
for (let scan of runner.scans) {
responseScans.push(scan.toResponse());
}
}
else {
for (let scan of runner.validScans) {
responseScans.push(scan.toResponse());
}
}
return responseScans;
}
@Post() @Post()
@Authorized("RUNNER:CREATE") @Authorized("RUNNER:CREATE")
@ResponseSchema(ResponseRunner) @ResponseSchema(ResponseRunner)
@ -98,7 +63,7 @@ export class RunnerController {
} }
runner = await this.runnerRepository.save(runner) runner = await this.runnerRepository.save(runner)
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] })); return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'scans.track', 'cards'] }));
} }
@Put('/:id') @Put('/:id')
@ -119,38 +84,28 @@ export class RunnerController {
} }
await this.runnerRepository.save(await runner.update(oldRunner)); await this.runnerRepository.save(await runner.update(oldRunner));
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] })); return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'scans.track', 'cards'] }));
} }
@Delete('/:id') @Delete('/:id')
@Authorized("RUNNER:DELETE") @Authorized("RUNNER:DELETE")
@ResponseSchema(ResponseRunner) @ResponseSchema(ResponseRunner)
@ResponseSchema(ResponseEmpty, { statusCode: 204 }) @ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(RunnerHasDistanceDonationsError, { statusCode: 406 })
@OnUndefined(204) @OnUndefined(204)
@OpenAPI({ description: 'Delete the runner whose id you provided. <br> This will also delete all scans and cards associated with the runner. <br> If no runner with this id exists it will just return 204(no content).' }) @OpenAPI({ description: 'Delete the runner whose id you provided. <br> This will also delete all scans and cards associated with the runner. <br> If no runner with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let runner = await this.runnerRepository.findOne({ id: id }); let runner = await this.runnerRepository.findOne({ id: id });
if (!runner) { return null; } if (!runner) { return null; }
const responseRunner = await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }); const responseRunner = await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'scans.track', 'cards'] });
if (!runner) { if (!runner) {
throw new RunnerNotFoundError(); throw new RunnerNotFoundError();
} }
const runnerDonations = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["distanceDonations"] })).distanceDonations;
if (runnerDonations.length > 0 && !force) {
throw new RunnerHasDistanceDonationsError();
}
const donationController = new DonationController();
for (let donation of runnerDonations) {
await donationController.remove(donation.id, force);
}
const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards; const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards;
const cardController = new RunnerCardController; const cardController = new RunnerCardController;
for (let card of runnerCards) { for (let scan of runnerCards) {
await cardController.remove(card.id, force); await cardController.remove(scan.id, force);
} }
const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans; const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans;

View File

@ -0,0 +1,127 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { RunnerOrganisationHasRunnersError, RunnerOrganisationHasTeamsError, RunnerOrganisationIdsNotMatchingError, RunnerOrganisationNotFoundError } from '../errors/RunnerOrganisationErrors';
import { CreateRunnerOrganisation } from '../models/actions/create/CreateRunnerOrganisation';
import { UpdateRunnerOrganisation } from '../models/actions/update/UpdateRunnerOrganisation';
import { RunnerOrganisation } from '../models/entities/RunnerOrganisation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunnerOrganisation } from '../models/responses/ResponseRunnerOrganisation';
import { RunnerController } from './RunnerController';
import { RunnerTeamController } from './RunnerTeamController';
@JsonController('/organisations')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class RunnerOrganisationController {
private runnerOrganisationRepository: Repository<RunnerOrganisation>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerOrganisationRepository = getConnectionManager().get().getRepository(RunnerOrganisation);
}
@Get()
@Authorized("ORGANISATION:GET")
@ResponseSchema(ResponseRunnerOrganisation, { isArray: true })
@OpenAPI({ description: 'Lists all organisations. <br> This includes their address, contact and teams (if existing/associated).' })
async getAll() {
let responseTeams: ResponseRunnerOrganisation[] = new Array<ResponseRunnerOrganisation>();
const runners = await this.runnerOrganisationRepository.find({ relations: ['address', 'contact', 'teams'] });
runners.forEach(runner => {
responseTeams.push(new ResponseRunnerOrganisation(runner));
});
return responseTeams;
}
@Get('/:id')
@Authorized("ORGANISATION:GET")
@ResponseSchema(ResponseRunnerOrganisation)
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerOrganisationNotFoundError)
@OpenAPI({ description: 'Lists all information about the organisation whose id got provided.' })
async getOne(@Param('id') id: number) {
let runnerOrg = await this.runnerOrganisationRepository.findOne({ id: id }, { relations: ['address', 'contact', 'teams'] });
if (!runnerOrg) { throw new RunnerOrganisationNotFoundError(); }
return new ResponseRunnerOrganisation(runnerOrg);
}
@Post()
@Authorized("ORGANISATION:CREATE")
@ResponseSchema(ResponseRunnerOrganisation)
@OpenAPI({ description: 'Create a new organsisation.' })
async post(@Body({ validate: true }) createRunnerOrganisation: CreateRunnerOrganisation) {
let runnerOrganisation;
try {
runnerOrganisation = await createRunnerOrganisation.toEntity();
} catch (error) {
throw error;
}
runnerOrganisation = await this.runnerOrganisationRepository.save(runnerOrganisation);
return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(runnerOrganisation, { relations: ['address', 'contact', 'teams'] }));
}
@Put('/:id')
@Authorized("ORGANISATION:UPDATE")
@ResponseSchema(ResponseRunnerOrganisation)
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerOrganisationIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the organisation whose id you provided. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) updateOrganisation: UpdateRunnerOrganisation) {
let oldRunnerOrganisation = await this.runnerOrganisationRepository.findOne({ id: id });
if (!oldRunnerOrganisation) {
throw new RunnerOrganisationNotFoundError();
}
if (oldRunnerOrganisation.id != updateOrganisation.id) {
throw new RunnerOrganisationIdsNotMatchingError();
}
await this.runnerOrganisationRepository.save(await updateOrganisation.update(oldRunnerOrganisation));
return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(id, { relations: ['address', 'contact', 'teams'] }));
}
@Delete('/:id')
@Authorized("ORGANISATION:DELETE")
@ResponseSchema(ResponseRunnerOrganisation)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(RunnerOrganisationHasTeamsError, { statusCode: 406 })
@ResponseSchema(RunnerOrganisationHasRunnersError, { statusCode: 406 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the organsisation whose id you provided. <br> If the organisation still has runners and/or teams associated this will fail. <br> To delete the organisation with all associated runners and teams set the force QueryParam to true (cascading deletion might take a while). <br> If no organisation with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let organisation = await this.runnerOrganisationRepository.findOne({ id: id });
if (!organisation) { return null; }
let runnerOrganisation = await this.runnerOrganisationRepository.findOne(organisation, { relations: ['address', 'contact', 'runners', 'teams'] });
if (!force) {
if (runnerOrganisation.teams.length != 0) {
throw new RunnerOrganisationHasTeamsError();
}
}
const teamController = new RunnerTeamController()
for (let team of runnerOrganisation.teams) {
await teamController.remove(team.id, true);
}
if (!force) {
if (runnerOrganisation.runners.length != 0) {
throw new RunnerOrganisationHasRunnersError();
}
}
const runnerController = new RunnerController()
for (let runner of runnerOrganisation.runners) {
await runnerController.remove(runner.id, true);
}
const responseOrganisation = new ResponseRunnerOrganisation(runnerOrganisation);
await this.runnerOrganisationRepository.delete(organisation);
return responseOrganisation;
}
}

View File

@ -1,156 +0,0 @@
import { Authorized, BadRequestError, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { RunnerOrganizationHasRunnersError, RunnerOrganizationHasTeamsError, RunnerOrganizationIdsNotMatchingError, RunnerOrganizationNotFoundError } from '../errors/RunnerOrganizationErrors';
import { CreateRunnerOrganization } from '../models/actions/create/CreateRunnerOrganization';
import { UpdateRunnerOrganization } from '../models/actions/update/UpdateRunnerOrganization';
import { Runner } from '../models/entities/Runner';
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunner } from '../models/responses/ResponseRunner';
import { ResponseRunnerOrganization } from '../models/responses/ResponseRunnerOrganization';
import { RunnerController } from './RunnerController';
import { RunnerTeamController } from './RunnerTeamController';
@JsonController('/organizations')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class RunnerOrganizationController {
private runnerOrganizationRepository: Repository<RunnerOrganization>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerOrganizationRepository = getConnectionManager().get().getRepository(RunnerOrganization);
}
@Get()
@Authorized("ORGANIZATION:GET")
@ResponseSchema(ResponseRunnerOrganization, { isArray: true })
@OpenAPI({ description: 'Lists all organizations. <br> This includes their address, contact and teams (if existing/associated).' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseOrgs: ResponseRunnerOrganization[] = new Array<ResponseRunnerOrganization>();
let orgs: Array<RunnerOrganization>;
if (page != undefined) {
orgs = await this.runnerOrganizationRepository.find({ relations: ['contact', 'teams'], skip: page * page_size, take: page_size });
} else {
orgs = await this.runnerOrganizationRepository.find({ relations: ['contact', 'teams'] });
}
orgs.forEach(org => {
responseOrgs.push(new ResponseRunnerOrganization(org));
});
return responseOrgs;
}
@Get('/:id')
@Authorized("ORGANIZATION:GET")
@ResponseSchema(ResponseRunnerOrganization)
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerOrganizationNotFoundError)
@OpenAPI({ description: 'Lists all information about the organization whose id got provided.' })
async getOne(@Param('id') id: number) {
let runnerOrg = await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['contact', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.scans.track', 'runners', 'runners.scans', 'runners.scans.track'] });
if (!runnerOrg) { throw new RunnerOrganizationNotFoundError(); }
return new ResponseRunnerOrganization(runnerOrg);
}
@Get('/:id/runners')
@Authorized(["RUNNER:GET", "SCAN:GET"])
@ResponseSchema(ResponseRunner, { isArray: true })
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Lists all runners from this org and it\'s teams (if you don\'t provide the ?onlyDirect=true param). <br> This includes the runner\'s group and distance ran.' })
async getRunners(@Param('id') id: number, @QueryParam('onlyDirect') onlyDirect: boolean) {
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
let runners: Runner[];
if (!onlyDirect) { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.group', 'teams.runners.group.parentGroup', 'teams.runners.scans', 'teams.runners.scans.track'] })).allRunners; }
else { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners; }
runners.forEach(runner => {
responseRunners.push(new ResponseRunner(runner));
});
return responseRunners;
}
@Post()
@Authorized("ORGANIZATION:CREATE")
@ResponseSchema(ResponseRunnerOrganization)
@OpenAPI({ description: 'Create a new organsisation.' })
async post(@Body({ validate: true }) createRunnerOrganization: CreateRunnerOrganization) {
let runnerOrganization;
try {
runnerOrganization = await createRunnerOrganization.toEntity();
} catch (error) {
throw error;
}
runnerOrganization = await this.runnerOrganizationRepository.save(runnerOrganization);
return new ResponseRunnerOrganization(await this.runnerOrganizationRepository.findOne(runnerOrganization, { relations: ['contact', 'teams'] }));
}
@Put('/:id')
@Authorized("ORGANIZATION:UPDATE")
@ResponseSchema(ResponseRunnerOrganization)
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerOrganizationIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the organization whose id you provided. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) updateOrganization: UpdateRunnerOrganization) {
let oldRunnerOrganization = await this.runnerOrganizationRepository.findOne({ id: id });
if (!oldRunnerOrganization) {
throw new RunnerOrganizationNotFoundError();
}
if (oldRunnerOrganization.id != updateOrganization.id) {
throw new RunnerOrganizationIdsNotMatchingError();
}
await this.runnerOrganizationRepository.save(await updateOrganization.update(oldRunnerOrganization));
return new ResponseRunnerOrganization(await this.runnerOrganizationRepository.findOne(id, { relations: ['contact', 'teams'] }));
}
@Delete('/:id')
@Authorized("ORGANIZATION:DELETE")
@ResponseSchema(ResponseRunnerOrganization)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(RunnerOrganizationHasTeamsError, { statusCode: 406 })
@ResponseSchema(RunnerOrganizationHasRunnersError, { statusCode: 406 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the organsisation whose id you provided. <br> If the organization still has runners and/or teams associated this will fail. <br> To delete the organization with all associated runners and teams set the force QueryParam to true (cascading deletion might take a while). <br> This won\'t delete the associated contact. <br> If no organization with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
if (id == 1) {
throw new BadRequestError("You can't delete the citizen runner org.");
}
let organization = await this.runnerOrganizationRepository.findOne({ id: id });
if (!organization) { return null; }
let runnerOrganization = await this.runnerOrganizationRepository.findOne(organization, { relations: ['contact', 'runners', 'teams'] });
if (!force) {
if (runnerOrganization.teams.length != 0) {
throw new RunnerOrganizationHasTeamsError();
}
}
const teamController = new RunnerTeamController()
for (let team of runnerOrganization.teams) {
await teamController.remove(team.id, true);
}
if (!force) {
if (runnerOrganization.runners.length != 0) {
throw new RunnerOrganizationHasRunnersError();
}
}
const runnerController = new RunnerController()
for (let runner of runnerOrganization.runners) {
await runnerController.remove(runner.id, true);
}
const responseOrganization = new ResponseRunnerOrganization(runnerOrganization);
await this.runnerOrganizationRepository.delete(organization);
return responseOrganization;
}
}

View File

@ -1,244 +0,0 @@
import { Request } from "express";
import * as jwt from "jsonwebtoken";
import { BadRequestError, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { config } from '../config';
import { InvalidCredentialsError, JwtNotProvidedError } from '../errors/AuthError';
import { MailSendingError } from '../errors/MailErrors';
import { RunnerEmailNeededError, RunnerHasDistanceDonationsError, RunnerNotFoundError, RunnerSelfserviceTimeoutError } from '../errors/RunnerErrors';
import { RunnerOrganizationNotFoundError } from '../errors/RunnerOrganizationErrors';
import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
import { JwtCreator } from '../jwtcreator';
import { Mailer } from '../mailer';
import ScanAuth from '../middlewares/ScanAuth';
import { CreateSelfServiceCitizenRunner } from '../models/actions/create/CreateSelfServiceCitizenRunner';
import { CreateSelfServiceRunner } from '../models/actions/create/CreateSelfServiceRunner';
import { Runner } from '../models/entities/Runner';
import { RunnerGroup } from '../models/entities/RunnerGroup';
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
import { ScanStation } from '../models/entities/ScanStation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseScanStation } from '../models/responses/ResponseScanStation';
import { ResponseSelfServiceOrganisation } from '../models/responses/ResponseSelfServiceOrganisation';
import { ResponseSelfServiceRunner } from '../models/responses/ResponseSelfServiceRunner';
import { ResponseSelfServiceScan } from '../models/responses/ResponseSelfServiceScan';
import { DonationController } from './DonationController';
import { RunnerCardController } from './RunnerCardController';
import { ScanController } from './ScanController';
@JsonController()
export class RunnerSelfServiceController {
private runnerRepository: Repository<Runner>;
private orgRepository: Repository<RunnerOrganization>;
private stationRepository: Repository<ScanStation>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerRepository = getConnectionManager().get().getRepository(Runner);
this.orgRepository = getConnectionManager().get().getRepository(RunnerOrganization);
this.stationRepository = getConnectionManager().get().getRepository(ScanStation);
}
@Get('/runners/me/:jwt')
@ResponseSchema(ResponseSelfServiceRunner)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Lists all information about yourself. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please use the forgot endpoint.' })
async get(@Param('jwt') token: string) {
return (new ResponseSelfServiceRunner(await this.getRunner(token)));
}
@Delete('/runners/me/:jwt')
@ResponseSchema(ResponseSelfServiceRunner)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Deletes all information about yourself. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please use the forgot endpoint.' })
async remove(@Param('jwt') token: string, @QueryParam("force") force: boolean) {
const responseRunner = await this.getRunner(token);
let runner = await this.runnerRepository.findOne({ id: responseRunner.id });
if (!runner) { return null; }
if (!runner) {
throw new RunnerNotFoundError();
}
const runnerDonations = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["distanceDonations"] })).distanceDonations;
if (runnerDonations.length > 0 && !force) {
throw new RunnerHasDistanceDonationsError();
}
const donationController = new DonationController();
for (let donation of runnerDonations) {
await donationController.remove(donation.id, force);
}
const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards;
const cardController = new RunnerCardController;
for (let card of runnerCards) {
await cardController.remove(card.id, force);
}
const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans;
const scanController = new ScanController;
for (let scan of runnerScans) {
await scanController.remove(scan.id, force);
}
await this.runnerRepository.delete(runner);
return new ResponseSelfServiceRunner(responseRunner);
}
@Get('/runners/me/:jwt/scans')
@ResponseSchema(ResponseSelfServiceScan, { isArray: true })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Lists all your (runner) scans. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please contact support.' })
async getScans(@Param('jwt') token: string) {
const scans = (await this.getRunner(token)).scans;
let responseScans = new Array<ResponseSelfServiceScan>()
for (let scan of scans) {
responseScans.push(new ResponseSelfServiceScan(scan));
}
return responseScans;
}
@Get('/stations/me')
@UseBefore(ScanAuth)
@ResponseSchema(ResponseScanStation)
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
@OnUndefined(ScanStationNotFoundError)
@OpenAPI({ description: 'Lists basic information about the station whose token got provided. <br> This includes it\'s associated track.', security: [{ "StationApiToken": [] }] })
async getStationMe(@Req() req: Request) {
let scan = await this.stationRepository.findOne({ id: parseInt(req.headers["station_id"].toString()) }, { relations: ['track'] })
if (!scan) { throw new ScanStationNotFoundError(); }
return scan.toResponse();
}
@Post('/runners/login')
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(ResponseEmpty)
@OpenAPI({ description: 'Use this endpoint to reuqest a new selfservice magic-login-link to be sent to your mail address (rate limited to one mail every 15mins).' })
async requestNewToken(@QueryParam('mail') mail: string, @QueryParam("locale") locale: string = "en") {
if (!mail) {
throw new RunnerNotFoundError();
}
const runner = await this.runnerRepository.findOne({ email: mail });
if (!runner) { throw new RunnerNotFoundError(); }
if (runner.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 60 * 15)) { throw new RunnerSelfserviceTimeoutError(); }
const token = JwtCreator.createSelfService(runner);
try {
await Mailer.sendSelfserviceForgottenMail(runner.email, token, locale)
} catch (error) {
throw new MailSendingError();
}
runner.resetRequestedTimestamp = Math.floor(Date.now() / 1000);
await this.runnerRepository.save(runner);
return { token };
}
@Post('/runners/register')
@ResponseSchema(ResponseSelfServiceRunner)
@ResponseSchema(RunnerEmailNeededError, { statusCode: 406 })
@OpenAPI({ description: 'Create a new selfservice runner in the citizen org. <br> This endpoint shoud be used to allow "everyday citizen" to register themselves. <br> You have to provide a mail address, b/c the future we\'ll implement email verification.' })
async registerRunner(@Body({ validate: true }) createRunner: CreateSelfServiceCitizenRunner, @QueryParam("locale") locale: string = "en") {
let runner = await createRunner.toEntity();
if (await this.getRunnerExistsByMail(runner.email)) {
throw new BadRequestError("E-Mail already registered")
}
runner = await this.runnerRepository.save(runner);
let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
response.token = JwtCreator.createSelfService(runner);
try {
await Mailer.sendSelfserviceWelcomeMail(runner.email, response.token, locale)
} catch (error) {
throw new MailSendingError();
}
return response;
}
@Post('/runners/register/:token')
@ResponseSchema(ResponseSelfServiceRunner)
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new selfservice runner in a provided org. <br> The orgs get provided and authorized via api tokens that can be optained via the /organizations endpoint.' })
async registerOrganizationRunner(@Param('token') token: string, @Body({ validate: true }) createRunner: CreateSelfServiceRunner, @QueryParam("locale") locale: string = "en") {
const org = await this.getOrgansisation(token);
let runner = await createRunner.toEntity(org);
if (await this.getRunnerExistsByMail(runner.email)) {
throw new BadRequestError("E-Mail already registered")
}
runner = await this.runnerRepository.save(runner);
let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
response.token = JwtCreator.createSelfService(runner);
try {
await Mailer.sendSelfserviceWelcomeMail(runner.email, response.token, locale)
} catch (error) {
throw new MailSendingError();
}
return response;
}
@Get('/organizations/selfservice/:token')
@ResponseSchema(ResponseSelfServiceOrganisation, { isArray: false })
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Get the basic info and teams for a org.' })
async getSelfserviceOrg(@Param('token') token: string) {
const orgid = (await this.getOrgansisation(token)).id;
const org = await this.orgRepository.findOne({ id: orgid }, { relations: ['teams'] })
return new ResponseSelfServiceOrganisation(<RunnerOrganization>org);
}
/**
* Get's a runner by a provided jwt token.
* @param token The runner jwt provided by the runner to identitfy themselves.
*/
private async getRunner(token: string): Promise<Runner> {
if (token == "") { throw new JwtNotProvidedError(); }
let jwtPayload = undefined
try {
jwtPayload = <any>jwt.verify(token, config.jwt_secret);
} catch (error) {
throw new InvalidCredentialsError();
}
const runner = await this.runnerRepository.findOne({ id: jwtPayload["id"] }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] });
if (!runner) { throw new RunnerNotFoundError() }
return runner;
}
/**
* Get's a runner org by a provided registration api key.
* @param token The organization's registration api token.
*/
private async getOrgansisation(token: string): Promise<RunnerGroup> {
token = Buffer.from(token, 'base64').toString('utf8');
const organization = await this.orgRepository.findOne({ key: token });
if (!organization) { throw new RunnerOrganizationNotFoundError; }
return organization;
}
/**
* Checks if a runner already exists
* @param email The runner's email address
* @returns Boolean (true if exists, false if not)
*/
private async getRunnerExistsByMail(email: string): Promise<boolean> {
const runner = await this.runnerRepository.findOne({ email });
return runner != undefined
}
}

View File

@ -1,12 +1,11 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { RunnerTeamHasRunnersError, RunnerTeamIdsNotMatchingError, RunnerTeamNotFoundError } from '../errors/RunnerTeamErrors'; import { RunnerTeamHasRunnersError, RunnerTeamIdsNotMatchingError, RunnerTeamNotFoundError } from '../errors/RunnerTeamErrors';
import { CreateRunnerTeam } from '../models/actions/create/CreateRunnerTeam'; import { CreateRunnerTeam } from '../models/actions/create/CreateRunnerTeam';
import { UpdateRunnerTeam } from '../models/actions/update/UpdateRunnerTeam'; import { UpdateRunnerTeam } from '../models/actions/update/UpdateRunnerTeam';
import { RunnerTeam } from '../models/entities/RunnerTeam'; import { RunnerTeam } from '../models/entities/RunnerTeam';
import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunner } from '../models/responses/ResponseRunner';
import { ResponseRunnerTeam } from '../models/responses/ResponseRunnerTeam'; import { ResponseRunnerTeam } from '../models/responses/ResponseRunnerTeam';
import { RunnerController } from './RunnerController'; import { RunnerController } from './RunnerController';
@ -26,19 +25,12 @@ export class RunnerTeamController {
@Get() @Get()
@Authorized("TEAM:GET") @Authorized("TEAM:GET")
@ResponseSchema(ResponseRunnerTeam, { isArray: true }) @ResponseSchema(ResponseRunnerTeam, { isArray: true })
@OpenAPI({ description: 'Lists all teams. <br> This includes their parent organization and contact (if existing/associated).' }) @OpenAPI({ description: 'Lists all teams. <br> This includes their parent organisation and contact (if existing/associated).' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { async getAll() {
let responseTeams: ResponseRunnerTeam[] = new Array<ResponseRunnerTeam>(); let responseTeams: ResponseRunnerTeam[] = new Array<ResponseRunnerTeam>();
let teams: Array<RunnerTeam>; const runners = await this.runnerTeamRepository.find({ relations: ['parentGroup', 'contact'] });
runners.forEach(runner => {
if (page != undefined) { responseTeams.push(new ResponseRunnerTeam(runner));
teams = await this.runnerTeamRepository.find({ relations: ['parentGroup', 'contact'], skip: page * page_size, take: page_size });
} else {
teams = await this.runnerTeamRepository.find({ relations: ['parentGroup', 'contact'] });
}
teams.forEach(team => {
responseTeams.push(new ResponseRunnerTeam(team));
}); });
return responseTeams; return responseTeams;
} }
@ -50,25 +42,11 @@ export class RunnerTeamController {
@OnUndefined(RunnerTeamNotFoundError) @OnUndefined(RunnerTeamNotFoundError)
@OpenAPI({ description: 'Lists all information about the team whose id got provided.' }) @OpenAPI({ description: 'Lists all information about the team whose id got provided.' })
async getOne(@Param('id') id: number) { async getOne(@Param('id') id: number) {
let runnerTeam = await this.runnerTeamRepository.findOne({ id: id }, { relations: ['parentGroup', 'contact', 'runners', 'runners.scans', 'runners.scans.track'] }); let runnerTeam = await this.runnerTeamRepository.findOne({ id: id }, { relations: ['parentGroup', 'contact'] });
if (!runnerTeam) { throw new RunnerTeamNotFoundError(); } if (!runnerTeam) { throw new RunnerTeamNotFoundError(); }
return new ResponseRunnerTeam(runnerTeam); return new ResponseRunnerTeam(runnerTeam);
} }
@Get('/:id/runners')
@Authorized(["RUNNER:GET", "SCAN:GET"])
@ResponseSchema(ResponseRunner, { isArray: true })
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Lists all runners from this team. <br> This includes the runner\'s group and distance ran.' })
async getRunners(@Param('id') id: number) {
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
const runners = (await this.runnerTeamRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners;
runners.forEach(runner => {
responseRunners.push(new ResponseRunner(runner));
});
return responseRunners;
}
@Post() @Post()
@Authorized("TEAM:CREATE") @Authorized("TEAM:CREATE")
@ResponseSchema(ResponseRunnerTeam) @ResponseSchema(ResponseRunnerTeam)
@ -115,7 +93,7 @@ export class RunnerTeamController {
@ResponseSchema(ResponseEmpty, { statusCode: 204 }) @ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(RunnerTeamHasRunnersError, { statusCode: 406 }) @ResponseSchema(RunnerTeamHasRunnersError, { statusCode: 406 })
@OnUndefined(204) @OnUndefined(204)
@OpenAPI({ description: 'Delete the team whose id you provided. <br> If the team still has runners associated this will fail. <br> To delete the team with all associated runners set the force QueryParam to true (cascading deletion might take a while). <br> This won\'t delete the associated contact.<br> If no team with this id exists it will just return 204(no content).' }) @OpenAPI({ description: 'Delete the team whose id you provided. <br> If the team still has runners associated this will fail. <br> To delete the team with all associated runners set the force QueryParam to true (cascading deletion might take a while). <br> If no team with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let team = await this.runnerTeamRepository.findOne({ id: id }); let team = await this.runnerTeamRepository.findOne({ id: id });
if (!team) { return null; } if (!team) { return null; }

View File

@ -1,7 +1,6 @@
import { Request } from "express"; import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam, UseBefore } from 'routing-controllers';
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam, Req, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { RunnerNotFoundError } from '../errors/RunnerErrors'; import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors'; import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors';
import { ScanStationNotFoundError } from '../errors/ScanStationErrors'; import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
@ -15,6 +14,7 @@ import { TrackScan } from '../models/entities/TrackScan';
import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseScan } from '../models/responses/ResponseScan'; import { ResponseScan } from '../models/responses/ResponseScan';
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan'; import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
@JsonController('/scans') @JsonController('/scans')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class ScanController { export class ScanController {
@ -34,16 +34,9 @@ export class ScanController {
@ResponseSchema(ResponseScan, { isArray: true }) @ResponseSchema(ResponseScan, { isArray: true })
@ResponseSchema(ResponseTrackScan, { isArray: true }) @ResponseSchema(ResponseTrackScan, { isArray: true })
@OpenAPI({ description: 'Lists all scans (normal or track) from all runners. <br> This includes the scan\'s runner\'s distance ran.' }) @OpenAPI({ description: 'Lists all scans (normal or track) from all runners. <br> This includes the scan\'s runner\'s distance ran.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { async getAll() {
let responseScans: ResponseScan[] = new Array<ResponseScan>(); let responseScans: ResponseScan[] = new Array<ResponseScan>();
let scans: Array<Scan>; const scans = await this.scanRepository.find({ relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] });
if (page != undefined) {
scans = await this.scanRepository.find({ relations: ['runner', 'track'], skip: page * page_size, take: page_size });
} else {
scans = await this.scanRepository.find({ relations: ['runner', 'track'] });
}
scans.forEach(scan => { scans.forEach(scan => {
responseScans.push(scan.toResponse()); responseScans.push(scan.toResponse());
}); });
@ -58,7 +51,7 @@ export class ScanController {
@OnUndefined(ScanNotFoundError) @OnUndefined(ScanNotFoundError)
@OpenAPI({ description: 'Lists all information about the scan whose id got provided. This includes the scan\'s runner\'s distance ran.' }) @OpenAPI({ description: 'Lists all information about the scan whose id got provided. This includes the scan\'s runner\'s distance ran.' })
async getOne(@Param('id') id: number) { async getOne(@Param('id') id: number) {
let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.group', 'card', 'station'] }) let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })
if (!scan) { throw new ScanNotFoundError(); } if (!scan) { throw new ScanNotFoundError(); }
return scan.toResponse(); return scan.toResponse();
} }
@ -67,26 +60,22 @@ export class ScanController {
@UseBefore(ScanAuth) @UseBefore(ScanAuth)
@ResponseSchema(ResponseScan) @ResponseSchema(ResponseScan)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) @ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new scan (not track scan - use /scans/trackscans instead). <br> Please rmemember to provide the scan\'s runner\'s id and distance.', security: [{ "StationApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @OpenAPI({ description: 'Create a new scan (not track scan - use /scans/trackscans instead). <br> Please rmemember to provide the scan\'s runner\'s id and distance.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async post(@Body({ validate: true }) createScan: CreateScan) { async post(@Body({ validate: true }) createScan: CreateScan) {
let scan = await createScan.toEntity(); let scan = await createScan.toEntity();
scan = await this.scanRepository.save(scan); scan = await this.scanRepository.save(scan);
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse(); return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
} }
@Post("/trackscans") @Post("/trackscans")
@UseBefore(ScanAuth) @UseBefore(ScanAuth)
@ResponseSchema(ResponseTrackScan) @ResponseSchema(ResponseTrackScan)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) @ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new track scan (for "normal" scans use /scans instead). <br> Please remember that to provide the scan\'s card\'s station\'s id.', security: [{ "StationApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @OpenAPI({ description: 'Create a new track scan (for "normal" scans use /scans instead). <br> Please remember that to provide the scan\'s card\'s station\'s id.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan, @Req() req: Request) { async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan) {
const station_id = req.headers["station_id"];
if (station_id) {
createScan.station = parseInt(station_id.toString());
}
let scan = await createScan.toEntity(); let scan = await createScan.toEntity();
scan = await this.trackScanRepository.save(scan); scan = await this.trackScanRepository.save(scan);
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse(); return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
} }
@Put('/:id') @Put('/:id')
@ -108,7 +97,7 @@ export class ScanController {
} }
await this.scanRepository.save(await scan.update(oldScan)); await this.scanRepository.save(await scan.update(oldScan));
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse(); return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
} }
@Put('/trackscans/:id') @Put('/trackscans/:id')
@ -131,7 +120,7 @@ export class ScanController {
} }
await this.trackScanRepository.save(await scan.update(oldScan)); await this.trackScanRepository.save(await scan.update(oldScan));
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse(); return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
} }
@Delete('/:id') @Delete('/:id')
@ -143,7 +132,7 @@ export class ScanController {
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let scan = await this.scanRepository.findOne({ id: id }); let scan = await this.scanRepository.findOne({ id: id });
if (!scan) { return null; } if (!scan) { return null; }
const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] }); const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] });
await this.scanRepository.delete(scan); await this.scanRepository.delete(scan);
return responseScan.toResponse(); return responseScan.toResponse();

View File

@ -1,6 +1,6 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { ScanStationHasScansError, ScanStationIdsNotMatchingError, ScanStationNotFoundError } from '../errors/ScanStationErrors'; import { ScanStationHasScansError, ScanStationIdsNotMatchingError, ScanStationNotFoundError } from '../errors/ScanStationErrors';
import { TrackNotFoundError } from '../errors/TrackErrors'; import { TrackNotFoundError } from '../errors/TrackErrors';
import { CreateScanStation } from '../models/actions/create/CreateScanStation'; import { CreateScanStation } from '../models/actions/create/CreateScanStation';
@ -26,16 +26,9 @@ export class ScanStationController {
@Authorized("STATION:GET") @Authorized("STATION:GET")
@ResponseSchema(ResponseScanStation, { isArray: true }) @ResponseSchema(ResponseScanStation, { isArray: true })
@OpenAPI({ description: 'Lists all stations. <br> This includes their associated tracks.' }) @OpenAPI({ description: 'Lists all stations. <br> This includes their associated tracks.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { async getAll() {
let responseStations: ResponseScanStation[] = new Array<ResponseScanStation>(); let responseStations: ResponseScanStation[] = new Array<ResponseScanStation>();
let stations: Array<ScanStation>; const stations = await this.stationRepository.find({ relations: ['track'] });
if (page != undefined) {
stations = await this.stationRepository.find({ relations: ['track'], skip: page * page_size, take: page_size });
} else {
stations = await this.stationRepository.find({ relations: ['track'] });
}
stations.forEach(station => { stations.forEach(station => {
responseStations.push(station.toResponse()); responseStations.push(station.toResponse());
}); });

View File

@ -1,6 +1,6 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam } from 'routing-controllers'; import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { StatsClientNotFoundError } from '../errors/StatsClientErrors'; import { StatsClientNotFoundError } from '../errors/StatsClientErrors';
import { TrackNotFoundError } from "../errors/TrackErrors"; import { TrackNotFoundError } from "../errors/TrackErrors";
import { CreateStatsClient } from '../models/actions/create/CreateStatsClient'; import { CreateStatsClient } from '../models/actions/create/CreateStatsClient';
@ -24,16 +24,9 @@ export class StatsClientController {
@Authorized("STATSCLIENT:GET") @Authorized("STATSCLIENT:GET")
@ResponseSchema(ResponseStatsClient, { isArray: true }) @ResponseSchema(ResponseStatsClient, { isArray: true })
@OpenAPI({ description: 'Lists all stats clients. Please remember that the key can only be viewed on creation.' }) @OpenAPI({ description: 'Lists all stats clients. Please remember that the key can only be viewed on creation.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { async getAll() {
let responseClients: ResponseStatsClient[] = new Array<ResponseStatsClient>(); let responseClients: ResponseStatsClient[] = new Array<ResponseStatsClient>();
let clients: Array<StatsClient>; const clients = await this.clientRepository.find();
if (page != undefined) {
clients = await this.clientRepository.find({ skip: page * page_size, take: page_size });
} else {
clients = await this.clientRepository.find();
}
clients.forEach(clients => { clients.forEach(clients => {
responseClients.push(new ResponseStatsClient(clients)); responseClients.push(new ResponseStatsClient(clients));
}); });

View File

@ -1,17 +1,15 @@
import { Get, JsonController, QueryParam, UseBefore } from 'routing-controllers'; import { Get, JsonController, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnection } from 'typeorm'; import { getConnection } from 'typeorm';
import StatsAuth from '../middlewares/StatsAuth'; import StatsAuth from '../middlewares/StatsAuth';
import { Donation } from '../models/entities/Donation'; import { Donation } from '../models/entities/Donation';
import { Donor } from '../models/entities/Donor';
import { Runner } from '../models/entities/Runner'; import { Runner } from '../models/entities/Runner';
import { RunnerOrganization } from '../models/entities/RunnerOrganization'; import { RunnerOrganisation } from '../models/entities/RunnerOrganisation';
import { RunnerTeam } from '../models/entities/RunnerTeam'; import { RunnerTeam } from '../models/entities/RunnerTeam';
import { Scan } from '../models/entities/Scan'; import { Scan } from '../models/entities/Scan';
import { TrackScan } from '../models/entities/TrackScan';
import { User } from '../models/entities/User'; import { User } from '../models/entities/User';
import { ResponseStats } from '../models/responses/ResponseStats'; import { ResponseStats } from '../models/responses/ResponseStats';
import { ResponseStatsOrgnisation } from '../models/responses/ResponseStatsOrganization'; import { ResponseStatsOrgnisation } from '../models/responses/ResponseStatsOrganisation';
import { ResponseStatsRunner } from '../models/responses/ResponseStatsRunner'; import { ResponseStatsRunner } from '../models/responses/ResponseStatsRunner';
import { ResponseStatsTeam } from '../models/responses/ResponseStatsTeam'; import { ResponseStatsTeam } from '../models/responses/ResponseStatsTeam';
@ -22,26 +20,14 @@ export class StatsController {
@ResponseSchema(ResponseStats) @ResponseSchema(ResponseStats)
@OpenAPI({ description: "A very basic stats endpoint providing basic counters for a dashboard or simmilar" }) @OpenAPI({ description: "A very basic stats endpoint providing basic counters for a dashboard or simmilar" })
async get() { async get() {
const connection = getConnection(); let connection = getConnection();
const runners = await connection.getRepository(Runner).count(); let runners = await connection.getRepository(Runner).find({ relations: ['scans', 'scans.track'] });
const teams = await connection.getRepository(RunnerTeam).count(); let teams = await connection.getRepository(RunnerTeam).find();
const orgs = await connection.getRepository(RunnerOrganization).count(); let orgs = await connection.getRepository(RunnerOrganisation).find();
const users = await connection.getRepository(User).count(); let users = await connection.getRepository(User).find();
const scans = await connection.getRepository(Scan).count({ where: { valid: true } }); let scans = await connection.getRepository(Scan).find();
const distance_query = await connection.getRepository(Scan).createQueryBuilder('scan')
.leftJoinAndSelect("scan.track", "track").where("scan.valid = TRUE")
.select("SUM(track.distance)", "sum_track").addSelect("SUM(_distance)", "sum_distance")
.getRawOne();
let distace = parseInt(distance_query.sum_track)
if (distance_query.sum_distance) {
distace += parseInt(distance_query.sum_distance)
}
let donations = await connection.getRepository(Donation).find({ relations: ['runner', 'runner.scans', 'runner.scans.track'] }); let donations = await connection.getRepository(Donation).find({ relations: ['runner', 'runner.scans', 'runner.scans.track'] });
const donors = await connection.getRepository(Donor).count(); return new ResponseStats(runners, teams, orgs, users, scans, donations)
return new ResponseStats(runners, teams, orgs, users, scans, donations, distace, donors)
} }
@Get("/runners/distance") @Get("/runners/distance")
@ -50,10 +36,7 @@ export class StatsController {
@OpenAPI({ description: "Returns the top ten runners by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @OpenAPI({ description: "Returns the top ten runners by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopRunnersByDistance() { async getTopRunnersByDistance() {
let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] }); let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] });
if (!runners || runners.length == 0) { let topRunners = runners.sort((runner1, runner2) => runner1.distance - runner2.distance).slice(0, 9);
return [];
}
let topRunners = runners.sort((runner1, runner2) => runner2.distance - runner1.distance).slice(0, 10);
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>(); let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
topRunners.forEach(runner => { topRunners.forEach(runner => {
responseRunners.push(new ResponseStatsRunner(runner)); responseRunners.push(new ResponseStatsRunner(runner));
@ -66,11 +49,8 @@ export class StatsController {
@ResponseSchema(ResponseStatsRunner, { isArray: true }) @ResponseSchema(ResponseStatsRunner, { isArray: true })
@OpenAPI({ description: "Returns the top ten runners by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @OpenAPI({ description: "Returns the top ten runners by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopRunnersByDonations() { async getTopRunnersByDonations() {
let runners = await getConnection().getRepository(Runner).find({ relations: ['group', 'distanceDonations', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }); let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] });
if (!runners || runners.length == 0) { let topRunners = runners.sort((runner1, runner2) => runner1.distanceDonationAmount - runner2.distanceDonationAmount).slice(0, 9);
return [];
}
let topRunners = runners.sort((runner1, runner2) => runner2.distanceDonationAmount - runner1.distanceDonationAmount).slice(0, 10);
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>(); let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
topRunners.forEach(runner => { topRunners.forEach(runner => {
responseRunners.push(new ResponseStatsRunner(runner)); responseRunners.push(new ResponseStatsRunner(runner));
@ -78,34 +58,6 @@ export class StatsController {
return responseRunners; return responseRunners;
} }
@Get("/runners/laptime")
@UseBefore(StatsAuth)
@ResponseSchema(ResponseStatsRunner, { isArray: true })
@OpenAPI({ description: "Returns the top ten runners by fastest laptime on your selected track (track by id).", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopRunnersByLaptime(@QueryParam("track") track: number) {
let scans = await getConnection().getRepository(TrackScan).find({ relations: ['track', 'runner', 'runner.group', 'runner.scans', 'runner.scans.track', 'runner.distanceDonations'] });
if (!scans || scans.length == 0) {
return [];
}
scans = scans.filter((s) => { return s.track.id == track && s.valid == true && s.lapTime != 0 }).sort((scan1, scan2) => scan1.lapTime - scan2.lapTime);
let topScans = new Array<TrackScan>();
let knownRunners = new Array<number>();
for (let i = 0; i < scans.length && topScans.length < 10; i++) {
const element = scans[i];
if (!knownRunners.includes(element.runner.id)) {
topScans.push(element);
knownRunners.push(element.runner.id);
}
}
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
topScans.forEach(scan => {
responseRunners.push(new ResponseStatsRunner(scan.runner, scan.lapTime));
});
return responseRunners;
}
@Get("/scans") @Get("/scans")
@UseBefore(StatsAuth) @UseBefore(StatsAuth)
@ResponseSchema(ResponseStatsRunner, { isArray: true }) @ResponseSchema(ResponseStatsRunner, { isArray: true })
@ -119,11 +71,8 @@ export class StatsController {
@ResponseSchema(ResponseStatsTeam, { isArray: true }) @ResponseSchema(ResponseStatsTeam, { isArray: true })
@OpenAPI({ description: "Returns the top ten teams by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @OpenAPI({ description: "Returns the top ten teams by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopTeamsByDistance() { async getTopTeamsByDistance() {
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['parentGroup', 'runners', 'runners.scans', 'runners.scans.track'] }); let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
if (!teams || teams.length == 0) { let topTeams = teams.sort((team1, team2) => team1.distance - team2.distance).slice(0, 9);
return [];
}
let topTeams = teams.sort((team1, team2) => team2.distance - team1.distance).slice(0, 10);
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>(); let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
topTeams.forEach(team => { topTeams.forEach(team => {
responseTeams.push(new ResponseStatsTeam(team)); responseTeams.push(new ResponseStatsTeam(team));
@ -136,11 +85,8 @@ export class StatsController {
@ResponseSchema(ResponseStatsTeam, { isArray: true }) @ResponseSchema(ResponseStatsTeam, { isArray: true })
@OpenAPI({ description: "Returns the top ten teams by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @OpenAPI({ description: "Returns the top ten teams by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopTeamsByDonations() { async getTopTeamsByDonations() {
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['parentGroup', 'runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] }); let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
if (!teams || teams.length == 0) { let topTeams = teams.sort((team1, team2) => team1.distanceDonationAmount - team2.distanceDonationAmount).slice(0, 9);
return [];
}
let topTeams = teams.sort((team1, team2) => team2.distanceDonationAmount - team1.distanceDonationAmount).slice(0, 10);
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>(); let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
topTeams.forEach(team => { topTeams.forEach(team => {
responseTeams.push(new ResponseStatsTeam(team)); responseTeams.push(new ResponseStatsTeam(team));
@ -148,16 +94,13 @@ export class StatsController {
return responseTeams; return responseTeams;
} }
@Get("/organizations/distance") @Get("/organisations/distance")
@UseBefore(StatsAuth) @UseBefore(StatsAuth)
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true }) @ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
@OpenAPI({ description: "Returns the top ten organizations by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @OpenAPI({ description: "Returns the top ten organisations by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopOrgsByDistance() { async getTopOrgsByDistance() {
let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] }); let orgs = await getConnection().getRepository(RunnerOrganisation).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
if (!orgs || orgs.length == 0) { let topOrgs = orgs.sort((org1, org2) => org1.distance - org2.distance).slice(0, 9);
return [];
}
let topOrgs = orgs.sort((org1, org2) => org2.distance - org1.distance).slice(0, 10);
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>(); let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
topOrgs.forEach(org => { topOrgs.forEach(org => {
responseOrgs.push(new ResponseStatsOrgnisation(org)); responseOrgs.push(new ResponseStatsOrgnisation(org));
@ -165,16 +108,13 @@ export class StatsController {
return responseOrgs; return responseOrgs;
} }
@Get("/organizations/donations") @Get("/organisations/donations")
@UseBefore(StatsAuth) @UseBefore(StatsAuth)
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true }) @ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
@OpenAPI({ description: "Returns the top ten organizations by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @OpenAPI({ description: "Returns the top ten organisations by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopOrgsByDonations() { async getTopOrgsByDonations() {
let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.distanceDonations', 'runners.distanceDonations.runner', 'runners.distanceDonations.runner.scans', 'runners.distanceDonations.runner.scans.track', 'teams', 'teams.runners', 'teams.runners.distanceDonations', 'teams.runners.distanceDonations.runner', 'teams.runners.distanceDonations.runner.scans', 'teams.runners.distanceDonations.runner.scans.track'] }); let orgs = await getConnection().getRepository(RunnerOrganisation).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
if (!orgs || orgs.length == 0) { let topOrgs = orgs.sort((org1, org2) => org1.distanceDonationAmount - org2.distanceDonationAmount).slice(0, 9);
return [];
}
let topOrgs = orgs.sort((org1, org2) => org2.distanceDonationAmount - org1.distanceDonationAmount).slice(0, 10);
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>(); let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
topOrgs.forEach(org => { topOrgs.forEach(org => {
responseOrgs.push(new ResponseStatsOrgnisation(org)); responseOrgs.push(new ResponseStatsOrgnisation(org));

View File

@ -1,12 +1,11 @@
import { Get, JsonController } from 'routing-controllers'; import { Get, JsonController } from 'routing-controllers';
import { OpenAPI } from 'routing-controllers-openapi'; import { OpenAPI } from 'routing-controllers-openapi';
import { getConnection } from 'typeorm'; import { getConnection } from 'typeorm';
import { config } from '../config';
@JsonController() @JsonController('/status')
export class StatusController { export class StatusController {
@Get('/status') @Get()
@OpenAPI({ description: "A very basic status/health endpoint that just checks if the database connection is available. <br> The available information depth will be expanded later." }) @OpenAPI({ description: "A very basic status/health endpoint that just checks if the database connection is available. <br> The available information depth will be expanded later." })
get() { get() {
let connection; let connection;
@ -20,12 +19,4 @@ export class StatusController {
"database connection": "✔" "database connection": "✔"
}; };
} }
@Get('/version')
@OpenAPI({ description: "A very basic endpoint that just returns the curent package version." })
getVersion() {
return {
"version": config.version
}
}
} }

View File

@ -1,6 +1,6 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { TrackHasScanStationsError, TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors"; import { TrackHasScanStationsError, TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors";
import { CreateTrack } from '../models/actions/create/CreateTrack'; import { CreateTrack } from '../models/actions/create/CreateTrack';
import { UpdateTrack } from '../models/actions/update/UpdateTrack'; import { UpdateTrack } from '../models/actions/update/UpdateTrack';
@ -25,17 +25,9 @@ export class TrackController {
@Authorized("TRACK:GET") @Authorized("TRACK:GET")
@ResponseSchema(ResponseTrack, { isArray: true }) @ResponseSchema(ResponseTrack, { isArray: true })
@OpenAPI({ description: 'Lists all tracks.' }) @OpenAPI({ description: 'Lists all tracks.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { async getAll() {
let responseTracks: ResponseTrack[] = new Array<ResponseTrack>(); let responseTracks: ResponseTrack[] = new Array<ResponseTrack>();
let tracks: Array<Track>; const tracks = await this.trackRepository.find();
if (page != undefined) {
tracks = await this.trackRepository.find({ skip: page * page_size, take: page_size });
}
else {
tracks = await this.trackRepository.find();
}
tracks.forEach(track => { tracks.forEach(track => {
responseTracks.push(new ResponseTrack(track)); responseTracks.push(new ResponseTrack(track));
}); });

View File

@ -1,14 +1,13 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserDeletionNotConfirmedError, UserIdsNotMatchingError, UserNotFoundError, UsernameContainsIllegalCharacterError } from '../errors/UserErrors'; import { UserIdsNotMatchingError, UserNotFoundError } from '../errors/UserErrors';
import { UserGroupNotFoundError } from '../errors/UserGroupErrors'; import { UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUser } from '../models/actions/create/CreateUser'; import { CreateUser } from '../models/actions/create/CreateUser';
import { UpdateUser } from '../models/actions/update/UpdateUser'; import { UpdateUser } from '../models/actions/update/UpdateUser';
import { User } from '../models/entities/User'; import { User } from '../models/entities/User';
import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseUser } from '../models/responses/ResponseUser'; import { ResponseUser } from '../models/responses/ResponseUser';
import { ResponseUserPermissions } from '../models/responses/ResponseUserPermissions';
import { PermissionController } from './PermissionController'; import { PermissionController } from './PermissionController';
@ -27,18 +26,10 @@ export class UserController {
@Get() @Get()
@Authorized("USER:GET") @Authorized("USER:GET")
@ResponseSchema(ResponseUser, { isArray: true }) @ResponseSchema(ResponseUser, { isArray: true })
@OpenAPI({ description: 'Lists all users. <br> This includes their groups and permissions granted to them.' }) @OpenAPI({ description: 'Lists all users. <br> This includes their groups and permissions directly granted to them (if existing/associated).' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { async getAll() {
let responseUsers: ResponseUser[] = new Array<ResponseUser>(); let responseUsers: ResponseUser[] = new Array<ResponseUser>();
let users: Array<User>; const users = await this.userRepository.find({ relations: ['permissions', 'groups', 'groups.permissions'] });
if (page != undefined) {
users = await this.userRepository.find({ relations: ['permissions', 'groups', 'groups.permissions'], skip: page * page_size, take: page_size });
}
else {
users = await this.userRepository.find({ relations: ['permissions', 'groups', 'groups.permissions'] });
}
users.forEach(user => { users.forEach(user => {
responseUsers.push(new ResponseUser(user)); responseUsers.push(new ResponseUser(user));
}); });
@ -50,34 +41,17 @@ export class UserController {
@ResponseSchema(ResponseUser) @ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 }) @ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError) @OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Lists all information about the user whose id got provided. <br> Please remember that all permissions granted to the user will show up here.' }) @OpenAPI({ description: 'Lists all information about the user whose id got provided. <br> Please remember that only permissions granted directly to the user will show up here, not permissions inherited from groups.' })
async getOne(@Param('id') id: number) { async getOne(@Param('id') id: number) {
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] }) let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] })
if (!user) { throw new UserNotFoundError(); } if (!user) { throw new UserNotFoundError(); }
return new ResponseUser(user); return new ResponseUser(user);
} }
@Get('/:id/permissions')
@Authorized("USER:GET")
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Lists all permissions granted to the user sorted into directly granted and inherited as permission response objects.' })
async getPermissions(@Param('id') id: number) {
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
if (!user) { throw new UserNotFoundError(); }
return new ResponseUserPermissions(user);
}
@Post() @Post()
@Authorized("USER:CREATE") @Authorized("USER:CREATE")
@ResponseSchema(ResponseUser) @ResponseSchema(ResponseUser)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 }) @ResponseSchema(UserGroupNotFoundError)
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
@OpenAPI({ description: 'Create a new user. <br> If you want to grant permissions to the user you have to create them seperately by posting to /api/permissions after creating the user.' }) @OpenAPI({ description: 'Create a new user. <br> If you want to grant permissions to the user you have to create them seperately by posting to /api/permissions after creating the user.' })
async post(@Body({ validate: true }) createUser: CreateUser) { async post(@Body({ validate: true }) createUser: CreateUser) {
let user; let user;
@ -88,7 +62,7 @@ export class UserController {
} }
user = await this.userRepository.save(user) user = await this.userRepository.save(user)
return new ResponseUser(await this.userRepository.findOne({ id: user.id }, { relations: ['permissions', 'groups', 'groups.permissions'] })); return new ResponseUser(await this.userRepository.findOne({ id: user.id }, { relations: ['permissions', 'groups'] }));
} }
@Put('/:id') @Put('/:id')
@ -96,11 +70,6 @@ export class UserController {
@ResponseSchema(ResponseUser) @ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 }) @ResponseSchema(UserNotFoundError, { statusCode: 404 })
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 }) @ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
@OpenAPI({ description: "Update the user whose id you provided. <br> To change the permissions directly granted to the user please use /api/permissions instead. <br> Please remember that ids can't be changed." }) @OpenAPI({ description: "Update the user whose id you provided. <br> To change the permissions directly granted to the user please use /api/permissions instead. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) updateUser: UpdateUser) { async put(@Param('id') id: number, @Body({ validate: true }) updateUser: UpdateUser) {
let oldUser = await this.userRepository.findOne({ id: id }); let oldUser = await this.userRepository.findOne({ id: id });
@ -114,21 +83,19 @@ export class UserController {
} }
await this.userRepository.save(await updateUser.update(oldUser)); await this.userRepository.save(await updateUser.update(oldUser));
return new ResponseUser(await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] })); return new ResponseUser(await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] }));
} }
@Delete('/:id') @Delete('/:id')
@Authorized("USER:DELETE") @Authorized("USER:DELETE")
@ResponseSchema(ResponseUser) @ResponseSchema(ResponseUser)
@ResponseSchema(ResponseEmpty, { statusCode: 204 }) @ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 })
@OnUndefined(204) @OnUndefined(204)
@OpenAPI({ description: 'Delete the user whose id you provided. <br> You have to confirm your decision by providing the ?force=true query param. <br> If there are any permissions directly granted to the user they will get deleted as well. <br> If no user with this id exists it will just return 204(no content).' }) @OpenAPI({ description: 'Delete the user whose id you provided. <br> If there are any permissions directly granted to the user they will get deleted as well. <br> If no user with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
if (!force) { throw new UserDeletionNotConfirmedError; }
let user = await this.userRepository.findOne({ id: id }); let user = await this.userRepository.findOne({ id: id });
if (!user) { return null; } if (!user) { return null; }
const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] });; const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] });;
const permissionControler = new PermissionController(); const permissionControler = new PermissionController();
for (let permission of responseUser.permissions) { for (let permission of responseUser.permissions) {

View File

@ -1,13 +1,13 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
import { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/UserGroupErrors'; import { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUserGroup } from '../models/actions/create/CreateUserGroup'; import { CreateUserGroup } from '../models/actions/create/CreateUserGroup';
import { UpdateUserGroup } from '../models/actions/update/UpdateUserGroup'; import { UpdateUserGroup } from '../models/actions/update/UpdateUserGroup';
import { UserGroup } from '../models/entities/UserGroup'; import { UserGroup } from '../models/entities/UserGroup';
import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseUserGroup } from '../models/responses/ResponseUserGroup'; import { ResponseUserGroup } from '../models/responses/ResponseUserGroup';
import { ResponseUserGroupPermissions } from '../models/responses/ResponseUserGroupPermissions';
import { PermissionController } from './PermissionController'; import { PermissionController } from './PermissionController';
@ -25,44 +25,20 @@ export class UserGroupController {
@Get() @Get()
@Authorized("USERGROUP:GET") @Authorized("USERGROUP:GET")
@ResponseSchema(ResponseUserGroup, { isArray: true }) @ResponseSchema(UserGroup, { isArray: true })
@OpenAPI({ description: 'Lists all groups. <br> The information provided might change while the project continues to evolve.' }) @OpenAPI({ description: 'Lists all groups. <br> The information provided might change while the project continues to evolve.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) { getAll() {
let responseGroups: ResponseUserGroup[] = new Array<ResponseUserGroup>(); return this.userGroupsRepository.find({ relations: ["permissions"] });
let groups: Array<UserGroup>;
if (page != undefined) {
groups = await this.userGroupsRepository.find({ relations: ['permissions'], skip: page * page_size, take: page_size });
} else {
groups = await this.userGroupsRepository.find({ relations: ['permissions'] });
}
groups.forEach(group => {
responseGroups.push(group.toResponse());
});
return responseGroups;
} }
@Get('/:id') @Get('/:id')
@Authorized("USERGROUP:GET") @Authorized("USERGROUP:GET")
@ResponseSchema(ResponseUserGroup) @ResponseSchema(UserGroup)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 }) @ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@OnUndefined(UserGroupNotFoundError) @OnUndefined(UserGroupNotFoundError)
@OpenAPI({ description: 'Lists all information about the group whose id got provided. <br> The information provided might change while the project continues to evolve.' }) @OpenAPI({ description: 'Lists all information about the group whose id got provided. <br> The information provided might change while the project continues to evolve.' })
async getOne(@Param('id') id: number) { getOne(@Param('id') id: number) {
return await (await (this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] }))).toResponse(); return this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
}
@Get('/:id/permissions')
@Authorized("USERGROUP:GET")
@ResponseSchema(ResponseUserGroupPermissions)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@OnUndefined(UserGroupNotFoundError)
@OpenAPI({ description: 'Lists all permissions granted to the group as permission response objects.' })
async getPermissions(@Param('id') id: number) {
let group = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions', 'permissions.principal'] })
if (!group) { throw new UserGroupNotFoundError(); }
return new ResponseUserGroupPermissions(group);
} }
@Post() @Post()
@ -78,8 +54,7 @@ export class UserGroupController {
throw error; throw error;
} }
userGroup = await this.userGroupsRepository.save(userGroup); return this.userGroupsRepository.save(userGroup);
return (await (this.userGroupsRepository.findOne({ id: userGroup.id }, { relations: ["permissions"] }))).toResponse();
} }
@Put('/:id') @Put('/:id')
@ -88,7 +63,7 @@ export class UserGroupController {
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 }) @ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(UserGroupIdsNotMatchingError, { statusCode: 406 }) @ResponseSchema(UserGroupIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the group whose id you provided. <br> To change the permissions granted to the group please use /api/permissions instead. <br> Please remember that ids can't be changed." }) @OpenAPI({ description: "Update the group whose id you provided. <br> To change the permissions granted to the group please use /api/permissions instead. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) updateGroup: UpdateUserGroup) { async put(@Param('id') id: number, @EntityFromBody() updateGroup: UpdateUserGroup) {
let oldGroup = await this.userGroupsRepository.findOne({ id: id }); let oldGroup = await this.userGroupsRepository.findOne({ id: id });
if (!oldGroup) { if (!oldGroup) {
@ -100,7 +75,7 @@ export class UserGroupController {
} }
await this.userGroupsRepository.save(await updateGroup.update(oldGroup)); await this.userGroupsRepository.save(await updateGroup.update(oldGroup));
return (await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] })).toResponse(); return (await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] })).toResponse();
} }
@Delete('/:id') @Delete('/:id')
@ -110,7 +85,7 @@ export class UserGroupController {
@OnUndefined(204) @OnUndefined(204)
@OpenAPI({ description: 'Delete the group whose id you provided. <br> If there are any permissions directly granted to the group they will get deleted as well. <br> Users associated with this group won\'t get deleted - just deassociated. <br> If no group with this id exists it will just return 204(no content).' }) @OpenAPI({ description: 'Delete the group whose id you provided. <br> If there are any permissions directly granted to the group they will get deleted as well. <br> Users associated with this group won\'t get deleted - just deassociated. <br> If no group with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let group = await this.userGroupsRepository.findOne({ id: id }); let group = await this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
if (!group) { return null; } if (!group) { return null; }
const responseGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] }); const responseGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] });

View File

@ -1,57 +1,24 @@
import { IsString } from 'class-validator'; import { IsString } from 'class-validator';
import { BadRequestError } from 'routing-controllers'; import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/** /**
* Error to throw when an address's postal code fails validation. * Error to throw, when to provided address doesn't belong to the accepted types.
*/ */
export class AddressPostalCodeInvalidError extends BadRequestError { export class AddressWrongTypeError extends NotAcceptableError {
@IsString() @IsString()
name = "AddressPostalCodeInvalidError" name = "AddressWrongTypeError"
@IsString() @IsString()
message = "The postal code you provided is invalid. \n Please check if your postal code follows the postal code validation guidelines." message = "The address must be an existing address's id. \n You provided a object of another type."
} }
/** /**
* Error to throw when an non-empty address's first line isn't set. * Error to throw, when a non-existent address get's loaded.
*/ */
export class AddressFirstLineEmptyError extends BadRequestError { export class AddressNotFoundError extends NotFoundError {
@IsString() @IsString()
name = "AddressFirstLineEmptyError" name = "AddressNotFoundError"
@IsString() @IsString()
message = "You provided a empty first address line. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country" message = "The address you provided couldn't be located in the system. \n Please check your request."
}
/**
* Error to throw when an non-empty address's postal code isn't set.
*/
export class AddressPostalCodeEmptyError extends BadRequestError {
@IsString()
name = "AddressPostalCodeEmptyError"
@IsString()
message = "You provided a empty postal code. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
}
/**
* Error to throw when an non-empty address's city isn't set.
*/
export class AddressCityEmptyError extends BadRequestError {
@IsString()
name = "AddressCityEmptyError"
@IsString()
message = "You provided a empty city. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
}
/**
* Error to throw when an non-empty address's country isn't set.
*/
export class AddressCountryEmptyError extends BadRequestError {
@IsString()
name = "AddressCountryEmptyError"
@IsString()
message = "You provided a empty country. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
} }

View File

@ -1,25 +0,0 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a Donation couldn't be found.
*/
export class DonationNotFoundError extends NotFoundError {
@IsString()
name = "DonationNotFoundError"
@IsString()
message = "Donation not found!"
}
/**
* Error to throw when two Donations' ids don't match.
* Usually occurs when a user tries to change a Donation's id.
*/
export class DonationIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "DonationIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a Donation's id: This isn't allowed!"
}

View File

@ -33,15 +33,4 @@ export class DonorReceiptAddressNeededError extends NotAcceptableError {
@IsString() @IsString()
message = "An address is needed to create a receipt for a donor. \n You didn't provide one." message = "An address is needed to create a receipt for a donor. \n You didn't provide one."
}
/**
* Error to throw when a donor still has donations associated.
*/
export class DonorHasDonationsError extends NotAcceptableError {
@IsString()
name = "DonorHasDonationsError"
@IsString()
message = "This donor still has donations associated with it. \n If you want to delete this donor with all it's donations and teams add `?force` to your query."
} }

View File

@ -2,7 +2,18 @@ import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers'; import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/** /**
* Error to throw, when a non-existent contact get's requested. * Error to throw, when a provided groupContact doesn't belong to the accepted types.
*/
export class GroupContactWrongTypeError extends NotAcceptableError {
@IsString()
name = "GroupContactWrongTypeError"
@IsString()
message = "The groupContact must be an existing groupContact's id. \n You provided a object of another type."
}
/**
* Error to throw, when a non-existent groupContact get's loaded.
*/ */
export class GroupContactNotFoundError extends NotFoundError { export class GroupContactNotFoundError extends NotFoundError {
@IsString() @IsString()
@ -10,16 +21,4 @@ export class GroupContactNotFoundError extends NotFoundError {
@IsString() @IsString()
message = "The groupContact you provided couldn't be located in the system. \n Please check your request." message = "The groupContact you provided couldn't be located in the system. \n Please check your request."
} }
/**
* Error to throw when two contacts' ids don't match.
* Usually occurs when a user tries to change a contact's id.
*/
export class GroupContactIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "GroupContactIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a contact's id: This isn't allowed!"
}

View File

@ -1,17 +0,0 @@
import { IsString } from 'class-validator';
import { InternalServerError } from 'routing-controllers';
/**
* Error to throw when a permission couldn't be found.
*/
export class MailSendingError extends InternalServerError {
@IsString()
name = "MailSendingError"
@IsString()
message = "We had a problem sending the mail!"
constructor() {
super("We had a problem sending the mail!");
}
}

View File

@ -32,38 +32,5 @@ export class RunnerGroupNeededError extends NotAcceptableError {
name = "RunnerGroupNeededError" name = "RunnerGroupNeededError"
@IsString() @IsString()
message = "Runner's need to be part of one group (team or organization)! \n You provided neither." message = "Runner's need to be part of one group (team or organisation)! \n You provided neither."
}
/**
* Error to throw when a citizen runner has no mail-address.
*/
export class RunnerEmailNeededError extends NotAcceptableError {
@IsString()
name = "RunnerEmailNeededError"
@IsString()
message = "Citizenrunners have to provide an email address for verification and contacting."
}
/**
* Error to throw when a runner already requested a new selfservice link in the last 24hrs.
*/
export class RunnerSelfserviceTimeoutError extends NotAcceptableError {
@IsString()
name = "RunnerSelfserviceTimeoutError"
@IsString()
message = "You can only reqest a new token every 24hrs."
}
/**
* Error to throw when a runner still has distance donations associated.
*/
export class RunnerHasDistanceDonationsError extends NotAcceptableError {
@IsString()
name = "RunnerHasDistanceDonationsError"
@IsString()
message = "This runner still has distance donations associated with it. \n If you want to delete this runner with all it's donations and teams add `?force` to your query."
} }

View File

@ -0,0 +1,58 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a runner organisation couldn't be found.
*/
export class RunnerOrganisationNotFoundError extends NotFoundError {
@IsString()
name = "RunnerOrganisationNotFoundError"
@IsString()
message = "RunnerOrganisation not found!"
}
/**
* Error to throw when two runner organisation's ids don't match.
* Usually occurs when a user tries to change a runner organisation's id.
*/
export class RunnerOrganisationIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "RunnerOrganisationIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a runner organisation's id: This isn't allowed!"
}
/**
* Error to throw when a organisation still has runners associated.
*/
export class RunnerOrganisationHasRunnersError extends NotAcceptableError {
@IsString()
name = "RunnerOrganisationHasRunnersError"
@IsString()
message = "This organisation still has runners associated with it. \n If you want to delete this organisation with all it's runners and teams add `?force` to your query."
}
/**
* Error to throw when a organisation still has teams associated.
*/
export class RunnerOrganisationHasTeamsError extends NotAcceptableError {
@IsString()
name = "RunnerOrganisationHasTeamsError"
@IsString()
message = "This organisation still has teams associated with it. \n If you want to delete this organisation with all it's runners and teams add `?force` to your query."
}
/**
* Error to throw, when a provided runnerOrganisation doesn't belong to the accepted types.
*/
export class RunnerOrganisationWrongTypeError extends NotAcceptableError {
@IsString()
name = "RunnerOrganisationWrongTypeError"
@IsString()
message = "The runner organisation must be an existing organisation's id. \n You provided a object of another type."
}

View File

@ -1,58 +0,0 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a runner organization couldn't be found.
*/
export class RunnerOrganizationNotFoundError extends NotFoundError {
@IsString()
name = "RunnerOrganizationNotFoundError"
@IsString()
message = "RunnerOrganization not found!"
}
/**
* Error to throw when two runner organization's ids don't match.
* Usually occurs when a user tries to change a runner organization's id.
*/
export class RunnerOrganizationIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "RunnerOrganizationIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a runner organization's id: This isn't allowed!"
}
/**
* Error to throw when a organization still has runners associated.
*/
export class RunnerOrganizationHasRunnersError extends NotAcceptableError {
@IsString()
name = "RunnerOrganizationHasRunnersError"
@IsString()
message = "This organization still has runners associated with it. \n If you want to delete this organization with all it's runners and teams add `?force` to your query."
}
/**
* Error to throw when a organization still has teams associated.
*/
export class RunnerOrganizationHasTeamsError extends NotAcceptableError {
@IsString()
name = "RunnerOrganizationHasTeamsError"
@IsString()
message = "This organization still has teams associated with it. \n If you want to delete this organization with all it's runners and teams add `?force` to your query."
}
/**
* Error to throw, when a provided runnerOrganization doesn't belong to the accepted types.
*/
export class RunnerOrganizationWrongTypeError extends NotAcceptableError {
@IsString()
name = "RunnerOrganizationWrongTypeError"
@IsString()
message = "The runner organization must be an existing organization's id. \n You provided a object of another type."
}

View File

@ -43,5 +43,5 @@ export class RunnerTeamNeedsParentError extends NotAcceptableError {
name = "RunnerTeamNeedsParentError" name = "RunnerTeamNeedsParentError"
@IsString() @IsString()
message = "You provided no runner organization as this team's parent group." message = "You provided no runner organisation as this team's parent group."
} }

View File

@ -4,7 +4,7 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/** /**
* Error to throw when no username or email is set. * Error to throw when no username or email is set.
* We somehow need to identify you on login. * We somehow need to identify you :)
*/ */
export class UsernameOrEmailNeededError extends NotFoundError { export class UsernameOrEmailNeededError extends NotFoundError {
@IsString() @IsString()
@ -14,30 +14,6 @@ export class UsernameOrEmailNeededError extends NotFoundError {
message = "No username or email is set!" message = "No username or email is set!"
} }
/**
* Error to throw when no username contains illegal characters.
* Right now the only one is "@" but this could change in the future.
*/
export class UsernameContainsIllegalCharacterError extends NotAcceptableError {
@IsString()
name = "UsernameContainsIllegalCharacterError"
@IsString()
message = "The provided username contains illegal characters! \n Right now the following characters are considered illegal: '@'"
}
/**
* Error to throw when no email is set.
* We somehow need to identify you :)
*/
export class UserEmailNeededError extends NotFoundError {
@IsString()
name = "UserEmailNeededError"
@IsString()
message = "No email is set! \n You have to provide email addresses for users (used for password reset among others)."
}
/** /**
* Error to throw when a user couldn't be found. * Error to throw when a user couldn't be found.
*/ */
@ -59,45 +35,4 @@ export class UserIdsNotMatchingError extends NotAcceptableError {
@IsString() @IsString()
message = "The ids don't match!! \n And if you wanted to change a user's id: This isn't allowed!" message = "The ids don't match!! \n And if you wanted to change a user's id: This isn't allowed!"
}
/**
* Error to throw when two users' ids don't match.
* Usually occurs when a user tries to change a user's id.
*/
export class UserDeletionNotConfirmedError extends NotAcceptableError {
@IsString()
name = "UserDeletionNotConfirmedError"
@IsString()
message = "You are trying to delete a user! \n If you're sure about doing this: provide the ?force=true query param."
}
export class PasswordMustContainUppercaseLetterError extends NotAcceptableError {
@IsString()
name = "PasswordMustContainUppercaseLetterError"
@IsString()
message = "Passwords must contain at least one uppercase letter."
}
export class PasswordMustContainLowercaseLetterError extends NotAcceptableError {
@IsString()
name = "PasswordMustContainLowercaseLetterError"
@IsString()
message = "Passwords must contain at least one lowercase letter."
}
export class PasswordMustContainNumberError extends NotAcceptableError {
@IsString()
name = "PasswordMustContainNumberError"
@IsString()
message = "Passwords must contain at least one number."
}
export class PasswordTooShortError extends NotAcceptableError {
@IsString()
name = "PasswordTooShortError"
@IsString()
message = "Passwords must be at least ten characters long."
} }

View File

@ -1,7 +1,6 @@
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import * as jsonwebtoken from "jsonwebtoken"; import * as jsonwebtoken from "jsonwebtoken";
import { config } from './config'; import { config } from './config';
import { Runner } from './models/entities/Runner';
import { User } from './models/entities/User'; import { User } from './models/entities/User';
/** /**
@ -35,19 +34,6 @@ export class JwtCreator {
}, config.jwt_secret) }, config.jwt_secret)
} }
/**
* Creates a new selfservice token for a given runner.
* @param runner Runner entity that the access token shall be created for.
* @param expiry_timestamp Timestamp for the token expiry. Will be set about 9999 years if none provided.
*/
public static createSelfService(runner: Runner, expiry_timestamp?: number) {
if (!expiry_timestamp) { expiry_timestamp = Math.floor(Date.now() / 1000) + 36000 * 60 * 24 * 365 * 9999; }
return jsonwebtoken.sign({
id: runner.id,
exp: expiry_timestamp
}, config.jwt_secret)
}
/** /**
* Creates a new password reset token for a given user. * Creates a new password reset token for a given user.
* The token is valid for 15 minutes or 1 use - whatever comes first. * The token is valid for 15 minutes or 1 use - whatever comes first.

View File

@ -1,9 +1,6 @@
import { createConnection } from "typeorm"; import { createConnection } from "typeorm";
import { runSeeder } from 'typeorm-seeding'; import { runSeeder } from 'typeorm-seeding';
import { config } from '../config'; import { User } from '../models/entities/User';
import { ConfigFlag } from '../models/entities/ConfigFlags';
import SeedPublicOrg from '../seeds/SeedPublicOrg';
import SeedTestRunners from '../seeds/SeedTestRunners';
import SeedUsers from '../seeds/SeedUsers'; import SeedUsers from '../seeds/SeedUsers';
/** /**
* Loader for the database that creates the database connection and initializes the database tabels. * Loader for the database that creates the database connection and initializes the database tabels.
@ -12,20 +9,8 @@ import SeedUsers from '../seeds/SeedUsers';
export default async () => { export default async () => {
const connection = await createConnection(); const connection = await createConnection();
await connection.synchronize(); await connection.synchronize();
if (await connection.getRepository(User).count() === 0) {
//The data seeding part
if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:user", value: "true" }))) {
await runSeeder(SeedUsers); await runSeeder(SeedUsers);
await connection.getRepository(ConfigFlag).save({ option: "seeded:user", value: "true" });
} }
if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:citizenorg", value: "true" }))) {
await runSeeder(SeedPublicOrg);
await connection.getRepository(ConfigFlag).save({ option: "seeded:citizenorg", value: "true" });
}
if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:testdata", value: "true" })) && config.seedTestData == true) {
await runSeeder(SeedTestRunners);
await connection.getRepository(ConfigFlag).save({ option: "seeded:testdata", value: "true" });
}
return connection; return connection;
}; };

View File

@ -1,4 +1,4 @@
import { validationMetadatasToSchemas } from "@odit/class-validator-jsonschema"; import { validationMetadatasToSchemas } from "class-validator-jsonschema";
import express, { Application } from "express"; import express, { Application } from "express";
import path from 'path'; import path from 'path';
import { getMetadataArgsStorage } from "routing-controllers"; import { getMetadataArgsStorage } from "routing-controllers";

View File

@ -1,64 +0,0 @@
import axios from 'axios';
import { config } from './config';
import { MailSendingError } from './errors/MailErrors';
/**
* This class is responsible for all things mail sending.
* This uses axios to communicate with the mailer api (https://git.odit.services/lfk/mailer).
*/
export class Mailer {
public static base: string = config.mailer_url;
public static key: string = config.mailer_key;
public static testing: boolean = config.testing;
/**
* Function for sending a password reset mail.
* @param to_address The address the mail will be sent to. Should always get pulled from a user object.
* @param token The requested password reset token - will be combined with the app_url to generate a password reset link.
*/
public static async sendResetMail(to_address: string, token: string, locale: string = "en") {
try {
await axios.post(`${Mailer.base}/reset?locale=${locale}&key=${Mailer.key}`, {
address: to_address,
resetKey: token
});
} catch (error) {
if (Mailer.testing) { return true; }
throw new MailSendingError();
}
}
/**
* Function for sending a runner selfservice welcome mail.
* @param to_address The address the mail will be sent to. Should always get pulled from a runner object.
* @param token The requested selfservice token - will be combined with the app_url to generate a selfservice profile link.
*/
public static async sendSelfserviceWelcomeMail(to_address: string, token: string, locale: string = "en") {
try {
await axios.post(`${Mailer.base}/registration?locale=${locale}&key=${Mailer.key}`, {
address: to_address,
selfserviceToken: token
});
} catch (error) {
if (Mailer.testing) { return true; }
throw new MailSendingError();
}
}
/**
* Function for sending a runner selfservice link forgotten mail.
* @param to_address The address the mail will be sent to. Should always get pulled from a runner object.
* @param token The requested selfservice token - will be combined with the app_url to generate a selfservice profile link.
*/
public static async sendSelfserviceForgottenMail(to_address: string, token: string, locale: string = "en") {
try {
await axios.post(`${Mailer.base}/registration_forgot?locale=${locale}&key=${Mailer.key}`, {
address: to_address,
selfserviceToken: token
});
} catch (error) {
if (Mailer.testing) { return true; }
throw new MailSendingError();
}
}
}

View File

@ -15,14 +15,14 @@ import authchecker from './authchecker';
const ScanAuth = async (req: Request, res: Response, next: () => void) => { const ScanAuth = async (req: Request, res: Response, next: () => void) => {
let provided_token: string = req.headers["authorization"]; let provided_token: string = req.headers["authorization"];
if (provided_token == "" || provided_token === undefined || provided_token === null) { if (provided_token == "" || provided_token === undefined || provided_token === null) {
res.status(401).send({ http_code: 401, short: "no_token", message: "No api token provided." }); res.status(401).send("No api token provided.");
return; return;
} }
try { try {
provided_token = provided_token.replace("Bearer ", ""); provided_token = provided_token.replace("Bearer ", "");
} catch (error) { } catch (error) {
res.status(401).send({ http_code: 401, short: "no_token", message: "No valid jwt or api token provided." }); res.status(401).send("No valid jwt or api token provided.");
return; return;
} }
@ -32,7 +32,7 @@ const ScanAuth = async (req: Request, res: Response, next: () => void) => {
} }
finally { finally {
if (prefix == "" || prefix == undefined || prefix == null) { if (prefix == "" || prefix == undefined || prefix == null) {
res.status(401).send({ http_code: 401, short: "invalid_token", message: "Api token non-existent or invalid syntax." }); res.status(401).send("Api token non-existent or invalid syntax.");
return; return;
} }
} }
@ -46,7 +46,7 @@ const ScanAuth = async (req: Request, res: Response, next: () => void) => {
} }
finally { finally {
if (user_authorized == false) { if (user_authorized == false) {
res.status(401).send({ http_code: 401, short: "invalid_token", message: "Api token non-existent or invalid syntax." }); res.status(401).send("Api token non-existent or invalid syntax.");
return; return;
} }
else { else {
@ -56,13 +56,13 @@ const ScanAuth = async (req: Request, res: Response, next: () => void) => {
} }
else { else {
if (station.enabled == false) { if (station.enabled == false) {
res.status(401).send({ http_code: 401, short: "station_disabled", message: "Station is disabled." }); res.status(401).send("Station disabled.");
} }
if (!(await argon2.verify(station.key, provided_token))) { if (!(await argon2.verify(station.key, provided_token))) {
res.status(401).send({ http_code: 401, short: "invalid_token", message: "Api token non-existent or invalid syntax." }); res.status(401).send("Api token invalid.");
return; return;
} }
req.headers["station_id"] = station.id.toString();
next(); next();
} }
} }

View File

@ -42,7 +42,7 @@ const StatsAuth = async (req: Request, res: Response, next: () => void) => {
let user_authorized = false; let user_authorized = false;
try { try {
let action = { request: req, response: res, context: null, next: next } let action = { request: req, response: res, context: null, next: next }
user_authorized = await authchecker(action, ["RUNNER:GET", "TEAM:GET", "ORGANIZATION:GET"]); user_authorized = await authchecker(action, ["RUNNER:GET", "TEAM:GET", "ORGANISATION:GET"]);
} }
finally { finally {
if (user_authorized == false) { if (user_authorized == false) {

View File

@ -1,58 +0,0 @@
import cookie from "cookie";
import * as jwt from "jsonwebtoken";
import { Action } from 'routing-controllers';
import { getConnectionManager } from 'typeorm';
import { config } from '../config';
import { IllegalJWTError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from '../errors/AuthError';
import { JwtCreator, JwtUser } from '../jwtcreator';
import { User } from '../models/entities/User';
/**
* TODO:
*/
const UserChecker = async (action: Action) => {
let jwtPayload = undefined
try {
let provided_token = "" + action.request.headers["authorization"].replace("Bearer ", "");
jwtPayload = <any>jwt.verify(provided_token, config.jwt_secret);
jwtPayload = jwtPayload["userdetails"];
} catch (error) {
jwtPayload = await refresh(action);
}
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] })
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
if (user.enabled == false) { throw new UserDisabledError(); }
return user;
};
/**
* Handles soft-refreshing of access-tokens.
* @param action Routing-Controllers action object that provides request and response objects among other stuff.
*/
const refresh = async (action: Action) => {
let refresh_token = undefined;
try {
refresh_token = cookie.parse(action.request.headers["cookie"])["lfk_backend__refresh_token"];
}
catch {
throw new IllegalJWTError();
}
let jwtPayload = undefined;
try {
jwtPayload = <any>jwt.verify(refresh_token, config.jwt_secret);
} catch (error) {
throw new IllegalJWTError();
}
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions', 'groups', 'groups.permissions'] })
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
if (user.enabled == false) { throw new UserDisabledError(); }
let newAccess = JwtCreator.createAccess(user);
action.response.header("authorization", "Bearer " + newAccess);
return await new JwtUser(user);
}
export default UserChecker;

View File

@ -1,9 +1,9 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm'; import { getConnectionManager } from 'typeorm';
import { RunnerGroupNeededError } from '../../errors/RunnerErrors'; import { RunnerGroupNeededError } from '../../errors/RunnerErrors';
import { RunnerOrganizationNotFoundError } from '../../errors/RunnerOrganizationErrors'; import { RunnerOrganisationNotFoundError } from '../../errors/RunnerOrganisationErrors';
import { RunnerGroup } from '../entities/RunnerGroup'; import { RunnerGroup } from '../entities/RunnerGroup';
import { RunnerOrganization } from '../entities/RunnerOrganization'; import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { RunnerTeam } from '../entities/RunnerTeam'; import { RunnerTeam } from '../entities/RunnerTeam';
import { CreateRunner } from './create/CreateRunner'; import { CreateRunner } from './create/CreateRunner';
@ -78,9 +78,9 @@ export class ImportRunner {
let team = await getConnectionManager().get().getRepository(RunnerTeam).findOne({ id: groupID }); let team = await getConnectionManager().get().getRepository(RunnerTeam).findOne({ id: groupID });
if (team) { return team; } if (team) { return team; }
let org = await getConnectionManager().get().getRepository(RunnerOrganization).findOne({ id: groupID }); let org = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: groupID });
if (!org) { if (!org) {
throw new RunnerOrganizationNotFoundError(); throw new RunnerOrganisationNotFoundError();
} }
if (this.team === undefined) { return org; } if (this.team === undefined) { return org; }

View File

@ -0,0 +1,70 @@
import { IsNotEmpty, IsOptional, IsPostalCode, IsString } from 'class-validator';
import { config } from '../../../config';
import { Address } from '../../entities/Address';
/**
* This classed is used to create a new Address entity from a json body (post request).
*/
export class CreateAddress {
/**
* The newaddress's description.
*/
@IsString()
@IsOptional()
description?: string;
/**
* The new address's first line.
* Containing the street and house number.
*/
@IsString()
@IsNotEmpty()
address1: string;
/**
* The new address's second line.
* Containing optional information.
*/
@IsString()
@IsOptional()
address2?: string;
/**
* The new address's postal code.
* This will get checked against the postal code syntax for the configured country.
* TODO: Implement the config option.
*/
@IsString()
@IsNotEmpty()
@IsPostalCode(config.postalcode_validation_countrycode)
postalcode: string;
/**
* The new address's city.
*/
@IsString()
@IsNotEmpty()
city: string;
/**
* The new address's country.
*/
@IsString()
@IsNotEmpty()
country: string;
/**
* Creates a new Address entity from this.
*/
public async toEntity(): Promise<Address> {
let newAddress: Address = new Address();
newAddress.address1 = this.address1;
newAddress.address2 = this.address2;
newAddress.postalcode = this.postalcode;
newAddress.city = this.city;
newAddress.country = this.country;
return newAddress;
}
}

View File

@ -1,53 +0,0 @@
import { IsInt, IsPositive } from 'class-validator';
import { getConnection } from 'typeorm';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { DistanceDonation } from '../../entities/DistanceDonation';
import { Runner } from '../../entities/Runner';
import { CreateDonation } from './CreateDonation';
/**
* This class is used to create a new FixedDonation entity from a json body (post request).
*/
export class CreateDistanceDonation extends CreateDonation {
/**
* The donation's associated runner's id.
* This is important to link the runner's distance ran to the donation.
*/
@IsInt()
@IsPositive()
runner: number;
/**
* The donation's amount per distance (full kilometer aka 1000 meters).
* The unit is your currency's smallest unit (default: euro cent).
*/
@IsInt()
@IsPositive()
amountPerDistance: number;
/**
* Creates a new FixedDonation entity from this.
*/
public async toEntity(): Promise<DistanceDonation> {
let newDonation = new DistanceDonation;
newDonation.amountPerDistance = this.amountPerDistance;
newDonation.paidAmount = this.paidAmount;
newDonation.donor = await this.getDonor();
newDonation.runner = await this.getRunner();
return newDonation;
}
/**
* Gets a runner based on the runner id provided via this.runner.
*/
public async getRunner(): Promise<Runner> {
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
if (!runner) {
throw new RunnerNotFoundError();
}
return runner;
}
}

View File

@ -1,41 +0,0 @@
import { IsInt, IsOptional, IsPositive } from 'class-validator';
import { getConnection } from 'typeorm';
import { DonorNotFoundError } from '../../../errors/DonorErrors';
import { Donation } from '../../entities/Donation';
import { Donor } from '../../entities/Donor';
/**
* This class is used to create a new Donation entity from a json body (post request).
*/
export abstract class CreateDonation {
/**
* The donation's associated donor's id.
* This is important to link donations to donors.
*/
@IsInt()
@IsPositive()
donor: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
@IsOptional()
paidAmount?: number;
/**
* Creates a new Donation entity from this.
*/
public abstract toEntity(): Promise<Donation>;
/**
* Gets a donor based on the donor id provided via this.donor.
*/
public async getDonor(): Promise<Donor> {
const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor });
if (!donor) {
throw new DonorNotFoundError();
}
return donor;
}
}

View File

@ -1,6 +1,5 @@
import { IsBoolean, IsOptional } from 'class-validator'; import { IsBoolean, IsOptional } from 'class-validator';
import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors'; import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors';
import { Address } from '../../entities/Address';
import { Donor } from '../../entities/Donor'; import { Donor } from '../../entities/Donor';
import { CreateParticipant } from './CreateParticipant'; import { CreateParticipant } from './CreateParticipant';
@ -27,10 +26,10 @@ export class CreateDonor extends CreateParticipant {
newDonor.lastname = this.lastname; newDonor.lastname = this.lastname;
newDonor.phone = this.phone; newDonor.phone = this.phone;
newDonor.email = this.email; newDonor.email = this.email;
newDonor.address = await this.getAddress();
newDonor.receiptNeeded = this.receiptNeeded; newDonor.receiptNeeded = this.receiptNeeded;
newDonor.address = this.address;
Address.validate(newDonor.address); if (this.receiptNeeded == true && this.address == null) {
if (this.receiptNeeded == true && Address.isValidAddress(newDonor.address) == false) {
throw new DonorReceiptAddressNeededError() throw new DonorReceiptAddressNeededError()
} }

View File

@ -1,29 +0,0 @@
import { IsInt, IsPositive } from 'class-validator';
import { FixedDonation } from '../../entities/FixedDonation';
import { CreateDonation } from './CreateDonation';
/**
* This class is used to create a new FixedDonation entity from a json body (post request).
*/
export class CreateFixedDonation extends CreateDonation {
/**
* The donation's amount.
* The unit is your currency's smallest unit (default: euro cent).
*/
@IsInt()
@IsPositive()
amount: number;
/**
* Creates a new FixedDonation entity from this.
*/
public async toEntity(): Promise<FixedDonation> {
let newDonation = new FixedDonation;
newDonation.amount = this.amount;
newDonation.paidAmount = this.paidAmount;
newDonation.donor = await this.getDonor();
return newDonation;
}
}

View File

@ -1,13 +1,12 @@
import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator'; import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm'; import { getConnectionManager } from 'typeorm';
import { config } from '../../../config'; import { config } from '../../../config';
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors'; import { AddressNotFoundError, AddressWrongTypeError } from '../../../errors/AddressErrors';
import { Address } from '../../entities/Address'; import { Address } from '../../entities/Address';
import { GroupContact } from '../../entities/GroupContact'; import { GroupContact } from '../../entities/GroupContact';
import { RunnerGroup } from '../../entities/RunnerGroup';
/** /**
* This classed is used to create a new GroupContact entity from a json body (post request). * This classed is used to create a new Group entity from a json body (post request).
*/ */
export class CreateGroupContact { export class CreateGroupContact {
/** /**
@ -33,10 +32,11 @@ export class CreateGroupContact {
/** /**
* The new contact's address. * The new contact's address.
* Must be the address's id.
*/ */
@IsInt()
@IsOptional() @IsOptional()
@IsObject() address?: number;
address?: Address;
/** /**
* The contact's phone number. * The contact's phone number.
@ -47,51 +47,39 @@ export class CreateGroupContact {
phone?: string; phone?: string;
/** /**
* The new contact's email address. * The contact's email address.
*/ */
@IsOptional() @IsOptional()
@IsEmail() @IsEmail()
email?: string; email?: string;
/** /**
* The new contacts's groups' ids. * Gets the new contact's address by it's id.
* You can provide either one groupId or an array of groupIDs.
*/ */
@IsOptional() public async getAddress(): Promise<Address> {
groups?: number[] | number if (this.address === undefined || this.address === null) {
return null;
/**
* Get's all groups for this contact by their id's;
*/
public async getGroups(): Promise<RunnerGroup[]> {
if (!this.groups) { return null; }
let groups = new Array<RunnerGroup>();
if (!Array.isArray(this.groups)) {
this.groups = [this.groups]
} }
for (let group of this.groups) { if (!isNaN(this.address)) {
let found = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: group }); let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
if (!found) { throw new RunnerGroupNotFoundError(); } if (!address) { throw new AddressNotFoundError; }
groups.push(found); return address;
} }
return groups;
throw new AddressWrongTypeError;
} }
/** /**
* Creates a new GroupContact entity from this. * Creates a new Address entity from this.
*/ */
public async toEntity(): Promise<GroupContact> { public async toEntity(): Promise<GroupContact> {
let newContact: GroupContact = new GroupContact(); let contact: GroupContact = new GroupContact();
newContact.firstname = this.firstname; contact.firstname = this.firstname;
newContact.middlename = this.middlename; contact.middlename = this.middlename;
newContact.lastname = this.lastname; contact.lastname = this.lastname;
newContact.email = this.email; contact.email = this.email;
newContact.phone = this.phone; contact.phone = this.phone;
newContact.address = this.address; contact.address = await this.getAddress();
Address.validate(newContact.address); return null;
newContact.groups = await this.getGroups();
return newContact;
} }
} }

View File

@ -1,5 +1,7 @@
import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator'; import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { config } from '../../../config'; import { config } from '../../../config';
import { AddressNotFoundError, AddressWrongTypeError } from '../../../errors/AddressErrors';
import { Address } from '../../entities/Address'; import { Address } from '../../entities/Address';
/** /**
@ -46,8 +48,25 @@ export abstract class CreateParticipant {
/** /**
* The new participant's address. * The new participant's address.
* Must be of type number (address id).
*/ */
@IsInt()
@IsOptional() @IsOptional()
@IsObject() address?: number;
address?: Address;
/**
* Gets the new participant's address by it's address.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined || this.address === null) {
return null;
}
if (!isNaN(this.address)) {
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
if (!address) { throw new AddressNotFoundError; }
return address;
}
throw new AddressWrongTypeError;
}
} }

View File

@ -1,33 +1,39 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; import { IsEmail, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm'; import { getConnectionManager } from 'typeorm';
import { ResetAlreadyRequestedError, UserDisabledError, UserNotFoundError } from '../../../errors/AuthError'; import { ResetAlreadyRequestedError, UserDisabledError, UserNotFoundError } from '../../../errors/AuthError';
import { UserEmailNeededError } from '../../../errors/UserErrors'; import { UsernameOrEmailNeededError } from '../../../errors/UserErrors';
import { JwtCreator } from '../../../jwtcreator'; import { JwtCreator } from '../../../jwtcreator';
import { User } from '../../entities/User'; import { User } from '../../entities/User';
/** /**
* This class is used to create password reset tokens for users. * This calss is used to create password reset tokens for users.
* These password reset token can be used to set a new password for the user for the next 15mins. * These password reset token can be used to set a new password for the user for the next 15mins.
*/ */
export class CreateResetToken { export class CreateResetToken {
/**
* The username of the user that wants to reset their password.
*/
@IsOptional()
@IsString()
username?: string;
/** /**
* The email address of the user that wants to reset their password. * The email address of the user that wants to reset their password.
*/ */
@IsNotEmpty() @IsOptional()
@IsEmail() @IsEmail()
@IsString() @IsString()
email: string; email?: string;
/** /**
* Create a password reset token based on this. * Create a password reset token based on this.
*/ */
public async toResetToken(): Promise<string> { public async toResetToken(): Promise<any> {
if (!this.email) { if (this.email === undefined && this.username === undefined) {
throw new UserEmailNeededError(); throw new UsernameOrEmailNeededError();
} }
let found_user = await getConnectionManager().get().getRepository(User).findOne({ where: [{ email: this.email }] }); let found_user = await getConnectionManager().get().getRepository(User).findOne({ where: [{ username: this.username }, { email: this.email }] });
if (!found_user) { throw new UserNotFoundError(); } if (!found_user) { throw new UserNotFoundError(); }
if (found_user.enabled == false) { throw new UserDisabledError(); } if (found_user.enabled == false) { throw new UserDisabledError(); }
if (found_user.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 15 * 60)) { throw new ResetAlreadyRequestedError(); } if (found_user.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 15 * 60)) { throw new ResetAlreadyRequestedError(); }
@ -37,7 +43,7 @@ export class CreateResetToken {
await getConnectionManager().get().getRepository(User).save(found_user); await getConnectionManager().get().getRepository(User).save(found_user);
//Create the reset token //Create the reset token
let reset_token: string = JwtCreator.createReset(found_user); let reset_token = JwtCreator.createReset(found_user);
return reset_token; return reset_token;
} }

View File

@ -1,9 +1,8 @@
import { IsInt } from 'class-validator'; import { IsInt } from 'class-validator';
import { getConnectionManager } from 'typeorm'; import { getConnectionManager } from 'typeorm';
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors'; import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
import { RunnerOrganizationWrongTypeError } from '../../../errors/RunnerOrganizationErrors'; import { RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors'; import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
import { Address } from '../../entities/Address';
import { Runner } from '../../entities/Runner'; import { Runner } from '../../entities/Runner';
import { RunnerGroup } from '../../entities/RunnerGroup'; import { RunnerGroup } from '../../entities/RunnerGroup';
import { CreateParticipant } from './CreateParticipant'; import { CreateParticipant } from './CreateParticipant';
@ -31,8 +30,7 @@ export class CreateRunner extends CreateParticipant {
newRunner.phone = this.phone; newRunner.phone = this.phone;
newRunner.email = this.email; newRunner.email = this.email;
newRunner.group = await this.getGroup(); newRunner.group = await this.getGroup();
newRunner.address = this.address; newRunner.address = await this.getAddress();
Address.validate(newRunner.address);
return newRunner; return newRunner;
} }
@ -50,6 +48,6 @@ export class CreateRunner extends CreateParticipant {
return group; return group;
} }
throw new RunnerOrganizationWrongTypeError; throw new RunnerOrganisationWrongTypeError;
} }
} }

View File

@ -9,7 +9,7 @@ import { RunnerCard } from '../../entities/RunnerCard';
*/ */
export class CreateRunnerCard { export class CreateRunnerCard {
/** /**
* The card's associated runner's id. * The card's associated runner.
*/ */
@IsInt() @IsInt()
@IsOptional() @IsOptional()

View File

@ -1,6 +1,6 @@
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm'; import { getConnectionManager } from 'typeorm';
import { GroupContactNotFoundError } from '../../../errors/GroupContactErrors'; import { GroupContactNotFoundError, GroupContactWrongTypeError } from '../../../errors/GroupContactErrors';
import { GroupContact } from '../../entities/GroupContact'; import { GroupContact } from '../../entities/GroupContact';
/** /**
@ -15,7 +15,7 @@ export abstract class CreateRunnerGroup {
name: string; name: string;
/** /**
* The new group's contact's id. * The new group's contact.
* Optional * Optional
*/ */
@IsInt() @IsInt()
@ -26,10 +26,15 @@ export abstract class CreateRunnerGroup {
* Gets the new group's contact by it's id. * Gets the new group's contact by it's id.
*/ */
public async getContact(): Promise<GroupContact> { public async getContact(): Promise<GroupContact> {
if (!this.contact) { return null; } if (this.contact === undefined || this.contact === null) {
let contact = await getConnectionManager().get().getRepository(GroupContact).findOne({ id: this.contact }); return null;
if (!contact) { throw new GroupContactNotFoundError; } }
return contact; if (!isNaN(this.contact)) {
let contact = await getConnectionManager().get().getRepository(GroupContact).findOne({ id: this.contact });
if (!contact) { throw new GroupContactNotFoundError; }
return contact;
}
throw new GroupContactWrongTypeError;
} }
} }

View File

@ -0,0 +1,48 @@
import { IsInt, IsOptional } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { AddressNotFoundError, AddressWrongTypeError } from '../../../errors/AddressErrors';
import { Address } from '../../entities/Address';
import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
import { CreateRunnerGroup } from './CreateRunnerGroup';
/**
* This classed is used to create a new RunnerOrganisation entity from a json body (post request).
*/
export class CreateRunnerOrganisation extends CreateRunnerGroup {
/**
* The new organisation's address.
* Must be of type number (address id).
*/
@IsInt()
@IsOptional()
address?: number;
/**
* Gets the org's address by it's id.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined || this.address === null) {
return null;
}
if (!isNaN(this.address)) {
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
if (!address) { throw new AddressNotFoundError; }
return address;
}
throw new AddressWrongTypeError;
}
/**
* Creates a new RunnerOrganisation entity from this.
*/
public async toEntity(): Promise<RunnerOrganisation> {
let newRunnerOrganisation: RunnerOrganisation = new RunnerOrganisation();
newRunnerOrganisation.name = this.name;
newRunnerOrganisation.contact = await this.getContact();
// newRunnerOrganisation.address = await this.getAddress();
return newRunnerOrganisation;
}
}

View File

@ -1,43 +0,0 @@
import { IsBoolean, IsObject, IsOptional } from 'class-validator';
import * as uuid from 'uuid';
import { Address } from '../../entities/Address';
import { RunnerOrganization } from '../../entities/RunnerOrganization';
import { CreateRunnerGroup } from './CreateRunnerGroup';
/**
* This classed is used to create a new RunnerOrganization entity from a json body (post request).
*/
export class CreateRunnerOrganization extends CreateRunnerGroup {
/**
* The new organization's address.
*/
@IsOptional()
@IsObject()
address?: Address;
/**
* Is registration enabled for the new organization?
*/
@IsOptional()
@IsBoolean()
registrationEnabled?: boolean = false;
/**
* Creates a new RunnerOrganization entity from this.
*/
public async toEntity(): Promise<RunnerOrganization> {
let newRunnerOrganization: RunnerOrganization = new RunnerOrganization();
newRunnerOrganization.name = this.name;
newRunnerOrganization.contact = await this.getContact();
newRunnerOrganization.address = this.address;
Address.validate(newRunnerOrganization.address);
if (this.registrationEnabled) {
newRunnerOrganization.key = uuid.v4().toUpperCase();
}
return newRunnerOrganization;
}
}

View File

@ -1,8 +1,8 @@
import { IsInt, IsNotEmpty } from 'class-validator'; import { IsInt, IsNotEmpty } from 'class-validator';
import { getConnectionManager } from 'typeorm'; import { getConnectionManager } from 'typeorm';
import { RunnerOrganizationNotFoundError } from '../../../errors/RunnerOrganizationErrors'; import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors'; import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
import { RunnerOrganization } from '../../entities/RunnerOrganization'; import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
import { RunnerTeam } from '../../entities/RunnerTeam'; import { RunnerTeam } from '../../entities/RunnerTeam';
import { CreateRunnerGroup } from './CreateRunnerGroup'; import { CreateRunnerGroup } from './CreateRunnerGroup';
@ -12,7 +12,7 @@ import { CreateRunnerGroup } from './CreateRunnerGroup';
export class CreateRunnerTeam extends CreateRunnerGroup { export class CreateRunnerTeam extends CreateRunnerGroup {
/** /**
* The new team's parent org's id. * The new team's parent group (organisation).
*/ */
@IsInt() @IsInt()
@IsNotEmpty() @IsNotEmpty()
@ -21,13 +21,17 @@ export class CreateRunnerTeam extends CreateRunnerGroup {
/** /**
* Gets the new team's parent org based on it's id. * Gets the new team's parent org based on it's id.
*/ */
public async getParent(): Promise<RunnerOrganization> { public async getParent(): Promise<RunnerOrganisation> {
if (this.parentGroup === undefined || this.parentGroup === null) { if (this.parentGroup === undefined || this.parentGroup === null) {
throw new RunnerTeamNeedsParentError(); throw new RunnerTeamNeedsParentError();
} }
let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganization).findOne({ id: this.parentGroup }); if (!isNaN(this.parentGroup)) {
if (!parentGroup) { throw new RunnerOrganizationNotFoundError();; } let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: this.parentGroup });
return parentGroup; if (!parentGroup) { throw new RunnerOrganisationNotFoundError();; }
return parentGroup;
}
throw new RunnerOrganisationWrongTypeError;
} }
/** /**
@ -38,6 +42,7 @@ export class CreateRunnerTeam extends CreateRunnerGroup {
newRunnerTeam.name = this.name; newRunnerTeam.name = this.name;
newRunnerTeam.parentGroup = await this.getParent(); newRunnerTeam.parentGroup = await this.getParent();
newRunnerTeam.contact = await this.getContact() newRunnerTeam.contact = await this.getContact()
return newRunnerTeam; return newRunnerTeam;

View File

@ -9,7 +9,7 @@ import { Scan } from '../../entities/Scan';
*/ */
export abstract class CreateScan { export abstract class CreateScan {
/** /**
* The scan's associated runner's id. * The scan's associated runner.
* This is important to link ran distances to runners. * This is important to link ran distances to runners.
*/ */
@IsInt() @IsInt()

View File

@ -19,7 +19,7 @@ export class CreateScanStation {
description?: string; description?: string;
/** /**
* The station's associated track's id. * The station's associated track.
*/ */
@IsInt() @IsInt()
@IsPositive() @IsPositive()

View File

@ -1,52 +0,0 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { getConnection } from 'typeorm';
import { RunnerEmailNeededError } from '../../../errors/RunnerErrors';
import { Address } from '../../entities/Address';
import { Runner } from '../../entities/Runner';
import { RunnerOrganization } from '../../entities/RunnerOrganization';
import { CreateParticipant } from './CreateParticipant';
/**
* This classed is used to create a new Runner entity from a json body (post request).
*/
export class CreateSelfServiceCitizenRunner extends CreateParticipant {
/**
* The new runners's e-mail address.
* Must be provided for email-verification to work.
*/
@IsString()
@IsNotEmpty()
@IsEmail()
email: string;
/**
* Creates a new Runner entity from this.
*/
public async toEntity(): Promise<Runner> {
let newRunner: Runner = new Runner();
newRunner.firstname = this.firstname;
newRunner.middlename = this.middlename;
newRunner.lastname = this.lastname;
newRunner.phone = this.phone;
newRunner.email = this.email;
if (!newRunner.email) {
throw new RunnerEmailNeededError();
}
newRunner.group = await this.getGroup();
newRunner.address = this.address;
Address.validate(newRunner.address);
return newRunner;
}
/**
* Gets the new runner's group by it's id.
*/
public async getGroup(): Promise<RunnerOrganization> {
return await getConnection().getRepository(RunnerOrganization).findOne({ id: 1 });
}
}

View File

@ -1,55 +0,0 @@
import { IsInt, IsOptional } from 'class-validator';
import { getConnection } from 'typeorm';
import { RunnerTeamNotFoundError } from '../../../errors/RunnerTeamErrors';
import { Address } from '../../entities/Address';
import { Runner } from '../../entities/Runner';
import { RunnerGroup } from '../../entities/RunnerGroup';
import { RunnerTeam } from '../../entities/RunnerTeam';
import { CreateParticipant } from './CreateParticipant';
/**
* This classed is used to create a new Runner entity from a json body (post request).
*/
export class CreateSelfServiceRunner extends CreateParticipant {
/**
* The new runner's team's id.
* The team has to be a part of the runner's org.
* The team property may get ignored.
* If no team get's provided the runner's group will be their org.
*/
@IsInt()
@IsOptional()
team?: number;
/**
* Creates a new Runner entity from this.
*/
public async toEntity(group: RunnerGroup): Promise<Runner> {
let newRunner: Runner = new Runner();
newRunner.firstname = this.firstname;
newRunner.middlename = this.middlename;
newRunner.lastname = this.lastname;
newRunner.phone = this.phone;
newRunner.email = this.email;
newRunner.group = await this.getGroup(group);
newRunner.address = this.address;
Address.validate(newRunner.address);
return newRunner;
}
/**
* Gets the new runner's group by it's id.
*/
public async getGroup(group: RunnerGroup): Promise<RunnerGroup> {
if (!this.team) {
return group;
}
const team = await getConnection().getRepository(RunnerTeam).findOne({ id: this.team }, { relations: ["parentGroup"] });
if (!team) { throw new RunnerTeamNotFoundError(); }
if (team.parentGroup.id != group.id) { throw new RunnerTeamNotFoundError(); }
return team;
}
}

View File

@ -1,5 +1,4 @@
import { IsInt, IsOptional, IsPositive } from 'class-validator'; import { IsInt, IsPositive } from 'class-validator';
import { BadRequestError } from 'routing-controllers';
import { getConnection } from 'typeorm'; import { getConnection } from 'typeorm';
import { RunnerCardNotFoundError } from '../../../errors/RunnerCardErrors'; import { RunnerCardNotFoundError } from '../../../errors/RunnerCardErrors';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors'; import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
@ -13,7 +12,7 @@ import { TrackScan } from '../../entities/TrackScan';
*/ */
export class CreateTrackScan { export class CreateTrackScan {
/** /**
* The id of the runnerCard associated with the scan. * The runnerCard associated with the scan.
* This get's saved for documentation and management purposes. * This get's saved for documentation and management purposes.
*/ */
@IsInt() @IsInt()
@ -21,14 +20,12 @@ export class CreateTrackScan {
card: number; card: number;
/** /**
* The scanning station's id that created the scan. * The scanning station that created the scan.
* Mainly used for logging and traceing back scans (or errors). * Mainly used for logging and traceing back scans (or errors)
* You don't have to provide the station if you're authenticateing via a scanstation token (The server takes care of it for you).
*/ */
@IsInt() @IsInt()
@IsPositive() @IsPositive()
@IsOptional() station: number;
station?: number;
/** /**
* Creates a new Track entity from this. * Creates a new Track entity from this.
@ -47,32 +44,20 @@ export class CreateTrackScan {
} }
newScan.timestamp = Math.round(new Date().getTime() / 1000); newScan.timestamp = Math.round(new Date().getTime() / 1000);
newScan = await this.validateScan(newScan); newScan.valid = await this.validateScan(newScan);
return newScan; return newScan;
} }
/**
* Get's a runnerCard entity via the provided id.
* @returns The runnerCard whom's id you provided.
*/
public async getCard(): Promise<RunnerCard> { public async getCard(): Promise<RunnerCard> {
const id = this.card % 200000000000; const track = await getConnection().getRepository(RunnerCard).findOne({ id: this.card }, { relations: ["runner"] });
const runnerCard = await getConnection().getRepository(RunnerCard).findOne({ id: id }, { relations: ["runner"] }); if (!track) {
if (!runnerCard) {
throw new RunnerCardNotFoundError(); throw new RunnerCardNotFoundError();
} }
return runnerCard; return track;
} }
/**
* Get's a scanstation entity via the provided id.
* @returns The scanstation whom's id you provided.
*/
public async getStation(): Promise<ScanStation> { public async getStation(): Promise<ScanStation> {
if (!this.station) {
throw new BadRequestError("You are missing the station's id!")
}
const station = await getConnection().getRepository(ScanStation).findOne({ id: this.station }, { relations: ["track"] }); const station = await getConnection().getRepository(ScanStation).findOne({ id: this.station }, { relations: ["track"] });
if (!station) { if (!station) {
throw new ScanStationNotFoundError(); throw new ScanStationNotFoundError();
@ -80,21 +65,15 @@ export class CreateTrackScan {
return station; return station;
} }
/** public async validateScan(scan: TrackScan): Promise<boolean> {
* Validates the scan and sets it's lap time; const scans = await getConnection().getRepository(TrackScan).find({ where: { runner: scan.runner, valid: true }, relations: ["track"] });
* @param scan The scan you want to validate if (scans.length == 0) { return true; }
* @returns The validated scan with it's laptime set.
*/ const newestScan = scans[scans.length - 1];
public async validateScan(scan: TrackScan): Promise<TrackScan> { if ((scan.timestamp - newestScan.timestamp) > scan.track.minimumLapTime) {
const latestScan = await getConnection().getRepository(TrackScan).findOne({ where: { runner: scan.runner, valid: true }, relations: ["track"], order: { id: 'DESC' } }); return true;
if (!latestScan) {
scan.lapTime = 0;
scan.valid = true;
} }
else {
scan.lapTime = scan.timestamp - latestScan.timestamp; return false;
scan.valid = (scan.lapTime > scan.track.minimumLapTime);
}
return scan;
} }
} }

View File

@ -1,10 +1,9 @@
import * as argon2 from "argon2"; import * as argon2 from "argon2";
import { passwordStrength } from "check-password-strength"; import { IsBoolean, IsEmail, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm'; import { getConnectionManager } from 'typeorm';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
import { config } from '../../../config'; import { config } from '../../../config';
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserEmailNeededError, UsernameContainsIllegalCharacterError } from '../../../errors/UserErrors'; import { UsernameOrEmailNeededError } from '../../../errors/UserErrors';
import { UserGroupNotFoundError } from '../../../errors/UserGroupErrors'; import { UserGroupNotFoundError } from '../../../errors/UserGroupErrors';
import { User } from '../../entities/User'; import { User } from '../../entities/User';
import { UserGroup } from '../../entities/UserGroup'; import { UserGroup } from '../../entities/UserGroup';
@ -34,7 +33,7 @@ export class CreateUser {
/** /**
* The new user's username. * The new user's username.
* You have to provide a email addres, so this is optional. * You have to provide at least one of: {email, username}.
*/ */
@IsOptional() @IsOptional()
@IsString() @IsString()
@ -42,11 +41,12 @@ export class CreateUser {
/** /**
* The new user's email address. * The new user's email address.
* You have to provide at least one of: {email, username}.
*/ */
@IsEmail() @IsEmail()
@IsString() @IsString()
@IsNotEmpty() @IsOptional()
email: string; email?: string;
/** /**
* The new user's phone number. * The new user's phone number.
@ -72,7 +72,7 @@ export class CreateUser {
enabled?: boolean = true; enabled?: boolean = true;
/** /**
* The new user's groups' ids. * The new user's groups' id(s).
* You can provide either one groupId or an array of groupIDs. * You can provide either one groupId or an array of groupIDs.
*/ */
@IsOptional() @IsOptional()
@ -92,16 +92,9 @@ export class CreateUser {
public async toEntity(): Promise<User> { public async toEntity(): Promise<User> {
let newUser: User = new User(); let newUser: User = new User();
if (!this.email) { if (this.email === undefined && this.username === undefined) {
throw new UserEmailNeededError(); throw new UsernameOrEmailNeededError();
} }
if (this.username?.includes("@")) { throw new UsernameContainsIllegalCharacterError(); }
let password_strength = passwordStrength(this.password);
if (!password_strength.contains.includes("uppercase")) { throw new PasswordMustContainUppercaseLetterError(); }
if (!password_strength.contains.includes("lowercase")) { throw new PasswordMustContainLowercaseLetterError(); }
if (!password_strength.contains.includes("number")) { throw new PasswordMustContainNumberError(); }
if (!(password_strength.length > 9)) { throw new PasswordTooShortError(); }
newUser.email = this.email newUser.email = this.email
newUser.username = this.username newUser.username = this.username
@ -114,7 +107,7 @@ export class CreateUser {
newUser.groups = await this.getGroups(); newUser.groups = await this.getGroups();
newUser.enabled = this.enabled; newUser.enabled = this.enabled;
if (!this.profilePic) { newUser.profilePic = `https://lauf-fuer-kaya.de/lfk-logo.png`; } if (!this.profilePic) { newUser.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; }
else { newUser.profilePic = this.profilePic; } else { newUser.profilePic = this.profilePic; }
return newUser; return newUser;

View File

@ -1,52 +0,0 @@
import { IsInt, IsPositive } from 'class-validator';
import { getConnection } from 'typeorm';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { DistanceDonation } from '../../entities/DistanceDonation';
import { Runner } from '../../entities/Runner';
import { UpdateDonation } from './UpdateDonation';
/**
* This class is used to update a DistanceDonation entity (via put request).
*/
export class UpdateDistanceDonation extends UpdateDonation {
/**
* The donation's associated runner's id.
* This is important to link the runner's distance ran to the donation.
*/
@IsInt()
@IsPositive()
runner: number;
/**
* The donation's amount per distance (full kilometer aka 1000 meters).
* The unit is your currency's smallest unit (default: euro cent).
*/
@IsInt()
@IsPositive()
amountPerDistance: number;
/**
* Update a DistanceDonation entity based on this.
* @param donation The donation that shall be updated.
*/
public async update(donation: DistanceDonation): Promise<DistanceDonation> {
donation.amountPerDistance = this.amountPerDistance;
donation.paidAmount = this.paidAmount;
donation.donor = await this.getDonor();
donation.runner = await this.getRunner();
return donation;
}
/**
* Gets a runner based on the runner id provided via this.runner.
*/
public async getRunner(): Promise<Runner> {
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
if (!runner) {
throw new RunnerNotFoundError();
}
return runner;
}
}

View File

@ -1,48 +0,0 @@
import { IsInt, IsOptional, IsPositive } from 'class-validator';
import { getConnection } from 'typeorm';
import { DonorNotFoundError } from '../../../errors/DonorErrors';
import { Donation } from '../../entities/Donation';
import { Donor } from '../../entities/Donor';
/**
* This class is used to update a Donation entity (via put request).
*/
export abstract class UpdateDonation {
/**
* The updated donation's id.
* This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
*/
@IsInt()
id: number;
/**
* The updated donation's associated donor's id.
* This is important to link donations to donors.
*/
@IsInt()
@IsPositive()
donor: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
@IsOptional()
paidAmount?: number;
/**
* Creates a new Donation entity from this.
*/
public abstract update(donation: Donation): Promise<Donation>;
/**
* Gets a donor based on the donor id provided via this.donor.
*/
public async getDonor(): Promise<Donor> {
const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor });
if (!donor) {
throw new DonorNotFoundError();
}
return donor;
}
}

View File

@ -1,6 +1,5 @@
import { IsBoolean, IsInt, IsOptional } from 'class-validator'; import { IsBoolean, IsInt, IsOptional } from 'class-validator';
import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors'; import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors';
import { Address } from '../../entities/Address';
import { Donor } from '../../entities/Donor'; import { Donor } from '../../entities/Donor';
import { CreateParticipant } from '../create/CreateParticipant'; import { CreateParticipant } from '../create/CreateParticipant';
@ -34,10 +33,9 @@ export class UpdateDonor extends CreateParticipant {
donor.phone = this.phone; donor.phone = this.phone;
donor.email = this.email; donor.email = this.email;
donor.receiptNeeded = this.receiptNeeded; donor.receiptNeeded = this.receiptNeeded;
if (!this.address) { donor.address.reset(); } donor.address = await this.getAddress();
else { donor.address = this.address; }
Address.validate(donor.address); if (this.receiptNeeded == true && this.address == null) {
if (this.receiptNeeded == true && Address.isValidAddress(donor.address) == false) {
throw new DonorReceiptAddressNeededError() throw new DonorReceiptAddressNeededError()
} }

View File

@ -1,28 +0,0 @@
import { IsInt, IsPositive } from 'class-validator';
import { FixedDonation } from '../../entities/FixedDonation';
import { UpdateDonation } from './UpdateDonation';
/**
* This class is used to update a FixedDonation entity (via put request).
*/
export class UpdateFixedDonation extends UpdateDonation {
/**
* The updated donation's amount.
* The unit is your currency's smallest unit (default: euro cent).
*/
@IsInt()
@IsPositive()
amount: number;
/**
* Update a FixedDonation entity based on this.
* @param donation The donation that shall be updated.
*/
public async update(donation: FixedDonation): Promise<FixedDonation> {
donation.amount = this.amount;
donation.paidAmount = this.paidAmount;
donation.donor = await this.getDonor();
return donation;
}
}

View File

@ -1,106 +0,0 @@
import { IsEmail, IsInt, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { config } from '../../../config';
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
import { Address } from '../../entities/Address';
import { GroupContact } from '../../entities/GroupContact';
import { RunnerGroup } from '../../entities/RunnerGroup';
/**
* This class is used to update a GroupContact entity (via put request).
*/
export class UpdateGroupContact {
/**
* The updated contact's id.
* This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
*/
@IsInt()
id: number;
/**
* The updated contact's first name.
*/
@IsNotEmpty()
@IsString()
firstname: string;
/**
* The updated contact's middle name.
*/
@IsOptional()
@IsString()
middlename?: string;
/**
* The updated contact's last name.
*/
@IsNotEmpty()
@IsString()
lastname: string;
/**
* The updated contact's address.
*/
@IsOptional()
@IsObject()
address?: Address;
/**
* The updated contact's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
*/
@IsOptional()
@IsPhoneNumber(config.phone_validation_countrycode)
phone?: string;
/**
* The updated contact's email address.
*/
@IsOptional()
@IsEmail()
email?: string;
/**
* The updated contacts's groups' ids.
* You can provide either one groupId or an array of groupIDs.
*/
@IsOptional()
groups?: number[] | number
/**
* Get's all groups for this contact by their id's;
*/
public async getGroups(): Promise<RunnerGroup[]> {
if (!this.groups) { return null; }
let groups = new Array<RunnerGroup>();
if (!Array.isArray(this.groups)) {
this.groups = [this.groups]
}
for (let group of this.groups) {
let found = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: group });
if (!found) { throw new RunnerGroupNotFoundError(); }
groups.push(found);
}
return groups;
}
/**
* Updates a provided Donor entity based on this.
* @param contact the contact you want to update.
*/
public async update(contact: GroupContact): Promise<GroupContact> {
contact.firstname = this.firstname; GroupContact
contact.middlename = this.middlename;
contact.lastname = this.lastname;
contact.phone = this.phone;
contact.email = this.email;
if (!this.address) { contact.address.reset(); }
else { contact.address = this.address; }
Address.validate(contact.address);
contact.groups = await this.getGroups();
return contact;
}
}

View File

@ -1,7 +1,7 @@
import { IsInt, IsNotEmpty, IsPositive } from 'class-validator'; import { IsInt, IsNotEmpty, IsObject } from 'class-validator';
import { getConnectionManager } from 'typeorm'; import { getConnectionManager } from 'typeorm';
import { PermissionNeedsPrincipalError } from '../../../errors/PermissionErrors'; import { PermissionNeedsPrincipalError } from '../../../errors/PermissionErrors';
import { PrincipalNotFoundError } from '../../../errors/PrincipalErrors'; import { PrincipalNotFoundError, PrincipalWrongTypeError } from '../../../errors/PrincipalErrors';
import { Permission } from '../../entities/Permission'; import { Permission } from '../../entities/Permission';
import { Principal } from '../../entities/Principal'; import { Principal } from '../../entities/Principal';
import { PermissionAction } from '../../enums/PermissionAction'; import { PermissionAction } from '../../enums/PermissionAction';
@ -20,11 +20,12 @@ export class UpdatePermission {
id: number; id: number;
/** /**
* The updated permissions's principal's id. * The updated permissions's principal.
* Just has to contain the principal's id -everything else won't be checked or changed.
*/ */
@IsInt() @IsObject()
@IsPositive() @IsNotEmpty()
principal: number; principal: Principal;
/** /**
* The permissions's target. * The permissions's target.
@ -56,8 +57,12 @@ export class UpdatePermission {
if (this.principal === undefined || this.principal === null) { if (this.principal === undefined || this.principal === null) {
throw new PermissionNeedsPrincipalError(); throw new PermissionNeedsPrincipalError();
} }
let principal = await getConnectionManager().get().getRepository(Principal).findOne({ id: this.principal }); if (!isNaN(this.principal.id)) {
if (!principal) { throw new PrincipalNotFoundError(); } let principal = await getConnectionManager().get().getRepository(Principal).findOne({ id: this.principal.id });
return principal; if (!principal) { throw new PrincipalNotFoundError(); }
return principal;
}
throw new PrincipalWrongTypeError();
} }
} }

View File

@ -1,8 +1,8 @@
import { IsInt, IsPositive } from 'class-validator'; import { IsInt, IsObject } from 'class-validator';
import { getConnectionManager } from 'typeorm'; import { getConnectionManager } from 'typeorm';
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors'; import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
import { RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors'; import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
import { Address } from '../../entities/Address';
import { Runner } from '../../entities/Runner'; import { Runner } from '../../entities/Runner';
import { RunnerGroup } from '../../entities/RunnerGroup'; import { RunnerGroup } from '../../entities/RunnerGroup';
import { CreateParticipant } from '../create/CreateParticipant'; import { CreateParticipant } from '../create/CreateParticipant';
@ -20,11 +20,11 @@ export class UpdateRunner extends CreateParticipant {
id: number; id: number;
/** /**
* The updated runner's group's id. * The updated runner's new team/org.
* Just has to contain the group's id -everything else won't be checked or changed.
*/ */
@IsInt() @IsObject()
@IsPositive() group: RunnerGroup;
group: number;
/** /**
* Updates a provided Runner entity based on this. * Updates a provided Runner entity based on this.
@ -36,9 +36,7 @@ export class UpdateRunner extends CreateParticipant {
runner.phone = this.phone; runner.phone = this.phone;
runner.email = this.email; runner.email = this.email;
runner.group = await this.getGroup(); runner.group = await this.getGroup();
if (!this.address) { runner.address.reset(); } runner.address = await this.getAddress();
else { runner.address = this.address; }
Address.validate(runner.address);
return runner; return runner;
} }
@ -50,8 +48,12 @@ export class UpdateRunner extends CreateParticipant {
if (this.group === undefined || this.group === null) { if (this.group === undefined || this.group === null) {
throw new RunnerTeamNeedsParentError(); throw new RunnerTeamNeedsParentError();
} }
let group = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: this.group }); if (!isNaN(this.group.id)) {
if (!group) { throw new RunnerGroupNotFoundError; } let group = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: this.group.id });
return group; if (!group) { throw new RunnerGroupNotFoundError; }
return group;
}
throw new RunnerOrganisationWrongTypeError;
} }
} }

View File

@ -17,7 +17,7 @@ export class UpdateRunnerCard {
id?: number; id?: number;
/** /**
* The updated card's associated runner's id. * The updated card's associated runner.
*/ */
@IsInt() @IsInt()
@IsOptional() @IsOptional()

View File

@ -0,0 +1,52 @@
import { IsInt, IsOptional } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { AddressNotFoundError } from '../../../errors/AddressErrors';
import { Address } from '../../entities/Address';
import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
import { CreateRunnerGroup } from '../create/CreateRunnerGroup';
/**
* This class is used to update a RunnerOrganisation entity (via put request).
*/
export class UpdateRunnerOrganisation extends CreateRunnerGroup {
/**
* The updated orgs's id.
* This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
*/
@IsInt()
id: number;
/**
* The updated organisation's address.
* Just has to contain the address's id - everything else won't be checked or changed.
* Optional.
*/
@IsInt()
@IsOptional()
address?: Address;
/**
* Loads the organisation's address based on it's id.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined || this.address === null) {
return null;
}
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address.id });
if (!address) { throw new AddressNotFoundError; }
return address;
}
/**
* Updates a provided RunnerOrganisation entity based on this.
*/
public async update(organisation: RunnerOrganisation): Promise<RunnerOrganisation> {
organisation.name = this.name;
organisation.contact = await this.getContact();
// organisation.address = await this.getAddress();
return organisation;
}
}

View File

@ -1,53 +0,0 @@
import { IsBoolean, IsInt, IsObject, IsOptional } from 'class-validator';
import * as uuid from 'uuid';
import { Address } from '../../entities/Address';
import { RunnerOrganization } from '../../entities/RunnerOrganization';
import { CreateRunnerGroup } from '../create/CreateRunnerGroup';
/**
* This class is used to update a RunnerOrganization entity (via put request).
*/
export class UpdateRunnerOrganization extends CreateRunnerGroup {
/**
* The updated orgs's id.
* This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
*/
@IsInt()
id: number;
/**
* The updated organization's address.
*/
@IsOptional()
@IsObject()
address?: Address;
/**
* Is registration enabled for the updated organization?
*/
@IsOptional()
@IsBoolean()
registrationEnabled?: boolean = false;
/**
* Updates a provided RunnerOrganization entity based on this.
*/
public async update(organization: RunnerOrganization): Promise<RunnerOrganization> {
organization.name = this.name;
organization.contact = await this.getContact();
if (!this.address) { organization.address.reset(); }
else { organization.address = this.address; }
Address.validate(organization.address);
if (this.registrationEnabled && !organization.key) {
organization.key = uuid.v4().toUpperCase();
}
else {
organization.key = null;
}
return organization;
}
}

View File

@ -1,8 +1,8 @@
import { IsInt, IsPositive } from 'class-validator'; import { IsInt, IsNotEmpty, IsObject } from 'class-validator';
import { getConnectionManager } from 'typeorm'; import { getConnectionManager } from 'typeorm';
import { RunnerOrganizationNotFoundError } from '../../../errors/RunnerOrganizationErrors'; import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors'; import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
import { RunnerOrganization } from '../../entities/RunnerOrganization'; import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
import { RunnerTeam } from '../../entities/RunnerTeam'; import { RunnerTeam } from '../../entities/RunnerTeam';
import { CreateRunnerGroup } from '../create/CreateRunnerGroup'; import { CreateRunnerGroup } from '../create/CreateRunnerGroup';
@ -19,22 +19,27 @@ export class UpdateRunnerTeam extends CreateRunnerGroup {
id: number; id: number;
/** /**
* The updated team's parentGroup's id. * The updated team's parentGroup.
* Just has to contain the organisation's id - everything else won't be checked or changed.
*/ */
@IsInt() @IsObject()
@IsPositive() @IsNotEmpty()
parentGroup: number; parentGroup: RunnerOrganisation;
/** /**
* Loads the updated teams's parentGroup based on it's id. * Loads the updated teams's parentGroup based on it's id.
*/ */
public async getParent(): Promise<RunnerOrganization> { public async getParent(): Promise<RunnerOrganisation> {
if (this.parentGroup === undefined || this.parentGroup === null) { if (this.parentGroup === undefined || this.parentGroup === null) {
throw new RunnerTeamNeedsParentError(); throw new RunnerTeamNeedsParentError();
} }
let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganization).findOne({ id: this.parentGroup }); if (!isNaN(this.parentGroup.id)) {
if (!parentGroup) { throw new RunnerOrganizationNotFoundError();; } let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: this.parentGroup.id });
return parentGroup; if (!parentGroup) { throw new RunnerOrganisationNotFoundError();; }
return parentGroup;
}
throw new RunnerOrganisationWrongTypeError;
} }
/** /**

View File

@ -16,7 +16,7 @@ export abstract class UpdateScan {
id: number; id: number;
/** /**
* The updated scan's associated runner's id. * The updated scan's associated runner.
* This is important to link ran distances to runners. * This is important to link ran distances to runners.
*/ */
@IsInt() @IsInt()

View File

@ -1,9 +1,9 @@
import { IsBoolean, IsInt, IsOptional, IsPositive } from 'class-validator'; import { IsBoolean, IsInt, IsOptional } from 'class-validator';
import { getConnection } from 'typeorm'; import { getConnection } from 'typeorm';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors'; import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { TrackNotFoundError } from '../../../errors/TrackErrors'; import { ScanStationNotFoundError } from '../../../errors/ScanStationErrors';
import { Runner } from '../../entities/Runner'; import { Runner } from '../../entities/Runner';
import { Track } from '../../entities/Track'; import { ScanStation } from '../../entities/ScanStation';
import { TrackScan } from '../../entities/TrackScan'; import { TrackScan } from '../../entities/TrackScan';
/** /**
@ -18,12 +18,12 @@ export abstract class UpdateTrackScan {
id: number; id: number;
/** /**
* The updated scan's associated runner's id. * The updated scan's associated runner.
* This is important to link ran distances to runners. * This is important to link ran distances to runners.
*/ */
@IsInt() @IsInt()
@IsPositive() @IsOptional()
runner: number; runner?: number;
/** /**
* Is the updated scan valid (for fraud reasons). * Is the updated scan valid (for fraud reasons).
@ -33,12 +33,12 @@ export abstract class UpdateTrackScan {
valid?: boolean = true; valid?: boolean = true;
/** /**
* The updated scan's associated station's id. * The updated scan's associated station.
* This is important to link ran distances to runners. * This is important to link ran distances to runners.
*/ */
@IsInt() @IsInt()
@IsPositive() @IsOptional()
public track: number; public station?: number;
/** /**
* Update a TrackScan entity based on this. * Update a TrackScan entity based on this.
@ -46,8 +46,13 @@ export abstract class UpdateTrackScan {
*/ */
public async update(scan: TrackScan): Promise<TrackScan> { public async update(scan: TrackScan): Promise<TrackScan> {
scan.valid = this.valid; scan.valid = this.valid;
scan.runner = await this.getRunner(); if (this.runner) {
scan.track = await this.getTrack(); scan.runner = await this.getRunner();
}
if (this.station) {
scan.station = await this.getStation();
}
scan.track = scan.station.track;
return scan; return scan;
} }
@ -66,11 +71,11 @@ export abstract class UpdateTrackScan {
/** /**
* Gets a runner based on the runner id provided via this.runner. * Gets a runner based on the runner id provided via this.runner.
*/ */
public async getTrack(): Promise<Track> { public async getStation(): Promise<ScanStation> {
const track = await getConnection().getRepository(Track).findOne({ id: this.track }); const station = await getConnection().getRepository(ScanStation).findOne({ id: this.station });
if (!track) { if (!station) {
throw new TrackNotFoundError(); throw new ScanStationNotFoundError();
} }
return track; return station;
} }
} }

View File

@ -1,14 +1,12 @@
import * as argon2 from "argon2"; import * as argon2 from "argon2";
import { passwordStrength } from "check-password-strength"; import { IsBoolean, IsEmail, IsInt, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm'; import { getConnectionManager } from 'typeorm';
import { config } from '../../../config'; import { config } from '../../../config';
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserEmailNeededError, UsernameContainsIllegalCharacterError } from '../../../errors/UserErrors'; import { UsernameOrEmailNeededError } from '../../../errors/AuthError';
import { UserGroupNotFoundError } from '../../../errors/UserGroupErrors'; import { UserGroupNotFoundError } from '../../../errors/UserGroupErrors';
import { User } from '../../entities/User'; import { User } from '../../entities/User';
import { UserGroup } from '../../entities/UserGroup'; import { UserGroup } from '../../entities/UserGroup';
/** /**
* This class is used to update a User entity (via put request). * This class is used to update a User entity (via put request).
*/ */
@ -42,7 +40,7 @@ export class UpdateUser {
/** /**
* The updated user's username. * The updated user's username.
* You have to provide a email addres, so this is optional. * You have to provide at least one of: {email, username}.
*/ */
@IsOptional() @IsOptional()
@IsString() @IsString()
@ -50,11 +48,12 @@ export class UpdateUser {
/** /**
* The updated user's email address. * The updated user's email address.
* You have to provide at least one of: {email, username}.
*/ */
@IsEmail() @IsEmail()
@IsString() @IsString()
@IsNotEmpty() @IsOptional()
email: string; email?: string;
/** /**
* The updated user's phone number. * The updated user's phone number.
@ -78,14 +77,14 @@ export class UpdateUser {
* Should the user be enabled? * Should the user be enabled?
*/ */
@IsBoolean() @IsBoolean()
@IsOptional()
enabled: boolean = true; enabled: boolean = true;
/** /**
* The updated user's groups' ids. * The updated user's groups.
* This just has to contain the group's id - everything else won't be changed.
*/ */
@IsOptional() @IsOptional()
groups?: number | number[] groups?: UserGroup[]
/** /**
* The user's profile pic (or rather a url pointing to it). * The user's profile pic (or rather a url pointing to it).
@ -100,23 +99,16 @@ export class UpdateUser {
* @param user The user that shall be updated. * @param user The user that shall be updated.
*/ */
public async update(user: User): Promise<User> { public async update(user: User): Promise<User> {
if (!this.email) { user.email = this.email;
throw new UserEmailNeededError(); user.username = this.username;
if ((user.email === undefined || user.email === null) && (user.username === undefined || user.username === null)) {
throw new UsernameOrEmailNeededError();
} }
if (this.username.includes("@")) { throw new UsernameContainsIllegalCharacterError(); }
if (this.password) { if (this.password) {
let password_strength = passwordStrength(this.password);
if (!password_strength.contains.includes("uppercase")) { throw new PasswordMustContainUppercaseLetterError(); }
if (!password_strength.contains.includes("lowercase")) { throw new PasswordMustContainLowercaseLetterError(); }
if (!password_strength.contains.includes("number")) { throw new PasswordMustContainNumberError(); }
if (!(password_strength.length > 9)) { throw new PasswordTooShortError(); }
user.password = await argon2.hash(this.password + user.uuid); user.password = await argon2.hash(this.password + user.uuid);
user.refreshTokenCount = user.refreshTokenCount + 1; user.refreshTokenCount = user.refreshTokenCount + 1;
} }
user.email = this.email;
user.username = this.username;
user.enabled = this.enabled; user.enabled = this.enabled;
user.firstname = this.firstname user.firstname = this.firstname
user.middlename = this.middlename user.middlename = this.middlename
@ -124,14 +116,14 @@ export class UpdateUser {
user.phone = this.phone; user.phone = this.phone;
user.groups = await this.getGroups(); user.groups = await this.getGroups();
if (!this.profilePic) { user.profilePic = `https://lauf-fuer-kaya.de/lfk-logo.png`; } if (!this.profilePic) { user.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; }
else { user.profilePic = this.profilePic; } else { user.profilePic = this.profilePic; }
return user; return user;
} }
/** /**
* Get's all groups for this user by their id's; * Loads the updated user's groups based on their ids.
*/ */
public async getGroups() { public async getGroups() {
if (!this.groups) { return null; } if (!this.groups) { return null; }
@ -140,7 +132,7 @@ export class UpdateUser {
this.groups = [this.groups] this.groups = [this.groups]
} }
for (let group of this.groups) { for (let group of this.groups) {
let found = await getConnectionManager().get().getRepository(UserGroup).findOne({ id: group }); let found = await getConnectionManager().get().getRepository(UserGroup).findOne({ id: group.id });
if (!found) { throw new UserGroupNotFoundError(); } if (!found) { throw new UserGroupNotFoundError(); }
groups.push(found); groups.push(found);
} }

View File

@ -1,24 +1,44 @@
import { import {
IsInt,
IsNotEmpty,
IsOptional,
IsPostalCode, IsPostalCode,
IsString IsString
} from "class-validator"; } from "class-validator";
import { Column } from "typeorm"; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import ValidatorJS from 'validator';
import { config } from '../../config'; import { config } from '../../config';
import { AddressCityEmptyError, AddressCountryEmptyError, AddressFirstLineEmptyError, AddressPostalCodeEmptyError, AddressPostalCodeInvalidError } from '../../errors/AddressErrors'; import { IAddressUser } from './IAddressUser';
/** /**
* Defines the Address class. * Defines the Address entity.
* Implemented this way to prevent any formatting differences. * Implemented this way to prevent any formatting differences.
*/ */
@Entity()
export class Address { export class Address {
/**
* Autogenerated unique id (primary key).
*/
@PrimaryGeneratedColumn()
@IsInt()
id: number;
/**
* The address's description.
* Optional and mostly for UX.
*/
@Column({ nullable: true })
@IsString()
@IsOptional()
description?: string;
/** /**
* The address's first line. * The address's first line.
* Containing the street and house number. * Containing the street and house number.
*/ */
@Column({ nullable: true }) @Column()
@IsString() @IsString()
address1?: string; @IsNotEmpty()
address1: string;
/** /**
* The address's second line. * The address's second line.
@ -26,61 +46,45 @@ export class Address {
*/ */
@Column({ nullable: true }) @Column({ nullable: true })
@IsString() @IsString()
@IsOptional()
address2?: string; address2?: string;
/** /**
* The address's postal code. * The address's postal code.
* This will get checked against the postal code syntax for the configured country. * This will get checked against the postal code syntax for the configured country.
*/ */
@Column({ nullable: true }) @Column()
@IsString() @IsString()
@IsNotEmpty()
@IsPostalCode(config.postalcode_validation_countrycode) @IsPostalCode(config.postalcode_validation_countrycode)
postalcode: string; postalcode: string;
/** /**
* The address's city. * The address's city.
*/ */
@Column({ nullable: true }) @Column()
@IsString() @IsString()
@IsNotEmpty()
city: string; city: string;
/** /**
* The address's country. * The address's country.
*/ */
@Column({ nullable: true }) @Column()
@IsString() @IsString()
@IsNotEmpty()
country: string; country: string;
public reset() { /**
this.address1 = null; * Used to link the address to participants.
this.address2 = null; */
this.city = null; @OneToMany(() => IAddressUser, addressUser => addressUser.address, { nullable: true })
this.country = null; addressUsers: IAddressUser[];
this.postalcode = null;
}
/** /**
* Checks if this is a valid address * Turns this entity into it's response class.
*/ */
public static isValidAddress(address: Address): Boolean { public toResponse() {
if (address == null) { return false; } return new Error("NotImplemented");
if (address.address1 == null || address.city == null || address.country == null || address.postalcode == null) { return false; }
if (ValidatorJS.isPostalCode(address.postalcode, config.postalcode_validation_countrycode) == false) { return false; }
return true;
}
/**
* This function validates addresses.
* This is a workaround for non-existant class validation for embedded entities.
* @param address The address that shall get validated.
*/
public static validate(address: Address) {
if (address == null) { return; }
if (address.address1 == null && address.city == null && address.country == null && address.postalcode == null) { return; }
if (address.address1 == null) { throw new AddressFirstLineEmptyError(); }
if (address.postalcode == null) { throw new AddressPostalCodeEmptyError(); }
if (address.city == null) { throw new AddressCityEmptyError(); }
if (address.country == null) { throw new AddressCountryEmptyError(); }
if (ValidatorJS.isPostalCode(address.postalcode.toString(), config.postalcode_validation_countrycode) == false) { throw new AddressPostalCodeInvalidError(); }
} }
} }

View File

@ -1,27 +0,0 @@
import {
IsNotEmpty,
IsString
} from "class-validator";
import { Column, Entity, PrimaryColumn } from "typeorm";
/**
* Defines the ConfigFlag entity.
* This entity can be used to set some flags on db init.
*/
@Entity()
export class ConfigFlag {
/**
* The flag's name (primary).
*/
@PrimaryColumn()
@IsString()
option: string;
/**
* The flag's value.
*/
@Column()
@IsString()
@IsNotEmpty()
value: string;
}

View File

@ -1,6 +1,5 @@
import { IsInt, IsNotEmpty, IsPositive } from "class-validator"; import { IsInt, IsNotEmpty, IsPositive } from "class-validator";
import { ChildEntity, Column, ManyToOne } from "typeorm"; import { ChildEntity, Column, ManyToOne } from "typeorm";
import { ResponseDistanceDonation } from '../responses/ResponseDistanceDonation';
import { Donation } from "./Donation"; import { Donation } from "./Donation";
import { Runner } from "./Runner"; import { Runner } from "./Runner";
@ -32,7 +31,7 @@ export class DistanceDonation extends Donation {
* Get's calculated from the runner's distance ran and the amount donated per kilometer. * Get's calculated from the runner's distance ran and the amount donated per kilometer.
*/ */
public get amount(): number { public get amount(): number {
let calculatedAmount = 0; let calculatedAmount = -1;
try { try {
calculatedAmount = this.amountPerDistance * (this.runner.distance / 1000); calculatedAmount = this.amountPerDistance * (this.runner.distance / 1000);
} catch (error) { } catch (error) {
@ -44,7 +43,7 @@ export class DistanceDonation extends Donation {
/** /**
* Turns this entity into it's response class. * Turns this entity into it's response class.
*/ */
public toResponse(): ResponseDistanceDonation { public toResponse() {
return new ResponseDistanceDonation(this); return new Error("NotImplemented");
} }
} }

View File

@ -2,8 +2,7 @@ import {
IsInt, IsInt,
IsNotEmpty IsNotEmpty
} from "class-validator"; } from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; import { Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseDonation } from '../responses/ResponseDonation';
import { Donor } from './Donor'; import { Donor } from './Donor';
/** /**
@ -32,20 +31,12 @@ export abstract class Donation {
* The donation's amount in cents (or whatever your currency's smallest unit is.). * The donation's amount in cents (or whatever your currency's smallest unit is.).
* The exact implementation may differ for each type of donation. * The exact implementation may differ for each type of donation.
*/ */
public abstract get amount(): number; abstract amount: number;
/**
* The donation's paid amount in cents (or whatever your currency's smallest unit is.).
* Used to mark donations as paid.
*/
@Column({ nullable: true })
@IsInt()
paidAmount: number;
/** /**
* Turns this entity into it's response class. * Turns this entity into it's response class.
*/ */
public toResponse(): ResponseDonation { public toResponse() {
return new ResponseDonation(this); return new Error("NotImplemented");
} }
} }

View File

@ -1,4 +1,4 @@
import { IsBoolean, IsInt } from "class-validator"; import { IsBoolean } from "class-validator";
import { ChildEntity, Column, OneToMany } from "typeorm"; import { ChildEntity, Column, OneToMany } from "typeorm";
import { ResponseDonor } from '../responses/ResponseDonor'; import { ResponseDonor } from '../responses/ResponseDonor';
import { Donation } from './Donation'; import { Donation } from './Donation';
@ -24,24 +24,6 @@ export class Donor extends Participant {
@OneToMany(() => Donation, donation => donation.donor, { nullable: true }) @OneToMany(() => Donation, donation => donation.donor, { nullable: true })
donations: Donation[]; donations: Donation[];
/**
* Returns the total donations of a donor based on his linked donations.
*/
@IsInt()
public get donationAmount(): number {
if (!this.donations) { return 0; }
return this.donations.reduce((sum, current) => sum + current.amount, 0);
}
/**
* Returns the total paid donations of a donor based on his linked donations.
*/
@IsInt()
public get paidDonationAmount(): number {
if (!this.donations) { return 0; }
return this.donations.reduce((sum, current) => sum + current.paidAmount, 0);
}
/** /**
* Turns this entity into it's response class. * Turns this entity into it's response class.
*/ */

View File

@ -1,6 +1,5 @@
import { IsInt, IsPositive } from "class-validator"; import { IsInt, IsPositive } from "class-validator";
import { ChildEntity, Column } from "typeorm"; import { ChildEntity, Column } from "typeorm";
import { ResponseDonation } from '../responses/ResponseDonation';
import { Donation } from "./Donation"; import { Donation } from "./Donation";
/** /**
@ -12,33 +11,16 @@ export class FixedDonation extends Donation {
/** /**
* The donation's amount in cents (or whatever your currency's smallest unit is.). * The donation's amount in cents (or whatever your currency's smallest unit is.).
* This is the "real" value used by fixed donations.
*/ */
@Column() @Column()
@IsInt() @IsInt()
@IsPositive() @IsPositive()
private _amount: number; amount: number;
/**
* The donation's amount in cents (or whatever your currency's smallest unit is.).
*/
@IsInt()
@IsPositive()
public get amount(): number {
return this._amount;
}
/**
* The donation's amount in cents (or whatever your currency's smallest unit is.).
*/
public set amount(value: number) {
this._amount = value;
}
/** /**
* Turns this entity into it's response class. * Turns this entity into it's response class.
*/ */
public toResponse(): ResponseDonation { public toResponse() {
return new ResponseDonation(this); return new Error("NotImplemented");
} }
} }

View File

@ -7,10 +7,10 @@ import {
IsString IsString
} from "class-validator"; } from "class-validator";
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { config } from '../../config'; import { config } from '../../config';
import { ResponseGroupContact } from '../responses/ResponseGroupContact';
import { Address } from "./Address"; import { Address } from "./Address";
import { IAddressUser } from './IAddressUser';
import { RunnerGroup } from "./RunnerGroup"; import { RunnerGroup } from "./RunnerGroup";
/** /**
@ -18,7 +18,7 @@ import { RunnerGroup } from "./RunnerGroup";
* Mainly it's own class to reduce duplicate code and enable contact's to be associated with multiple groups. * Mainly it's own class to reduce duplicate code and enable contact's to be associated with multiple groups.
*/ */
@Entity() @Entity()
export class GroupContact { export class GroupContact implements IAddressUser {
/** /**
* Autogenerated unique id (primary key). * Autogenerated unique id (primary key).
*/ */
@ -54,7 +54,8 @@ export class GroupContact {
* The contact's address. * The contact's address.
* This is a address object to prevent any formatting differences. * This is a address object to prevent any formatting differences.
*/ */
@Column(type => Address) @IsOptional()
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
address?: Address; address?: Address;
/** /**
@ -84,7 +85,7 @@ export class GroupContact {
/** /**
* Turns this entity into it's response class. * Turns this entity into it's response class.
*/ */
public toResponse(): ResponseGroupContact { public toResponse() {
return new ResponseGroupContact(this); return new Error("NotImplemented");
} }
} }

View File

@ -0,0 +1,20 @@
import { Entity, ManyToOne, PrimaryColumn } from 'typeorm';
import { Address } from './Address';
/**
* The interface(tm) all entities using addresses have to implement.
* This is a abstract class, because apparently typeorm can't really work with interfaces :/
*/
@Entity()
export abstract class IAddressUser {
@PrimaryColumn()
id: number;
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
address?: Address
/**
* Turns this entity into it's response class.
*/
public abstract toResponse();
}

View File

@ -7,10 +7,11 @@ import {
IsString IsString
} from "class-validator"; } from "class-validator";
import { Column, Entity, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { config } from '../../config'; import { config } from '../../config';
import { ResponseParticipant } from '../responses/ResponseParticipant'; import { ResponseParticipant } from '../responses/ResponseParticipant';
import { Address } from "./Address"; import { Address } from "./Address";
import { IAddressUser } from './IAddressUser';
/** /**
* Defines the Participant entity. * Defines the Participant entity.
@ -18,7 +19,7 @@ import { Address } from "./Address";
*/ */
@Entity() @Entity()
@TableInheritance({ column: { name: "type", type: "varchar" } }) @TableInheritance({ column: { name: "type", type: "varchar" } })
export abstract class Participant { export abstract class Participant implements IAddressUser {
/** /**
* Autogenerated unique id (primary key). * Autogenerated unique id (primary key).
*/ */
@ -54,7 +55,7 @@ export abstract class Participant {
* The participant's address. * The participant's address.
* This is a address object to prevent any formatting differences. * This is a address object to prevent any formatting differences.
*/ */
@Column(type => Address) @ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
address?: Address; address?: Address;
/** /**

View File

@ -1,5 +1,5 @@
import { IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; import { IsInt, IsNotEmpty } from "class-validator";
import { ChildEntity, Column, ManyToOne, OneToMany } from "typeorm"; import { ChildEntity, ManyToOne, OneToMany } from "typeorm";
import { ResponseRunner } from '../responses/ResponseRunner'; import { ResponseRunner } from '../responses/ResponseRunner';
import { DistanceDonation } from "./DistanceDonation"; import { DistanceDonation } from "./DistanceDonation";
import { Participant } from "./Participant"; import { Participant } from "./Participant";
@ -16,7 +16,7 @@ import { Scan } from "./Scan";
export class Runner extends Participant { export class Runner extends Participant {
/** /**
* The runner's associated group. * The runner's associated group.
* Can be a runner team or organization. * Can be a runner team or organisation.
*/ */
@IsNotEmpty() @IsNotEmpty()
@ManyToOne(() => RunnerGroup, group => group.runners) @ManyToOne(() => RunnerGroup, group => group.runners)
@ -43,15 +43,6 @@ export class Runner extends Participant {
@OneToMany(() => Scan, scan => scan.runner, { nullable: true }) @OneToMany(() => Scan, scan => scan.runner, { nullable: true })
scans: Scan[]; scans: Scan[];
/**
* The last time the runner requested a selfservice link.
* Used to prevent spamming of the selfservice link forgotten route.
*/
@Column({ nullable: true, unique: false })
@IsString()
@IsOptional()
resetRequestedTimestamp?: number;
/** /**
* Returns all valid scans associated with this runner. * Returns all valid scans associated with this runner.
* This is implemented here to avoid duplicate code in other files. * This is implemented here to avoid duplicate code in other files.
@ -74,7 +65,7 @@ export class Runner extends Participant {
*/ */
@IsInt() @IsInt()
public get distanceDonationAmount(): number { public get distanceDonationAmount(): number {
return this.distanceDonations.reduce((sum, current) => sum + current.amount, 0); return this.distanceDonations.reduce((sum, current) => sum + current.amountPerDistance, 0) * this.distance;
} }
/** /**

View File

@ -52,7 +52,13 @@ export class RunnerCard {
* Generates a ean-13 compliant string for barcode generation. * Generates a ean-13 compliant string for barcode generation.
*/ */
public get code(): string { public get code(): string {
return this.paddedId const multiply = [1, 3];
let total = 0;
this.paddedId.split('').forEach((letter, index) => {
total += parseInt(letter, 10) * multiply[index % 2];
});
const checkSum = (Math.ceil(total / 10) * 10) - total;
return this.paddedId + checkSum.toString();
} }
/** /**
@ -61,11 +67,10 @@ export class RunnerCard {
private get paddedId(): string { private get paddedId(): string {
let id: string = this.id.toString(); let id: string = this.id.toString();
if (id.length > 11) { if (id.length > 12) {
throw new RunnerCardIdOutOfRangeError(); throw new RunnerCardIdOutOfRangeError();
} }
while (id.length < 11) { id = '0' + id; } while (id.length < 12) { id = '0' + id; }
id = '2' + id;
return id; return id;
} }

Some files were not shown because too many files have changed in this diff Show More