Compare commits
576 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
7910beb5cb
|
|||
|
1be8836df1
|
|||
|
70d6091a6a
|
|||
|
1d9c451dfb
|
|||
|
3197498ab3
|
|||
|
a1a2c2747c
|
|||
|
80e7e7939c
|
|||
|
6caa1850e3
|
|||
|
7aaac65af4
|
|||
|
13e0c81957
|
|||
|
329a29aca7
|
|||
|
abdadb8e64
|
|||
|
abce517d86
|
|||
|
a1e697acb2
|
|||
|
c9b8614f53
|
|||
|
cbf1da31c9
|
|||
|
fd18e56251
|
|||
|
3bb8b202b0
|
|||
|
d1c4744231
|
|||
|
fe90414dd9
|
|||
|
21ceb9fa26
|
|||
|
5081819281
|
|||
|
240bd9cba1
|
|||
|
53fb0389cd
|
|||
|
d230350027
|
|||
|
024e647295
|
|||
|
d3e0206a3c
|
|||
|
b0c6759813
|
|||
|
526738e487
|
|||
|
778f159405
|
|||
|
2da8247978
|
|||
|
bbf6ea6c0f
|
|||
|
3584b3facf
|
|||
|
e27e819609
|
|||
|
0f532b139c
|
|||
|
eebcc2e328
|
|||
|
284954d064
|
|||
|
401ca923a6
|
|||
|
bf1f6411e0
|
|||
|
f225cc4954
|
|||
|
728f8a14e9
|
|||
|
a4480589a0
|
|||
|
0ad9eeb52f
|
|||
|
4494afc64b
|
|||
|
f4747c51de
|
|||
|
07a0195f12
|
|||
|
7ac98229d1
|
|||
|
dd5b538783
|
|||
|
8e6d67428c
|
|||
|
7ffb7523aa
|
|||
|
f4bf309821
|
|||
|
02b1cb9904
|
|||
|
7697acff82
|
|||
|
bacfc437f9
|
|||
|
9875b4f392
|
|||
|
ce9b765b81
|
|||
|
2ab6e985e3
|
|||
|
d06f6a4407
|
|||
|
a50d72f2f5
|
|||
|
4723d9738e
|
|||
|
1a478bd784
|
|||
|
284cb0f8b3
|
|||
|
6e63c57936
|
|||
|
30b61db2c1
|
|||
|
8237d5f210
|
|||
|
03e0a29096
|
|||
|
a6afba93e2
|
|||
|
a41758cd9c
|
|||
|
d6755ed134
|
|||
|
599c75fc00
|
|||
|
bb213f001e
|
|||
|
5415cd38a7
|
|||
|
175ba52ffa
|
|||
|
5c5000a218
|
|||
|
d559d04031
|
|||
|
2af682d1dd
|
|||
|
30905e481c
|
|||
|
752d405bda
|
|||
|
8fa4ed7c33
|
|||
|
c4201e9a68
|
|||
|
78dcad0857
|
|||
|
93e0cdf577
|
|||
|
6efcd94726
|
|||
|
2e271bcd52
|
|||
|
ebde8c6ffd
|
|||
| a3639dd89b | |||
|
0a43f1bb5b
|
|||
|
8c6fdb2239
|
|||
|
c0d5af5d7a
|
|||
|
4008a5ee72
|
|||
|
07bf28b144
|
|||
|
6764bf80ea
|
|||
|
b3a73b25e8
|
|||
| bda1f971d1 | |||
|
765ef84903
|
|||
|
296ba8ddab
|
|||
|
6eff243803
|
|||
|
0f4c8b2051
|
|||
|
d842c14240
|
|||
|
a54cb287a4
|
|||
|
74d334f9b7
|
|||
|
cd3cd81360
|
|||
|
cf48c00ddb
|
|||
|
3192365793
|
|||
|
075d484f11
|
|||
|
5082b1b8b1
|
|||
|
50dd703a1b
|
|||
|
057a8ee699
|
|||
|
8d9418635d
|
|||
|
f2832a2dae
|
|||
|
0d21596e2b
|
|||
|
245827e9c6
|
|||
|
4608a36df6
|
|||
|
cb1305aa77
|
|||
|
12a9ae2493
|
|||
|
b9fe9f1c24
|
|||
|
b25b0db760
|
|||
|
fe59e3a557
|
|||
|
42c23a5883
|
|||
|
6ee5328dbc
|
|||
|
6f39ac42da
|
|||
|
301f334674
|
|||
|
fcee3909f4
|
|||
|
f0e20e4130
|
|||
|
80de188565
|
|||
|
2f305e127c
|
|||
|
513d7f6fba
|
|||
|
244da61892
|
|||
|
2a72aea10e
|
|||
|
71ebce6f8e
|
|||
|
f60025b6de
|
|||
|
0fa663a341
|
|||
|
538622aa18
|
|||
|
86a21dbfa4
|
|||
|
1e9e24d99d
|
|||
|
4493c0e3d9
|
|||
|
f5d48fc638
|
|||
|
b35a2dd2fa
|
|||
|
a28ffe06e5
|
|||
|
d873674819
|
|||
|
37b2ac974b
|
|||
|
81aed1de40
|
|||
|
0f0c3c7214
|
|||
|
3909ed34f7
|
|||
|
b2ac70e0ae
|
|||
|
5f17e7f783
|
|||
|
a5a56a263a
|
|||
|
2d8f7528d9
|
|||
|
9581185b24
|
|||
|
2905884c02
|
|||
|
e9914e317b
|
|||
|
702070da66
|
|||
|
cc89ba8afb
|
|||
|
7c4ff42a3b
|
|||
|
8007117434
|
|||
|
23fa78eb9d
|
|||
|
3b3e68900b
|
|||
|
3ff666fd3e
|
|||
|
4e4435010f
|
|||
|
de9af5a909
|
|||
|
ac631f0af4
|
|||
|
6bbdd5bb04
|
|||
|
a8fc755840
|
|||
|
27e74e824c
|
|||
|
b5c0a288ac
|
|||
|
85dc3444ac
|
|||
|
d02743984d
|
|||
|
734c826fac
|
|||
|
33b25c9743
|
|||
|
6275aaa326
|
|||
|
2a94bfa622
|
|||
|
a64f6c9822
|
|||
|
93d43b7684
|
|||
|
16ce0a8480
|
|||
|
9a8d618ae4
|
|||
|
38da2d3318
|
|||
|
068deb4960
|
|||
|
13f093bb61
|
|||
|
6289f30740
|
|||
|
6ff764bc34
|
|||
|
ea87cc793b
|
|||
|
92517e3653
|
|||
|
ffee887ddf
|
|||
|
3bac75e7ab
|
|||
|
d05eddcae1
|
|||
|
d5c689d693
|
|||
|
8fedd4ef3b
|
|||
|
e8b2e6f261
|
|||
|
39f3b0e01f
|
|||
|
edaf255e8f
|
|||
|
41c4ed4d0f
|
|||
|
f2bd88aadf
|
|||
|
67a3661448
|
|||
|
0c763a2dfd
|
|||
|
a7297ff933
|
|||
|
4cdba8bc77
|
|||
|
77c6303014
|
|||
| 2b641faa29 | |||
| 9fa8b93c08 | |||
|
4b676bc853
|
|||
|
4433ddb1e1
|
|||
|
39aa7598b7
|
|||
|
19a290c3a9
|
|||
|
9bc80aac8a
|
|||
|
e184673963
|
|||
|
68cd746a9f
|
|||
|
69651d9f6c
|
|||
| 6fd246f43c | |||
| ae14d6c74f | |||
|
2fa56b82d1
|
|||
|
9cc66eebdf
|
|||
|
4c10e20b91
|
|||
|
9217421221
|
|||
|
4570845b3e
|
|||
|
0e78951300
|
|||
|
6ad56b3126
|
|||
|
d95c6d3365
|
|||
|
1f2c8abb22
|
|||
|
a6d5693ccd
|
|||
| 31b258b4ce | |||
| f19f2808d8 | |||
| 3b9cd2e1bb | |||
| 95320ca1bc | |||
| f2d127fc98 | |||
| eb526fb57f | |||
| 348fe52c42 | |||
| eef0fa6952 | |||
| 8a82e059b7 | |||
| 2229cdf20d | |||
| 3220b194d4 | |||
| 278c4a6a41 | |||
| ec50ac31c4 | |||
| a2f0d814fc | |||
| 6468b35708 | |||
| 3558e99090 | |||
| 520608aef0 | |||
| 6df5f634f3 | |||
| da266a8dd6 | |||
| 8ae4b85827 | |||
| 8fe3243693 | |||
| 49b174f29f | |||
| 30c6d3d8db | |||
| 6c14ed9c89 | |||
| 01ed51489e | |||
| 0636616dad | |||
| 34dbaaafe0 | |||
| b4c31ee9b5 | |||
| 99307423c5 | |||
| 71542bc388 | |||
| d64f470b60 | |||
| b8fbb72fa0 | |||
| 0c61ff457d | |||
| 1d82f65b0d | |||
| 610988ec16 | |||
| 6e236ede14 | |||
| b7ad5d3a31 | |||
| a694ad225c | |||
| 5633e85f41 | |||
| 95e1eec313 | |||
| 377d5dadb2 | |||
| 4a294b1e17 | |||
| 720774fcf4 | |||
| dcdbdd15ac | |||
| 132b48cf2a | |||
| 23bd432c5f | |||
| 71b33ab05b | |||
| 87f444c30d | |||
| 4a73eab134 | |||
| f8baca5ab2 | |||
| 10221b9f2e | |||
| 1d8c8c8e9c | |||
| 4603a84f16 | |||
| 2cd8f3f7f3 | |||
| 107eeeae7f | |||
| b8767b8bd4 | |||
| bf686e89e0 | |||
| 6163f0a90b | |||
| 8f0f795a70 | |||
| 22cae39bd3 | |||
| 0b07a53ed2 | |||
| d4a02e7db2 | |||
| b9a7dc84f0 | |||
| 7111068361 | |||
| 63964fbf2c | |||
| cbcb829fbd | |||
| 057ae0d797 | |||
| 257f320ee3 | |||
| 7b15c2d88b | |||
| 988f17a795 | |||
| 4471e57438 | |||
| 51daf969cf | |||
| cb71fcd13b | |||
| a6a526dc5d | |||
| dd6d799c84 | |||
| e89e07d0fc | |||
| c28843c405 | |||
| 4834a6698b | |||
| 69afd4d587 | |||
| 24d152fdc8 | |||
| 4279e43743 | |||
| d837654617 | |||
| 0767943721 | |||
| ca87774767 | |||
| f693f2cde9 | |||
| d70c5b1bbc | |||
| 71e3d0efe2 | |||
| b517dff8a8 | |||
| 114c246ace | |||
| d7703c9e07 | |||
| dc3071f7d2 | |||
| 5fb355f450 | |||
| 33c13de32c | |||
| 1be073a4fa | |||
| b0d8249452 | |||
| 7af883f271 | |||
| f5433076b0 | |||
| 6aafe4a6ae | |||
| bdeeb03645 | |||
| 675c8762e8 | |||
| 89e392473c | |||
| 6c9b91d75a | |||
| 8c00aefd6c | |||
| 3afd785a54 | |||
| 8099999e2c | |||
| a139554e05 | |||
| 0290b0e5f5 | |||
| 0f7fa990d4 | |||
| 2f568c9cb8 | |||
| 1cb2dc9d53 | |||
| 6005b0661f | |||
| 5a36c8dcae | |||
| 58f4d2151f | |||
| 95135ddc89 | |||
| a7fe1e1759 | |||
| 56a5f41686 | |||
| c23b4d907f | |||
| bd7b81efe7 | |||
| 274a146b9b | |||
| 5a3fc5b2bd | |||
| 070560e863 | |||
| 536900091a | |||
| 8154e715bb | |||
| 4c6665062f | |||
| cb3ea9b1eb | |||
| 7a64f23937 | |||
| 96ba25ec6c | |||
| e6a8ebcb5b | |||
| 888cab5898 | |||
| 383a8095b8 | |||
| 63f6526e4f | |||
| b24e24ff7d | |||
| 9ce35d8eb7 | |||
| 48a87e8936 | |||
| b8c28ebb08 | |||
| 5daaa3a73c | |||
| 24c38cce26 | |||
| bd00f4f8d5 | |||
| 03d76e6d0b | |||
| 3f8e8ce3a6 | |||
| c9bd6de476 | |||
| e702118d4d | |||
| 97159dd9f8 | |||
| 942d9dbc76 | |||
| 88844e1a44 | |||
| e76a9cef95 | |||
| 20aeed8778 | |||
| ccb7ae29a3 | |||
| dcb12b0ac2 | |||
| dd1258333e | |||
| 3ef3a94b20 | |||
| 135852eb9a | |||
| 963253cbc8 | |||
| 539a6509b1 | |||
| f3d73d5346 | |||
| f159252651 | |||
| 6ab60998d4 | |||
| 30d220bc36 | |||
| 24aff3bac4 | |||
| ce63043887 | |||
| e40017a6b8 | |||
| e843a464e7 | |||
| d0ae50d557 | |||
| 7a49e7c5c9 | |||
| 1dd64204cc | |||
| 438ff0fc3f | |||
| c1bbda51f0 | |||
| 4705a39aab | |||
| 4d721f62d9 | |||
| b0328ffdaf | |||
| 031cede542 | |||
| 3c69f8c4a8 | |||
| cc6568c381 | |||
| a3a1395a46 | |||
| b08acc6660 | |||
| 7a303c2b2c | |||
| 3f9a7049e3 | |||
| 6249419fae | |||
| f347b7ad49 | |||
| 74faec85c8 | |||
| fbdadbef1f | |||
| c87c97c90f | |||
| a6bca59ffe | |||
| 732a1b88d9 | |||
| 4c960feeb2 | |||
| 72fee96a08 | |||
| fcb43f92b0 | |||
| 5ba8f1dd44 | |||
| 3d3790c2eb | |||
| 1fa3fa75ee | |||
| c8882ae6a1 | |||
| 673e896aa3 | |||
| 0ed7f78b2c | |||
| 1d38d308ad | |||
| d709ee7479 | |||
| aae042c041 | |||
| ca7a84eb3e | |||
| 1f32ed0727 | |||
| 289f9e2196 | |||
| 937a9fad4d | |||
| 7c3a1b8fff | |||
| a8ea4fa659 | |||
| c1dd4518d1 | |||
| bdc7bb67e7 | |||
| 54988ba0fe | |||
| ce3ca9f1c8 | |||
| 46b7aceb0b | |||
| 486e450a58 | |||
| 623b5a1873 | |||
| a7958eecd6 | |||
| 13e839902c | |||
| 94001a48f1 | |||
| 2cb7ec7317 | |||
| 757332ed2b | |||
| 8ba7ee1d48 | |||
| c5178e0181 | |||
| a1a94ec9da | |||
| f7af777104 | |||
| 076aa87dba | |||
| ca6fa633a1 | |||
| 641e2aed52 | |||
| cba4455d53 | |||
| d5930f7c46 | |||
| 5541ae6ebd | |||
| 6c43872198 | |||
| e4ed20da3e | |||
| cb6e78fc17 | |||
| bf1ec976e3 | |||
| d0a7e34de8 | |||
| 08957d4dc2 | |||
| 1d762f5662 | |||
| a95a9b4ec4 | |||
| e5dab3469c | |||
| c01233b4d6 | |||
| 92920273be | |||
| 6bb3ae8ba9 | |||
| cedc1750c2 | |||
| 3f372123fd | |||
| a3437475ca | |||
| 83765136cc | |||
| e26b7d4923 | |||
| e7f0cb45c9 | |||
| ffcd45e572 | |||
| d7099717c2 | |||
| 66d6023335 | |||
| 5f5c8a061e | |||
| bf71e35ecd | |||
| 64da0eadb3 | |||
| 52728290b4 | |||
| 3f2a2d2929 | |||
| f1d85cfb85 | |||
| 15356c1030 | |||
| 82c65b632c | |||
| ae7d617690 | |||
| bf6b70106e | |||
| 33310cdb44 | |||
| db58a280b3 | |||
| 149f3a83b2 | |||
| a5d2a6ecd3 | |||
| bb9bad6d90 | |||
| ada679823c | |||
| 9a1678acf0 | |||
| 485c247cd3 | |||
| ddea02db57 | |||
| 1551a444ba | |||
| f289afd8bc | |||
| a9e06c9055 | |||
| c2fdfeed4f | |||
| 0342757d92 | |||
| 5833f4218f | |||
| 0fcc729b56 | |||
| a2c97a11a3 | |||
| aa833736d3 | |||
| 771a205fe6 | |||
| 6074ac5b3a | |||
| 030b2255d4 | |||
| f7f6df41ff | |||
| be397c8899 | |||
| dd3c9275d6 | |||
| 764b7ffe00 | |||
| d870b2fd01 | |||
| aaec09d2ab | |||
| bce8811925 | |||
| 3afc207903 | |||
| fca997beb8 | |||
| 39ebfbf0b6 | |||
| 3736b29e54 | |||
| b4c9369a53 | |||
| 5d6c8c957a | |||
| 09fe47b9aa | |||
| b4acd157fc | |||
| b1fced7764 | |||
| c0cafb4d51 | |||
| 45d61b487e | |||
| 28ef139a70 | |||
| 656f63dfd5 | |||
| ba3b5eeefc | |||
| ba396e0eba | |||
| 3c11d88557 | |||
| 305fa0078d | |||
| a46d14278b | |||
| 680ae8ebbb | |||
| cc869f69ad | |||
| b9aac71676 | |||
| a30a342e00 | |||
| bdcfce88cb | |||
| dd81f4c7e4 | |||
| 416f2a1366 | |||
| 5e353db206 | |||
| 0c9867d706 | |||
| 8379c3e29c | |||
| c4edccace7 | |||
| 74de6559d7 | |||
| a6f73c733c | |||
| ca3d093e54 | |||
| 28cfbaa662 | |||
| 90e1ad7db7 | |||
| 906a1dc9e7 | |||
| 5872c6335b | |||
| 701706c028 | |||
| 09bbc70f5f | |||
| dd9cb6d3ef | |||
| 23c732b690 | |||
| 656d564baa | |||
| f3f5cb462e | |||
| 9959172f2a | |||
| 8f0a396dd0 | |||
| a18d4d3cee | |||
| 390b36dfd4 | |||
| 3b718f3ce5 | |||
| 321b20b073 | |||
| f7a0ec7174 | |||
| 110a84783e | |||
| 333e806da4 | |||
| f4f621973a | |||
| bcad691045 | |||
| 74791df68b | |||
| 8425043099 | |||
| 74b982afba | |||
| 3aefa75412 | |||
| 71cab4e836 | |||
| 4e10077901 | |||
| c32fa93673 | |||
| 3d1baae0cc | |||
| 94dd7963b7 | |||
| 7ba67b9dca | |||
| 6e5f1bd5ff | |||
| 60ee6ebc1e | |||
| 02295346da | |||
| ff7406e71a | |||
| 8dc2810c0c | |||
| ff8af090e3 | |||
| bcc15e4286 | |||
| 2a87819486 | |||
| 9d5e486c6d | |||
| e44cc4c4cb | |||
| 581ca5ff6c |
163
.drone.yml
163
.drone.yml
@@ -1,163 +0,0 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: tests:node_latest
|
||||
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: node:latest
|
||||
commands:
|
||||
- yarn
|
||||
- yarn test:ci
|
||||
trigger:
|
||||
event:
|
||||
- pull_request
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build:dev
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps:
|
||||
- name: clone
|
||||
image: alpine/git
|
||||
commands:
|
||||
- git clone $DRONE_REMOTE_URL .
|
||||
- git checkout dev
|
||||
- name: build dev
|
||||
image: plugins/docker
|
||||
depends_on: [clone]
|
||||
settings:
|
||||
username:
|
||||
from_secret: DOCKER_REGISTRY_USER
|
||||
password:
|
||||
from_secret: DOCKER_REGISTRY_PASSWORD
|
||||
repo: registry.odit.services/lfk/backend
|
||||
tags:
|
||||
- dev
|
||||
registry: registry.odit.services
|
||||
- name: run changelog export
|
||||
depends_on: ["clone"]
|
||||
image: node:latest
|
||||
commands:
|
||||
- npx auto-changelog --commit-limit false -p -u --hide-credit
|
||||
- name: push new changelog to repo
|
||||
depends_on: ["run changelog export"]
|
||||
image: appleboy/drone-git-push
|
||||
settings:
|
||||
branch: dev
|
||||
commit: true
|
||||
commit_message: 🧾New changelog file version [CI SKIP] [skip ci]
|
||||
author_email: bot@odit.services
|
||||
remote: git@git.odit.services:lfk/backend.git
|
||||
ssh_key:
|
||||
from_secret: GITLAB_SSHKEY
|
||||
- name: run full license export
|
||||
depends_on: ["clone"]
|
||||
image: node:14.15.1-alpine3.12
|
||||
commands:
|
||||
- yarn
|
||||
- yarn licenses:export
|
||||
- name: push new licenses file to repo
|
||||
depends_on: ["run full license export"]
|
||||
image: appleboy/drone-git-push
|
||||
settings:
|
||||
branch: dev
|
||||
commit: true
|
||||
commit_message: 📖New license file version [CI SKIP] [skip ci]
|
||||
author_email: bot@odit.services
|
||||
remote: git@git.odit.services:lfk/backend.git
|
||||
skip_verify: true
|
||||
ssh_key:
|
||||
from_secret: GITLAB_SSHKEY
|
||||
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- dev
|
||||
event:
|
||||
- push
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
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: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: DOCKER_REGISTRY_USER
|
||||
password:
|
||||
from_secret: DOCKER_REGISTRY_PASSWORD
|
||||
repo: registry.odit.services/lfk/backend
|
||||
tags:
|
||||
- latest
|
||||
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: GITLAB_SSHKEY
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build:tags
|
||||
|
||||
steps:
|
||||
- name: build $DRONE_TAG
|
||||
image: plugins/docker
|
||||
depends_on: [clone]
|
||||
settings:
|
||||
username:
|
||||
from_secret: DOCKER_REGISTRY_USER
|
||||
password:
|
||||
from_secret: DOCKER_REGISTRY_PASSWORD
|
||||
repo: registry.odit.services/lfk/backend
|
||||
tags:
|
||||
- '${DRONE_TAG}'
|
||||
registry: registry.odit.services
|
||||
- name: trigger node lib build
|
||||
image: idcooldi/drone-webhook
|
||||
settings:
|
||||
urls: https://ci.odit.services/api/repos/lfk/lfk-client-node/builds?SOURCE_TAG=${DRONE_TAG}
|
||||
bearer:
|
||||
from_secret: BOT_DRONE_KEY
|
||||
- name: trigger js lib build
|
||||
image: idcooldi/drone-webhook
|
||||
settings:
|
||||
urls: https://ci.odit.services/api/repos/lfk/lfk-client-js/builds?SOURCE_TAG=${DRONE_TAG}
|
||||
bearer:
|
||||
from_secret: BOT_DRONE_KEY
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
@@ -7,4 +7,8 @@ DB_PASSWORD=bla
|
||||
DB_NAME=./test.sqlite
|
||||
NODE_ENV=production
|
||||
POSTALCODE_COUNTRYCODE=DE
|
||||
SEED_TEST_DATA=false
|
||||
SEED_TEST_DATA=false
|
||||
SELFSERVICE_URL=bla
|
||||
STATION_TOKEN_SECRET=<replace-with-random-secret-min-32-chars>
|
||||
NATS_URL=nats://localhost:4222
|
||||
NATS_PREWARM=false
|
||||
30
.gitea/workflows/release.yml
Normal file
30
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Build release images
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*.*.*"
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun 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
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -126,8 +126,12 @@ dist
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# Old package manager lockfiles (Bun migration - keep bun.lock)
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
|
||||
build
|
||||
|
||||
*.sqlite
|
||||
@@ -135,4 +139,4 @@ build
|
||||
/docs
|
||||
lib
|
||||
/oss-attribution
|
||||
*.tmp
|
||||
*.tmp
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -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",
|
||||
|
||||
282
AGENTS.md
Normal file
282
AGENTS.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# AGENTS.md — LfK Backend
|
||||
|
||||
Guidance for agentic coding agents working in this repository.
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
Express + [`routing-controllers`](https://github.com/typestack/routing-controllers) REST API written in TypeScript. Uses TypeORM for database access (SQLite in dev/test, PostgreSQL or MySQL in production). OpenAPI docs are auto-generated from decorators at startup.
|
||||
|
||||
**Runtime & Package Manager**: Bun (replaces Node.js + npm/pnpm).
|
||||
|
||||
---
|
||||
|
||||
## Build / Run / Test Commands
|
||||
|
||||
### Development
|
||||
|
||||
```sh
|
||||
bun run dev # Start dev server with auto-reload (uses Bun's --watch)
|
||||
```
|
||||
|
||||
**Auto-reload**: The `dev` script uses Bun's built-in `--watch` flag, which automatically restarts the server when TypeScript files in `src/` change. Bun runs TypeScript directly - no build step needed.
|
||||
|
||||
**Performance**: Bun delivers 8-15% better latency under concurrent load compared to Node.js. See `BUN_BENCHMARK_RESULTS.md` for details.
|
||||
|
||||
### Build
|
||||
|
||||
```sh
|
||||
bun run build # rimraf dist && tsc && copy static assets → dist/
|
||||
```
|
||||
|
||||
**Note**: The build script exists for legacy compatibility and type-checking, but is **not required** for development or production. Bun runs TypeScript source files directly.
|
||||
|
||||
### Production
|
||||
|
||||
```sh
|
||||
bun start # bun src/app.ts (runs TypeScript directly)
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
Tests are **integration tests** that hit a live running server via HTTP. The server must be started before Jest is invoked.
|
||||
|
||||
```sh
|
||||
# Full CI test flow (generates .env, starts server, runs jest):
|
||||
bun run test:ci
|
||||
|
||||
# Run Jest directly (server must already be running):
|
||||
bun test
|
||||
|
||||
# Watch mode:
|
||||
bun run test:watch
|
||||
|
||||
# Run a single test file:
|
||||
bunx jest src/tests/runners/runner_add.spec.ts
|
||||
|
||||
# Run tests matching a name pattern:
|
||||
bunx jest --testNamePattern="POST /api/runners"
|
||||
|
||||
# Run all tests in a subdirectory:
|
||||
bunx jest src/tests/runners/
|
||||
```
|
||||
|
||||
# Run all tests in a subdirectory:
|
||||
bunx jest src/tests/runners/
|
||||
```
|
||||
|
||||
> **Important:** `bun test` alone will fail unless the dev server is already running on `http://localhost:<config.internal_port>`. In CI, `start-server-and-test` handles this automatically via `bun run test:ci`.
|
||||
|
||||
### Other Utilities
|
||||
|
||||
```sh
|
||||
bun run seed # Sync DB schema and run seeders
|
||||
bun run openapi:export # Export OpenAPI spec to file
|
||||
bun run docs # Generate TypeDoc documentation
|
||||
bun run licenses:export # Export third-party license report
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Configuration
|
||||
|
||||
- **Target:** ES2020, **Module:** CommonJS
|
||||
- **`strict: false`** — TypeScript strictness is disabled; types are used but not exhaustively enforced
|
||||
- **`experimentalDecorators: true`** and **`emitDecoratorMetadata: true`** — required by `routing-controllers`, `TypeORM`, and `class-validator`
|
||||
- Spec files (`**/*.spec.ts`) are excluded from compilation
|
||||
- Source root: `src/`, output: `dist/`
|
||||
|
||||
---
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### No Linter / Formatter Configured
|
||||
|
||||
There is no ESLint or Prettier configuration. Follow the patterns already established in the codebase rather than introducing new tooling.
|
||||
|
||||
### Imports
|
||||
|
||||
- Use named imports for decorator packages: `import { Get, JsonController, Param } from 'routing-controllers'`
|
||||
- Use named imports for TypeORM: `import { Column, Entity, getConnectionManager } from 'typeorm'`
|
||||
- Use named imports for class-validator: `import { IsInt, IsOptional, IsString } from 'class-validator'`
|
||||
- Use `import * as X from 'module'` for modules without clean default exports (e.g., `import * as jwt from 'jsonwebtoken'`)
|
||||
- Use default imports for simple modules (e.g., `import cookie from 'cookie'`)
|
||||
- `reflect-metadata` is imported once at the top of `src/app.ts` — do not re-import it
|
||||
- No barrel/index re-export files; import source files directly by path
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
| Construct | Convention | Example |
|
||||
|---|---|---|
|
||||
| Classes | `PascalCase` | `RunnerController`, `CreateRunner` |
|
||||
| Files | `PascalCase.ts` matching class name | `RunnerController.ts` |
|
||||
| Local variables | `camelCase` (some `snake_case` in tests) | `accessToken`, `access_token` |
|
||||
| DB entity fields | `snake_case` preferred | `created_at`, `updated_at` |
|
||||
| Controller methods | REST-conventional | `getAll`, `getOne`, `post`, `put`, `remove` |
|
||||
| Custom errors | `{Entity}{Issue}Error` | `RunnerNotFoundError`, `RunnerIdsNotMatchingError` |
|
||||
| Response DTOs | `Response{Entity}` | `ResponseRunner`, `ResponseAuth` |
|
||||
| Create DTOs | `Create{Entity}` | `CreateRunner` |
|
||||
| Update DTOs | `Update{Entity}` | `UpdateRunner` |
|
||||
| Enums | `PascalCase` | `ResponseObjectType`, `PermissionAction` |
|
||||
|
||||
### Formatting
|
||||
|
||||
- 4-space indentation (observed throughout the codebase)
|
||||
- Single quotes for string literals in most files
|
||||
- No trailing semicolons style inconsistency — follow what's already in the file you're editing
|
||||
|
||||
### Types
|
||||
|
||||
- Add TypeScript types to all function parameters and return values
|
||||
- Use `class-validator` decorators (`@IsString`, `@IsInt`, `@IsOptional`, `@IsUUID`, etc.) on every DTO and response class field — these drive both runtime validation and OpenAPI schema generation
|
||||
- Use abstract classes for shared entity base types (e.g., `abstract class Participant`)
|
||||
- Use interfaces for response contracts (e.g., `interface IResponse`)
|
||||
- Use enums for typed string/number constants
|
||||
- Avoid `any` where possible; when unavoidable, keep it localised
|
||||
- `strict` is off — but still annotate types explicitly rather than relying on inference
|
||||
|
||||
### Controller Pattern
|
||||
|
||||
```typescript
|
||||
import { Authorized, Body, Delete, Get, JsonController, Param, Post, Put } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
|
||||
@JsonController('/runners')
|
||||
@Authorized()
|
||||
export class RunnerController {
|
||||
@Get('/')
|
||||
@OpenAPI({ description: 'Returns all runners' })
|
||||
@ResponseSchema(ResponseRunner, { isArray: true })
|
||||
async getAll() { ... }
|
||||
|
||||
@Get('/:id')
|
||||
@ResponseSchema(ResponseRunner)
|
||||
async getOne(@Param('id') id: number) { ... }
|
||||
|
||||
@Post('/')
|
||||
@ResponseSchema(ResponseRunner)
|
||||
async post(@Body({ validate: true }) createRunner: CreateRunner) { ... }
|
||||
|
||||
@Put('/:id')
|
||||
@ResponseSchema(ResponseRunner)
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) updateRunner: UpdateRunner) { ... }
|
||||
|
||||
@Delete('/:id')
|
||||
@ResponseSchema(ResponseRunner)
|
||||
async remove(@Param('id') id: number) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Define custom error classes in `src/errors/` extending `routing-controllers` error types (`NotFoundError`, `NotAcceptableError`, etc.)
|
||||
- Every custom error class must include `@IsString()` decorated `name` and `message` fields for OpenAPI schema generation
|
||||
- Throw custom errors directly in controllers: `throw new RunnerNotFoundError()`
|
||||
- Use try/catch in controllers and re-throw meaningful errors; do not swallow errors silently
|
||||
- The global `ErrorHandler` middleware (registered in `src/middlewares/`) catches all unhandled errors and serialises them as JSON — do not duplicate this logic in controllers
|
||||
- Auth errors are thrown from `src/middlewares/authchecker.ts`, not from individual controllers
|
||||
|
||||
### Entity Pattern (TypeORM)
|
||||
|
||||
- Entities live in `src/models/entities/`
|
||||
- Decorate every entity with `@Entity()` and every column with the appropriate `@Column`, `@PrimaryGeneratedColumn`, etc.
|
||||
- Use `@CreateDateColumn()` / `@UpdateDateColumn()` for timestamp fields
|
||||
- Use table inheritance (`@TableInheritance` + `@ChildEntity`) for polymorphic entities
|
||||
- Access repositories via `getConnectionManager().get().getRepository(EntityClass)` — do not inject repositories as constructor dependencies
|
||||
- Database schema is synchronised automatically on startup (`connection.synchronize()`) — no manual migration files
|
||||
|
||||
### DTO Pattern (Create / Update)
|
||||
|
||||
- Create DTOs in `src/models/actions/create/` and `src/models/actions/update/`
|
||||
- Use `class-validator` decorators for every field
|
||||
- `@IsOptional()` for fields that are not required on update; all fields on create DTOs should be mandatory unless explicitly optional in the API contract
|
||||
- Response DTOs live in `src/models/responses/` and follow the `Response{Entity}` naming pattern
|
||||
|
||||
---
|
||||
|
||||
## Test Style Guidelines
|
||||
|
||||
> **IMPORTANT: Do not run existing tests and do not create new tests.** The existing test suite in `src/tests/` is outdated and no longer reflects the current state of the codebase. Ignore all test files when working in this repository. Do not write new tests for any changes or additions.
|
||||
|
||||
All tests are integration tests in `src/tests/` organised by domain entity:
|
||||
|
||||
```
|
||||
src/tests/
|
||||
auth/
|
||||
auth_login.spec.ts
|
||||
auth_refresh.spec.ts
|
||||
runners/
|
||||
runner_add.spec.ts
|
||||
runner_get.spec.ts
|
||||
runner_update.spec.ts
|
||||
runner_delete.spec.ts
|
||||
...
|
||||
```
|
||||
|
||||
### Test File Template
|
||||
|
||||
```typescript
|
||||
import axios from 'axios';
|
||||
import { config } from '../../config';
|
||||
const base = "http://localhost:" + config.internal_port;
|
||||
|
||||
let access_token: string;
|
||||
let axios_config: object;
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(20000);
|
||||
const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" });
|
||||
access_token = res.data["access_token"];
|
||||
axios_config = {
|
||||
headers: { "authorization": "Bearer " + access_token },
|
||||
validateStatus: undefined // prevents axios from throwing on non-2xx responses
|
||||
};
|
||||
});
|
||||
|
||||
describe('POST /api/runners working', () => {
|
||||
it('creating a runner with required params should return 200', async () => {
|
||||
const res = await axios.post(base + '/api/runners', { ... }, axios_config);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers['content-type']).toContain("application/json");
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/runners failing', () => {
|
||||
it('creating a runner without required params should return 400', async () => {
|
||||
const res = await axios.post(base + '/api/runners', {}, axios_config);
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- Always set `validateStatus: undefined` in `axios_config` to prevent axios throwing on error responses
|
||||
- Group tests by HTTP verb + route in `describe()` blocks; separate "working" and "failing" cases
|
||||
- Use `jest.setTimeout(20000)` in `beforeAll` for slow integration tests
|
||||
- Assert both `res.status` and `res.headers['content-type']` on success paths
|
||||
|
||||
---
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
- Copy `.env.example` to `.env` and fill in values before running locally
|
||||
- Database type is set via `DB_TYPE` env var (`sqlite`, `postgres`, or `mysql`)
|
||||
- Server port is set via `INTERNAL_PORT` (accessed as `config.internal_port` in code)
|
||||
- All config values are validated at startup in `src/config.ts`
|
||||
- CI env is generated by `bun run test:ci:generate_env` (`scripts/create_testenv.ts`)
|
||||
|
||||
### NATS Configuration
|
||||
|
||||
The backend uses **NATS JetStream** as a KV cache for scan intake performance optimization.
|
||||
|
||||
- `NATS_URL` — connection URL for NATS server (default: `nats://localhost:4222`)
|
||||
- `NATS_PREWARM` — if `true`, preloads all runner state into the KV cache at startup to eliminate DB reads from the first scan onward (default: `false`)
|
||||
|
||||
**KV buckets** (auto-created by `NatsClient` at startup):
|
||||
- `station_state` — station token cache (1-hour TTL)
|
||||
- `card_state` — card→runner mapping cache (1-hour TTL)
|
||||
- `runner_state` — runner display name, total distance, latest scan timestamp (no TTL, CAS-based updates)
|
||||
|
||||
**Development**: NATS runs in Docker via `docker-compose.yml` (port 4222). The JetStream volume is persisted to `./nats-data/` to survive container restarts.
|
||||
|
||||
**Station intake hot path**: `POST /api/scans/trackscans` from scan stations uses a KV-first flow that eliminates DB reads on cache hits and prevents race conditions via compare-and-swap (CAS) updates. See `SCAN_NATS_PLAN.md` for full architecture details.
|
||||
958
CHANGELOG.md
958
CHANGELOG.md
@@ -2,20 +2,955 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||
|
||||
#### [1.8.3](https://git.odit.services/lfk/backend/compare/1.8.2...1.8.3)
|
||||
|
||||
- fix(RunnerSelfServiceController): Update getStationMe method to use req.stationId instead of header [`70d6091`](https://git.odit.services/lfk/backend/commit/70d6091a6a8a1a3d532c7648d9af51cdf8735c94)
|
||||
- refactor(ScanController, ResponseScanIntake, RunnerKV): Rename totalDistance to distance for consistency [`1be8836`](https://git.odit.services/lfk/backend/commit/1be8836df1a7dec7261356faac8d23bc9558df56)
|
||||
|
||||
#### [1.8.2](https://git.odit.services/lfk/backend/compare/1.8.1...1.8.2)
|
||||
|
||||
> 20 February 2026
|
||||
|
||||
- chore(PERFORMANCE_IDEAS): Remove outdated performance optimization ideas document [`a1a2c27`](https://git.odit.services/lfk/backend/commit/a1a2c2747cda8ad4c049d0d3b188e993daa67a01)
|
||||
- chore(release): 1.8.2 [`1d9c451`](https://git.odit.services/lfk/backend/commit/1d9c451dfbcac105d7440797c32080e30252fd18)
|
||||
- refactor(Dockerfile): Update build process and entry point for TypeScript application [`3197498`](https://git.odit.services/lfk/backend/commit/3197498ab37221156eef42311e58a4038c3309d1)
|
||||
- fix(deps): Add @types/bun dependency to devDependencies [`6caa185`](https://git.odit.services/lfk/backend/commit/6caa1850e3668bbf72912121d2d1923a5e22d6e8)
|
||||
- fix(CreateUser): Await password hashing in toEntity method [`80e7e79`](https://git.odit.services/lfk/backend/commit/80e7e7939c1ab35da9ece1cd9e6e1002e4e50d3a)
|
||||
|
||||
#### [1.8.1](https://git.odit.services/lfk/backend/compare/1.8.0...1.8.1)
|
||||
|
||||
> 20 February 2026
|
||||
|
||||
- perf(stats): Cache stats results for 60 seconds [`13e0c81`](https://git.odit.services/lfk/backend/commit/13e0c81957768c1b380914a0b93d3617c60e08a0)
|
||||
- chore(release): 1.8.1 [`7aaac65`](https://git.odit.services/lfk/backend/commit/7aaac65af4e2d04653645adcf859ca69449e2332)
|
||||
|
||||
#### [1.8.0](https://git.odit.services/lfk/backend/compare/1.7.2...1.8.0)
|
||||
|
||||
> 20 February 2026
|
||||
|
||||
- chore(release): 1.8.0 [`329a29a`](https://git.odit.services/lfk/backend/commit/329a29aca70b8c779c592149dc1cfe197ab62463)
|
||||
- refactor: Switch from official argon2 to Bun's implementation [`a1e697a`](https://git.odit.services/lfk/backend/commit/a1e697acb264a753534c5ff8f5f43357cbc287da)
|
||||
- refactor: Replace uuid and dotenv with bun primitives [`abce517`](https://git.odit.services/lfk/backend/commit/abce517d86daa00d76d691081907cb832494cb91)
|
||||
- refactor(deps): Remove unused glob dependency from package.json and bun.lock [`abdadb8`](https://git.odit.services/lfk/backend/commit/abdadb8e6419c5ec9f8cc0a9e5ebf68671d84a94)
|
||||
|
||||
#### [1.7.2](https://git.odit.services/lfk/backend/compare/1.7.1...1.7.2)
|
||||
|
||||
> 20 February 2026
|
||||
|
||||
- fix(dev): We did it funky bun dev workarounds are no more [`3bb8b20`](https://git.odit.services/lfk/backend/commit/3bb8b202b00f8b7c52c700373ed09a92714528be)
|
||||
- docs: Added agents file to support ai assisted coding [`cbf1da3`](https://git.odit.services/lfk/backend/commit/cbf1da31c9f02a810d8c85caae60ab9483f826c2)
|
||||
- refactor(dev): Yeet the funky dev script out of this codebase [`fd18e56`](https://git.odit.services/lfk/backend/commit/fd18e562518f5b3437f11ceb68e69e50f042891e)
|
||||
- chore(release): 1.7.2 [`c9b8614`](https://git.odit.services/lfk/backend/commit/c9b8614f53619ec76ccf76875c138c986699c746)
|
||||
|
||||
#### [1.7.1](https://git.odit.services/lfk/backend/compare/1.7.0...1.7.1)
|
||||
|
||||
> 20 February 2026
|
||||
|
||||
- fix(ci): Switch to bun in ci [`fe90414`](https://git.odit.services/lfk/backend/commit/fe90414dd910baff8107197408575b6af0cc4cbf)
|
||||
- perf(db): Added indexes [`21ceb9f`](https://git.odit.services/lfk/backend/commit/21ceb9fa265df2f2193a6c4fb58080ead9c72bf8)
|
||||
- chore(release): 1.7.1 [`d1c4744`](https://git.odit.services/lfk/backend/commit/d1c47442314508a95bfa66b83740c957b75f152a)
|
||||
|
||||
#### [1.7.0](https://git.odit.services/lfk/backend/compare/1.6.0...1.7.0)
|
||||
|
||||
> 20 February 2026
|
||||
|
||||
- refactor: Bun by default [`240bd9c`](https://git.odit.services/lfk/backend/commit/240bd9cba10636bfc100ea2732508d805639f105)
|
||||
- chore(release): 1.7.0 [`5081819`](https://git.odit.services/lfk/backend/commit/5081819281eacd6beb8d4876f0a9df71c901e84e)
|
||||
|
||||
#### [1.6.0](https://git.odit.services/lfk/backend/compare/1.5.2...1.6.0)
|
||||
|
||||
> 20 February 2026
|
||||
|
||||
- feat(data): Added nats jetstream dependency [`bbf6ea6`](https://git.odit.services/lfk/backend/commit/bbf6ea6c0fdffa11dacdf4b9afb6160ce54e197d)
|
||||
- chore(deps): Bump typescript and get rid of now legacy imports [`2da8247`](https://git.odit.services/lfk/backend/commit/2da8247978c5142eec194651a7520fa53396d762)
|
||||
- chore(release): 1.6.0 [`53fb038`](https://git.odit.services/lfk/backend/commit/53fb0389cd1da2b71b82102e82fc3d30f0be3820)
|
||||
- feat(nats): Implement caching for card, runner, and station entries with improved key management [`b0c6759`](https://git.odit.services/lfk/backend/commit/b0c67598132deffce697f19c83bd4826420abe76)
|
||||
- feat(auth): Implement caching for scanauth [`526738e`](https://git.odit.services/lfk/backend/commit/526738e48722fffe4493102fad69f65b40fc3b49)
|
||||
- refactor(scan): Implement KV-backed scan station submissions and response model [`d3e0206`](https://git.odit.services/lfk/backend/commit/d3e0206a3ccbff0e69024426bb2bf266cde30eeb)
|
||||
- fix(types): Add custom Express request types for station authentication [`778f159`](https://git.odit.services/lfk/backend/commit/778f15940594d5c2e423ef001eddd2d505ebd5f5)
|
||||
- perf(nats): Implement bulk cache prewarming for runners to optimize startup performance [`024e647`](https://git.odit.services/lfk/backend/commit/024e64729594237773f3819646bdbc806ee985bc)
|
||||
- feat(auth): Switch scanstation auth from argon2 to sha256 to improve performance [`3584b3f`](https://git.odit.services/lfk/backend/commit/3584b3facf7641f18db6eafe7035f17de8c5086c)
|
||||
- perf(nats): Implement bulk cache prewarming for runners to optimize startup performance [`d230350`](https://git.odit.services/lfk/backend/commit/d230350027dea4dcdad9feddd9408a866ed787df)
|
||||
|
||||
#### [1.5.2](https://git.odit.services/lfk/backend/compare/1.5.1...1.5.2)
|
||||
|
||||
> 26 May 2025
|
||||
|
||||
- feat(mailer): Add logging for selfservice forgotten mail requests [`eebcc2e`](https://git.odit.services/lfk/backend/commit/eebcc2e3284230135e3911b4edaecd1a9cfd2100)
|
||||
- chore(release): 1.5.2 [`e27e819`](https://git.odit.services/lfk/backend/commit/e27e8196097da19e24af22368ca8be5a8d9ef6b9)
|
||||
- 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->@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)
|
||||
|
||||
#### [v0.13.3](https://git.odit.services/lfk/backend/compare/v0.13.2...v0.13.3)
|
||||
|
||||
> 15 February 2023
|
||||
|
||||
- 🚀Bumped version to v0.13.3 [`3bac75e`](https://git.odit.services/lfk/backend/commit/3bac75e7ab9f16ecab1fbfa9915a7edb923883f6)
|
||||
- Merge pull request 'feature/201-no_citizen-deletion' (#202) from feature/201-no_citizen-deletion into dev [`d05eddc`](https://git.odit.services/lfk/backend/commit/d05eddcae198427ce9a334096563b3aadcff2b56)
|
||||
- Updated tests [`d5c689d`](https://git.odit.services/lfk/backend/commit/d5c689d6937288df7dca14ce26fbbd4f46a8752a)
|
||||
- Added delete check for citizen org [`8fedd4e`](https://git.odit.services/lfk/backend/commit/8fedd4ef3bdd48dc42abc1d53006eefc145175e3)
|
||||
|
||||
#### [v0.13.2](https://git.odit.services/lfk/backend/compare/v0.13.1...v0.13.2)
|
||||
|
||||
> 3 February 2023
|
||||
|
||||
- 🚀Bumped version to v0.13.2 [`e8b2e6f`](https://git.odit.services/lfk/backend/commit/e8b2e6f26140a18c06b017e4461742d7e7942f08)
|
||||
- Merge pull request 'move selfservice magic link endpoint to 15min rate limit' (#200) from feature/runner-selfservice-login-link-rate-limit into dev [`39f3b0e`](https://git.odit.services/lfk/backend/commit/39f3b0e01f03bfbcfcb0ea08d697268ce068e63d)
|
||||
- move to 15min limit [`edaf255`](https://git.odit.services/lfk/backend/commit/edaf255e8f609185dcd6c2c0cd2e8b007b785e0c)
|
||||
- Merge pull request 'Releases 0.12.0 and 0.13.0' (#199) from dev into main [`41c4ed4`](https://git.odit.services/lfk/backend/commit/41c4ed4d0faaed382801bbe480f31dafa6f3912d)
|
||||
|
||||
#### [v0.13.1](https://git.odit.services/lfk/backend/compare/v0.13.0...v0.13.1)
|
||||
|
||||
> 2 February 2023
|
||||
|
||||
- 🚀Bumped version to v0.13.1 [`f2bd88a`](https://git.odit.services/lfk/backend/commit/f2bd88aadfcb6ffa0485ea6afac8c7664a37f5f4)
|
||||
- Updated description [`67a3661`](https://git.odit.services/lfk/backend/commit/67a36614485b2ea83c2de41e0684708b95a05b32)
|
||||
|
||||
#### [v0.13.0](https://git.odit.services/lfk/backend/compare/v0.12.0...v0.13.0)
|
||||
|
||||
> 2 February 2023
|
||||
|
||||
- Added faker for testing [`e184673`](https://git.odit.services/lfk/backend/commit/e1846739638905aab6ba7e059fd2cbf8ff467bf3)
|
||||
- 📖New license file version [CI SKIP] [skip ci] [`2b641fa`](https://git.odit.services/lfk/backend/commit/2b641faa29c47d95f69983770dc4ab37e674604f)
|
||||
- 🚀Bumped version to v0.13.0 [`0c763a2`](https://git.odit.services/lfk/backend/commit/0c763a2dfd39607b480d9aff7d3c883791f41700)
|
||||
- Updated selfservice tests to prevent email duplication [`9bc80aa`](https://git.odit.services/lfk/backend/commit/9bc80aac8aab9b4dedc26c9bc3ce705d7fe9c0bf)
|
||||
- Moved license and changelog export to releaseit hooks [`77c6303`](https://git.odit.services/lfk/backend/commit/77c6303014578edbbadeeaa790f7974bde2a9764)
|
||||
- Updated readme [`4cdba8b`](https://git.odit.services/lfk/backend/commit/4cdba8bc77ce543f6fb636711b8728bce794eac7)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`ae14d6c`](https://git.odit.services/lfk/backend/commit/ae14d6c74f9205440b41ca5fdbd052ca449148fc)
|
||||
- Added selfservice runner create check to prevent duplicate email [`68cd746`](https://git.odit.services/lfk/backend/commit/68cd746a9f3360b3630a9ba570213d2aa62497b4)
|
||||
- Updated tests for new login in selfservice [`39aa759`](https://git.odit.services/lfk/backend/commit/39aa7598b7cd0ecb0f077f50ebdd31c6e205f06d)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`9fa8b93`](https://git.odit.services/lfk/backend/commit/9fa8b93c08ee52335b18e743f9d205b19e6095c6)
|
||||
- Moved changelog generation to package script [`a7297ff`](https://git.odit.services/lfk/backend/commit/a7297ff933ae1372a9d508cdae1a54d2ebbcc647)
|
||||
- Merge pull request 'feature/197-duplicate_runner_mail' (#198) from feature/197-duplicate_runner_mail into dev [`4b676bc`](https://git.odit.services/lfk/backend/commit/4b676bc85336c2d494e9e74823d38deec5cc0400)
|
||||
- Updated logo url [`4433ddb`](https://git.odit.services/lfk/backend/commit/4433ddb1e15a35481728670e22049200644bf337)
|
||||
- depends_on: ["clone"] [`9cc66ee`](https://git.odit.services/lfk/backend/commit/9cc66eebdfe8e7a2888bbc97197d1756ff44de30)
|
||||
- Fixed typo [`19a290c`](https://git.odit.services/lfk/backend/commit/19a290c3a931ead0d9ae9ebb0985bfbaac54df59)
|
||||
- Rename selfservice forgot to login [`69651d9`](https://git.odit.services/lfk/backend/commit/69651d9f6cd826b6d4720f164897a2a72a57c851)
|
||||
- 📖New license file version [CI SKIP] [skip ci] [`6fd246f`](https://git.odit.services/lfk/backend/commit/6fd246f43cb3f4d0ccb6e017ee699889ba17daac)
|
||||
- Add git for changelog fun [`2fa56b8`](https://git.odit.services/lfk/backend/commit/2fa56b82d1e082a1deae943e5fca5101f24e3ef5)
|
||||
|
||||
#### [v0.12.0](https://git.odit.services/lfk/backend/compare/v0.11.1...v0.12.0)
|
||||
|
||||
> 2 February 2023
|
||||
|
||||
- Pinned versions [`a6d5693`](https://git.odit.services/lfk/backend/commit/a6d5693ccdeb25b15a09af8f7438142114268807)
|
||||
- Drone -> Kaniko based builds [`0e78951`](https://git.odit.services/lfk/backend/commit/0e789513008085d0db94fc3b2dd9e74a5e583049)
|
||||
- Drone images to odit registry [`6ad56b3`](https://git.odit.services/lfk/backend/commit/6ad56b31269bf19a740c1b6b1a303a8a9d7d59d0)
|
||||
- Bumped container base images [`d95c6d3`](https://git.odit.services/lfk/backend/commit/d95c6d33657f6aa977a8ebfefad7e199bb1cc9c3)
|
||||
- Enabled tag via release script [`9217421`](https://git.odit.services/lfk/backend/commit/92174212213f874e41c9472a927bcf87b963ac94)
|
||||
- Pinned pnpm for builds [`4570845`](https://git.odit.services/lfk/backend/commit/4570845b3e1bd00c228fe1b09b658c24e20aba7f)
|
||||
- 🚀Bumped version to v0.12.0 [`4c10e20`](https://git.odit.services/lfk/backend/commit/4c10e20b91a8101ee37b230373ceb3e024582b41)
|
||||
- Ignore pnpm lock [`1f2c8ab`](https://git.odit.services/lfk/backend/commit/1f2c8abb22f3ff1e61b7350b517bd699c3e315f6)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`31b258b`](https://git.odit.services/lfk/backend/commit/31b258b4ce82213144160a4233b7fd127e456776)
|
||||
|
||||
#### [v0.11.1](https://git.odit.services/lfk/backend/compare/v0.11.0...v0.11.1)
|
||||
|
||||
> 22 April 2021
|
||||
|
||||
- Merge pull request 'Release 0.11.1' (#196) from dev into main [`f19f280`](https://git.odit.services/lfk/backend/commit/f19f2808d88414f1877c01f10996dac68b6f9617)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`2229cdf`](https://git.odit.services/lfk/backend/commit/2229cdf20db1a98f9f76a99fa9d3f463cdf6d804)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`348fe52`](https://git.odit.services/lfk/backend/commit/348fe52c42cfa32239b703041820f725e147154e)
|
||||
- Now prefixing runnercards with 2 [`8a82e05`](https://git.odit.services/lfk/backend/commit/8a82e059b74ceabf43c9cbfe9c9b89ef6ce15a28)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`3b9cd2e`](https://git.odit.services/lfk/backend/commit/3b9cd2e1bbbe8e69c3883233a98f286d768c2b79)
|
||||
- Added fix for the appended 2 [`eb526fb`](https://git.odit.services/lfk/backend/commit/eb526fb57faf631fd6e84af99af738ab1b3481c7)
|
||||
- 🚀Bumped version to v0.11.1 [`95320ca`](https://git.odit.services/lfk/backend/commit/95320ca1bccc2886553accea6a428aadffda0a27)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`f2d127f`](https://git.odit.services/lfk/backend/commit/f2d127fc98d75ce658424624abd382c087737ca0)
|
||||
|
||||
#### [v0.11.0](https://git.odit.services/lfk/backend/compare/v0.10.2...v0.11.0)
|
||||
|
||||
> 14 April 2021
|
||||
|
||||
- Merge pull request 'Release 0.11.0' (#195) from dev into main [`3220b19`](https://git.odit.services/lfk/backend/commit/3220b194d4c704835d6d106ec4d9d54a17a38b62)
|
||||
- Fixed spelling [`da266a8`](https://git.odit.services/lfk/backend/commit/da266a8dd68dbb575997ae343624982b690486ec)
|
||||
- Updated tests [`01ed514`](https://git.odit.services/lfk/backend/commit/01ed51489eb92fff907d46a930ecf0b0eb5cad2b)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`520608a`](https://git.odit.services/lfk/backend/commit/520608aef05b21f4daadf55cfc8caddba06b8f01)
|
||||
- Added payedDonationAmount to donor and responsedonor [`8ae4b85`](https://git.odit.services/lfk/backend/commit/8ae4b8582749332f4fb081eee0c520293347001f)
|
||||
- Responses now contain the donation status [`34dbaaa`](https://git.odit.services/lfk/backend/commit/34dbaaafe0422234848eabe3f52b26879c9e5a49)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`278c4a6`](https://git.odit.services/lfk/backend/commit/278c4a6a415434487a92ff66f8114bb2547aac48)
|
||||
- Marked payedAmount as optional during creation and/or update [`0636616`](https://git.odit.services/lfk/backend/commit/0636616dad5afb41ffe47a857d91ac75b4f2f20a)
|
||||
- Added payed amount fileld to donation class [`b8fbb72`](https://git.odit.services/lfk/backend/commit/b8fbb72fa0b659c9acc406c72a8a59c2174351b4)
|
||||
- Added status to tests [`30c6d3d`](https://git.odit.services/lfk/backend/commit/30c6d3d8db9fe37a51e596a73add8b87e8616e54)
|
||||
- Added payed amount to response class [`d64f470`](https://git.odit.services/lfk/backend/commit/d64f470b608b3f179ec77da0210de51c328ef3f2)
|
||||
- 📖New license file version [CI SKIP] [skip ci] [`a2f0d81`](https://git.odit.services/lfk/backend/commit/a2f0d814fc782ad440500e7d6ec779b6ab7f0ac6)
|
||||
- 🚀Bumped version to v0.11.0 [`3558e99`](https://git.odit.services/lfk/backend/commit/3558e9909088647bd4f1f4334f50c07a5ef00214)
|
||||
- Merge pull request 'Donation payment management feature/193-donation_payments' (#194) from feature/193-donation_payments into dev [`6df5f63`](https://git.odit.services/lfk/backend/commit/6df5f634f3123e04c015889573ccc5674a8bab27)
|
||||
- Added payed amount to crealte classes [`71542bc`](https://git.odit.services/lfk/backend/commit/71542bc3887b97c15436d03280e49f7b3f0fcb06)
|
||||
- Added donation status enum [`b4c31ee`](https://git.odit.services/lfk/backend/commit/b4c31ee9b5b35d6e11b07f50f3d30ca12e0f7728)
|
||||
- Added payed amount to update classes [`9930742`](https://git.odit.services/lfk/backend/commit/99307423c533f8cde847b59a80bffc2ff42c9769)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`0c61ff4`](https://git.odit.services/lfk/backend/commit/0c61ff457d02f750efa457dd75464187683b037a)
|
||||
- Added mssing check to tests [`6c14ed9`](https://git.odit.services/lfk/backend/commit/6c14ed9c89eadc1a10db8c912d8ea2711a518766)
|
||||
- No longer answering with null, but 0 [`49b174f`](https://git.odit.services/lfk/backend/commit/49b174f29f63e963e600d74b6923a20211d832eb)
|
||||
- Saved missing file [`8fe3243`](https://git.odit.services/lfk/backend/commit/8fe32436935d7cd6c17eae1e138383d3b714e1ba)
|
||||
|
||||
#### [v0.10.2](https://git.odit.services/lfk/backend/compare/v0.10.1...v0.10.2)
|
||||
|
||||
> 7 April 2021
|
||||
|
||||
- Merge pull request 'Release 0.10.2' (#192) from dev into main [`1d82f65`](https://git.odit.services/lfk/backend/commit/1d82f65b0d3a32d10c1a10c991353c18696d58bf)
|
||||
- Added first selfservice test [`057ae0d`](https://git.odit.services/lfk/backend/commit/057ae0d79758cd627d6d128406a0d201b6b7ad9b)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`b7ad5d3`](https://git.odit.services/lfk/backend/commit/b7ad5d3a31b8b4f5960852d3ac38af133719ebcd)
|
||||
- First try of the laptime sort [`4471e57`](https://git.odit.services/lfk/backend/commit/4471e57438582d55ff846fd69c2cfcc26b40df2a)
|
||||
- Potential fix for all remaining errors [`377d5da`](https://git.odit.services/lfk/backend/commit/377d5dadb2a14cb2d70e0b2dc77026f51b3fb51c)
|
||||
- At least one fewer test should fail now [`87f444c`](https://git.odit.services/lfk/backend/commit/87f444c30d69d65a9f918c63631a859a389eeee3)
|
||||
- Tried workaround for no availdable stats [`8f0f795`](https://git.odit.services/lfk/backend/commit/8f0f795a709db216396998b68b8bbd64ff4d44ff)
|
||||
- Reverted temp bugfix [`4603a84`](https://git.odit.services/lfk/backend/commit/4603a84f16fb53a14d1792447100f5b470969dd0)
|
||||
- Fixed sorting algo [`988f17a`](https://git.odit.services/lfk/backend/commit/988f17a795bb2d867e9d1d8e78051dff1a14ec30)
|
||||
- Added runners stats tests [`7111068`](https://git.odit.services/lfk/backend/commit/7111068361e00cc1308664a3ae650a56e28c015c)
|
||||
- Added basic laptime endpoint [`cb71fcd`](https://git.odit.services/lfk/backend/commit/cb71fcd13bc61e6214e2fd7b70e72094749463d3)
|
||||
- Added orgs by donations stats tests [`d4a02e7`](https://git.odit.services/lfk/backend/commit/d4a02e7db2ff4976be21605e31aac2f3c82a49c0)
|
||||
- Added teams stats endpoint tests [`b9a7dc8`](https://git.odit.services/lfk/backend/commit/b9a7dc84f05441445453193974b2a793b5197fa5)
|
||||
- Now resolving all missing relations [`257f320`](https://git.odit.services/lfk/backend/commit/257f320ee3bf6429c4314c64023520366f9f730b)
|
||||
- Added min laptime to StatsRunner [`51daf96`](https://git.odit.services/lfk/backend/commit/51daf969cf74792b2c2f2f16ce4359d9fca47bc8)
|
||||
- Fixed sorting [`7b15c2d`](https://git.odit.services/lfk/backend/commit/7b15c2d88b14e7279aad97b0c950202ddb5acaaa)
|
||||
- Fixed top-ten bein top 9 [`a6a526d`](https://git.odit.services/lfk/backend/commit/a6a526dc5d8b1613ea34e82e477081349e764aec)
|
||||
- added new ci secret [`5633e85`](https://git.odit.services/lfk/backend/commit/5633e85f41cb69b10fd8a86f57f1bd2f50848f7b)
|
||||
- Added temp console log for test [`22cae39`](https://git.odit.services/lfk/backend/commit/22cae39bd351ca285880e50187ea0d46a7a26437)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`610988e`](https://git.odit.services/lfk/backend/commit/610988ec16b8df61cca61cf2252a469d30318d81)
|
||||
- Added temp console log for ci debugging [`4a73eab`](https://git.odit.services/lfk/backend/commit/4a73eab134c3a9f58771be996bc8811b62cf378e)
|
||||
- Temp disabled runners by donations test [`0b07a53`](https://git.odit.services/lfk/backend/commit/0b07a53ed209c6193ead3c4d199545e22333ab32)
|
||||
- Updated default docker-compose [`f8baca5`](https://git.odit.services/lfk/backend/commit/f8baca5ab2c56b906751bc7edd71477456ad91f3)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`dd6d799`](https://git.odit.services/lfk/backend/commit/dd6d799c847fc96aec1be8f2667ad371890076fb)
|
||||
- Resolved missing parentgroup relation [`23bd432`](https://git.odit.services/lfk/backend/commit/23bd432c5f33a0863217120d97e2e4ea52a08baf)
|
||||
- Removed console logs for now working tests [`71b33ab`](https://git.odit.services/lfk/backend/commit/71b33ab05b53b62c8b271bd2995c94b2fc212dfd)
|
||||
- Fixed typo in test [`cbcb829`](https://git.odit.services/lfk/backend/commit/cbcb829fbde3a4a5e7f94de5dcf24d854c5fc257)
|
||||
- Ptotential fix for stats failing [`dcdbdd1`](https://git.odit.services/lfk/backend/commit/dcdbdd15acfe6eef4220b7ed66db60d78107d1f9)
|
||||
- 🚀Bumped version to v0.10.2 [`6e236ed`](https://git.odit.services/lfk/backend/commit/6e236ede145e164ee84543fb62404b4776550973)
|
||||
- Merge pull request 'stats/runners/laptime feature/190-runners_laptime' (#191) from feature/190-runners_laptime into dev [`a694ad2`](https://git.odit.services/lfk/backend/commit/a694ad225c68fa23152402acba871c857433cc70)
|
||||
- Removed all useless console.logs [`95e1eec`](https://git.odit.services/lfk/backend/commit/95e1eec313a79458dd75307a9d0f8319af0d0904)
|
||||
- Pinned testing container tag to prod container tag [`10221b9`](https://git.odit.services/lfk/backend/commit/10221b9f2e4493080f3ff095d9772bcfd0ac50eb)
|
||||
- Now resolving all relations for orgs by distance [`4a294b1`](https://git.odit.services/lfk/backend/commit/4a294b1e17c44294274b06748ec8141812c2d217)
|
||||
- Added temp console log [`720774f`](https://git.odit.services/lfk/backend/commit/720774fcf47c38601ab88d5d74cfcd0e47b21acf)
|
||||
- Removed console log for passing tests [`132b48c`](https://git.odit.services/lfk/backend/commit/132b48cf2a9e990a5e830c744ed8244bd25e8b3a)
|
||||
- Removed console log [`1d8c8c8`](https://git.odit.services/lfk/backend/commit/1d8c8c8e9cefa58449f7abb2481d9396fe37ba20)
|
||||
- Temp test logging workaround [`bf686e8`](https://git.odit.services/lfk/backend/commit/bf686e89e02998ccc80c838ef890c736c252634c)
|
||||
- Temp test logging workaround [`6163f0a`](https://git.odit.services/lfk/backend/commit/6163f0a90b3721d3a1488f89cbb39ddff7152241)
|
||||
- Removed test for content type [`63964fb`](https://git.odit.services/lfk/backend/commit/63964fbf2c41d9b90f995f056e9db65ab07d54a8)
|
||||
|
||||
#### [v0.10.1](https://git.odit.services/lfk/backend/compare/v0.10.0...v0.10.1)
|
||||
|
||||
> 3 April 2021
|
||||
|
||||
- Merge pull request 'Release 0.10.1' (#189) from dev into main [`e89e07d`](https://git.odit.services/lfk/backend/commit/e89e07d0fc99f14148b01204fb8ed39e2da77e38)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`69afd4d`](https://git.odit.services/lfk/backend/commit/69afd4d5877401eb46df430f43a7feb273abda1e)
|
||||
- 🚀Bumped version to v0.10.1 [`24d152f`](https://git.odit.services/lfk/backend/commit/24d152fdc8fe17fffa2f2a718d7145ba8a91d79c)
|
||||
- New class: ResponseSelfServiceDonor [`d70c5b1`](https://git.odit.services/lfk/backend/commit/d70c5b1bbc9f02782f8755b6929e2d3458e10221)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`4279e43`](https://git.odit.services/lfk/backend/commit/4279e4374304887e8db40eab77763b20bbce91a1)
|
||||
- Removed duplicate openapi statement [`4834a66`](https://git.odit.services/lfk/backend/commit/4834a6698b0958602421c1478a95fec7edda910b)
|
||||
- Switched selfservice donation.donor from string to object [`0767943`](https://git.odit.services/lfk/backend/commit/0767943721b6964d542f580c541e744f86444ac6)
|
||||
- Adjusted runner property names [`ca87774`](https://git.odit.services/lfk/backend/commit/ca87774767807a2c4bc869b0de95cc73832a8405)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`71e3d0e`](https://git.odit.services/lfk/backend/commit/71e3d0efe2cbde47aea0f26cb5a8b5cd3312707d)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`c28843c`](https://git.odit.services/lfk/backend/commit/c28843c405dc4fd06a10f0fb85814acede15a769)
|
||||
- Merge pull request 'Selfservice donations reformatting feature/187-selfservice_donation' (#188) from feature/187-selfservice_donation into dev [`d837654`](https://git.odit.services/lfk/backend/commit/d837654617f7de5d055ffb06c65e2cd52f65c604)
|
||||
- Added new responsetype for new class [`f693f2c`](https://git.odit.services/lfk/backend/commit/f693f2cde9a04147155aea4de5d52e1d19d722ca)
|
||||
|
||||
#### [v0.10.0](https://git.odit.services/lfk/backend/compare/v0.9.2...v0.10.0)
|
||||
|
||||
> 1 April 2021
|
||||
|
||||
- Merge pull request 'Release 0.10.0' (#186) from dev into main [`b517dff`](https://git.odit.services/lfk/backend/commit/b517dff8a82c960836d9f0be90fd89f3ba2fae7d)
|
||||
- 🚀Bumped version to v0.10.0 [`dc3071f`](https://git.odit.services/lfk/backend/commit/dc3071f7d2be298f0bb02d86ec67ed1125cd3b49)
|
||||
- Added locale to mail related runner endpoints [`7af883f`](https://git.odit.services/lfk/backend/commit/7af883f27198206af542bcaff4686221d3788e87)
|
||||
- Added locale to mail related runner endpoints [`f543307`](https://git.odit.services/lfk/backend/commit/f5433076b01c743ed9af085fccadb8f1edc26419)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`5fb355f`](https://git.odit.services/lfk/backend/commit/5fb355f450f19e96d3671b1a46e94d564495942b)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`114c246`](https://git.odit.services/lfk/backend/commit/114c246aceba566cc0dd6daab51a77b951b031cc)
|
||||
- Merge pull request 'Mail locales feature/184-mail_locales' (#185) from feature/184-mail_locales into dev [`33c13de`](https://git.odit.services/lfk/backend/commit/33c13de32c68a3d9e87e4fd9ad12a815ed8c9fde)
|
||||
- Added locale to mail related user endpoints [`1be073a`](https://git.odit.services/lfk/backend/commit/1be073a4fa39f0332a46f567ee6af10a9137844c)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`6aafe4a`](https://git.odit.services/lfk/backend/commit/6aafe4a6ae7d253ab39220e551c52ae067cc481a)
|
||||
|
||||
#### [v0.9.2](https://git.odit.services/lfk/backend/compare/v0.9.1...v0.9.2)
|
||||
|
||||
> 29 March 2021
|
||||
|
||||
- Merge pull request 'Release 0.9.2' (#183) from dev into main [`bdeeb03`](https://git.odit.services/lfk/backend/commit/bdeeb036459c2a2131e843d8a5a6b338e0ba46ea)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`675c876`](https://git.odit.services/lfk/backend/commit/675c8762e8e4cf28d2f334d5ab2e1cb6b594e33c)
|
||||
- Fixed bug in return creation [`6c9b91d`](https://git.odit.services/lfk/backend/commit/6c9b91d75a0d08fc4ab0e72c7a09bd0133566368)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`8c00aef`](https://git.odit.services/lfk/backend/commit/8c00aefd6ce3723d9f83d1c94e6491d5d597391f)
|
||||
- 🚀Bumped version to v0.9.2 [`89e3924`](https://git.odit.services/lfk/backend/commit/89e392473c52a3f328545699a0f4df89be33ba89)
|
||||
|
||||
#### [v0.9.1](https://git.odit.services/lfk/backend/compare/v0.9.0...v0.9.1)
|
||||
|
||||
> 29 March 2021
|
||||
|
||||
- Merge pull request 'Release v0.9.1' (#182) from dev into main [`3afd785`](https://git.odit.services/lfk/backend/commit/3afd785a54fac91c12af789af19b45e6124e0e39)
|
||||
- 🚀Bumped version to v0.9.1 [`a139554`](https://git.odit.services/lfk/backend/commit/a139554e059e9a10acb1733ce1a82b610cc99269)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`8099999`](https://git.odit.services/lfk/backend/commit/8099999e2cdfc8046f9ff4a90681281b671e402d)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`0290b0e`](https://git.odit.services/lfk/backend/commit/0290b0e5f531364d37d8157e639614cf5a6b4189)
|
||||
- Merge pull request 'Return cards generated in bulk feature/180-blank_generation_return' (#181) from feature/180-blank_generation_return into dev [`0f7fa99`](https://git.odit.services/lfk/backend/commit/0f7fa990d473ce2dce032c47c39f79c1d0e8df90)
|
||||
- Added query param to return created runenrcards [`5a36c8d`](https://git.odit.services/lfk/backend/commit/5a36c8dcae3d79b3b05ffb30a7ebb0d31dc8183a)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`58f4d21`](https://git.odit.services/lfk/backend/commit/58f4d2151f459bc72692cc70e02a59b77abfb9f0)
|
||||
- Added test for returnCards=true array length [`1cb2dc9`](https://git.odit.services/lfk/backend/commit/1cb2dc9d53b530435f5798f9cdf7ee866eb7416e)
|
||||
- Added test for single card generation with returnCards=true [`6005b06`](https://git.odit.services/lfk/backend/commit/6005b0661f1d5c461bb102e243cc209a8adc21fa)
|
||||
- Fixed copy-paste oversight [`2f568c9`](https://git.odit.services/lfk/backend/commit/2f568c9cb8ae39ce40ec8df6d9acbaf0d5ae1a26)
|
||||
|
||||
#### [v0.9.0](https://git.odit.services/lfk/backend/compare/v0.8.0...v0.9.0)
|
||||
|
||||
> 26 March 2021
|
||||
|
||||
- Merge pull request 'Release 0.9.0' (#179) from dev into main [`95135dd`](https://git.odit.services/lfk/backend/commit/95135ddc893dcf64be67b47b0ef2b0d9041253bd)
|
||||
- Reenabled user tests [`4c66650`](https://git.odit.services/lfk/backend/commit/4c6665062fe6717242e43b58e66c1f1d030c018d)
|
||||
- Moved to tmp files to better check for other problems [`7a64f23`](https://git.odit.services/lfk/backend/commit/7a64f2393783f97a9729356bc1dfd831927dd312)
|
||||
- Added user creation invalid tests [`888cab5`](https://git.odit.services/lfk/backend/commit/888cab5898caf9e552c421346934bf90f717a653)
|
||||
- Updated auth test to comply with the new pw requirements [`63f6526`](https://git.odit.services/lfk/backend/commit/63f6526e4f59621edbf1fad59fc569b4bd6acbf2)
|
||||
- Added user deletion tests [`e6a8ebc`](https://git.odit.services/lfk/backend/commit/e6a8ebcb5b4f430254da4afe159141b21d8da0ed)
|
||||
- Added user creation valid tests [`383a809`](https://git.odit.services/lfk/backend/commit/383a8095b8286d51fb2fb24ae2fd0156230e56ab)
|
||||
- 📖New license file version [CI SKIP] [skip ci] [`bd7b81e`](https://git.odit.services/lfk/backend/commit/bd7b81efe795c02512c87f3b5dd5eec796580144)
|
||||
- Added password errors [`24c38cc`](https://git.odit.services/lfk/backend/commit/24c38cce26da41ccf375e1ccf04afa1868aad8df)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`274a146`](https://git.odit.services/lfk/backend/commit/274a146b9bccfe5e1a879ca137ebb4f51eaa5d57)
|
||||
- Fixed test params [`070560e`](https://git.odit.services/lfk/backend/commit/070560e8632e833dd26505c02ccb2474462b63ac)
|
||||
- No longer using createuser in seeding process [`96ba25e`](https://git.odit.services/lfk/backend/commit/96ba25ec6c6c397cd2aa322afa79024395f658fe)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`a7fe1e1`](https://git.odit.services/lfk/backend/commit/a7fe1e175918edd7a98983ece570b47075e85e9a)
|
||||
- 🚀Bumped version to v0.8.0 [`c23b4d9`](https://git.odit.services/lfk/backend/commit/c23b4d907f20ed7af37a6de6ea4c61433e30b29b)
|
||||
- 🚀Bumped version to v0.9.0 [`56a5f41`](https://git.odit.services/lfk/backend/commit/56a5f4168621263daeab5d2fda97b944cdc6ab31)
|
||||
- Merge pull request 'Password security feature/99-password_checks' (#177) from feature/99-password_checks into dev [`5a3fc5b`](https://git.odit.services/lfk/backend/commit/5a3fc5b2bd06b3e26177d017d3503f4f627be3f2)
|
||||
- Added pw errors to user controller [`b24e24f`](https://git.odit.services/lfk/backend/commit/b24e24ff7dd75d972cdab0fd1e2fe6c532ca2b2f)
|
||||
- Now checking password rules on user creation [`5daaa3a`](https://git.odit.services/lfk/backend/commit/5daaa3a73c4eca2817d67e226679d125928a3645)
|
||||
- Now checking password rules on user update [`48a87e8`](https://git.odit.services/lfk/backend/commit/48a87e8936e13c48f4baa3f4b10f781ad2f55a44)
|
||||
- Fixed pw not getting hashed currectly; [`cb3ea9b`](https://git.odit.services/lfk/backend/commit/cb3ea9b1ebb82c650abd83d4be8629cfe29a5b21)
|
||||
- Added pw errors to me controller [`9ce35d8`](https://git.odit.services/lfk/backend/commit/9ce35d8eb78a01f40af8c70e640eca3bcb142304)
|
||||
- Now forceing user deletion in tests [`8154e71`](https://git.odit.services/lfk/backend/commit/8154e715bbf18938bd5d1031656a88d39231fa81)
|
||||
- Added password checker dependency [`bd00f4f`](https://git.odit.services/lfk/backend/commit/bd00f4f8d585fb6878874810f7de0b8b9f3950d5)
|
||||
- Fixed empty object getting called [`5369000`](https://git.odit.services/lfk/backend/commit/536900091afd7366128f21058490d0d4f15c6c89)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`03d76e6`](https://git.odit.services/lfk/backend/commit/03d76e6d0bc5b4655f7f441232681c9462815526)
|
||||
- Formatting [`b8c28eb`](https://git.odit.services/lfk/backend/commit/b8c28ebb0808395218b5fb9031f477ae1d48e65e)
|
||||
|
||||
#### [v0.8.0](https://git.odit.services/lfk/backend/compare/v0.7.1...v0.8.0)
|
||||
|
||||
> 26 March 2021
|
||||
|
||||
- Merge pull request 'Release 0.8.0' (#176) from dev into main [`3f8e8ce`](https://git.odit.services/lfk/backend/commit/3f8e8ce3a66a943801c0c8e17885e71feeee744f)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`c9bd6de`](https://git.odit.services/lfk/backend/commit/c9bd6de4762fec04e1e02cd3b667838d05ef39a7)
|
||||
- Merge pull request 'Selfservice deletion feature/174-selfservice_deletion' (#175) from feature/174-selfservice_deletion into dev [`e702118`](https://git.odit.services/lfk/backend/commit/e702118d4d80e362e41bb88c74343d50530d1338)
|
||||
- Added tests for the new endpoint [`20aeed8`](https://git.odit.services/lfk/backend/commit/20aeed87780247dc6401bba725801fc1874e50b5)
|
||||
- Removed param from test [`97159dd`](https://git.odit.services/lfk/backend/commit/97159dd9f81aed080c174a3eb8da9e66dfea9b10)
|
||||
- Added selfservice deletion endpoint [`dcb12b0`](https://git.odit.services/lfk/backend/commit/dcb12b0ac289f8df148ba10ae6389727c16f53fd)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`88844e1`](https://git.odit.services/lfk/backend/commit/88844e1a44d87a7dc253bf9aedf2fb3f6cdd1cfe)
|
||||
- Fixed response bug [`ccb7ae2`](https://git.odit.services/lfk/backend/commit/ccb7ae29a39387c0f2762861565dc22996a2493a)
|
||||
- Updated old hint [`dd12583`](https://git.odit.services/lfk/backend/commit/dd1258333ef67243f8a8df97c176ec5a054a5e3b)
|
||||
|
||||
#### [v0.7.1](https://git.odit.services/lfk/backend/compare/v0.7.0...v0.7.1)
|
||||
|
||||
> 26 March 2021
|
||||
|
||||
- Merge pull request 'Release 0.7.1' (#173) from dev into main [`e76a9ce`](https://git.odit.services/lfk/backend/commit/e76a9cef956b00de7bbb11b6d863d4f33e3d5a34)
|
||||
- Revert "Set timeout even higher b/c sqlite just kills itself during these tests" [`f159252`](https://git.odit.services/lfk/backend/commit/f159252651942e442026dbcaae09b242e05d8204)
|
||||
- Set timeout even higher b/c sqlite just kills itself during these tests [`6ab6099`](https://git.odit.services/lfk/backend/commit/6ab60998d4f716aded93bb3b5d15594fc5e0434a)
|
||||
- Adjusted jest timeout to mitigate sqlite from invalidateing all tests⏱ [`30d220b`](https://git.odit.services/lfk/backend/commit/30d220bc36a28f224406e49ed27ff3f6b4f409e9)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`963253c`](https://git.odit.services/lfk/backend/commit/963253cbc84ed07af13ed0925952ec1b7dcc53ad)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`3ef3a94`](https://git.odit.services/lfk/backend/commit/3ef3a94b20c1abf6fd2f19472e5f448b4c72bd7f)
|
||||
- 🚀Bumped version to v0.7.1 [`135852e`](https://git.odit.services/lfk/backend/commit/135852eb9a91010a4ab972ba9efc7b71dfe4d68f)
|
||||
- Merge pull request 'RESPONSERUNNERCARD fix bugfix/171-responserunnercards' (#172) from bugfix/171-responserunnercards into dev [`539a650`](https://git.odit.services/lfk/backend/commit/539a6509b17cfd373eef8e443eaa7d41168ac7a9)
|
||||
- Now resolveing runnercards [`24aff3b`](https://git.odit.services/lfk/backend/commit/24aff3bac458a9886ca40163484bc72733dc766a)
|
||||
- Tests now keep the group [`f3d73d5`](https://git.odit.services/lfk/backend/commit/f3d73d53467a4d00011d280c24e1e12fbb8e443d)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`ce63043`](https://git.odit.services/lfk/backend/commit/ce63043887769e1f92a8c064d6647e0deb81b7fa)
|
||||
|
||||
#### [v0.7.0](https://git.odit.services/lfk/backend/compare/v0.6.4...v0.7.0)
|
||||
|
||||
> 23 March 2021
|
||||
|
||||
- Merge pull request 'Release 0.7.0' (#170) from dev into main [`e40017a`](https://git.odit.services/lfk/backend/commit/e40017a6b88d83d5bfc57ff4603abeaca7a9a37b)
|
||||
- Added bulk card creation tests [`438ff0f`](https://git.odit.services/lfk/backend/commit/438ff0fc3f246f83b1fa04cb11828f4a61dfcd1e)
|
||||
- Added new "bulk" endpoint [`c1bbda5`](https://git.odit.services/lfk/backend/commit/c1bbda51f067cbd9ac1a9a5378ae3f5d7b9f4eca)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`7a49e7c`](https://git.odit.services/lfk/backend/commit/7a49e7c5c98eb23af1cd0d2084914641e9a1bf90)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`e843a46`](https://git.odit.services/lfk/backend/commit/e843a464e747c0d41280484cb54495cb2de2a9e8)
|
||||
- 🚀Bumped version to v0.7.0 [`d0ae50d`](https://git.odit.services/lfk/backend/commit/d0ae50d5579e969ad33d6b9cfd66dac7fa472223)
|
||||
- Merge pull request 'Bulk card creation feature/168-runnercards_bulk' (#169) from feature/168-runnercards_bulk into dev [`1dd6420`](https://git.odit.services/lfk/backend/commit/1dd64204cc63fb1a8a4a4aa503c21da42945eafd)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`4705a39`](https://git.odit.services/lfk/backend/commit/4705a39aabaad894d332a5062df03840c23c6bfa)
|
||||
|
||||
#### [v0.6.4](https://git.odit.services/lfk/backend/compare/v0.6.3...v0.6.4)
|
||||
|
||||
> 19 March 2021
|
||||
|
||||
- Merge pull request 'Release 0.6.4' (#167) from dev into main [`4d721f6`](https://git.odit.services/lfk/backend/commit/4d721f62d9a5f6a1361ef2811a3a2ff63011b2ad)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`b0328ff`](https://git.odit.services/lfk/backend/commit/b0328ffdaffc8ef2e6e01e808c29748f58f42cac)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`cc6568c`](https://git.odit.services/lfk/backend/commit/cc6568c3810fed3ff2597df0db73a6ca9e072413)
|
||||
- 🚀Bumped version to v0.6.4 [`031cede`](https://git.odit.services/lfk/backend/commit/031cede5426742dc3c2b9dc6b049951d7c14871c)
|
||||
- Adjsuted endpoint [`3c69f8c`](https://git.odit.services/lfk/backend/commit/3c69f8c4a824e588977b06dbb45119cccb03c6bc)
|
||||
|
||||
#### [v0.6.3](https://git.odit.services/lfk/backend/compare/v0.6.2...v0.6.3)
|
||||
|
||||
> 18 March 2021
|
||||
|
||||
- Merge pull request 'Release 0.6.3' (#165) from dev into main [`a3a1395`](https://git.odit.services/lfk/backend/commit/a3a1395a46d7970cff1b8cc2e84306a97791ed88)
|
||||
- The basic bugfix 🐞 [`fbdadbe`](https://git.odit.services/lfk/backend/commit/fbdadbef1f9eb835e1914e8d3770cca836b4c443)
|
||||
- The basic bugfix 🐞 [`c87c97c`](https://git.odit.services/lfk/backend/commit/c87c97c90f5e1229f92671b1f2ebe1fa0d2307cd)
|
||||
- Updated tests 🧪 [`f347b7a`](https://git.odit.services/lfk/backend/commit/f347b7ad4982ed3760117c08e11dca5c3f72d495)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`3f9a704`](https://git.odit.services/lfk/backend/commit/3f9a7049e31a6948125a07e847233b804f27ba31)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`b08acc6`](https://git.odit.services/lfk/backend/commit/b08acc666035ed766cc6ccfa9a410a54db4d7321)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`a6bca59`](https://git.odit.services/lfk/backend/commit/a6bca59ffe06a37f03af21500c442cebeaa74c7e)
|
||||
- 🚀Bumped version to v0.6.3 [`7a303c2`](https://git.odit.services/lfk/backend/commit/7a303c2b2c267d6dd566b1470649e65bc1c1b2ee)
|
||||
- Merge pull request 'TrackScan Update bug 🐞bugfix/163-trackscan_updates' (#164) from bugfix/163-trackscan_updates into dev [`6249419`](https://git.odit.services/lfk/backend/commit/6249419fae22e0203c046c1a3cd82c07f94f510c)
|
||||
|
||||
#### [v0.6.2](https://git.odit.services/lfk/backend/compare/v0.6.1...v0.6.2)
|
||||
|
||||
> 17 March 2021
|
||||
|
||||
- Merge pull request 'Release 0.6.2' (#162) from dev into main [`732a1b8`](https://git.odit.services/lfk/backend/commit/732a1b88d916720ea82cd4b192fc696640ade2aa)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`fcb43f9`](https://git.odit.services/lfk/backend/commit/fcb43f92b0b7a8fa2ed3772357c3eab8e6564eef)
|
||||
- Fixed trackscan vaildation [`a8ea4fa`](https://git.odit.services/lfk/backend/commit/a8ea4fa659732ca2c922fc3c75d2238be2feb5c7)
|
||||
- Added comments✏ [`289f9e2`](https://git.odit.services/lfk/backend/commit/289f9e219692789f86c631f52c67b578216acb48)
|
||||
- Added comments✏ [`937a9fa`](https://git.odit.services/lfk/backend/commit/937a9fad4d8914b83fc6300f776c0720b756a9f4)
|
||||
- Removed duplicate openapi declarations 🗑 [`c8882ae`](https://git.odit.services/lfk/backend/commit/c8882ae6a18188a9c98a237dd594548ebac6f460)
|
||||
- Now defining security per endpoint 🔐 [`d709ee7`](https://git.odit.services/lfk/backend/commit/d709ee74795b785599cda50b4351bd566a0b8573)
|
||||
- Changed the method of getting a parameter from the headers🛠 [`1d38d30`](https://git.odit.services/lfk/backend/commit/1d38d308ad8ae00d67c2b807b584da4f00bd9a58)
|
||||
- Now auto-etting the station token🔥🔥🔥 [`aae042c`](https://git.odit.services/lfk/backend/commit/aae042c041e325626b89b146d005e900bd880453)
|
||||
- Marked station as optional (quality of life improvements incoming) [`1f32ed0`](https://git.odit.services/lfk/backend/commit/1f32ed0727cb1117e5d201b5530b2f2d7f0323d8)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`4c960fe`](https://git.odit.services/lfk/backend/commit/4c960feeb22f819d1c618ced73f5799a3c7e4f00)
|
||||
- Fixed missing renameing🛠 [`0ed7f78`](https://git.odit.services/lfk/backend/commit/0ed7f78b2c284909d47fa0533424c279adef0ba3)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`c1dd451`](https://git.odit.services/lfk/backend/commit/c1dd4518d128edd8b8e36981a513744471241a25)
|
||||
- 🚀Bumped version to v0.6.2 [`5ba8f1d`](https://git.odit.services/lfk/backend/commit/5ba8f1dd4451c1a1b38fdd36cf632c9e6efa829c)
|
||||
- Merge pull request 'Bugfixes for trackscans feature/160-responseTrackScan_total_distance' (#161) from feature/160-responseTrackScan_total_distance into dev [`3d3790c`](https://git.odit.services/lfk/backend/commit/3d3790c2eb6a92bb5b1d2c7e44c75aef4e1b015f)
|
||||
- Fixed wrong error type 👀👀 [`1fa3fa7`](https://git.odit.services/lfk/backend/commit/1fa3fa75ee447b9919585e02c7997e3f1de9c8a7)
|
||||
- Added missing discription [`673e896`](https://git.odit.services/lfk/backend/commit/673e896aa3dc853b301a2e560e785c464a449b6f)
|
||||
|
||||
#### [v0.6.1](https://git.odit.services/lfk/backend/compare/v0.6.0...v0.6.1)
|
||||
|
||||
> 17 March 2021
|
||||
|
||||
#### [v0.6.0](https://git.odit.services/lfk/backend/compare/v0.5.0...v0.6.0)
|
||||
|
||||
> 17 March 2021
|
||||
|
||||
- Merge pull request 'Release v0.6.0' (#159) from dev into main [`bdc7bb6`](https://git.odit.services/lfk/backend/commit/bdc7bb67e7e21769d95a762c3b6dfbf82e7e38d0)
|
||||
- 📖New license file version [CI SKIP] [skip ci] [`5f5c8a0`](https://git.odit.services/lfk/backend/commit/5f5c8a061eb94361e4cd02e9a6469194a9092513)
|
||||
- As requested by @philpp [`2cb7ec7`](https://git.odit.services/lfk/backend/commit/2cb7ec7317d8a48364261506facb2c11c7cf895f)
|
||||
- Updated ci with new kubernetes secrets 🚀🚀🚀 [`5541ae6`](https://git.odit.services/lfk/backend/commit/5541ae6ebd7f36f4482ae752f358102a18b95de0)
|
||||
- Added selfservice forgott positive tests [`bf1ec97`](https://git.odit.services/lfk/backend/commit/bf1ec976e3732b6ac052a55a51ee2ee18a8b1d3d)
|
||||
- Added all "negative" tests [`d0a7e34`](https://git.odit.services/lfk/backend/commit/d0a7e34de8095fca282adefff01fa5f72e7cdba3)
|
||||
- Added mailer functions [`8376513`](https://git.odit.services/lfk/backend/commit/83765136ccacd82ba6a8f9fb43eed78191ee0aa5)
|
||||
- Added tests for the new endpoint [`757332e`](https://git.odit.services/lfk/backend/commit/757332ed2b3325d8730ef1b284ac6ba40356df93)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`e4ed20d`](https://git.odit.services/lfk/backend/commit/e4ed20da3e0a9e32a2e4664d50f316f9131564f0)
|
||||
- Added first selfservice forgotten test [`a95a9b4`](https://git.odit.services/lfk/backend/commit/a95a9b4ec4a3012a91f6f622cfb9f5bff3376344)
|
||||
- Created basic endpoint for user forgotten mails [`d709971`](https://git.odit.services/lfk/backend/commit/d7099717c2eee8aaf1b580345717cc5acc06dbd2)
|
||||
- Implemented the "real" errors [`e26b7d4`](https://git.odit.services/lfk/backend/commit/e26b7d4923777a3013368e29c122709de7e1d9da)
|
||||
- Runner controller now uses the Mailer functions [`a343747`](https://git.odit.services/lfk/backend/commit/a3437475caf6b435ae4bdf6d48aeb7da7d43b25f)
|
||||
- Added scanstation me endpoint [`c5178e0`](https://git.odit.services/lfk/backend/commit/c5178e01814cedaa4402773b10f24d186714c1d2)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`54988ba`](https://git.odit.services/lfk/backend/commit/54988ba0fe012ce87d44c9068f7546a9be73723c)
|
||||
- Added last reset requested timestamp to runners [`66d6023`](https://git.odit.services/lfk/backend/commit/66d6023335c7a9d1a145c4189b610940ef5a525a)
|
||||
- Scanauth return objects [`46b7ace`](https://git.odit.services/lfk/backend/commit/46b7aceb0b86b03688faf0ec6661e4c9fbc6115c)
|
||||
- Revert "Switched normal images to chached registry" [`ca6fa63`](https://git.odit.services/lfk/backend/commit/ca6fa633a156a265d8f643a5f23090b6ab32260d)
|
||||
- Switched normal images to chached registry [`cba4455`](https://git.odit.services/lfk/backend/commit/cba4455d53f9a39b6f9993c36b5abd281201dfa1)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`a7958ee`](https://git.odit.services/lfk/backend/commit/a7958eecd65116ab937f640cbebcae1962cb86c8)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`076aa87`](https://git.odit.services/lfk/backend/commit/076aa87dba1d6fc544e76c16f99c64d37fc82ea0)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`486e450`](https://git.odit.services/lfk/backend/commit/486e450a58d3671dc867ae1a99d052d9fe814c1a)
|
||||
- Updated request timeout [`ffcd45e`](https://git.odit.services/lfk/backend/commit/ffcd45e5724fccdec9b1dbc48f1320525dcd7288)
|
||||
- Added testing env check [`3f37212`](https://git.odit.services/lfk/backend/commit/3f372123fd2e1fae467e9cb20985de1eeb9f6a57)
|
||||
- 🚀Bumped version to v0.6.1 [`ce3ca9f`](https://git.odit.services/lfk/backend/commit/ce3ca9f1c86a6fe72e4dd77e3a0d60bf1e1bf542)
|
||||
- 🚀Bumped version to v0.6.0 [`623b5a1`](https://git.odit.services/lfk/backend/commit/623b5a1873afa73a984251543995b7da1cfdb5c9)
|
||||
- Merge pull request 'Scanstation "me" endpoint feature/157-scanstation_me' (#158) from feature/157-scanstation_me into dev [`13e8399`](https://git.odit.services/lfk/backend/commit/13e839902c063057e902fdb52b403be081d1667e)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`a1a94ec`](https://git.odit.services/lfk/backend/commit/a1a94ec9dafecd9b4c453cc8cfe32c2e90acccf5)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`d5930f7`](https://git.odit.services/lfk/backend/commit/d5930f7c46f4fc8ed56b6eeec9f784d435fd3b2b)
|
||||
- Changed ci pipeline type to kubernetes [`6c43872`](https://git.odit.services/lfk/backend/commit/6c43872198c3dba44b3af3a7cfc7b628d5b304a3)
|
||||
- Mailer now ignores mailing erros when env is set to test [`6bb3ae8`](https://git.odit.services/lfk/backend/commit/6bb3ae8ba992bd6c4d5809d75a264c710999cdcf)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`bf71e35`](https://git.odit.services/lfk/backend/commit/bf71e35ecd333d888d63213d69b04fc681a9d0bd)
|
||||
- Adjusted tests for the new testing env [`9292027`](https://git.odit.services/lfk/backend/commit/92920273bec409563d1e38ea27f4d30f893598e8)
|
||||
- Applied Docker MTU fix 🛠 [`f7af777`](https://git.odit.services/lfk/backend/commit/f7af77710421d7aae5efb048e0622cd067fc20eb)
|
||||
- Updated description [`94001a4`](https://git.odit.services/lfk/backend/commit/94001a48f1b314e91ea5ec982e5585124f9541b6)
|
||||
- Now adding station id to headers of request for scan auth [`8ba7ee1`](https://git.odit.services/lfk/backend/commit/8ba7ee1d481e44e686489e237980b21aaaf6071c)
|
||||
- Merge pull request 'selfservice forgotten mails feature/154-selfservice_forgotten' (#155) from feature/154-selfservice_forgotten into dev [`cb6e78f`](https://git.odit.services/lfk/backend/commit/cb6e78fc176ec9efe94311b64286020b3c5bf633)
|
||||
- Changed endpoint url to avoid conflicts [`e5dab34`](https://git.odit.services/lfk/backend/commit/e5dab3469c3cef6298fc8deb1192a38f7d18406b)
|
||||
- Added console logging when a testing env get's discovered [`c01233b`](https://git.odit.services/lfk/backend/commit/c01233b4d663aefece26dbb86f8b6bcd5c916325)
|
||||
- Added not found error logic [`e7f0cb4`](https://git.odit.services/lfk/backend/commit/e7f0cb45c9ac3aa06e2a57786aa1cc51c9d66598)
|
||||
- Updated to new responsetype [`08957d4`](https://git.odit.services/lfk/backend/commit/08957d4dc2951cfeec56a54680c2ae4ef1525ab2)
|
||||
- Added readme description for testing env [`cedc175`](https://git.odit.services/lfk/backend/commit/cedc1750c21ad256c3337f293f06e894e2c2ef9f)
|
||||
- Renamed test [`1d762f5`](https://git.odit.services/lfk/backend/commit/1d762f56628eff47f4e1a910c7152bd0158283bd)
|
||||
|
||||
#### [v0.5.0](https://git.odit.services/lfk/backend/compare/v0.4.6...v0.5.0)
|
||||
|
||||
> 4 March 2021
|
||||
|
||||
- Merge pull request 'Alpha Release 0.5.0' (#153) from dev into main [`64da0ea`](https://git.odit.services/lfk/backend/commit/64da0eadb313f3bd3ae20a66bcaf4401528008d9)
|
||||
- Removed mail templates [`c2fdfee`](https://git.odit.services/lfk/backend/commit/c2fdfeed4f5fc454b02bc4b198965889c173bbaa)
|
||||
- Removed mail config [`0342757`](https://git.odit.services/lfk/backend/commit/0342757d929b12635c88e74f17495df656865b1a)
|
||||
- Added selfservice scan response class [`6074ac5`](https://git.odit.services/lfk/backend/commit/6074ac5b3a8e43fd98394c1fb70c6e1dea8fcd5e)
|
||||
- Removed old mailer code [`0fcc729`](https://git.odit.services/lfk/backend/commit/0fcc729b56430f0fdb56242857aa1d883d5a4866)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`5272829`](https://git.odit.services/lfk/backend/commit/52728290b477d3f90ee7c14e0d438c4c74415322)
|
||||
- Added the new mailer code [`1551a44`](https://git.odit.services/lfk/backend/commit/1551a444babc025cde6e894c66d2be2c84ab26da)
|
||||
- Removed (now useless) mail controller [`485c247`](https://git.odit.services/lfk/backend/commit/485c247cd3305c4c4422d5582b1d61cc7af84989)
|
||||
- Trackscans now have a laptime that get's calculated on creation [`aa83373`](https://git.odit.services/lfk/backend/commit/aa833736d32993b1656abeeb02a4f8b021ec6252)
|
||||
- Removed useless functions and updated comments [`ada6798`](https://git.odit.services/lfk/backend/commit/ada679823cda8bc31d45c0ff6905f3d270cfd729)
|
||||
- Added new selfservice scans endpoint [`771a205`](https://git.odit.services/lfk/backend/commit/771a205fe634fc5c07e794b3245c59483ff14bd8)
|
||||
- Updated mail errors [`f289afd`](https://git.odit.services/lfk/backend/commit/f289afd8bc47f6eae9f12f765322b2db974ba918)
|
||||
- Laptime is now a part of the response [`a2c97a1`](https://git.odit.services/lfk/backend/commit/a2c97a11a3dc82543076e3844f20d1218943bbf9)
|
||||
- Updated readme env section [`db58a28`](https://git.odit.services/lfk/backend/commit/db58a280b3792b768eb2b1c82a76d9a9836978b1)
|
||||
- Added locale to pw reset endpoint [`a5d2a6e`](https://git.odit.services/lfk/backend/commit/a5d2a6ecd31dc9c186d4201aef5c52e34cbef3b5)
|
||||
- Now using mailer as static funtion [`9a1678a`](https://git.odit.services/lfk/backend/commit/9a1678acf0929dab9f84bd2c6a961b52e36172ce)
|
||||
- Updated readme env section [`149f3a8`](https://git.odit.services/lfk/backend/commit/149f3a83b2e9d59bfbf36c7ea9e27bc7f514856d)
|
||||
- Now checking for mails being set [`bb9bad6`](https://git.odit.services/lfk/backend/commit/bb9bad6d90370e768d4baffaae23ec756cc8353b)
|
||||
- Updated auth reset test for new mailer [`ae7d617`](https://git.odit.services/lfk/backend/commit/ae7d6176902699f82ea127194908ee360233e7b4)
|
||||
- Added scans returns 200 test [`82c65b6`](https://git.odit.services/lfk/backend/commit/82c65b632cdf44165b083494702b836c74e46a41)
|
||||
- 🚀Bumped version to v0.4.7 [`f1d85cf`](https://git.odit.services/lfk/backend/commit/f1d85cfb855c2aae581ade69751b3969ce38f020)
|
||||
- Now generateing bs mailer config in test env [`bf6b701`](https://git.odit.services/lfk/backend/commit/bf6b70106eb735d9ad6f6ad89f09194680af5ae1)
|
||||
- Added new mailer settings to config [`ddea02d`](https://git.odit.services/lfk/backend/commit/ddea02db574cc348685558f3fa3ecc84adbd6b65)
|
||||
- 🚀Bumped version to v0.5.0 [`3f2a2d2`](https://git.odit.services/lfk/backend/commit/3f2a2d292979c7f8162d92465b60b220f2634e7a)
|
||||
- Merge pull request 'Features for the new selfservice feature/151-selfservice_scans_mails' (#152) from feature/151-selfservice_scans_mails into dev [`15356c1`](https://git.odit.services/lfk/backend/commit/15356c1030988d03e3739f3ffe770669789759f2)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`be397c8`](https://git.odit.services/lfk/backend/commit/be397c8899d5b4406c17e8f9951555c54f852901)
|
||||
- Promoted axios to dependency [`a9e06c9`](https://git.odit.services/lfk/backend/commit/a9e06c905537b6da24706389e304e825a33a28ad)
|
||||
- Removed nodemailer from backend [`5833f42`](https://git.odit.services/lfk/backend/commit/5833f4218f9a4c97b69021814df92470a1816917)
|
||||
- Added another resonse type [`030b225`](https://git.odit.services/lfk/backend/commit/030b2255d42aab21d8974fc3a7235285934d53b7)
|
||||
- Added new selfservice response type [`f7f6df4`](https://git.odit.services/lfk/backend/commit/f7f6df41ff74708482db3ea2db717ffb562131c0)
|
||||
|
||||
#### [v0.4.6](https://git.odit.services/lfk/backend/compare/v0.4.5...v0.4.6)
|
||||
|
||||
> 26 February 2021
|
||||
|
||||
- Merge pull request 'Alpha Release 0.4.6' (#148) from dev into main [`dd3c927`](https://git.odit.services/lfk/backend/commit/dd3c9275d60cb5bb1a40fbe91f666f17a8e0c8d3)
|
||||
- Added tests for the new org selfservice endpoints [`28ef139`](https://git.odit.services/lfk/backend/commit/28ef139a70e0c063982b2eb9167b7abe41db1621)
|
||||
- Added selfservice org response model [`ba3b5ee`](https://git.odit.services/lfk/backend/commit/ba3b5eeefc45f9bd94aef24f9f509f6835f5ea7c)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`764b7ff`](https://git.odit.services/lfk/backend/commit/764b7ffe00086248e1f1cccb265ca920a568c0a0)
|
||||
- Merge pull request 'Fixed wrong body acceptance type' (#150) from bugfix/146-usergroup_update into dev [`d870b2f`](https://git.odit.services/lfk/backend/commit/d870b2fd01b11b1732fcbb6feecaf6a6155fa702)
|
||||
- Added selfservice team response model [`ba396e0`](https://git.odit.services/lfk/backend/commit/ba396e0eba15647b3004437a5a9949c7a69e828d)
|
||||
- 📖New license file version [CI SKIP] [skip ci] [`bce8811`](https://git.odit.services/lfk/backend/commit/bce8811925e7f77c64fc507d55335ac45b0e5572)
|
||||
- 📖New license file version [CI SKIP] [skip ci] [`b1fced7`](https://git.odit.services/lfk/backend/commit/b1fced77640b6c26438331474f368f2b0708b672)
|
||||
- Added selfservice org info endpoint [`656f63d`](https://git.odit.services/lfk/backend/commit/656f63dfd5fdbe13554fc98440e416be7e56d909)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`c0cafb4`](https://git.odit.services/lfk/backend/commit/c0cafb4d510116773fed12592cad1efc2ef09f38)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`09fe47b`](https://git.odit.services/lfk/backend/commit/09fe47b9aaac47b65d4e910ef89d558c47fd7364)
|
||||
- Fixed wrong body acceptance type [`aaec09d`](https://git.odit.services/lfk/backend/commit/aaec09d2ab08a76e9d367fdfefc01cea5588f1b9)
|
||||
- Pinned package version to avoid dependency conflicts 📌 [`39ebfbf`](https://git.odit.services/lfk/backend/commit/39ebfbf0b633ecc479a33fdf851cd6550616bfee)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`3736b29`](https://git.odit.services/lfk/backend/commit/3736b29e5435abb05de03e5d99d9adb438cd7d7e)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`305fa00`](https://git.odit.services/lfk/backend/commit/305fa0078d44b39b0391e84ba67b048285cf77b9)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`3afc207`](https://git.odit.services/lfk/backend/commit/3afc207903c9cf1e62e6f4a62601b4213f608192)
|
||||
- Quick bugfix [`5d6c8c9`](https://git.odit.services/lfk/backend/commit/5d6c8c957acd098a20e674ce5529f60cbc9f4151)
|
||||
- 🚀Bumped version to v0.4.6 [`b4acd15`](https://git.odit.services/lfk/backend/commit/b4acd157fc075154a60946c1ee8876ee5f5dfbee)
|
||||
- Merge pull request 'New org selfservice endpoint feature/146-more_selfservice_endpoints' (#147) from feature/146-more_selfservice_endpoints into dev [`45d61b4`](https://git.odit.services/lfk/backend/commit/45d61b487e8e6fdd8e00c184a08c9d6e34a1b6bf)
|
||||
- Added new response types [`3c11d88`](https://git.odit.services/lfk/backend/commit/3c11d88557a2612bf4320ff669323bc048634e94)
|
||||
|
||||
#### [v0.4.5](https://git.odit.services/lfk/backend/compare/v0.4.4...v0.4.5)
|
||||
|
||||
> 9 February 2021
|
||||
|
||||
- Merge pull request 'Alpha release 0.4.5' (#145) from dev into main [`a46d142`](https://git.odit.services/lfk/backend/commit/a46d14278b9a084ca54f8f90e5e70b04739c2dd7)
|
||||
- 🚀Bumped version to v0.4.5 [`cc869f6`](https://git.odit.services/lfk/backend/commit/cc869f69add1f1a175ff94510d52888f81bccb69)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`680ae8e`](https://git.odit.services/lfk/backend/commit/680ae8ebbb39d103085fe1fe8781d71b3c3ed055)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`b9aac71`](https://git.odit.services/lfk/backend/commit/b9aac7167681ff0945e538dd177abd6f97771bf2)
|
||||
- Merge pull request 'usergroups/permissions endpoint feature/143-usergroup_permissions_endpoint' (#144) from feature/143-usergroup_permissions_endpoint into dev [`a30a342`](https://git.odit.services/lfk/backend/commit/a30a342e00ba944f8014044bba28141c0657a17f)
|
||||
- Implemented /groups/permissions endpoint [`0c9867d`](https://git.odit.services/lfk/backend/commit/0c9867d70616615c8f3c72bbec37a4441e4868ef)
|
||||
- Now all /usergroups endpoints return ResponseUserGroup [`bdcfce8`](https://git.odit.services/lfk/backend/commit/bdcfce88cbe069f9ba1925fcaac06367a109d2b7)
|
||||
- The ResponseUserGroup now returns their permisssions as a string array [`416f2a1`](https://git.odit.services/lfk/backend/commit/416f2a1366c570998011d022ebd7f5f44276b2c9)
|
||||
- The ResponseUserGroup now returns their permisssions as a string array [`5e353db`](https://git.odit.services/lfk/backend/commit/5e353db2061c30b4d10965c47f0dcbecb7f59fc5)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`8379c3e`](https://git.odit.services/lfk/backend/commit/8379c3e29c45f0d7c4c84bce1f3abc718158fa84)
|
||||
|
||||
#### [v0.4.4](https://git.odit.services/lfk/backend/compare/v0.4.3...v0.4.4)
|
||||
|
||||
> 9 February 2021
|
||||
|
||||
- Merge pull request 'Alpha release 0.4.4' (#142) from dev into main [`c4edcca`](https://git.odit.services/lfk/backend/commit/c4edccace78765dd5caa0f0e79c52f07c8a3568e)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`ca3d093`](https://git.odit.services/lfk/backend/commit/ca3d093e54bfaaa77c97e96738a74eeb25aee440)
|
||||
- Now loading runner's group's parentgroup with every runner controller request [`701706c`](https://git.odit.services/lfk/backend/commit/701706c0289b357439608b4e2eaa66c617d16e9d)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`74de655`](https://git.odit.services/lfk/backend/commit/74de6559d7c5e8c6d257d41dc91396b53bf0c071)
|
||||
- The group/runners endpoints now also deliver the runner's group's parentGroup [`906a1dc`](https://git.odit.services/lfk/backend/commit/906a1dc9e79ea4eb298a561cf98e6ae42b3ae4ec)
|
||||
- 🚀Bumped version to v0.4.4 [`a6f73c7`](https://git.odit.services/lfk/backend/commit/a6f73c733c8cfc8d84beb7e0bbd5bcd1313df9d0)
|
||||
- Merge pull request 'Expanded runner response feature/140-runner_group_parent' (#141) from feature/140-runner_group_parent into dev [`28cfbaa`](https://git.odit.services/lfk/backend/commit/28cfbaa6624d0bc65e2a9b72ffed17060e828735)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`09bbc70`](https://git.odit.services/lfk/backend/commit/09bbc70f5fd1f026148be07fe889a6907bc3f75a)
|
||||
- Adjusted test for the new response depth [`90e1ad7`](https://git.odit.services/lfk/backend/commit/90e1ad7db72732d13002c87461c33560b74befa6)
|
||||
- Adjusted test for the new response depth [`5872c63`](https://git.odit.services/lfk/backend/commit/5872c6335be573d849cdc3746b261c6cf476c3de)
|
||||
|
||||
#### [v0.4.3](https://git.odit.services/lfk/backend/compare/v0.4.2...v0.4.3)
|
||||
|
||||
> 7 February 2021
|
||||
|
||||
- Merge pull request 'Alpha Release 0.4.3' (#139) from dev into main [`dd9cb6d`](https://git.odit.services/lfk/backend/commit/dd9cb6d3ef60cb118391abc2ba17fd0f83db0b1c)
|
||||
- 🚀Bumped version to v0.4.3 [`656d564`](https://git.odit.services/lfk/backend/commit/656d564baa8c8bf1f63523b0301ad9ff23e08aa4)
|
||||
- Bugfix for @lfk/frontend/#43 [`8f0a396`](https://git.odit.services/lfk/backend/commit/8f0a396dd07937fb7ccfb345d1acbac86eb5d9bb)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`f3f5cb4`](https://git.odit.services/lfk/backend/commit/f3f5cb462e4ecf932ad55eb519815222b4e5dd17)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`23c732b`](https://git.odit.services/lfk/backend/commit/23c732b6905cc9f6479a53a7744b72d01e345ecb)
|
||||
- Merge pull request 'Bugfix for @lfk/frontend/#43' (#138) from bugfix/118-encode_jwt_in_mail into dev [`9959172`](https://git.odit.services/lfk/backend/commit/9959172f2ae11cbb7a2b8e97bad432956fc70a80)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`a18d4d3`](https://git.odit.services/lfk/backend/commit/a18d4d3cee58f8eb9dc428b051a2394bd3ece5c2)
|
||||
|
||||
#### [v0.4.2](https://git.odit.services/lfk/backend/compare/v0.4.1...v0.4.2)
|
||||
|
||||
> 2 February 2021
|
||||
|
||||
- Merge pull request 'Alpha Release 0.4.2' (#137) from dev into main [`390b36d`](https://git.odit.services/lfk/backend/commit/390b36dfd46cf8829126581bd62dd3d4ce8558fa)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`3b718f3`](https://git.odit.services/lfk/backend/commit/3b718f3ce58f12007b6068e5db00a00dbe1c5398)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`f7a0ec7`](https://git.odit.services/lfk/backend/commit/f7a0ec7174521b1863a4bc58c7ff2b86cafdee66)
|
||||
- 🚀Bumped version to v0.4.2 [`321b20b`](https://git.odit.services/lfk/backend/commit/321b20b073b6debd605a92544779d0dfc0449f10)
|
||||
- Merge pull request 'Imprint&Privacy Links feature/135-imprint_and_privacy' (#136) from feature/135-imprint_and_privacy into dev [`110a847`](https://git.odit.services/lfk/backend/commit/110a84783e023407cbcf81506deb7cc204db9659)
|
||||
- 📖New license file version [CI SKIP] [skip ci] [`74791df`](https://git.odit.services/lfk/backend/commit/74791df68b40355e1d1a1f7f5ae4f64422571dc9)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`8425043`](https://git.odit.services/lfk/backend/commit/84250430996920ada15af23b68756daba8f99257)
|
||||
- Added new url env vars to config [`bcad691`](https://git.odit.services/lfk/backend/commit/bcad691045d00c9630bedb0936c123610b655946)
|
||||
- fixed license-exporter call [`74b982a`](https://git.odit.services/lfk/backend/commit/74b982afba3ec82a038c4748420920732fe32a51)
|
||||
- Added documentation about the new env vars to the readme [`333e806`](https://git.odit.services/lfk/backend/commit/333e806da42d7654e2b9fc13abae984726c689e7)
|
||||
- Added imprint and privacy to the api spec [`f4f6219`](https://git.odit.services/lfk/backend/commit/f4f621973aa98645dee3d43252bb18f125087c54)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`3aefa75`](https://git.odit.services/lfk/backend/commit/3aefa754128fc58a8200b280ee036c49cdaaac4a)
|
||||
|
||||
#### [v0.4.1](https://git.odit.services/lfk/backend/compare/v0.4.0...v0.4.1)
|
||||
|
||||
> 30 January 2021
|
||||
|
||||
- Merge pull request 'Alpha Release 0.4.1' (#134) from dev into main [`71cab4e`](https://git.odit.services/lfk/backend/commit/71cab4e836dca2e2072b62676337f5f90796f105)
|
||||
- Deleted useless file [ci skip] [`94dd796`](https://git.odit.services/lfk/backend/commit/94dd7963b73d98089f086175ed79002f59195c26)
|
||||
- Implemented the interface in all responses [`9d5e486`](https://git.odit.services/lfk/backend/commit/9d5e486c6ddb4db87d36409fbd8bca1bf9659e9f)
|
||||
- Adjusted tests for the new responseType parameter (part 1) [`bcc15e4`](https://git.odit.services/lfk/backend/commit/bcc15e42863b641b97cd03440f141332e112c889)
|
||||
- Cleaned up realations regarding response classes [`ff7406e`](https://git.odit.services/lfk/backend/commit/ff7406e71a4a76670d381b415bfb66f602e1206b)
|
||||
- Added Responseobjecttype enum [`581ca5f`](https://git.odit.services/lfk/backend/commit/581ca5ff6c67ed1c701c06532671441293ee0706)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`0229534`](https://git.odit.services/lfk/backend/commit/02295346dad109745faa492dc968abbc98522804)
|
||||
- Dependency bump🔝 [skip ci] [`3d1baae`](https://git.odit.services/lfk/backend/commit/3d1baae0cce26b0c1f6948778b15f4b0097a77a9)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`7ba67b9`](https://git.odit.services/lfk/backend/commit/7ba67b9dcab7692c8c1e548ccbc3895eb084eedd)
|
||||
- Adjusted tests for the new responseType parameter (part 3) [`8dc2810`](https://git.odit.services/lfk/backend/commit/8dc2810c0c4097e26bf1b517bb9d0b102494f6c1)
|
||||
- Added a Response interface [`e44cc4c`](https://git.odit.services/lfk/backend/commit/e44cc4c4cbecdf7c8d90f0af73fffd8b01eba61e)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`4e10077`](https://git.odit.services/lfk/backend/commit/4e100779017c5ce542b59cf2156bd6ab5fdceed2)
|
||||
- Adjusted tests for the new responseType parameter (part 2) [`ff8af09`](https://git.odit.services/lfk/backend/commit/ff8af090e3ea1760fb6110284cff870a0b4cbf84)
|
||||
- 🚀Bumped version to v0.4.1 [`c32fa93`](https://git.odit.services/lfk/backend/commit/c32fa93673010ab18e009ab24fc139ed4f67310e)
|
||||
- Merge pull request 'Response object types feature/132-object_types' (#133) from feature/132-object_types into dev [`6e5f1bd`](https://git.odit.services/lfk/backend/commit/6e5f1bd5ff217a88ec2dfaf154e5e26dee588e12)
|
||||
- Fixed typos and missing types [`2a87819`](https://git.odit.services/lfk/backend/commit/2a878194865d406123805659c011c329c955f669)
|
||||
|
||||
#### [v0.4.0](https://git.odit.services/lfk/backend/compare/v0.3.1...v0.4.0)
|
||||
|
||||
> 30 January 2021
|
||||
|
||||
- Merge pull request 'Alpha Release 0.4.0' (#131) from dev into main [`c4ea808`](https://git.odit.services/lfk/backend/commit/c4ea808e06bdc68add5636346f99a7d292852ab0)
|
||||
- Added pw reset template provided by @philipp [`c116338`](https://git.odit.services/lfk/backend/commit/c116338cd74cf726362f8fa0ae5eea7ec9fabac4)
|
||||
- Added test mail templates [`8270029`](https://git.odit.services/lfk/backend/commit/827002989ee6e3f0d776b5b55b8fb6d65f10c898)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`09b24aa`](https://git.odit.services/lfk/backend/commit/09b24aa6094a980debf4428a1c32583cdb7b606f)
|
||||
- Table fix [`1f0c842`](https://git.odit.services/lfk/backend/commit/1f0c842d9e086456f1ae0f6908e474258a04beb4)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`fea4857`](https://git.odit.services/lfk/backend/commit/fea485768570eb5de2bbd2783e339794a67db2de)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`e07f258`](https://git.odit.services/lfk/backend/commit/e07f258a315898d1183c311e7fcd8f65a415504c)
|
||||
- 🚀Bumped version to v0.4.0 [`e5f4f6e`](https://git.odit.services/lfk/backend/commit/e5f4f6ee590e0885d6eef9151ce7eb76578b70ca)
|
||||
- Merge pull request 'Implemented testmail endpoint feature/124-testmail' (#130) from feature/124-testmail into dev [`f9e75d0`](https://git.odit.services/lfk/backend/commit/f9e75d06b8ee8ff79f60fb384cb2c35ccf19811d)
|
||||
- Merge pull request 'Email Basics feature/118-emails' (#128) from feature/118-emails into dev [`348e6cd`](https://git.odit.services/lfk/backend/commit/348e6cdec7411345953243edfb5322a17ad7614d)
|
||||
- Merge pull request 'Mail+Env documentation feature/123-mail_documentation' (#129) from feature/123-mail_documentation into dev [`61bbeb0`](https://git.odit.services/lfk/backend/commit/61bbeb0d8f3fd6bfafb65bd11eb4c076a27b4a53)
|
||||
- Added pw reset template provided by @philipp [`c116338`](https://git.odit.services/lfk/backend/commit/c116338cd74cf726362f8fa0ae5eea7ec9fabac4)
|
||||
- Implemented automatic ci env generation [`536de2a`](https://git.odit.services/lfk/backend/commit/536de2a3199b1befed54b6fe520a2e3fcefe0d90)
|
||||
- Implemented a basic mailer with reset link sending [`6379753`](https://git.odit.services/lfk/backend/commit/637975305f1adf9bf505507790638cf1e229cfb1)
|
||||
- Table fix [`1f0c842`](https://git.odit.services/lfk/backend/commit/1f0c842d9e086456f1ae0f6908e474258a04beb4)
|
||||
- Implemented the test-mail endpoint via a new mailcontroller [`54ed313`](https://git.odit.services/lfk/backend/commit/54ed313342a72b029b9545bc5ea193e3f0c2166d)
|
||||
- Added documentation for the env vars [`13ccab5`](https://git.odit.services/lfk/backend/commit/13ccab5e289d0a629cefb7fe281a85a46058ae97)
|
||||
- Added comments [`9bd7636`](https://git.odit.services/lfk/backend/commit/9bd7636a23b5a9662ea2b965d2a2407727a188fb)
|
||||
@@ -24,8 +959,15 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
- Implementes mail sending on pw reset request [`e26744b`](https://git.odit.services/lfk/backend/commit/e26744b7925d32d65ef4cc3911651758cfc9274f)
|
||||
- Added a txt variant of the pw-reset mail [`d3647e3`](https://git.odit.services/lfk/backend/commit/d3647e339990d989dbca4d91aa8c3fe5789dd24a)
|
||||
- Changed order [`583a4bc`](https://git.odit.services/lfk/backend/commit/583a4bc0dd0de8026bb2eb6a9b0c31f59344e813)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`fea4857`](https://git.odit.services/lfk/backend/commit/fea485768570eb5de2bbd2783e339794a67db2de)
|
||||
- Translated the pw reset mail to english [`5cade25`](https://git.odit.services/lfk/backend/commit/5cade25eeb137eb5890b3fd450646acfbdff2e8b)
|
||||
- The auth tests now use mail to identify the user [`c43334b`](https://git.odit.services/lfk/backend/commit/c43334bf96901bfd5116301ff7cf4b2ae1dfcbd3)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`e07f258`](https://git.odit.services/lfk/backend/commit/e07f258a315898d1183c311e7fcd8f65a415504c)
|
||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`b972395`](https://git.odit.services/lfk/backend/commit/b972395ae8bb06184baa56296a3b8bfc16ae96a7)
|
||||
- 🚀Bumped version to v0.4.0 [`e5f4f6e`](https://git.odit.services/lfk/backend/commit/e5f4f6ee590e0885d6eef9151ce7eb76578b70ca)
|
||||
- Merge pull request 'Implemented testmail endpoint feature/124-testmail' (#130) from feature/124-testmail into dev [`f9e75d0`](https://git.odit.services/lfk/backend/commit/f9e75d06b8ee8ff79f60fb384cb2c35ccf19811d)
|
||||
- Merge pull request 'Email Basics feature/118-emails' (#128) from feature/118-emails into dev [`348e6cd`](https://git.odit.services/lfk/backend/commit/348e6cdec7411345953243edfb5322a17ad7614d)
|
||||
- Merge pull request 'Mail+Env documentation feature/123-mail_documentation' (#129) from feature/123-mail_documentation into dev [`61bbeb0`](https://git.odit.services/lfk/backend/commit/61bbeb0d8f3fd6bfafb65bd11eb4c076a27b4a53)
|
||||
- Added a test mail sending function [`b94179e`](https://git.odit.services/lfk/backend/commit/b94179e3caaf4be0654ca3372f57a490fb32e208)
|
||||
- Added the first mail error [`c418603`](https://git.odit.services/lfk/backend/commit/c4186034233a296b5971fbef16e7ef6809fbac51)
|
||||
- Now also sending txt mail body [`b92f633`](https://git.odit.services/lfk/backend/commit/b92f633d68604636cecc5e9fdd0d6990b9cb83fe)
|
||||
|
||||
35
Dockerfile
35
Dockerfile
@@ -1,16 +1,23 @@
|
||||
# Typescript Build
|
||||
FROM node:14.15.1-alpine3.12
|
||||
# Build stage - install dependencies
|
||||
FROM registry.odit.services/hub/oven/bun:1.3.9-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN npm i -g pnpm
|
||||
RUN pnpm i
|
||||
COPY tsconfig.json ormconfig.js ./
|
||||
|
||||
COPY package.json bun.lockb* ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Production dependencies only
|
||||
RUN rm -rf /app/node_modules \
|
||||
&& bun install --production --frozen-lockfile
|
||||
|
||||
# Final image - run TypeScript directly
|
||||
FROM registry.odit.services/hub/oven/bun:1.3.9-alpine AS final
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /app/package.json /app/package.json
|
||||
COPY --from=build /app/bun.lockb* /app/
|
||||
COPY --from=build /app/node_modules /app/node_modules
|
||||
|
||||
COPY ormconfig.js bunfig.toml tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm run build
|
||||
# final image
|
||||
FROM node:14.15.1-alpine3.12
|
||||
COPY package.json ormconfig.js ./
|
||||
RUN npm i -g pnpm
|
||||
RUN pnpm i --prod
|
||||
COPY --from=0 /app/dist dist
|
||||
ENTRYPOINT ["node", "dist/app.js"]
|
||||
|
||||
ENTRYPOINT ["bun", "/app/src/app.ts"]
|
||||
141
README.md
141
README.md
@@ -2,73 +2,120 @@
|
||||
|
||||
Backend Server
|
||||
|
||||
## Quickstart 🐳
|
||||
> Use this to run the backend with a postgresql db in docker
|
||||
## Prerequisites
|
||||
|
||||
1. Clone the repo or copy the docker-compose
|
||||
2. Run in toe folder that contains the docker-compose file: `docker-compose up -d`
|
||||
This project uses **Bun** as the runtime and package manager. Install Bun first:
|
||||
|
||||
```bash
|
||||
# macOS/Linux
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# Windows
|
||||
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||
```
|
||||
|
||||
Or visit [bun.sh](https://bun.sh) for other installation methods.
|
||||
|
||||
## Quickstart 🐳
|
||||
> Use this to run the backend with a PostgreSQL db in Docker
|
||||
|
||||
1. Clone the repo or copy the docker-compose
|
||||
2. Run in the folder that contains the docker-compose file: `docker-compose up -d`
|
||||
3. Visit http://127.0.0.1:4010/api/docs to check if the server is running
|
||||
4. You can now use the default admin user (`demo:demo`)
|
||||
|
||||
## Dev Setup 🛠
|
||||
> Local dev setup utilizing sqlite3 as the database.
|
||||
> Local dev setup utilizing SQLite3 as the database and NATS for caching.
|
||||
|
||||
1. Rename the .env.example file to .env (you can adjust app port and other settings, if needed)
|
||||
2. Install Dependencies
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
3. Start the server
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
1. Rename the `.env.example` file to `.env` (you can adjust app port and other settings if needed)
|
||||
2. Start NATS (required for KV cache):
|
||||
```bash
|
||||
docker-compose up -d nats
|
||||
```
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
4. Start the server:
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
**Note**: Bun cannot run TypeScript source files directly due to circular TypeORM dependencies. The `dev` script automatically builds and runs the compiled output. For hot-reload during development, you may need to rebuild manually after code changes.
|
||||
|
||||
### Run Tests
|
||||
```bash
|
||||
# Run tests once (server has to run)
|
||||
yarn test
|
||||
# Run tests once (server has to be running)
|
||||
bun test
|
||||
|
||||
# Run test in watch mode (reruns on change)
|
||||
yarn test:watch
|
||||
bun run test:watch
|
||||
|
||||
# Run test in ci mode (automaticly starts the dev server)
|
||||
yarn test:ci
|
||||
# Run test in CI mode (automatically starts the dev server)
|
||||
bun run 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).
|
||||
### Run Benchmarks
|
||||
```bash
|
||||
# Start the server first
|
||||
bun run dev
|
||||
|
||||
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)
|
||||
# In another terminal:
|
||||
bun run benchmark
|
||||
```
|
||||
|
||||
### Generate Docs
|
||||
```bash
|
||||
yarn docs
|
||||
bun run docs
|
||||
```
|
||||
|
||||
### Other Commands
|
||||
```bash
|
||||
# Build for production
|
||||
bun run build
|
||||
|
||||
# Start production server
|
||||
bun start
|
||||
|
||||
# Seed database with test data
|
||||
bun run seed
|
||||
|
||||
# Export OpenAPI spec
|
||||
bun run openapi:export
|
||||
|
||||
# Generate license report
|
||||
bun run licenses:export
|
||||
|
||||
# Generate changelog
|
||||
bun run changelog:export
|
||||
```
|
||||
|
||||
## ENV Vars
|
||||
> You can provide them via .env file or docker env vars.
|
||||
> You can use the `test:ci:generate_env` package script to generate a example env (uses [ethereal.email](https://ethereal.email) as the mailserver).
|
||||
> You can use the `test:ci:generate_env` package script to generate an example env (uses placeholder data for test server and ignores the errors).
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------------------- | ------------------ | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| APP_PORT | Number | 4010 | The port the backend server listens on. Is optional. |
|
||||
| DB_TYPE | String | N/A | The type of the db you want to use. Supported by TypeORM. Possible: `sqlite`, `mysql`, `postgresql` |
|
||||
| DB_HOST | String | N/A | The db's host IP address/FQDN or file path for sqlite |
|
||||
| DB_PORT | String | N/A | The db's port |
|
||||
| DB_USER | String | N/A | The user for accessing the db |
|
||||
| DB_PASSWORD | String | N/A | The user's password for accessing the db |
|
||||
| DB_NAME | String | N/A | The db's name |
|
||||
| NODE_ENV | String | dev | The app's env - influences debug info. When set to "test", mailing errors get ignored. |
|
||||
| POSTALCODE_COUNTRYCODE | String/CountryCode | N/A | The country code used to validate address postal codes |
|
||||
| PHONE_COUNTRYCODE | String/CountryCode | null (international) | The country code used to validate phone numbers |
|
||||
| SEED_TEST_DATA | Boolean | false | If you want the app to seed example data, set this to true |
|
||||
| STATION_TOKEN_SECRET | String | N/A | Secret key for HMAC-SHA256 station token generation (min 32 chars). **Required.** |
|
||||
| NATS_URL | String(URL) | nats://localhost:4222 | NATS server connection URL for KV cache |
|
||||
| NATS_PREWARM | Boolean | false | Preload all runner state into NATS cache at startup (eliminates DB reads on first scan) |
|
||||
| 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 an 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) |
|
||||
|
||||
| Name | Type | Default | Description
|
||||
| - | - | - | -
|
||||
| APP_PORT | Number | 4010 | The port the backend server listens on. Is optional.
|
||||
| DB_TYPE | String | N/A | The type of the db u want to use. It has to be supported by typeorm. Possible: `sqlite`, `mysql`, `postgresql`
|
||||
| DB_HOST | String | N/A | The db's host's ip-address/fqdn or file path for sqlite
|
||||
| DB_PORT | String | N/A | The db's port
|
||||
| DB_USER | String | N/A | The user for accessing the db
|
||||
| DB_PASSWORD | String | N/A | The user's password for accessing the db
|
||||
| DB_NAME | String | N/A | The db's name
|
||||
| NODE_ENV | String | dev | The apps env - influences debug info.
|
||||
| POSTALCODE_COUNTRYCODE | String/CountryCode | N/A | The countrycode used to validate address's postal codes
|
||||
| PHONE_COUNTRYCODE | String/CountryCode | null (international) | The countrycode used to validate phone numers
|
||||
| SEED_TEST_DATA | Boolean | False | If you want the app to seed some example data set this to true
|
||||
| MAIL_SERVER | String | N/A | The smtp server's ip-address/fqdn
|
||||
| MAIL_PORT | String | N/A | The smtp server's port
|
||||
| MAIL_USER | String | N/A | The username for sending mails
|
||||
| MAIL_PASSWORD | String | N/A | The user's password for sending mails
|
||||
| MAIL_FROM | String | N/A | The from-address for sending mails
|
||||
|
||||
## Recommended Editor
|
||||
|
||||
@@ -85,10 +132,10 @@ yarn docs
|
||||
* A new release tag automaticly triggers the release ci pipeline
|
||||
* main: Protected "release" branch
|
||||
* The latest tag of the docker image get's build from this
|
||||
* New releases get created as tags from this
|
||||
* dev: Current dev branch for merging the different feature branches and bugfixes
|
||||
* New releases get created as tags from this
|
||||
* 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`
|
||||
13
bunfig.toml
Normal file
13
bunfig.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
# Bun configuration
|
||||
# See: https://bun.sh/docs/runtime/bunfig
|
||||
|
||||
[runtime]
|
||||
# Enable Node.js compatibility mode
|
||||
bun = true
|
||||
|
||||
# TypeScript transpiler settings
|
||||
# Required for TypeORM decorators
|
||||
[transpiler]
|
||||
tsconfig = "tsconfig.json"
|
||||
emitDecoratorMetadata = true
|
||||
experimentalDecorators = true
|
||||
@@ -1,18 +1,30 @@
|
||||
version: "3"
|
||||
services:
|
||||
backend_server:
|
||||
build: .
|
||||
nats:
|
||||
image: mirror.gcr.io/library/nats:alpine
|
||||
command: ["--jetstream", "--store_dir", "/data"]
|
||||
ports:
|
||||
- 4010:4010
|
||||
environment:
|
||||
APP_PORT: 4010
|
||||
DB_TYPE: sqlite
|
||||
DB_HOST: bla
|
||||
DB_PORT: bla
|
||||
DB_USER: bla
|
||||
DB_PASSWORD: bla
|
||||
DB_NAME: dev.sqlite
|
||||
NODE_ENV: production
|
||||
- "4222:4222"
|
||||
- "8222:8222"
|
||||
volumes:
|
||||
- nats_data:/data
|
||||
|
||||
# backend_server:
|
||||
# build: .
|
||||
# ports:
|
||||
# - 4010:4010
|
||||
# environment:
|
||||
# APP_PORT: 4010
|
||||
# DB_TYPE: sqlite
|
||||
# DB_HOST: bla
|
||||
# DB_PORT: bla
|
||||
# DB_USER: bla
|
||||
# DB_PASSWORD: bla
|
||||
# DB_NAME: ./db.sqlite
|
||||
# NODE_ENV: production
|
||||
# POSTALCODE_COUNTRYCODE: DE
|
||||
# SEED_TEST_DATA: "true"
|
||||
# MAILER_URL: https://dev.lauf-fuer-kaya.de/mailer
|
||||
# MAILER_KEY: asdasd
|
||||
# APP_PORT: 4010
|
||||
# DB_TYPE: postgres
|
||||
# DB_HOST: backend_db
|
||||
@@ -29,3 +41,6 @@ services:
|
||||
# POSTGRES_USER: lfk
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
|
||||
volumes:
|
||||
nats_data:
|
||||
|
||||
642
licenses.md
642
licenses.md
@@ -1,12 +1,12 @@
|
||||
# argon2
|
||||
**Author**: Ranieri Althoff <ranisalt+argon2@gmail.com>
|
||||
**Repo**: [object Object]
|
||||
# @odit/class-validator-jsonschema
|
||||
**Author**: Aleksi Pekkala <aleksipekkala@gmail.com>
|
||||
**Repo**: git@github.com:epiphone/class-validator-jsonschema.git
|
||||
**License**: MIT
|
||||
**Description**: An Argon2 library for Node
|
||||
**Description**: Convert class-validator-decorated classes into JSON schema
|
||||
## License Text
|
||||
The MIT License (MIT)
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015 Ranieri Althoff
|
||||
Copyright (c) 2017 Aleksi Pekkala
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -25,7 +25,33 @@ 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]
|
||||
**License**: MIT
|
||||
**Description**: Promise based HTTP client for the browser and node.js
|
||||
## License Text
|
||||
Copyright (c) 2014-present Matt Zabriskie
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
# body-parser
|
||||
@@ -59,6 +85,35 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
# check-password-strength
|
||||
**Author**: deanilvincent
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: A NPM Password strength checker based from Javascript RegExp. Check passphrase if it's "Weak", "Medium" or "Strong"
|
||||
## License Text
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Mark Deanil Vicente
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
# class-transformer
|
||||
**Author**: [object Object]
|
||||
**Repo**: [object Object]
|
||||
@@ -88,22 +143,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
# class-validator
|
||||
**Author**: [object Object]
|
||||
**Author**: TypeStack contributors
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: Class-based validation with Typescript / ES6 / ES5 using decorators or validation schemas. Supports both node.js and browser
|
||||
**Description**: Decorator-based property validation for classes.
|
||||
## License Text
|
||||
|
||||
|
||||
# class-validator-jsonschema
|
||||
**Author**: Aleksi Pekkala <aleksipekkala@gmail.com>
|
||||
**Repo**: git@github.com:epiphone/class-validator-jsonschema.git
|
||||
**License**: MIT
|
||||
**Description**: Convert class-validator-decorated classes into JSON schema
|
||||
## License Text
|
||||
MIT License
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017 Aleksi Pekkala
|
||||
Copyright (c) 2015-2020 TypeStack
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -112,17 +160,16 @@ 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 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.
|
||||
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
# consola
|
||||
**Author**: undefined
|
||||
@@ -240,37 +287,6 @@ The above copyright notice and this permission notice shall be included in all c
|
||||
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.
|
||||
|
||||
|
||||
# dotenv
|
||||
**Author**: undefined
|
||||
**Repo**: [object Object]
|
||||
**License**: BSD-2-Clause
|
||||
**Description**: Loads environment variables from .env file
|
||||
## License Text
|
||||
Copyright (c) 2015, Scott Motte
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
# express
|
||||
**Author**: TJ Holowaychuk <tj@vision-media.ca>
|
||||
**Repo**: expressjs/express
|
||||
@@ -332,12 +348,269 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
# libphonenumber-js
|
||||
**Author**: catamphetamine <purecatamphetamine@gmail.com>
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: A simpler (and smaller) rewrite of Google Android's libphonenumber library in javascript
|
||||
## License Text
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2016 @catamphetamine <purecatamphetamine@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# mysql
|
||||
**Author**: Felix Geisendörfer <felix@debuggable.com> (http://debuggable.com/)
|
||||
**Repo**: mysqljs/mysql
|
||||
**License**: MIT
|
||||
**Description**: A node.js driver for mysql. It is written in JavaScript, does not require compiling, and is 100% MIT licensed.
|
||||
## License Text
|
||||
Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
# nats
|
||||
**Author**: [object Object]
|
||||
**Repo**: [object Object]
|
||||
**License**: Apache-2.0
|
||||
**Description**: Node.js client for NATS, a lightweight, high-performance cloud native messaging system
|
||||
## License Text
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2013-2018 The NATS Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
# pg
|
||||
@@ -545,23 +818,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
# uuid
|
||||
**Author**: undefined
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: RFC4122 (v1, v4, and v5) UUIDs
|
||||
## License Text
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2020 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
# validator
|
||||
**Author**: Chris O'Hara <cohara87@gmail.com>
|
||||
**Repo**: [object Object]
|
||||
@@ -590,11 +846,80 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
# @faker-js/faker
|
||||
**Author**: undefined
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: Generate massive amounts of fake contextual data
|
||||
## License Text
|
||||
Faker - Copyright (c) 2022
|
||||
|
||||
This software consists of voluntary contributions made by many individuals.
|
||||
For exact contribution history, see the revision history
|
||||
available at https://github.com/faker-js/faker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
===
|
||||
|
||||
From: https://github.com/faker-js/faker/commit/a9f98046c7d5eeaabe12fc587024c06d683800b8
|
||||
To: https://github.com/faker-js/faker/commit/29234378807c4141588861f69421bf20b5ac635e
|
||||
|
||||
Based on faker.js, copyright Marak Squires and contributor, what follows below is the original license.
|
||||
|
||||
===
|
||||
|
||||
faker.js - Copyright (c) 2020
|
||||
Marak Squires
|
||||
http://github.com/marak/faker.js/
|
||||
|
||||
faker.js was inspired by and has used data definitions from:
|
||||
|
||||
* https://github.com/stympy/faker/ - Copyright (c) 2007-2010 Benjamin Curtis
|
||||
* http://search.cpan.org/~jasonk/Data-Faker-0.07/ - Copyright 2004-2005 by Jason Kohles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
# @odit/license-exporter
|
||||
**Author**: ODIT.Services
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: A simple license crawler
|
||||
**Description**: A simple license crawler for crediting open source work
|
||||
## License Text
|
||||
MIT License Copyright (c) 2020 ODIT.Services (info@odit.services)
|
||||
|
||||
@@ -617,6 +942,35 @@ 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.
|
||||
|
||||
|
||||
# @types/bun
|
||||
**Author**: undefined
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: TypeScript definitions for bun
|
||||
## License Text
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
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
|
||||
|
||||
|
||||
# @types/cors
|
||||
**Author**: undefined
|
||||
**Repo**: [object Object]
|
||||
@@ -679,7 +1033,7 @@ OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
**Author**: undefined
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: TypeScript definitions for Express
|
||||
**Description**: TypeScript definitions for express
|
||||
## License Text
|
||||
MIT License
|
||||
|
||||
@@ -708,7 +1062,7 @@ OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
**Author**: undefined
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: TypeScript definitions for Jest
|
||||
**Description**: TypeScript definitions for jest
|
||||
## License Text
|
||||
MIT License
|
||||
|
||||
@@ -766,7 +1120,7 @@ OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
**Author**: undefined
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: TypeScript definitions for Node.js
|
||||
**Description**: TypeScript definitions for node
|
||||
## License Text
|
||||
MIT License
|
||||
|
||||
@@ -791,42 +1145,15 @@ OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
SOFTWARE
|
||||
|
||||
|
||||
# @types/uuid
|
||||
**Author**: undefined
|
||||
# auto-changelog
|
||||
**Author**: Pete Cook <pete@cookpete.com> (https://github.com/cookpete)
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: TypeScript definitions for uuid
|
||||
**Description**: Command line tool for generating a changelog from git tags and commit history
|
||||
## License Text
|
||||
MIT License
|
||||
The MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
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]
|
||||
**License**: MIT
|
||||
**Description**: Promise based HTTP client for the browser and node.js
|
||||
## License Text
|
||||
Copyright (c) 2014-present Matt Zabriskie
|
||||
Copyright (c) 2017 Pete Cook https://cookpete.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -905,15 +1232,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
# nodemon
|
||||
# release-it
|
||||
**Author**: [object Object]
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: Simple monitor script for use during development of a node.js app.
|
||||
**Description**: Generic CLI tool to automate versioning and package publishing related tasks.
|
||||
## License Text
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2010 - present, Remy Sharp, https://remysharp.com <remy@remysharp.com>
|
||||
Copyright (c) 2018 Lars Kappert
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -936,25 +1263,65 @@ SOFTWARE.
|
||||
|
||||
# rimraf
|
||||
**Author**: Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)
|
||||
**Repo**: git://github.com/isaacs/rimraf.git
|
||||
**License**: ISC
|
||||
**Repo**: git@github.com:isaacs/rimraf.git
|
||||
**License**: BlueOak-1.0.0
|
||||
**Description**: A deep deletion module for node (like `rm -rf`)
|
||||
## License Text
|
||||
The ISC License
|
||||
# Blue Oak Model License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
Version 1.0.0
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
## Purpose
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
This license gives everyone as much permission to work with
|
||||
this software as possible, while protecting contributors
|
||||
from liability.
|
||||
|
||||
## Acceptance
|
||||
|
||||
In order to receive this license, you must agree to its
|
||||
rules. The rules of this license are both obligations
|
||||
under that agreement and conditions to your license.
|
||||
You must not do anything with this software that triggers
|
||||
a rule that you cannot or will not follow.
|
||||
|
||||
## Copyright
|
||||
|
||||
Each contributor licenses you to do everything with this
|
||||
software that would otherwise infringe that contributor's
|
||||
copyright in it.
|
||||
|
||||
## Notices
|
||||
|
||||
You must ensure that everyone who gets a copy of
|
||||
any part of this software from you, with or without
|
||||
changes, also gets the text of this license or a link to
|
||||
<https://blueoakcouncil.org/license/1.0.0>.
|
||||
|
||||
## Excuse
|
||||
|
||||
If anyone notifies you in writing that you have not
|
||||
complied with [Notices](#notices), you can keep your
|
||||
license by taking all practical steps to comply within 30
|
||||
days after the notice. If you do not do so, your license
|
||||
ends immediately.
|
||||
|
||||
## Patent
|
||||
|
||||
Each contributor licenses you to do everything with this
|
||||
software that would otherwise infringe any patent claims
|
||||
they can license or become able to license.
|
||||
|
||||
## Reliability
|
||||
|
||||
No contributor can revoke this license.
|
||||
|
||||
## No Liability
|
||||
|
||||
***As far as the law allows, this software comes as is,
|
||||
without any warranty or condition, and no contributor
|
||||
will be liable to anyone for any damages related to this
|
||||
software or this license, under any kind of legal claim.***
|
||||
|
||||
|
||||
# start-server-and-test
|
||||
@@ -994,35 +1361,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
# ts-node
|
||||
**Author**: [object Object]
|
||||
**Repo**: [object Object]
|
||||
**License**: MIT
|
||||
**Description**: TypeScript execution environment and REPL for node.js, with source map support
|
||||
## License Text
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
# typedoc
|
||||
**Author**: undefined
|
||||
**Repo**: [object Object]
|
||||
|
||||
11
ormconfig.js
11
ormconfig.js
@@ -1,7 +1,3 @@
|
||||
const dotenv = require('dotenv');
|
||||
dotenv.config();
|
||||
//
|
||||
const SOURCE_PATH = process.env.NODE_ENV === 'production' ? 'dist' : 'src';
|
||||
module.exports = {
|
||||
type: process.env.DB_TYPE,
|
||||
host: process.env.DB_HOST,
|
||||
@@ -9,8 +5,7 @@ module.exports = {
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
// entities: ["src/**/entities/*.ts"],
|
||||
entities: [ `${SOURCE_PATH}/**/entities/*{.ts,.js}` ],
|
||||
seeds: [ `${SOURCE_PATH}/**/seeds/*{.ts,.js}` ]
|
||||
// seeds: ['src/seeds/*.ts'],
|
||||
// Run directly from TypeScript source (Bun workflow)
|
||||
entities: ["src/models/entities/**/*.ts"],
|
||||
seeds: ["src/seeds/**/*.ts"]
|
||||
};
|
||||
|
||||
212
package.json
212
package.json
@@ -1,107 +1,105 @@
|
||||
{
|
||||
"name": "@odit/lfk-backend",
|
||||
"version": "0.4.0",
|
||||
"main": "src/app.ts",
|
||||
"repository": "https://git.odit.services/lfk/backend",
|
||||
"author": {
|
||||
"name": "ODIT.Services",
|
||||
"email": "info@odit.services",
|
||||
"url": "https://odit.services"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Philipp Dormann",
|
||||
"email": "philipp@philippdormann.de",
|
||||
"url": "https://philippdormann.de"
|
||||
},
|
||||
{
|
||||
"name": "Nicolai Ort",
|
||||
"email": "info@nicolai-ort.com",
|
||||
"url": "https://nicolai-ort.com"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-NC-SA-4.0",
|
||||
"dependencies": {
|
||||
"@odit/class-validator-jsonschema": "2.1.1",
|
||||
"argon2": "^0.27.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"class-transformer": "0.3.1",
|
||||
"class-validator": "^0.13.1",
|
||||
"consola": "^2.15.0",
|
||||
"cookie": "^0.4.1",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"csvtojson": "^2.0.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"libphonenumber-js": "^1.9.7",
|
||||
"mysql": "^2.18.1",
|
||||
"nodemailer": "^6.4.17",
|
||||
"pg": "^8.5.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"routing-controllers": "^0.9.0-alpha.6",
|
||||
"routing-controllers-openapi": "^2.2.0",
|
||||
"sqlite3": "5.0.0",
|
||||
"typeorm": "^0.2.29",
|
||||
"typeorm-routing-controllers-extensions": "^0.2.0",
|
||||
"typeorm-seeding": "^1.6.1",
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "^13.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@odit/license-exporter": "^0.0.9",
|
||||
"@types/cors": "^2.8.9",
|
||||
"@types/csvtojson": "^1.1.5",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/jest": "^26.0.16",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/node": "^14.14.20",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"axios": "^0.21.1",
|
||||
"cp-cli": "^2.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"nodemon": "^2.0.7",
|
||||
"release-it": "^14.2.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"start-server-and-test": "^1.11.7",
|
||||
"ts-jest": "^26.4.4",
|
||||
"ts-node": "^9.1.1",
|
||||
"typedoc": "^0.20.14",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nodemon src/app.ts",
|
||||
"build": "rimraf ./dist && tsc && cp-cli ./src/static ./dist/static",
|
||||
"docs": "typedoc --out docs src",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watchAll",
|
||||
"test:ci:generate_env": "ts-node scripts/create_testenv.ts",
|
||||
"test:ci:run": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test",
|
||||
"test:ci": "npm run test:ci:generate_env && npm run test:ci:run",
|
||||
"seed": "ts-node ./node_modules/typeorm/cli.js schema:sync && ts-node ./node_modules/typeorm-seeding/dist/cli.js seed",
|
||||
"openapi:export": "ts-node scripts/openapi_export.ts",
|
||||
"licenses:export": "license-exporter --md",
|
||||
"release": "release-it --only-version"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
"commit": true,
|
||||
"requireCleanWorkingDir": false,
|
||||
"commitMessage": "🚀Bumped version to v${version}",
|
||||
"requireBranch": "dev",
|
||||
"push": false,
|
||||
"tag": false
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
}
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"src/tests/*",
|
||||
"docs/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "@odit/lfk-backend",
|
||||
"version": "1.8.3",
|
||||
"main": "src/app.ts",
|
||||
"repository": "https://git.odit.services/lfk/backend",
|
||||
"author": {
|
||||
"name": "ODIT.Services",
|
||||
"email": "info@odit.services",
|
||||
"url": "https://odit.services"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Philipp Dormann",
|
||||
"email": "philipp@philippdormann.de",
|
||||
"url": "https://philippdormann.de"
|
||||
},
|
||||
{
|
||||
"name": "Nicolai Ort",
|
||||
"email": "info@nicolai-ort.com",
|
||||
"url": "https://nicolai-ort.com"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-NC-SA-4.0",
|
||||
"dependencies": {
|
||||
"@odit/class-validator-jsonschema": "2.1.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.0",
|
||||
"consola": "2.15.0",
|
||||
"cookie": "0.4.1",
|
||||
"cookie-parser": "1.4.5",
|
||||
"cors": "2.8.5",
|
||||
"csvtojson": "2.0.10",
|
||||
"express": "4.17.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"libphonenumber-js": "1.9.9",
|
||||
"mysql": "2.18.1",
|
||||
"nats": "^2.29.3",
|
||||
"pg": "8.5.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"routing-controllers": "0.9.0-alpha.6",
|
||||
"routing-controllers-openapi": "2.2.0",
|
||||
"sqlite3": "5.1.7",
|
||||
"typeorm": "0.2.30",
|
||||
"typeorm-routing-controllers-extensions": "0.2.0",
|
||||
"typeorm-seeding": "1.6.1",
|
||||
"validator": "13.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "7.6.0",
|
||||
"@odit/license-exporter": "0.0.9",
|
||||
"@types/bun": "^1.3.9",
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/csvtojson": "1.1.5",
|
||||
"@types/express": "5.0.6",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/node": "25.3.0",
|
||||
"auto-changelog": "2.4.0",
|
||||
"cp-cli": "2.0.0",
|
||||
"jest": "26.6.3",
|
||||
"release-it": "14.2.2",
|
||||
"rimraf": "^6.1.3",
|
||||
"start-server-and-test": "1.11.7",
|
||||
"ts-jest": "26.5.0",
|
||||
"typedoc": "0.20.19",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/app.ts",
|
||||
"start": "bun src/app.ts",
|
||||
"docs": "typedoc --out docs src",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watchAll",
|
||||
"test:ci:generate_env": "bun scripts/create_testenv.ts",
|
||||
"test:ci:run": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test",
|
||||
"test:ci": "bun run test:ci:generate_env && bun run test:ci:run",
|
||||
"benchmark": "bun scripts/benchmark_scan_intake.ts",
|
||||
"seed": "bun ./node_modules/typeorm/cli.js schema:sync && bun ./node_modules/typeorm-seeding/dist/cli.js seed",
|
||||
"openapi:export": "bun scripts/openapi_export.ts",
|
||||
"licenses:export": "license-exporter --markdown",
|
||||
"changelog:export": "auto-changelog --commit-limit false -p -u --hide-credit",
|
||||
"release": "release-it --only-version"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
"commit": true,
|
||||
"requireCleanWorkingDir": false,
|
||||
"commitMessage": "chore(release): ${version}",
|
||||
"requireBranch": "dev",
|
||||
"push": true,
|
||||
"tag": true,
|
||||
"tagName": "${version}",
|
||||
"tagAnnotation": "${version}"
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
},
|
||||
"hooks": {
|
||||
"after:bump": "bun run changelog:export && bun run licenses:export && git add CHANGELOG.md && git add licenses.md"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9427
pnpm-lock.yaml
generated
Normal file
9427
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- sqlite3
|
||||
367
scripts/benchmark_scan_intake.ts
Normal file
367
scripts/benchmark_scan_intake.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* Scan Intake Benchmark Script
|
||||
*
|
||||
* Measures TrackScan creation performance before and after each optimisation phase.
|
||||
* Run against a live dev server: bun run dev
|
||||
*
|
||||
* Usage:
|
||||
* bun run benchmark
|
||||
* bun scripts/benchmark_scan_intake.ts --base http://localhost:4010
|
||||
*
|
||||
* What it measures:
|
||||
* 1. Single sequential scans — baseline latency per request (p50/p95/p99/max)
|
||||
* 2. Parallel scans (10 stations) — simulates 10 concurrent stations each submitting
|
||||
* one scan at a time at the expected event rate
|
||||
* (~1 scan/3s per station = ~3.3 scans/s total)
|
||||
*
|
||||
* The script self-provisions all required data (org, runners, cards, track, stations)
|
||||
* and cleans up after itself. It authenticates via the station token, matching the
|
||||
* real production auth path exactly.
|
||||
*
|
||||
* Output is printed to stdout in a copy-paste-friendly table format so results can
|
||||
* be compared across phases.
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BASE = (() => {
|
||||
const idx = process.argv.indexOf('--base');
|
||||
return idx !== -1 ? process.argv[idx + 1] : 'http://localhost:4010';
|
||||
})();
|
||||
|
||||
const API = `${BASE}/api`;
|
||||
|
||||
// Number of simulated scan stations
|
||||
const STATION_COUNT = 10;
|
||||
|
||||
// Sequential benchmark: total number of scans to send, one at a time
|
||||
const SEQUENTIAL_SCAN_COUNT = 50;
|
||||
|
||||
// Parallel benchmark: number of rounds. Each round fires STATION_COUNT scans concurrently.
|
||||
// 20 rounds × 10 stations = 200 total scans, matching the expected event throughput pattern.
|
||||
const PARALLEL_ROUNDS = 20;
|
||||
|
||||
// Minimum lap time on the test track (seconds). Set low so most scans are valid.
|
||||
// The benchmark measures submission speed, not business logic.
|
||||
const TRACK_MINIMUM_LAP_TIME = 1;
|
||||
|
||||
// Track distance (metres)
|
||||
const TRACK_DISTANCE = 400;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface StationHandle {
|
||||
id: number;
|
||||
key: string; // cleartext token, used as Bearer token
|
||||
cardCode: number; // EAN-13 barcode of the card assigned to this station's runner
|
||||
axiosInstance: AxiosInstance;
|
||||
}
|
||||
|
||||
interface Percentiles {
|
||||
p50: number;
|
||||
p95: number;
|
||||
p99: number;
|
||||
max: number;
|
||||
min: number;
|
||||
mean: number;
|
||||
}
|
||||
|
||||
interface BenchmarkResult {
|
||||
label: string;
|
||||
totalScans: number;
|
||||
totalTimeMs: number;
|
||||
scansPerSecond: number;
|
||||
latencies: Percentiles;
|
||||
errors: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTTP helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const adminClient = axios.create({
|
||||
baseURL: API,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
|
||||
async function adminLogin(): Promise<string> {
|
||||
const res = await adminClient.post('/auth/login', { username: 'demo', password: 'demo' });
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Login failed: ${res.status} ${JSON.stringify(res.data)}`);
|
||||
}
|
||||
return res.data.access_token;
|
||||
}
|
||||
|
||||
function authedClient(token: string): AxiosInstance {
|
||||
return axios.create({
|
||||
baseURL: API,
|
||||
validateStatus: () => true,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data provisioning
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function provision(adminToken: string): Promise<{
|
||||
stations: StationHandle[];
|
||||
trackId: number;
|
||||
orgId: number;
|
||||
cleanup: () => Promise<void>;
|
||||
}> {
|
||||
const client = authedClient(adminToken);
|
||||
const createdIds: { type: string; id: number }[] = [];
|
||||
|
||||
const create = async (path: string, body: object): Promise<any> => {
|
||||
const res = await client.post(path, body);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`POST ${path} failed: ${res.status} ${JSON.stringify(res.data)}`);
|
||||
}
|
||||
return res.data;
|
||||
};
|
||||
|
||||
process.stdout.write('Provisioning test data... ');
|
||||
|
||||
// Organisation
|
||||
const org = await create('/organizations', { name: 'benchmark-org' });
|
||||
createdIds.push({ type: 'organizations', id: org.id });
|
||||
|
||||
// Track with a low minimumLapTime so re-scans within the benchmark are mostly valid
|
||||
const track = await create('/tracks', {
|
||||
name: 'benchmark-track',
|
||||
distance: TRACK_DISTANCE,
|
||||
minimumLapTime: TRACK_MINIMUM_LAP_TIME,
|
||||
});
|
||||
createdIds.push({ type: 'tracks', id: track.id });
|
||||
|
||||
// One runner + card + station per simulated scan station
|
||||
const stations: StationHandle[] = [];
|
||||
|
||||
for (let i = 0; i < STATION_COUNT; i++) {
|
||||
const runner = await create('/runners', {
|
||||
firstname: `Bench`,
|
||||
lastname: `Runner${i}`,
|
||||
group: org.id,
|
||||
});
|
||||
createdIds.push({ type: 'runners', id: runner.id });
|
||||
|
||||
const card = await create('/cards', { runner: runner.id });
|
||||
createdIds.push({ type: 'cards', id: card.id });
|
||||
|
||||
const station = await create('/stations', {
|
||||
track: track.id,
|
||||
description: `bench-station-${i}`,
|
||||
});
|
||||
createdIds.push({ type: 'stations', id: station.id });
|
||||
|
||||
stations.push({
|
||||
id: station.id,
|
||||
key: station.key,
|
||||
cardCode: card.id, // the test spec uses card.id directly as the barcode value
|
||||
axiosInstance: axios.create({
|
||||
baseURL: API,
|
||||
validateStatus: () => true,
|
||||
headers: { authorization: `Bearer ${station.key}` },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`done. (${STATION_COUNT} stations, ${STATION_COUNT} runners, ${STATION_COUNT} cards)`);
|
||||
|
||||
const cleanup = async () => {
|
||||
process.stdout.write('Cleaning up test data... ');
|
||||
// Delete in reverse-dependency order
|
||||
for (const item of [...createdIds].reverse()) {
|
||||
await client.delete(`/${item.type}/${item.id}?force=true`);
|
||||
}
|
||||
console.log('done.');
|
||||
};
|
||||
|
||||
return { stations, trackId: track.id, orgId: org.id, cleanup };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Single scan submission (returns latency in ms)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function submitScan(station: StationHandle): Promise<{ latencyMs: number; ok: boolean }> {
|
||||
const start = performance.now();
|
||||
const res = await station.axiosInstance.post('/scans/trackscans', {
|
||||
card: station.cardCode,
|
||||
station: station.id,
|
||||
});
|
||||
const latencyMs = performance.now() - start;
|
||||
const ok = res.status === 200;
|
||||
return { latencyMs, ok };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Statistics
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function percentiles(latencies: number[]): Percentiles {
|
||||
const sorted = [...latencies].sort((a, b) => a - b);
|
||||
const at = (pct: number) => sorted[Math.floor((pct / 100) * sorted.length)] ?? sorted[sorted.length - 1];
|
||||
const mean = sorted.reduce((s, v) => s + v, 0) / sorted.length;
|
||||
return {
|
||||
p50: Math.round(at(50)),
|
||||
p95: Math.round(at(95)),
|
||||
p99: Math.round(at(99)),
|
||||
max: Math.round(sorted[sorted.length - 1]),
|
||||
min: Math.round(sorted[0]),
|
||||
mean: Math.round(mean),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Benchmark 1 — Sequential (single station, one scan at a time)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function benchmarkSequential(station: StationHandle): Promise<BenchmarkResult> {
|
||||
const latencies: number[] = [];
|
||||
let errors = 0;
|
||||
|
||||
process.stdout.write(` Running ${SEQUENTIAL_SCAN_COUNT} sequential scans`);
|
||||
const wallStart = performance.now();
|
||||
|
||||
for (let i = 0; i < SEQUENTIAL_SCAN_COUNT; i++) {
|
||||
const { latencyMs, ok } = await submitScan(station);
|
||||
latencies.push(latencyMs);
|
||||
if (!ok) errors++;
|
||||
if ((i + 1) % 10 === 0) process.stdout.write('.');
|
||||
}
|
||||
|
||||
const totalTimeMs = performance.now() - wallStart;
|
||||
console.log(' done.');
|
||||
|
||||
return {
|
||||
label: 'Sequential (1 station)',
|
||||
totalScans: SEQUENTIAL_SCAN_COUNT,
|
||||
totalTimeMs,
|
||||
scansPerSecond: (SEQUENTIAL_SCAN_COUNT / totalTimeMs) * 1000,
|
||||
latencies: percentiles(latencies),
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Benchmark 2 — Parallel (10 stations, concurrent rounds)
|
||||
//
|
||||
// Models the real event scenario: every ~3 seconds each station submits one scan.
|
||||
// We don't actually sleep between rounds — we fire each round as fast as the
|
||||
// previous one completes, which gives us the worst-case sustained throughput
|
||||
// (all stations submitting at maximum rate simultaneously).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function benchmarkParallel(stations: StationHandle[]): Promise<BenchmarkResult> {
|
||||
const latencies: number[] = [];
|
||||
let errors = 0;
|
||||
|
||||
process.stdout.write(` Running ${PARALLEL_ROUNDS} rounds × ${STATION_COUNT} concurrent stations`);
|
||||
const wallStart = performance.now();
|
||||
|
||||
for (let round = 0; round < PARALLEL_ROUNDS; round++) {
|
||||
const results = await Promise.all(stations.map(s => submitScan(s)));
|
||||
for (const { latencyMs, ok } of results) {
|
||||
latencies.push(latencyMs);
|
||||
if (!ok) errors++;
|
||||
}
|
||||
if ((round + 1) % 4 === 0) process.stdout.write('.');
|
||||
}
|
||||
|
||||
const totalTimeMs = performance.now() - wallStart;
|
||||
const totalScans = PARALLEL_ROUNDS * STATION_COUNT;
|
||||
console.log(' done.');
|
||||
|
||||
return {
|
||||
label: `Parallel (${STATION_COUNT} stations concurrent)`,
|
||||
totalScans,
|
||||
totalTimeMs,
|
||||
scansPerSecond: (totalScans / totalTimeMs) * 1000,
|
||||
latencies: percentiles(latencies),
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output formatting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function printResult(result: BenchmarkResult) {
|
||||
const { label, totalScans, totalTimeMs, scansPerSecond, latencies, errors } = result;
|
||||
console.log(`\n ${label}`);
|
||||
console.log(` ${'─'.repeat(52)}`);
|
||||
console.log(` Total scans : ${totalScans}`);
|
||||
console.log(` Total time : ${totalTimeMs.toFixed(0)} ms`);
|
||||
console.log(` Throughput : ${scansPerSecond.toFixed(2)} scans/sec`);
|
||||
console.log(` Latency min : ${latencies.min} ms`);
|
||||
console.log(` Latency mean : ${latencies.mean} ms`);
|
||||
console.log(` Latency p50 : ${latencies.p50} ms`);
|
||||
console.log(` Latency p95 : ${latencies.p95} ms`);
|
||||
console.log(` Latency p99 : ${latencies.p99} ms`);
|
||||
console.log(` Latency max : ${latencies.max} ms`);
|
||||
console.log(` Errors : ${errors}`);
|
||||
}
|
||||
|
||||
function printSummary(results: BenchmarkResult[]) {
|
||||
const now = new Date().toISOString();
|
||||
console.log('\n');
|
||||
console.log('═'.repeat(60));
|
||||
console.log(` SCAN INTAKE BENCHMARK RESULTS — ${now}`);
|
||||
console.log(` Server: ${BASE}`);
|
||||
console.log('═'.repeat(60));
|
||||
for (const r of results) {
|
||||
printResult(r);
|
||||
}
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
console.log(' Copy the block above to compare across phases.');
|
||||
console.log('═'.repeat(60) + '\n');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
console.log(`\nScan Intake Benchmark — target: ${BASE}\n`);
|
||||
|
||||
let adminToken: string;
|
||||
try {
|
||||
adminToken = await adminLogin();
|
||||
} catch (err) {
|
||||
console.error(`Could not authenticate. Is the server running at ${BASE}?\n`, err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { stations, cleanup } = await provision(adminToken);
|
||||
|
||||
const results: BenchmarkResult[] = [];
|
||||
|
||||
try {
|
||||
console.log('\nBenchmark 1 — Sequential');
|
||||
results.push(await benchmarkSequential(stations[0]));
|
||||
|
||||
// Brief pause between benchmarks so the sequential scans don't skew
|
||||
// the parallel benchmark's first-scan latency (minimumLapTime window)
|
||||
await new Promise(r => setTimeout(r, (TRACK_MINIMUM_LAP_TIME + 1) * 1000));
|
||||
|
||||
console.log('\nBenchmark 2 — Parallel');
|
||||
results.push(await benchmarkParallel(stations));
|
||||
} finally {
|
||||
await cleanup();
|
||||
}
|
||||
|
||||
printSummary(results);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Benchmark failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,15 +1,8 @@
|
||||
import consola from "consola";
|
||||
import fs from "fs";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
|
||||
nodemailer.createTestAccount((err, account) => {
|
||||
if (err) {
|
||||
console.error('Failed to create a testing account. ' + err.message);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
const env = `
|
||||
const env = `
|
||||
APP_PORT=4010
|
||||
DB_TYPE=sqlite
|
||||
DB_HOST=bla
|
||||
@@ -17,21 +10,15 @@ DB_PORT=bla
|
||||
DB_USER=bla
|
||||
DB_PASSWORD=bla
|
||||
DB_NAME=./test.sqlite
|
||||
NODE_ENV=dev
|
||||
NODE_ENV=test
|
||||
POSTALCODE_COUNTRYCODE=DE
|
||||
SEED_TEST_DATA=true
|
||||
MAIL_SERVER=${account.smtp.host}
|
||||
MAIL_PORT=${account.smtp.port}
|
||||
MAIL_USER=${account.user}
|
||||
MAIL_PASSWORD=${account.pass}
|
||||
MAIL_FROM=${account.user}`
|
||||
|
||||
try {
|
||||
fs.writeFileSync("./.env", env, { encoding: "utf-8" });
|
||||
consola.success("Exported ci env to .env");
|
||||
} catch (error) {
|
||||
consola.error("Couldn't export the ci env");
|
||||
}
|
||||
|
||||
});
|
||||
MAILER_URL=https://dev.lauf-fuer-kaya.de/mailer
|
||||
MAILER_KEY=asdasd`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync("./.env", env, { encoding: "utf-8" });
|
||||
consola.success("Exported ci env to .env");
|
||||
} catch (error) {
|
||||
consola.error("Couldn't export the ci env");
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export function generateSpec(storage: MetadataArgsStorage, schemas) {
|
||||
}
|
||||
},
|
||||
info: {
|
||||
description: "The the backend API for the LfK! runner system.",
|
||||
description: `The the backend API for the LfK! runner system. <br>[Imprint](${config.imprint_url}) & [Privacy](${config.privacy_url})`,
|
||||
title: "LfK! Backend API",
|
||||
version: config.version
|
||||
},
|
||||
|
||||
49
src/app.ts
49
src/app.ts
@@ -7,7 +7,28 @@ import authchecker from "./middlewares/authchecker";
|
||||
import { ErrorHandler } from './middlewares/ErrorHandler';
|
||||
import UserChecker from './middlewares/UserChecker';
|
||||
|
||||
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
|
||||
// Import all controllers directly to avoid Bun + routing-controllers glob/require issues
|
||||
import { AuthController } from './controllers/AuthController';
|
||||
import { DonationController } from './controllers/DonationController';
|
||||
import { DonorController } from './controllers/DonorController';
|
||||
import { GroupContactController } from './controllers/GroupContactController';
|
||||
import { ImportController } from './controllers/ImportController';
|
||||
import { MeController } from './controllers/MeController';
|
||||
import { PermissionController } from './controllers/PermissionController';
|
||||
import { RunnerCardController } from './controllers/RunnerCardController';
|
||||
import { RunnerController } from './controllers/RunnerController';
|
||||
import { RunnerOrganizationController } from './controllers/RunnerOrganizationController';
|
||||
import { RunnerSelfServiceController } from './controllers/RunnerSelfServiceController';
|
||||
import { RunnerTeamController } from './controllers/RunnerTeamController';
|
||||
import { ScanController } from './controllers/ScanController';
|
||||
import { ScanStationController } from './controllers/ScanStationController';
|
||||
import { StatsClientController } from './controllers/StatsClientController';
|
||||
import { StatsController } from './controllers/StatsController';
|
||||
import { StatusController } from './controllers/StatusController';
|
||||
import { TrackController } from './controllers/TrackController';
|
||||
import { UserController } from './controllers/UserController';
|
||||
import { UserGroupController } from './controllers/UserGroupController';
|
||||
|
||||
const app = createExpressServer({
|
||||
authorizationChecker: authchecker,
|
||||
currentUserChecker: UserChecker,
|
||||
@@ -15,11 +36,35 @@ const app = createExpressServer({
|
||||
development: config.development,
|
||||
cors: true,
|
||||
routePrefix: "/api",
|
||||
controllers: [`${__dirname}/controllers/*.${CONTROLLERS_FILE_EXTENSION}`],
|
||||
controllers: [
|
||||
AuthController,
|
||||
DonationController,
|
||||
DonorController,
|
||||
GroupContactController,
|
||||
ImportController,
|
||||
MeController,
|
||||
PermissionController,
|
||||
RunnerCardController,
|
||||
RunnerController,
|
||||
RunnerOrganizationController,
|
||||
RunnerSelfServiceController,
|
||||
RunnerTeamController,
|
||||
ScanController,
|
||||
ScanStationController,
|
||||
StatsClientController,
|
||||
StatsController,
|
||||
StatusController,
|
||||
TrackController,
|
||||
UserController,
|
||||
UserGroupController,
|
||||
],
|
||||
});
|
||||
|
||||
async function main() {
|
||||
await loaders(app);
|
||||
if (config.testing) {
|
||||
consola.info("🛠[config]: Discovered testing env. Mailing errors will get ignored!")
|
||||
}
|
||||
app.listen(config.internal_port, () => {
|
||||
consola.success(
|
||||
`⚡️[server]: Server is running at http://localhost:${config.internal_port}`
|
||||
|
||||
104
src/config.ts
104
src/config.ts
@@ -1,47 +1,59 @@
|
||||
import { config as configDotenv } from 'dotenv';
|
||||
import { CountryCode } from 'libphonenumber-js';
|
||||
import ValidatorJS from 'validator';
|
||||
|
||||
configDotenv();
|
||||
export const config = {
|
||||
internal_port: parseInt(process.env.APP_PORT) || 4010,
|
||||
development: process.env.NODE_ENV === "production",
|
||||
jwt_secret: process.env.JWT_SECRET || "secretjwtsecret",
|
||||
phone_validation_countrycode: getPhoneCodeLocale(),
|
||||
postalcode_validation_countrycode: getPostalCodeLocale(),
|
||||
version: process.env.VERSION || require('../package.json').version,
|
||||
seedTestData: getDataSeeding(),
|
||||
app_url: process.env.APP_URL || "http://localhost:4010",
|
||||
mail_server: process.env.MAIL_SERVER,
|
||||
mail_port: Number(process.env.MAIL_PORT) || 25,
|
||||
mail_user: process.env.MAIL_USER,
|
||||
mail_password: process.env.MAIL_PASSWORD,
|
||||
mail_from: process.env.MAIL_FROM
|
||||
}
|
||||
let errors = 0
|
||||
if (typeof config.internal_port !== "number") {
|
||||
errors++
|
||||
}
|
||||
if (typeof config.development !== "boolean") {
|
||||
errors++
|
||||
}
|
||||
function getPhoneCodeLocale(): CountryCode {
|
||||
return (process.env.PHONE_COUNTRYCODE as CountryCode);
|
||||
}
|
||||
function getPostalCodeLocale(): any {
|
||||
try {
|
||||
const stringArray: String[] = ValidatorJS.isPostalCodeLocales;
|
||||
let index = stringArray.indexOf(process.env.POSTALCODE_COUNTRYCODE);
|
||||
return ValidatorJS.isPostalCodeLocales[index];
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function getDataSeeding(): Boolean {
|
||||
try {
|
||||
return JSON.parse(process.env.SEED_TEST_DATA);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
import consola from 'consola';
|
||||
import { CountryCode } from 'libphonenumber-js';
|
||||
import ValidatorJS from 'validator';
|
||||
|
||||
export const config = {
|
||||
internal_port: parseInt(process.env.APP_PORT) || 4010,
|
||||
development: process.env.NODE_ENV === "production",
|
||||
testing: process.env.NODE_ENV === "test",
|
||||
jwt_secret: process.env.JWT_SECRET || "secretjwtsecret",
|
||||
station_token_secret: process.env.STATION_TOKEN_SECRET || "",
|
||||
nats_url: process.env.NATS_URL || "nats://localhost:4222",
|
||||
nats_prewarm: process.env.NATS_PREWARM === "true",
|
||||
phone_validation_countrycode: getPhoneCodeLocale(),
|
||||
postalcode_validation_countrycode: getPostalCodeLocale(),
|
||||
version: process.env.VERSION || require('../package.json').version,
|
||||
seedTestData: getDataSeeding(),
|
||||
app_url: process.env.APP_URL || "http://localhost:8080",
|
||||
privacy_url: process.env.PRIVACY_URL || "/privacy",
|
||||
imprint_url: process.env.IMPRINT_URL || "/imprint",
|
||||
mailer_url: process.env.MAILER_URL || "",
|
||||
mailer_key: process.env.MAILER_KEY || ""
|
||||
}
|
||||
let errors = 0
|
||||
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++;
|
||||
}
|
||||
if (config.station_token_secret.length < 32) {
|
||||
consola.error("Error: STATION_TOKEN_SECRET must be set and at least 32 characters long")
|
||||
errors++;
|
||||
}
|
||||
function getPhoneCodeLocale(): CountryCode {
|
||||
return (process.env.PHONE_COUNTRYCODE as CountryCode);
|
||||
}
|
||||
function getPostalCodeLocale(): any {
|
||||
try {
|
||||
const stringArray: String[] = ValidatorJS.isPostalCodeLocales;
|
||||
let index = stringArray.indexOf(process.env.POSTALCODE_COUNTRYCODE);
|
||||
return ValidatorJS.isPostalCodeLocales[index];
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function getDataSeeding(): Boolean {
|
||||
try {
|
||||
return JSON.parse(process.env.SEED_TEST_DATA);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export let e = errors
|
||||
@@ -1,110 +1,106 @@
|
||||
import { Body, CookieParam, JsonController, Param, Post, Req, Res } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError';
|
||||
import { UserNotFoundError } from '../errors/UserErrors';
|
||||
import { Mailer } from '../mailer';
|
||||
import { CreateAuth } from '../models/actions/create/CreateAuth';
|
||||
import { CreateResetToken } from '../models/actions/create/CreateResetToken';
|
||||
import { HandleLogout } from '../models/actions/HandleLogout';
|
||||
import { RefreshAuth } from '../models/actions/RefreshAuth';
|
||||
import { ResetPassword } from '../models/actions/ResetPassword';
|
||||
import { ResponseAuth } from '../models/responses/ResponseAuth';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { Logout } from '../models/responses/ResponseLogout';
|
||||
|
||||
@JsonController('/auth')
|
||||
export class AuthController {
|
||||
|
||||
private mailer: Mailer;
|
||||
|
||||
constructor() {
|
||||
this.mailer = new Mailer();
|
||||
}
|
||||
|
||||
@Post("/login")
|
||||
@ResponseSchema(ResponseAuth)
|
||||
@ResponseSchema(InvalidCredentialsError)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(UsernameOrEmailNeededError)
|
||||
@ResponseSchema(PasswordNeededError)
|
||||
@ResponseSchema(InvalidCredentialsError)
|
||||
@OpenAPI({ description: 'Login with your username/email and password. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)' })
|
||||
async login(@Body({ validate: true }) createAuth: CreateAuth, @Res() response: any) {
|
||||
let auth;
|
||||
try {
|
||||
auth = await createAuth.toAuth();
|
||||
response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
|
||||
response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
|
||||
return response.send(auth)
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@Post("/logout")
|
||||
@ResponseSchema(Logout)
|
||||
@ResponseSchema(InvalidCredentialsError)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(UsernameOrEmailNeededError)
|
||||
@ResponseSchema(PasswordNeededError)
|
||||
@ResponseSchema(InvalidCredentialsError)
|
||||
@OpenAPI({ description: 'Logout using your refresh token. <br> This instantly invalidates all your access and refresh tokens.', security: [{ "RefreshTokenCookie": [] }] })
|
||||
async logout(@Body({ validate: true }) handleLogout: HandleLogout, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any) {
|
||||
if (refresh_token && refresh_token.length != 0 && handleLogout.token == undefined) {
|
||||
handleLogout.token = refresh_token;
|
||||
}
|
||||
|
||||
let logout;
|
||||
try {
|
||||
logout = await handleLogout.logout()
|
||||
await response.cookie('lfk_backend__refresh_token', "expired", { expires: new Date(Date.now()), httpOnly: true });
|
||||
response.cookie('lfk_backend__refresh_token_expires_at', "expired", { expires: new Date(Date.now()), httpOnly: true });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return response.send(logout)
|
||||
}
|
||||
|
||||
@Post("/refresh")
|
||||
@ResponseSchema(ResponseAuth)
|
||||
@ResponseSchema(JwtNotProvidedError)
|
||||
@ResponseSchema(IllegalJWTError)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(RefreshTokenCountInvalidError)
|
||||
@OpenAPI({ description: 'Refresh your access and refresh tokens using a valid refresh token. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)', security: [{ "RefreshTokenCookie": [] }] })
|
||||
async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any, @Req() req: any) {
|
||||
if (refresh_token && refresh_token.length != 0 && refreshAuth.token == undefined) {
|
||||
refreshAuth.token = refresh_token;
|
||||
}
|
||||
let auth;
|
||||
try {
|
||||
auth = await refreshAuth.toAuth();
|
||||
response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
|
||||
response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return response.send(auth)
|
||||
}
|
||||
|
||||
@Post("/reset")
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 200 })
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UsernameOrEmailNeededError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Request a password reset token. <br> This will provide you with a reset token that you can use by posting to /api/auth/reset/{token}." })
|
||||
async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken) {
|
||||
const reset_token: String = await passwordReset.toResetToken();
|
||||
await this.mailer.sendResetMail(passwordReset.email, reset_token);
|
||||
return new ResponseEmpty();
|
||||
}
|
||||
|
||||
@Post("/reset/:token")
|
||||
@ResponseSchema(ResponseAuth)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(UsernameOrEmailNeededError)
|
||||
@OpenAPI({ description: "Reset a user's utilising a valid password reset token. <br> This will set the user's password to the one you provided in the body. <br> To get a reset token post to /api/auth/reset with your username." })
|
||||
async resetPassword(@Param("token") token: string, @Body({ validate: true }) passwordReset: ResetPassword) {
|
||||
passwordReset.resetToken = token;
|
||||
return await passwordReset.resetPassword();
|
||||
}
|
||||
}
|
||||
import { Body, CookieParam, JsonController, Param, Post, QueryParam, Req, Res } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError';
|
||||
import { MailSendingError } from '../errors/MailErrors';
|
||||
import { UserNotFoundError } from '../errors/UserErrors';
|
||||
import { Mailer } from '../mailer';
|
||||
import { CreateAuth } from '../models/actions/create/CreateAuth';
|
||||
import { CreateResetToken } from '../models/actions/create/CreateResetToken';
|
||||
import { HandleLogout } from '../models/actions/HandleLogout';
|
||||
import { RefreshAuth } from '../models/actions/RefreshAuth';
|
||||
import { ResetPassword } from '../models/actions/ResetPassword';
|
||||
import { ResponseAuth } from '../models/responses/ResponseAuth';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { Logout } from '../models/responses/ResponseLogout';
|
||||
|
||||
@JsonController('/auth')
|
||||
export class AuthController {
|
||||
|
||||
@Post("/login")
|
||||
@ResponseSchema(ResponseAuth)
|
||||
@ResponseSchema(InvalidCredentialsError)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(UsernameOrEmailNeededError)
|
||||
@ResponseSchema(PasswordNeededError)
|
||||
@ResponseSchema(InvalidCredentialsError)
|
||||
@OpenAPI({ description: 'Login with your username/email and password. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)' })
|
||||
async login(@Body({ validate: true }) createAuth: CreateAuth, @Res() response: any) {
|
||||
let auth;
|
||||
try {
|
||||
auth = await createAuth.toAuth();
|
||||
response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
|
||||
response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
|
||||
return response.send(auth)
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@Post("/logout")
|
||||
@ResponseSchema(Logout)
|
||||
@ResponseSchema(InvalidCredentialsError)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(UsernameOrEmailNeededError)
|
||||
@ResponseSchema(PasswordNeededError)
|
||||
@ResponseSchema(InvalidCredentialsError)
|
||||
@OpenAPI({ description: 'Logout using your refresh token. <br> This instantly invalidates all your access and refresh tokens.', security: [{ "RefreshTokenCookie": [] }] })
|
||||
async logout(@Body({ validate: true }) handleLogout: HandleLogout, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any) {
|
||||
if (refresh_token && refresh_token.length != 0 && handleLogout.token == undefined) {
|
||||
handleLogout.token = refresh_token;
|
||||
}
|
||||
|
||||
let logout;
|
||||
try {
|
||||
logout = await handleLogout.logout()
|
||||
await response.cookie('lfk_backend__refresh_token', "expired", { expires: new Date(Date.now()), httpOnly: true });
|
||||
response.cookie('lfk_backend__refresh_token_expires_at', "expired", { expires: new Date(Date.now()), httpOnly: true });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return response.send(logout)
|
||||
}
|
||||
|
||||
@Post("/refresh")
|
||||
@ResponseSchema(ResponseAuth)
|
||||
@ResponseSchema(JwtNotProvidedError)
|
||||
@ResponseSchema(IllegalJWTError)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(RefreshTokenCountInvalidError)
|
||||
@OpenAPI({ description: 'Refresh your access and refresh tokens using a valid refresh token. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)', security: [{ "RefreshTokenCookie": [] }] })
|
||||
async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any, @Req() req: any) {
|
||||
if (refresh_token && refresh_token.length != 0 && refreshAuth.token == undefined) {
|
||||
refreshAuth.token = refresh_token;
|
||||
}
|
||||
let auth;
|
||||
try {
|
||||
auth = await refreshAuth.toAuth();
|
||||
response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
|
||||
response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return response.send(auth)
|
||||
}
|
||||
|
||||
@Post("/reset")
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 200 })
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UsernameOrEmailNeededError, { statusCode: 406 })
|
||||
@ResponseSchema(MailSendingError, { statusCode: 500 })
|
||||
@OpenAPI({ description: "Request a password reset token. <br> This will provide you with a reset token that you can use by posting to /api/auth/reset/{token}." })
|
||||
async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken, @QueryParam("locale") locale: string = "en") {
|
||||
const reset_token: string = await passwordReset.toResetToken();
|
||||
await Mailer.sendResetMail(passwordReset.email, reset_token, locale);
|
||||
return new ResponseEmpty();
|
||||
}
|
||||
|
||||
@Post("/reset/:token")
|
||||
@ResponseSchema(ResponseAuth)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(UsernameOrEmailNeededError)
|
||||
@OpenAPI({ description: "Reset a user's utilising a valid password reset token. <br> This will set the user's password to the one you provided in the body. <br> To get a reset token post to /api/auth/reset with your username." })
|
||||
async resetPassword(@Param("token") token: string, @Body({ validate: true }) passwordReset: ResetPassword) {
|
||||
passwordReset.resetToken = token;
|
||||
return await passwordReset.resetPassword();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -78,7 +100,7 @@ export class DonationController {
|
||||
async postDistance(@Body({ validate: true }) createDonation: CreateDistanceDonation) {
|
||||
let donation = await createDonation.toEntity();
|
||||
donation = await this.distanceDonationRepository.save(donation);
|
||||
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
|
||||
return (await this.distanceDonationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
|
||||
}
|
||||
|
||||
@Put('/fixed/:id')
|
||||
@@ -124,7 +146,7 @@ export class DonationController {
|
||||
}
|
||||
|
||||
await this.distanceDonationRepository.save(await donation.update(oldDonation));
|
||||
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
|
||||
return (await this.distanceDonationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Authorized, JsonController, Post } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { config } from '../config';
|
||||
import { Mailer } from '../mailer';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
|
||||
|
||||
@JsonController('/mails')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class MailController {
|
||||
|
||||
private mailer: Mailer;
|
||||
|
||||
constructor() {
|
||||
this.mailer = new Mailer();
|
||||
}
|
||||
|
||||
@Post('/test')
|
||||
@Authorized(["MAIL:CREATE"])
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 200 })
|
||||
@OpenAPI({ description: 'Sends a test email to the configured from-address.' })
|
||||
async get() {
|
||||
await this.mailer.sendTestMail(config.mail_from);
|
||||
return new ResponseEmpty();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Body, CurrentUser, Delete, Get, JsonController, OnUndefined, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors';
|
||||
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors';
|
||||
import { UpdateUser } from '../models/actions/update/UpdateUser';
|
||||
import { User } from '../models/entities/User';
|
||||
import { ResponseUser } from '../models/responses/ResponseUser';
|
||||
@@ -32,7 +32,7 @@ export class MeController {
|
||||
return new ResponseUser(user);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@Get('/permissions')
|
||||
@ResponseSchema(ResponseUserPermissions)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(UserNotFoundError)
|
||||
@@ -48,6 +48,10 @@ export class MeController {
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
|
||||
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the yourself. <br> You can't edit your own permissions or group memberships here - Please use the /api/users/:id enpoint instead. <br> Please remember that ids can't be changed." })
|
||||
async put(@CurrentUser() currentUser: User, @Body({ validate: true }) updateUser: UpdateUser) {
|
||||
let oldUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['groups'] });
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -1,106 +1,164 @@
|
||||
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 { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { Repository, getConnectionManager } from 'typeorm';
|
||||
import { 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 { RunnerCard } from '../models/entities/RunnerCard';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseRunnerCard } from '../models/responses/ResponseRunnerCard';
|
||||
import { ScanController } from './ScanController';
|
||||
|
||||
@JsonController('/cards')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class RunnerCardController {
|
||||
private cardRepository: Repository<RunnerCard>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.cardRepository = getConnectionManager().get().getRepository(RunnerCard);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("CARD:GET")
|
||||
@ResponseSchema(ResponseRunnerCard, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all card.' })
|
||||
async getAll() {
|
||||
let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
|
||||
const cards = await this.cardRepository.find({ relations: ['runner'] });
|
||||
cards.forEach(card => {
|
||||
responseCards.push(new ResponseRunnerCard(card));
|
||||
});
|
||||
return responseCards;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("CARD:GET")
|
||||
@ResponseSchema(ResponseRunnerCard)
|
||||
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerCardNotFoundError)
|
||||
@OpenAPI({ description: "Lists all information about the card whose id got provided." })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let card = await this.cardRepository.findOne({ id: id }, { relations: ['runner'] });
|
||||
if (!card) { throw new RunnerCardNotFoundError(); }
|
||||
return card.toResponse();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("CARD:CREATE")
|
||||
@ResponseSchema(ResponseRunnerCard)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: "Create a new card. <br> You can provide a associated runner by id but you don't have to." })
|
||||
async post(@Body({ validate: true }) createCard: CreateRunnerCard) {
|
||||
let card = await createCard.toEntity();
|
||||
card = await this.cardRepository.save(card);
|
||||
return (await this.cardRepository.findOne({ id: card.id }, { relations: ['runner'] })).toResponse();
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("CARD:UPDATE")
|
||||
@ResponseSchema(ResponseRunnerCard)
|
||||
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerCardIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the card whose id you provided. <br> Scans created via this card will still be associated with the old runner. <br> Please remember that ids can't be changed." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) card: UpdateRunnerCard) {
|
||||
let oldCard = await this.cardRepository.findOne({ id: id });
|
||||
|
||||
if (!oldCard) {
|
||||
throw new RunnerCardNotFoundError();
|
||||
}
|
||||
|
||||
if (oldCard.id != card.id) {
|
||||
throw new RunnerCardIdsNotMatchingError();
|
||||
}
|
||||
|
||||
import { deleteCardEntry } from '../nats/CardKV';
|
||||
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';
|
||||
import { ScanController } from './ScanController';
|
||||
|
||||
@JsonController('/cards')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class RunnerCardController {
|
||||
private cardRepository: Repository<RunnerCard>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.cardRepository = getConnectionManager().get().getRepository(RunnerCard);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("CARD:GET")
|
||||
@ResponseSchema(ResponseRunnerCard, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all card.' })
|
||||
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
|
||||
let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
|
||||
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));
|
||||
});
|
||||
return responseCards;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("CARD:GET")
|
||||
@ResponseSchema(ResponseRunnerCard)
|
||||
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerCardNotFoundError)
|
||||
@OpenAPI({ description: "Lists all information about the card whose id got provided." })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let card = await this.cardRepository.findOne({ id: id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] });
|
||||
if (!card) { throw new RunnerCardNotFoundError(); }
|
||||
return card.toResponse();
|
||||
}
|
||||
|
||||
@Post('/bulk')
|
||||
@Authorized("CARD:CREATE")
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 200 })
|
||||
@OpenAPI({ description: "Create blank cards in bulk. <br> Just provide the count as a query param and wait for the 200 response. <br> You can provide the 'returnCards' query param if you want to receive the RESPONSERUNNERCARD objects in the response." })
|
||||
async postBlancoBulk(@QueryParam("count") count: number, @QueryParam("returnCards") returnCards: boolean = false) {
|
||||
let createPromises = new Array<any>();
|
||||
for (let index = 0; index < count; index++) {
|
||||
createPromises.push(this.cardRepository.save({ runner: null, enabled: true }))
|
||||
}
|
||||
|
||||
const cards = await Promise.all(createPromises);
|
||||
|
||||
if (returnCards) {
|
||||
let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
|
||||
for await (let card of cards) {
|
||||
let dbCard = await this.cardRepository.findOne({ id: card.id });
|
||||
responseCards.push(new ResponseRunnerCard(dbCard));
|
||||
}
|
||||
return responseCards;
|
||||
}
|
||||
let response = new ResponseEmpty();
|
||||
response.response = `Created ${count} new blanco cards.`
|
||||
return response;
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("CARD:CREATE")
|
||||
@ResponseSchema(ResponseRunnerCard)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: "Create a new card. <br> You can provide a associated runner by id but you don't have to." })
|
||||
async post(@Body({ validate: true }) createCard: CreateRunnerCard) {
|
||||
let card = await createCard.toEntity();
|
||||
card = await this.cardRepository.save(card);
|
||||
return (await this.cardRepository.findOne({ id: card.id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] })).toResponse();
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("CARD:UPDATE")
|
||||
@ResponseSchema(ResponseRunnerCard)
|
||||
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerCardIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the card whose id you provided. <br> Scans created via this card will still be associated with the old runner. <br> Please remember that ids can't be changed." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) card: UpdateRunnerCard) {
|
||||
let oldCard = await this.cardRepository.findOne({ id: id });
|
||||
|
||||
if (!oldCard) {
|
||||
throw new RunnerCardNotFoundError();
|
||||
}
|
||||
|
||||
if (oldCard.id != card.id) {
|
||||
throw new RunnerCardIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.cardRepository.save(await card.update(oldCard));
|
||||
return (await this.cardRepository.findOne({ id: id }, { relations: ['runner'] })).toResponse();
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("CARD:DELETE")
|
||||
@ResponseSchema(ResponseRunnerCard)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@ResponseSchema(RunnerCardHasScansError, { statusCode: 406 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: "Delete the card whose id you provided. <br> If no card with this id exists it will just return 204(no content). <br> If the card still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with by this card - please disable it instead or just remove the runner association)." })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let card = await this.cardRepository.findOne({ id: id });
|
||||
if (!card) { return null; }
|
||||
|
||||
const cardScans = (await this.cardRepository.findOne({ id: id }, { relations: ["scans"] })).scans;
|
||||
if (cardScans.length != 0 && !force) {
|
||||
throw new RunnerCardHasScansError();
|
||||
}
|
||||
const scanController = new ScanController;
|
||||
await deleteCardEntry(id);
|
||||
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)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@ResponseSchema(RunnerCardHasScansError, { statusCode: 406 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: "Delete the card whose id you provided. <br> If no card with this id exists it will just return 204(no content). <br> If the card still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with by this card - please disable it instead or just remove the runner association)." })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let card = await this.cardRepository.findOne({ id: id });
|
||||
if (!card) { return null; }
|
||||
|
||||
const cardScans = (await this.cardRepository.findOne({ id: id }, { relations: ["scans"] })).scans;
|
||||
if (cardScans.length != 0 && !force) {
|
||||
throw new RunnerCardHasScansError();
|
||||
}
|
||||
const scanController = new ScanController;
|
||||
for (let scan of cardScans) {
|
||||
await scanController.remove(scan.id, force);
|
||||
}
|
||||
|
||||
await deleteCardEntry(id);
|
||||
await this.cardRepository.delete(card);
|
||||
return card.toResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
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 { RunnerGroupNeededError, RunnerHasDistanceDonationsError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
|
||||
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
|
||||
import { CreateRunner } from '../models/actions/create/CreateRunner';
|
||||
import { UpdateRunner } from '../models/actions/update/UpdateRunner';
|
||||
import { Runner } from '../models/entities/Runner';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseRunner } from '../models/responses/ResponseRunner';
|
||||
import { ResponseScan } from '../models/responses/ResponseScan';
|
||||
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
|
||||
import { DonationController } from './DonationController';
|
||||
import { RunnerCardController } from './RunnerCardController';
|
||||
import { ScanController } from './ScanController';
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { Repository, getConnectionManager } from 'typeorm';
|
||||
import { RunnerGroupNeededError, RunnerHasDistanceDonationsError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
|
||||
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
|
||||
import { deleteRunnerEntry } from '../nats/RunnerKV';
|
||||
import { CreateRunner } from '../models/actions/create/CreateRunner';
|
||||
import { UpdateRunner } from '../models/actions/update/UpdateRunner';
|
||||
import { Runner } from '../models/entities/Runner';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseRunner } from '../models/responses/ResponseRunner';
|
||||
import { ResponseScan } from '../models/responses/ResponseScan';
|
||||
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
|
||||
import { DonationController } from './DonationController';
|
||||
import { RunnerCardController } from './RunnerCardController';
|
||||
import { ScanController } from './ScanController';
|
||||
|
||||
@JsonController('/runners')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
@@ -30,11 +31,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', '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 +61,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', '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 +106,7 @@ export class RunnerController {
|
||||
}
|
||||
|
||||
runner = await this.runnerRepository.save(runner)
|
||||
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'scans.track', 'cards'] }));
|
||||
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }), true);
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@@ -111,8 +126,9 @@ export class RunnerController {
|
||||
throw new RunnerIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.runnerRepository.save(await runner.update(oldRunner));
|
||||
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'scans.track', 'cards'] }));
|
||||
await this.runnerRepository.save(await runner.update(oldRunner));
|
||||
await deleteRunnerEntry(id);
|
||||
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }), true);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@@ -125,7 +141,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', 'scans.track', 'cards'] });
|
||||
const responseRunner = await this.runnerRepository.findOne(runner);
|
||||
|
||||
if (!runner) {
|
||||
throw new RunnerNotFoundError();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
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.scans', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.group', 'teams.runners.scans', 'teams.runners.scans.track'] })).allRunners; }
|
||||
else { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.scans', 'runners.scans.track'] })).runners; }
|
||||
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;
|
||||
}
|
||||
@@ -114,6 +121,10 @@ export class RunnerOrganizationController {
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the organsisation whose id you provided. <br> If the organization still has runners and/or teams associated this will fail. <br> To delete the organization with all associated runners and teams set the force QueryParam to true (cascading deletion might take a while). <br> This won\'t delete the associated contact. <br> If no organization with this id exists it will just return 204(no content).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
if (id == 1) {
|
||||
throw new BadRequestError("You can't delete the citizen runner org.");
|
||||
}
|
||||
|
||||
let organization = await this.runnerOrganizationRepository.findOne({ id: id });
|
||||
if (!organization) { return null; }
|
||||
let runnerOrganization = await this.runnerOrganizationRepository.findOne(organization, { relations: ['contact', 'runners', 'teams'] });
|
||||
|
||||
@@ -1,98 +1,248 @@
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import { Body, Get, JsonController, OnUndefined, Param, Post } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { config } from '../config';
|
||||
import { InvalidCredentialsError, JwtNotProvidedError } from '../errors/AuthError';
|
||||
import { RunnerEmailNeededError, RunnerNotFoundError } from '../errors/RunnerErrors';
|
||||
import { RunnerOrganizationNotFoundError } from '../errors/RunnerOrganizationErrors';
|
||||
import { JwtCreator } from '../jwtcreator';
|
||||
import { CreateSelfServiceCitizenRunner } from '../models/actions/create/CreateSelfServiceCitizenRunner';
|
||||
import { CreateSelfServiceRunner } from '../models/actions/create/CreateSelfServiceRunner';
|
||||
import { Runner } from '../models/entities/Runner';
|
||||
import { RunnerGroup } from '../models/entities/RunnerGroup';
|
||||
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
|
||||
import { ResponseSelfServiceRunner } from '../models/responses/ResponseSelfServiceRunner';
|
||||
|
||||
|
||||
@JsonController('/runners')
|
||||
export class RunnerSelfServiceController {
|
||||
private runnerRepository: Repository<Runner>;
|
||||
private orgRepository: Repository<RunnerOrganization>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.runnerRepository = getConnectionManager().get().getRepository(Runner);
|
||||
this.orgRepository = getConnectionManager().get().getRepository(RunnerOrganization);
|
||||
}
|
||||
|
||||
@Get('/me/:jwt')
|
||||
@ResponseSchema(ResponseSelfServiceRunner)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about yourself. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please contact support.' })
|
||||
async get(@Param('jwt') token: string) {
|
||||
return (new ResponseSelfServiceRunner(await this.getRunner(token)));
|
||||
}
|
||||
|
||||
@Post('/register')
|
||||
@ResponseSchema(ResponseSelfServiceRunner)
|
||||
@ResponseSchema(RunnerEmailNeededError, { statusCode: 406 })
|
||||
@OpenAPI({ description: 'Create a new selfservice runner in the citizen org. <br> This endpoint shoud be used to allow "everyday citizen" to register themselves. <br> You have to provide a mail address, b/c the future we\'ll implement email verification.' })
|
||||
async registerRunner(@Body({ validate: true }) createRunner: CreateSelfServiceCitizenRunner) {
|
||||
let runner = await createRunner.toEntity();
|
||||
|
||||
runner = await this.runnerRepository.save(runner);
|
||||
let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
|
||||
response.token = JwtCreator.createSelfService(runner);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Post('/register/:token')
|
||||
@ResponseSchema(ResponseSelfServiceRunner)
|
||||
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Create a new selfservice runner in a provided org. <br> The orgs get provided and authorized via api tokens that can be optained via the /organizations endpoint.' })
|
||||
async registerOrganizationRunner(@Param('token') token: string, @Body({ validate: true }) createRunner: CreateSelfServiceRunner) {
|
||||
const org = await this.getOrgansisation(token);
|
||||
|
||||
let runner = await createRunner.toEntity(org);
|
||||
runner = await this.runnerRepository.save(runner);
|
||||
|
||||
let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
|
||||
response.token = JwtCreator.createSelfService(runner);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's a runner by a provided jwt token.
|
||||
* @param token The runner jwt provided by the runner to identitfy themselves.
|
||||
*/
|
||||
private async getRunner(token: string): Promise<Runner> {
|
||||
if (token == "") { throw new JwtNotProvidedError(); }
|
||||
let jwtPayload = undefined
|
||||
try {
|
||||
jwtPayload = <any>jwt.verify(token, config.jwt_secret);
|
||||
} catch (error) {
|
||||
throw new InvalidCredentialsError();
|
||||
import type { Request } from "express";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import { BadRequestError, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { config } from '../config';
|
||||
import { InvalidCredentialsError, JwtNotProvidedError } from '../errors/AuthError';
|
||||
import { MailSendingError } from '../errors/MailErrors';
|
||||
import { RunnerEmailNeededError, RunnerHasDistanceDonationsError, RunnerNotFoundError, RunnerSelfserviceTimeoutError } from '../errors/RunnerErrors';
|
||||
import { RunnerOrganizationNotFoundError } from '../errors/RunnerOrganizationErrors';
|
||||
import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
|
||||
import { JwtCreator } from '../jwtcreator';
|
||||
import { Mailer } from '../mailer';
|
||||
import ScanAuth from '../middlewares/ScanAuth';
|
||||
import { CreateSelfServiceCitizenRunner } from '../models/actions/create/CreateSelfServiceCitizenRunner';
|
||||
import { CreateSelfServiceRunner } from '../models/actions/create/CreateSelfServiceRunner';
|
||||
import { Runner } from '../models/entities/Runner';
|
||||
import { RunnerGroup } from '../models/entities/RunnerGroup';
|
||||
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
|
||||
import { ScanStation } from '../models/entities/ScanStation';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseScanStation } from '../models/responses/ResponseScanStation';
|
||||
import { ResponseSelfServiceOrganisation } from '../models/responses/ResponseSelfServiceOrganisation';
|
||||
import { ResponseSelfServiceRunner } from '../models/responses/ResponseSelfServiceRunner';
|
||||
import { ResponseSelfServiceScan } from '../models/responses/ResponseSelfServiceScan';
|
||||
import { DonationController } from './DonationController';
|
||||
import { RunnerCardController } from './RunnerCardController';
|
||||
import { ScanController } from './ScanController';
|
||||
|
||||
@JsonController()
|
||||
export class RunnerSelfServiceController {
|
||||
private runnerRepository: Repository<Runner>;
|
||||
private orgRepository: Repository<RunnerOrganization>;
|
||||
private stationRepository: Repository<ScanStation>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.runnerRepository = getConnectionManager().get().getRepository(Runner);
|
||||
this.orgRepository = getConnectionManager().get().getRepository(RunnerOrganization);
|
||||
this.stationRepository = getConnectionManager().get().getRepository(ScanStation);
|
||||
}
|
||||
|
||||
@Get('/runners/me/:jwt')
|
||||
@ResponseSchema(ResponseSelfServiceRunner)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about yourself. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please use the forgot endpoint.' })
|
||||
async get(@Param('jwt') token: string) {
|
||||
return (new ResponseSelfServiceRunner(await this.getRunner(token)));
|
||||
}
|
||||
|
||||
@Delete('/runners/me/:jwt')
|
||||
@ResponseSchema(ResponseSelfServiceRunner)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerNotFoundError)
|
||||
@OpenAPI({ description: 'Deletes all information about yourself. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please use the forgot endpoint.' })
|
||||
async remove(@Param('jwt') token: string, @QueryParam("force") force: boolean) {
|
||||
const responseRunner = await this.getRunner(token);
|
||||
let runner = await this.runnerRepository.findOne({ id: responseRunner.id });
|
||||
|
||||
if (!runner) { return null; }
|
||||
if (!runner) {
|
||||
throw new RunnerNotFoundError();
|
||||
}
|
||||
|
||||
const runnerDonations = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["distanceDonations"] })).distanceDonations;
|
||||
if (runnerDonations.length > 0 && !force) {
|
||||
throw new RunnerHasDistanceDonationsError();
|
||||
}
|
||||
const donationController = new DonationController();
|
||||
for (let donation of runnerDonations) {
|
||||
await donationController.remove(donation.id, force);
|
||||
}
|
||||
|
||||
const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards;
|
||||
const cardController = new RunnerCardController;
|
||||
for (let card of runnerCards) {
|
||||
await cardController.remove(card.id, force);
|
||||
}
|
||||
|
||||
const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans;
|
||||
const scanController = new ScanController;
|
||||
for (let scan of runnerScans) {
|
||||
await scanController.remove(scan.id, force);
|
||||
}
|
||||
|
||||
await this.runnerRepository.delete(runner);
|
||||
return new ResponseSelfServiceRunner(responseRunner);
|
||||
}
|
||||
|
||||
@Get('/runners/me/:jwt/scans')
|
||||
@ResponseSchema(ResponseSelfServiceScan, { isArray: true })
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all your (runner) scans. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please contact support.' })
|
||||
async getScans(@Param('jwt') token: string) {
|
||||
const scans = (await this.getRunner(token)).scans;
|
||||
let responseScans = new Array<ResponseSelfServiceScan>()
|
||||
for (let scan of scans) {
|
||||
responseScans.push(new ResponseSelfServiceScan(scan));
|
||||
}
|
||||
return responseScans;
|
||||
}
|
||||
|
||||
@Get('/stations/me')
|
||||
@UseBefore(ScanAuth)
|
||||
@ResponseSchema(ResponseScanStation)
|
||||
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(ScanStationNotFoundError)
|
||||
@OpenAPI({ description: 'Lists basic information about the station whose token got provided. <br> This includes it\'s associated track.', security: [{ "StationApiToken": [] }] })
|
||||
async getStationMe(@Req() req: Request) {
|
||||
// ScanAuth middleware sets req.stationId (not a header)
|
||||
if (!req.stationId) {
|
||||
throw new ScanStationNotFoundError();
|
||||
}
|
||||
|
||||
const runner = await this.runnerRepository.findOne({ id: jwtPayload["id"] }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] });
|
||||
if (!runner) { throw new RunnerNotFoundError() }
|
||||
return runner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's a runner org by a provided registration api key.
|
||||
* @param token The organization's registration api token.
|
||||
*/
|
||||
private async getOrgansisation(token: string): Promise<RunnerGroup> {
|
||||
token = Buffer.from(token, 'base64').toString('utf8');
|
||||
|
||||
const organization = await this.orgRepository.findOne({ key: token });
|
||||
if (!organization) { throw new RunnerOrganizationNotFoundError; }
|
||||
|
||||
return organization;
|
||||
let scan = await this.stationRepository.findOne({ id: req.stationId }, { relations: ['track'] })
|
||||
if (!scan) { throw new ScanStationNotFoundError(); }
|
||||
return scan.toResponse();
|
||||
}
|
||||
|
||||
@Post('/runners/login')
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(ResponseEmpty)
|
||||
@OpenAPI({ description: 'Use this endpoint to reuqest a new selfservice magic-login-link to be sent to your mail address (rate limited to one mail every 15mins).' })
|
||||
async requestNewToken(@QueryParam('mail') mail: string, @QueryParam("locale") locale: string = "en") {
|
||||
if (!mail) {
|
||||
throw new RunnerNotFoundError();
|
||||
}
|
||||
const runner = await this.runnerRepository.findOne({ email: mail });
|
||||
if (!runner) { throw new RunnerNotFoundError(); }
|
||||
|
||||
if (runner.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 30)) { throw new RunnerSelfserviceTimeoutError(); }
|
||||
const token = JwtCreator.createSelfService(runner);
|
||||
|
||||
try {
|
||||
await Mailer.sendSelfserviceForgottenMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, token, locale)
|
||||
} catch (error) {
|
||||
throw new MailSendingError();
|
||||
}
|
||||
|
||||
runner.resetRequestedTimestamp = Math.floor(Date.now() / 1000);
|
||||
await this.runnerRepository.save(runner);
|
||||
|
||||
return { token };
|
||||
}
|
||||
|
||||
@Post('/runners/register')
|
||||
@ResponseSchema(ResponseSelfServiceRunner)
|
||||
@ResponseSchema(RunnerEmailNeededError, { statusCode: 406 })
|
||||
@OpenAPI({ description: 'Create a new selfservice runner in the citizen org. <br> This endpoint shoud be used to allow "everyday citizen" to register themselves. <br> You have to provide a mail address, b/c the future we\'ll implement email verification.' })
|
||||
async registerRunner(@Body({ validate: true }) createRunner: CreateSelfServiceCitizenRunner, @QueryParam("locale") locale: string = "en") {
|
||||
let runner = await createRunner.toEntity();
|
||||
if (await this.getRunnerExistsByMail(runner.email)) {
|
||||
throw new BadRequestError("E-Mail already registered")
|
||||
}
|
||||
runner = await this.runnerRepository.save(runner);
|
||||
|
||||
let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
|
||||
response.token = JwtCreator.createSelfService(runner);
|
||||
|
||||
try {
|
||||
await Mailer.sendSelfserviceWelcomeMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, response.token, locale)
|
||||
} catch (error) {
|
||||
throw new MailSendingError();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Post('/runners/register/:token')
|
||||
@ResponseSchema(ResponseSelfServiceRunner)
|
||||
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Create a new selfservice runner in a provided org. <br> The orgs get provided and authorized via api tokens that can be optained via the /organizations endpoint.' })
|
||||
async registerOrganizationRunner(@Param('token') token: string, @Body({ validate: true }) createRunner: CreateSelfServiceRunner, @QueryParam("locale") locale: string = "en") {
|
||||
const org = await this.getOrgansisation(token);
|
||||
|
||||
let runner = await createRunner.toEntity(org);
|
||||
if (await this.getRunnerExistsByMail(runner.email)) {
|
||||
throw new BadRequestError("E-Mail already registered")
|
||||
}
|
||||
runner = await this.runnerRepository.save(runner);
|
||||
|
||||
let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
|
||||
response.token = JwtCreator.createSelfService(runner);
|
||||
|
||||
try {
|
||||
await Mailer.sendSelfserviceWelcomeMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, response.token, locale)
|
||||
} catch (error) {
|
||||
throw new MailSendingError();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Get('/organizations/selfservice/:token')
|
||||
@ResponseSchema(ResponseSelfServiceOrganisation, { isArray: false })
|
||||
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Get the basic info and teams for a org.' })
|
||||
async getSelfserviceOrg(@Param('token') token: string) {
|
||||
const orgid = (await this.getOrgansisation(token)).id;
|
||||
const org = await this.orgRepository.findOne({ id: orgid }, { relations: ['teams'] })
|
||||
|
||||
return new ResponseSelfServiceOrganisation(<RunnerOrganization>org);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's a runner by a provided jwt token.
|
||||
* @param token The runner jwt provided by the runner to identitfy themselves.
|
||||
*/
|
||||
private async getRunner(token: string): Promise<Runner> {
|
||||
if (token == "") { throw new JwtNotProvidedError(); }
|
||||
let jwtPayload = undefined
|
||||
try {
|
||||
jwtPayload = <any>jwt.verify(token, config.jwt_secret);
|
||||
} catch (error) {
|
||||
throw new InvalidCredentialsError();
|
||||
}
|
||||
|
||||
const runner = await this.runnerRepository.findOne({ id: jwtPayload["id"] }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] });
|
||||
if (!runner) { throw new RunnerNotFoundError() }
|
||||
return runner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's a runner org by a provided registration api key.
|
||||
* @param token The organization's registration api token.
|
||||
*/
|
||||
private async getOrgansisation(token: string): Promise<RunnerGroup> {
|
||||
token = Buffer.from(token, 'base64').toString('utf8');
|
||||
|
||||
const organization = await this.orgRepository.findOne({ key: token });
|
||||
if (!organization) { throw new RunnerOrganizationNotFoundError; }
|
||||
|
||||
return organization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a runner already exists
|
||||
* @param email The runner's email address
|
||||
* @returns Boolean (true if exists, false if not)
|
||||
*/
|
||||
private async getRunnerExistsByMail(email: string): Promise<boolean> {
|
||||
const runner = await this.runnerRepository.findOne({ email });
|
||||
return runner != undefined
|
||||
}
|
||||
}
|
||||
@@ -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.scans', 'runners.scans.track'] })).runners;
|
||||
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) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam, UseBefore } from 'routing-controllers';
|
||||
import type { Request } from "express";
|
||||
import { Authorized, Body, Delete, Get, HttpError, JsonController, OnUndefined, Param, Post, Put, QueryParam, Req, UseBefore } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { getConnection, getConnectionManager, Repository } from 'typeorm';
|
||||
import { RunnerNotFoundError } from '../errors/RunnerErrors';
|
||||
import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors';
|
||||
import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
|
||||
@@ -9,12 +10,16 @@ import { CreateScan } from '../models/actions/create/CreateScan';
|
||||
import { CreateTrackScan } from '../models/actions/create/CreateTrackScan';
|
||||
import { UpdateScan } from '../models/actions/update/UpdateScan';
|
||||
import { UpdateTrackScan } from '../models/actions/update/UpdateTrackScan';
|
||||
import { RunnerCard } from '../models/entities/RunnerCard';
|
||||
import { Scan } from '../models/entities/Scan';
|
||||
import { TrackScan } from '../models/entities/TrackScan';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseScan } from '../models/responses/ResponseScan';
|
||||
import { ResponseScanIntake, ResponseScanIntakeRunner } from '../models/responses/ResponseScanIntake';
|
||||
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
|
||||
|
||||
import { getCardEntry, setCardEntry } from '../nats/CardKV';
|
||||
import { deleteRunnerEntry, getRunnerEntry, RunnerKVEntry, setRunnerEntry, warmRunner } from '../nats/RunnerKV';
|
||||
import { getStationEntryById } from '../nats/StationKV';
|
||||
@JsonController('/scans')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class ScanController {
|
||||
@@ -34,9 +39,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.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 +63,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.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();
|
||||
}
|
||||
@@ -60,22 +72,120 @@ export class ScanController {
|
||||
@UseBefore(ScanAuth)
|
||||
@ResponseSchema(ResponseScan)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Create a new scan (not track scan - use /scans/trackscans instead). <br> Please rmemember to provide the scan\'s runner\'s id and distance.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
@OpenAPI({ description: 'Create a new scan (not track scan - use /scans/trackscans instead). <br> Please rmemember to provide the scan\'s runner\'s id and distance.', security: [{ "StationApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async post(@Body({ validate: true }) createScan: CreateScan) {
|
||||
let scan = await createScan.toEntity();
|
||||
scan = await this.scanRepository.save(scan);
|
||||
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
}
|
||||
|
||||
@Post("/trackscans")
|
||||
@UseBefore(ScanAuth)
|
||||
@ResponseSchema(ResponseTrackScan)
|
||||
@ResponseSchema(ResponseScanIntake)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Create a new track scan (for "normal" scans use /scans instead). <br> Please remember that to provide the scan\'s card\'s station\'s id.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan) {
|
||||
@OpenAPI({ description: 'Create a new track scan (for "normal" scans use /scans instead). <br> Please remember that to provide the scan\'s card\'s station\'s id.', security: [{ "StationApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan, @Req() req: Request) {
|
||||
// Station token path — KV-backed intake flow
|
||||
if (req.isStationAuth) {
|
||||
return this.stationIntake(createScan.card, req.stationId);
|
||||
}
|
||||
// JWT path — existing full flow, unchanged
|
||||
createScan.station = createScan.station;
|
||||
let scan = await createScan.toEntity();
|
||||
scan = await this.trackScanRepository.save(scan);
|
||||
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* KV-backed hot path for scan station submissions.
|
||||
* Zero DB reads on a fully warm cache. Fixes the race condition via CAS on the runner KV entry.
|
||||
*/
|
||||
private async stationIntake(rawCard: number, stationId: number): Promise<ResponseScanIntake> {
|
||||
const MAX_RETRIES = 3;
|
||||
const cardId = rawCard % 200000000000;
|
||||
|
||||
// --- Station (already verified by ScanAuth, just need track data) ---
|
||||
const stationEntry = await getStationEntryById(stationId);
|
||||
// stationEntry is always populated here — ScanAuth wrote it on the cold path
|
||||
const trackDistance = stationEntry.trackDistance;
|
||||
const minimumLapTime = stationEntry.minimumLapTime;
|
||||
|
||||
// --- Card ---
|
||||
let cardEntry = await getCardEntry(cardId);
|
||||
if (!cardEntry) {
|
||||
// Cold path: load from DB and cache
|
||||
const card = await getConnection().getRepository(RunnerCard).findOne({ id: cardId }, { relations: ['runner'] });
|
||||
if (!card) throw new ScanNotFoundError();
|
||||
if (!card.runner) throw new RunnerNotFoundError();
|
||||
cardEntry = {
|
||||
runnerId: card.runner.id,
|
||||
runnerDisplayName: `${card.runner.firstname} ${card.runner.lastname}`,
|
||||
enabled: card.enabled,
|
||||
};
|
||||
await setCardEntry(cardId, cardEntry);
|
||||
}
|
||||
if (!cardEntry.enabled) throw new HttpError(400, 'Card is disabled.');
|
||||
const runnerId = cardEntry.runnerId;
|
||||
|
||||
// --- Runner state + CAS update (fixes race condition) ---
|
||||
const now = Math.round(Date.now() / 1000);
|
||||
let retries = 0;
|
||||
let response: ResponseScanIntake;
|
||||
|
||||
while (retries < MAX_RETRIES) {
|
||||
// Get current runner state (warm or cold)
|
||||
let result = await getRunnerEntry(runnerId);
|
||||
if (!result) {
|
||||
const warmed = await warmRunner(runnerId);
|
||||
result = { entry: warmed, revision: undefined };
|
||||
}
|
||||
const { entry, revision } = result;
|
||||
|
||||
// Compute
|
||||
const lapTime = entry.latestTimestamp === 0 ? 0 : now - entry.latestTimestamp;
|
||||
const valid = minimumLapTime === 0 || lapTime > minimumLapTime;
|
||||
const newDistance = entry.distance + (valid ? trackDistance : 0);
|
||||
const newTimestamp = valid ? now : entry.latestTimestamp;
|
||||
|
||||
const updated: RunnerKVEntry = {
|
||||
displayName: entry.displayName,
|
||||
distance: newDistance,
|
||||
latestTimestamp: newTimestamp,
|
||||
};
|
||||
|
||||
// CAS write — if revision is undefined (warmed this request), plain put
|
||||
const success = await setRunnerEntry(runnerId, updated, revision);
|
||||
if (!success) {
|
||||
retries++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// DB insert — synchronous, keeps DB as source of truth
|
||||
const newScan = new TrackScan();
|
||||
newScan.runner = { id: runnerId } as any;
|
||||
newScan.card = { id: cardId } as any;
|
||||
newScan.station = { id: stationId } as any;
|
||||
newScan.track = { id: stationEntry.trackId } as any;
|
||||
newScan.timestamp = now;
|
||||
newScan.lapTime = lapTime;
|
||||
newScan.valid = valid;
|
||||
await this.trackScanRepository.save(newScan);
|
||||
|
||||
const runnerInfo = new ResponseScanIntakeRunner();
|
||||
runnerInfo.displayName = entry.displayName;
|
||||
runnerInfo.distance = newDistance;
|
||||
|
||||
response = new ResponseScanIntake();
|
||||
response.accepted = true;
|
||||
response.valid = valid;
|
||||
response.lapTime = lapTime;
|
||||
response.runner = runnerInfo;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
throw new HttpError(409, 'Scan rejected: too many concurrent scans for this runner. Please retry.');
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@@ -86,7 +196,7 @@ export class ScanController {
|
||||
@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the scan (not track scan use /scans/trackscans/:id instead) whose id you provided. <br> Please remember that ids can't be changed and distances must be positive." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) scan: UpdateScan) {
|
||||
let oldScan = await this.scanRepository.findOne({ id: id });
|
||||
let oldScan = await this.scanRepository.findOne({ id: id }, { relations: ['runner'] });
|
||||
|
||||
if (!oldScan) {
|
||||
throw new ScanNotFoundError();
|
||||
@@ -96,8 +206,10 @@ export class ScanController {
|
||||
throw new ScanIdsNotMatchingError();
|
||||
}
|
||||
|
||||
const runnerId = oldScan.runner?.id;
|
||||
await this.scanRepository.save(await scan.update(oldScan));
|
||||
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
if (runnerId) await deleteRunnerEntry(runnerId);
|
||||
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
}
|
||||
|
||||
@Put('/trackscans/:id')
|
||||
@@ -109,7 +221,7 @@ export class ScanController {
|
||||
@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: 'Update the track scan (not "normal" scan use /scans/trackscans/:id instead) whose id you provided. <br> Please remember that only the validity, runner and track can be changed.' })
|
||||
async putTrackScan(@Param('id') id: number, @Body({ validate: true }) scan: UpdateTrackScan) {
|
||||
let oldScan = await this.trackScanRepository.findOne({ id: id });
|
||||
let oldScan = await this.trackScanRepository.findOne({ id: id }, { relations: ['runner'] });
|
||||
|
||||
if (!oldScan) {
|
||||
throw new ScanNotFoundError();
|
||||
@@ -119,8 +231,10 @@ export class ScanController {
|
||||
throw new ScanIdsNotMatchingError();
|
||||
}
|
||||
|
||||
const runnerId = oldScan.runner?.id;
|
||||
await this.trackScanRepository.save(await scan.update(oldScan));
|
||||
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
if (runnerId) await deleteRunnerEntry(runnerId);
|
||||
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@@ -132,7 +246,7 @@ export class ScanController {
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let scan = await this.scanRepository.findOne({ id: id });
|
||||
if (!scan) { return null; }
|
||||
const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] });
|
||||
const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] });
|
||||
|
||||
await this.scanRepository.delete(scan);
|
||||
return responseScan.toResponse();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { deleteStationEntry } from '../nats/StationKV';
|
||||
import { CreateScanStation } from '../models/actions/create/CreateScanStation';
|
||||
import { UpdateScanStation } from '../models/actions/update/UpdateScanStation';
|
||||
import { ScanStation } from '../models/entities/ScanStation';
|
||||
@@ -26,9 +27,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());
|
||||
});
|
||||
@@ -78,6 +86,7 @@ export class ScanStationController {
|
||||
}
|
||||
|
||||
await this.stationRepository.save(await station.update(oldStation));
|
||||
await deleteStationEntry(oldStation.prefix);
|
||||
return (await this.stationRepository.findOne({ id: id }, { relations: ['track'] })).toResponse();
|
||||
}
|
||||
|
||||
@@ -102,6 +111,7 @@ export class ScanStationController {
|
||||
}
|
||||
|
||||
const responseStation = await this.stationRepository.findOne({ id: station.id }, { relations: ["track"] });
|
||||
await deleteStationEntry(station.prefix);
|
||||
await this.stationRepository.delete(station);
|
||||
return responseStation.toResponse();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { Get, JsonController, UseBefore } from 'routing-controllers';
|
||||
import { Get, JsonController, QueryParam, UseBefore } from 'routing-controllers';
|
||||
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';
|
||||
import { Scan } from '../models/entities/Scan';
|
||||
import { TrackScan } from '../models/entities/TrackScan';
|
||||
import { User } from '../models/entities/User';
|
||||
import { ResponseStats } from '../models/responses/ResponseStats';
|
||||
import { ResponseStatsOrgnisation } from '../models/responses/ResponseStatsOrganization';
|
||||
import { ResponseStatsRunner } from '../models/responses/ResponseStatsRunner';
|
||||
import { ResponseStatsTeam } from '../models/responses/ResponseStatsTeam';
|
||||
import { getStatsCache, setStatsCache } from '../nats/StatsKV';
|
||||
|
||||
@JsonController('/stats')
|
||||
export class StatsController {
|
||||
@@ -20,14 +23,40 @@ 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();
|
||||
// Try cache first
|
||||
const cached = await getStatsCache<ResponseStats>('overview');
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Cache miss - compute fresh stats
|
||||
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();
|
||||
|
||||
const result = new ResponseStats(runnersViaSelfservice, runners, teams, orgs, users, scans, donations, distace, donors, runnersViaKiosk);
|
||||
|
||||
// Store in cache for 60 seconds
|
||||
await setStatsCache('overview', result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get("/runners/distance")
|
||||
@@ -35,12 +64,26 @@ export class StatsController {
|
||||
@ResponseSchema(ResponseStatsRunner, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten runners by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopRunnersByDistance() {
|
||||
// Try cache first
|
||||
const cached = await getStatsCache<ResponseStatsRunner[]>('runners.distance');
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Cache miss - compute fresh stats
|
||||
let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] });
|
||||
let topRunners = runners.sort((runner1, runner2) => runner1.distance - runner2.distance).slice(0, 9);
|
||||
if (!runners || runners.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topRunners = runners.sort((runner1, runner2) => runner2.distance - runner1.distance).slice(0, 10);
|
||||
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
|
||||
topRunners.forEach(runner => {
|
||||
responseRunners.push(new ResponseStatsRunner(runner));
|
||||
});
|
||||
|
||||
// Store in cache for 60 seconds
|
||||
await setStatsCache('runners.distance', responseRunners);
|
||||
|
||||
return responseRunners;
|
||||
}
|
||||
|
||||
@@ -49,12 +92,66 @@ export class StatsController {
|
||||
@ResponseSchema(ResponseStatsRunner, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten runners by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopRunnersByDonations() {
|
||||
let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] });
|
||||
let topRunners = runners.sort((runner1, runner2) => runner1.distanceDonationAmount - runner2.distanceDonationAmount).slice(0, 9);
|
||||
// Try cache first
|
||||
const cached = await getStatsCache<ResponseStatsRunner[]>('runners.donations');
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Cache miss - compute fresh stats
|
||||
let runners = await getConnection().getRepository(Runner).find({ relations: ['group', 'distanceDonations', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] });
|
||||
if (!runners || runners.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topRunners = runners.sort((runner1, runner2) => runner2.distanceDonationAmount - runner1.distanceDonationAmount).slice(0, 10);
|
||||
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
|
||||
topRunners.forEach(runner => {
|
||||
responseRunners.push(new ResponseStatsRunner(runner));
|
||||
});
|
||||
|
||||
// Store in cache for 60 seconds
|
||||
await setStatsCache('runners.donations', responseRunners);
|
||||
|
||||
return responseRunners;
|
||||
}
|
||||
|
||||
@Get("/runners/laptime")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsRunner, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten runners by fastest laptime on your selected track (track by id).", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopRunnersByLaptime(@QueryParam("track") track: number) {
|
||||
// Try cache first (cache key includes track id, using dots for NATS KV compatibility)
|
||||
const cacheKey = `runners.laptime.${track}`;
|
||||
const cached = await getStatsCache<ResponseStatsRunner[]>(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Cache miss - compute fresh stats
|
||||
let scans = await getConnection().getRepository(TrackScan).find({ relations: ['track', 'runner', 'runner.group', 'runner.scans', 'runner.scans.track', 'runner.distanceDonations'] });
|
||||
if (!scans || scans.length == 0) {
|
||||
return [];
|
||||
}
|
||||
scans = scans.filter((s) => { return s.track.id == track && s.valid == true && s.lapTime != 0 }).sort((scan1, scan2) => scan1.lapTime - scan2.lapTime);
|
||||
|
||||
let topScans = new Array<TrackScan>();
|
||||
let knownRunners = new Array<number>();
|
||||
for (let i = 0; i < scans.length && topScans.length < 10; i++) {
|
||||
const element = scans[i];
|
||||
if (!knownRunners.includes(element.runner.id)) {
|
||||
topScans.push(element);
|
||||
knownRunners.push(element.runner.id);
|
||||
}
|
||||
}
|
||||
|
||||
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
|
||||
topScans.forEach(scan => {
|
||||
responseRunners.push(new ResponseStatsRunner(scan.runner, scan.lapTime));
|
||||
});
|
||||
|
||||
// Store in cache for 60 seconds
|
||||
await setStatsCache(cacheKey, responseRunners);
|
||||
|
||||
return responseRunners;
|
||||
}
|
||||
|
||||
@@ -71,12 +168,26 @@ export class StatsController {
|
||||
@ResponseSchema(ResponseStatsTeam, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten teams by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopTeamsByDistance() {
|
||||
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
|
||||
let topTeams = teams.sort((team1, team2) => team1.distance - team2.distance).slice(0, 9);
|
||||
// Try cache first
|
||||
const cached = await getStatsCache<ResponseStatsTeam[]>('teams.distance');
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Cache miss - compute fresh stats
|
||||
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['parentGroup', 'runners', 'runners.scans', 'runners.scans.track'] });
|
||||
if (!teams || teams.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topTeams = teams.sort((team1, team2) => team2.distance - team1.distance).slice(0, 10);
|
||||
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
|
||||
topTeams.forEach(team => {
|
||||
responseTeams.push(new ResponseStatsTeam(team));
|
||||
});
|
||||
|
||||
// Store in cache for 60 seconds
|
||||
await setStatsCache('teams.distance', responseTeams);
|
||||
|
||||
return responseTeams;
|
||||
}
|
||||
|
||||
@@ -85,12 +196,26 @@ export class StatsController {
|
||||
@ResponseSchema(ResponseStatsTeam, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten teams by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopTeamsByDonations() {
|
||||
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
|
||||
let topTeams = teams.sort((team1, team2) => team1.distanceDonationAmount - team2.distanceDonationAmount).slice(0, 9);
|
||||
// Try cache first
|
||||
const cached = await getStatsCache<ResponseStatsTeam[]>('teams.donations');
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Cache miss - compute fresh stats
|
||||
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['parentGroup', 'runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
|
||||
if (!teams || teams.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topTeams = teams.sort((team1, team2) => team2.distanceDonationAmount - team1.distanceDonationAmount).slice(0, 10);
|
||||
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
|
||||
topTeams.forEach(team => {
|
||||
responseTeams.push(new ResponseStatsTeam(team));
|
||||
});
|
||||
|
||||
// Store in cache for 60 seconds
|
||||
await setStatsCache('teams.donations', responseTeams);
|
||||
|
||||
return responseTeams;
|
||||
}
|
||||
|
||||
@@ -99,12 +224,26 @@ export class StatsController {
|
||||
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten organizations by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopOrgsByDistance() {
|
||||
// Try cache first
|
||||
const cached = await getStatsCache<ResponseStatsOrgnisation[]>('organizations.distance');
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Cache miss - compute fresh stats
|
||||
let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
|
||||
let topOrgs = orgs.sort((org1, org2) => org1.distance - org2.distance).slice(0, 9);
|
||||
if (!orgs || orgs.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topOrgs = orgs.sort((org1, org2) => org2.distance - org1.distance).slice(0, 10);
|
||||
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
|
||||
topOrgs.forEach(org => {
|
||||
responseOrgs.push(new ResponseStatsOrgnisation(org));
|
||||
});
|
||||
|
||||
// Store in cache for 60 seconds
|
||||
await setStatsCache('organizations.distance', responseOrgs);
|
||||
|
||||
return responseOrgs;
|
||||
}
|
||||
|
||||
@@ -113,12 +252,26 @@ export class StatsController {
|
||||
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten organizations by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopOrgsByDonations() {
|
||||
let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
|
||||
let topOrgs = orgs.sort((org1, org2) => org1.distanceDonationAmount - org2.distanceDonationAmount).slice(0, 9);
|
||||
// Try cache first
|
||||
const cached = await getStatsCache<ResponseStatsOrgnisation[]>('organizations.donations');
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Cache miss - compute fresh stats
|
||||
let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.distanceDonations', 'runners.distanceDonations.runner', 'runners.distanceDonations.runner.scans', 'runners.distanceDonations.runner.scans.track', 'teams', 'teams.runners', 'teams.runners.distanceDonations', 'teams.runners.distanceDonations.runner', 'teams.runners.distanceDonations.runner.scans', 'teams.runners.distanceDonations.runner.scans.track'] });
|
||||
if (!orgs || orgs.length == 0) {
|
||||
return [];
|
||||
}
|
||||
let topOrgs = orgs.sort((org1, org2) => org2.distanceDonationAmount - org1.distanceDonationAmount).slice(0, 10);
|
||||
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
|
||||
topOrgs.forEach(org => {
|
||||
responseOrgs.push(new ResponseStatsOrgnisation(org));
|
||||
});
|
||||
|
||||
// Store in cache for 60 seconds
|
||||
await setStatsCache('organizations.donations', responseOrgs);
|
||||
|
||||
return responseOrgs;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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 { 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));
|
||||
});
|
||||
@@ -66,6 +74,10 @@ export class UserController {
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
|
||||
@OpenAPI({ description: 'Create a new user. <br> If you want to grant permissions to the user you have to create them seperately by posting to /api/permissions after creating the user.' })
|
||||
async post(@Body({ validate: true }) createUser: CreateUser) {
|
||||
let user;
|
||||
@@ -85,6 +97,10 @@ export class UserController {
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
|
||||
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the user whose id you provided. <br> To change the permissions directly granted to the user please use /api/permissions instead. <br> Please remember that ids can't be changed." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) updateUser: UpdateUser) {
|
||||
let oldUser = await this.userRepository.findOne({ id: id });
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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 { EntityFromBody } from 'typeorm-routing-controllers-extensions';
|
||||
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';
|
||||
import { UserGroup } from '../models/entities/UserGroup';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseUserGroup } from '../models/responses/ResponseUserGroup';
|
||||
import { ResponseUserGroupPermissions } from '../models/responses/ResponseUserGroupPermissions';
|
||||
import { PermissionController } from './PermissionController';
|
||||
|
||||
|
||||
@@ -25,20 +25,44 @@ export class UserGroupController {
|
||||
|
||||
@Get()
|
||||
@Authorized("USERGROUP:GET")
|
||||
@ResponseSchema(UserGroup, { isArray: true })
|
||||
@ResponseSchema(ResponseUserGroup, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all groups. <br> The information provided might change while the project continues to evolve.' })
|
||||
getAll() {
|
||||
return this.userGroupsRepository.find({ relations: ["permissions"] });
|
||||
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
|
||||
let responseGroups: ResponseUserGroup[] = new Array<ResponseUserGroup>();
|
||||
let groups: Array<UserGroup>;
|
||||
|
||||
if (page != undefined) {
|
||||
groups = await this.userGroupsRepository.find({ relations: ['permissions'], skip: page * page_size, take: page_size });
|
||||
} else {
|
||||
groups = await this.userGroupsRepository.find({ relations: ['permissions'] });
|
||||
}
|
||||
|
||||
groups.forEach(group => {
|
||||
responseGroups.push(group.toResponse());
|
||||
});
|
||||
return responseGroups;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("USERGROUP:GET")
|
||||
@ResponseSchema(UserGroup)
|
||||
@ResponseSchema(ResponseUserGroup)
|
||||
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(UserGroupNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about the group whose id got provided. <br> The information provided might change while the project continues to evolve.' })
|
||||
getOne(@Param('id') id: number) {
|
||||
return this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
|
||||
async getOne(@Param('id') id: number) {
|
||||
return await (await (this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] }))).toResponse();
|
||||
}
|
||||
|
||||
@Get('/:id/permissions')
|
||||
@Authorized("USERGROUP:GET")
|
||||
@ResponseSchema(ResponseUserGroupPermissions)
|
||||
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(UserGroupNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all permissions granted to the group as permission response objects.' })
|
||||
async getPermissions(@Param('id') id: number) {
|
||||
let group = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions', 'permissions.principal'] })
|
||||
if (!group) { throw new UserGroupNotFoundError(); }
|
||||
return new ResponseUserGroupPermissions(group);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@@ -54,7 +78,8 @@ export class UserGroupController {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return this.userGroupsRepository.save(userGroup);
|
||||
userGroup = await this.userGroupsRepository.save(userGroup);
|
||||
return (await (this.userGroupsRepository.findOne({ id: userGroup.id }, { relations: ["permissions"] }))).toResponse();
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@@ -63,7 +88,7 @@ export class UserGroupController {
|
||||
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UserGroupIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the group whose id you provided. <br> To change the permissions granted to the group please use /api/permissions instead. <br> Please remember that ids can't be changed." })
|
||||
async put(@Param('id') id: number, @EntityFromBody() updateGroup: UpdateUserGroup) {
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) updateGroup: UpdateUserGroup) {
|
||||
let oldGroup = await this.userGroupsRepository.findOne({ id: id });
|
||||
|
||||
if (!oldGroup) {
|
||||
@@ -75,7 +100,7 @@ export class UserGroupController {
|
||||
}
|
||||
await this.userGroupsRepository.save(await updateGroup.update(oldGroup));
|
||||
|
||||
return (await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] })).toResponse();
|
||||
return (await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] })).toResponse();
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@@ -85,7 +110,7 @@ export class UserGroupController {
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the group whose id you provided. <br> If there are any permissions directly granted to the group they will get deleted as well. <br> Users associated with this group won\'t get deleted - just deassociated. <br> If no group with this id exists it will just return 204(no content).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let group = await this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
|
||||
let group = await this.userGroupsRepository.findOne({ id: id });
|
||||
if (!group) { return null; }
|
||||
const responseGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] });
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { IsString } from 'class-validator'
|
||||
import { IsString } from 'class-validator';
|
||||
import { InternalServerError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a permission couldn't be found.
|
||||
*/
|
||||
export class MailServerConfigError extends Error {
|
||||
export class MailSendingError extends InternalServerError {
|
||||
@IsString()
|
||||
name = "MailServerConfigError"
|
||||
name = "MailSendingError"
|
||||
|
||||
@IsString()
|
||||
message = "The SMTP server you provided couldn't be reached!"
|
||||
message = "We had a problem sending the mail!"
|
||||
|
||||
constructor() {
|
||||
super("We had a problem sending the mail!");
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,17 @@ export class RunnerEmailNeededError extends NotAcceptableError {
|
||||
message = "Citizenrunners have to provide an email address for verification and contacting."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a runner already requested a new selfservice link in the last 30s.
|
||||
*/
|
||||
export class RunnerSelfserviceTimeoutError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerSelfserviceTimeoutError"
|
||||
|
||||
@IsString()
|
||||
message = "You can only reqest a new token every 30s."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a runner still has distance donations associated.
|
||||
*/
|
||||
|
||||
@@ -71,4 +71,33 @@ export class UserDeletionNotConfirmedError extends NotAcceptableError {
|
||||
|
||||
@IsString()
|
||||
message = "You are trying to delete a user! \n If you're sure about doing this: provide the ?force=true query param."
|
||||
}
|
||||
|
||||
export class PasswordMustContainUppercaseLetterError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "PasswordMustContainUppercaseLetterError"
|
||||
|
||||
@IsString()
|
||||
message = "Passwords must contain at least one uppercase letter."
|
||||
}
|
||||
export class PasswordMustContainLowercaseLetterError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "PasswordMustContainLowercaseLetterError"
|
||||
|
||||
@IsString()
|
||||
message = "Passwords must contain at least one lowercase letter."
|
||||
}
|
||||
export class PasswordMustContainNumberError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "PasswordMustContainNumberError"
|
||||
|
||||
@IsString()
|
||||
message = "Passwords must contain at least one number."
|
||||
}
|
||||
export class PasswordTooShortError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "PasswordTooShortError"
|
||||
|
||||
@IsString()
|
||||
message = "Passwords must be at least ten characters long."
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createConnection } from "typeorm";
|
||||
import { runSeeder } from 'typeorm-seeding';
|
||||
import consola from 'consola';
|
||||
import { config } from '../config';
|
||||
import { ConfigFlag } from '../models/entities/ConfigFlags';
|
||||
import SeedPublicOrg from '../seeds/SeedPublicOrg';
|
||||
@@ -11,6 +12,11 @@ import SeedUsers from '../seeds/SeedUsers';
|
||||
*/
|
||||
export default async () => {
|
||||
const connection = await createConnection();
|
||||
|
||||
// Log discovered entities for debugging
|
||||
consola.info(`TypeORM discovered ${connection.entityMetadatas.length} entities:`);
|
||||
consola.info(connection.entityMetadatas.map(m => m.name).sort().join(', '));
|
||||
|
||||
await connection.synchronize();
|
||||
|
||||
//The data seeding part
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Application } from "express";
|
||||
import consola from "consola";
|
||||
import { config } from "../config";
|
||||
import NatsClient from "../nats/NatsClient";
|
||||
import { warmAll } from "../nats/RunnerKV";
|
||||
import databaseLoader from "./database";
|
||||
import expressLoader from "./express";
|
||||
import openapiLoader from "./openapi";
|
||||
@@ -9,6 +13,16 @@ import openapiLoader from "./openapi";
|
||||
*/
|
||||
export default async (app: Application) => {
|
||||
await databaseLoader();
|
||||
await NatsClient.connect();
|
||||
|
||||
if (config.nats_prewarm) {
|
||||
consola.info("Prewarming NATS runner cache...");
|
||||
const startTime = Date.now();
|
||||
await warmAll();
|
||||
const duration = Date.now() - startTime;
|
||||
consola.success(`NATS runner cache prewarmed in ${duration}ms`);
|
||||
}
|
||||
|
||||
await openapiLoader(app);
|
||||
await expressLoader(app);
|
||||
return app;
|
||||
|
||||
159
src/mailer.ts
159
src/mailer.ts
@@ -1,79 +1,118 @@
|
||||
import fs from "fs";
|
||||
import nodemailer from 'nodemailer';
|
||||
import { MailOptions } from 'nodemailer/lib/json-transport';
|
||||
import Mail from 'nodemailer/lib/mailer';
|
||||
import axios from 'axios';
|
||||
import { config } from './config';
|
||||
import { MailServerConfigError } from './errors/MailErrors';
|
||||
import { MailSendingError } from './errors/MailErrors';
|
||||
|
||||
/**
|
||||
* This class is responsible for all things mail sending.
|
||||
* This uses the mail emplates from src/static/mail_templates
|
||||
* This uses axios to communicate with the mailer api (https://git.odit.services/lfk/mailer).
|
||||
*/
|
||||
export class Mailer {
|
||||
private transport: Mail;
|
||||
public static base: string = config.mailer_url;
|
||||
public static key: string = config.mailer_key;
|
||||
public static testing: boolean = config.testing;
|
||||
|
||||
/**
|
||||
* The class's default constructor.
|
||||
* Creates the transporter and tests the connection.
|
||||
*/
|
||||
constructor() {
|
||||
this.transport = nodemailer.createTransport({
|
||||
host: config.mail_server,
|
||||
port: config.mail_port,
|
||||
auth: {
|
||||
user: config.mail_user,
|
||||
pass: config.mail_password
|
||||
}
|
||||
});
|
||||
|
||||
this.transport.verify(function (error, success) {
|
||||
if (error) {
|
||||
throw new MailServerConfigError();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function for sending a test mail from the test mail template.
|
||||
* Function for sending a password reset mail.
|
||||
* @param to_address The address the mail will be sent to. Should always get pulled from a user object.
|
||||
* @param token The requested password reset token - will be combined with the app_url to generate a password reset link.
|
||||
*/
|
||||
public async sendResetMail(to_address: string, token: String) {
|
||||
const reset_link = `${config.app_url}/reset/${token}`
|
||||
const body_html = fs.readFileSync(__dirname + '/static/mail_templates/pw-reset.html', { encoding: 'utf8' }).replace("{{reset_link}}", reset_link).replace("{{recipient_mail}}", to_address).replace("{{copyright_owner}}", "LfK!").replace("{{link_imprint}}", `${config.app_url}/imprint`).replace("{{link_privacy}}", `${config.app_url}/privacy`);
|
||||
const body_txt = fs.readFileSync(__dirname + '/static/mail_templates/pw-reset.html', { encoding: 'utf8' }).replace("{{reset_link}}", reset_link).replace("{{recipient_mail}}", to_address).replace("{{copyright_owner}}", "LfK!").replace("{{link_imprint}}", `${config.app_url}/imprint`).replace("{{link_privacy}}", `${config.app_url}/privacy`);
|
||||
|
||||
const mail: MailOptions = {
|
||||
to: to_address,
|
||||
subject: "LfK! Password Reset",
|
||||
text: body_txt,
|
||||
html: body_html
|
||||
};
|
||||
await this.sendMail(mail);
|
||||
public static async sendResetMail(to_address: string, token: string, locale: string = "en") {
|
||||
try {
|
||||
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; }
|
||||
throw new MailSendingError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function for sending a test mail from the test mail template.
|
||||
* @param to_address The address the test mail will be sent to - this is the configured from-address by default.
|
||||
*/
|
||||
public async sendTestMail(to_address: string = config.mail_from) {
|
||||
const body_html = fs.readFileSync(__dirname + '/static/mail_templates/test.html', { encoding: 'utf8' }).replace("{{recipient_mail}}", to_address).replace("{{copyright_owner}}", "LfK!").replace("{{link_imprint}}", `${config.app_url}/imprint`).replace("{{link_privacy}}", `${config.app_url}/privacy`);
|
||||
const body_txt = fs.readFileSync(__dirname + '/static/mail_templates/test.txt', { encoding: 'utf8' }).replace("{{recipient_mail}}", to_address).replace("{{copyright_owner}}", "LfK!").replace("{{link_imprint}}", `${config.app_url}/imprint`).replace("{{link_privacy}}", `${config.app_url}/privacy`);
|
||||
const mail: MailOptions = {
|
||||
to: to_address,
|
||||
subject: "LfK! Test Mail",
|
||||
text: body_txt,
|
||||
html: body_html
|
||||
};
|
||||
await this.sendMail(mail);
|
||||
* 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, runner_id: number, firstname: string, middlename: string, lastname: string, token: string, locale: string = "en") {
|
||||
try {
|
||||
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; }
|
||||
throw new MailSendingError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function for sending a mail via this object's transporter.
|
||||
* @param mail MailOptions object containing the
|
||||
*/
|
||||
public async sendMail(mail: MailOptions) {
|
||||
mail.from = config.mail_from;
|
||||
await this.transport.sendMail(mail);
|
||||
* 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, runner_id: number, firstname: string, middlename: string, lastname: string, token: string, locale: string = "en") {
|
||||
try {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,129 @@
|
||||
import * as argon2 from "argon2";
|
||||
import crypto from 'crypto';
|
||||
import { Request, Response } from 'express';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { config } from '../config';
|
||||
import { deleteStationEntry, getStationEntry, setStationEntry, StationKVEntry } from '../nats/StationKV';
|
||||
import { ScanStation } from '../models/entities/ScanStation';
|
||||
import authchecker from './authchecker';
|
||||
|
||||
/**
|
||||
* Computes the HMAC-SHA256 of the provided token using the station token secret.
|
||||
*/
|
||||
function computeHmac(token: string): string {
|
||||
return crypto.createHmac('sha256', config.station_token_secret).update(token).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant-time comparison of two hex HMAC strings.
|
||||
* Returns true if they match.
|
||||
*/
|
||||
function verifyHmac(provided_token: string, storedHash: string): boolean {
|
||||
const expectedHash = computeHmac(provided_token);
|
||||
const expectedBuf = Buffer.from(expectedHash);
|
||||
const storedBuf = Buffer.from(storedHash);
|
||||
return expectedBuf.length === storedBuf.length && crypto.timingSafeEqual(expectedBuf, storedBuf);
|
||||
}
|
||||
|
||||
/**
|
||||
* This middleware handles the authentication of scan station api tokens.
|
||||
* The tokens have to be provided via Bearer authorization header.
|
||||
*
|
||||
* Auth flow:
|
||||
* 1. Extract prefix from token (PREFIX.KEY format)
|
||||
* 2. Try NATS KV cache lookup by prefix — warm path: HMAC verify, no DB
|
||||
* 3. On cache miss: DB lookup → HMAC verify → write to KV cache
|
||||
* 4. On no station match at all: fall back to JWT auth (SCAN:CREATE permission)
|
||||
*
|
||||
* On success sets req.isStationAuth = true and req.stationId on the request object.
|
||||
* These are internal server-side properties — not HTTP headers, not spoofable by clients.
|
||||
*
|
||||
* You have to manually use this middleware via @UseBefore(ScanAuth) instead of using @Authorized().
|
||||
* @param req Express request object.
|
||||
* @param res Express response object.
|
||||
* @param next Next function to call on success.
|
||||
*/
|
||||
const ScanAuth = async (req: Request, res: Response, next: () => void) => {
|
||||
let provided_token: string = req.headers["authorization"];
|
||||
if (provided_token == "" || provided_token === undefined || provided_token === null) {
|
||||
res.status(401).send("No api token provided.");
|
||||
let provided_token: string = req.headers['authorization'];
|
||||
if (!provided_token) {
|
||||
res.status(401).send({ http_code: 401, short: 'no_token', message: 'No api token provided.' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
provided_token = provided_token.replace("Bearer ", "");
|
||||
} catch (error) {
|
||||
res.status(401).send("No valid jwt or api token provided.");
|
||||
provided_token = provided_token.replace('Bearer ', '');
|
||||
|
||||
const prefix = provided_token.split('.')[0];
|
||||
if (!prefix) {
|
||||
res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
|
||||
return;
|
||||
}
|
||||
|
||||
let prefix = "";
|
||||
try {
|
||||
prefix = provided_token.split(".")[0];
|
||||
}
|
||||
finally {
|
||||
if (prefix == "" || prefix == undefined || prefix == null) {
|
||||
res.status(401).send("Api token non-existent or invalid syntax.");
|
||||
// --- KV cache lookup (warm path) ---
|
||||
const cached = await getStationEntry(prefix);
|
||||
if (cached) {
|
||||
if (!cached.enabled) {
|
||||
res.status(401).send({ http_code: 401, short: 'station_disabled', message: 'Station is disabled.' });
|
||||
return;
|
||||
}
|
||||
if (!verifyHmac(provided_token, cached.tokenHash)) {
|
||||
res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
|
||||
return;
|
||||
}
|
||||
req.isStationAuth = true;
|
||||
req.stationId = cached.id;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const station = await getConnectionManager().get().getRepository(ScanStation).findOne({ prefix: prefix });
|
||||
// --- DB lookup (cold path) ---
|
||||
const station = await getConnectionManager().get().getRepository(ScanStation).findOne({ prefix }, { relations: ['track'] });
|
||||
|
||||
if (!station) {
|
||||
// No station with this prefix — fall back to JWT auth
|
||||
let user_authorized = false;
|
||||
try {
|
||||
let action = { request: req, response: res, context: null, next: next }
|
||||
user_authorized = await authchecker(action, ["SCAN:CREATE"]);
|
||||
}
|
||||
finally {
|
||||
if (user_authorized == false) {
|
||||
res.status(401).send("Api token non-existent or invalid syntax.");
|
||||
const action = { request: req, response: res, context: null, next };
|
||||
user_authorized = await authchecker(action, ['SCAN:CREATE']);
|
||||
} finally {
|
||||
if (!user_authorized) {
|
||||
res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
|
||||
return;
|
||||
}
|
||||
else {
|
||||
next();
|
||||
}
|
||||
next();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (station.enabled == false) {
|
||||
res.status(401).send("Station disabled.");
|
||||
}
|
||||
if (!(await argon2.verify(station.key, provided_token))) {
|
||||
res.status(401).send("Api token invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
// Station found — verify token before caching
|
||||
const tokenHash = computeHmac(provided_token);
|
||||
const storedBuf = Buffer.from(station.key);
|
||||
const computedBuf = Buffer.from(tokenHash);
|
||||
const valid = computedBuf.length === storedBuf.length && crypto.timingSafeEqual(computedBuf, storedBuf);
|
||||
|
||||
if (!valid) {
|
||||
res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
export default ScanAuth;
|
||||
|
||||
if (!station.enabled) {
|
||||
res.status(401).send({ http_code: 401, short: 'station_disabled', message: 'Station is disabled.' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Write to KV cache for subsequent requests
|
||||
const entry: StationKVEntry = {
|
||||
id: station.id,
|
||||
enabled: station.enabled,
|
||||
tokenHash,
|
||||
trackId: station.track.id,
|
||||
trackDistance: station.track.distance,
|
||||
minimumLapTime: station.track.minimumLapTime ?? 0,
|
||||
};
|
||||
await setStationEntry(prefix, entry);
|
||||
|
||||
req.isStationAuth = true;
|
||||
req.stationId = station.id;
|
||||
next();
|
||||
};
|
||||
|
||||
export default ScanAuth;
|
||||
export { deleteStationEntry };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as argon2 from "argon2";
|
||||
import * as Bun from 'bun';
|
||||
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 Bun.password.verify(provided_token, client.key))) {
|
||||
res.status(401).send("Api token invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as argon2 from "argon2";
|
||||
import * as Bun from 'bun';
|
||||
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 Bun.password.hash(this.password + found_user.uuid);
|
||||
await getConnectionManager().get().getRepository(User).save(found_user);
|
||||
|
||||
return "password reset successfull";
|
||||
|
||||
29
src/models/actions/create/CreateAnonymousDonation.ts
Normal file
29
src/models/actions/create/CreateAnonymousDonation.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as argon2 from "argon2";
|
||||
import * as Bun from 'bun';
|
||||
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 Bun.password.verify(this.password + found_user.uuid, found_user.password))) {
|
||||
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;
|
||||
|
||||
@@ -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.
|
||||
@@ -33,6 +48,7 @@ export class CreateDistanceDonation extends CreateDonation {
|
||||
let newDonation = new DistanceDonation;
|
||||
|
||||
newDonation.amountPerDistance = this.amountPerDistance;
|
||||
newDonation.paidAmount = this.paidAmount;
|
||||
newDonation.donor = await this.getDonor();
|
||||
newDonation.runner = await this.getRunner();
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IsInt, 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,14 +7,14 @@ 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;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
paidAmount?: number;
|
||||
|
||||
/**
|
||||
* Creates a new Donation entity from this.
|
||||
*/
|
||||
@@ -26,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;
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
@@ -21,6 +36,7 @@ export class CreateFixedDonation extends CreateDonation {
|
||||
let newDonation = new FixedDonation;
|
||||
|
||||
newDonation.amount = this.amount;
|
||||
newDonation.paidAmount = this.paidAmount;
|
||||
newDonation.donor = await this.getDonor();
|
||||
|
||||
return newDonation;
|
||||
|
||||
@@ -50,4 +50,11 @@ export abstract class CreateParticipant {
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
address?: Address;
|
||||
|
||||
/**
|
||||
* how the participant got into the system
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
created_via?: string;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export class CreateResetToken {
|
||||
/**
|
||||
* Create a password reset token based on this.
|
||||
*/
|
||||
public async toResetToken(): Promise<any> {
|
||||
public async toResetToken(): Promise<string> {
|
||||
if (!this.email) {
|
||||
throw new UserEmailNeededError();
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export class CreateResetToken {
|
||||
await getConnectionManager().get().getRepository(User).save(found_user);
|
||||
|
||||
//Create the reset token
|
||||
let reset_token = JwtCreator.createReset(found_user);
|
||||
let reset_token: string = JwtCreator.createReset(found_user);
|
||||
|
||||
return reset_token;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IsBoolean, IsObject, IsOptional } from 'class-validator';
|
||||
import * as uuid from 'uuid';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { RunnerOrganization } from '../../entities/RunnerOrganization';
|
||||
import { CreateRunnerGroup } from './CreateRunnerGroup';
|
||||
@@ -35,7 +34,7 @@ export class CreateRunnerOrganization extends CreateRunnerGroup {
|
||||
Address.validate(newRunnerOrganization.address);
|
||||
|
||||
if (this.registrationEnabled) {
|
||||
newRunnerOrganization.key = uuid.v4().toUpperCase();
|
||||
newRunnerOrganization.key = crypto.randomUUID()
|
||||
}
|
||||
|
||||
return newRunnerOrganization;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as argon2 from "argon2";
|
||||
import { IsBoolean, IsInt, IsOptional, IsPositive, IsString } from 'class-validator';
|
||||
import crypto from 'crypto';
|
||||
import { getConnection } from 'typeorm';
|
||||
import * as uuid from 'uuid';
|
||||
import { config } from '../../../config';
|
||||
import { TrackNotFoundError } from '../../../errors/TrackErrors';
|
||||
import { ScanStation } from '../../entities/ScanStation';
|
||||
import { Track } from '../../entities/Track';
|
||||
@@ -42,10 +41,10 @@ export class CreateScanStation {
|
||||
newStation.enabled = this.enabled;
|
||||
newStation.track = await this.getTrack();
|
||||
|
||||
let newUUID = uuid.v4().toUpperCase();
|
||||
let newUUID = crypto.randomUUID().toUpperCase();
|
||||
newStation.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
|
||||
newStation.key = await argon2.hash(newStation.prefix + "." + newUUID);
|
||||
newStation.cleartextkey = newStation.prefix + "." + newUUID;
|
||||
newStation.key = crypto.createHmac("sha256", config.station_token_secret).update(newStation.cleartextkey).digest('hex');
|
||||
|
||||
return newStation;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as argon2 from "argon2";
|
||||
import * as Bun from 'bun';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import crypto from 'crypto';
|
||||
import * as uuid from 'uuid';
|
||||
import { StatsClient } from '../../entities/StatsClient';
|
||||
|
||||
/**
|
||||
@@ -23,9 +22,9 @@ export class CreateStatsClient {
|
||||
|
||||
newClient.description = this.description;
|
||||
|
||||
let newUUID = uuid.v4().toUpperCase();
|
||||
let newUUID = crypto.randomUUID().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 Bun.password.hash(newClient.prefix + "." + newUUID);
|
||||
newClient.cleartextkey = newClient.prefix + "." + newUUID;
|
||||
|
||||
return newClient;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IsInt, IsPositive } from 'class-validator';
|
||||
import { IsInt, IsOptional, IsPositive } from 'class-validator';
|
||||
import { BadRequestError } from 'routing-controllers';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { RunnerCardNotFoundError } from '../../../errors/RunnerCardErrors';
|
||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
|
||||
@@ -22,10 +23,12 @@ export class CreateTrackScan {
|
||||
/**
|
||||
* The scanning station's id that created the scan.
|
||||
* Mainly used for logging and traceing back scans (or errors).
|
||||
* You don't have to provide the station if you're authenticateing via a scanstation token (The server takes care of it for you).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
station: number;
|
||||
@IsOptional()
|
||||
station?: number;
|
||||
|
||||
/**
|
||||
* Creates a new Track entity from this.
|
||||
@@ -44,20 +47,32 @@ export class CreateTrackScan {
|
||||
}
|
||||
|
||||
newScan.timestamp = Math.round(new Date().getTime() / 1000);
|
||||
newScan.valid = await this.validateScan(newScan);
|
||||
newScan = await this.validateScan(newScan);
|
||||
|
||||
return newScan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's a runnerCard entity via the provided id.
|
||||
* @returns The runnerCard whom's id you provided.
|
||||
*/
|
||||
public async getCard(): Promise<RunnerCard> {
|
||||
const track = await getConnection().getRepository(RunnerCard).findOne({ id: this.card }, { relations: ["runner"] });
|
||||
if (!track) {
|
||||
const id = this.card % 200000000000;
|
||||
const runnerCard = await getConnection().getRepository(RunnerCard).findOne({ id: id }, { relations: ["runner"] });
|
||||
if (!runnerCard) {
|
||||
throw new RunnerCardNotFoundError();
|
||||
}
|
||||
return track;
|
||||
return runnerCard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's a scanstation entity via the provided id.
|
||||
* @returns The scanstation whom's id you provided.
|
||||
*/
|
||||
public async getStation(): Promise<ScanStation> {
|
||||
if (!this.station) {
|
||||
throw new BadRequestError("You are missing the station's id!")
|
||||
}
|
||||
const station = await getConnection().getRepository(ScanStation).findOne({ id: this.station }, { relations: ["track"] });
|
||||
if (!station) {
|
||||
throw new ScanStationNotFoundError();
|
||||
@@ -65,15 +80,21 @@ export class CreateTrackScan {
|
||||
return station;
|
||||
}
|
||||
|
||||
public async validateScan(scan: TrackScan): Promise<boolean> {
|
||||
const scans = await getConnection().getRepository(TrackScan).find({ where: { runner: scan.runner, valid: true }, relations: ["track"] });
|
||||
if (scans.length == 0) { return true; }
|
||||
|
||||
const newestScan = scans[scans.length - 1];
|
||||
if ((scan.timestamp - newestScan.timestamp) > scan.track.minimumLapTime) {
|
||||
return true;
|
||||
/**
|
||||
* Validates the scan and sets it's lap time;
|
||||
* @param scan The scan you want to validate
|
||||
* @returns The validated scan with it's laptime set.
|
||||
*/
|
||||
public async validateScan(scan: TrackScan): Promise<TrackScan> {
|
||||
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;
|
||||
}
|
||||
|
||||
return false;
|
||||
else {
|
||||
scan.lapTime = scan.timestamp - latestScan.timestamp;
|
||||
scan.valid = (scan.lapTime > scan.track.minimumLapTime);
|
||||
}
|
||||
return scan;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as argon2 from "argon2";
|
||||
import * as Bun from 'bun';
|
||||
import { passwordStrength } from "check-password-strength";
|
||||
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import * as uuid from 'uuid';
|
||||
import { config } from '../../../config';
|
||||
import { UserEmailNeededError, UsernameContainsIllegalCharacterError } from '../../../errors/UserErrors';
|
||||
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserEmailNeededError, UsernameContainsIllegalCharacterError } from '../../../errors/UserErrors';
|
||||
import { UserGroupNotFoundError } from '../../../errors/UserGroupErrors';
|
||||
import { User } from '../../entities/User';
|
||||
import { UserGroup } from '../../entities/UserGroup';
|
||||
@@ -94,20 +94,26 @@ export class CreateUser {
|
||||
if (!this.email) {
|
||||
throw new UserEmailNeededError();
|
||||
}
|
||||
if (this.username.includes("@")) { throw new UsernameContainsIllegalCharacterError(); }
|
||||
if (this.username?.includes("@")) { throw new UsernameContainsIllegalCharacterError(); }
|
||||
|
||||
let password_strength = passwordStrength(this.password);
|
||||
if (!password_strength.contains.includes("uppercase")) { throw new PasswordMustContainUppercaseLetterError(); }
|
||||
if (!password_strength.contains.includes("lowercase")) { throw new PasswordMustContainLowercaseLetterError(); }
|
||||
if (!password_strength.contains.includes("number")) { throw new PasswordMustContainNumberError(); }
|
||||
if (!(password_strength.length > 9)) { throw new PasswordTooShortError(); }
|
||||
|
||||
newUser.email = this.email
|
||||
newUser.username = this.username
|
||||
newUser.firstname = this.firstname
|
||||
newUser.middlename = this.middlename
|
||||
newUser.lastname = this.lastname
|
||||
newUser.uuid = uuid.v4()
|
||||
newUser.uuid = crypto.randomUUID()
|
||||
newUser.phone = this.phone
|
||||
newUser.password = await argon2.hash(this.password + newUser.uuid);
|
||||
newUser.password = await Bun.password.hash(this.password + newUser.uuid);
|
||||
newUser.groups = await this.getGroups();
|
||||
newUser.enabled = this.enabled;
|
||||
|
||||
if (!this.profilePic) { newUser.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; }
|
||||
if (!this.profilePic) { newUser.profilePic = `https://lauf-fuer-kaya.de/lfk-logo.png`; }
|
||||
else { newUser.profilePic = this.profilePic; }
|
||||
|
||||
return newUser;
|
||||
|
||||
@@ -32,6 +32,7 @@ export class UpdateDistanceDonation extends UpdateDonation {
|
||||
*/
|
||||
public async update(donation: DistanceDonation): Promise<DistanceDonation> {
|
||||
donation.amountPerDistance = this.amountPerDistance;
|
||||
donation.paidAmount = this.paidAmount;
|
||||
donation.donor = await this.getDonor();
|
||||
donation.runner = await this.getRunner();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsInt, IsPositive } from 'class-validator';
|
||||
import { IsInt, IsOptional, IsPositive } from 'class-validator';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { DonorNotFoundError } from '../../../errors/DonorErrors';
|
||||
import { Donation } from '../../entities/Donation';
|
||||
@@ -23,6 +23,13 @@ export abstract class UpdateDonation {
|
||||
@IsPositive()
|
||||
donor: number;
|
||||
|
||||
/**
|
||||
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
paidAmount?: number;
|
||||
|
||||
/**
|
||||
* Creates a new Donation entity from this.
|
||||
*/
|
||||
|
||||
@@ -20,6 +20,7 @@ export class UpdateFixedDonation extends UpdateDonation {
|
||||
*/
|
||||
public async update(donation: FixedDonation): Promise<FixedDonation> {
|
||||
donation.amount = this.amount;
|
||||
donation.paidAmount = this.paidAmount;
|
||||
donation.donor = await this.getDonor();
|
||||
|
||||
return donation;
|
||||
|
||||
50
src/models/actions/update/UpdateRunnerCardByCode.ts
Normal file
50
src/models/actions/update/UpdateRunnerCardByCode.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IsBoolean, IsInt, IsObject, IsOptional } from 'class-validator';
|
||||
import * as uuid from 'uuid';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { RunnerOrganization } from '../../entities/RunnerOrganization';
|
||||
import { CreateRunnerGroup } from '../create/CreateRunnerGroup';
|
||||
@@ -42,7 +41,7 @@ export class UpdateRunnerOrganization extends CreateRunnerGroup {
|
||||
Address.validate(organization.address);
|
||||
|
||||
if (this.registrationEnabled && !organization.key) {
|
||||
organization.key = uuid.v4().toUpperCase();
|
||||
organization.key = crypto.randomUUID().toUpperCase();
|
||||
}
|
||||
else {
|
||||
organization.key = null;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IsBoolean, IsInt, IsOptional, IsPositive } from 'class-validator';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
|
||||
import { ScanStationNotFoundError } from '../../../errors/ScanStationErrors';
|
||||
import { TrackNotFoundError } from '../../../errors/TrackErrors';
|
||||
import { Runner } from '../../entities/Runner';
|
||||
import { ScanStation } from '../../entities/ScanStation';
|
||||
import { Track } from '../../entities/Track';
|
||||
import { TrackScan } from '../../entities/TrackScan';
|
||||
|
||||
/**
|
||||
@@ -38,7 +38,7 @@ export abstract class UpdateTrackScan {
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
public station: number;
|
||||
public track: number;
|
||||
|
||||
/**
|
||||
* Update a TrackScan entity based on this.
|
||||
@@ -47,8 +47,7 @@ export abstract class UpdateTrackScan {
|
||||
public async update(scan: TrackScan): Promise<TrackScan> {
|
||||
scan.valid = this.valid;
|
||||
scan.runner = await this.getRunner();
|
||||
scan.station = await this.getStation();
|
||||
scan.track = scan.station.track;
|
||||
scan.track = await this.getTrack();
|
||||
|
||||
return scan;
|
||||
}
|
||||
@@ -67,11 +66,11 @@ export abstract class UpdateTrackScan {
|
||||
/**
|
||||
* Gets a runner based on the runner id provided via this.runner.
|
||||
*/
|
||||
public async getStation(): Promise<ScanStation> {
|
||||
const station = await getConnection().getRepository(ScanStation).findOne({ id: this.station }, { relations: ['track'] });
|
||||
if (!station) {
|
||||
throw new ScanStationNotFoundError();
|
||||
public async getTrack(): Promise<Track> {
|
||||
const track = await getConnection().getRepository(Track).findOne({ id: this.track });
|
||||
if (!track) {
|
||||
throw new TrackNotFoundError();
|
||||
}
|
||||
return station;
|
||||
return track;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import * as argon2 from "argon2";
|
||||
import * as Bun from 'bun';
|
||||
import { passwordStrength } from "check-password-strength";
|
||||
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { config } from '../../../config';
|
||||
import { UserEmailNeededError, UsernameContainsIllegalCharacterError } from '../../../errors/UserErrors';
|
||||
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserEmailNeededError, UsernameContainsIllegalCharacterError } from '../../../errors/UserErrors';
|
||||
import { UserGroupNotFoundError } from '../../../errors/UserGroupErrors';
|
||||
import { User } from '../../entities/User';
|
||||
import { UserGroup } from '../../entities/UserGroup';
|
||||
|
||||
|
||||
/**
|
||||
* This class is used to update a User entity (via put request).
|
||||
*/
|
||||
@@ -104,7 +106,12 @@ export class UpdateUser {
|
||||
if (this.username.includes("@")) { throw new UsernameContainsIllegalCharacterError(); }
|
||||
|
||||
if (this.password) {
|
||||
user.password = await argon2.hash(this.password + user.uuid);
|
||||
let password_strength = passwordStrength(this.password);
|
||||
if (!password_strength.contains.includes("uppercase")) { throw new PasswordMustContainUppercaseLetterError(); }
|
||||
if (!password_strength.contains.includes("lowercase")) { throw new PasswordMustContainLowercaseLetterError(); }
|
||||
if (!password_strength.contains.includes("number")) { throw new PasswordMustContainNumberError(); }
|
||||
if (!(password_strength.length > 9)) { throw new PasswordTooShortError(); }
|
||||
user.password = await Bun.password.hash(this.password + user.uuid);
|
||||
user.refreshTokenCount = user.refreshTokenCount + 1;
|
||||
}
|
||||
|
||||
@@ -117,7 +124,7 @@ export class UpdateUser {
|
||||
user.phone = this.phone;
|
||||
user.groups = await this.getGroups();
|
||||
|
||||
if (!this.profilePic) { user.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; }
|
||||
if (!this.profilePic) { user.profilePic = `https://lauf-fuer-kaya.de/lfk-logo.png`; }
|
||||
else { user.profilePic = this.profilePic; }
|
||||
|
||||
return user;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { IsInt, IsNotEmpty, IsPositive } from "class-validator";
|
||||
import { ChildEntity, Column, ManyToOne } from "typeorm";
|
||||
import { ChildEntity, Column, Index, ManyToOne } from "typeorm";
|
||||
import { ResponseDistanceDonation } from '../responses/ResponseDistanceDonation';
|
||||
import { Donation } from "./Donation";
|
||||
import { Runner } from "./Runner";
|
||||
import type { Runner } from "./Runner";
|
||||
|
||||
/**
|
||||
* Defines the DistanceDonation entity.
|
||||
* For distanceDonations a donor pledges to donate a certain amount for each kilometer ran by a runner.
|
||||
*/
|
||||
*/
|
||||
@ChildEntity()
|
||||
@Index(['runner'])
|
||||
export class DistanceDonation extends Donation {
|
||||
/**
|
||||
* The donation's associated runner.
|
||||
* Used as the source of the donation's distance.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@ManyToOne(() => Runner, runner => runner.distanceDonations)
|
||||
runner: Runner;
|
||||
@ManyToOne(() => require("./Runner").Runner, (runner: Runner) => runner.distanceDonations)
|
||||
runner!: Runner;
|
||||
|
||||
/**
|
||||
* The donation's amount donated per distance.
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import {
|
||||
IsInt,
|
||||
IsNotEmpty
|
||||
IsPositive
|
||||
} from "class-validator";
|
||||
import { Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||
import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||
import { ResponseDonation } from '../responses/ResponseDonation';
|
||||
import { Donor } from './Donor';
|
||||
import type { Donor } from './Donor';
|
||||
|
||||
/**
|
||||
* Defines the Donation entity.
|
||||
* A donation just associates a donor with a donation amount.
|
||||
* The specifics of the amoun's determination has to be implemented in child classes.
|
||||
*/
|
||||
*/
|
||||
@Entity()
|
||||
@TableInheritance({ column: { name: "type", type: "varchar" } })
|
||||
@Index(['donor'])
|
||||
export abstract class Donation {
|
||||
/**
|
||||
* Autogenerated unique id (primary key).
|
||||
@@ -24,9 +25,8 @@ export abstract class Donation {
|
||||
/**
|
||||
* The donations's donor.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@ManyToOne(() => Donor, donor => donor.donations)
|
||||
donor: Donor;
|
||||
@ManyToOne(() => require("./Donor").Donor, (donor: Donor) => donor.donations)
|
||||
donor!: Donor;
|
||||
|
||||
/**
|
||||
* The donation's amount in cents (or whatever your currency's smallest unit is.).
|
||||
@@ -34,6 +34,34 @@ export abstract class Donation {
|
||||
*/
|
||||
public abstract get amount(): number;
|
||||
|
||||
/**
|
||||
* The donation's paid amount in cents (or whatever your currency's smallest unit is.).
|
||||
* Used to mark donations as paid.
|
||||
*/
|
||||
@Column({ nullable: true })
|
||||
@IsInt()
|
||||
paidAmount: number;
|
||||
|
||||
@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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IsBoolean, IsInt } from "class-validator";
|
||||
import { ChildEntity, Column, OneToMany } from "typeorm";
|
||||
import { ResponseDonor } from '../responses/ResponseDonor';
|
||||
import { Donation } from './Donation';
|
||||
import type { Donation } from './Donation';
|
||||
import { Participant } from "./Participant";
|
||||
|
||||
/**
|
||||
@@ -21,8 +21,8 @@ export class Donor extends Participant {
|
||||
* Used to link the participant as the donor of a donation.
|
||||
* Attention: Only runner's can be associated as a distanceDonations distance source.
|
||||
*/
|
||||
@OneToMany(() => Donation, donation => donation.donor, { nullable: true })
|
||||
donations: Donation[];
|
||||
@OneToMany(() => require("./Donation").Donation, (donation: Donation) => donation.donor, { nullable: true })
|
||||
donations!: Donation[];
|
||||
|
||||
/**
|
||||
* Returns the total donations of a donor based on his linked donations.
|
||||
@@ -33,6 +33,15 @@ export class Donor extends Participant {
|
||||
return this.donations.reduce((sum, current) => sum + current.amount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total paid donations of a donor based on his linked donations.
|
||||
*/
|
||||
@IsInt()
|
||||
public get paidDonationAmount(): number {
|
||||
if (!this.donations) { return 0; }
|
||||
return this.donations.reduce((sum, current) => sum + current.paidAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns this entity into it's response class.
|
||||
*/
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import {
|
||||
IsEmail,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsPhoneNumber,
|
||||
|
||||
IsString
|
||||
} from "class-validator";
|
||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { config } from '../../config';
|
||||
import { ResponseGroupContact } from '../responses/ResponseGroupContact';
|
||||
import { Address } from "./Address";
|
||||
import { RunnerGroup } from "./RunnerGroup";
|
||||
import {
|
||||
IsEmail,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsPhoneNumber,
|
||||
|
||||
IsPositive,
|
||||
|
||||
IsString
|
||||
} from "class-validator";
|
||||
import { BeforeInsert, BeforeUpdate, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { config } from '../../config';
|
||||
import { ResponseGroupContact } from '../responses/ResponseGroupContact';
|
||||
import { Address } from "./Address";
|
||||
import type { RunnerGroup } from "./RunnerGroup";
|
||||
|
||||
/**
|
||||
* Defines the GroupContact entity.
|
||||
@@ -75,11 +77,32 @@ export class GroupContact {
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* Used to link contacts to groups.
|
||||
*/
|
||||
@OneToMany(() => RunnerGroup, group => group.contact, { nullable: true })
|
||||
groups: RunnerGroup[];
|
||||
/**
|
||||
* Used to link contacts to groups.
|
||||
*/
|
||||
@OneToMany(() => require("./RunnerGroup").RunnerGroup, (group: RunnerGroup) => 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.
|
||||
|
||||
@@ -5,20 +5,23 @@ import {
|
||||
IsOptional,
|
||||
IsPhoneNumber,
|
||||
|
||||
IsPositive,
|
||||
|
||||
IsString
|
||||
} from "class-validator";
|
||||
import { Column, Entity, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||
import { BeforeInsert, BeforeUpdate, Column, Entity, Index, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||
import { config } from '../../config';
|
||||
import { ResponseParticipant } from '../responses/ResponseParticipant';
|
||||
import { Address } from "./Address";
|
||||
|
||||
/**
|
||||
* Defines the Participant entity.
|
||||
* Participans can donate and therefor be associated with donation entities.
|
||||
*/
|
||||
@Entity()
|
||||
@TableInheritance({ column: { name: "type", type: "varchar" } })
|
||||
export abstract class Participant {
|
||||
/**
|
||||
* Defines the Participant entity.
|
||||
* Participans can donate and therefor be associated with donation entities.
|
||||
*/
|
||||
@Entity()
|
||||
@TableInheritance({ column: { name: "type", type: "varchar" } })
|
||||
@Index(['email'])
|
||||
export abstract class Participant {
|
||||
/**
|
||||
* Autogenerated unique id (primary key).
|
||||
*/
|
||||
@@ -75,6 +78,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.
|
||||
*/
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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';
|
||||
import { Principal } from './Principal';
|
||||
import type { Principal } from './Principal';
|
||||
/**
|
||||
* Defines the Permission entity.
|
||||
* Permissions can be granted to principals.
|
||||
@@ -25,8 +26,8 @@ export class Permission {
|
||||
/**
|
||||
* The permission's principal.
|
||||
*/
|
||||
@ManyToOne(() => Principal, principal => principal.permissions)
|
||||
principal: Principal;
|
||||
@ManyToOne(() => require("./Principal").Principal, (principal: Principal) => principal.permissions)
|
||||
principal!: Principal;
|
||||
|
||||
/**
|
||||
* The permission's target.
|
||||
@@ -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).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
import type { Permission } from './Permission';
|
||||
|
||||
/**
|
||||
* Defines the principal entity.
|
||||
@@ -20,8 +20,29 @@ export abstract class Principal {
|
||||
/**
|
||||
* The participant's permissions.
|
||||
*/
|
||||
@OneToMany(() => Permission, permission => permission.principal, { nullable: true })
|
||||
permissions: Permission[];
|
||||
@OneToMany(() => require("./Permission").Permission, (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.
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { IsInt, IsNotEmpty } from "class-validator";
|
||||
import { ChildEntity, ManyToOne, OneToMany } from "typeorm";
|
||||
import { IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||
import { ChildEntity, Column, Index, ManyToOne, OneToMany } from "typeorm";
|
||||
import { ResponseRunner } from '../responses/ResponseRunner';
|
||||
import { DistanceDonation } from "./DistanceDonation";
|
||||
import type { DistanceDonation } from "./DistanceDonation";
|
||||
import { Participant } from "./Participant";
|
||||
import { RunnerCard } from "./RunnerCard";
|
||||
import type { RunnerCard } from "./RunnerCard";
|
||||
import { RunnerGroup } from "./RunnerGroup";
|
||||
import { Scan } from "./Scan";
|
||||
import type { Scan } from "./Scan";
|
||||
|
||||
/**
|
||||
* Defines the runner entity.
|
||||
* Runners differ from participants in being able to actually accumulate a ran distance through scans.
|
||||
* Runner's get organized in groups.
|
||||
*/
|
||||
*/
|
||||
@ChildEntity()
|
||||
@Index(['group'])
|
||||
export class Runner extends Participant {
|
||||
/**
|
||||
* The runner's associated group.
|
||||
@@ -26,29 +27,41 @@ export class Runner extends Participant {
|
||||
* The runner's associated distanceDonations.
|
||||
* Used to link runners to distanceDonations in order to calculate the donation's amount based on the distance the runner ran.
|
||||
*/
|
||||
@OneToMany(() => DistanceDonation, distanceDonation => distanceDonation.runner, { nullable: true })
|
||||
distanceDonations: DistanceDonation[];
|
||||
@OneToMany(() => require("./DistanceDonation").DistanceDonation, (distanceDonation: DistanceDonation) => distanceDonation.runner, { nullable: true })
|
||||
distanceDonations!: DistanceDonation[];
|
||||
|
||||
/**
|
||||
* The runner's associated cards.
|
||||
* Used to link runners to cards - yes a runner be associated with multiple cards this came in handy in the past.
|
||||
*/
|
||||
@OneToMany(() => RunnerCard, card => card.runner, { nullable: true })
|
||||
cards: RunnerCard[];
|
||||
@OneToMany(() => require("./RunnerCard").RunnerCard, (card: RunnerCard) => card.runner, { nullable: true })
|
||||
cards!: RunnerCard[];
|
||||
|
||||
/**
|
||||
* The runner's associated scans.
|
||||
* Used to link runners to scans (valid and fraudulant).
|
||||
*/
|
||||
@OneToMany(() => Scan, scan => scan.runner, { nullable: true })
|
||||
scans: Scan[];
|
||||
@OneToMany(() => require("./Scan").Scan, (scan: Scan) => scan.runner, { nullable: true })
|
||||
scans!: Scan[];
|
||||
|
||||
/**
|
||||
* The last time the runner requested a selfservice link.
|
||||
* Used to prevent spamming of the selfservice link forgotten route.
|
||||
*/
|
||||
@Column({ nullable: true, unique: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
resetRequestedTimestamp?: number;
|
||||
|
||||
/**
|
||||
* Returns all valid scans associated with this runner.
|
||||
* 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 []
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,6 +85,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);
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,23 @@ import {
|
||||
|
||||
IsInt,
|
||||
|
||||
IsOptional
|
||||
IsOptional,
|
||||
IsPositive
|
||||
} from "class-validator";
|
||||
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { RunnerCardIdOutOfRangeError } from '../../errors/RunnerCardErrors';
|
||||
import { ResponseRunnerCard } from '../responses/ResponseRunnerCard';
|
||||
import { Runner } from "./Runner";
|
||||
import { TrackScan } from "./TrackScan";
|
||||
import type { Runner } from "./Runner";
|
||||
import type { TrackScan } from "./TrackScan";
|
||||
|
||||
/**
|
||||
* Defines the RunnerCard entity.
|
||||
* A runnerCard is a physical representation for a runner.
|
||||
* It can be associated with a runner to create scans via the scan station's.
|
||||
*/
|
||||
*/
|
||||
@Entity()
|
||||
@Index(['runner'])
|
||||
@Index(['enabled'])
|
||||
export class RunnerCard {
|
||||
/**
|
||||
* Autogenerated unique id (primary key).
|
||||
@@ -30,8 +33,8 @@ export class RunnerCard {
|
||||
* To increase reusability a card can be reassigned.
|
||||
*/
|
||||
@IsOptional()
|
||||
@ManyToOne(() => Runner, runner => runner.cards, { nullable: true })
|
||||
runner: Runner;
|
||||
@ManyToOne(() => require("./Runner").Runner, (runner: Runner) => runner.cards, { nullable: true })
|
||||
runner!: Runner;
|
||||
|
||||
/**
|
||||
* Is the card enabled (for fraud reasons)?
|
||||
@@ -45,20 +48,35 @@ export class RunnerCard {
|
||||
* The card's associated scans.
|
||||
* Used to link cards to track scans.
|
||||
*/
|
||||
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
||||
scans: TrackScan[];
|
||||
@OneToMany(() => require("./TrackScan").TrackScan, (scan: TrackScan) => scan.card, { 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 +85,11 @@ export class RunnerCard {
|
||||
private get paddedId(): string {
|
||||
let id: string = this.id.toString();
|
||||
|
||||
if (id.length > 12) {
|
||||
if (id.length > 11) {
|
||||
throw new RunnerCardIdOutOfRangeError();
|
||||
}
|
||||
while (id.length < 12) { id = '0' + id; }
|
||||
while (id.length < 11) { id = '0' + id; }
|
||||
id = '2' + id;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ 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";
|
||||
import type { Runner } from "./Runner";
|
||||
|
||||
/**
|
||||
* Defines the RunnerGroup entity.
|
||||
@@ -43,14 +44,38 @@ export abstract class RunnerGroup {
|
||||
* The group's associated runners.
|
||||
* Used to link runners to a runner group.
|
||||
*/
|
||||
@OneToMany(() => Runner, runner => runner.group, { nullable: true })
|
||||
runners: Runner[];
|
||||
@OneToMany(() => require("./Runner").Runner, (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.
|
||||
*/
|
||||
@IsInt()
|
||||
public get distance(): number {
|
||||
if (!this.runners || this.runners.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
return this.runners.reduce((sum, current) => sum + current.distance, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ResponseRunnerOrganization } from '../responses/ResponseRunnerOrganizat
|
||||
import { Address } from './Address';
|
||||
import { Runner } from './Runner';
|
||||
import { RunnerGroup } from "./RunnerGroup";
|
||||
import { RunnerTeam } from "./RunnerTeam";
|
||||
import type { RunnerTeam } from "./RunnerTeam";
|
||||
|
||||
/**
|
||||
* Defines the RunnerOrganization entity.
|
||||
@@ -24,8 +24,8 @@ export class RunnerOrganization extends RunnerGroup {
|
||||
* The organization's teams.
|
||||
* Used to link teams to a organization.
|
||||
*/
|
||||
@OneToMany(() => RunnerTeam, team => team.parentGroup, { nullable: true })
|
||||
teams: RunnerTeam[];
|
||||
@OneToMany(() => require("./RunnerTeam").RunnerTeam, (team: RunnerTeam) => team.parentGroup, { nullable: true })
|
||||
teams!: RunnerTeam[];
|
||||
|
||||
/**
|
||||
* The organization's api key for self-service registration.
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ChildEntity, ManyToOne } from "typeorm";
|
||||
import { ChildEntity, Index, ManyToOne } from "typeorm";
|
||||
import { ResponseRunnerTeam } from '../responses/ResponseRunnerTeam';
|
||||
import { RunnerGroup } from "./RunnerGroup";
|
||||
import { RunnerOrganization } from "./RunnerOrganization";
|
||||
import type { RunnerOrganization } from "./RunnerOrganization";
|
||||
|
||||
/**
|
||||
* Defines the RunnerTeam entity.
|
||||
* This usually is a school class or department in a company.
|
||||
*/
|
||||
*/
|
||||
@ChildEntity()
|
||||
@Index(['parentGroup'])
|
||||
export class RunnerTeam extends RunnerGroup {
|
||||
|
||||
/**
|
||||
@@ -16,7 +17,7 @@ export class RunnerTeam extends RunnerGroup {
|
||||
* Every team has to be part of a runnerOrganization - this get's checked on creation and update.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@ManyToOne(() => RunnerOrganization, org => org.teams, { nullable: true })
|
||||
@ManyToOne(() => require("./RunnerOrganization").RunnerOrganization, (org: RunnerOrganization) => org.teams, { nullable: true })
|
||||
parentGroup?: RunnerOrganization;
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,16 +5,19 @@ import {
|
||||
|
||||
IsPositive
|
||||
} from "class-validator";
|
||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||
import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||
import { ResponseScan } from '../responses/ResponseScan';
|
||||
import { Runner } from "./Runner";
|
||||
import type { Runner } from "./Runner";
|
||||
|
||||
/**
|
||||
* Defines the Scan entity.
|
||||
* A scan basicly adds a certain distance to a runner's total ran distance.
|
||||
*/
|
||||
*/
|
||||
@Entity()
|
||||
@TableInheritance({ column: { name: "type", type: "varchar" } })
|
||||
@Index(['runner'])
|
||||
@Index(['runner', 'created_at'])
|
||||
@Index(['valid'])
|
||||
export class Scan {
|
||||
/**
|
||||
* Autogenerated unique id (primary key).
|
||||
@@ -28,8 +31,8 @@ export class Scan {
|
||||
* This is important to link ran distances to runners.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@ManyToOne(() => Runner, runner => runner.scans, { nullable: false })
|
||||
runner: Runner;
|
||||
@ManyToOne(() => require("./Runner").Runner, (runner: Runner) => runner.scans, { nullable: false })
|
||||
runner!: Runner;
|
||||
|
||||
/**
|
||||
* Is the scan valid (for fraud reasons).
|
||||
@@ -40,6 +43,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..
|
||||
|
||||
@@ -3,18 +3,22 @@ import {
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsPositive,
|
||||
IsString
|
||||
} from "class-validator";
|
||||
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { ResponseScanStation } from '../responses/ResponseScanStation';
|
||||
import { Track } from "./Track";
|
||||
import { TrackScan } from "./TrackScan";
|
||||
import type { Track } from "./Track";
|
||||
import type { TrackScan } from "./TrackScan";
|
||||
|
||||
/**
|
||||
* Defines the ScanStation entity.
|
||||
* ScanStations get used to create TrackScans for runners based on a scan of their runnerCard.
|
||||
*/
|
||||
*/
|
||||
@Entity()
|
||||
@Index(['track'])
|
||||
@Index(['prefix'])
|
||||
@Index(['enabled'])
|
||||
export class ScanStation {
|
||||
/**
|
||||
* Autogenerated unique id (primary key).
|
||||
@@ -37,8 +41,8 @@ export class ScanStation {
|
||||
* All scans created by this station will also be associated with this track.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@ManyToOne(() => Track, track => track.stations, { nullable: false })
|
||||
track: Track;
|
||||
@ManyToOne(() => require("./Track").Track, (track: Track) => track.stations, { nullable: false })
|
||||
track!: Track;
|
||||
|
||||
/**
|
||||
* The client's api key prefix.
|
||||
@@ -68,8 +72,8 @@ export class ScanStation {
|
||||
/**
|
||||
* Used to link track scans to a scan station.
|
||||
*/
|
||||
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
||||
scans: TrackScan[];
|
||||
@OneToMany(() => require("./TrackScan").TrackScan, (scan: TrackScan) => scan.station, { nullable: true })
|
||||
scans!: TrackScan[];
|
||||
|
||||
/**
|
||||
* Is this station enabled?
|
||||
@@ -78,6 +82,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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -5,10 +5,10 @@ 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";
|
||||
import type { ScanStation } from "./ScanStation";
|
||||
import type { TrackScan } from "./TrackScan";
|
||||
|
||||
/**
|
||||
* Defines the Track entity.
|
||||
@@ -53,15 +53,36 @@ export class Track {
|
||||
* Used to link scan stations to a certain track.
|
||||
* This makes the configuration of the scan stations easier.
|
||||
*/
|
||||
@OneToMany(() => ScanStation, station => station.track, { nullable: true })
|
||||
stations: ScanStation[];
|
||||
@OneToMany(() => require("./ScanStation").ScanStation, (station: ScanStation) => station.track, { nullable: true })
|
||||
stations!: ScanStation[];
|
||||
|
||||
/**
|
||||
* Used to link track scans to a track.
|
||||
* The scan will derive it's distance from the track's distance.
|
||||
*/
|
||||
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
||||
scans: TrackScan[];
|
||||
@OneToMany(() => require("./TrackScan").TrackScan, (scan: TrackScan) => 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.
|
||||
|
||||
@@ -2,44 +2,51 @@ import {
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
|
||||
IsNumber,
|
||||
|
||||
IsPositive
|
||||
} from "class-validator";
|
||||
import { ChildEntity, Column, ManyToOne } from "typeorm";
|
||||
import { ChildEntity, Column, Index, ManyToOne } from "typeorm";
|
||||
import { ResponseTrackScan } from '../responses/ResponseTrackScan';
|
||||
import { RunnerCard } from "./RunnerCard";
|
||||
import type { RunnerCard } from "./RunnerCard";
|
||||
import { Scan } from "./Scan";
|
||||
import { ScanStation } from "./ScanStation";
|
||||
import { Track } from "./Track";
|
||||
import type { ScanStation } from "./ScanStation";
|
||||
import type { Track } from "./Track";
|
||||
|
||||
/**
|
||||
* Defines the TrackScan entity.
|
||||
* A track scan usaually get's generated by a scan station.
|
||||
*/
|
||||
*/
|
||||
@ChildEntity()
|
||||
@Index(['track'])
|
||||
@Index(['card'])
|
||||
@Index(['station'])
|
||||
@Index(['timestamp'])
|
||||
@Index(['station', 'timestamp'])
|
||||
export class TrackScan extends Scan {
|
||||
/**
|
||||
* The scan's associated track.
|
||||
* This is used to determine the scan's distance.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@ManyToOne(() => Track, track => track.scans, { nullable: true })
|
||||
track: Track;
|
||||
@ManyToOne(() => require("./Track").Track, (track: Track) => track.scans, { nullable: true })
|
||||
track!: Track;
|
||||
|
||||
/**
|
||||
* The runnerCard associated with the scan.
|
||||
* This get's saved for documentation and management purposes.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@ManyToOne(() => RunnerCard, card => card.scans, { nullable: true })
|
||||
card: RunnerCard;
|
||||
@ManyToOne(() => require("./RunnerCard").RunnerCard, (card: RunnerCard) => card.scans, { nullable: true })
|
||||
card!: RunnerCard;
|
||||
|
||||
/**
|
||||
* The scanning station that created the scan.
|
||||
* Mainly used for logging and traceing back scans (or errors)
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@ManyToOne(() => ScanStation, station => station.scans, { nullable: true })
|
||||
station: ScanStation;
|
||||
@ManyToOne(() => require("./ScanStation").ScanStation, (station: ScanStation) => station.scans, { nullable: true })
|
||||
station!: ScanStation;
|
||||
|
||||
/**
|
||||
* The scan's distance in meters.
|
||||
@@ -59,6 +66,14 @@ export class TrackScan extends Scan {
|
||||
@IsInt()
|
||||
timestamp: number;
|
||||
|
||||
/**
|
||||
* The scan's lap time.
|
||||
* This simply get's calculated from the last lap time;
|
||||
*/
|
||||
@Column()
|
||||
@IsNumber()
|
||||
lapTime: number;
|
||||
|
||||
/**
|
||||
* Turns this entity into it's response class.
|
||||
*/
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl, IsUUID } from "class-validator";
|
||||
import { ChildEntity, Column, JoinTable, ManyToMany, OneToMany } from "typeorm";
|
||||
import { config } from '../../config';
|
||||
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
|
||||
import { ResponseUser } from '../responses/ResponseUser';
|
||||
import { Permission } from './Permission';
|
||||
import { Principal } from './Principal';
|
||||
import { UserAction } from './UserAction';
|
||||
import { UserGroup } from './UserGroup';
|
||||
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl, IsUUID } from "class-validator";
|
||||
import { ChildEntity, Column, Index, JoinTable, ManyToMany, OneToMany } from "typeorm";
|
||||
import { config } from '../../config';
|
||||
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
|
||||
import { ResponseUser } from '../responses/ResponseUser';
|
||||
import { Permission } from './Permission';
|
||||
import { Principal } from './Principal';
|
||||
import type { UserAction } from './UserAction';
|
||||
import { UserGroup } from './UserGroup';
|
||||
|
||||
/**
|
||||
* Defines the User entity.
|
||||
* Users are the ones that can use the "admin" webui and do stuff in the backend.
|
||||
*/
|
||||
@ChildEntity()
|
||||
export class User extends Principal {
|
||||
/**
|
||||
* Defines the User entity.
|
||||
* Users are the ones that can use the "admin" webui and do stuff in the backend.
|
||||
*/
|
||||
@ChildEntity()
|
||||
@Index(['enabled'])
|
||||
export class User extends Principal {
|
||||
/**
|
||||
* The user's uuid.
|
||||
* Mainly gets used as a per-user salt for the password hash.
|
||||
@@ -124,11 +125,11 @@ export class User extends Principal {
|
||||
|
||||
/**
|
||||
* The actions performed by this user.
|
||||
* For documentation purposes only, will be implemented later.
|
||||
*/
|
||||
@IsOptional()
|
||||
@OneToMany(() => UserAction, action => action.user, { nullable: true })
|
||||
actions: UserAction[]
|
||||
* For documentation purposes only, will be implemented later.
|
||||
*/
|
||||
@IsOptional()
|
||||
@OneToMany(() => require("./UserAction").UserAction, (action: UserAction) => action.user, { nullable: true })
|
||||
actions!: UserAction[]
|
||||
|
||||
/**
|
||||
* Resolves all permissions granted to this user through groups.
|
||||
|
||||
@@ -3,11 +3,12 @@ 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';
|
||||
import type { User } from './User';
|
||||
|
||||
/**
|
||||
* Defines the UserAction entity.
|
||||
@@ -25,8 +26,8 @@ export class UserAction {
|
||||
/**
|
||||
* The user that performed the action.
|
||||
*/
|
||||
@ManyToOne(() => User, user => user.actions)
|
||||
user: User
|
||||
@ManyToOne(() => require("./User").User, (user: User) => user.actions)
|
||||
user!: User
|
||||
|
||||
/**
|
||||
* The actions's target (e.g. Track#2)
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
IsString
|
||||
} from "class-validator";
|
||||
import { ChildEntity, Column } from "typeorm";
|
||||
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
|
||||
import { ResponseUserGroup } from '../responses/ResponseUserGroup';
|
||||
import { Principal } from './Principal';
|
||||
|
||||
@@ -34,7 +33,7 @@ export class UserGroup extends Principal {
|
||||
/**
|
||||
* Turns this entity into it's response class.
|
||||
*/
|
||||
public toResponse(): ResponsePrincipal {
|
||||
public toResponse(): ResponseUserGroup {
|
||||
return new ResponseUserGroup(this);
|
||||
}
|
||||
}
|
||||
35
src/models/entities/index.ts
Normal file
35
src/models/entities/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Entity barrel file for Bun compatibility.
|
||||
* Imports all entities in the correct order to resolve circular dependencies.
|
||||
*/
|
||||
|
||||
// Base/parent entities first
|
||||
export * from './Participant';
|
||||
export * from './Donation';
|
||||
export * from './Scan';
|
||||
|
||||
// Child entities that depend on the above
|
||||
export * from './Runner';
|
||||
export * from './DistanceDonation';
|
||||
export * from './FixedDonation';
|
||||
export * from './TrackScan';
|
||||
|
||||
// Entities with cross-references
|
||||
export * from './RunnerCard';
|
||||
export * from './RunnerGroup';
|
||||
export * from './RunnerOrganization';
|
||||
export * from './RunnerTeam';
|
||||
export * from './ScanStation';
|
||||
export * from './Track';
|
||||
|
||||
// Independent entities
|
||||
export * from './Address';
|
||||
export * from './ConfigFlags';
|
||||
export * from './Donor';
|
||||
export * from './GroupContact';
|
||||
export * from './Permission';
|
||||
export * from './Principal';
|
||||
export * from './StatsClient';
|
||||
export * from './User';
|
||||
export * from './UserAction';
|
||||
export * from './UserGroup';
|
||||
7
src/models/enums/DonationStatus.ts
Normal file
7
src/models/enums/DonationStatus.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* This enum contains all status a donation can inherit regarding it's payment status.
|
||||
*/
|
||||
export enum DonationStatus {
|
||||
OPEN = 'OPEN',
|
||||
PAID = 'PAID'
|
||||
}
|
||||
39
src/models/enums/ResponseObjectType.ts
Normal file
39
src/models/enums/ResponseObjectType.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* This enum contains all object types/entities a response can contain.
|
||||
*/
|
||||
export enum ResponseObjectType {
|
||||
AUTH = 'AUTH',
|
||||
DISTANCEDONATION = 'DISTANCEDONATION',
|
||||
DONATION = 'DONATION',
|
||||
DONOR = 'DONOR',
|
||||
EMPTY = 'EMPTY',
|
||||
GROUPCONTACT = 'GROUPCONTACT',
|
||||
LOGOUT = 'LOGOUT',
|
||||
PARTICIPANT = 'PARTICIPANT',
|
||||
PERMISSION = 'PERMISSION',
|
||||
PRINCIPAL = 'PRINCIPAL',
|
||||
RUNNER = 'RUNNER',
|
||||
RUNNERCARD = 'RUNNERCARD',
|
||||
RUNNERGROUP = 'RUNNERGROUP',
|
||||
RUNNERORGANIZATION = 'RUNNERORGANIZATION',
|
||||
RUNNERTEAM = 'RUNNERTEAM',
|
||||
SCAN = 'SCAN',
|
||||
SCANSTATION = 'SCANSTATION',
|
||||
SELFSERVICEDONATION = 'SELFSERVICEDONATION',
|
||||
SELFSERVICERUNNER = 'SELFSERVICRUNNER',
|
||||
SELFSERVICESCAN = 'SELFSERVICESCAN',
|
||||
SELFSERVICETRACKSCAN = 'SELFSERVICETRACKSCAN',
|
||||
SELFSERVICETEAM = 'SELFSERVICETEAM',
|
||||
SELFSERVICEORGANIZATION = 'SELFSERVICEORGANIZATION',
|
||||
STATS = 'STATS',
|
||||
STATSCLIENT = 'STATSCLIENT',
|
||||
STATSORGANIZATION = 'STATSORGANIZATION',
|
||||
STATSRUNNER = 'STATSRUNNER',
|
||||
STATSTEAM = 'STATSTEAM',
|
||||
TRACK = 'TRACK',
|
||||
TRACKSCAN = 'TRACKSCAN',
|
||||
USER = 'USER',
|
||||
USERGROUP = 'USERGROUP',
|
||||
USERPERMISSIONS = 'USERPERMISSIONS',
|
||||
SELFSERVICEDONOR = 'SELFSERVICEDONOR'
|
||||
}
|
||||
13
src/models/responses/IResponse.ts
Normal file
13
src/models/responses/IResponse.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ResponseObjectType } from '../enums/ResponseObjectType';
|
||||
|
||||
/**
|
||||
* Defines the repsonse interface.
|
||||
* This forces all response classes to implement the interfaces properties.
|
||||
*/
|
||||
export interface IResponse {
|
||||
/**
|
||||
* The responseType.
|
||||
* This contains the type of class/entity this response contains.
|
||||
*/
|
||||
responseType: ResponseObjectType;
|
||||
}
|
||||
68
src/models/responses/ResponseAnonymousDonation.ts
Normal file
68
src/models/responses/ResponseAnonymousDonation.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
import { IsInt, IsString } from 'class-validator';
|
||||
import { ResponseObjectType } from '../enums/ResponseObjectType';
|
||||
import { IResponse } from './IResponse';
|
||||
|
||||
/**
|
||||
* Defines the repsonse auth.
|
||||
*/
|
||||
export class ResponseAuth {
|
||||
export class ResponseAuth implements IResponse {
|
||||
|
||||
/**
|
||||
* The responseType.
|
||||
* This contains the type of class/entity this response contains.
|
||||
*/
|
||||
responseType: ResponseObjectType = ResponseObjectType.AUTH;
|
||||
|
||||
/**
|
||||
* The access_token - JWT shortterm access token.
|
||||
*/
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { IsInt, IsObject, IsPositive } from 'class-validator';
|
||||
import { DistanceDonation } from '../entities/DistanceDonation';
|
||||
import { ResponseObjectType } from '../enums/ResponseObjectType';
|
||||
import { IResponse } from './IResponse';
|
||||
import { ResponseDonation } from './ResponseDonation';
|
||||
import { ResponseRunner } from './ResponseRunner';
|
||||
|
||||
/**
|
||||
* Defines the distance donation response.
|
||||
*/
|
||||
export class ResponseDistanceDonation extends ResponseDonation {
|
||||
export class ResponseDistanceDonation extends ResponseDonation implements IResponse {
|
||||
/**
|
||||
* The responseType.
|
||||
* This contains the type of class/entity this response contains.
|
||||
*/
|
||||
responseType: ResponseObjectType = ResponseObjectType.DISTANCEDONATION;
|
||||
|
||||
/**
|
||||
* The donation's associated runner.
|
||||
@@ -29,7 +36,7 @@ export class ResponseDistanceDonation extends ResponseDonation {
|
||||
*/
|
||||
public constructor(donation: DistanceDonation) {
|
||||
super(donation);
|
||||
this.runner = donation.runner.toResponse();
|
||||
if (donation.runner) { this.runner = donation.runner.toResponse(); }
|
||||
this.amountPerDistance = donation.amountPerDistance;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user