Compare commits

...

146 Commits
v0.14.0 ... dev

Author SHA1 Message Date
e27e819609
chore(release): 1.5.2
All checks were successful
Build release images / build-container (push) Successful in 1m9s
2025-05-26 19:30:33 +02:00
0f532b139c
feat(mailer): Log error message when sending selfservice forgotten mail fails 2025-05-26 19:30:16 +02:00
eebcc2e328
feat(mailer): Add logging for selfservice forgotten mail requests 2025-05-26 19:29:37 +02:00
284954d064
chore(release): 1.5.1
All checks were successful
Build release images / build-container (push) Successful in 1m12s
2025-05-26 19:24:04 +02:00
401ca923a6
feat(mailer): Log error when sending selfservice forgotten mail fails 2025-05-26 19:23:30 +02:00
bf1f6411e0
chore(release): 1.5.0
All checks were successful
Build release images / build-container (push) Successful in 1m17s
2025-05-06 19:41:36 +02:00
f225cc4954
feat(responses): Added created_at/updated_at 2025-05-06 19:38:20 +02:00
728f8a14e9
feat(entities): Added created/updated at to all entities 2025-05-06 19:33:30 +02:00
a4480589a0
feat(participants): Added created/updated at 2025-05-06 19:29:46 +02:00
0ad9eeb52f
chore(release): 1.4.3
All checks were successful
Build release images / build-container (push) Successful in 1m15s
2025-05-01 16:02:40 +02:00
4494afc64b
feat(runners): Include collected distance donation amount in runner detail 2025-05-01 16:02:28 +02:00
f4747c51de
chore(release): 1.4.2
All checks were successful
Build release images / build-container (push) Successful in 1m16s
2025-05-01 15:57:57 +02:00
07a0195f12
fix(donations): Fixed creation bug 2025-05-01 15:56:42 +02:00
7ac98229d1
chore(release): 1.4.1
All checks were successful
Build release images / build-container (push) Successful in 1m19s
2025-04-28 21:36:31 +02:00
dd5b538783
refactor(auth): Increased token timeouts to 24hrs/7days 2025-04-28 21:36:12 +02:00
8e6d67428c
chore(release): 1.4.0
All checks were successful
Build release images / build-container (push) Successful in 1m14s
2025-04-28 19:41:55 +02:00
7ffb7523aa
Merge branch 'CreateAnonymousDonation-dedicated-enitity-controller' into dev 2025-04-28 19:41:35 +02:00
f4bf309821
feat(donations): Implement response type to indicate possible missing donor 2025-04-28 19:35:07 +02:00
02b1cb9904
refactor(donations): Make anon prepaid 2025-04-28 19:32:06 +02:00
7697acff82
fix(donations): Move donor over to the types that need it 2025-04-28 19:25:41 +02:00
bacfc437f9
chore(release): 1.3.12
All checks were successful
Build release images / build-container (push) Successful in 1m24s
2025-04-28 11:05:10 +02:00
9875b4f392
wip 2025-04-28 11:04:22 +02:00
ce9b765b81
refactor(config): improve consola error logs 2025-04-28 11:03:33 +02:00
2ab6e985e3
refactor: make Donation.donor optional 2025-04-28 10:56:06 +02:00
d06f6a4407
chore(release): 1.3.11
All checks were successful
Build release images / build-container (push) Successful in 1m40s
2025-04-17 20:46:56 +02:00
a50d72f2f5
feat(RunnerController): add selfservice_links parameter to getRunners method 2025-04-17 20:45:28 +02:00
4723d9738e
chore(release): 1.3.10
All checks were successful
Build release images / build-container (push) Successful in 1m19s
2025-04-11 12:11:08 +02:00
1a478bd784
feat(RunnerController.getAll): debug created_via query param filter 2025-04-11 12:09:10 +02:00
284cb0f8b3
chore(release): 1.3.9
All checks were successful
Build release images / build-container (push) Successful in 1m14s
2025-04-09 11:38:16 +02:00
6e63c57936
feat(RunnerController.getAll): add created_via query param filter 2025-04-09 11:37:49 +02:00
30b61db2c1
chore(release): 1.3.8
All checks were successful
Build release images / build-container (push) Successful in 1m20s
2025-04-09 10:23:48 +02:00
8237d5f210
feat(RunnerCardController): putByCode 2025-04-09 10:23:01 +02:00
03e0a29096
chore(release): 1.3.7
All checks were successful
Build release images / build-container (push) Successful in 1m16s
2025-04-08 21:15:59 +02:00
a6afba93e2
feat(stats): Publish runners by kiosk stat 2025-04-08 21:15:41 +02:00
a41758cd9c
chore(release): 1.3.6
All checks were successful
Build release images / build-container (push) Successful in 1m32s
2025-04-08 21:06:01 +02:00
d6755ed134
feat(runners): Allow created via being set via api 2025-04-08 21:04:38 +02:00
599c75fc00
fix(participant): Switch to correct type 2025-04-08 21:03:23 +02:00
bb213f001e
chore(release): 1.3.5
All checks were successful
Build release images / build-container (push) Successful in 1m16s
2025-04-08 20:01:13 +02:00
5415cd38a7
feat(runners): Generate selfservice urls on runner if requested or create/update/get single 2025-04-08 20:00:27 +02:00
175ba52ffa
chore(release): 1.3.4
All checks were successful
Build release images / build-container (push) Successful in 1m10s
2025-03-28 21:49:42 +01:00
5c5000a218
feat: add runnersViaSelfservice to statsControllerGet 2025-03-28 21:49:19 +01:00
d559d04031
chore(release): 1.3.3
All checks were successful
Build release images / build-container (push) Successful in 1m11s
2025-03-28 21:20:39 +01:00
2af682d1dd
ci: remove "v" prefix from tags 2025-03-28 21:20:30 +01:00
30905e481c
chore(release): v1.3.2
All checks were successful
Build release images / build-container (push) Successful in 1m17s
2025-03-28 21:16:41 +01:00
752d405bda
ci: pnpm@10.7 2025-03-28 21:16:28 +01:00
8fa4ed7c33
chore(release): v1.3.1
Some checks failed
Build release images / build-container (push) Failing after 6s
2025-03-28 21:15:27 +01:00
c4201e9a68
fix: TypeError: Cannot read properties of undefined (reading 'filter') - when trying to delete a org/team with runners
close #210
2025-03-28 21:14:14 +01:00
78dcad0857
pnpm@10.7, node@23, argon->@node-rs/argon2 2025-03-28 21:04:50 +01:00
93e0cdf577
chore(release): v1.3.0
Some checks failed
Build release images / build-container (push) Failing after 6s
2025-03-28 18:56:01 +01:00
6efcd94726
ci: change release commit message 2025-03-28 18:55:25 +01:00
2e271bcd52
fix: add .created_via to ResponseParticipant constructor 2025-03-28 18:24:53 +01:00
ebde8c6ffd
ci: move to gitea workflows 2025-03-28 18:07:10 +01:00
a3639dd89b feat: created_via for tracking how runners got into the system (#212)
close #211

squash merge please:)

Reviewed-on: #212
2025-03-28 17:05:10 +00:00
0a43f1bb5b
build: docker "AS" casing 2025-03-28 17:41:20 +01:00
8c6fdb2239
refactor(RunnerController.remove): only load necessary relations 2025-03-28 12:01:15 +01:00
c0d5af5d7a
refactor(RunnerTeamController.remove): only load necessary relations 2025-03-28 12:00:55 +01:00
4008a5ee72
chore(release): 1.2.1
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-12-11 22:23:58 +01:00
07bf28b144
refactor: allow selfservice link every 30s
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-12-11 22:22:54 +01:00
6764bf80ea
chore(release): 1.2.0
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-11 20:05:31 +01:00
b3a73b25e8
refactor(ci): Switch to new woodpecker 2024-12-11 20:05:07 +01:00
bda1f971d1 Merge pull request 'refactor: move to new mailer' (#209) from refactor/new-mailer into dev
Reviewed-on: #209
Reviewed-by: Nicolai Ort <info@nicolai-ort.com>
2024-12-11 19:04:11 +00:00
765ef84903
SELFSERVICE_URL 2024-12-11 18:43:11 +01:00
296ba8ddab
FRONTEND_URL env 2024-12-11 18:40:21 +01:00
6eff243803
feat: middlename 2024-12-11 18:34:40 +01:00
0f4c8b2051
refactor: move to new mailer 2024-12-11 18:26:57 +01:00
d842c14240
chore: update readme 2024-12-11 17:55:04 +01:00
a54cb287a4
🚀Bumped version to v1.1.4 2024-11-20 19:00:19 +01:00
74d334f9b7
fix(dependencies): Switch back to previous class-validator version to produce a working build 2024-11-20 18:59:50 +01:00
cd3cd81360
fix(deps): Bump sqlite3
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2023-11-06 20:32:05 +01:00
cf48c00ddb
fix(deps): Bumped argon2 to latest version for arm support
Some checks failed
ci/woodpecker/push/build Pipeline failed
2023-11-06 20:23:32 +01:00
3192365793
feat(ci)!: Switch to woodpecker
Some checks failed
ci/woodpecker/push/build Pipeline failed
2023-11-06 20:15:44 +01:00
075d484f11
ci: drop lfk-client-node 2023-11-06 18:09:31 +01:00
5082b1b8b1
fix: updated README for pnpm, typos 2023-11-06 18:04:32 +01:00
50dd703a1b
build: package lock 2023-11-06 18:01:15 +01:00
057a8ee699
🚀Bumped version to v1.1.3
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-10 13:38:14 +02:00
8d9418635d
feat(orgs): Also resolve child-teams' distances and add them to org total 2023-05-10 13:37:54 +02:00
f2832a2dae
fix(orgs): Removed unused log 2023-05-10 13:36:05 +02:00
0d21596e2b
🚀Bumped version to v1.1.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-10 13:16:33 +02:00
245827e9c6
feat(groups): Resolve the total group distance on group get single (aka get org and get team) 2023-05-10 13:15:59 +02:00
4608a36df6
chore(package): Formatting 2023-05-10 13:15:21 +02:00
cb1305aa77
🚀Bumped version to v1.1.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-19 18:10:52 +02:00
12a9ae2493
feat(donors): Resolve donations with donors via pagination 2023-04-19 18:10:26 +02:00
b9fe9f1c24
🚀Bumped version to v1.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-19 15:48:16 +02:00
b25b0db760
Added hints 2023-04-19 15:47:54 +02:00
fe59e3a557
Added average donation per distance to stats 2023-04-19 15:46:50 +02:00
42c23a5883
Formatting 2023-04-19 15:45:34 +02:00
6ee5328dbc
Added calls to controller 2023-04-19 15:41:49 +02:00
6f39ac42da
feat(stats): Added donation count and donor count to stats 2023-04-19 15:41:43 +02:00
301f334674
🚀Bumped version to v1.0.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-18 20:09:58 +02:00
fcee3909f4
fix(pagination) page=0 resulted in false thx JS 2023-04-18 20:09:44 +02:00
f0e20e4130
🚀Bumped version to v1.0.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-18 20:03:51 +02:00
80de188565
Merge pull request 'feature/205-pagination' (#206) from feature/205-pagination into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #206
Reviewed-by: Philipp Dormann <philipp@noreply.git.odit.services>
2023-04-18 18:03:20 +00:00
2f305e127c
Updated test for attribute
All checks were successful
continuous-integration/drone/pr Build is passing
2023-04-18 20:02:03 +02:00
513d7f6fba
usergroup pagination
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:44:15 +02:00
244da61892
users pagination
ref #205
2023-04-18 18:43:13 +02:00
2a72aea10e
Track pagination
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:41:57 +02:00
71ebce6f8e
statsclient pagination
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:40:45 +02:00
f60025b6de
scanstation pagination
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:39:37 +02:00
0fa663a341
RunnerTeam Pagination
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:38:27 +02:00
538622aa18
Added pagination for runner orgs
ref #205
2023-04-18 18:37:09 +02:00
86a21dbfa4
Get all pagination for permissions
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:35:25 +02:00
1e9e24d99d
Pagination for group contacts
ref #205
2023-04-18 18:34:08 +02:00
4493c0e3d9
Added pagination for get all donors
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:30:20 +02:00
f5d48fc638
Added pagination for donations
ref #205
2023-04-18 18:28:55 +02:00
b35a2dd2fa
Added pagination for runnercards
ref #205
2023-04-18 18:27:11 +02:00
a28ffe06e5
Formatting
ref #205
2023-04-18 18:21:09 +02:00
d873674819
Added pagination for runners
ref #205
2023-04-18 18:20:56 +02:00
37b2ac974b
Added pagination for get all scans
ref #205
2023-04-18 18:17:10 +02:00
81aed1de40
🚀Bumped version to v0.15.4
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 22:52:10 +02:00
0f0c3c7214
Fixed possible null
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 22:51:48 +02:00
3909ed34f7
🚀Bumped version to v0.15.3
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 22:36:09 +02:00
b2ac70e0ae
Faster stats (not including donations) 2023-04-15 22:35:55 +02:00
5f17e7f783
🚀Bumped version to v0.15.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 21:16:56 +02:00
a5a56a263a
Resolve groups again for card generation 2023-04-15 21:15:29 +02:00
2d8f7528d9
Don't resolve runner group and parten with get all card requests 2023-04-15 21:13:14 +02:00
9581185b24
🚀Bumped version to v0.15.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 21:11:56 +02:00
2905884c02
Log batch time in mass scan script 2023-04-15 21:11:32 +02:00
e9914e317b
Faster trackscan creation by only loading the latest scan 2023-04-15 21:08:08 +02:00
702070da66
Dont load cards with get all runners request 2023-04-15 20:55:22 +02:00
cc89ba8afb
🚀Bumped version to v0.15.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 20:51:28 +02:00
7c4ff42a3b
More scan request optimizations 2023-04-15 20:51:13 +02:00
8007117434
Added test script for creating mass scans 2023-04-15 20:50:48 +02:00
23fa78eb9d
Get all scans speed improvement 2023-04-15 20:31:52 +02:00
3b3e68900b
🚀Bumped version to v0.14.6
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 18:20:54 +02:00
3ff666fd3e
Missing orm file 2023-04-15 18:19:47 +02:00
4e4435010f
🚀Bumped version to v0.14.5
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 18:16:09 +02:00
de9af5a909
Entrypoint fix 2023-04-15 18:15:57 +02:00
ac631f0af4
Fixed copy 2023-04-15 18:13:58 +02:00
6bbdd5bb04
🚀Bumped version to v0.14.4
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build is passing
2023-04-15 18:09:49 +02:00
a8fc755840
Back to ean13 based codes
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is failing
2023-04-15 18:09:24 +02:00
27e74e824c
pinned pnpm to 8
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-04-12 14:12:05 +02:00
b5c0a288ac
coherent baseimage
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2023-03-29 20:35:16 +02:00
85dc3444ac
custom pnpm cache
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2023-03-29 20:29:56 +02:00
d02743984d
install prod in first step
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2023-03-29 20:29:08 +02:00
734c826fac
added missing ci env
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-29 20:27:01 +02:00
33b25c9743
bumped final pnpm version
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build is passing
2023-03-29 20:03:38 +02:00
6275aaa326
Switched ci over to pnpm + cache
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build is failing
2023-03-29 19:56:05 +02:00
2a94bfa622
pinned pnpm version 2023-03-29 19:53:42 +02:00
a64f6c9822
COPY by stage name 2023-03-29 19:52:59 +02:00
93d43b7684
Switched dockerfile to pnpm 8 with cache 2023-03-29 19:52:31 +02:00
16ce0a8480
🚀Bumped version to v0.14.3
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-18 22:15:02 +01:00
9a8d618ae4
Adjusted modulo for new fixed card length 2023-03-18 22:14:50 +01:00
38da2d3318
🚀Bumped version to v0.14.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-18 21:55:23 +01:00
068deb4960
Back to modulo 2023-03-18 21:55:10 +01:00
13f093bb61
🚀Bumped version to v0.14.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-18 21:46:38 +01:00
6289f30740
Switched from card prefix replacement via modulo to regex 2023-03-18 21:46:21 +01:00
83 changed files with 10679 additions and 437 deletions

View File

@ -1,175 +0,0 @@
---
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: pipeline
type: kubernetes
name: tests:node
clone:
disable: true
steps:
- name: checkout pr
image: alpine/git
commands:
- git clone $DRONE_REMOTE_URL .
- git checkout $DRONE_SOURCE_BRANCH
- name: run tests
image: registry.odit.services/hub/library/node:19.5.0-alpine3.16
commands:
- yarn
- yarn test:ci
trigger:
event:
- pull_request
---
kind: pipeline
type: kubernetes
name: build:dev
clone:
disable: true
steps:
- name: clone
image: alpine/git
commands:
- git clone $DRONE_REMOTE_URL .
- git checkout dev
- name: build dev
depends_on: ["clone"]
image: registry.odit.services/library/drone-kaniko
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
build_args:
- NPM_REGISTRY_DOMAIN:
from_secret: npmjs_domain
- NPM_REGISTRY_TOKEN:
from_secret: npmjs_token
repo: lfk/backend
tags:
- dev
cache: true
registry: registry.odit.services
trigger:
branch:
- dev
event:
- push
---
kind: pipeline
type: kubernetes
name: build:latest
clone:
disable: true
steps:
- name: clone
image: alpine/git
commands:
- git clone $DRONE_REMOTE_URL .
- git checkout dev
- git merge main
- git checkout main
- name: build latest
depends_on: ["clone"]
image: registry.odit.services/library/drone-kaniko
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
build_args:
- NPM_REGISTRY_DOMAIN:
from_secret: npmjs_domain
- NPM_REGISTRY_TOKEN:
from_secret: npmjs_token
repo: lfk/backend
tags:
- latest
cache: true
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:
branch:
- main
event:
- push
---
kind: pipeline
type: kubernetes
name: build:tags
steps:
- name: build $DRONE_TAG
depends_on: ["clone"]
image: registry.odit.services/library/drone-kaniko
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
build_args:
- NPM_REGISTRY_DOMAIN:
from_secret: npmjs_domain
- NPM_REGISTRY_TOKEN:
from_secret: npmjs_token
repo: lfk/backend
tags:
- "${DRONE_TAG}"
cache: true
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: ci_token
- name: trigger js lib build
image: idcooldi/drone-webhook
settings:
urls: https://ci.odit.services/api/repos/lfk/lfk-client-js/builds?SOURCE_TAG=${DRONE_TAG}
bearer:
from_secret: ci_token
trigger:
event:
- tag

View File

@ -7,4 +7,5 @@ DB_PASSWORD=bla
DB_NAME=./test.sqlite
NODE_ENV=production
POSTALCODE_COUNTRYCODE=DE
SEED_TEST_DATA=false
SEED_TEST_DATA=false
SELFSERVICE_URL=bla

View File

@ -0,0 +1,33 @@
name: Build release images
on:
push:
tags:
- "*.*.*"
jobs:
build-container:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 19
- run: npm i -g pnpm@10.7 && pnpm i
- run: pnpm licenses:export
- name: Login to registry
uses: docker/login-action@v3
with:
registry: registry.odit.services
username: ${{ vars.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
${{ vars.REGISTRY }}/lfk/backend:${{ github.ref_name }}
platforms: linux/amd64,linux/arm64

1
.gitignore vendored
View File

@ -136,4 +136,3 @@ build
lib
/oss-attribution
*.tmp
pnpm-lock.yaml

View File

@ -9,8 +9,7 @@
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features",
"editor.codeActionsOnSave": {
"source.organizeImports": true,
// "source.fixAll": true
"source.organizeImports": "explicit"
}
},
"javascript.preferences.quoteStyle": "single",

View File

@ -2,8 +2,354 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [1.5.2](https://git.odit.services/lfk/backend/compare/1.5.1...1.5.2)
- feat(mailer): Add logging for selfservice forgotten mail requests [`eebcc2e`](https://git.odit.services/lfk/backend/commit/eebcc2e3284230135e3911b4edaecd1a9cfd2100)
- feat(mailer): Log error message when sending selfservice forgotten mail fails [`0f532b1`](https://git.odit.services/lfk/backend/commit/0f532b139c2bc5cd89ca2dbff0867825a9363250)
#### [1.5.1](https://git.odit.services/lfk/backend/compare/1.5.0...1.5.1)
> 26 May 2025
- chore(release): 1.5.1 [`284954d`](https://git.odit.services/lfk/backend/commit/284954d064f09951c13584e9d50a83be2c4b9f72)
- feat(mailer): Log error when sending selfservice forgotten mail fails [`401ca92`](https://git.odit.services/lfk/backend/commit/401ca923a61bc5988e73209c086bc9a5a4fa04f9)
#### [1.5.0](https://git.odit.services/lfk/backend/compare/1.4.3...1.5.0)
> 6 May 2025
- feat(entities): Added created/updated at to all entities [`728f8a1`](https://git.odit.services/lfk/backend/commit/728f8a14e9fb7360fce92640bfa5658af8cadb4f)
- feat(responses): Added created_at/updated_at [`f225cc4`](https://git.odit.services/lfk/backend/commit/f225cc49548605de48cf6c6e6f7c86b163236545)
- feat(participants): Added created/updated at [`a448058`](https://git.odit.services/lfk/backend/commit/a4480589a0e23a4481332ab5efa0777c62bbab56)
- chore(release): 1.5.0 [`bf1f641`](https://git.odit.services/lfk/backend/commit/bf1f6411e0f5113842a537f5bcf632638bdf1048)
#### [1.4.3](https://git.odit.services/lfk/backend/compare/1.4.2...1.4.3)
> 1 May 2025
- feat(runners): Include collected distance donation amount in runner detail [`4494afc`](https://git.odit.services/lfk/backend/commit/4494afc64b433d26b54a293fe156d13c40faad95)
- chore(release): 1.4.3 [`0ad9eeb`](https://git.odit.services/lfk/backend/commit/0ad9eeb52f18af3ea7d86fe1bf15edb04f4cfd2d)
#### [1.4.2](https://git.odit.services/lfk/backend/compare/1.4.1...1.4.2)
> 1 May 2025
- fix(donations): Fixed creation bug [`07a0195`](https://git.odit.services/lfk/backend/commit/07a0195f125519f239d255a0cc081ddbde8f1da3)
- chore(release): 1.4.2 [`f4747c5`](https://git.odit.services/lfk/backend/commit/f4747c51de71d9b28cca1b00a91de3cfd6f0f56e)
#### [1.4.1](https://git.odit.services/lfk/backend/compare/1.4.0...1.4.1)
> 28 April 2025
- chore(release): 1.4.1 [`7ac9822`](https://git.odit.services/lfk/backend/commit/7ac98229d17e7cb019d5dcc5402870490a97f910)
- refactor(auth): Increased token timeouts to 24hrs/7days [`dd5b538`](https://git.odit.services/lfk/backend/commit/dd5b538783f9c806f0c883cd391754fb5c842ec8)
#### [1.4.0](https://git.odit.services/lfk/backend/compare/1.3.12...1.4.0)
> 28 April 2025
- feat(donations): Implement response type to indicate possible missing donor [`f4bf309`](https://git.odit.services/lfk/backend/commit/f4bf309821c140f2bc0ae8b6d96c7458fcc80978)
- wip [`9875b4f`](https://git.odit.services/lfk/backend/commit/9875b4f3926e04b502e7af64c17f54fd3c1d8e3e)
- refactor(donations): Make anon prepaid [`02b1cb9`](https://git.odit.services/lfk/backend/commit/02b1cb9904cc593faeac025ae302a8684f650f5e)
- chore(release): 1.4.0 [`8e6d674`](https://git.odit.services/lfk/backend/commit/8e6d67428c85b6ee504a379ff13a3a951f7b9543)
- fix(donations): Move donor over to the types that need it [`7697acf`](https://git.odit.services/lfk/backend/commit/7697acff82b23d0c05dbbd17fee6e70eb1b7061c)
#### [1.3.12](https://git.odit.services/lfk/backend/compare/1.3.11...1.3.12)
> 28 April 2025
- chore(release): 1.3.12 [`bacfc43`](https://git.odit.services/lfk/backend/commit/bacfc437f97cac6a20c32b79ae2d6391466f78a6)
- refactor: make Donation.donor optional [`2ab6e98`](https://git.odit.services/lfk/backend/commit/2ab6e985e356f0f3d8637d81630d191cc11b8806)
- refactor(config): improve consola error logs [`ce9b765`](https://git.odit.services/lfk/backend/commit/ce9b765b81b014623e79ce64d8d835f1f86cecf3)
#### [1.3.11](https://git.odit.services/lfk/backend/compare/1.3.10...1.3.11)
> 17 April 2025
- feat(RunnerController): add selfservice_links parameter to getRunners method [`a50d72f`](https://git.odit.services/lfk/backend/commit/a50d72f2f5281b8c28ca64a0970161a35a7af95a)
- chore(release): 1.3.11 [`d06f6a4`](https://git.odit.services/lfk/backend/commit/d06f6a44072971d1853411b255f9b49eb423b3a2)
#### [1.3.10](https://git.odit.services/lfk/backend/compare/1.3.9...1.3.10)
> 11 April 2025
- chore(release): 1.3.10 [`4723d97`](https://git.odit.services/lfk/backend/commit/4723d9738eacd63fb41f23c628fbe4181bd126de)
- feat(RunnerController.getAll): debug created_via query param filter [`1a478bd`](https://git.odit.services/lfk/backend/commit/1a478bd784e01b9d5a1c6635d1004a9535c9a0e9)
#### [1.3.9](https://git.odit.services/lfk/backend/compare/1.3.8...1.3.9)
> 9 April 2025
- feat(RunnerController.getAll): add created_via query param filter [`6e63c57`](https://git.odit.services/lfk/backend/commit/6e63c57936f06a29da5f1a94b1141d51b75df5f0)
- chore(release): 1.3.9 [`284cb0f`](https://git.odit.services/lfk/backend/commit/284cb0f8b3955d0d65c2b36d2ec427a39752ffe7)
#### [1.3.8](https://git.odit.services/lfk/backend/compare/1.3.7...1.3.8)
> 9 April 2025
- feat(RunnerCardController): putByCode [`8237d5f`](https://git.odit.services/lfk/backend/commit/8237d5f21067c0872a7eff7c8d1506edf44ec10c)
- chore(release): 1.3.8 [`30b61db`](https://git.odit.services/lfk/backend/commit/30b61db2c160c019bac381f26cefdc6524ea465e)
#### [1.3.7](https://git.odit.services/lfk/backend/compare/1.3.6...1.3.7)
> 8 April 2025
- feat(stats): Publish runners by kiosk stat [`a6afba9`](https://git.odit.services/lfk/backend/commit/a6afba93e243ca419c282a16cad023d06d864e0e)
- chore(release): 1.3.7 [`03e0a29`](https://git.odit.services/lfk/backend/commit/03e0a290965648579956ac1f8e8542c97a667ed8)
#### [1.3.6](https://git.odit.services/lfk/backend/compare/1.3.5...1.3.6)
> 8 April 2025
- chore(release): 1.3.6 [`a41758c`](https://git.odit.services/lfk/backend/commit/a41758cd9c83105c3a4b407744bafe2f0f6fb48a)
- feat(runners): Allow created via being set via api [`d6755ed`](https://git.odit.services/lfk/backend/commit/d6755ed134071df635bc9d5821ceb2396c0f1d22)
- fix(participant): Switch to correct type [`599c75f`](https://git.odit.services/lfk/backend/commit/599c75fc00217eaec3cc87c0de50d059bdde685f)
#### [1.3.5](https://git.odit.services/lfk/backend/compare/1.3.4...1.3.5)
> 8 April 2025
- feat(runners): Generate selfservice urls on runner if requested or create/update/get single [`5415cd3`](https://git.odit.services/lfk/backend/commit/5415cd38a727e76632a01a4d2634a1777df5542c)
- chore(release): 1.3.5 [`bb213f0`](https://git.odit.services/lfk/backend/commit/bb213f001eff2157abf8741128f624f9cc991afe)
#### [1.3.4](https://git.odit.services/lfk/backend/compare/1.3.3...1.3.4)
> 28 March 2025
- feat: add runnersViaSelfservice to statsControllerGet [`5c5000a`](https://git.odit.services/lfk/backend/commit/5c5000a218b47815e6846ac8b857dcd1995bfa6f)
- chore(release): 1.3.4 [`175ba52`](https://git.odit.services/lfk/backend/commit/175ba52ffae8e6ba1fdc1603ac2f5eba15602046)
#### [1.3.3](https://git.odit.services/lfk/backend/compare/v1.3.2...1.3.3)
> 28 March 2025
- chore(release): 1.3.3 [`d559d04`](https://git.odit.services/lfk/backend/commit/d559d0403191c703fd6da0e3f3dab53eec9258c0)
- ci: remove "v" prefix from tags [`2af682d`](https://git.odit.services/lfk/backend/commit/2af682d1dd09df496eb9f3a9111c50c0c4117356)
#### [v1.3.2](https://git.odit.services/lfk/backend/compare/v1.3.1...v1.3.2)
> 28 March 2025
- chore(release): v1.3.2 [`30905e4`](https://git.odit.services/lfk/backend/commit/30905e481c69cfe62b4261544b4277de3a1a43c2)
- ci: pnpm@10.7 [`752d405`](https://git.odit.services/lfk/backend/commit/752d405bda9129f3cd288a956d5444cab316c2af)
#### [v1.3.1](https://git.odit.services/lfk/backend/compare/1.3.0...v1.3.1)
> 28 March 2025
- fix: TypeError: Cannot read properties of undefined (reading 'filter') - when trying to delete a org/team with runners [`#210`](https://git.odit.services/lfk/backend/issues/210)
- pnpm@10.7, node@23, argon-&gt;@node-rs/argon2 [`78dcad0`](https://git.odit.services/lfk/backend/commit/78dcad085794c93829499dd550a786c38d6186f5)
- chore(release): v1.3.1 [`8fa4ed7`](https://git.odit.services/lfk/backend/commit/8fa4ed7c3319c3e56a71701ba266ceda64d2ef69)
#### [1.3.0](https://git.odit.services/lfk/backend/compare/1.2.1...1.3.0)
> 28 March 2025
- feat: created_via for tracking how runners got into the system [`#212`](https://git.odit.services/lfk/backend/pull/212)
- feat: created_via for tracking how runners got into the system (#212) [`#211`](https://git.odit.services/lfk/backend/issues/211)
- ci: move to gitea workflows [`ebde8c6`](https://git.odit.services/lfk/backend/commit/ebde8c6ffd8b17c6752da8c4d8eb3095105f6132)
- chore(release): v1.3.0 [`93e0cdf`](https://git.odit.services/lfk/backend/commit/93e0cdf577654898b2d63790d91598c458a2db59)
- build: docker "AS" casing [`0a43f1b`](https://git.odit.services/lfk/backend/commit/0a43f1bb5b26d3acb0d4d91648473f0dc55e8637)
- ci: change release commit message [`6efcd94`](https://git.odit.services/lfk/backend/commit/6efcd94726957b8c527820f1a9b0130151ce22f1)
- refactor(RunnerController.remove): only load necessary relations [`8c6fdb2`](https://git.odit.services/lfk/backend/commit/8c6fdb22390218e385780fadb3bdaf32148ac054)
- refactor(RunnerTeamController.remove): only load necessary relations [`c0d5af5`](https://git.odit.services/lfk/backend/commit/c0d5af5d7ab44cfdf19014e0d774fb560d08f6d7)
- fix: add .created_via to ResponseParticipant constructor [`2e271bc`](https://git.odit.services/lfk/backend/commit/2e271bcd52f02ab7449cd15916b0afc86e8b0a90)
#### [1.2.1](https://git.odit.services/lfk/backend/compare/1.2.0...1.2.1)
> 11 December 2024
- refactor: allow selfservice link every 30s [`07bf28b`](https://git.odit.services/lfk/backend/commit/07bf28b14458849930748ce041fb65e572759482)
- chore(release): 1.2.1 [`4008a5e`](https://git.odit.services/lfk/backend/commit/4008a5ee720b212bac9cba64417058bf4526060b)
#### [1.2.0](https://git.odit.services/lfk/backend/compare/v1.1.4...1.2.0)
> 11 December 2024
- refactor: move to new mailer [`0f4c8b2`](https://git.odit.services/lfk/backend/commit/0f4c8b2051cae17fbdd7e02017ad5b41c61e210c)
- refactor(ci): Switch to new woodpecker [`b3a73b2`](https://git.odit.services/lfk/backend/commit/b3a73b25e80a0466ff83e43481271fc0cd499a0d)
- feat: middlename [`6eff243`](https://git.odit.services/lfk/backend/commit/6eff2438035b368eb45931fad9402a6cb942b350)
- SELFSERVICE_URL [`765ef84`](https://git.odit.services/lfk/backend/commit/765ef849035ca4f8b2253bb76d15be8e9a3e6763)
- FRONTEND_URL env [`296ba8d`](https://git.odit.services/lfk/backend/commit/296ba8ddab1dba46f8201829d9a7e5fc1c88c0f8)
- chore: update readme [`d842c14`](https://git.odit.services/lfk/backend/commit/d842c14240fb4a7f70c66143bbe877f8168ef6d4)
- chore(release): 1.2.0 [`6764bf8`](https://git.odit.services/lfk/backend/commit/6764bf80eac832d186e688319d8a959543a1495f)
- Merge pull request 'refactor: move to new mailer' (#209) from refactor/new-mailer into dev [`bda1f97`](https://git.odit.services/lfk/backend/commit/bda1f971d1a14ea403439533c7ae31280c7df167)
#### [v1.1.4](https://git.odit.services/lfk/backend/compare/v1.1.3...v1.1.4)
> 20 November 2024
- build: package lock [`50dd703`](https://git.odit.services/lfk/backend/commit/50dd703a1bd276a607cc10a087c7e90fd880847a)
- fix(deps): Bump sqlite3 [`cd3cd81`](https://git.odit.services/lfk/backend/commit/cd3cd81360777e8bc4d78e861354e58c8da79cc7)
- feat(ci)!: Switch to woodpecker [`3192365`](https://git.odit.services/lfk/backend/commit/3192365793fae59f2b89e3231db298654f0a28e9)
- fix(deps): Bumped argon2 to latest version for arm support [`cf48c00`](https://git.odit.services/lfk/backend/commit/cf48c00ddb2ac33263549876928db50ae152c12d)
- fix: updated README for pnpm, typos [`5082b1b`](https://git.odit.services/lfk/backend/commit/5082b1b8b1c0ae9e8ffa9c71c4d7923fd9223c87)
- 🚀Bumped version to v1.1.4 [`a54cb28`](https://git.odit.services/lfk/backend/commit/a54cb287a4323ac8de77f51711cc6c52ec290859)
- ci: drop lfk-client-node [`075d484`](https://git.odit.services/lfk/backend/commit/075d484f1169bfc5c5b68cb9712116b0e270b471)
- fix(dependencies): Switch back to previous class-validator version to produce a working build [`74d334f`](https://git.odit.services/lfk/backend/commit/74d334f9b747a77115bd9b97729ef1120822e128)
#### [v1.1.3](https://git.odit.services/lfk/backend/compare/v1.1.2...v1.1.3)
> 10 May 2023
- 🚀Bumped version to v1.1.3 [`057a8ee`](https://git.odit.services/lfk/backend/commit/057a8ee699d08c0e4a80cb50a8820f819569c9ac)
- feat(orgs): Also resolve child-teams' distances and add them to org total [`8d94186`](https://git.odit.services/lfk/backend/commit/8d9418635d3e381c0f55a2521a3334ba497c169a)
- fix(orgs): Removed unused log [`f2832a2`](https://git.odit.services/lfk/backend/commit/f2832a2daecc7bc7bbee4d4fceeab8db194730cf)
#### [v1.1.2](https://git.odit.services/lfk/backend/compare/v1.1.1...v1.1.2)
> 10 May 2023
- 🚀Bumped version to v1.1.2 [`0d21596`](https://git.odit.services/lfk/backend/commit/0d21596e2b64a99258d4925ae2ad627d5cdbd984)
- feat(groups): Resolve the total group distance on group get single (aka get org and get team) [`245827e`](https://git.odit.services/lfk/backend/commit/245827e9c659cf76183dc33ab253becc22ddf032)
- chore(package): Formatting [`4608a36`](https://git.odit.services/lfk/backend/commit/4608a36df6b187520ca0c331b8dce615205257be)
#### [v1.1.1](https://git.odit.services/lfk/backend/compare/v1.1.0...v1.1.1)
> 19 April 2023
- feat(donors): Resolve donations with donors via pagination [`12a9ae2`](https://git.odit.services/lfk/backend/commit/12a9ae24933117acb3ff9815a7d72abca5eea7a7)
- 🚀Bumped version to v1.1.1 [`cb1305a`](https://git.odit.services/lfk/backend/commit/cb1305aa77c36aa9d7900f09e7413bc6d45f2c89)
#### [v1.1.0](https://git.odit.services/lfk/backend/compare/v1.0.1...v1.1.0)
> 19 April 2023
- feat(stats): Added donation count and donor count to stats [`6f39ac4`](https://git.odit.services/lfk/backend/commit/6f39ac42dafc2a589bbb2256b0417f3e774ae174)
- 🚀Bumped version to v1.1.0 [`b9fe9f1`](https://git.odit.services/lfk/backend/commit/b9fe9f1c24653b91255a6dbbdc32c30b1b411eeb)
- Added average donation per distance to stats [`fe59e3a`](https://git.odit.services/lfk/backend/commit/fe59e3a557903cf555d4c50098e935c49ca1fac4)
- Added hints [`b25b0db`](https://git.odit.services/lfk/backend/commit/b25b0db76071ef8d50cc60e950a399dc060a2a9f)
- Added calls to controller [`6ee5328`](https://git.odit.services/lfk/backend/commit/6ee5328dbc404603d19db3a5173ae4def560a9c9)
- Formatting [`42c23a5`](https://git.odit.services/lfk/backend/commit/42c23a5883dacda4e0147842d448b3ad35b197b1)
#### [v1.0.1](https://git.odit.services/lfk/backend/compare/v1.0.0...v1.0.1)
> 18 April 2023
- fix(pagination) page=0 resulted in false thx JS [`fcee390`](https://git.odit.services/lfk/backend/commit/fcee3909f4c4664115cc7ecb94f30e0dd8e78ce0)
- 🚀Bumped version to v1.0.1 [`301f334`](https://git.odit.services/lfk/backend/commit/301f33467489a8533bdac11fbd10efd1b791f5e3)
### [v1.0.0](https://git.odit.services/lfk/backend/compare/v0.15.4...v1.0.0)
> 18 April 2023
- 🚀Bumped version to v1.0.0 [`f0e20e4`](https://git.odit.services/lfk/backend/commit/f0e20e413014fe446c97754d2765cdad92c2cc3b)
- Merge pull request 'feature/205-pagination' (#206) from feature/205-pagination into dev [`80de188`](https://git.odit.services/lfk/backend/commit/80de188565523d642407612272432ef07672b890)
- Added pagination for runner orgs [`538622a`](https://git.odit.services/lfk/backend/commit/538622aa1841e27256f304e15b4204c2f6d24d76)
- RunnerTeam Pagination [`0fa663a`](https://git.odit.services/lfk/backend/commit/0fa663a34104d438dd8fc9ab02458fdf289329f8)
- users pagination [`244da61`](https://git.odit.services/lfk/backend/commit/244da618926377f58bb12dbbd89b7bb39d84596e)
- Track pagination [`2a72aea`](https://git.odit.services/lfk/backend/commit/2a72aea10ef940fbdd4a9e6137b22933fdec7734)
- usergroup pagination [`513d7f6`](https://git.odit.services/lfk/backend/commit/513d7f6fbaebe39beab6ec95e6e42eb10c62296d)
- statsclient pagination [`71ebce6`](https://git.odit.services/lfk/backend/commit/71ebce6f8eebf110bb973a53b91dd6a49e1def99)
- scanstation pagination [`f60025b`](https://git.odit.services/lfk/backend/commit/f60025b6de79b0f5f89995bf59260194f5de9af0)
- Get all pagination for permissions [`86a21db`](https://git.odit.services/lfk/backend/commit/86a21dbfa4b50d8e80c611ea6e3eabfc2b8ae365)
- Pagination for group contacts [`1e9e24d`](https://git.odit.services/lfk/backend/commit/1e9e24d99d75ce6dc846ff662e62c886646ea974)
- Added pagination for get all donors [`4493c0e`](https://git.odit.services/lfk/backend/commit/4493c0e3d9beebbf7f601b39e1a2579771b4d152)
- Added pagination for donations [`f5d48fc`](https://git.odit.services/lfk/backend/commit/f5d48fc638080c9333efe474d86f131794c809af)
- Added pagination for runnercards [`b35a2dd`](https://git.odit.services/lfk/backend/commit/b35a2dd2fab708253373b3326f11ab574be18371)
- Added pagination for runners [`d873674`](https://git.odit.services/lfk/backend/commit/d873674819e6cb33cf89da4f8fdc30a0b41707e4)
- Added pagination for get all scans [`37b2ac9`](https://git.odit.services/lfk/backend/commit/37b2ac974b2276efd13538c127ba5ddda2537fe3)
- Updated test for attribute [`2f305e1`](https://git.odit.services/lfk/backend/commit/2f305e127c75e9e6ff8e9fc0cfc10cc3db44759d)
- Formatting [`a28ffe0`](https://git.odit.services/lfk/backend/commit/a28ffe06e5f3f69e4af6fdf0c66c9a1dfda10cfa)
#### [v0.15.4](https://git.odit.services/lfk/backend/compare/v0.15.3...v0.15.4)
> 15 April 2023
- Fixed possible null [`0f0c3c7`](https://git.odit.services/lfk/backend/commit/0f0c3c7214f357d991518aafd015ffc4d387ce59)
- 🚀Bumped version to v0.15.4 [`81aed1d`](https://git.odit.services/lfk/backend/commit/81aed1de40166f4cefabdb478d7638017127b25c)
#### [v0.15.3](https://git.odit.services/lfk/backend/compare/v0.15.2...v0.15.3)
> 15 April 2023
- Faster stats (not including donations) [`b2ac70e`](https://git.odit.services/lfk/backend/commit/b2ac70e0aec1064e54a5043a104e7892984b2338)
- 🚀Bumped version to v0.15.3 [`3909ed3`](https://git.odit.services/lfk/backend/commit/3909ed34f739e9fee90828f16757c75da90bab0f)
#### [v0.15.2](https://git.odit.services/lfk/backend/compare/v0.15.1...v0.15.2)
> 15 April 2023
- 🚀Bumped version to v0.15.2 [`5f17e7f`](https://git.odit.services/lfk/backend/commit/5f17e7f783a7e8e2efc8f7dbbf2c98bcd1d80240)
- Don't resolve runner group and parten with get all card requests [`2d8f752`](https://git.odit.services/lfk/backend/commit/2d8f7528d98144832e7609f5aa6fac8de4723c4a)
- Resolve groups again for card generation [`a5a56a2`](https://git.odit.services/lfk/backend/commit/a5a56a263a01dbd911a799ab57084166e17b80ac)
#### [v0.15.1](https://git.odit.services/lfk/backend/compare/v0.15.0...v0.15.1)
> 15 April 2023
- 🚀Bumped version to v0.15.1 [`9581185`](https://git.odit.services/lfk/backend/commit/9581185b24039338e7f238ecdcc3881bb5203759)
- Faster trackscan creation by only loading the latest scan [`e9914e3`](https://git.odit.services/lfk/backend/commit/e9914e317b7fd78863cfd8549bad65da9292b7ca)
- Log batch time in mass scan script [`2905884`](https://git.odit.services/lfk/backend/commit/2905884c024d7f275b3ad2c2858a2f0911adb95b)
- Dont load cards with get all runners request [`702070d`](https://git.odit.services/lfk/backend/commit/702070da669cc605b93e6f5b62d712c28f079dd0)
#### [v0.15.0](https://git.odit.services/lfk/backend/compare/v0.14.6...v0.15.0)
> 15 April 2023
- Added test script for creating mass scans [`8007117`](https://git.odit.services/lfk/backend/commit/80071174342d87199fcbd981cd8c92300b0a51e4)
- 🚀Bumped version to v0.15.0 [`cc89ba8`](https://git.odit.services/lfk/backend/commit/cc89ba8afb3120569613a889baf962555612e95a)
- Get all scans speed improvement [`23fa78e`](https://git.odit.services/lfk/backend/commit/23fa78eb9dcc01ecc036347f6703aacc0d163d7d)
- More scan request optimizations [`7c4ff42`](https://git.odit.services/lfk/backend/commit/7c4ff42a3b3e7b186e16c85a97d9ecc854a32cb0)
#### [v0.14.6](https://git.odit.services/lfk/backend/compare/v0.14.5...v0.14.6)
> 15 April 2023
- 🚀Bumped version to v0.14.6 [`3b3e689`](https://git.odit.services/lfk/backend/commit/3b3e68900beca16cfff88dbef22540f77750d29b)
- Missing orm file [`3ff666f`](https://git.odit.services/lfk/backend/commit/3ff666fd3e84ac8cf41b30e9e17082b10548d55b)
#### [v0.14.5](https://git.odit.services/lfk/backend/compare/v0.14.4...v0.14.5)
> 15 April 2023
- 🚀Bumped version to v0.14.5 [`4e44350`](https://git.odit.services/lfk/backend/commit/4e4435010fd7095e3b9742e207cba1b68cd6da3b)
- Entrypoint fix [`de9af5a`](https://git.odit.services/lfk/backend/commit/de9af5a90907dcfc9bfb1d5a56420eed8bb59922)
- Fixed copy [`ac631f0`](https://git.odit.services/lfk/backend/commit/ac631f0af467446552478873b7b4802a9310f865)
#### [v0.14.4](https://git.odit.services/lfk/backend/compare/v0.14.3...v0.14.4)
> 15 April 2023
- Switched ci over to pnpm + cache [`6275aaa`](https://git.odit.services/lfk/backend/commit/6275aaa326f1c02c8dd42aa31608978408c44ab7)
- 🚀Bumped version to v0.14.4 [`6bbdd5b`](https://git.odit.services/lfk/backend/commit/6bbdd5bb04a1c38e4b3a150db24b76e9c96490dd)
- Back to ean13 based codes [`a8fc755`](https://git.odit.services/lfk/backend/commit/a8fc7558408b97da4b2c469ae5e73ab502b4fda0)
- install prod in first step [`d027439`](https://git.odit.services/lfk/backend/commit/d02743984dfea8057be3081bd3a32a8f67e610aa)
- Switched dockerfile to pnpm 8 with cache [`93d43b7`](https://git.odit.services/lfk/backend/commit/93d43b76843d7cb411f37fd2066c6a5364c05415)
- COPY by stage name [`a64f6c9`](https://git.odit.services/lfk/backend/commit/a64f6c9822af2b927e91b0b55f1f50176de30169)
- pinned pnpm version [`2a94bfa`](https://git.odit.services/lfk/backend/commit/2a94bfa6227d14f635b5fc2789b59c36d490937e)
- custom pnpm cache [`85dc344`](https://git.odit.services/lfk/backend/commit/85dc3444acc677ddd242f9f2543ce477fe427a7c)
- added missing ci env [`734c826`](https://git.odit.services/lfk/backend/commit/734c826face58dd5c3bb2607bda6e7f6d051012e)
- pinned pnpm to 8 [`27e74e8`](https://git.odit.services/lfk/backend/commit/27e74e824cd1e23d4d53c1a983a1668dd87f5d59)
- coherent baseimage [`b5c0a28`](https://git.odit.services/lfk/backend/commit/b5c0a288ac3c020f5d753c558aee160fea0bae14)
- bumped final pnpm version [`33b25c9`](https://git.odit.services/lfk/backend/commit/33b25c9743abb7cefb3538f08cc2f78a646905c8)
#### [v0.14.3](https://git.odit.services/lfk/backend/compare/v0.14.2...v0.14.3)
> 18 March 2023
- 🚀Bumped version to v0.14.3 [`16ce0a8`](https://git.odit.services/lfk/backend/commit/16ce0a848050b74c4b6dd93f17e5a6e9024cdb7d)
- Adjusted modulo for new fixed card length [`9a8d618`](https://git.odit.services/lfk/backend/commit/9a8d618ae4584640e8be1ce9fe4bddd2ef7a92ae)
#### [v0.14.2](https://git.odit.services/lfk/backend/compare/v0.14.1...v0.14.2)
> 18 March 2023
- 🚀Bumped version to v0.14.2 [`38da2d3`](https://git.odit.services/lfk/backend/commit/38da2d33187f4b24eef878642e153663ecd95de1)
- Back to modulo [`068deb4`](https://git.odit.services/lfk/backend/commit/068deb4960bd16decf99887ffbda7a7d3dd9ff0b)
#### [v0.14.1](https://git.odit.services/lfk/backend/compare/v0.14.0...v0.14.1)
> 18 March 2023
- 🚀Bumped version to v0.14.1 [`13f093b`](https://git.odit.services/lfk/backend/commit/13f093bb6138a498f93a05ef6dd812ae92f2676a)
- Switched from card prefix replacement via modulo to regex [`6289f30`](https://git.odit.services/lfk/backend/commit/6289f307400aacaa9cfe03f3024c1e0d5554d4f2)
#### [v0.14.0](https://git.odit.services/lfk/backend/compare/v0.13.3...v0.14.0)
> 15 March 2023
- 🚀Bumped version to v0.14.0 [`6ff764b`](https://git.odit.services/lfk/backend/commit/6ff764bc340ca25b3bdd62c6892259e228723973)
- Updated default length [`ea87cc7`](https://git.odit.services/lfk/backend/commit/ea87cc793b163bf0d4405a25bbe83fbc8e31c206)
- breaking(runnercards): shorter runnercard codes (padding to 12 was a bit tooo ambitious) [`ffee887`](https://git.odit.services/lfk/backend/commit/ffee887ddf6a71102ee39533d7cd504d1fd6698f)
- Removed sqlite journal [`92517e3`](https://git.odit.services/lfk/backend/commit/92517e365393f4baac3814f5668874b5752dc7c8)

View File

@ -1,15 +1,27 @@
# Typescript Build
FROM registry.odit.services/hub/library/node:19.5.0-alpine3.16
FROM registry.odit.services/hub/library/node:23.10.0-alpine3.21 AS build
ARG NPM_REGISTRY_URL=https://registry.npmjs.org
WORKDIR /app
COPY package.json ./
RUN npx pnpm@7.26.3 i
COPY pnpm-workspace.yaml ./
COPY pnpm-lock.yaml ./
RUN npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@10.7
RUN mkdir /pnpm && pnpm config set store-dir /pnpm && pnpm i
COPY tsconfig.json ormconfig.js ./
COPY src ./src
RUN npm run build
RUN pnpm run build \
&& rm -rf /app/node_modules \
&& pnpm i --production --prefer-offline
# final image
FROM registry.odit.services/hub/library/node:19.5.0-alpine3.16
COPY package.json ormconfig.js ./
RUN npx pnpm@7.26.3 i --prod
COPY --from=0 /app/dist dist
ENTRYPOINT ["node", "dist/app.js"]
FROM registry.odit.services/hub/library/node:23.10.0-alpine3.21 AS final
WORKDIR /app
COPY --from=build /app/package.json /app/package.json
COPY --from=build /app/pnpm-lock.yaml /app/pnpm-lock.yaml
COPY --from=build /app/pnpm-workspace.yaml /app/pnpm-workspace.yaml
COPY --from=build /app/ormconfig.js /app/ormconfig.js
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
ENTRYPOINT ["node", "/app/dist/app.js"]

View File

@ -15,36 +15,29 @@ Backend Server
1. Rename the .env.example file to .env (you can adjust app port and other settings, if needed)
2. Install Dependencies
```bash
yarn
```
```bash
pnpm i
```
3. Start the server
```bash
yarn dev
```
```bash
pnpm dev
```
### Run Tests
```bash
# Run tests once (server has to run)
yarn test
pnpm test
# Run test in watch mode (reruns on change)
yarn test:watch
pnpm test:watch
# Run test in ci mode (automaticly starts the dev server)
yarn test:ci
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
```bash
yarn docs
pnpm docs
```
## ENV Vars
@ -66,6 +59,7 @@ yarn docs
| 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. |
| SELFSERVICE_URL | String(Url) | N/A | The link to selfservice (no trailing slash) |
| 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) |
@ -90,5 +84,5 @@ yarn docs
* The dev tag of the docker image get's build from this
* Only push minor changes to this branch!
* To merge a feature branch into this please create a pull request
* feature/xyz: Feature branches - nameing scheme: `feature/issueid-title`
* bugfix/xyz: Branches for bugfixes - nameing scheme:`bugfix/issueid-title`
* feature/xyz: Feature branches - naming scheme: `feature/issueid-title`
* bugfix/xyz: Branches for bugfixes - naming scheme:`bugfix/issueid-title`

View File

@ -1,4 +1,3 @@
version: "3"
services:
backend_server:
build: .
@ -14,7 +13,7 @@ services:
DB_NAME: ./db.sqlite
NODE_ENV: production
POSTALCODE_COUNTRYCODE: DE
SEED_TEST_DATA: "false"
SEED_TEST_DATA: "true"
MAILER_URL: https://dev.lauf-fuer-kaya.de/mailer
MAILER_KEY: asdasd
# APP_PORT: 4010

View File

@ -1,3 +1,32 @@
# @node-rs/argon2
**Author**: undefined
**Repo**: [object Object]
**License**: MIT
**Description**: RustCrypto: Argon2 binding for Node.js
## License Text
MIT License
Copyright (c) 2020-present LongYinan
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/class-validator-jsonschema
**Author**: Aleksi Pekkala <aleksipekkala@gmail.com>
**Repo**: git@github.com:epiphone/class-validator-jsonschema.git
@ -27,36 +56,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# argon2
**Author**: Ranieri Althoff <ranisalt+argon2@gmail.com>
**Repo**: [object Object]
**License**: MIT
**Description**: An Argon2 library for Node
## License Text
The MIT License (MIT)
Copyright (c) 2015 Ranieri Althoff
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.
# axios
**Author**: Matt Zabriskie
**Repo**: [object Object]

View File

@ -1,6 +1,6 @@
{
"name": "@odit/lfk-backend",
"version": "0.14.0",
"version": "1.5.2",
"main": "src/app.ts",
"repository": "https://git.odit.services/lfk/backend",
"author": {
@ -22,13 +22,13 @@
],
"license": "CC-BY-NC-SA-4.0",
"dependencies": {
"@node-rs/argon2": "^2.0.2",
"@odit/class-validator-jsonschema": "2.1.1",
"argon2": "0.27.1",
"axios": "0.21.1",
"body-parser": "1.19.0",
"check-password-strength": "2.0.2",
"class-transformer": "0.3.1",
"class-validator": "0.13.1",
"class-validator": "0.13.0",
"consola": "2.15.0",
"cookie": "0.4.1",
"cookie-parser": "1.4.5",
@ -43,7 +43,7 @@
"reflect-metadata": "0.1.13",
"routing-controllers": "0.9.0-alpha.6",
"routing-controllers-openapi": "2.2.0",
"sqlite3": "5.0.0",
"sqlite3": "5.1.7",
"typeorm": "0.2.30",
"typeorm-routing-controllers-extensions": "0.2.0",
"typeorm-seeding": "1.6.1",
@ -51,7 +51,7 @@
"validator": "13.5.2"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@faker-js/faker": "7.6.0",
"@odit/license-exporter": "0.0.9",
"@types/cors": "2.8.9",
"@types/csvtojson": "1.1.5",
@ -60,7 +60,7 @@
"@types/jsonwebtoken": "8.5.0",
"@types/node": "14.14.22",
"@types/uuid": "8.3.0",
"auto-changelog": "^2.4.0",
"auto-changelog": "2.4.0",
"cp-cli": "2.0.0",
"jest": "26.6.3",
"nodemon": "2.0.7",
@ -91,12 +91,12 @@
"git": {
"commit": true,
"requireCleanWorkingDir": false,
"commitMessage": "🚀Bumped version to v${version}",
"commitMessage": "chore(release): ${version}",
"requireBranch": "dev",
"push": true,
"tag": true,
"tagName": "v${version}",
"tagAnnotation": "v${version}"
"tagName": "${version}",
"tagAnnotation": "${version}"
},
"npm": {
"publish": false

9134
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,2 @@
onlyBuiltDependencies:
- sqlite3

View File

@ -1,3 +1,4 @@
import consola from 'consola';
import { config as configDotenv } from 'dotenv';
import { CountryCode } from 'libphonenumber-js';
import ValidatorJS from 'validator';
@ -20,12 +21,15 @@ export const config = {
}
let errors = 0
if (typeof config.internal_port !== "number") {
consola.error("Error: APP_PORT is not a number")
errors++
}
if (typeof config.development !== "boolean") {
consola.error("Error: NODE_ENV is not a boolean")
errors++
}
if (config.mailer_url == "" || config.mailer_key == "") {
consola.error("Error: invalid mailer config")
errors++;
}
function getPhoneCodeLocale(): CountryCode {

View File

@ -1,9 +1,10 @@
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 { Repository, getConnectionManager } from 'typeorm';
import { DonationIdsNotMatchingError, DonationNotFoundError } from '../errors/DonationErrors';
import { DonorNotFoundError } from '../errors/DonorErrors';
import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { CreateAnonymousDonation } from '../models/actions/create/CreateAnonymousDonation';
import { CreateDistanceDonation } from '../models/actions/create/CreateDistanceDonation';
import { CreateFixedDonation } from '../models/actions/create/CreateFixedDonation';
import { UpdateDistanceDonation } from '../models/actions/update/UpdateDistanceDonation';
@ -11,6 +12,7 @@ import { UpdateFixedDonation } from '../models/actions/update/UpdateFixedDonatio
import { DistanceDonation } from '../models/entities/DistanceDonation';
import { Donation } from '../models/entities/Donation';
import { FixedDonation } from '../models/entities/FixedDonation';
import { ResponseAnonymousDonation } from '../models/responses/ResponseAnonymousDonation';
import { ResponseDistanceDonation } from '../models/responses/ResponseDistanceDonation';
import { ResponseDonation } from '../models/responses/ResponseDonation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
@ -35,10 +37,18 @@ export class DonationController {
@Authorized("DONATION:GET")
@ResponseSchema(ResponseDonation, { isArray: true })
@ResponseSchema(ResponseDistanceDonation, { isArray: true })
@ResponseSchema(ResponseAnonymousDonation, { 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() {
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseDonations: ResponseDonation[] = new Array<ResponseDonation>();
const donations = await this.donationRepository.find({ relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] });
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());
});
@ -49,6 +59,7 @@ export class DonationController {
@Authorized("DONATION:GET")
@ResponseSchema(ResponseDonation)
@ResponseSchema(ResponseDistanceDonation)
@ResponseSchema(ResponseAnonymousDonation)
@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).' })
@ -69,6 +80,17 @@ export class DonationController {
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
}
@Post('/anonymous')
@Authorized("DONATION:CREATE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a anonymous donation' })
async postAnonymous(@Body({ validate: true }) createDonation: CreateAnonymousDonation) {
let donation = await createDonation.toEntity();
donation = await this.fixedDonationRepository.save(donation);
return (await this.donationRepository.findOne({ id: donation.id })).toResponse();
}
@Post('/distance')
@Authorized("DONATION:CREATE")
@ResponseSchema(ResponseDistanceDonation)

View File

@ -1,6 +1,6 @@
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 { Repository, getConnectionManager } from 'typeorm';
import { DonorHasDonationsError, DonorIdsNotMatchingError, DonorNotFoundError } from '../errors/DonorErrors';
import { CreateDonor } from '../models/actions/create/CreateDonor';
import { UpdateDonor } from '../models/actions/update/UpdateDonor';
@ -25,9 +25,16 @@ export class DonorController {
@Authorized("DONOR:GET")
@ResponseSchema(ResponseDonor, { isArray: true })
@OpenAPI({ description: 'Lists all donor. <br> This includes the donor\'s current donation amount.' })
async getAll() {
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseDonors: ResponseDonor[] = new Array<ResponseDonor>();
const donors = await this.donorRepository.find({ relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] });
let donors: Array<Donor>;
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 => {
responseDonors.push(new ResponseDonor(donor));
});

View File

@ -1,6 +1,6 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnection, getConnectionManager, Repository } from 'typeorm';
import { Repository, getConnection, getConnectionManager } from 'typeorm';
import { GroupContactIdsNotMatchingError, GroupContactNotFoundError } from '../errors/GroupContactErrors';
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
import { CreateGroupContact } from '../models/actions/create/CreateGroupContact';
@ -26,9 +26,16 @@ export class GroupContactController {
@Authorized("CONTACT:GET")
@ResponseSchema(ResponseGroupContact, { isArray: true })
@OpenAPI({ description: 'Lists all contacts. <br> This includes the contact\'s associated groups.' })
async getAll() {
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseContacts: ResponseGroupContact[] = new Array<ResponseGroupContact>();
const contacts = await this.contactRepository.find({ relations: ['groups', 'groups.parentGroup'] });
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());
});

View File

@ -1,6 +1,6 @@
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 { Repository, getConnectionManager } from 'typeorm';
import { PermissionIdsNotMatchingError, PermissionNeedsPrincipalError, PermissionNotFoundError } from '../errors/PermissionErrors';
import { PrincipalNotFoundError } from '../errors/PrincipalErrors';
import { CreatePermission } from '../models/actions/create/CreatePermission';
@ -27,9 +27,16 @@ export class PermissionController {
@Authorized("PERMISSION:GET")
@ResponseSchema(ResponsePermission, { isArray: true })
@OpenAPI({ description: 'Lists all permissions for all users and groups.' })
async getAll() {
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responsePermissions: ResponsePermission[] = new Array<ResponsePermission>();
const permissions = await this.permissionRepository.find({ relations: ['principal'] });
let permissions: Array<Permission>;
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 => {
responsePermissions.push(new ResponsePermission(permission));
});

View File

@ -1,10 +1,11 @@
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 { Repository, getConnectionManager } from 'typeorm';
import { RunnerCardHasScansError, RunnerCardIdsNotMatchingError, RunnerCardNotFoundError } from '../errors/RunnerCardErrors';
import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { CreateRunnerCard } from '../models/actions/create/CreateRunnerCard';
import { UpdateRunnerCard } from '../models/actions/update/UpdateRunnerCard';
import { UpdateRunnerCardByCode } from '../models/actions/update/UpdateRunnerCardByCode';
import { RunnerCard } from '../models/entities/RunnerCard';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunnerCard } from '../models/responses/ResponseRunnerCard';
@ -26,9 +27,16 @@ export class RunnerCardController {
@Authorized("CARD:GET")
@ResponseSchema(ResponseRunnerCard, { isArray: true })
@OpenAPI({ description: 'Lists all card.' })
async getAll() {
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
const cards = await this.cardRepository.find({ relations: ['runner', 'runner.group', 'runner.group.parentGroup'] });
let cards: Array<RunnerCard>;
if (page != undefined) {
cards = await this.cardRepository.find({ relations: ['runner', 'runner.group', 'runner.group.parentGroup'], skip: page * page_size, take: page_size });
} else {
cards = await this.cardRepository.find({ relations: ['runner', 'runner.group', 'runner.group.parentGroup'] });
}
cards.forEach(card => {
responseCards.push(new ResponseRunnerCard(card));
});
@ -105,6 +113,28 @@ export class RunnerCardController {
return (await this.cardRepository.findOne({ id: id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] })).toResponse();
}
@Put('/:code')
@Authorized("CARD:UPDATE")
@ResponseSchema(ResponseRunnerCard)
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerCardIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the card whose code you provided." })
async putByCode(@Param('code') code: string, @Body({ validate: true }) card: UpdateRunnerCardByCode) {
let oldCard = await this.cardRepository.findOne({ code: code });
if (!oldCard) {
throw new RunnerCardNotFoundError();
}
if (oldCard.code != card.code) {
throw new RunnerCardIdsNotMatchingError();
}
await this.cardRepository.save(await card.update(oldCard));
return (await this.cardRepository.findOne({ code: code }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] })).toResponse();
}
@Delete('/:id')
@Authorized("CARD:DELETE")
@ResponseSchema(ResponseRunnerCard)

View File

@ -1,6 +1,6 @@
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 { Repository, getConnectionManager } from 'typeorm';
import { RunnerGroupNeededError, RunnerHasDistanceDonationsError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
import { CreateRunner } from '../models/actions/create/CreateRunner';
@ -30,11 +30,25 @@ export class RunnerController {
@Authorized("RUNNER:GET")
@ResponseSchema(ResponseRunner, { isArray: true })
@OpenAPI({ description: 'Lists all runners from all teams/orgs. <br> This includes the runner\'s group and distance ran.' })
async getAll() {
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100, @QueryParam("created_via", { required: false }) created_via: string = "all", @QueryParam("selfservice_links", { required: false }) selfservice_links: boolean = false) {
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
const runners = await this.runnerRepository.find({ relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] });
let runners: Array<Runner>;
console.log("call to RunnerController.getAll() with page: " + page + " and page_size: " + page_size + " and created_via: " + created_via + " and selfservice_links: " + selfservice_links);
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 => {
responseRunners.push(new ResponseRunner(runner));
if (created_via === "all") {
responseRunners.push(new ResponseRunner(runner, selfservice_links));
} else {
if (runner.created_via === created_via) {
responseRunners.push(new ResponseRunner(runner, selfservice_links));
}
}
});
return responseRunners;
}
@ -46,9 +60,9 @@ export class RunnerController {
@OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Lists all information about the runner whose id got provided.' })
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', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations'] })
if (!runner) { throw new RunnerNotFoundError(); }
return new ResponseRunner(runner);
return new ResponseRunner(runner, true);
}
@Get('/:id/scans')
@ -91,7 +105,7 @@ export class RunnerController {
}
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', 'group.parentGroup', 'scans.track', 'cards'] }), true);
}
@Put('/:id')
@ -112,7 +126,7 @@ export class RunnerController {
}
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', 'group.parentGroup', 'scans.track', 'cards'] }), true);
}
@Delete('/:id')
@ -125,7 +139,7 @@ export class RunnerController {
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let runner = await this.runnerRepository.findOne({ id: id });
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);
if (!runner) {
throw new RunnerNotFoundError();

View File

@ -1,6 +1,6 @@
import { Authorized, BadRequestError, 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 { 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';
@ -29,13 +29,20 @@ export class RunnerOrganizationController {
@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() {
let responseTeams: ResponseRunnerOrganization[] = new Array<ResponseRunnerOrganization>();
const runners = await this.runnerOrganizationRepository.find({ relations: ['contact', 'teams'] });
runners.forEach(runner => {
responseTeams.push(new ResponseRunnerOrganization(runner));
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 responseTeams;
return responseOrgs;
}
@Get('/:id')
@ -45,7 +52,7 @@ export class RunnerOrganizationController {
@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'] });
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);
}
@ -55,13 +62,13 @@ export class RunnerOrganizationController {
@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) {
async getRunners(@Param('id') id: number, @QueryParam('onlyDirect') onlyDirect: boolean, @QueryParam("selfservice_links", { required: false }) selfservice_links: boolean = false) {
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));
responseRunners.push(new ResponseRunner(runner, selfservice_links));
});
return responseRunners;
}

View File

@ -127,11 +127,11 @@ export class RunnerSelfServiceController {
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(); }
if (runner.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 30)) { throw new RunnerSelfserviceTimeoutError(); }
const token = JwtCreator.createSelfService(runner);
try {
await Mailer.sendSelfserviceForgottenMail(runner.email, token, locale)
await Mailer.sendSelfserviceForgottenMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, token, locale)
} catch (error) {
throw new MailSendingError();
}
@ -157,7 +157,7 @@ export class RunnerSelfServiceController {
response.token = JwtCreator.createSelfService(runner);
try {
await Mailer.sendSelfserviceWelcomeMail(runner.email, response.token, locale)
await Mailer.sendSelfserviceWelcomeMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, response.token, locale)
} catch (error) {
throw new MailSendingError();
}
@ -182,7 +182,7 @@ export class RunnerSelfServiceController {
response.token = JwtCreator.createSelfService(runner);
try {
await Mailer.sendSelfserviceWelcomeMail(runner.email, response.token, locale)
await Mailer.sendSelfserviceWelcomeMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, response.token, locale)
} catch (error) {
throw new MailSendingError();
}

View File

@ -1,6 +1,6 @@
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 { Repository, getConnectionManager } from 'typeorm';
import { RunnerTeamHasRunnersError, RunnerTeamIdsNotMatchingError, RunnerTeamNotFoundError } from '../errors/RunnerTeamErrors';
import { CreateRunnerTeam } from '../models/actions/create/CreateRunnerTeam';
import { UpdateRunnerTeam } from '../models/actions/update/UpdateRunnerTeam';
@ -27,11 +27,18 @@ export class RunnerTeamController {
@Authorized("TEAM:GET")
@ResponseSchema(ResponseRunnerTeam, { isArray: true })
@OpenAPI({ description: 'Lists all teams. <br> This includes their parent organization and contact (if existing/associated).' })
async getAll() {
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseTeams: ResponseRunnerTeam[] = new Array<ResponseRunnerTeam>();
const runners = await this.runnerTeamRepository.find({ relations: ['parentGroup', 'contact'] });
runners.forEach(runner => {
responseTeams.push(new ResponseRunnerTeam(runner));
let teams: Array<RunnerTeam>;
if (page != undefined) {
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;
}
@ -43,7 +50,7 @@ export class RunnerTeamController {
@OnUndefined(RunnerTeamNotFoundError)
@OpenAPI({ description: 'Lists all information about the team whose id got provided.' })
async getOne(@Param('id') id: number) {
let runnerTeam = await this.runnerTeamRepository.findOne({ id: id }, { relations: ['parentGroup', 'contact'] });
let runnerTeam = await this.runnerTeamRepository.findOne({ id: id }, { relations: ['parentGroup', 'contact', 'runners', 'runners.scans', 'runners.scans.track'] });
if (!runnerTeam) { throw new RunnerTeamNotFoundError(); }
return new ResponseRunnerTeam(runnerTeam);
}
@ -53,11 +60,11 @@ export class RunnerTeamController {
@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) {
async getRunners(@Param('id') id: number, @QueryParam("selfservice_links", { required: false }) selfservice_links: boolean = false) {
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));
responseRunners.push(new ResponseRunner(runner, selfservice_links));
});
return responseRunners;
}
@ -112,7 +119,7 @@ export class RunnerTeamController {
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let team = await this.runnerTeamRepository.findOne({ id: id });
if (!team) { return null; }
let runnerTeam = await this.runnerTeamRepository.findOne(team, { relations: ['parentGroup', 'contact', 'runners'] });
let runnerTeam = await this.runnerTeamRepository.findOne(team, { relations: ['runners'] });
if (!force) {
if (runnerTeam.runners.length != 0) {

View File

@ -1,7 +1,7 @@
import { Request } from "express";
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam, Req, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { Repository, getConnectionManager } from 'typeorm';
import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors';
import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
@ -34,9 +34,16 @@ export class ScanController {
@ResponseSchema(ResponseScan, { 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.' })
async getAll() {
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseScans: ResponseScan[] = new Array<ResponseScan>();
const scans = await this.scanRepository.find({ relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] });
let scans: Array<Scan>;
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 => {
responseScans.push(scan.toResponse());
});
@ -51,7 +58,7 @@ export class ScanController {
@OnUndefined(ScanNotFoundError)
@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) {
let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })
let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.group', 'card', 'station'] })
if (!scan) { throw new ScanNotFoundError(); }
return scan.toResponse();
}

View File

@ -1,6 +1,6 @@
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 { Repository, getConnectionManager } from 'typeorm';
import { ScanStationHasScansError, ScanStationIdsNotMatchingError, ScanStationNotFoundError } from '../errors/ScanStationErrors';
import { TrackNotFoundError } from '../errors/TrackErrors';
import { CreateScanStation } from '../models/actions/create/CreateScanStation';
@ -26,9 +26,16 @@ export class ScanStationController {
@Authorized("STATION:GET")
@ResponseSchema(ResponseScanStation, { isArray: true })
@OpenAPI({ description: 'Lists all stations. <br> This includes their associated tracks.' })
async getAll() {
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseStations: ResponseScanStation[] = new Array<ResponseScanStation>();
const stations = await this.stationRepository.find({ relations: ['track'] });
let stations: Array<ScanStation>;
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 => {
responseStations.push(station.toResponse());
});

View File

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

View File

@ -3,6 +3,7 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnection } from 'typeorm';
import StatsAuth from '../middlewares/StatsAuth';
import { Donation } from '../models/entities/Donation';
import { Donor } from '../models/entities/Donor';
import { Runner } from '../models/entities/Runner';
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
import { RunnerTeam } from '../models/entities/RunnerTeam';
@ -21,14 +22,28 @@ export class StatsController {
@ResponseSchema(ResponseStats)
@OpenAPI({ description: "A very basic stats endpoint providing basic counters for a dashboard or simmilar" })
async get() {
let connection = getConnection();
let runners = await connection.getRepository(Runner).find({ relations: ['scans', 'scans.track'] });
let teams = await connection.getRepository(RunnerTeam).find();
let orgs = await connection.getRepository(RunnerOrganization).find();
let users = await connection.getRepository(User).find();
let scans = await connection.getRepository(Scan).find();
const connection = getConnection();
const runnersViaSelfservice = await connection.getRepository(Runner).count({ where: { created_via: "selfservice" } });
const runnersViaKiosk = await connection.getRepository(Runner).count({ where: { created_via: "kiosk" } });
const runners = await connection.getRepository(Runner).count();
const teams = await connection.getRepository(RunnerTeam).count();
const orgs = await connection.getRepository(RunnerOrganization).count();
const users = await connection.getRepository(User).count();
const scans = await connection.getRepository(Scan).count({ where: { valid: true } });
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'] });
return new ResponseStats(runners, teams, orgs, users, scans, donations)
const donors = await connection.getRepository(Donor).count();
return new ResponseStats(runnersViaSelfservice, runners, teams, orgs, users, scans, donations, distace, donors, runnersViaKiosk)
}
@Get("/runners/distance")

View File

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

View File

@ -1,7 +1,7 @@
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 { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors';
import { Repository, getConnectionManager } from 'typeorm';
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserDeletionNotConfirmedError, UserIdsNotMatchingError, UserNotFoundError, UsernameContainsIllegalCharacterError } from '../errors/UserErrors';
import { UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUser } from '../models/actions/create/CreateUser';
import { UpdateUser } from '../models/actions/update/UpdateUser';
@ -28,9 +28,17 @@ export class UserController {
@Authorized("USER:GET")
@ResponseSchema(ResponseUser, { isArray: true })
@OpenAPI({ description: 'Lists all users. <br> This includes their groups and permissions granted to them.' })
async getAll() {
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseUsers: ResponseUser[] = new Array<ResponseUser>();
const users = await this.userRepository.find({ relations: ['permissions', 'groups', 'groups.permissions'] });
let users: Array<User>;
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 => {
responseUsers.push(new ResponseUser(user));
});

View File

@ -1,6 +1,6 @@
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 { Repository, getConnectionManager } from 'typeorm';
import { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUserGroup } from '../models/actions/create/CreateUserGroup';
import { UpdateUserGroup } from '../models/actions/update/UpdateUserGroup';
@ -27,9 +27,16 @@ export class UserGroupController {
@Authorized("USERGROUP:GET")
@ResponseSchema(ResponseUserGroup, { isArray: true })
@OpenAPI({ description: 'Lists all groups. <br> The information provided might change while the project continues to evolve.' })
async getAll() {
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseGroups: ResponseUserGroup[] = new Array<ResponseUserGroup>();
const groups = await 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());
});

View File

@ -47,14 +47,14 @@ export class RunnerEmailNeededError extends NotAcceptableError {
}
/**
* Error to throw when a runner already requested a new selfservice link in the last 24hrs.
* Error to throw when a runner already requested a new selfservice link in the last 30s.
*/
export class RunnerSelfserviceTimeoutError extends NotAcceptableError {
@IsString()
name = "RunnerSelfserviceTimeoutError"
@IsString()
message = "You can only reqest a new token every 24hrs."
message = "You can only reqest a new token every 30s."
}
/**

View File

@ -18,9 +18,19 @@ export class Mailer {
*/
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
await axios.request({
method: 'POST',
url: `${Mailer.base}/api/v1/email`,
headers: {
authorization: `Bearer ${Mailer.key}`,
'content-type': 'application/json'
},
data: {
to: to_address,
templateName: 'password-reset',
language: locale,
data: { token: token }
}
});
} catch (error) {
if (Mailer.testing) { return true; }
@ -32,12 +42,26 @@ export class Mailer {
* 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") {
*/
public static async sendSelfserviceWelcomeMail(to_address: string, runner_id: number, firstname: string, middlename: string, lastname: string, token: string, locale: string = "en") {
try {
await axios.post(`${Mailer.base}/registration?locale=${locale}&key=${Mailer.key}`, {
address: to_address,
selfserviceToken: token
await axios.request({
method: 'POST',
url: `${Mailer.base}/api/v1/email`,
headers: {
authorization: `Bearer ${Mailer.key}`,
'content-type': 'application/json'
},
data: {
to: to_address,
templateName: 'welcome',
language: locale,
data: {
name: `${firstname} ${middlename} ${lastname}`,
barcode_content: `${runner_id}`,
link: `${process.env.SELFSERVICE_URL}/profile/${token}`
}
}
});
} catch (error) {
if (Mailer.testing) { return true; }
@ -49,15 +73,45 @@ export class Mailer {
* 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") {
*/
public static async sendSelfserviceForgottenMail(to_address: string, runner_id: number, firstname: string, middlename: string, lastname: string, token: string, locale: string = "en") {
try {
await axios.post(`${Mailer.base}/registration_forgot?locale=${locale}&key=${Mailer.key}`, {
address: to_address,
selfserviceToken: token
console.log("Mail request", {
to: to_address,
templateName: 'welcome',
language: locale,
data: {
to: to_address,
templateName: 'welcome',
language: locale,
data: {
name: `${firstname} ${middlename} ${lastname}`,
barcode_content: `${runner_id}`,
link: `${process.env.SELFSERVICE_URL}/profile/${token}`
}
}
})
await axios.request({
method: 'POST',
url: `${Mailer.base}/api/v1/email`,
headers: {
authorization: `Bearer ${Mailer.key}`,
'content-type': 'application/json'
},
data: {
to: to_address,
templateName: 'welcome',
language: locale,
data: {
name: `${firstname} ${middlename} ${lastname}`,
barcode_content: `${runner_id}`,
link: `${process.env.SELFSERVICE_URL}/profile/${token}`
}
}
});
} catch (error) {
if (Mailer.testing) { return true; }
console.error("Error while sending selfservice forgotten mail:", error.message);
throw new MailSendingError();
}
}

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { verify } from '@node-rs/argon2';
import { Request, Response } from 'express';
import { getConnectionManager } from 'typeorm';
import { ScanStation } from '../models/entities/ScanStation';
@ -58,7 +58,7 @@ const ScanAuth = async (req: Request, res: Response, next: () => void) => {
if (station.enabled == false) {
res.status(401).send({ http_code: 401, short: "station_disabled", message: "Station is disabled." });
}
if (!(await argon2.verify(station.key, provided_token))) {
if (!(await verify(station.key, provided_token))) {
res.status(401).send({ http_code: 401, short: "invalid_token", message: "Api token non-existent or invalid syntax." });
return;
}

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { verify } from '@node-rs/argon2';
import { Request, Response } from 'express';
import { getConnectionManager } from 'typeorm';
import { StatsClient } from '../models/entities/StatsClient';
@ -55,7 +55,7 @@ const StatsAuth = async (req: Request, res: Response, next: () => void) => {
}
}
else {
if (!(await argon2.verify(client.key, provided_token))) {
if (!(await verify(client.key, provided_token))) {
res.status(401).send("Api token invalid.");
return;
}

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from '@node-rs/argon2';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import * as jsonwebtoken from 'jsonwebtoken';
import { getConnectionManager } from 'typeorm';
@ -49,7 +49,7 @@ export class ResetPassword {
if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) { throw new RefreshTokenCountInvalidError(); }
found_user.refreshTokenCount = found_user.refreshTokenCount + 1;
found_user.password = await argon2.hash(this.password + found_user.uuid);
found_user.password = await hash(this.password + found_user.uuid);
await getConnectionManager().get().getRepository(User).save(found_user);
return "password reset successfull";

View File

@ -0,0 +1,29 @@
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 CreateAnonymousDonation 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.amount;
return newDonation;
}
}

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { verify } from '@node-rs/argon2';
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { InvalidCredentialsError, PasswordNeededError, UserDisabledError, UserNotFoundError } from '../../../errors/AuthError';
@ -56,16 +56,16 @@ export class CreateAuth {
throw new UserNotFoundError();
}
if (found_user.enabled == false) { throw new UserDisabledError(); }
if (!(await argon2.verify(found_user.password, this.password + found_user.uuid))) {
if (!(await verify(found_user.password, this.password + found_user.uuid))) {
throw new InvalidCredentialsError();
}
//Create the access token
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 24 * 60 * 60
newAuth.access_token = JwtCreator.createAccess(found_user, timestamp_accesstoken_expiry);
newAuth.access_token_expires_at = timestamp_accesstoken_expiry
//Create the refresh token
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 10 * 36000
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
newAuth.refresh_token = JwtCreator.createRefresh(found_user, timestamp_refresh_expiry);
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
return newAuth;

View File

@ -1,4 +1,4 @@
import { IsInt, IsPositive } from 'class-validator';
import { IsInt, IsOptional, IsPositive } from 'class-validator';
import { getConnection } from 'typeorm';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { DistanceDonation } from '../../entities/DistanceDonation';
@ -10,6 +10,21 @@ import { CreateDonation } from './CreateDonation';
*/
export class CreateDistanceDonation extends 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;
/**
* The donation's associated runner's id.
* This is important to link the runner's distance ran to the donation.

View File

@ -1,6 +1,5 @@
import { IsInt, IsOptional, IsPositive } from 'class-validator';
import { IsInt, IsOptional } from 'class-validator';
import { getConnection } from 'typeorm';
import { DonorNotFoundError } from '../../../errors/DonorErrors';
import { Donation } from '../../entities/Donation';
import { Donor } from '../../entities/Donor';
@ -8,17 +7,10 @@ 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()
@IsOptional()
donor: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
@IsOptional()
paidAmount?: number;
@ -33,9 +25,6 @@ export abstract class CreateDonation {
*/
public async getDonor(): Promise<Donor> {
const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor });
if (!donor) {
throw new DonorNotFoundError();
}
return donor;
}
}

View File

@ -6,6 +6,21 @@ 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 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()
paidAmount?: number;
/**
* The donation's amount.
* The unit is your currency's smallest unit (default: euro cent).

View File

@ -50,4 +50,11 @@ export abstract class CreateParticipant {
@IsOptional()
@IsObject()
address?: Address;
/**
* how the participant got into the system
*/
@IsOptional()
@IsString()
created_via?: string;
}

View File

@ -32,6 +32,9 @@ export class CreateRunner extends CreateParticipant {
newRunner.email = this.email;
newRunner.group = await this.getGroup();
newRunner.address = this.address;
if (this.created_via) {
newRunner.created_via = this.created_via;
}
Address.validate(newRunner.address);
return newRunner;

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from '@node-rs/argon2';
import { IsBoolean, IsInt, IsOptional, IsPositive, IsString } from 'class-validator';
import crypto from 'crypto';
import { getConnection } from 'typeorm';
@ -44,7 +44,7 @@ export class CreateScanStation {
let newUUID = uuid.v4().toUpperCase();
newStation.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
newStation.key = await argon2.hash(newStation.prefix + "." + newUUID);
newStation.key = await hash(newStation.prefix + "." + newUUID);
newStation.cleartextkey = newStation.prefix + "." + newUUID;
return newStation;

View File

@ -26,6 +26,7 @@ export class CreateSelfServiceCitizenRunner extends CreateParticipant {
public async toEntity(): Promise<Runner> {
let newRunner: Runner = new Runner();
newRunner.created_via = "selfservice";
newRunner.firstname = this.firstname;
newRunner.middlename = this.middlename;
newRunner.lastname = this.lastname;

View File

@ -28,6 +28,7 @@ export class CreateSelfServiceRunner extends CreateParticipant {
public async toEntity(group: RunnerGroup): Promise<Runner> {
let newRunner: Runner = new Runner();
newRunner.created_via = "selfservice";
newRunner.firstname = this.firstname;
newRunner.middlename = this.middlename;
newRunner.lastname = this.lastname;

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from '@node-rs/argon2';
import { IsOptional, IsString } from 'class-validator';
import crypto from 'crypto';
import * as uuid from 'uuid';
@ -25,7 +25,7 @@ export class CreateStatsClient {
let newUUID = uuid.v4().toUpperCase();
newClient.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
newClient.key = await argon2.hash(newClient.prefix + "." + newUUID);
newClient.key = await hash(newClient.prefix + "." + newUUID);
newClient.cleartextkey = newClient.prefix + "." + newUUID;
return newClient;

View File

@ -58,11 +58,11 @@ export class CreateTrackScan {
*/
public async getCard(): Promise<RunnerCard> {
const id = this.card % 200000000000;
const track = await getConnection().getRepository(RunnerCard).findOne({ id: id }, { relations: ["runner"] });
if (!track) {
const runnerCard = await getConnection().getRepository(RunnerCard).findOne({ id: id }, { relations: ["runner"] });
if (!runnerCard) {
throw new RunnerCardNotFoundError();
}
return track;
return runnerCard;
}
/**
@ -86,14 +86,13 @@ export class CreateTrackScan {
* @returns The validated scan with it's laptime set.
*/
public async validateScan(scan: TrackScan): Promise<TrackScan> {
const scans = await getConnection().getRepository(TrackScan).find({ where: { runner: scan.runner, valid: true }, relations: ["track"] });
if (scans.length == 0) {
const latestScan = await getConnection().getRepository(TrackScan).findOne({ where: { runner: scan.runner, valid: true }, relations: ["track"], order: { id: 'DESC' } });
if (!latestScan) {
scan.lapTime = 0;
scan.valid = true;
}
else {
const newestScan = scans[scans.length - 1];
scan.lapTime = scan.timestamp - newestScan.timestamp;
scan.lapTime = scan.timestamp - latestScan.timestamp;
scan.valid = (scan.lapTime > scan.track.minimumLapTime);
}
return scan;

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from "@node-rs/argon2";
import { passwordStrength } from "check-password-strength";
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
@ -110,7 +110,7 @@ export class CreateUser {
newUser.lastname = this.lastname
newUser.uuid = uuid.v4()
newUser.phone = this.phone
newUser.password = await argon2.hash(this.password + newUser.uuid);
newUser.password = await hash(this.password + newUser.uuid);
newUser.groups = await this.getGroups();
newUser.enabled = this.enabled;

View File

@ -0,0 +1,50 @@
import { IsBoolean, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnection } from 'typeorm';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { Runner } from '../../entities/Runner';
import { RunnerCard } from '../../entities/RunnerCard';
/**
* This class is used to update a RunnerCard entity (via put request).
*/
export class UpdateRunnerCardByCode {
/**
* The card's code.
*/
@IsString()
@IsNotEmpty()
code?: string;
/**
* The runner's id.
*/
@IsInt()
@IsOptional()
runner?: number;
/**
* Is the updated card enabled (for fraud reasons)?
* Default: true
*/
@IsBoolean()
enabled: boolean = true;
/**
* Creates a new RunnerCard entity from this.
*/
public async update(card: RunnerCard): Promise<RunnerCard> {
card.enabled = this.enabled;
card.runner = await this.getRunner();
return card;
}
public async getRunner(): Promise<Runner> {
if (!this.runner) { return null; }
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
if (!runner) {
throw new RunnerNotFoundError();
}
return runner;
}
}

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from '@node-rs/argon2';
import { passwordStrength } from "check-password-strength";
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
@ -111,7 +111,7 @@ export class UpdateUser {
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 hash(this.password + user.uuid);
user.refreshTokenCount = user.refreshTokenCount + 1;
}

View File

@ -1,8 +1,10 @@
import {
IsInt,
IsNotEmpty,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, PrimaryColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, PrimaryColumn } from "typeorm";
/**
* Defines the ConfigFlag entity.
@ -24,4 +26,25 @@ export class ConfigFlag {
@IsString()
@IsNotEmpty()
value: string;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
}

View File

@ -1,8 +1,8 @@
import {
IsInt,
IsNotEmpty
IsPositive
} from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseDonation } from '../responses/ResponseDonation';
import { Donor } from './Donor';
@ -24,7 +24,6 @@ export abstract class Donation {
/**
* The donations's donor.
*/
@IsNotEmpty()
@ManyToOne(() => Donor, donor => donor.donations)
donor: Donor;
@ -42,6 +41,27 @@ export abstract class Donation {
@IsInt()
paidAmount: number;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -5,9 +5,11 @@ import {
IsOptional,
IsPhoneNumber,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { config } from '../../config';
import { ResponseGroupContact } from '../responses/ResponseGroupContact';
import { Address } from "./Address";
@ -81,6 +83,27 @@ export class GroupContact {
@OneToMany(() => RunnerGroup, group => group.contact, { nullable: true })
groups: RunnerGroup[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -5,9 +5,11 @@ import {
IsOptional,
IsPhoneNumber,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { config } from '../../config';
import { ResponseParticipant } from '../responses/ResponseParticipant';
import { Address } from "./Address";
@ -75,6 +77,35 @@ export abstract class Participant {
@IsEmail()
email?: string;
/**
* how the participant got into the system
*/
@Column({ nullable: true, default: "backend" })
@IsOptional()
@IsString()
created_via?: string;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -1,9 +1,10 @@
import {
IsEnum,
IsInt,
IsNotEmpty
IsNotEmpty,
IsPositive
} from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { PermissionAction } from '../enums/PermissionAction';
import { PermissionTarget } from '../enums/PermissionTargets';
import { ResponsePermission } from '../responses/ResponsePermission';
@ -45,6 +46,27 @@ export class Permission {
@IsEnum(PermissionAction)
action: PermissionAction;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turn this into a string for exporting and jwts.
* Mainly used to shrink the size of jwts (otherwise the would contain entire objects).

View File

@ -1,5 +1,5 @@
import { IsInt } from 'class-validator';
import { Entity, OneToMany, PrimaryGeneratedColumn, TableInheritance } from 'typeorm';
import { IsInt, IsPositive } from 'class-validator';
import { BeforeInsert, BeforeUpdate, Column, Entity, OneToMany, PrimaryGeneratedColumn, TableInheritance } from 'typeorm';
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
import { Permission } from './Permission';
@ -23,6 +23,27 @@ export abstract class Principal {
@OneToMany(() => Permission, permission => permission.principal, { nullable: true })
permissions: Permission[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -57,7 +57,10 @@ export class Runner extends Participant {
* This is implemented here to avoid duplicate code in other files.
*/
public get validScans(): Scan[] {
return this.scans.filter(scan => scan.valid == true);
if (this.scans) {
return this.scans.filter(scan => scan.valid == true);
}
return []
}
/**
@ -81,6 +84,6 @@ export class Runner extends Participant {
* Turns this entity into it's response class.
*/
public toResponse(): ResponseRunner {
return new ResponseRunner(this);
return new ResponseRunner(this, true);
}
}

View File

@ -3,9 +3,10 @@ import {
IsInt,
IsOptional
IsOptional,
IsPositive
} from "class-validator";
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { RunnerCardIdOutOfRangeError } from '../../errors/RunnerCardErrors';
import { ResponseRunnerCard } from '../responses/ResponseRunnerCard';
import { Runner } from "./Runner";
@ -48,17 +49,32 @@ export class RunnerCard {
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Generates a ean-13 compliant string for barcode generation.
*/
public get code(): string {
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();
return this.paddedId
}
/**
@ -67,10 +83,10 @@ export class RunnerCard {
private get paddedId(): string {
let id: string = this.id.toString();
if (id.length > 6) {
if (id.length > 11) {
throw new RunnerCardIdOutOfRangeError();
}
while (id.length < 6) { id = '0' + id; }
while (id.length < 11) { id = '0' + id; }
id = '2' + id;
return id;

View File

@ -2,9 +2,10 @@ import {
IsInt,
IsNotEmpty,
IsOptional,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseRunnerGroup } from '../responses/ResponseRunnerGroup';
import { GroupContact } from "./GroupContact";
import { Runner } from "./Runner";
@ -46,6 +47,27 @@ export abstract class RunnerGroup {
@OneToMany(() => Runner, runner => runner.group, { nullable: true })
runners: Runner[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Returns the total distance ran by this group's runners based on all their valid scans.
*/

View File

@ -5,7 +5,7 @@ import {
IsPositive
} from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseScan } from '../responses/ResponseScan';
import { Runner } from "./Runner";
@ -40,6 +40,27 @@ export class Scan {
@IsBoolean()
valid: boolean = true;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* The scan's distance in meters.
* This is the "real" value used by "normal" scans..

View File

@ -3,9 +3,10 @@ import {
IsInt,
IsNotEmpty,
IsOptional,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { ResponseScanStation } from '../responses/ResponseScanStation';
import { Track } from "./Track";
import { TrackScan } from "./TrackScan";
@ -78,6 +79,27 @@ export class ScanStation {
@IsBoolean()
enabled?: boolean = true;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -1,5 +1,5 @@
import { IsInt, IsOptional, IsString } from "class-validator";
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { IsInt, IsOptional, IsPositive, IsString } from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { ResponseStatsClient } from '../responses/ResponseStatsClient';
/**
* Defines the StatsClient entity.
@ -47,6 +47,27 @@ export class StatsClient {
@IsOptional()
cleartextkey?: string;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -5,7 +5,7 @@ import {
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { ResponseTrack } from '../responses/ResponseTrack';
import { ScanStation } from "./ScanStation";
import { TrackScan } from "./TrackScan";
@ -63,6 +63,27 @@ export class Track {
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -3,9 +3,10 @@ import {
IsInt,
IsNotEmpty,
IsOptional,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { PermissionAction } from '../enums/PermissionAction';
import { User } from './User';
@ -53,6 +54,27 @@ export class UserAction {
@IsString()
changed: string;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -0,0 +1,68 @@
import { IsInt, IsPositive } from "class-validator";
import { Donation } from '../entities/Donation';
import { DonationStatus } from '../enums/DonationStatus';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
/**
* Defines the donation response.
*/
export class ResponseAnonymousDonation implements IResponse {
/**
* The responseType.
* This contains the type of class/entity this response contains.
*/
responseType: ResponseObjectType = ResponseObjectType.DONATION;
/**
* The donation's payment status.
* Provides you with a quick indicator of it's payment status.
*/
status: DonationStatus;
/**
* The donation's id.
*/
@IsInt()
@IsPositive()
id: number;
/**
* The donation's amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
amount: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
paidAmount: number;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseDonation object from a scan.
* @param donation The donation the response shall be build for.
*/
public constructor(donation: Donation) {
this.id = donation.id;
this.amount = donation.amount;
this.paidAmount = donation.paidAmount || 0;
if (this.paidAmount < this.amount) {
this.status = DonationStatus.OPEN;
}
else {
this.status = DonationStatus.PAID;
}
this.created_at = donation.created_at;
this.updated_at = donation.updated_at;
}
}

View File

@ -47,13 +47,23 @@ export class ResponseDonation implements IResponse {
@IsInt()
paidAmount: number;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseDonation object from a scan.
* @param donation The donation the response shall be build for.
*/
public constructor(donation: Donation) {
this.id = donation.id;
this.donor = donation.donor.toResponse();
if (donation.donor) {
this.donor = donation.donor.toResponse();
}
this.amount = donation.amount;
this.paidAmount = donation.paidAmount || 0;
if (this.paidAmount < this.amount) {
@ -62,5 +72,7 @@ export class ResponseDonation implements IResponse {
else {
this.status = DonationStatus.PAID;
}
this.created_at = donation.created_at;
this.updated_at = donation.updated_at;
}
}

View File

@ -4,6 +4,7 @@ import {
import { Donor } from '../entities/Donor';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
import { ResponseDonation } from './ResponseDonation';
import { ResponseParticipant } from './ResponseParticipant';
/**
@ -34,6 +35,8 @@ export class ResponseDonor extends ResponseParticipant implements IResponse {
@IsInt()
paidDonationAmount: number;
donations: Array<ResponseDonation>;
/**
* Creates a ResponseRunner object from a runner.
* @param runner The user the response shall be build for.
@ -43,5 +46,11 @@ export class ResponseDonor extends ResponseParticipant implements IResponse {
this.receiptNeeded = donor.receiptNeeded;
this.donationAmount = donor.donationAmount;
this.paidDonationAmount = donor.paidDonationAmount;
this.donations = new Array<ResponseDonation>();
if (donor.donations?.length > 0) {
for (const donation of donor.donations) {
this.donations.push(donation.toResponse())
}
}
}
}

View File

@ -1,4 +1,4 @@
import { IsInt, IsObject, IsString } from "class-validator";
import { IsInt, IsObject, IsPositive, IsString } from "class-validator";
import { Address } from '../entities/Address';
import { GroupContact } from '../entities/GroupContact';
import { ResponseObjectType } from '../enums/ResponseObjectType';
@ -64,6 +64,14 @@ export class ResponseGroupContact implements IResponse {
@IsObject()
address?: Address;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseGroupContact object from a contact.
* @param contact The contact the response shall be build for.
@ -82,5 +90,7 @@ export class ResponseGroupContact implements IResponse {
this.groups.push(group.toResponse());
}
}
this.created_at = contact.created_at;
this.updated_at = contact.updated_at;
}
}

View File

@ -1,4 +1,4 @@
import { IsInt, IsObject, IsOptional, IsString } from "class-validator";
import { IsInt, IsObject, IsOptional, IsPositive, IsString } from "class-validator";
import { Address } from '../entities/Address';
import { Participant } from '../entities/Participant';
import { ResponseObjectType } from '../enums/ResponseObjectType';
@ -50,6 +50,12 @@ export abstract class ResponseParticipant implements IResponse {
@IsString()
email?: string;
/**
* how the participant got into the system
*/
@IsString()
created_via?: string;
/**
* The participant's address.
*/
@ -57,6 +63,14 @@ export abstract class ResponseParticipant implements IResponse {
@IsObject()
address?: Address;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseParticipant object from a participant.
* @param participant The participant the response shall be build for.
@ -64,10 +78,13 @@ export abstract class ResponseParticipant implements IResponse {
public constructor(participant: Participant) {
this.id = participant.id;
this.firstname = participant.firstname;
this.created_via = participant.created_via;
this.middlename = participant.middlename;
this.lastname = participant.lastname;
this.phone = participant.phone;
this.email = participant.email;
this.address = participant.address;
this.created_at = participant.created_at;
this.updated_at = participant.updated_at;
}
}

View File

@ -2,7 +2,8 @@ import {
IsEnum,
IsInt,
IsNotEmpty,
IsObject
IsObject,
IsPositive
} from "class-validator";
import { Permission } from '../entities/Permission';
import { PermissionAction } from '../enums/PermissionAction';
@ -48,6 +49,14 @@ export class ResponsePermission implements IResponse {
@IsEnum(PermissionAction)
action: PermissionAction;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponsePermission object from a permission.
* @param permission The permission the response shall be build for.
@ -57,5 +66,7 @@ export class ResponsePermission implements IResponse {
this.principal = permission.principal.toResponse();
this.target = permission.target;
this.action = permission.action;
this.created_at = permission.created_at;
this.updated_at = permission.updated_at;
}
}

View File

@ -1,5 +1,6 @@
import {
IsInt
IsInt,
IsPositive
} from "class-validator";
import { Principal } from '../entities/Principal';
import { ResponseObjectType } from '../enums/ResponseObjectType';
@ -22,11 +23,21 @@ export abstract class ResponsePrincipal implements IResponse {
@IsInt()
id: number;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponsePrincipal object from a principal.
* @param principal The principal the response shall be build for.
*/
public constructor(principal: Principal) {
this.id = principal.id;
this.created_at = principal.created_at;
this.updated_at = principal.updated_at;
}
}

View File

@ -1,7 +1,10 @@
import {
IsInt,
IsObject
IsObject,
IsOptional,
IsString
} from "class-validator";
import { JwtCreator } from '../../jwtcreator';
import { Runner } from '../entities/Runner';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
@ -24,20 +27,43 @@ export class ResponseRunner extends ResponseParticipant implements IResponse {
@IsInt()
distance: number;
/**
* The runner's current donation amount based on distance.
* Only available for queries for single runners.
*/
@IsInt()
donationAmount: number;
/**
* The runner's group.
*/
@IsObject()
group: ResponseRunnerGroup;
/**
* A selfservice link for our new runner.
*/
@IsOptional()
@IsString()
selfserviceLink: string;
/**
* Creates a ResponseRunner object from a runner.
* @param runner The user the response shall be build for.
*/
public constructor(runner: Runner) {
public constructor(runner: Runner, generateSelfServiceLink: boolean = false) {
super(runner);
if (!runner.scans) { this.distance = 0 }
else { this.distance = runner.validScans.reduce((sum, current) => sum + current.distance, 0); }
if (runner.group) { this.group = runner.group.toResponse(); }
if (runner.distanceDonations) {
this.donationAmount = runner.distanceDonations.reduce((sum, current) => sum + (current.amountPerDistance * runner.distance / 1000), 0);
}
if (generateSelfServiceLink) {
const token = JwtCreator.createSelfService(runner);
this.selfserviceLink = `${process.env.SELFSERVICE_URL}/profile/${token}`;
}
}
}

View File

@ -1,4 +1,4 @@
import { IsBoolean, IsEAN, IsInt, IsNotEmpty, IsObject, IsString } from "class-validator";
import { IsBoolean, IsEAN, IsInt, IsNotEmpty, IsObject, IsPositive, IsString } from "class-validator";
import { RunnerCard } from '../entities/RunnerCard';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
@ -42,6 +42,14 @@ export class ResponseRunnerCard implements IResponse {
@IsBoolean()
enabled: boolean = true;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseRunnerCard object from a runner card.
* @param card The card the response shall be build for.
@ -57,5 +65,7 @@ export class ResponseRunnerCard implements IResponse {
}
this.enabled = card.enabled;
this.created_at = card.created_at;
this.updated_at = card.updated_at;
}
}

View File

@ -1,4 +1,4 @@
import { IsInt, IsNotEmpty, IsObject, IsOptional, IsString } from "class-validator";
import { IsInt, IsNotEmpty, IsNumber, IsObject, IsOptional, IsPositive, IsString } from "class-validator";
import { RunnerGroup } from '../entities/RunnerGroup';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
@ -36,6 +36,18 @@ export abstract class ResponseRunnerGroup implements IResponse {
@IsOptional()
contact?: ResponseGroupContact;
@IsOptional()
@IsNumber()
total_distance: number
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseRunnerGroup object from a runnerGroup.
* @param group The runnerGroup the response shall be build for.
@ -44,5 +56,8 @@ export abstract class ResponseRunnerGroup implements IResponse {
this.id = group.id;
this.name = group.name;
if (group.contact) { this.contact = group.contact.toResponse(); };
if (group.runners) { this.total_distance = group.runners.reduce((p, c) => p + c.distance, 0) }
this.created_at = group.created_at;
this.updated_at = group.updated_at;
}
}

View File

@ -67,6 +67,9 @@ export class ResponseRunnerOrganization extends ResponseRunnerGroup implements I
for (let team of org.teams) {
this.teams.push(team.toResponse());
}
for (const team of this.teams) {
this.total_distance += team.total_distance;
}
}
if (!org.key) { this.registrationEnabled = false; }

View File

@ -41,6 +41,14 @@ export class ResponseScan implements IResponse {
@IsPositive()
distance: number;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseScan object from a scan.
* @param scan The scan the response shall be build for.
@ -50,5 +58,7 @@ export class ResponseScan implements IResponse {
if (scan.runner) { this.runner = scan.runner.toResponse(); }
this.distance = scan.distance;
this.valid = scan.valid;
this.created_at = scan.created_at;
this.updated_at = scan.updated_at;
}
}

View File

@ -1,5 +1,4 @@
import {
IsBoolean,
IsInt,
@ -8,6 +7,7 @@ import {
IsObject,
IsOptional,
IsPositive,
IsString
} from "class-validator";
import { ScanStation } from '../entities/ScanStation';
@ -63,6 +63,14 @@ export class ResponseScanStation implements IResponse {
@IsBoolean()
enabled?: boolean = true;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseStatsClient object from a statsClient.
* @param client The statsClient the response shall be build for.
@ -74,5 +82,7 @@ export class ResponseScanStation implements IResponse {
this.key = "Only visible on creation.";
if (station.track) { this.track = station.track.toResponse(); }
this.enabled = station.enabled;
this.created_at = station.created_at;
this.updated_at = station.updated_at;
}
}

View File

@ -2,11 +2,6 @@ import {
IsInt
} from "class-validator";
import { Donation } from '../entities/Donation';
import { Runner } from '../entities/Runner';
import { RunnerOrganization } from '../entities/RunnerOrganization';
import { RunnerTeam } from '../entities/RunnerTeam';
import { Scan } from '../entities/Scan';
import { User } from '../entities/User';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
@ -21,6 +16,18 @@ export class ResponseStats implements IResponse {
*/
responseType: ResponseObjectType = ResponseObjectType.STATS;
/**
* The amount of runners registered via selfservice.
*/
@IsInt()
runnersViaSelfservice: number;
/**
* The amount of runners registered via kiosk.
*/
@IsInt()
runnersViaKiosk: number;
/**
* The amount of runners registered in the system.
*/
@ -63,29 +70,53 @@ export class ResponseStats implements IResponse {
@IsInt()
total_donation: number;
/**
* The total donation count (cent).
*/
@IsInt()
total_donations: number;
/**
* The total donor count.
*/
@IsInt()
total_donors: number;
/**
* The average distance ran per runner.
*/
@IsInt()
average_distance: number;
/**
* The average donation per distance (cent).
*/
@IsInt()
average_donation: number;
/**
* Creates a new stats response containing some basic statistics for a dashboard or public display.
* @param runners Array containing all runners - the following relations have to be resolved: scans, scans.track
* @param teams Array containing all teams - no relations have to be resolved.
* @param orgs Array containing all orgs - no relations have to be resolved.
* @param users Array containing all users - no relations have to be resolved.
* @param scans Array containing all scans - no relations have to be resolved.
* @param runnersViaSelfservice number of runners registered via selfservice
* @param runners number of runners
* @param teams number of teams - no relations have to be resolved.
* @param orgs number of orgs - no relations have to be resolved.
* @param users number of users - no relations have to be resolved.
* @param scans number of scans - no relations have to be resolved.
* @param donations Array containing all donations - the following relations have to be resolved: runner, runner.scans, runner.scans.track
*/
public constructor(runners: Runner[], teams: RunnerTeam[], orgs: RunnerOrganization[], users: User[], scans: Scan[], donations: Donation[]) {
this.total_runners = runners.length;
this.total_teams = teams.length;
this.total_orgs = orgs.length;
this.total_users = users.length;
this.total_scans = scans.filter(scan => { scan.valid === true }).length;
this.total_distance = runners.reduce((sum, current) => sum + current.distance, 0);
public constructor(runnersViaSelfservice: number, runners: number, teams: number, orgs: number, users: number, scans: number, donations: Donation[], distance: number, donors: number, runnersViaKiosk: number) {
this.runnersViaSelfservice = runnersViaSelfservice;
this.total_runners = runners;
this.total_teams = teams;
this.total_orgs = orgs;
this.total_users = users;
this.total_scans = scans;
this.total_distance = distance;
this.total_donation = donations.reduce((sum, current) => sum + current.amount, 0);
this.total_donations = donations.length;
this.average_donation = this.total_donation / this.total_donations
this.total_donors = donors;
this.average_distance = this.total_distance / this.total_runners;
this.runnersViaKiosk = runnersViaKiosk;
}
}

View File

@ -1,10 +1,10 @@
import {
IsInt,
IsNotEmpty,
IsOptional,
IsPositive,
IsString
} from "class-validator";
import { StatsClient } from '../entities/StatsClient';
@ -49,6 +49,14 @@ export class ResponseStatsClient implements IResponse {
@IsNotEmpty()
prefix: string;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseStatsClient object from a statsClient.
* @param client The statsClient the response shall be build for.
@ -58,5 +66,7 @@ export class ResponseStatsClient implements IResponse {
this.description = client.description;
this.prefix = client.prefix;
this.key = "Only visible on creation.";
this.created_at = client.created_at;
this.updated_at = client.updated_at;
}
}

View File

@ -1,4 +1,4 @@
import { IsInt, IsOptional, IsString } from "class-validator";
import { IsInt, IsOptional, IsPositive, IsString } from "class-validator";
import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors';
import { Track } from '../entities/Track';
import { ResponseObjectType } from '../enums/ResponseObjectType';
@ -40,6 +40,14 @@ export class ResponseTrack implements IResponse {
@IsOptional()
minimumLapTime?: number;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseTrack object from a track.
* @param track The track the response shall be build for.
@ -52,5 +60,7 @@ export class ResponseTrack implements IResponse {
if (this.minimumLapTime < 0) {
throw new TrackLapTimeCantBeNegativeError();
}
this.created_at = track.created_at;
this.updated_at = track.updated_at;
}
}

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from '@node-rs/argon2';
import { Connection } from 'typeorm';
import { Factory, Seeder } from 'typeorm-seeding';
import * as uuid from 'uuid';
@ -11,7 +11,7 @@ import { PermissionAction } from '../models/enums/PermissionAction';
import { PermissionTarget } from '../models/enums/PermissionTargets';
/**
* Seeds a admin group with a demo user into the database for initial setup and auto recovery.
* We know that the nameing isn't perfectly fitting. Feel free to change it.
* We know that the naming isn't perfectly fitting. Feel free to change it.
*/
export default class SeedUsers implements Seeder {
public async run(factory: Factory, connection: Connection): Promise<any> {
@ -33,7 +33,7 @@ export default class SeedUsers implements Seeder {
initialUser.lastname = "demo";
initialUser.username = "demo";
initialUser.uuid = uuid.v4();
initialUser.password = await argon2.hash("demo" + initialUser.uuid);
initialUser.password = await hash("demo" + initialUser.uuid);
initialUser.email = "demo@dev.lauf-fuer-kaya.de"
initialUser.groups = [group];
return await connection.getRepository(User).save(initialUser);

View File

@ -0,0 +1,19 @@
import axios from 'axios';
async function main() {
console.time("batches")
for (let i = 0; i < 100; i++) {
const batch = [];
for (let i = 0; i < 6; i++) {
batch.push(axios.post('http://localhost:4010/api/scans/trackscans', { card: 200000000001, station: 2 }, {
headers: {
Authorization: 'Bearer 10F2E64.BB4F6CC5-2148-4CCF-88B5-0AA85D0508A9'
}
}))
}
await Promise.all(batch)
console.timeLog("batches", `Finished batch ${i}`)
}
console.timeEnd("batches")
}
main();

View File

@ -66,6 +66,8 @@ describe('adding + getting scans', () => {
const res = await axios.get(base + '/api/scans/' + added_scan.id, axios_config);
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
delete res.data.runner.distance;
delete added_scan.runner.distance;
expect(res.data).toEqual(added_scan);
});
it('check if scans was added via the runner/scans endpoint.', async () => {