Compare commits
841 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 | |||
| c4ea808e06 | |||
| ff7406e71a | |||
| 8dc2810c0c | |||
| ff8af090e3 | |||
| bcc15e4286 | |||
| 2a87819486 | |||
| 9d5e486c6d | |||
| e44cc4c4cb | |||
| 581ca5ff6c | |||
| b972395ae8 | |||
| e5f4f6ee59 | |||
| fea4857685 | |||
| f9e75d06b8 | |||
| 38223b194b | |||
| 09b24aa609 | |||
| 348e6cdec7 | |||
| bd1813a0e8 | |||
| e07f258a31 | |||
| 61bbeb0d8f | |||
| 650a55e586 | |||
| 2071c4db33 | |||
| 80e606aa96 | |||
| 20f960ed67 | |||
| e6fe8fcd58 | |||
| 870fd47c83 | |||
| 644045db44 | |||
| 8611fcb849 | |||
| 08e6e59655 | |||
| ae74b3963f | |||
| 54ed313342 | |||
| ad4b903c25 | |||
| 9bd7636a23 | |||
| b94179e3ca | |||
| 827002989e | |||
| eeff67c192 | |||
| 583a4bc0dd | |||
| 53fcff77d0 | |||
| 1f0c842d9e | |||
| 13ccab5e28 | |||
| b5018eb114 | |||
| aedfcfcc83 | |||
| db0876015b | |||
| 69417e93c0 | |||
| f71a22f4dd | |||
| 570c34bed0 | |||
| 7be2971a9e | |||
| b92f633d68 | |||
| d3647e3399 | |||
| 389e423850 | |||
| 46af786516 | |||
| b4c117b7dc | |||
| 5cade25eeb | |||
| fb77f4d798 | |||
| c116338cd7 | |||
| 979d36ea91 | |||
| c43334bf96 | |||
| 71c4caae8b | |||
| 536de2a319 | |||
| e26744b792 | |||
| d02e9dec56 | |||
| 637975305f | |||
| c418603423 | |||
| 78d2ac3027 | |||
| 470703c4de | |||
| e260e16d66 | |||
| 6b0155f014 | |||
| 33890b544b | |||
| d7ea928714 | |||
| 908ac4f1ce | |||
| cf012c0b7e | |||
| 71898d576c | |||
| c964591839 | |||
| cc4bf4451c | |||
| 7dbbd3780d | |||
| 3697783e19 | |||
| 161feaf364 | |||
| 75e2a44c9c | |||
| cd7e9b86b4 | |||
| c6c643ecf1 | |||
| ef15d0d576 | |||
| 5660aecb50 | |||
| 6a66dd803b | |||
| b42f0722d7 | |||
| 45c8bb83be | |||
| 6469e3bc97 | |||
| 10f98e9c99 | |||
| e5b6f650b2 | |||
| 3b2ed3f0f2 | |||
| 20e102ec5c | |||
| 5a003945ac | |||
| 29aeb046de | |||
| 72941da1cb | |||
| 81d2197a3e | |||
| 9dd9304a71 | |||
| 0c87906cc3 | |||
| 1227408407 | |||
| f8d7544517 | |||
| a9843ed459 | |||
| 46f9503543 | |||
| c5d0646c42 | |||
| b441658570 | |||
| e95c457e44 | |||
| 6de9d547b7 | |||
| 3a93c9c078 | |||
| 36d01a0a89 | |||
| 6434b4dfce | |||
| e964a8ed44 | |||
| c39a59e54e | |||
| 34c852b12a | |||
| 7b00b19fce | |||
| ad446500f9 | |||
| d490247d1e | |||
| dee36395a6 | |||
| 6df195b6ec | |||
| 946efef252 | |||
| 73b1114883 | |||
| 1b5465bea8 | |||
| 5288c701c1 | |||
| 10af1ba341 | |||
| 26dff4f418 | |||
| b5f3dec93b | |||
| a82fc0fb9e | |||
| e2ec0a3b64 | |||
| f4668b6e81 | |||
| d5281348b6 | |||
| 1717df113e | |||
| 0355bdbbab | |||
| 02677de5c0 | |||
| 886c1092d6 | |||
| 191569792c | |||
| da1fe34249 | |||
| 4ee807973e | |||
| c5f7cb2c68 | |||
| 88a7089289 | |||
| b89f7ac1b4 | |||
| 8079769881 | |||
| 2274b476d6 | |||
| e12aedd1aa | |||
| 434aaf6136 | |||
| d8b6669d12 | |||
| dd3d93edc7 | |||
| 7bc603028d | |||
| c18012f65a | |||
| b15967ff31 | |||
| 2db6510a8a | |||
| 1837336865 | |||
| eab0e634a2 | |||
| 8870ebdb5e | |||
| 9df9d9ae80 | |||
| 67ba489fe2 | |||
| da9a359251 | |||
| 0661729e5f | |||
| ddafd90d3e | |||
| 8960aa5545 | |||
| a0c2b5ade8 | |||
| a1acd3519f | |||
| c3d008ec0f | |||
| 8ae53f1c49 | |||
| 179c2a5157 | |||
| dd7e5dae36 | |||
| e165f01930 | |||
| 940d62cde4 | |||
| b002cf2df1 | |||
| 56c73c2555 | |||
| 28fb9834e1 | |||
| 6b4b16c13b | |||
| d743f7ee12 | |||
| a4e8311cbd | |||
| c172aa8bf8 | |||
| d1926fe372 | |||
| 2b658ac381 | |||
| 321d291b4b | |||
| 2eb26e4e38 | |||
| 3b06d1a6ef | |||
| de824375d3 | |||
| 11af9c02d9 | |||
| 09e429fc67 | |||
| 703b4f89a6 | |||
| 32e054eb84 | |||
| 5e368552ea | |||
| 0379786cbd | |||
| a9a5eb6735 | |||
| ab70f7e498 | |||
| 1407fe36f3 | |||
| d12801e34d | |||
| 3e7190e279 | |||
| 41423feffe | |||
| 30b585c0c1 | |||
| a3c93f0d39 | |||
| f53894b16a | |||
| 7533c349ef | |||
| 91569ced40 | |||
| f9ae778b21 | |||
| 427dfaafab | |||
| ae589aeb54 | |||
| 1b9d2969eb | |||
| daffbcde72 | |||
| 9445c6f21e | |||
| 6febb99499 | |||
| 6e6979cfe3 | |||
| 230cdb0e37 | |||
| ce450e9b6d | |||
| de36a24191 | |||
| b167ba07f7 | |||
| 4d40225a44 | |||
| 57b9c2babc | |||
| 9dc9ce37d8 | |||
| f245840cde | |||
| 4824547dde | |||
| 8dbee32eee | |||
| ae7c5ff0c3 | |||
| 2a465f88c5 | |||
| 58ae9b589a | |||
| 8bc01d3f24 | |||
| d0df5dd641 | |||
| 2cd15d25e9 | |||
| dafac06bc8 | |||
| e2651728c5 | |||
| 673dea2e57 | |||
| 7fbe649dc9 | |||
| 3766899c83 | |||
| a6c7d54fe7 | |||
| 79bc04bec1 | |||
| f9834b5f4d | |||
| fc7b8f4c16 | |||
| 4f6e81677c | |||
| 6b7ecd3044 | |||
| 8ef5f90abd | |||
| a334adffc6 | |||
| f1db883609 | |||
| e586a11e2a | |||
| 50b893f537 | |||
| 02efb9a8e5 | |||
| 38b9a772cd | |||
| 618430433d | |||
| 84cd398c09 | |||
| 385a9bba73 | |||
| 8218a452bd | |||
| a77e2eb3ad | |||
| d1a0bed00e | |||
| 66d4770858 | |||
| 80c5f9b84d | |||
| 79f46cb745 | |||
| de32a9862d | |||
| 0e119e4834 | |||
| 29c8e00477 | |||
| dc6ad9cdd3 | |||
| dcd754dac8 | |||
| d88fb18319 | |||
| 420e9c4662 | |||
| 98d6a1cc64 | |||
| 09ad081b37 | |||
| aa0fd9cafd | |||
| bae8290273 | |||
| 1b799a6973 | |||
| ed3b55a1e2 | |||
| 97c01ce81a | |||
| e96637219f | |||
| 17244b0006 | |||
| 67a02f06da | |||
| 6b6f345618 | |||
| 2ac9d3e977 | |||
| 93692ec255 | |||
| 99852f591e | |||
| b89525746d | |||
| c05834f2a1 | |||
| 9bbfb4763d | |||
| 22e6070e53 | |||
| ba218c85e0 | |||
| 644d2b06ac | |||
| 8d4c8a4553 | |||
| 077174a9a2 | |||
| ce31b95fb7 |
146
.drone.yml
146
.drone.yml
@@ -1,146 +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
|
||||
- mv .env.ci .env
|
||||
- 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
|
||||
|
||||
steps:
|
||||
- name: build latest
|
||||
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:
|
||||
- latest
|
||||
registry: registry.odit.services
|
||||
|
||||
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
|
||||
2
.env.ci
2
.env.ci
@@ -6,4 +6,4 @@ DB_USER=unused
|
||||
DB_PASSWORD=bla
|
||||
DB_NAME=./test.sqlite
|
||||
NODE_ENV=dev
|
||||
POSTALCODE_COUNTRYCODE=null
|
||||
POSTALCODE_COUNTRYCODE=DE
|
||||
11
.env.example
11
.env.example
@@ -1,9 +1,14 @@
|
||||
APP_PORT=4010
|
||||
DB_TYPE=bla
|
||||
DB_TYPE=sqlite
|
||||
DB_HOST=bla
|
||||
DB_PORT=bla
|
||||
DB_USER=bla
|
||||
DB_PASSWORD=bla
|
||||
DB_NAME=bla
|
||||
DB_NAME=./test.sqlite
|
||||
NODE_ENV=production
|
||||
POSTALCODE_COUNTRYCODE=null
|
||||
POSTALCODE_COUNTRYCODE=DE
|
||||
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.
|
||||
1219
CHANGELOG.md
1219
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
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"]
|
||||
172
README.md
172
README.md
@@ -2,62 +2,140 @@
|
||||
|
||||
Backend Server
|
||||
|
||||
## Dev Setup 🛠
|
||||
## Prerequisites
|
||||
|
||||
### Local w/ sqlite
|
||||
|
||||
1. Create a .env file in the project root containing:
|
||||
```
|
||||
APP_PORT=4010
|
||||
DB_TYPE=sqlite
|
||||
DB_HOST=bla
|
||||
DB_PORT=bla
|
||||
DB_USER=bla
|
||||
DB_PASSWORD=bla
|
||||
DB_NAME=./test.sqlite
|
||||
```
|
||||
2. Install Dependencies
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
3. Start the server
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Generate Docs
|
||||
```
|
||||
yarn docs
|
||||
```
|
||||
|
||||
### Docker w/ postgres 🐳
|
||||
This project uses **Bun** as the runtime and package manager. Install Bun first:
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
# 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 and NATS for caching.
|
||||
|
||||
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 be running)
|
||||
bun test
|
||||
|
||||
# Run test in watch mode (reruns on change)
|
||||
bun run test:watch
|
||||
|
||||
# Run test in CI mode (automatically starts the dev server)
|
||||
bun run test:ci
|
||||
```
|
||||
|
||||
### Run Benchmarks
|
||||
```bash
|
||||
# Start the server first
|
||||
bun run dev
|
||||
|
||||
# In another terminal:
|
||||
bun run benchmark
|
||||
```
|
||||
|
||||
### Generate Docs
|
||||
```bash
|
||||
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 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) |
|
||||
|
||||
|
||||
## Recommended Editor
|
||||
|
||||
[Visual Studio Code](https://code.visualstudio.com/)
|
||||
|
||||
### Recommended Extensions
|
||||
|
||||
- will be automatically recommended via ./vscode/extensions.json
|
||||
* will be automatically recommended via ./vscode/extensions.json
|
||||
|
||||
## Branches
|
||||
- main: Protected "release" branch
|
||||
- dev: Current dev branch for merging the different features - only push for merges or minor changes!
|
||||
- feature/xyz: Feature branches - `feature/issueid-title`
|
||||
- bugfix/xyz: Branches for bugfixes - `bugfix/issueid-title` (no id for readme changes needed)
|
||||
|
||||
|
||||
## File Structure
|
||||
|
||||
- src/models/entities\* - database models (typeorm entities)
|
||||
- src/models/actions\* - actions models
|
||||
- src/models/responses\* - response models
|
||||
- src/controllers/\* - routing-controllers
|
||||
- src/loaders/\* - loaders for the different init steps of the api server
|
||||
- src/middlewares/\* - express middlewares (mainly auth r/n)
|
||||
- src/errors/* - our custom (http) errors
|
||||
- src/routes/\* - express routes for everything we don't do via routing-controllers (depreciated)
|
||||
## Staging
|
||||
### Branches & Tags
|
||||
* vX.Y.Z: Release tags created from the main branch
|
||||
* The version numbers follow the semver standard
|
||||
* 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
|
||||
* 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 - 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"]
|
||||
};
|
||||
|
||||
207
package.json
207
package.json
@@ -1,102 +1,105 @@
|
||||
{
|
||||
"name": "@odit/lfk-backend",
|
||||
"version": "0.0.12",
|
||||
"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": {
|
||||
"argon2": "^0.27.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"class-transformer": "^0.3.1",
|
||||
"class-validator": "^0.12.2",
|
||||
"class-validator-jsonschema": "^2.1.0",
|
||||
"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",
|
||||
"mysql": "^2.18.1",
|
||||
"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/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": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test",
|
||||
"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);
|
||||
});
|
||||
24
scripts/create_testenv.ts
Normal file
24
scripts/create_testenv.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import consola from "consola";
|
||||
import fs from "fs";
|
||||
|
||||
|
||||
const env = `
|
||||
APP_PORT=4010
|
||||
DB_TYPE=sqlite
|
||||
DB_HOST=bla
|
||||
DB_PORT=bla
|
||||
DB_USER=bla
|
||||
DB_PASSWORD=bla
|
||||
DB_NAME=./test.sqlite
|
||||
NODE_ENV=test
|
||||
POSTALCODE_COUNTRYCODE=DE
|
||||
SEED_TEST_DATA=true
|
||||
MAILER_URL=https://dev.lauf-fuer-kaya.de/mailer
|
||||
MAILER_KEY=asdasd`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync("./.env", env, { encoding: "utf-8" });
|
||||
consola.success("Exported ci env to .env");
|
||||
} catch (error) {
|
||||
consola.error("Couldn't export the ci env");
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
|
||||
import { validationMetadatasToSchemas } from '@odit/class-validator-jsonschema';
|
||||
import consola from "consola";
|
||||
import fs from "fs";
|
||||
import "reflect-metadata";
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
51
src/app.ts
51
src/app.ts
@@ -5,19 +5,66 @@ import { config, e as errors } from './config';
|
||||
import loaders from "./loaders/index";
|
||||
import authchecker from "./middlewares/authchecker";
|
||||
import { ErrorHandler } from './middlewares/ErrorHandler';
|
||||
import UserChecker from './middlewares/UserChecker';
|
||||
|
||||
// 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 CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
|
||||
const app = createExpressServer({
|
||||
authorizationChecker: authchecker,
|
||||
currentUserChecker: UserChecker,
|
||||
middlewares: [ErrorHandler],
|
||||
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}`
|
||||
|
||||
@@ -1,35 +1,59 @@
|
||||
import { config as configDotenv } from 'dotenv';
|
||||
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: process.env.PHONE_COUNTRYCODE || "ZZ",
|
||||
postalcode_validation_countrycode: getPostalCodeLocale(),
|
||||
version: process.env.VERSION || require('../package.json').version
|
||||
}
|
||||
let errors = 0
|
||||
if (typeof config.internal_port !== "number") {
|
||||
errors++
|
||||
}
|
||||
if (typeof config.phone_validation_countrycode !== "string") {
|
||||
errors++
|
||||
}
|
||||
if (config.phone_validation_countrycode.length !== 2) {
|
||||
errors++
|
||||
}
|
||||
if (typeof config.development !== "boolean") {
|
||||
errors++
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
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,103 +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 { 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 { Logout } from '../models/responses/ResponseLogout';
|
||||
|
||||
@JsonController('/auth')
|
||||
export class AuthController {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
@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(ResponseAuth)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(UsernameOrEmailNeededError)
|
||||
@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) {
|
||||
//This really shouldn't just get returned, but sent via mail or sth like that. But for dev only this is fine.
|
||||
return { "resetToken": await passwordReset.toResetToken() };
|
||||
}
|
||||
|
||||
@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));
|
||||
});
|
||||
|
||||
114
src/controllers/GroupContactController.ts
Normal file
114
src/controllers/GroupContactController.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { Repository, getConnection, getConnectionManager } from 'typeorm';
|
||||
import { GroupContactIdsNotMatchingError, GroupContactNotFoundError } from '../errors/GroupContactErrors';
|
||||
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
|
||||
import { CreateGroupContact } from '../models/actions/create/CreateGroupContact';
|
||||
import { UpdateGroupContact } from '../models/actions/update/UpdateGroupContact';
|
||||
import { GroupContact } from '../models/entities/GroupContact';
|
||||
import { RunnerGroup } from '../models/entities/RunnerGroup';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseGroupContact } from '../models/responses/ResponseGroupContact';
|
||||
|
||||
@JsonController('/contacts')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class GroupContactController {
|
||||
private contactRepository: Repository<GroupContact>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.contactRepository = getConnectionManager().get().getRepository(GroupContact);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("CONTACT:GET")
|
||||
@ResponseSchema(ResponseGroupContact, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all contacts. <br> This includes the contact\'s associated groups.' })
|
||||
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
|
||||
let responseContacts: ResponseGroupContact[] = new Array<ResponseGroupContact>();
|
||||
let contacts: Array<GroupContact>;
|
||||
|
||||
if (page != undefined) {
|
||||
contacts = await this.contactRepository.find({ relations: ['groups', 'groups.parentGroup'], skip: page * page_size, take: page_size });
|
||||
} else {
|
||||
contacts = await this.contactRepository.find({ relations: ['groups', 'groups.parentGroup'] });
|
||||
}
|
||||
|
||||
contacts.forEach(contact => {
|
||||
responseContacts.push(contact.toResponse());
|
||||
});
|
||||
return responseContacts;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("CONTACT:GET")
|
||||
@ResponseSchema(ResponseGroupContact)
|
||||
@ResponseSchema(GroupContactNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(GroupContactNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about the contact whose id got provided. <br> This includes the contact\'s associated groups.' })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let contact = await this.contactRepository.findOne({ id: id }, { relations: ['groups', 'groups.parentGroup'] })
|
||||
if (!contact) { throw new GroupContactNotFoundError(); }
|
||||
return contact.toResponse();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("CONTACT:CREATE")
|
||||
@ResponseSchema(ResponseGroupContact)
|
||||
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Create a new contact.' })
|
||||
async post(@Body({ validate: true }) createContact: CreateGroupContact) {
|
||||
let contact;
|
||||
try {
|
||||
contact = await createContact.toEntity();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
contact = await this.contactRepository.save(contact)
|
||||
return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups', 'groups.parentGroup'] })).toResponse();
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("CONTACT:UPDATE")
|
||||
@ResponseSchema(ResponseGroupContact)
|
||||
@ResponseSchema(GroupContactNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(GroupContactIdsNotMatchingError, { statusCode: 406 })
|
||||
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: "Update the contact whose id you provided. <br> Please remember that ids can't be changed." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) contact: UpdateGroupContact) {
|
||||
let oldContact = await this.contactRepository.findOne({ id: id });
|
||||
|
||||
if (!oldContact) {
|
||||
throw new GroupContactNotFoundError();
|
||||
}
|
||||
|
||||
if (oldContact.id != contact.id) {
|
||||
throw new GroupContactIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.contactRepository.save(await contact.update(oldContact));
|
||||
return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups', 'groups.parentGroup'] })).toResponse();
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("CONTACT:DELETE")
|
||||
@ResponseSchema(ResponseGroupContact)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the contact whose id you provided. <br> If no contact with this id exists it will just return 204(no content). <br> This won\'t delete any groups associated with the contact.' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let contact = await this.contactRepository.findOne({ id: id });
|
||||
if (!contact) { return null; }
|
||||
const responseContact = await this.contactRepository.findOne(contact, { relations: ['groups', 'groups.parentGroup'] });
|
||||
for (let group of responseContact.groups) {
|
||||
group.contact = null;
|
||||
await getConnection().getRepository(RunnerGroup).save(group);
|
||||
}
|
||||
|
||||
await this.contactRepository.delete(contact);
|
||||
return responseContact.toResponse();
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export class ImportController {
|
||||
return responseRunners;
|
||||
}
|
||||
|
||||
@Post('/organisations/:id/import')
|
||||
@Post('/organizations/:id/import')
|
||||
@ContentType("application/json")
|
||||
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||
@@ -78,7 +78,7 @@ export class ImportController {
|
||||
return await this.postJSON(importRunners, groupID);
|
||||
}
|
||||
|
||||
@Post('/organisations/:id/import/csv')
|
||||
@Post('/organizations/:id/import/csv')
|
||||
@ContentType("application/json")
|
||||
@UseBefore(RawBodyMiddleware)
|
||||
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||
|
||||
90
src/controllers/MeController.ts
Normal file
90
src/controllers/MeController.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Body, CurrentUser, Delete, Get, JsonController, OnUndefined, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors';
|
||||
import { UpdateUser } from '../models/actions/update/UpdateUser';
|
||||
import { User } from '../models/entities/User';
|
||||
import { ResponseUser } from '../models/responses/ResponseUser';
|
||||
import { ResponseUserPermissions } from '../models/responses/ResponseUserPermissions';
|
||||
import { PermissionController } from './PermissionController';
|
||||
|
||||
|
||||
@JsonController('/users/me')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class MeController {
|
||||
private userRepository: Repository<User>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.userRepository = getConnectionManager().get().getRepository(User);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(UserNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about yourself.' })
|
||||
async get(@CurrentUser() currentUser: User) {
|
||||
let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
|
||||
if (!user) { throw new UserNotFoundError(); }
|
||||
return new ResponseUser(user);
|
||||
}
|
||||
|
||||
@Get('/permissions')
|
||||
@ResponseSchema(ResponseUserPermissions)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(UserNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all permissions granted to the you sorted into directly granted and inherited as permission response objects.' })
|
||||
async getPermissions(@CurrentUser() currentUser: User) {
|
||||
let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
|
||||
if (!user) { throw new UserNotFoundError(); }
|
||||
return new ResponseUserPermissions(user);
|
||||
}
|
||||
|
||||
@Put('/')
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
|
||||
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
|
||||
@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the yourself. <br> You can't edit your own permissions or group memberships here - Please use the /api/users/:id enpoint instead. <br> Please remember that ids can't be changed." })
|
||||
async put(@CurrentUser() currentUser: User, @Body({ validate: true }) updateUser: UpdateUser) {
|
||||
let oldUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['groups'] });
|
||||
updateUser.groups = oldUser.groups.map(g => g.id);
|
||||
|
||||
if (!oldUser) {
|
||||
throw new UserNotFoundError();
|
||||
}
|
||||
|
||||
if (oldUser.id != updateUser.id) {
|
||||
throw new UserIdsNotMatchingError();
|
||||
}
|
||||
await this.userRepository.save(await updateUser.update(oldUser));
|
||||
|
||||
return new ResponseUser(await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] }));
|
||||
}
|
||||
|
||||
@Delete('/')
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 })
|
||||
@OpenAPI({ description: 'Delete yourself. <br> You have to confirm your decision by providing the ?force=true query param. <br> If there are any permissions directly granted to you they will get deleted as well.' })
|
||||
async remove(@CurrentUser() currentUser: User, @QueryParam("force") force: boolean) {
|
||||
if (!force) { throw new UserDeletionNotConfirmedError; }
|
||||
if (!currentUser) { return UserNotFoundError; }
|
||||
const responseUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] });;
|
||||
|
||||
const permissionControler = new PermissionController();
|
||||
for (let permission of responseUser.permissions) {
|
||||
await permissionControler.remove(permission.id, true);
|
||||
}
|
||||
|
||||
await this.userRepository.delete(currentUser);
|
||||
return new ResponseUser(responseUser);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
@@ -90,7 +97,7 @@ export class PermissionController {
|
||||
if (oldPermission.id != permission.id) {
|
||||
throw new PermissionIdsNotMatchingError();
|
||||
}
|
||||
let existingPermission = await this.permissionRepository.findOne({ target: permission.target, action: permission.action, principal: permission.principal }, { relations: ['principal'] });
|
||||
let existingPermission = await this.permissionRepository.findOne({ target: permission.target, action: permission.action, principal: await permission.getPrincipal() }, { relations: ['principal'] });
|
||||
if (existingPermission) {
|
||||
await this.remove(permission.id, true);
|
||||
return new ResponsePermission(existingPermission);
|
||||
|
||||
@@ -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,16 +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 { 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": [] }] })
|
||||
@@ -28,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;
|
||||
}
|
||||
@@ -44,9 +61,34 @@ 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')
|
||||
@Authorized(["RUNNER:GET", "SCAN:GET"])
|
||||
@ResponseSchema(ResponseScan, { isArray: true })
|
||||
@ResponseSchema(ResponseTrackScan, { isArray: true })
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Lists all scans of the runner whose id got provided. <br> If you only want the valid scans just add the ?onlyValid=true query param.' })
|
||||
async getScans(@Param('id') id: number, onlyValid?: boolean) {
|
||||
let responseScans: ResponseScan[] = new Array<ResponseScan>();
|
||||
let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'scans.track', 'scans.station', 'scans.runner'] })
|
||||
if (!runner) { throw new RunnerNotFoundError(); }
|
||||
|
||||
if (!onlyValid) {
|
||||
for (let scan of runner.scans) {
|
||||
responseScans.push(scan.toResponse());
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let scan of runner.validScans) {
|
||||
responseScans.push(scan.toResponse());
|
||||
}
|
||||
}
|
||||
|
||||
return responseScans;
|
||||
}
|
||||
|
||||
@Post()
|
||||
@@ -64,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')
|
||||
@@ -84,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')
|
||||
@@ -98,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,127 +0,0 @@
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { RunnerOrganisationHasRunnersError, RunnerOrganisationHasTeamsError, RunnerOrganisationIdsNotMatchingError, RunnerOrganisationNotFoundError } from '../errors/RunnerOrganisationErrors';
|
||||
import { CreateRunnerOrganisation } from '../models/actions/create/CreateRunnerOrganisation';
|
||||
import { UpdateRunnerOrganisation } from '../models/actions/update/UpdateRunnerOrganisation';
|
||||
import { RunnerOrganisation } from '../models/entities/RunnerOrganisation';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseRunnerOrganisation } from '../models/responses/ResponseRunnerOrganisation';
|
||||
import { RunnerController } from './RunnerController';
|
||||
import { RunnerTeamController } from './RunnerTeamController';
|
||||
|
||||
|
||||
@JsonController('/organisations')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class RunnerOrganisationController {
|
||||
private runnerOrganisationRepository: Repository<RunnerOrganisation>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.runnerOrganisationRepository = getConnectionManager().get().getRepository(RunnerOrganisation);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("ORGANISATION:GET")
|
||||
@ResponseSchema(ResponseRunnerOrganisation, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all organisations. <br> This includes their address, contact and teams (if existing/associated).' })
|
||||
async getAll() {
|
||||
let responseTeams: ResponseRunnerOrganisation[] = new Array<ResponseRunnerOrganisation>();
|
||||
const runners = await this.runnerOrganisationRepository.find({ relations: ['address', 'contact', 'teams'] });
|
||||
runners.forEach(runner => {
|
||||
responseTeams.push(new ResponseRunnerOrganisation(runner));
|
||||
});
|
||||
return responseTeams;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("ORGANISATION:GET")
|
||||
@ResponseSchema(ResponseRunnerOrganisation)
|
||||
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerOrganisationNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about the organisation whose id got provided.' })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let runnerOrg = await this.runnerOrganisationRepository.findOne({ id: id }, { relations: ['address', 'contact', 'teams'] });
|
||||
if (!runnerOrg) { throw new RunnerOrganisationNotFoundError(); }
|
||||
return new ResponseRunnerOrganisation(runnerOrg);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("ORGANISATION:CREATE")
|
||||
@ResponseSchema(ResponseRunnerOrganisation)
|
||||
@OpenAPI({ description: 'Create a new organsisation.' })
|
||||
async post(@Body({ validate: true }) createRunnerOrganisation: CreateRunnerOrganisation) {
|
||||
let runnerOrganisation;
|
||||
try {
|
||||
runnerOrganisation = await createRunnerOrganisation.toEntity();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
runnerOrganisation = await this.runnerOrganisationRepository.save(runnerOrganisation);
|
||||
|
||||
return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(runnerOrganisation, { relations: ['address', 'contact', 'teams'] }));
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("ORGANISATION:UPDATE")
|
||||
@ResponseSchema(ResponseRunnerOrganisation)
|
||||
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerOrganisationIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the organisation whose id you provided. <br> Please remember that ids can't be changed." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) updateOrganisation: UpdateRunnerOrganisation) {
|
||||
let oldRunnerOrganisation = await this.runnerOrganisationRepository.findOne({ id: id });
|
||||
|
||||
if (!oldRunnerOrganisation) {
|
||||
throw new RunnerOrganisationNotFoundError();
|
||||
}
|
||||
|
||||
if (oldRunnerOrganisation.id != updateOrganisation.id) {
|
||||
throw new RunnerOrganisationIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.runnerOrganisationRepository.save(await updateOrganisation.update(oldRunnerOrganisation));
|
||||
|
||||
return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(id, { relations: ['address', 'contact', 'teams'] }));
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("ORGANISATION:DELETE")
|
||||
@ResponseSchema(ResponseRunnerOrganisation)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@ResponseSchema(RunnerOrganisationHasTeamsError, { statusCode: 406 })
|
||||
@ResponseSchema(RunnerOrganisationHasRunnersError, { statusCode: 406 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the organsisation whose id you provided. <br> If the organisation still has runners and/or teams associated this will fail. <br> To delete the organisation with all associated runners and teams set the force QueryParam to true (cascading deletion might take a while). <br> If no organisation with this id exists it will just return 204(no content).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let organisation = await this.runnerOrganisationRepository.findOne({ id: id });
|
||||
if (!organisation) { return null; }
|
||||
let runnerOrganisation = await this.runnerOrganisationRepository.findOne(organisation, { relations: ['address', 'contact', 'runners', 'teams'] });
|
||||
|
||||
if (!force) {
|
||||
if (runnerOrganisation.teams.length != 0) {
|
||||
throw new RunnerOrganisationHasTeamsError();
|
||||
}
|
||||
}
|
||||
const teamController = new RunnerTeamController()
|
||||
for (let team of runnerOrganisation.teams) {
|
||||
await teamController.remove(team.id, true);
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
if (runnerOrganisation.runners.length != 0) {
|
||||
throw new RunnerOrganisationHasRunnersError();
|
||||
}
|
||||
}
|
||||
const runnerController = new RunnerController()
|
||||
for (let runner of runnerOrganisation.runners) {
|
||||
await runnerController.remove(runner.id, true);
|
||||
}
|
||||
|
||||
const responseOrganisation = new ResponseRunnerOrganisation(runnerOrganisation);
|
||||
await this.runnerOrganisationRepository.delete(organisation);
|
||||
return responseOrganisation;
|
||||
}
|
||||
}
|
||||
156
src/controllers/RunnerOrganizationController.ts
Normal file
156
src/controllers/RunnerOrganizationController.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { Authorized, BadRequestError, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { Repository, getConnectionManager } from 'typeorm';
|
||||
import { RunnerOrganizationHasRunnersError, RunnerOrganizationHasTeamsError, RunnerOrganizationIdsNotMatchingError, RunnerOrganizationNotFoundError } from '../errors/RunnerOrganizationErrors';
|
||||
import { CreateRunnerOrganization } from '../models/actions/create/CreateRunnerOrganization';
|
||||
import { UpdateRunnerOrganization } from '../models/actions/update/UpdateRunnerOrganization';
|
||||
import { Runner } from '../models/entities/Runner';
|
||||
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseRunner } from '../models/responses/ResponseRunner';
|
||||
import { ResponseRunnerOrganization } from '../models/responses/ResponseRunnerOrganization';
|
||||
import { RunnerController } from './RunnerController';
|
||||
import { RunnerTeamController } from './RunnerTeamController';
|
||||
|
||||
|
||||
@JsonController('/organizations')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class RunnerOrganizationController {
|
||||
private runnerOrganizationRepository: Repository<RunnerOrganization>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.runnerOrganizationRepository = getConnectionManager().get().getRepository(RunnerOrganization);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("ORGANIZATION:GET")
|
||||
@ResponseSchema(ResponseRunnerOrganization, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all organizations. <br> This includes their address, contact and teams (if existing/associated).' })
|
||||
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
|
||||
let responseOrgs: ResponseRunnerOrganization[] = new Array<ResponseRunnerOrganization>();
|
||||
let orgs: Array<RunnerOrganization>;
|
||||
|
||||
if (page != undefined) {
|
||||
orgs = await this.runnerOrganizationRepository.find({ relations: ['contact', 'teams'], skip: page * page_size, take: page_size });
|
||||
} else {
|
||||
orgs = await this.runnerOrganizationRepository.find({ relations: ['contact', 'teams'] });
|
||||
}
|
||||
|
||||
orgs.forEach(org => {
|
||||
responseOrgs.push(new ResponseRunnerOrganization(org));
|
||||
});
|
||||
return responseOrgs;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("ORGANIZATION:GET")
|
||||
@ResponseSchema(ResponseRunnerOrganization)
|
||||
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerOrganizationNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about the organization whose id got provided.' })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let runnerOrg = await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['contact', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.scans.track', 'runners', 'runners.scans', 'runners.scans.track'] });
|
||||
if (!runnerOrg) { throw new RunnerOrganizationNotFoundError(); }
|
||||
return new ResponseRunnerOrganization(runnerOrg);
|
||||
}
|
||||
|
||||
@Get('/:id/runners')
|
||||
@Authorized(["RUNNER:GET", "SCAN:GET"])
|
||||
@ResponseSchema(ResponseRunner, { isArray: true })
|
||||
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Lists all runners from this org and it\'s teams (if you don\'t provide the ?onlyDirect=true param). <br> This includes the runner\'s group and distance ran.' })
|
||||
async getRunners(@Param('id') id: number, @QueryParam('onlyDirect') onlyDirect: boolean, @QueryParam("selfservice_links", { required: false }) selfservice_links: boolean = false) {
|
||||
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
|
||||
let runners: Runner[];
|
||||
if (!onlyDirect) { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.group', 'teams.runners.group.parentGroup', 'teams.runners.scans', 'teams.runners.scans.track'] })).allRunners; }
|
||||
else { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners; }
|
||||
runners.forEach(runner => {
|
||||
responseRunners.push(new ResponseRunner(runner, selfservice_links));
|
||||
});
|
||||
return responseRunners;
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("ORGANIZATION:CREATE")
|
||||
@ResponseSchema(ResponseRunnerOrganization)
|
||||
@OpenAPI({ description: 'Create a new organsisation.' })
|
||||
async post(@Body({ validate: true }) createRunnerOrganization: CreateRunnerOrganization) {
|
||||
let runnerOrganization;
|
||||
try {
|
||||
runnerOrganization = await createRunnerOrganization.toEntity();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
runnerOrganization = await this.runnerOrganizationRepository.save(runnerOrganization);
|
||||
|
||||
return new ResponseRunnerOrganization(await this.runnerOrganizationRepository.findOne(runnerOrganization, { relations: ['contact', 'teams'] }));
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("ORGANIZATION:UPDATE")
|
||||
@ResponseSchema(ResponseRunnerOrganization)
|
||||
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerOrganizationIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the organization whose id you provided. <br> Please remember that ids can't be changed." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) updateOrganization: UpdateRunnerOrganization) {
|
||||
let oldRunnerOrganization = await this.runnerOrganizationRepository.findOne({ id: id });
|
||||
|
||||
if (!oldRunnerOrganization) {
|
||||
throw new RunnerOrganizationNotFoundError();
|
||||
}
|
||||
|
||||
if (oldRunnerOrganization.id != updateOrganization.id) {
|
||||
throw new RunnerOrganizationIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.runnerOrganizationRepository.save(await updateOrganization.update(oldRunnerOrganization));
|
||||
|
||||
return new ResponseRunnerOrganization(await this.runnerOrganizationRepository.findOne(id, { relations: ['contact', 'teams'] }));
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("ORGANIZATION:DELETE")
|
||||
@ResponseSchema(ResponseRunnerOrganization)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@ResponseSchema(RunnerOrganizationHasTeamsError, { statusCode: 406 })
|
||||
@ResponseSchema(RunnerOrganizationHasRunnersError, { statusCode: 406 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the organsisation whose id you provided. <br> If the organization still has runners and/or teams associated this will fail. <br> To delete the organization with all associated runners and teams set the force QueryParam to true (cascading deletion might take a while). <br> This won\'t delete the associated contact. <br> If no organization with this id exists it will just return 204(no content).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
if (id == 1) {
|
||||
throw new BadRequestError("You can't delete the citizen runner org.");
|
||||
}
|
||||
|
||||
let organization = await this.runnerOrganizationRepository.findOne({ id: id });
|
||||
if (!organization) { return null; }
|
||||
let runnerOrganization = await this.runnerOrganizationRepository.findOne(organization, { relations: ['contact', 'runners', 'teams'] });
|
||||
|
||||
if (!force) {
|
||||
if (runnerOrganization.teams.length != 0) {
|
||||
throw new RunnerOrganizationHasTeamsError();
|
||||
}
|
||||
}
|
||||
const teamController = new RunnerTeamController()
|
||||
for (let team of runnerOrganization.teams) {
|
||||
await teamController.remove(team.id, true);
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
if (runnerOrganization.runners.length != 0) {
|
||||
throw new RunnerOrganizationHasRunnersError();
|
||||
}
|
||||
}
|
||||
const runnerController = new RunnerController()
|
||||
for (let runner of runnerOrganization.runners) {
|
||||
await runnerController.remove(runner.id, true);
|
||||
}
|
||||
|
||||
const responseOrganization = new ResponseRunnerOrganization(runnerOrganization);
|
||||
await this.runnerOrganizationRepository.delete(organization);
|
||||
return responseOrganization;
|
||||
}
|
||||
}
|
||||
248
src/controllers/RunnerSelfServiceController.ts
Normal file
248
src/controllers/RunnerSelfServiceController.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
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();
|
||||
}
|
||||
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,11 +1,12 @@
|
||||
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';
|
||||
import { RunnerTeam } from '../models/entities/RunnerTeam';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseRunner } from '../models/responses/ResponseRunner';
|
||||
import { ResponseRunnerTeam } from '../models/responses/ResponseRunnerTeam';
|
||||
import { RunnerController } from './RunnerController';
|
||||
|
||||
@@ -25,12 +26,19 @@ export class RunnerTeamController {
|
||||
@Get()
|
||||
@Authorized("TEAM:GET")
|
||||
@ResponseSchema(ResponseRunnerTeam, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all teams. <br> This includes their parent organisation and contact (if existing/associated).' })
|
||||
async getAll() {
|
||||
@OpenAPI({ description: 'Lists all teams. <br> This includes their parent organization and contact (if existing/associated).' })
|
||||
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;
|
||||
}
|
||||
@@ -42,11 +50,25 @@ 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);
|
||||
}
|
||||
|
||||
@Get('/:id/runners')
|
||||
@Authorized(["RUNNER:GET", "SCAN:GET"])
|
||||
@ResponseSchema(ResponseRunner, { isArray: true })
|
||||
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Lists all runners from this team. <br> This includes the runner\'s group and distance ran.' })
|
||||
async getRunners(@Param('id') id: number, @QueryParam("selfservice_links", { required: false }) selfservice_links: boolean = false) {
|
||||
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
|
||||
const runners = (await this.runnerTeamRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners;
|
||||
runners.forEach(runner => {
|
||||
responseRunners.push(new ResponseRunner(runner, selfservice_links));
|
||||
});
|
||||
return responseRunners;
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("TEAM:CREATE")
|
||||
@ResponseSchema(ResponseRunnerTeam)
|
||||
@@ -93,11 +115,11 @@ export class RunnerTeamController {
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@ResponseSchema(RunnerTeamHasRunnersError, { statusCode: 406 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the team whose id you provided. <br> If the team still has runners associated this will fail. <br> To delete the team with all associated runners set the force QueryParam to true (cascading deletion might take a while). <br> If no team with this id exists it will just return 204(no content).' })
|
||||
@OpenAPI({ description: 'Delete the team whose id you provided. <br> If the team still has runners associated this will fail. <br> To delete the team with all associated runners set the force QueryParam to true (cascading deletion might take a while). <br> This won\'t delete the associated contact.<br> If no team with this id exists it will just return 204(no content).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
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 { RunnerOrganisation } from '../models/entities/RunnerOrganisation';
|
||||
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/ResponseStatsOrganisation';
|
||||
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(RunnerOrganisation).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,40 +196,82 @@ 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;
|
||||
}
|
||||
|
||||
@Get("/organisations/distance")
|
||||
@Get("/organizations/distance")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten organisations by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
@OpenAPI({ description: "Returns the top ten organizations by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopOrgsByDistance() {
|
||||
let orgs = await getConnection().getRepository(RunnerOrganisation).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
|
||||
let topOrgs = orgs.sort((org1, org2) => org1.distance - org2.distance).slice(0, 9);
|
||||
// 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'] });
|
||||
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;
|
||||
}
|
||||
|
||||
@Get("/organisations/donations")
|
||||
@Get("/organizations/donations")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten organisations by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
@OpenAPI({ description: "Returns the top ten organizations by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopOrgsByDonations() {
|
||||
let orgs = await getConnection().getRepository(RunnerOrganisation).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
|
||||
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 { 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 });
|
||||
@@ -105,9 +121,11 @@ export class UserController {
|
||||
@Authorized("USER:DELETE")
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the user whose id you provided. <br> If there are any permissions directly granted to the user they will get deleted as well. <br> If no user with this id exists it will just return 204(no content).' })
|
||||
@OpenAPI({ description: 'Delete the user whose id you provided. <br> You have to confirm your decision by providing the ?force=true query param. <br> If there are any permissions directly granted to the user they will get deleted as well. <br> If no user with this id exists it will just return 204(no content).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
if (!force) { throw new UserDeletionNotConfirmedError; }
|
||||
let user = await this.userRepository.findOne({ id: id });
|
||||
if (!user) { return null; }
|
||||
const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] });;
|
||||
|
||||
@@ -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,24 +1,57 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
import { BadRequestError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw, when to provided address doesn't belong to the accepted types.
|
||||
* Error to throw when an address's postal code fails validation.
|
||||
*/
|
||||
export class AddressWrongTypeError extends NotAcceptableError {
|
||||
export class AddressPostalCodeInvalidError extends BadRequestError {
|
||||
@IsString()
|
||||
name = "AddressWrongTypeError"
|
||||
name = "AddressPostalCodeInvalidError"
|
||||
|
||||
@IsString()
|
||||
message = "The address must be an existing address's id. \n You provided a object of another type."
|
||||
message = "The postal code you provided is invalid. \n Please check if your postal code follows the postal code validation guidelines."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw, when a non-existent address get's loaded.
|
||||
* Error to throw when an non-empty address's first line isn't set.
|
||||
*/
|
||||
export class AddressNotFoundError extends NotFoundError {
|
||||
export class AddressFirstLineEmptyError extends BadRequestError {
|
||||
@IsString()
|
||||
name = "AddressNotFoundError"
|
||||
name = "AddressFirstLineEmptyError"
|
||||
|
||||
@IsString()
|
||||
message = "The address you provided couldn't be located in the system. \n Please check your request."
|
||||
message = "You provided a empty first address line. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when an non-empty address's postal code isn't set.
|
||||
*/
|
||||
export class AddressPostalCodeEmptyError extends BadRequestError {
|
||||
@IsString()
|
||||
name = "AddressPostalCodeEmptyError"
|
||||
|
||||
@IsString()
|
||||
message = "You provided a empty postal code. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when an non-empty address's city isn't set.
|
||||
*/
|
||||
export class AddressCityEmptyError extends BadRequestError {
|
||||
@IsString()
|
||||
name = "AddressCityEmptyError"
|
||||
|
||||
@IsString()
|
||||
message = "You provided a empty city. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when an non-empty address's country isn't set.
|
||||
*/
|
||||
export class AddressCountryEmptyError extends BadRequestError {
|
||||
@IsString()
|
||||
name = "AddressCountryEmptyError"
|
||||
|
||||
@IsString()
|
||||
message = "You provided a empty country. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
|
||||
}
|
||||
@@ -2,18 +2,7 @@ import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw, when a provided groupContact doesn't belong to the accepted types.
|
||||
*/
|
||||
export class GroupContactWrongTypeError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "GroupContactWrongTypeError"
|
||||
|
||||
@IsString()
|
||||
message = "The groupContact must be an existing groupContact's id. \n You provided a object of another type."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw, when a non-existent groupContact get's loaded.
|
||||
* Error to throw, when a non-existent contact get's requested.
|
||||
*/
|
||||
export class GroupContactNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
@@ -21,4 +10,16 @@ export class GroupContactNotFoundError extends NotFoundError {
|
||||
|
||||
@IsString()
|
||||
message = "The groupContact you provided couldn't be located in the system. \n Please check your request."
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two contacts' ids don't match.
|
||||
* Usually occurs when a user tries to change a contact's id.
|
||||
*/
|
||||
export class GroupContactIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "GroupContactIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The ids don't match! \n And if you wanted to change a contact's id: This isn't allowed!"
|
||||
}
|
||||
|
||||
17
src/errors/MailErrors.ts
Normal file
17
src/errors/MailErrors.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { InternalServerError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a permission couldn't be found.
|
||||
*/
|
||||
export class MailSendingError extends InternalServerError {
|
||||
@IsString()
|
||||
name = "MailSendingError"
|
||||
|
||||
@IsString()
|
||||
message = "We had a problem sending the mail!"
|
||||
|
||||
constructor() {
|
||||
super("We had a problem sending the mail!");
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,29 @@ export class RunnerGroupNeededError extends NotAcceptableError {
|
||||
name = "RunnerGroupNeededError"
|
||||
|
||||
@IsString()
|
||||
message = "Runner's need to be part of one group (team or organisation)! \n You provided neither."
|
||||
message = "Runner's need to be part of one group (team or organization)! \n You provided neither."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a citizen runner has no mail-address.
|
||||
*/
|
||||
export class RunnerEmailNeededError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerEmailNeededError"
|
||||
|
||||
@IsString()
|
||||
message = "Citizenrunners have to provide an email address for verification and contacting."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a runner already requested a new selfservice link in the last 30s.
|
||||
*/
|
||||
export class RunnerSelfserviceTimeoutError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerSelfserviceTimeoutError"
|
||||
|
||||
@IsString()
|
||||
message = "You can only reqest a new token every 30s."
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a runner organisation couldn't be found.
|
||||
*/
|
||||
export class RunnerOrganisationNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
name = "RunnerOrganisationNotFoundError"
|
||||
|
||||
@IsString()
|
||||
message = "RunnerOrganisation not found!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two runner organisation's ids don't match.
|
||||
* Usually occurs when a user tries to change a runner organisation's id.
|
||||
*/
|
||||
export class RunnerOrganisationIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerOrganisationIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The ids don't match! \n And if you wanted to change a runner organisation's id: This isn't allowed!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a organisation still has runners associated.
|
||||
*/
|
||||
export class RunnerOrganisationHasRunnersError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerOrganisationHasRunnersError"
|
||||
|
||||
@IsString()
|
||||
message = "This organisation still has runners associated with it. \n If you want to delete this organisation with all it's runners and teams add `?force` to your query."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a organisation still has teams associated.
|
||||
*/
|
||||
export class RunnerOrganisationHasTeamsError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerOrganisationHasTeamsError"
|
||||
|
||||
@IsString()
|
||||
message = "This organisation still has teams associated with it. \n If you want to delete this organisation with all it's runners and teams add `?force` to your query."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw, when a provided runnerOrganisation doesn't belong to the accepted types.
|
||||
*/
|
||||
export class RunnerOrganisationWrongTypeError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerOrganisationWrongTypeError"
|
||||
|
||||
@IsString()
|
||||
message = "The runner organisation must be an existing organisation's id. \n You provided a object of another type."
|
||||
}
|
||||
58
src/errors/RunnerOrganizationErrors.ts
Normal file
58
src/errors/RunnerOrganizationErrors.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a runner organization couldn't be found.
|
||||
*/
|
||||
export class RunnerOrganizationNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
name = "RunnerOrganizationNotFoundError"
|
||||
|
||||
@IsString()
|
||||
message = "RunnerOrganization not found!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two runner organization's ids don't match.
|
||||
* Usually occurs when a user tries to change a runner organization's id.
|
||||
*/
|
||||
export class RunnerOrganizationIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerOrganizationIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The ids don't match! \n And if you wanted to change a runner organization's id: This isn't allowed!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a organization still has runners associated.
|
||||
*/
|
||||
export class RunnerOrganizationHasRunnersError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerOrganizationHasRunnersError"
|
||||
|
||||
@IsString()
|
||||
message = "This organization still has runners associated with it. \n If you want to delete this organization with all it's runners and teams add `?force` to your query."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a organization still has teams associated.
|
||||
*/
|
||||
export class RunnerOrganizationHasTeamsError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerOrganizationHasTeamsError"
|
||||
|
||||
@IsString()
|
||||
message = "This organization still has teams associated with it. \n If you want to delete this organization with all it's runners and teams add `?force` to your query."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw, when a provided runnerOrganization doesn't belong to the accepted types.
|
||||
*/
|
||||
export class RunnerOrganizationWrongTypeError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerOrganizationWrongTypeError"
|
||||
|
||||
@IsString()
|
||||
message = "The runner organization must be an existing organization's id. \n You provided a object of another type."
|
||||
}
|
||||
@@ -43,5 +43,5 @@ export class RunnerTeamNeedsParentError extends NotAcceptableError {
|
||||
name = "RunnerTeamNeedsParentError"
|
||||
|
||||
@IsString()
|
||||
message = "You provided no runner organisation as this team's parent group."
|
||||
message = "You provided no runner organization as this team's parent group."
|
||||
}
|
||||
@@ -59,4 +59,45 @@ export class UserIdsNotMatchingError extends NotAcceptableError {
|
||||
|
||||
@IsString()
|
||||
message = "The ids don't match!! \n And if you wanted to change a user's id: This isn't allowed!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two users' ids don't match.
|
||||
* Usually occurs when a user tries to change a user's id.
|
||||
*/
|
||||
export class UserDeletionNotConfirmedError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "UserDeletionNotConfirmedError"
|
||||
|
||||
@IsString()
|
||||
message = "You are trying to delete a user! \n If you're sure about doing this: provide the ?force=true query param."
|
||||
}
|
||||
|
||||
export class PasswordMustContainUppercaseLetterError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "PasswordMustContainUppercaseLetterError"
|
||||
|
||||
@IsString()
|
||||
message = "Passwords must contain at least one uppercase letter."
|
||||
}
|
||||
export class PasswordMustContainLowercaseLetterError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "PasswordMustContainLowercaseLetterError"
|
||||
|
||||
@IsString()
|
||||
message = "Passwords must contain at least one lowercase letter."
|
||||
}
|
||||
export class PasswordMustContainNumberError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "PasswordMustContainNumberError"
|
||||
|
||||
@IsString()
|
||||
message = "Passwords must contain at least one number."
|
||||
}
|
||||
export class PasswordTooShortError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "PasswordTooShortError"
|
||||
|
||||
@IsString()
|
||||
message = "Passwords must be at least ten characters long."
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import * as jsonwebtoken from "jsonwebtoken";
|
||||
import { config } from './config';
|
||||
import { Runner } from './models/entities/Runner';
|
||||
import { User } from './models/entities/User';
|
||||
|
||||
/**
|
||||
@@ -34,6 +35,19 @@ export class JwtCreator {
|
||||
}, config.jwt_secret)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new selfservice token for a given runner.
|
||||
* @param runner Runner entity that the access token shall be created for.
|
||||
* @param expiry_timestamp Timestamp for the token expiry. Will be set about 9999 years if none provided.
|
||||
*/
|
||||
public static createSelfService(runner: Runner, expiry_timestamp?: number) {
|
||||
if (!expiry_timestamp) { expiry_timestamp = Math.floor(Date.now() / 1000) + 36000 * 60 * 24 * 365 * 9999; }
|
||||
return jsonwebtoken.sign({
|
||||
id: runner.id,
|
||||
exp: expiry_timestamp
|
||||
}, config.jwt_secret)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new password reset token for a given user.
|
||||
* The token is valid for 15 minutes or 1 use - whatever comes first.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { createConnection } from "typeorm";
|
||||
import { runSeeder } from 'typeorm-seeding';
|
||||
import { User } from '../models/entities/User';
|
||||
import consola from 'consola';
|
||||
import { config } from '../config';
|
||||
import { ConfigFlag } from '../models/entities/ConfigFlags';
|
||||
import SeedPublicOrg from '../seeds/SeedPublicOrg';
|
||||
import SeedTestRunners from '../seeds/SeedTestRunners';
|
||||
import SeedUsers from '../seeds/SeedUsers';
|
||||
/**
|
||||
* Loader for the database that creates the database connection and initializes the database tabels.
|
||||
@@ -8,9 +12,26 @@ 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();
|
||||
if (await connection.getRepository(User).count() === 0) {
|
||||
|
||||
//The data seeding part
|
||||
if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:user", value: "true" }))) {
|
||||
await runSeeder(SeedUsers);
|
||||
await connection.getRepository(ConfigFlag).save({ option: "seeded:user", value: "true" });
|
||||
}
|
||||
if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:citizenorg", value: "true" }))) {
|
||||
await runSeeder(SeedPublicOrg);
|
||||
await connection.getRepository(ConfigFlag).save({ option: "seeded:citizenorg", value: "true" });
|
||||
}
|
||||
if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:testdata", value: "true" })) && config.seedTestData == true) {
|
||||
await runSeeder(SeedTestRunners);
|
||||
await connection.getRepository(ConfigFlag).save({ option: "seeded:testdata", value: "true" });
|
||||
}
|
||||
|
||||
return connection;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { validationMetadatasToSchemas } from "class-validator-jsonschema";
|
||||
import { validationMetadatasToSchemas } from "@odit/class-validator-jsonschema";
|
||||
import express, { Application } from "express";
|
||||
import path from 'path';
|
||||
import { getMetadataArgsStorage } from "routing-controllers";
|
||||
|
||||
118
src/mailer.ts
Normal file
118
src/mailer.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import axios from 'axios';
|
||||
import { config } from './config';
|
||||
import { MailSendingError } from './errors/MailErrors';
|
||||
|
||||
/**
|
||||
* This class is responsible for all things mail sending.
|
||||
* This uses axios to communicate with the mailer api (https://git.odit.services/lfk/mailer).
|
||||
*/
|
||||
export class Mailer {
|
||||
public static base: string = config.mailer_url;
|
||||
public static key: string = config.mailer_key;
|
||||
public static testing: boolean = config.testing;
|
||||
|
||||
/**
|
||||
* Function for sending a password reset mail.
|
||||
* @param to_address The address the mail will be sent to. Should always get pulled from a user object.
|
||||
* @param token The requested password reset token - will be combined with the app_url to generate a password reset link.
|
||||
*/
|
||||
public static async sendResetMail(to_address: string, token: string, locale: string = "en") {
|
||||
try {
|
||||
await axios.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 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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';
|
||||
@@ -42,7 +42,7 @@ const StatsAuth = async (req: Request, res: Response, next: () => void) => {
|
||||
let user_authorized = false;
|
||||
try {
|
||||
let action = { request: req, response: res, context: null, next: next }
|
||||
user_authorized = await authchecker(action, ["RUNNER:GET", "TEAM:GET", "ORGANISATION:GET"]);
|
||||
user_authorized = await authchecker(action, ["RUNNER:GET", "TEAM:GET", "ORGANIZATION:GET"]);
|
||||
}
|
||||
finally {
|
||||
if (user_authorized == false) {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
58
src/middlewares/UserChecker.ts
Normal file
58
src/middlewares/UserChecker.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import cookie from "cookie";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import { Action } from 'routing-controllers';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { config } from '../config';
|
||||
import { IllegalJWTError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from '../errors/AuthError';
|
||||
import { JwtCreator, JwtUser } from '../jwtcreator';
|
||||
import { User } from '../models/entities/User';
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
*/
|
||||
const UserChecker = async (action: Action) => {
|
||||
let jwtPayload = undefined
|
||||
try {
|
||||
let provided_token = "" + action.request.headers["authorization"].replace("Bearer ", "");
|
||||
jwtPayload = <any>jwt.verify(provided_token, config.jwt_secret);
|
||||
jwtPayload = jwtPayload["userdetails"];
|
||||
} catch (error) {
|
||||
jwtPayload = await refresh(action);
|
||||
}
|
||||
|
||||
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] })
|
||||
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
|
||||
if (user.enabled == false) { throw new UserDisabledError(); }
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles soft-refreshing of access-tokens.
|
||||
* @param action Routing-Controllers action object that provides request and response objects among other stuff.
|
||||
*/
|
||||
const refresh = async (action: Action) => {
|
||||
let refresh_token = undefined;
|
||||
try {
|
||||
refresh_token = cookie.parse(action.request.headers["cookie"])["lfk_backend__refresh_token"];
|
||||
}
|
||||
catch {
|
||||
throw new IllegalJWTError();
|
||||
}
|
||||
|
||||
let jwtPayload = undefined;
|
||||
try {
|
||||
jwtPayload = <any>jwt.verify(refresh_token, config.jwt_secret);
|
||||
} catch (error) {
|
||||
throw new IllegalJWTError();
|
||||
}
|
||||
|
||||
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions', 'groups', 'groups.permissions'] })
|
||||
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
|
||||
if (user.enabled == false) { throw new UserDisabledError(); }
|
||||
|
||||
let newAccess = JwtCreator.createAccess(user);
|
||||
action.response.header("authorization", "Bearer " + newAccess);
|
||||
|
||||
return await new JwtUser(user);
|
||||
}
|
||||
export default UserChecker;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { RunnerGroupNeededError } from '../../errors/RunnerErrors';
|
||||
import { RunnerOrganisationNotFoundError } from '../../errors/RunnerOrganisationErrors';
|
||||
import { RunnerOrganizationNotFoundError } from '../../errors/RunnerOrganizationErrors';
|
||||
import { RunnerGroup } from '../entities/RunnerGroup';
|
||||
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
|
||||
import { RunnerOrganization } from '../entities/RunnerOrganization';
|
||||
import { RunnerTeam } from '../entities/RunnerTeam';
|
||||
import { CreateRunner } from './create/CreateRunner';
|
||||
|
||||
@@ -78,9 +78,9 @@ export class ImportRunner {
|
||||
let team = await getConnectionManager().get().getRepository(RunnerTeam).findOne({ id: groupID });
|
||||
if (team) { return team; }
|
||||
|
||||
let org = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: groupID });
|
||||
let org = await getConnectionManager().get().getRepository(RunnerOrganization).findOne({ id: groupID });
|
||||
if (!org) {
|
||||
throw new RunnerOrganisationNotFoundError();
|
||||
throw new RunnerOrganizationNotFoundError();
|
||||
}
|
||||
if (this.team === undefined) { return org; }
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { IsNotEmpty, IsOptional, IsPostalCode, IsString } from 'class-validator';
|
||||
import { config } from '../../../config';
|
||||
import { Address } from '../../entities/Address';
|
||||
|
||||
/**
|
||||
* This classed is used to create a new Address entity from a json body (post request).
|
||||
*/
|
||||
export class CreateAddress {
|
||||
/**
|
||||
* The newaddress's description.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* The new address's first line.
|
||||
* Containing the street and house number.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
address1: string;
|
||||
|
||||
/**
|
||||
* The new address's second line.
|
||||
* Containing optional information.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
address2?: string;
|
||||
|
||||
/**
|
||||
* The new address's postal code.
|
||||
* This will get checked against the postal code syntax for the configured country.
|
||||
* TODO: Implement the config option.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsPostalCode(config.postalcode_validation_countrycode)
|
||||
postalcode: string;
|
||||
|
||||
/**
|
||||
* The new address's city.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
city: string;
|
||||
|
||||
/**
|
||||
* The new address's country.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
country: string;
|
||||
|
||||
/**
|
||||
* Creates a new Address entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<Address> {
|
||||
let newAddress: Address = new Address();
|
||||
|
||||
newAddress.address1 = this.address1;
|
||||
newAddress.address2 = this.address2;
|
||||
newAddress.postalcode = this.postalcode;
|
||||
newAddress.city = this.city;
|
||||
newAddress.country = this.country;
|
||||
|
||||
return newAddress;
|
||||
}
|
||||
}
|
||||
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';
|
||||
@@ -11,7 +11,22 @@ import { CreateDonation } from './CreateDonation';
|
||||
export class CreateDistanceDonation extends CreateDonation {
|
||||
|
||||
/**
|
||||
* The donation's associated runner.
|
||||
* 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.
|
||||
*/
|
||||
@IsInt()
|
||||
@@ -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.
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IsBoolean, IsOptional } from 'class-validator';
|
||||
import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { Donor } from '../../entities/Donor';
|
||||
import { CreateParticipant } from './CreateParticipant';
|
||||
|
||||
@@ -26,10 +27,10 @@ export class CreateDonor extends CreateParticipant {
|
||||
newDonor.lastname = this.lastname;
|
||||
newDonor.phone = this.phone;
|
||||
newDonor.email = this.email;
|
||||
newDonor.address = await this.getAddress();
|
||||
newDonor.receiptNeeded = this.receiptNeeded;
|
||||
|
||||
if (this.receiptNeeded == true && this.address == null) {
|
||||
newDonor.address = this.address;
|
||||
Address.validate(newDonor.address);
|
||||
if (this.receiptNeeded == true && Address.isValidAddress(newDonor.address) == false) {
|
||||
throw new DonorReceiptAddressNeededError()
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||
import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { config } from '../../../config';
|
||||
import { AddressNotFoundError, AddressWrongTypeError } from '../../../errors/AddressErrors';
|
||||
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { GroupContact } from '../../entities/GroupContact';
|
||||
import { RunnerGroup } from '../../entities/RunnerGroup';
|
||||
|
||||
/**
|
||||
* This classed is used to create a new Group entity from a json body (post request).
|
||||
* This classed is used to create a new GroupContact entity from a json body (post request).
|
||||
*/
|
||||
export class CreateGroupContact {
|
||||
/**
|
||||
@@ -32,11 +33,10 @@ export class CreateGroupContact {
|
||||
|
||||
/**
|
||||
* The new contact's address.
|
||||
* Must be the address's id.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
address?: number;
|
||||
@IsObject()
|
||||
address?: Address;
|
||||
|
||||
/**
|
||||
* The contact's phone number.
|
||||
@@ -47,39 +47,51 @@ export class CreateGroupContact {
|
||||
phone?: string;
|
||||
|
||||
/**
|
||||
* The contact's email address.
|
||||
* The new contact's email address.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* Gets the new contact's address by it's id.
|
||||
* The new contacts's groups' ids.
|
||||
* You can provide either one groupId or an array of groupIDs.
|
||||
*/
|
||||
public async getAddress(): Promise<Address> {
|
||||
if (this.address === undefined || this.address === null) {
|
||||
return null;
|
||||
}
|
||||
if (!isNaN(this.address)) {
|
||||
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
|
||||
if (!address) { throw new AddressNotFoundError; }
|
||||
return address;
|
||||
}
|
||||
@IsOptional()
|
||||
groups?: number[] | number
|
||||
|
||||
throw new AddressWrongTypeError;
|
||||
|
||||
/**
|
||||
* Get's all groups for this contact by their id's;
|
||||
*/
|
||||
public async getGroups(): Promise<RunnerGroup[]> {
|
||||
if (!this.groups) { return null; }
|
||||
let groups = new Array<RunnerGroup>();
|
||||
if (!Array.isArray(this.groups)) {
|
||||
this.groups = [this.groups]
|
||||
}
|
||||
for (let group of this.groups) {
|
||||
let found = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: group });
|
||||
if (!found) { throw new RunnerGroupNotFoundError(); }
|
||||
groups.push(found);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Address entity from this.
|
||||
* Creates a new GroupContact entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<GroupContact> {
|
||||
let contact: GroupContact = new GroupContact();
|
||||
contact.firstname = this.firstname;
|
||||
contact.middlename = this.middlename;
|
||||
contact.lastname = this.lastname;
|
||||
contact.email = this.email;
|
||||
contact.phone = this.phone;
|
||||
contact.address = await this.getAddress();
|
||||
return null;
|
||||
let newContact: GroupContact = new GroupContact();
|
||||
newContact.firstname = this.firstname;
|
||||
newContact.middlename = this.middlename;
|
||||
newContact.lastname = this.lastname;
|
||||
newContact.email = this.email;
|
||||
newContact.phone = this.phone;
|
||||
newContact.address = this.address;
|
||||
Address.validate(newContact.address);
|
||||
newContact.groups = await this.getGroups();
|
||||
|
||||
return newContact;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||
import { config } from '../../../config';
|
||||
import { AddressNotFoundError, AddressWrongTypeError } from '../../../errors/AddressErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
|
||||
/**
|
||||
@@ -48,25 +46,15 @@ export abstract class CreateParticipant {
|
||||
|
||||
/**
|
||||
* The new participant's address.
|
||||
* Must be of type number (address id).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
address?: number;
|
||||
@IsObject()
|
||||
address?: Address;
|
||||
|
||||
/**
|
||||
* Gets the new participant's address by it's address.
|
||||
* how the participant got into the system
|
||||
*/
|
||||
public async getAddress(): Promise<Address> {
|
||||
if (this.address === undefined || this.address === null) {
|
||||
return null;
|
||||
}
|
||||
if (!isNaN(this.address)) {
|
||||
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
|
||||
if (!address) { throw new AddressNotFoundError; }
|
||||
return address;
|
||||
}
|
||||
|
||||
throw new AddressWrongTypeError;
|
||||
}
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
created_via?: string;
|
||||
}
|
||||
@@ -1,39 +1,33 @@
|
||||
import { IsEmail, IsOptional, IsString } from 'class-validator';
|
||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { ResetAlreadyRequestedError, UserDisabledError, UserNotFoundError } from '../../../errors/AuthError';
|
||||
import { UsernameOrEmailNeededError } from '../../../errors/UserErrors';
|
||||
import { UserEmailNeededError } from '../../../errors/UserErrors';
|
||||
import { JwtCreator } from '../../../jwtcreator';
|
||||
import { User } from '../../entities/User';
|
||||
|
||||
/**
|
||||
* This calss is used to create password reset tokens for users.
|
||||
* This class is used to create password reset tokens for users.
|
||||
* These password reset token can be used to set a new password for the user for the next 15mins.
|
||||
*/
|
||||
export class CreateResetToken {
|
||||
/**
|
||||
* The username of the user that wants to reset their password.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
username?: string;
|
||||
|
||||
/**
|
||||
* The email address of the user that wants to reset their password.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
@IsString()
|
||||
email?: string;
|
||||
email: string;
|
||||
|
||||
|
||||
/**
|
||||
* Create a password reset token based on this.
|
||||
*/
|
||||
public async toResetToken(): Promise<any> {
|
||||
if (this.email === undefined && this.username === undefined) {
|
||||
throw new UsernameOrEmailNeededError();
|
||||
public async toResetToken(): Promise<string> {
|
||||
if (!this.email) {
|
||||
throw new UserEmailNeededError();
|
||||
}
|
||||
let found_user = await getConnectionManager().get().getRepository(User).findOne({ where: [{ username: this.username }, { email: this.email }] });
|
||||
let found_user = await getConnectionManager().get().getRepository(User).findOne({ where: [{ email: this.email }] });
|
||||
if (!found_user) { throw new UserNotFoundError(); }
|
||||
if (found_user.enabled == false) { throw new UserDisabledError(); }
|
||||
if (found_user.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 15 * 60)) { throw new ResetAlreadyRequestedError(); }
|
||||
@@ -43,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;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { IsInt } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
|
||||
import { RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
|
||||
import { RunnerOrganizationWrongTypeError } from '../../../errors/RunnerOrganizationErrors';
|
||||
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { Runner } from '../../entities/Runner';
|
||||
import { RunnerGroup } from '../../entities/RunnerGroup';
|
||||
import { CreateParticipant } from './CreateParticipant';
|
||||
@@ -30,7 +31,11 @@ export class CreateRunner extends CreateParticipant {
|
||||
newRunner.phone = this.phone;
|
||||
newRunner.email = this.email;
|
||||
newRunner.group = await this.getGroup();
|
||||
newRunner.address = await this.getAddress();
|
||||
newRunner.address = this.address;
|
||||
if (this.created_via) {
|
||||
newRunner.created_via = this.created_via;
|
||||
}
|
||||
Address.validate(newRunner.address);
|
||||
|
||||
return newRunner;
|
||||
}
|
||||
@@ -48,6 +53,6 @@ export class CreateRunner extends CreateParticipant {
|
||||
return group;
|
||||
}
|
||||
|
||||
throw new RunnerOrganisationWrongTypeError;
|
||||
throw new RunnerOrganizationWrongTypeError;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { RunnerCard } from '../../entities/RunnerCard';
|
||||
*/
|
||||
export class CreateRunnerCard {
|
||||
/**
|
||||
* The card's associated runner.
|
||||
* The card's associated runner's id.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { GroupContactNotFoundError, GroupContactWrongTypeError } from '../../../errors/GroupContactErrors';
|
||||
import { GroupContactNotFoundError } from '../../../errors/GroupContactErrors';
|
||||
import { GroupContact } from '../../entities/GroupContact';
|
||||
|
||||
/**
|
||||
@@ -15,7 +15,7 @@ export abstract class CreateRunnerGroup {
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The new group's contact.
|
||||
* The new group's contact's id.
|
||||
* Optional
|
||||
*/
|
||||
@IsInt()
|
||||
@@ -26,15 +26,10 @@ export abstract class CreateRunnerGroup {
|
||||
* Gets the new group's contact by it's id.
|
||||
*/
|
||||
public async getContact(): Promise<GroupContact> {
|
||||
if (this.contact === undefined || this.contact === null) {
|
||||
return null;
|
||||
}
|
||||
if (!isNaN(this.contact)) {
|
||||
let contact = await getConnectionManager().get().getRepository(GroupContact).findOne({ id: this.contact });
|
||||
if (!contact) { throw new GroupContactNotFoundError; }
|
||||
return contact;
|
||||
}
|
||||
if (!this.contact) { return null; }
|
||||
let contact = await getConnectionManager().get().getRepository(GroupContact).findOne({ id: this.contact });
|
||||
if (!contact) { throw new GroupContactNotFoundError; }
|
||||
return contact;
|
||||
|
||||
throw new GroupContactWrongTypeError;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { IsInt, IsOptional } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { AddressNotFoundError, AddressWrongTypeError } from '../../../errors/AddressErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
|
||||
import { CreateRunnerGroup } from './CreateRunnerGroup';
|
||||
|
||||
/**
|
||||
* This classed is used to create a new RunnerOrganisation entity from a json body (post request).
|
||||
*/
|
||||
export class CreateRunnerOrganisation extends CreateRunnerGroup {
|
||||
/**
|
||||
* The new organisation's address.
|
||||
* Must be of type number (address id).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
address?: number;
|
||||
|
||||
/**
|
||||
* Gets the org's address by it's id.
|
||||
*/
|
||||
public async getAddress(): Promise<Address> {
|
||||
if (this.address === undefined || this.address === null) {
|
||||
return null;
|
||||
}
|
||||
if (!isNaN(this.address)) {
|
||||
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
|
||||
if (!address) { throw new AddressNotFoundError; }
|
||||
return address;
|
||||
}
|
||||
|
||||
throw new AddressWrongTypeError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new RunnerOrganisation entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<RunnerOrganisation> {
|
||||
let newRunnerOrganisation: RunnerOrganisation = new RunnerOrganisation();
|
||||
|
||||
newRunnerOrganisation.name = this.name;
|
||||
newRunnerOrganisation.contact = await this.getContact();
|
||||
// newRunnerOrganisation.address = await this.getAddress();
|
||||
|
||||
return newRunnerOrganisation;
|
||||
}
|
||||
}
|
||||
42
src/models/actions/create/CreateRunnerOrganization.ts
Normal file
42
src/models/actions/create/CreateRunnerOrganization.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { IsBoolean, IsObject, IsOptional } from 'class-validator';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { RunnerOrganization } from '../../entities/RunnerOrganization';
|
||||
import { CreateRunnerGroup } from './CreateRunnerGroup';
|
||||
|
||||
|
||||
/**
|
||||
* This classed is used to create a new RunnerOrganization entity from a json body (post request).
|
||||
*/
|
||||
export class CreateRunnerOrganization extends CreateRunnerGroup {
|
||||
/**
|
||||
* The new organization's address.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
address?: Address;
|
||||
|
||||
/**
|
||||
* Is registration enabled for the new organization?
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
registrationEnabled?: boolean = false;
|
||||
|
||||
/**
|
||||
* Creates a new RunnerOrganization entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<RunnerOrganization> {
|
||||
let newRunnerOrganization: RunnerOrganization = new RunnerOrganization();
|
||||
|
||||
newRunnerOrganization.name = this.name;
|
||||
newRunnerOrganization.contact = await this.getContact();
|
||||
newRunnerOrganization.address = this.address;
|
||||
Address.validate(newRunnerOrganization.address);
|
||||
|
||||
if (this.registrationEnabled) {
|
||||
newRunnerOrganization.key = crypto.randomUUID()
|
||||
}
|
||||
|
||||
return newRunnerOrganization;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IsInt, IsNotEmpty } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
|
||||
import { RunnerOrganizationNotFoundError } from '../../../errors/RunnerOrganizationErrors';
|
||||
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
|
||||
import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
|
||||
import { RunnerOrganization } from '../../entities/RunnerOrganization';
|
||||
import { RunnerTeam } from '../../entities/RunnerTeam';
|
||||
import { CreateRunnerGroup } from './CreateRunnerGroup';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { CreateRunnerGroup } from './CreateRunnerGroup';
|
||||
export class CreateRunnerTeam extends CreateRunnerGroup {
|
||||
|
||||
/**
|
||||
* The new team's parent group (organisation).
|
||||
* The new team's parent org's id.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
@@ -21,17 +21,13 @@ export class CreateRunnerTeam extends CreateRunnerGroup {
|
||||
/**
|
||||
* Gets the new team's parent org based on it's id.
|
||||
*/
|
||||
public async getParent(): Promise<RunnerOrganisation> {
|
||||
public async getParent(): Promise<RunnerOrganization> {
|
||||
if (this.parentGroup === undefined || this.parentGroup === null) {
|
||||
throw new RunnerTeamNeedsParentError();
|
||||
}
|
||||
if (!isNaN(this.parentGroup)) {
|
||||
let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: this.parentGroup });
|
||||
if (!parentGroup) { throw new RunnerOrganisationNotFoundError();; }
|
||||
return parentGroup;
|
||||
}
|
||||
|
||||
throw new RunnerOrganisationWrongTypeError;
|
||||
let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganization).findOne({ id: this.parentGroup });
|
||||
if (!parentGroup) { throw new RunnerOrganizationNotFoundError();; }
|
||||
return parentGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +38,6 @@ export class CreateRunnerTeam extends CreateRunnerGroup {
|
||||
|
||||
newRunnerTeam.name = this.name;
|
||||
newRunnerTeam.parentGroup = await this.getParent();
|
||||
|
||||
newRunnerTeam.contact = await this.getContact()
|
||||
|
||||
return newRunnerTeam;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Scan } from '../../entities/Scan';
|
||||
*/
|
||||
export abstract class CreateScan {
|
||||
/**
|
||||
* The scan's associated runner.
|
||||
* The scan's associated runner's id.
|
||||
* This is important to link ran distances to runners.
|
||||
*/
|
||||
@IsInt()
|
||||
|
||||
@@ -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';
|
||||
@@ -19,7 +18,7 @@ export class CreateScanStation {
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* The station's associated track.
|
||||
* The station's associated track's id.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
53
src/models/actions/create/CreateSelfServiceCitizenRunner.ts
Normal file
53
src/models/actions/create/CreateSelfServiceCitizenRunner.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { RunnerEmailNeededError } from '../../../errors/RunnerErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { Runner } from '../../entities/Runner';
|
||||
import { RunnerOrganization } from '../../entities/RunnerOrganization';
|
||||
import { CreateParticipant } from './CreateParticipant';
|
||||
|
||||
/**
|
||||
* This classed is used to create a new Runner entity from a json body (post request).
|
||||
*/
|
||||
export class CreateSelfServiceCitizenRunner extends CreateParticipant {
|
||||
|
||||
/**
|
||||
* The new runners's e-mail address.
|
||||
* Must be provided for email-verification to work.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* Creates a new Runner entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<Runner> {
|
||||
let newRunner: Runner = new Runner();
|
||||
|
||||
newRunner.created_via = "selfservice";
|
||||
newRunner.firstname = this.firstname;
|
||||
newRunner.middlename = this.middlename;
|
||||
newRunner.lastname = this.lastname;
|
||||
newRunner.phone = this.phone;
|
||||
newRunner.email = this.email;
|
||||
|
||||
if (!newRunner.email) {
|
||||
throw new RunnerEmailNeededError();
|
||||
}
|
||||
|
||||
newRunner.group = await this.getGroup();
|
||||
newRunner.address = this.address;
|
||||
Address.validate(newRunner.address);
|
||||
|
||||
return newRunner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the new runner's group by it's id.
|
||||
*/
|
||||
public async getGroup(): Promise<RunnerOrganization> {
|
||||
return await getConnection().getRepository(RunnerOrganization).findOne({ id: 1 });
|
||||
}
|
||||
}
|
||||
56
src/models/actions/create/CreateSelfServiceRunner.ts
Normal file
56
src/models/actions/create/CreateSelfServiceRunner.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { IsInt, IsOptional } from 'class-validator';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { RunnerTeamNotFoundError } from '../../../errors/RunnerTeamErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { Runner } from '../../entities/Runner';
|
||||
import { RunnerGroup } from '../../entities/RunnerGroup';
|
||||
import { RunnerTeam } from '../../entities/RunnerTeam';
|
||||
import { CreateParticipant } from './CreateParticipant';
|
||||
|
||||
/**
|
||||
* This classed is used to create a new Runner entity from a json body (post request).
|
||||
*/
|
||||
export class CreateSelfServiceRunner extends CreateParticipant {
|
||||
|
||||
/**
|
||||
* The new runner's team's id.
|
||||
* The team has to be a part of the runner's org.
|
||||
* The team property may get ignored.
|
||||
* If no team get's provided the runner's group will be their org.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
team?: number;
|
||||
|
||||
/**
|
||||
* Creates a new Runner entity from this.
|
||||
*/
|
||||
public async toEntity(group: RunnerGroup): Promise<Runner> {
|
||||
let newRunner: Runner = new Runner();
|
||||
|
||||
newRunner.created_via = "selfservice";
|
||||
newRunner.firstname = this.firstname;
|
||||
newRunner.middlename = this.middlename;
|
||||
newRunner.lastname = this.lastname;
|
||||
newRunner.phone = this.phone;
|
||||
newRunner.email = this.email;
|
||||
newRunner.group = await this.getGroup(group);
|
||||
newRunner.address = this.address;
|
||||
Address.validate(newRunner.address);
|
||||
|
||||
return newRunner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the new runner's group by it's id.
|
||||
*/
|
||||
public async getGroup(group: RunnerGroup): Promise<RunnerGroup> {
|
||||
if (!this.team) {
|
||||
return group;
|
||||
}
|
||||
const team = await getConnection().getRepository(RunnerTeam).findOne({ id: this.team }, { relations: ["parentGroup"] });
|
||||
if (!team) { throw new RunnerTeamNotFoundError(); }
|
||||
if (team.parentGroup.id != group.id) { throw new RunnerTeamNotFoundError(); }
|
||||
return team;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -12,7 +13,7 @@ import { TrackScan } from '../../entities/TrackScan';
|
||||
*/
|
||||
export class CreateTrackScan {
|
||||
/**
|
||||
* The runnerCard associated with the scan.
|
||||
* The id of the runnerCard associated with the scan.
|
||||
* This get's saved for documentation and management purposes.
|
||||
*/
|
||||
@IsInt()
|
||||
@@ -20,12 +21,14 @@ export class CreateTrackScan {
|
||||
card: number;
|
||||
|
||||
/**
|
||||
* The scanning station that created the scan.
|
||||
* Mainly used for logging and traceing back scans (or errors)
|
||||
* 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';
|
||||
@@ -71,7 +71,7 @@ export class CreateUser {
|
||||
enabled?: boolean = true;
|
||||
|
||||
/**
|
||||
* The new user's groups' id(s).
|
||||
* The new user's groups' ids.
|
||||
* You can provide either one groupId or an array of groupIDs.
|
||||
*/
|
||||
@IsOptional()
|
||||
@@ -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;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { UpdateDonation } from './UpdateDonation';
|
||||
export class UpdateDistanceDonation extends UpdateDonation {
|
||||
|
||||
/**
|
||||
* The donation's associated runner.
|
||||
* The donation's associated runner's id.
|
||||
* This is important to link the runner's distance ran to the donation.
|
||||
*/
|
||||
@IsInt()
|
||||
@@ -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';
|
||||
@@ -16,13 +16,20 @@ export abstract class UpdateDonation {
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The updated donation's associated donor.
|
||||
* The updated donation's associated donor's id.
|
||||
* This is important to link donations to donors.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
donor: number;
|
||||
|
||||
/**
|
||||
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
paidAmount?: number;
|
||||
|
||||
/**
|
||||
* Creates a new Donation entity from this.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
|
||||
import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { Donor } from '../../entities/Donor';
|
||||
import { CreateParticipant } from '../create/CreateParticipant';
|
||||
|
||||
@@ -33,9 +34,10 @@ export class UpdateDonor extends CreateParticipant {
|
||||
donor.phone = this.phone;
|
||||
donor.email = this.email;
|
||||
donor.receiptNeeded = this.receiptNeeded;
|
||||
donor.address = await this.getAddress();
|
||||
|
||||
if (this.receiptNeeded == true && this.address == null) {
|
||||
if (!this.address) { donor.address.reset(); }
|
||||
else { donor.address = this.address; }
|
||||
Address.validate(donor.address);
|
||||
if (this.receiptNeeded == true && Address.isValidAddress(donor.address) == false) {
|
||||
throw new DonorReceiptAddressNeededError()
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
106
src/models/actions/update/UpdateGroupContact.ts
Normal file
106
src/models/actions/update/UpdateGroupContact.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { IsEmail, IsInt, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { config } from '../../../config';
|
||||
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { GroupContact } from '../../entities/GroupContact';
|
||||
import { RunnerGroup } from '../../entities/RunnerGroup';
|
||||
|
||||
|
||||
/**
|
||||
* This class is used to update a GroupContact entity (via put request).
|
||||
*/
|
||||
export class UpdateGroupContact {
|
||||
/**
|
||||
* The updated contact's id.
|
||||
* This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
|
||||
*/
|
||||
@IsInt()
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The updated contact's first name.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
firstname: string;
|
||||
|
||||
/**
|
||||
* The updated contact's middle name.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
middlename?: string;
|
||||
|
||||
/**
|
||||
* The updated contact's last name.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
lastname: string;
|
||||
|
||||
/**
|
||||
* The updated contact's address.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
address?: Address;
|
||||
|
||||
/**
|
||||
* The updated contact's phone number.
|
||||
* This will be validated against the configured country phone numer syntax (default: international).
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsPhoneNumber(config.phone_validation_countrycode)
|
||||
phone?: string;
|
||||
|
||||
/**
|
||||
* The updated contact's email address.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* The updated contacts's groups' ids.
|
||||
* You can provide either one groupId or an array of groupIDs.
|
||||
*/
|
||||
@IsOptional()
|
||||
groups?: number[] | number
|
||||
|
||||
|
||||
/**
|
||||
* Get's all groups for this contact by their id's;
|
||||
*/
|
||||
public async getGroups(): Promise<RunnerGroup[]> {
|
||||
if (!this.groups) { return null; }
|
||||
let groups = new Array<RunnerGroup>();
|
||||
if (!Array.isArray(this.groups)) {
|
||||
this.groups = [this.groups]
|
||||
}
|
||||
for (let group of this.groups) {
|
||||
let found = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: group });
|
||||
if (!found) { throw new RunnerGroupNotFoundError(); }
|
||||
groups.push(found);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a provided Donor entity based on this.
|
||||
* @param contact the contact you want to update.
|
||||
*/
|
||||
public async update(contact: GroupContact): Promise<GroupContact> {
|
||||
contact.firstname = this.firstname; GroupContact
|
||||
contact.middlename = this.middlename;
|
||||
contact.lastname = this.lastname;
|
||||
contact.phone = this.phone;
|
||||
contact.email = this.email;
|
||||
if (!this.address) { contact.address.reset(); }
|
||||
else { contact.address = this.address; }
|
||||
Address.validate(contact.address);
|
||||
contact.groups = await this.getGroups();
|
||||
|
||||
return contact;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IsInt, IsNotEmpty, IsObject } from 'class-validator';
|
||||
import { IsInt, IsNotEmpty, IsPositive } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { PermissionNeedsPrincipalError } from '../../../errors/PermissionErrors';
|
||||
import { PrincipalNotFoundError, PrincipalWrongTypeError } from '../../../errors/PrincipalErrors';
|
||||
import { PrincipalNotFoundError } from '../../../errors/PrincipalErrors';
|
||||
import { Permission } from '../../entities/Permission';
|
||||
import { Principal } from '../../entities/Principal';
|
||||
import { PermissionAction } from '../../enums/PermissionAction';
|
||||
@@ -20,12 +20,11 @@ export class UpdatePermission {
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The updated permissions's principal.
|
||||
* Just has to contain the principal's id -everything else won't be checked or changed.
|
||||
* The updated permissions's principal's id.
|
||||
*/
|
||||
@IsObject()
|
||||
@IsNotEmpty()
|
||||
principal: Principal;
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
principal: number;
|
||||
|
||||
/**
|
||||
* The permissions's target.
|
||||
@@ -57,12 +56,8 @@ export class UpdatePermission {
|
||||
if (this.principal === undefined || this.principal === null) {
|
||||
throw new PermissionNeedsPrincipalError();
|
||||
}
|
||||
if (!isNaN(this.principal.id)) {
|
||||
let principal = await getConnectionManager().get().getRepository(Principal).findOne({ id: this.principal.id });
|
||||
if (!principal) { throw new PrincipalNotFoundError(); }
|
||||
return principal;
|
||||
}
|
||||
|
||||
throw new PrincipalWrongTypeError();
|
||||
let principal = await getConnectionManager().get().getRepository(Principal).findOne({ id: this.principal });
|
||||
if (!principal) { throw new PrincipalNotFoundError(); }
|
||||
return principal;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IsInt, IsObject } from 'class-validator';
|
||||
import { IsInt, IsPositive } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
|
||||
import { RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
|
||||
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { Runner } from '../../entities/Runner';
|
||||
import { RunnerGroup } from '../../entities/RunnerGroup';
|
||||
import { CreateParticipant } from '../create/CreateParticipant';
|
||||
@@ -20,11 +20,11 @@ export class UpdateRunner extends CreateParticipant {
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The updated runner's new team/org.
|
||||
* Just has to contain the group's id -everything else won't be checked or changed.
|
||||
* The updated runner's group's id.
|
||||
*/
|
||||
@IsObject()
|
||||
group: RunnerGroup;
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
group: number;
|
||||
|
||||
/**
|
||||
* Updates a provided Runner entity based on this.
|
||||
@@ -36,7 +36,9 @@ export class UpdateRunner extends CreateParticipant {
|
||||
runner.phone = this.phone;
|
||||
runner.email = this.email;
|
||||
runner.group = await this.getGroup();
|
||||
runner.address = await this.getAddress();
|
||||
if (!this.address) { runner.address.reset(); }
|
||||
else { runner.address = this.address; }
|
||||
Address.validate(runner.address);
|
||||
|
||||
return runner;
|
||||
}
|
||||
@@ -48,12 +50,8 @@ export class UpdateRunner extends CreateParticipant {
|
||||
if (this.group === undefined || this.group === null) {
|
||||
throw new RunnerTeamNeedsParentError();
|
||||
}
|
||||
if (!isNaN(this.group.id)) {
|
||||
let group = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: this.group.id });
|
||||
if (!group) { throw new RunnerGroupNotFoundError; }
|
||||
return group;
|
||||
}
|
||||
|
||||
throw new RunnerOrganisationWrongTypeError;
|
||||
let group = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: this.group });
|
||||
if (!group) { throw new RunnerGroupNotFoundError; }
|
||||
return group;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export class UpdateRunnerCard {
|
||||
id?: number;
|
||||
|
||||
/**
|
||||
* The updated card's associated runner.
|
||||
* The updated card's associated runner's id.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
|
||||
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,52 +0,0 @@
|
||||
import { IsInt, IsOptional } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { AddressNotFoundError } from '../../../errors/AddressErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
|
||||
import { CreateRunnerGroup } from '../create/CreateRunnerGroup';
|
||||
|
||||
/**
|
||||
* This class is used to update a RunnerOrganisation entity (via put request).
|
||||
*/
|
||||
export class UpdateRunnerOrganisation extends CreateRunnerGroup {
|
||||
|
||||
/**
|
||||
* The updated orgs's id.
|
||||
* This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
|
||||
*/
|
||||
@IsInt()
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The updated organisation's address.
|
||||
* Just has to contain the address's id - everything else won't be checked or changed.
|
||||
* Optional.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
address?: Address;
|
||||
|
||||
/**
|
||||
* Loads the organisation's address based on it's id.
|
||||
*/
|
||||
public async getAddress(): Promise<Address> {
|
||||
if (this.address === undefined || this.address === null) {
|
||||
return null;
|
||||
}
|
||||
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address.id });
|
||||
if (!address) { throw new AddressNotFoundError; }
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a provided RunnerOrganisation entity based on this.
|
||||
*/
|
||||
public async update(organisation: RunnerOrganisation): Promise<RunnerOrganisation> {
|
||||
|
||||
organisation.name = this.name;
|
||||
organisation.contact = await this.getContact();
|
||||
// organisation.address = await this.getAddress();
|
||||
|
||||
return organisation;
|
||||
}
|
||||
}
|
||||
52
src/models/actions/update/UpdateRunnerOrganization.ts
Normal file
52
src/models/actions/update/UpdateRunnerOrganization.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { IsBoolean, IsInt, IsObject, IsOptional } from 'class-validator';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { RunnerOrganization } from '../../entities/RunnerOrganization';
|
||||
import { CreateRunnerGroup } from '../create/CreateRunnerGroup';
|
||||
|
||||
/**
|
||||
* This class is used to update a RunnerOrganization entity (via put request).
|
||||
*/
|
||||
export class UpdateRunnerOrganization extends CreateRunnerGroup {
|
||||
|
||||
/**
|
||||
* The updated orgs's id.
|
||||
* This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
|
||||
*/
|
||||
@IsInt()
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The updated organization's address.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
address?: Address;
|
||||
|
||||
/**
|
||||
* Is registration enabled for the updated organization?
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
registrationEnabled?: boolean = false;
|
||||
|
||||
/**
|
||||
* Updates a provided RunnerOrganization entity based on this.
|
||||
*/
|
||||
public async update(organization: RunnerOrganization): Promise<RunnerOrganization> {
|
||||
|
||||
organization.name = this.name;
|
||||
organization.contact = await this.getContact();
|
||||
if (!this.address) { organization.address.reset(); }
|
||||
else { organization.address = this.address; }
|
||||
Address.validate(organization.address);
|
||||
|
||||
if (this.registrationEnabled && !organization.key) {
|
||||
organization.key = crypto.randomUUID().toUpperCase();
|
||||
}
|
||||
else {
|
||||
organization.key = null;
|
||||
}
|
||||
|
||||
return organization;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IsInt, IsNotEmpty, IsObject } from 'class-validator';
|
||||
import { IsInt, IsPositive } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
|
||||
import { RunnerOrganizationNotFoundError } from '../../../errors/RunnerOrganizationErrors';
|
||||
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
|
||||
import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
|
||||
import { RunnerOrganization } from '../../entities/RunnerOrganization';
|
||||
import { RunnerTeam } from '../../entities/RunnerTeam';
|
||||
import { CreateRunnerGroup } from '../create/CreateRunnerGroup';
|
||||
|
||||
@@ -19,27 +19,22 @@ export class UpdateRunnerTeam extends CreateRunnerGroup {
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The updated team's parentGroup.
|
||||
* Just has to contain the organisation's id - everything else won't be checked or changed.
|
||||
* The updated team's parentGroup's id.
|
||||
*/
|
||||
@IsObject()
|
||||
@IsNotEmpty()
|
||||
parentGroup: RunnerOrganisation;
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
parentGroup: number;
|
||||
|
||||
/**
|
||||
* Loads the updated teams's parentGroup based on it's id.
|
||||
*/
|
||||
public async getParent(): Promise<RunnerOrganisation> {
|
||||
public async getParent(): Promise<RunnerOrganization> {
|
||||
if (this.parentGroup === undefined || this.parentGroup === null) {
|
||||
throw new RunnerTeamNeedsParentError();
|
||||
}
|
||||
if (!isNaN(this.parentGroup.id)) {
|
||||
let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: this.parentGroup.id });
|
||||
if (!parentGroup) { throw new RunnerOrganisationNotFoundError();; }
|
||||
return parentGroup;
|
||||
}
|
||||
|
||||
throw new RunnerOrganisationWrongTypeError;
|
||||
let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganization).findOne({ id: this.parentGroup });
|
||||
if (!parentGroup) { throw new RunnerOrganizationNotFoundError();; }
|
||||
return parentGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@ export abstract class UpdateScan {
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The updated scan's associated runner.
|
||||
* The updated scan's associated runner's id.
|
||||
* This is important to link ran distances to runners.
|
||||
*/
|
||||
@IsInt()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -18,12 +18,12 @@ export abstract class UpdateTrackScan {
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The updated scan's associated runner.
|
||||
* The updated scan's associated runner's id.
|
||||
* This is important to link ran distances to runners.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
runner?: number;
|
||||
@IsPositive()
|
||||
runner: number;
|
||||
|
||||
/**
|
||||
* Is the updated scan valid (for fraud reasons).
|
||||
@@ -33,12 +33,12 @@ export abstract class UpdateTrackScan {
|
||||
valid?: boolean = true;
|
||||
|
||||
/**
|
||||
* The updated scan's associated station.
|
||||
* The updated scan's associated station's id.
|
||||
* This is important to link ran distances to runners.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
public station?: number;
|
||||
@IsPositive()
|
||||
public track: number;
|
||||
|
||||
/**
|
||||
* Update a TrackScan entity based on this.
|
||||
@@ -46,13 +46,8 @@ export abstract class UpdateTrackScan {
|
||||
*/
|
||||
public async update(scan: TrackScan): Promise<TrackScan> {
|
||||
scan.valid = this.valid;
|
||||
if (this.runner) {
|
||||
scan.runner = await this.getRunner();
|
||||
}
|
||||
if (this.station) {
|
||||
scan.station = await this.getStation();
|
||||
}
|
||||
scan.track = scan.station.track;
|
||||
scan.runner = await this.getRunner();
|
||||
scan.track = await this.getTrack();
|
||||
|
||||
return scan;
|
||||
}
|
||||
@@ -71,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 });
|
||||
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).
|
||||
*/
|
||||
@@ -76,14 +78,14 @@ export class UpdateUser {
|
||||
* Should the user be enabled?
|
||||
*/
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* The updated user's groups.
|
||||
* This just has to contain the group's id - everything else won't be changed.
|
||||
* The updated user's groups' ids.
|
||||
*/
|
||||
@IsOptional()
|
||||
groups?: UserGroup[]
|
||||
groups?: number | number[]
|
||||
|
||||
/**
|
||||
* The user's profile pic (or rather a url pointing to it).
|
||||
@@ -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,14 +124,14 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the updated user's groups based on their ids.
|
||||
* Get's all groups for this user by their id's;
|
||||
*/
|
||||
public async getGroups() {
|
||||
if (!this.groups) { return null; }
|
||||
@@ -133,7 +140,7 @@ export class UpdateUser {
|
||||
this.groups = [this.groups]
|
||||
}
|
||||
for (let group of this.groups) {
|
||||
let found = await getConnectionManager().get().getRepository(UserGroup).findOne({ id: group.id });
|
||||
let found = await getConnectionManager().get().getRepository(UserGroup).findOne({ id: group });
|
||||
if (!found) { throw new UserGroupNotFoundError(); }
|
||||
groups.push(found);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user