Compare commits
684 Commits
aef2f9562a
...
v0.0.8
| Author | SHA1 | Date | |
|---|---|---|---|
| d948fe2631 | |||
| 2b5525323b | |||
| b57fde9b0a | |||
| 86706f9422 | |||
| 0687f268fc | |||
| bc426831db | |||
| 276e553e13 | |||
| e7ab302c61 | |||
| a5d70ce4b5 | |||
| d67be313e6 | |||
| 15d2d029dc | |||
| b6ea5e6549 | |||
| f378b0651a | |||
| 1a0573e0d0 | |||
| 9f103d8df1 | |||
| daa899a1ef | |||
| 59cb72a11d | |||
| 28c1b6d31d | |||
| dcb791c9a2 | |||
| 907259bf73 | |||
| 02f7ddbb37 | |||
| 63b1ca9b56 | |||
| 39857cf6e6 | |||
| 3090ae69f3 | |||
| 92186a86cc | |||
| 97e8470b0d | |||
| 6b0e3503a7 | |||
| 1e2de7656e | |||
| 56c6a7efb0 | |||
| 9c4e54fc6e | |||
| 2c47436259 | |||
| 9b5d16ae92 | |||
| deb13674b2 | |||
| 17c82ff409 | |||
| f9e314bf9f | |||
| e4c1930dd1 | |||
| b337ab424d | |||
| 82a0e194cb | |||
| 599296c4e3 | |||
| 2594a607dc | |||
| 335d4e24da | |||
| becc277123 | |||
| 52cdd41ec8 | |||
| 53548ba7a6 | |||
| 1dc438beb2 | |||
| c9ba69792f | |||
| ab67e5f4aa | |||
| 557608e318 | |||
| a83fedc9b8 | |||
| 61a17b198f | |||
| 3df1db4ad8 | |||
| e46cfa0d77 | |||
| 4126d31a5e | |||
| 9d9549cdd4 | |||
| eb40de6eb4 | |||
| 6efd09db73 | |||
| 3f09e3d387 | |||
| 05868e0e00 | |||
| 580a73f9a5 | |||
| ab7110d49f | |||
| 875781335c | |||
| 625340cf8a | |||
| 8d9dbc3957 | |||
| 07d813082b | |||
| a684f60252 | |||
| 931cae3c98 | |||
| dfd82a6293 | |||
| 82d4b11de3 | |||
| 75473937cf | |||
| a68bbab8ab | |||
| 5cfd2c9a52 | |||
| 6c7b31d76c | |||
| 2924ac2900 | |||
| a501625dd6 | |||
| cc64ce4498 | |||
| c9378e6cae | |||
| 62c7f26540 | |||
| 18e3ef9a79 | |||
| 395b0101a8 | |||
| 84a0bd2cd9 | |||
| 9cd181c5b8 | |||
| 41828a6e41 | |||
| 356e398caf | |||
| 6cb978df98 | |||
| 4cb0efa6bd | |||
| e0fa58da57 | |||
| 5d31d8d1a2 | |||
| 53a01ad977 | |||
| d7791756dc | |||
| dd48ee2f7e | |||
| ec64ec3d63 | |||
| 35dbfeb5e7 | |||
| a9ecfccfd2 | |||
| d850650aeb | |||
| 43e256f38c | |||
| b5f9cf201d | |||
| 6e121a3ce2 | |||
| 555e37eaf7 | |||
| 9675e79441 | |||
| 345851bf1d | |||
| 7c5a3893ef | |||
| b53b5cf91f | |||
| 04813173e4 | |||
| c4270b0839 | |||
| bb24ed53a4 | |||
| 1b74b21420 | |||
| b7cbe2a0b4 | |||
| 500b94b44a | |||
| 641466a731 | |||
| e3ea83bb47 | |||
| b6043744a9 | |||
| 2b38044271 | |||
| 4c3d2643c1 | |||
| e2cc0c0b80 | |||
| ce55dce011 | |||
| a738c19316 | |||
| 63b8176bdf | |||
| bc76afafce | |||
| 1f49ad43a1 | |||
| 6a762f570d | |||
| 1b7424f750 | |||
| bdd4f705be | |||
| ded14b1b3b | |||
| fbd3f615ad | |||
| a22a7a19c2 | |||
| 2d263814db | |||
| a79bed259b | |||
| f2970f4cd8 | |||
| b3f741234e | |||
| 6a8247f88a | |||
| b737fe6a08 | |||
| 607630c4f9 | |||
| a7976c0ee2 | |||
| b51da15007 | |||
| 5ed5f181d1 | |||
| e33076c04d | |||
| ae35f50da2 | |||
| cc5d90cb4f | |||
| c33236c516 | |||
| eee2bbcac7 | |||
| 519d11beef | |||
| cbed5fc0b2 | |||
| 59fdfe9f40 | |||
| c93e93be31 | |||
| d3760f7b80 | |||
| 11c7d041ef | |||
| 9ab6eb5314 | |||
| ce0500ef8c | |||
| 0b4d30b3f3 | |||
| bb70bf58fb | |||
| 9fc282d858 | |||
| 39ad43bbb2 | |||
| bd46a48f76 | |||
| ebedea97ed | |||
| 5c3c3eb167 | |||
| d8e38f404d | |||
| aa1042ca51 | |||
| 9994f8ddc4 | |||
| 3ac536ef23 | |||
| 5d75f70296 | |||
| c34bde7d4f | |||
| 1f061c7ea6 | |||
| 578f9301db | |||
| 9b47f3ab05 | |||
| 84b97bee8d | |||
| 767841d405 | |||
| 16e5b6921d | |||
| 58a12c7fa1 | |||
| f256dec121 | |||
| 9bb4865b2d | |||
| 66631f5e0a | |||
| 8de35f3431 | |||
| 05319e6f6e | |||
| b7827fef54 | |||
| a4ddeee8e4 | |||
| 50f2462eb9 | |||
| dae51cfd47 | |||
| e1341fc126 | |||
| a9dbf1d0d2 | |||
| c6ecde29b5 | |||
| 13949af938 | |||
| 3c003a60b2 | |||
| 69796a888f | |||
| a85e914759 | |||
| af2744885f | |||
| 8d73a9dd59 | |||
| 853876a09c | |||
| cdc90b0770 | |||
| d0cfc16f8b | |||
| ce5f4b467d | |||
| 84a7f30a60 | |||
| f3008979f3 | |||
| b8c93bf476 | |||
| 146787fd66 | |||
| 9458b774ea | |||
| bf4250babd | |||
| a16c4c564a | |||
| 8d860cb2e1 | |||
| 2f7b0d5606 | |||
| 4b9bfe3b79 | |||
| 17ee682029 | |||
| 48685451be | |||
| 5aad581c2d | |||
| caeb17311b | |||
| 5aa83fe2f0 | |||
| aef8485f59 | |||
| 61aff5e629 | |||
| aa146cd6c1 | |||
| 6042089074 | |||
| b6cf3b24d4 | |||
| 19422edbae | |||
| 1bf6d3d564 | |||
| 7d5f3b092f | |||
| 0ef6d9cc48 | |||
| 48bef8db60 | |||
| 1d0d79f3da | |||
| d20d738218 | |||
| a03f1a438d | |||
| 75332983c2 | |||
| a85d52437b | |||
| a88c0389c1 | |||
| 43a4f1118d | |||
| de91d491e5 | |||
| 2199cb0aef | |||
| ee76f1c0e8 | |||
| 75b6489f8d | |||
| 389f6347c3 | |||
| 37afc10e44 | |||
| 82ced34750 | |||
| 5de81ad093 | |||
| c1d784e29c | |||
| 4ca85a1f22 | |||
| 7a4238f1f7 | |||
| fbe2b358bd | |||
| 532b5a56a5 | |||
| 18ede29ea5 | |||
| ec4d75128b | |||
| b2bd6173a5 | |||
| cc68948a20 | |||
| 24de82f6df | |||
| cf583a22fa | |||
| b55d210aff | |||
| adec2bcc5b | |||
| 3850bd9681 | |||
| 14b6651f96 | |||
| 4a21c1fb5c | |||
| ca142376b3 | |||
| 314606addd | |||
| a0a08f7724 | |||
| cea5993049 | |||
| 3e940c2db5 | |||
| 631310f158 | |||
| c3e3c6bed1 | |||
| 8f48d2593b | |||
| 23758e7a91 | |||
| c7fd0593fb | |||
| b19f18ada1 | |||
| d742ccd581 | |||
| d670b814a4 | |||
| 1a9c860188 | |||
| f25ae9ba4f | |||
| 744faba7ee | |||
| cdfd0e0d64 | |||
| e25fc795fe | |||
| 2240a45a91 | |||
| 595a9213c1 | |||
| 428e2c38ce | |||
| 1d54fb085b | |||
| 6403e386ab | |||
| 65a8449ea3 | |||
| b21dd6f0c0 | |||
| 445e96dcdf | |||
| 6237e62a03 | |||
| b9e91502cd | |||
| 9dc336f0bb | |||
| 6a7e8ccc37 | |||
| 882065470a | |||
| ff3a5b4545 | |||
| d4293c164d | |||
| 145a08b1b4 | |||
| dc485c02ea | |||
| ebb0c5faca | |||
| d89fcb84a2 | |||
| 388fc6ba6a | |||
| bb4ea485fd | |||
| 5dc9edfe40 | |||
| eb9473e230 | |||
| 476afc6a99 | |||
| ed53627bbe | |||
| efecffb72d | |||
| 3aae8f85c4 | |||
| cc5a30980a | |||
| c90f9f1dd4 | |||
| 15ed9f58d5 | |||
| 9db4344153 | |||
| 03b7e346ab | |||
| 0d8fbf1eca | |||
| 71228fbf33 | |||
| 97494aeaf7 | |||
| 4801e010b4 | |||
| 1b59d58c60 | |||
| cad30c7f63 | |||
| a8ec0142b0 | |||
| 30952aa14f | |||
| 2e4a4f1661 | |||
| b9fd2379f4 | |||
| 1b1f8f2b09 | |||
| 39b932a81c | |||
| ec69f6caf3 | |||
| ad908a3555 | |||
| 3e6c7b6302 | |||
| d0c5323cb6 | |||
| fcb3e35b29 | |||
| 4705b5a0b4 | |||
| 0c6f3d1f12 | |||
| ff178f9d77 | |||
| e59630b17e | |||
| 20ec6e0cd6 | |||
| e10a3947ba | |||
| 8d00487359 | |||
| f304b86cb6 | |||
| 421ddc50ed | |||
| c3aa88c212 | |||
| 10dbd233a0 | |||
| c321da613a | |||
| ff84209683 | |||
| df3c231fd2 | |||
| ac2da0af63 | |||
| 40fb081332 | |||
| 30928180e6 | |||
| 6aa1e0d573 | |||
| aca3eaaeea | |||
| 615b54ec4f | |||
| c07d40ae93 | |||
| db5da3d3c2 | |||
| 0e003d2dc4 | |||
| a1c3751164 | |||
| 359e955926 | |||
| c391201570 | |||
| e3980096e2 | |||
| a7e27c6f6c | |||
| bcb266e29b | |||
| 95f40a9c28 | |||
| 8bcaf710ad | |||
| b8aebc14e8 | |||
| 5ccdfe1540 | |||
| a1e3289a88 | |||
| 47e4f6cd7e | |||
| 36fbccb286 | |||
| 7429407843 | |||
| 10640f40aa | |||
| 1e625b0775 | |||
| 6cfaec8397 | |||
| 0f419625d2 | |||
| a83a23a647 | |||
| 553a35bb8e | |||
| ef3fcee2a9 | |||
| 61b2baaee7 | |||
| 31e7d074dc | |||
| 1fbddf5ef8 | |||
| 7a79f35b58 | |||
| 79e418f918 | |||
| abb13045e6 | |||
| d543dfb201 | |||
| 5eabc1fe18 | |||
| 473033aa50 | |||
| effa79032b | |||
| 57f6775140 | |||
| 5a27689e80 | |||
| d295100a48 | |||
| 09decd5600 | |||
| 6eee80d357 | |||
| cada962e5a | |||
| 721af32989 | |||
| 92dee666ee | |||
| 6e12b014e7 | |||
| c20f01f485 | |||
| eda8abb668 | |||
| 02877ece9c | |||
| f3000f14cd | |||
| 068e5bd72b | |||
| bd07763455 | |||
| 64725d9e7a | |||
| d2e0384f3c | |||
| e223c060d4 | |||
| 49ac7be367 | |||
| 9df86953cf | |||
| aaeef4a27e | |||
| d5e5e27ca3 | |||
| 7fe9480c94 | |||
| bc80be947d | |||
| 47862f2e1d | |||
| d0d050e6c6 | |||
| e2feffa1c5 | |||
| ff6a4eaca1 | |||
| 3e961e34a1 | |||
| 32a92b1ad7 | |||
| 105efdd454 | |||
| 6e316a7533 | |||
| 71e5be2ba4 | |||
| e3a5b41b5e | |||
| 80ef7e8c3a | |||
| d2898ff60f | |||
| 429041fef1 | |||
| df5b8ac141 | |||
| cbecff85f0 | |||
| 622bdf7a3f | |||
| a068c4d318 | |||
| 6396fffc04 | |||
| 5845a91f15 | |||
| f4abbfcee4 | |||
| c3258b9304 | |||
| 4cfe9df429 | |||
| 0fc0b87c67 | |||
| ff96ba23d7 | |||
| 3ae124ef68 | |||
| 09f4998499 | |||
| 18ef8df3a9 | |||
| 77b769446f | |||
| 00215a81a7 | |||
| af1ad482d4 | |||
| b4b52717fc | |||
| 02236caa41 | |||
| 2d603a1467 | |||
| 862834c877 | |||
| 76065538c9 | |||
| 99209981d9 | |||
| 204e2352a9 | |||
| 13f96e3190 | |||
| 4e3b038dec | |||
| 42d3f9cb98 | |||
| 0a0050368f | |||
| e4cb8eba1d | |||
| 6da7c23c04 | |||
| db5feb00cc | |||
| 381ce9c828 | |||
| 7bb7da4eed | |||
| 4df63a8cc0 | |||
| 34fa94ea4f | |||
| fcfc10f7d1 | |||
| 9f7d004c3b | |||
| 39cefbc593 | |||
| 2cdc91ec83 | |||
| 99d8a0360f | |||
| 1748fd4034 | |||
| 34567f24c3 | |||
| def7ca3eb2 | |||
| 4dd0217c93 | |||
| a3e8973004 | |||
| 29acabfca3 | |||
| 4ff6f8c540 | |||
| a671bf8bcb | |||
| 15e3d04215 | |||
| 07e03ff04e | |||
| 5103e8a6e5 | |||
| ad6c9e7211 | |||
| 1fb09e577c | |||
| f58a715c45 | |||
| a3a809bb40 | |||
| 33b3bcb8c2 | |||
| 1ae466a6f4 | |||
| 431fd608a6 | |||
| 21ad622c10 | |||
| 61e7ae4f86 | |||
| 8ae5cea631 | |||
| 0e924449d6 | |||
| 5c259484ee | |||
| 740d7f10f5 | |||
| 993096741d | |||
| 8607af62b5 | |||
| 76e19ca28d | |||
| 3ac150331a | |||
| 5a4a6cdcef | |||
| e5b605cc55 | |||
| 7e4ce00c30 | |||
| 13d568ba3f | |||
| 65b2399eaa | |||
| 4352910d54 | |||
| 8c229dba82 | |||
| 675717f8ca | |||
| 0d21497c2f | |||
| e5f65d0b80 | |||
| 51addd4a31 | |||
| 126799dab9 | |||
| 82f31185a1 | |||
| c0c95056bf | |||
| 093f6f5f78 | |||
| 2f902755c4 | |||
| 975d30e411 | |||
| a0fe8c0017 | |||
| c33097f773 | |||
| 28c2b862f0 | |||
| d23ed002b2 | |||
| 8870b26ce6 | |||
| 0e3cf07b91 | |||
| 179add80f4 | |||
| 45675b0699 | |||
| 9c63a34fe1 | |||
| 1850dd542d | |||
| 2a1b65f424 | |||
| bd0c7ce042 | |||
| d46ad59546 | |||
| b8bc39d691 | |||
| 52dfe83354 | |||
| aca13f7308 | |||
| ef54dd5e9c | |||
| a1105f06ab | |||
| 109e145a19 | |||
| a42595bd15 | |||
| dadaacfaae | |||
| 74ee77f814 | |||
| 6ae0c1b955 | |||
| 6244c969af | |||
| d803704eee | |||
| 5d7d80d2e7 | |||
| a5b1804e19 | |||
| a7854fbe81 | |||
| 3e38bc5950 | |||
| 7f3358d284 | |||
| 92cd58e641 | |||
| 6cb01090d0 | |||
| 48484f04c9 | |||
| 56202ec309 | |||
| c4b7ece974 | |||
| c5c3058f3d | |||
| a7afcf4cd1 | |||
| f251b7acdb | |||
| b0a24c6a74 | |||
| b9bbdee826 | |||
| afef95e14e | |||
| b480912bd8 | |||
| 7b08489533 | |||
| 1f3b312675 | |||
| a437ada3b3 | |||
| fe46e5d667 | |||
| c53e94d205 | |||
| 056413560e | |||
| a7cf86eae4 | |||
| c30922e325 | |||
| d4753a02d4 | |||
| 451d0c92dd | |||
| 8beb658bcc | |||
| a3b79ef21d | |||
| b101682e3c | |||
| 3275b5fd80 | |||
| 913033373b | |||
| 65f995cb9f | |||
| 795599fd38 | |||
| 5b7f3ae12f | |||
| d556e9ba19 | |||
| 1efca47336 | |||
| 091b455460 | |||
| a0e6424d48 | |||
| c4f02023b9 | |||
| ca917b0577 | |||
| ce2c38e188 | |||
| ae24c3394b | |||
| acd9a691f8 | |||
| 838cedfe6c | |||
| 0c6528bdc5 | |||
| 658009c401 | |||
| 5e0fcf1f4a | |||
| f1629440fe | |||
| b47fad2c3a | |||
| 330cbd5f57 | |||
| 36b2e82f4e | |||
| 92bafb0cf4 | |||
| e8727ca922 | |||
| 8292ec3a1e | |||
| 3a04bb54bd | |||
| 32edc3c68f | |||
| a8956223c2 | |||
| e3133e0d5e | |||
| 5bbf522362 | |||
| 32c4270dff | |||
| ec2ff981b2 | |||
| af75d6cfec | |||
| a35e6d0a9f | |||
| a191d8ca2e | |||
| 0d5a2109d5 | |||
| 1fe00c5b0e | |||
| 9051b7565c | |||
| ffc31506e3 | |||
| 8ef6f933a7 | |||
| ee35da7342 | |||
| 084691c294 | |||
| 983fa41cba | |||
| d2c826c7c9 | |||
| 34d4ebc7cb | |||
| 8d1dd78194 | |||
| 098699e09a | |||
| 9e3ee433d2 | |||
| 9395813f5a | |||
| 6d81fc1309 | |||
| da4597fa62 | |||
| 33d159dbcf | |||
| 684e7c4ddb | |||
| a86465c554 | |||
| efd75823f9 | |||
| 6464033a58 | |||
| 4c80ab1516 | |||
| 3ade01def9 | |||
| cb5d5e546d | |||
| 1cf35f016b | |||
| f50e7f0b3a | |||
| 52e9d3a908 | |||
| 6932dd9e6e | |||
| aa565c6b34 | |||
| 7bbf769bdd | |||
| 980ac64688 | |||
| 18fdd367ac | |||
| 7d9e003a6d | |||
| 701207e100 | |||
| a78bbb1de5 | |||
| e4d5afbebe | |||
| 63843cc4c9 | |||
| 1d57264922 | |||
| 82ca8f48dc | |||
| 4a9fd57356 | |||
| d47983a032 | |||
| 4ad8afd512 | |||
| 48e28e7b7a | |||
| 932e782a14 | |||
| ac0ce799f9 | |||
| 5bf978d32d | |||
| dd5f4488be | |||
| d0a1ea3292 | |||
| 84dd1fe4a5 | |||
| abf7aaeda3 | |||
| c82cc9a670 | |||
| c1242b2a2a | |||
| 4d593eb840 | |||
| f32291d714 | |||
| 8e2eac9dc0 | |||
| 0d9d72c223 | |||
| 7ac46a7cc7 | |||
| f28b08ed65 | |||
| 029e4beaf5 | |||
| a6222a8025 | |||
| 4075276130 | |||
| 2b693917b0 | |||
| 1c43442300 | |||
| dca9aef258 | |||
| 2bd0cbadbe | |||
| 8b2d6840a8 | |||
| df3715d8d6 | |||
| 084e2d9930 | |||
| 79eecbb329 | |||
| f7beebce3f | |||
| fbbb5df64f | |||
| 96d70d5048 | |||
| ac40527fa2 | |||
| 66f7a7928c | |||
| deae0bb84b | |||
| 6c32a9ebe9 | |||
| a8d1ec6f9b | |||
| daea0568a8 | |||
| b632c09924 | |||
| 7ce8c375a2 | |||
| 5a04e61d1c | |||
| 748fff5c32 | |||
| 57ba0c3051 | |||
| 72f80859a9 | |||
| f999c416c4 | |||
| f8e1bf715b | |||
| f350007ae5 | |||
| a2cf8d1f2c | |||
| abb7f7f894 | |||
| 96a99c4e3b | |||
| 5b4224b8f5 | |||
| f0a7cbbcae | |||
| 15552eff54 | |||
| 6e0447165b | |||
| 26499658a4 | |||
| 624ea6ba33 | |||
| 01542ae6a8 | |||
| d85c126c27 | |||
| e29c0cb7a1 | |||
| 37baa4ea45 | |||
| 5f4aed2f02 | |||
| cb5f5b9ecb | |||
| c15b650181 | |||
| e29d59ac02 | |||
| d5c6c9238f |
121
.drone.yml
Normal file
121
.drone.yml
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
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:alpine
|
||||||
|
commands:
|
||||||
|
- yarn
|
||||||
|
- yarn test:ci
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- pull_request
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: build:dev
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- 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 full license export
|
||||||
|
depends_on: ["clone"]
|
||||||
|
image: node:alpine
|
||||||
|
commands:
|
||||||
|
- yarn
|
||||||
|
- yarn licenses:export
|
||||||
|
- name: push new licenses file to repo
|
||||||
|
depends_on: ["run full license export"]
|
||||||
|
image: appleboy/drone-git-push
|
||||||
|
settings:
|
||||||
|
branch: dev
|
||||||
|
commit: true
|
||||||
|
commit_message: new license file version [CI SKIP]
|
||||||
|
author_email: bot@odit.services
|
||||||
|
remote: git@git.odit.services:lfk/backend.git
|
||||||
|
ssh_key:
|
||||||
|
from_secret: GITLAB_SSHKEY
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
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
|
||||||
9
.env.ci
Normal file
9
.env.ci
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
APP_PORT=4010
|
||||||
|
DB_TYPE=sqlite
|
||||||
|
DB_HOST=unused
|
||||||
|
DB_PORT=unused
|
||||||
|
DB_USER=unused
|
||||||
|
DB_PASSWORD=bla
|
||||||
|
DB_NAME=./test.sqlite
|
||||||
|
NODE_ENV=dev
|
||||||
|
POSTALCODE_COUNTRYCODE=null
|
||||||
@@ -6,3 +6,4 @@ DB_USER=bla
|
|||||||
DB_PASSWORD=bla
|
DB_PASSWORD=bla
|
||||||
DB_NAME=bla
|
DB_NAME=bla
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
POSTALCODE_COUNTRYCODE=null
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -131,3 +131,7 @@ package-lock.json
|
|||||||
build
|
build
|
||||||
|
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
*.sqlite-jurnal
|
||||||
|
/docs
|
||||||
|
lib
|
||||||
|
/oss-attribution
|
||||||
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -7,6 +7,14 @@
|
|||||||
},
|
},
|
||||||
"prettier.enable": false,
|
"prettier.enable": false,
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
"editor.defaultFormatter": "vscode.typescript-language-features",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": true,
|
||||||
|
// "source.fixAll": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"javascript.preferences.quoteStyle": "single",
|
||||||
|
"javascript.preferences.importModuleSpecifierEnding": "minimal",
|
||||||
|
"typescript.preferences.importModuleSpecifierEnding": "minimal",
|
||||||
|
"typescript.preferences.includePackageJsonAutoImports": "on"
|
||||||
}
|
}
|
||||||
20
Dockerfile
20
Dockerfile
@@ -1,6 +1,16 @@
|
|||||||
FROM node:alpine
|
# Typescript Build
|
||||||
|
FROM node:14.15.1-alpine3.12
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./package.json ./
|
COPY package.json ./
|
||||||
RUN npm i
|
RUN npm i -g pnpm
|
||||||
COPY ./ ./
|
RUN pnpm i
|
||||||
ENTRYPOINT [ "yarn","dev" ]
|
COPY tsconfig.json ormconfig.js ./
|
||||||
|
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"]
|
||||||
20
README.md
20
README.md
@@ -25,6 +25,11 @@ Backend Server
|
|||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Generate Docs
|
||||||
|
```
|
||||||
|
yarn docs
|
||||||
|
```
|
||||||
|
|
||||||
### Docker w/ postgres 🐳
|
### Docker w/ postgres 🐳
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -39,9 +44,20 @@ docker-compose up --build
|
|||||||
|
|
||||||
- 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
|
## File Structure
|
||||||
|
|
||||||
- src/models/\* - database models (typeorm entities)
|
- src/models/entities\* - database models (typeorm entities)
|
||||||
|
- src/models/actions\* - actions models
|
||||||
|
- src/models/responses\* - response models
|
||||||
- src/controllers/\* - routing-controllers
|
- src/controllers/\* - routing-controllers
|
||||||
- src/loaders/\* - loaders for the different init steps of the api server
|
- src/loaders/\* - loaders for the different init steps of the api server
|
||||||
- src/routes/\* - express routes for everything we don't do via routing-controllers (shouldn't be much)
|
- 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)
|
||||||
@@ -6,15 +6,26 @@ services:
|
|||||||
- 4010:4010
|
- 4010:4010
|
||||||
environment:
|
environment:
|
||||||
APP_PORT: 4010
|
APP_PORT: 4010
|
||||||
DB_TYPE: postgres
|
DB_TYPE: sqlite
|
||||||
DB_HOST: backend_db
|
DB_HOST: bla
|
||||||
DB_PORT: 5432
|
DB_PORT: bla
|
||||||
DB_USER: lfk
|
DB_USER: bla
|
||||||
DB_PASSWORD: changeme
|
DB_PASSWORD: bla
|
||||||
DB_NAME: lfk
|
DB_NAME: dev.sqlite
|
||||||
backend_db:
|
NODE_ENV: production
|
||||||
image: postgres:11-alpine
|
# APP_PORT: 4010
|
||||||
environment:
|
# DB_TYPE: postgres
|
||||||
POSTGRES_DB: lfk
|
# DB_HOST: backend_db
|
||||||
POSTGRES_PASSWORD: changeme
|
# DB_PORT: 5432
|
||||||
POSTGRES_USER: lfk
|
# DB_USER: lfk
|
||||||
|
# DB_PASSWORD: changeme
|
||||||
|
# DB_NAME: lfk
|
||||||
|
# NODE_ENV: production
|
||||||
|
# backend_db:
|
||||||
|
# image: postgres:11-alpine
|
||||||
|
# environment:
|
||||||
|
# POSTGRES_DB: lfk
|
||||||
|
# POSTGRES_PASSWORD: changeme
|
||||||
|
# POSTGRES_USER: lfk
|
||||||
|
# ports:
|
||||||
|
# - 5432:5432
|
||||||
|
|||||||
4
jest.config.js
Normal file
4
jest.config.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
};
|
||||||
1296
licenses.md
Normal file
1296
licenses.md
Normal file
File diff suppressed because it is too large
Load Diff
16
ormconfig.js
Normal file
16
ormconfig.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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,
|
||||||
|
port: process.env.DB_PORT,
|
||||||
|
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'],
|
||||||
|
};
|
||||||
12
ormconfig.ts
12
ormconfig.ts
@@ -1,12 +0,0 @@
|
|||||||
import { config } from 'dotenv-safe';
|
|
||||||
config();
|
|
||||||
|
|
||||||
export default {
|
|
||||||
type: process.env.DB_TYPE,
|
|
||||||
host: process.env.DB_HOST,
|
|
||||||
port: process.env.DB_PORT,
|
|
||||||
username: process.env.DB_USER,
|
|
||||||
password: process.env.DB_PASSWORD,
|
|
||||||
database: process.env.DB_NAME,
|
|
||||||
entities: ["src/models/*.ts"]
|
|
||||||
};
|
|
||||||
49
package.json
49
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lfk/backend",
|
"name": "@odit/lfk-backend",
|
||||||
"version": "1.0.0",
|
"version": "0.0.8",
|
||||||
"main": "src/app.ts",
|
"main": "src/app.ts",
|
||||||
"repository": "https://git.odit.services/lfk/backend",
|
"repository": "https://git.odit.services/lfk/backend",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -22,41 +22,66 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-NC-SA-4.0",
|
"license": "CC-BY-NC-SA-4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"argon2": "^0.27.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"class-transformer": "^0.3.1",
|
"class-transformer": "^0.3.1",
|
||||||
"class-validator": "^0.12.2",
|
"class-validator": "^0.12.2",
|
||||||
"class-validator-jsonschema": "^2.0.3",
|
"class-validator-jsonschema": "^2.0.3",
|
||||||
"consola": "^2.15.0",
|
"consola": "^2.15.0",
|
||||||
|
"cookie": "^0.4.1",
|
||||||
|
"cookie-parser": "^1.4.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"csvtojson": "^2.0.10",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"helmet": "^4.2.0",
|
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"multer": "^1.4.2",
|
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
"pg": "^8.5.1",
|
"pg": "^8.5.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"routing-controllers": "^0.9.0-alpha.6",
|
"routing-controllers": "^0.9.0-alpha.6",
|
||||||
"routing-controllers-openapi": "^2.1.0",
|
"routing-controllers-openapi": "^2.1.0",
|
||||||
"swagger-ui-express": "^4.1.5",
|
"sqlite3": "^5.0.0",
|
||||||
"typeorm": "^0.2.29",
|
"typeorm": "^0.2.29",
|
||||||
"typeorm-routing-controllers-extensions": "^0.2.0"
|
"typeorm-routing-controllers-extensions": "^0.2.0",
|
||||||
|
"typeorm-seeding": "^1.6.1",
|
||||||
|
"uuid": "^8.3.1",
|
||||||
|
"validator": "^13.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@odit/license-exporter": "^0.0.8",
|
||||||
"@types/cors": "^2.8.8",
|
"@types/cors": "^2.8.8",
|
||||||
"@types/dotenv-safe": "^8.1.1",
|
"@types/csvtojson": "^1.1.5",
|
||||||
"@types/express": "^4.17.9",
|
"@types/express": "^4.17.9",
|
||||||
|
"@types/jest": "^26.0.16",
|
||||||
"@types/jsonwebtoken": "^8.5.0",
|
"@types/jsonwebtoken": "^8.5.0",
|
||||||
"@types/multer": "^1.4.4",
|
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.14.9",
|
||||||
"@types/swagger-ui-express": "^4.1.2",
|
"@types/uuid": "^8.3.0",
|
||||||
"dotenv-safe": "^8.2.0",
|
"axios": "^0.21.0",
|
||||||
|
"cp-cli": "^2.0.0",
|
||||||
|
"jest": "^26.6.3",
|
||||||
"nodemon": "^2.0.6",
|
"nodemon": "^2.0.6",
|
||||||
"sqlite3": "^5.0.0",
|
"rimraf": "^2.7.1",
|
||||||
|
"start-server-and-test": "^1.11.6",
|
||||||
|
"ts-jest": "^26.4.4",
|
||||||
"ts-node": "^9.0.0",
|
"ts-node": "^9.0.0",
|
||||||
|
"typedoc": "^0.19.2",
|
||||||
"typescript": "^4.1.2"
|
"typescript": "^4.1.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon src/app.ts",
|
"dev": "nodemon src/app.ts",
|
||||||
"build": "tsc"
|
"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"
|
||||||
|
},
|
||||||
|
"nodemonConfig": {
|
||||||
|
"ignore": [
|
||||||
|
"src/tests/*",
|
||||||
|
"docs/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
68
scripts/openapi_export.ts
Normal file
68
scripts/openapi_export.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
|
||||||
|
import consola from "consola";
|
||||||
|
import fs from "fs";
|
||||||
|
import "reflect-metadata";
|
||||||
|
import { createExpressServer, getMetadataArgsStorage } from "routing-controllers";
|
||||||
|
import { routingControllersToSpec } from 'routing-controllers-openapi';
|
||||||
|
import { config } from '../src/config';
|
||||||
|
import authchecker from "../src/middlewares/authchecker";
|
||||||
|
import { ErrorHandler } from '../src/middlewares/ErrorHandler';
|
||||||
|
|
||||||
|
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
|
||||||
|
createExpressServer({
|
||||||
|
authorizationChecker: authchecker,
|
||||||
|
middlewares: [ErrorHandler],
|
||||||
|
development: config.development,
|
||||||
|
cors: true,
|
||||||
|
routePrefix: "/api",
|
||||||
|
controllers: [`${__dirname}/../src/controllers/*.${CONTROLLERS_FILE_EXTENSION}`],
|
||||||
|
});
|
||||||
|
|
||||||
|
const storage = getMetadataArgsStorage();
|
||||||
|
const schemas = validationMetadatasToSchemas({
|
||||||
|
refPointerPrefix: "#/components/schemas/",
|
||||||
|
});
|
||||||
|
|
||||||
|
//Spec creation based on the previously created schemas
|
||||||
|
const spec = routingControllersToSpec(
|
||||||
|
storage,
|
||||||
|
{
|
||||||
|
routePrefix: "/api"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
schemas,
|
||||||
|
"securitySchemes": {
|
||||||
|
"AuthToken": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer",
|
||||||
|
"bearerFormat": "JWT",
|
||||||
|
description: "A JWT based access token. Use /api/auth/login or /api/auth/refresh to get one."
|
||||||
|
},
|
||||||
|
"RefreshTokenCookie": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"in": "cookie",
|
||||||
|
"name": "lfk_backend__refresh_token",
|
||||||
|
description: "A cookie containing a JWT based refreh token. Attention: Doesn't work in swagger-ui. Use /api/auth/login or /api/auth/refresh to get one."
|
||||||
|
},
|
||||||
|
"StatsApiToken": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer",
|
||||||
|
description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
description: "The the backend API for the LfK! runner system.",
|
||||||
|
title: "LfK! Backend API",
|
||||||
|
version: "0.0.8",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync("./openapi.json", JSON.stringify(spec), { encoding: "utf-8" });
|
||||||
|
consola.success("Exported openapi spec to openapi.json");
|
||||||
|
} catch (error) {
|
||||||
|
consola.error("Couldn't export the openapi spec");
|
||||||
|
}
|
||||||
29
src/app.ts
29
src/app.ts
@@ -1,25 +1,32 @@
|
|||||||
import "reflect-metadata";
|
|
||||||
import * as dotenvSafe from "dotenv-safe";
|
|
||||||
import { createExpressServer } from "routing-controllers";
|
|
||||||
import consola from "consola";
|
import consola from "consola";
|
||||||
|
import "reflect-metadata";
|
||||||
|
import { createExpressServer } from "routing-controllers";
|
||||||
|
import { config, e as errors } from './config';
|
||||||
import loaders from "./loaders/index";
|
import loaders from "./loaders/index";
|
||||||
|
import authchecker from "./middlewares/authchecker";
|
||||||
|
import { ErrorHandler } from './middlewares/ErrorHandler';
|
||||||
|
|
||||||
dotenvSafe.config();
|
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
|
||||||
const PORT = process.env.APP_PORT || 4010;
|
|
||||||
|
|
||||||
const app = createExpressServer({
|
const app = createExpressServer({
|
||||||
development: process.env.NODE_ENV === "production",
|
authorizationChecker: authchecker,
|
||||||
|
middlewares: [ErrorHandler],
|
||||||
|
development: config.development,
|
||||||
cors: true,
|
cors: true,
|
||||||
routePrefix: "/api",
|
routePrefix: "/api",
|
||||||
controllers: [__dirname + "/controllers/*.ts"],
|
controllers: [`${__dirname}/controllers/*.${CONTROLLERS_FILE_EXTENSION}`],
|
||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await loaders(app);
|
await loaders(app);
|
||||||
app.listen(PORT, () => {
|
app.listen(config.internal_port, () => {
|
||||||
consola.success(
|
consola.success(
|
||||||
`⚡️[server]: Server is running at http://localhost:${PORT}`
|
`⚡️[server]: Server is running at http://localhost:${config.internal_port}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
main();
|
if (errors === 0) {
|
||||||
|
main();
|
||||||
|
} else {
|
||||||
|
consola.error("error");
|
||||||
|
// something's wrong
|
||||||
|
}
|
||||||
|
|||||||
34
src/config.ts
Normal file
34
src/config.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export let e = errors
|
||||||
103
src/controllers/AuthController.ts
Normal file
103
src/controllers/AuthController.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
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/CreateAuth';
|
||||||
|
import { CreateResetToken } from '../models/actions/CreateResetToken';
|
||||||
|
import { HandleLogout } from '../models/actions/HandleLogout';
|
||||||
|
import { RefreshAuth } from '../models/actions/RefreshAuth';
|
||||||
|
import { ResetPassword } from '../models/actions/ResetPassword';
|
||||||
|
import { Auth } from '../models/responses/ResponseAuth';
|
||||||
|
import { Logout } from '../models/responses/ResponseLogout';
|
||||||
|
|
||||||
|
@JsonController('/auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/login")
|
||||||
|
@ResponseSchema(Auth)
|
||||||
|
@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(Auth)
|
||||||
|
@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(Auth)
|
||||||
|
@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(Auth)
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/controllers/DonorController.ts
Normal file
105
src/controllers/DonorController.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
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 { DonorIdsNotMatchingError, DonorNotFoundError } from '../errors/DonorErrors';
|
||||||
|
import { CreateDonor } from '../models/actions/CreateDonor';
|
||||||
|
import { UpdateDonor } from '../models/actions/UpdateDonor';
|
||||||
|
import { Donor } from '../models/entities/Donor';
|
||||||
|
import { ResponseDonor } from '../models/responses/ResponseDonor';
|
||||||
|
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||||
|
|
||||||
|
@JsonController('/donors')
|
||||||
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
export class DonorController {
|
||||||
|
private donorRepository: Repository<Donor>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the repository of this controller's model/entity.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.donorRepository = getConnectionManager().get().getRepository(Donor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Authorized("DONOR:GET")
|
||||||
|
@ResponseSchema(ResponseDonor, { isArray: true })
|
||||||
|
@OpenAPI({ description: 'Lists all runners from all teams/orgs. <br> This includes the runner\'s group and distance ran.' })
|
||||||
|
async getAll() {
|
||||||
|
let responseDonors: ResponseDonor[] = new Array<ResponseDonor>();
|
||||||
|
const donors = await this.donorRepository.find();
|
||||||
|
donors.forEach(donor => {
|
||||||
|
responseDonors.push(new ResponseDonor(donor));
|
||||||
|
});
|
||||||
|
return responseDonors;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
@Authorized("DONOR:GET")
|
||||||
|
@ResponseSchema(ResponseDonor)
|
||||||
|
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
|
||||||
|
@OnUndefined(DonorNotFoundError)
|
||||||
|
@OpenAPI({ description: 'Lists all information about the runner whose id got provided.' })
|
||||||
|
async getOne(@Param('id') id: number) {
|
||||||
|
let donor = await this.donorRepository.findOne({ id: id })
|
||||||
|
if (!donor) { throw new DonorNotFoundError(); }
|
||||||
|
return new ResponseDonor(donor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Authorized("DONOR:CREATE")
|
||||||
|
@ResponseSchema(ResponseDonor)
|
||||||
|
@OpenAPI({ description: 'Create a new runner. <br> Please remeber to provide the runner\'s group\'s id.' })
|
||||||
|
async post(@Body({ validate: true }) createRunner: CreateDonor) {
|
||||||
|
let donor;
|
||||||
|
try {
|
||||||
|
donor = await createRunner.toDonor();
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
donor = await this.donorRepository.save(donor)
|
||||||
|
return new ResponseDonor(await this.donorRepository.findOne(donor));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:id')
|
||||||
|
@Authorized("DONOR:UPDATE")
|
||||||
|
@ResponseSchema(ResponseDonor)
|
||||||
|
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(DonorIdsNotMatchingError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Update the runner whose id you provided. <br> Please remember that ids can't be changed." })
|
||||||
|
async put(@Param('id') id: number, @Body({ validate: true }) donor: UpdateDonor) {
|
||||||
|
let oldDonor = await this.donorRepository.findOne({ id: id });
|
||||||
|
|
||||||
|
if (!oldDonor) {
|
||||||
|
throw new DonorNotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldDonor.id != donor.id) {
|
||||||
|
throw new DonorIdsNotMatchingError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.donorRepository.save(await donor.updateDonor(oldDonor));
|
||||||
|
return new ResponseDonor(await this.donorRepository.findOne({ id: id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id')
|
||||||
|
@Authorized("DONOR:DELETE")
|
||||||
|
@ResponseSchema(ResponseDonor)
|
||||||
|
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||||
|
@OnUndefined(204)
|
||||||
|
@OpenAPI({ description: 'Delete the runner whose id you provided. <br> If no runner with this id exists it will just return 204(no content).' })
|
||||||
|
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||||
|
let donor = await this.donorRepository.findOne({ id: id });
|
||||||
|
if (!donor) { return null; }
|
||||||
|
const responseDonor = await this.donorRepository.findOne(donor);
|
||||||
|
|
||||||
|
if (!donor) {
|
||||||
|
throw new DonorNotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: DELETE DONATIONS AND WARN FOR FORCE (https://git.odit.services/lfk/backend/issues/66)
|
||||||
|
|
||||||
|
await this.donorRepository.delete(donor);
|
||||||
|
return new ResponseDonor(responseDonor);
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/controllers/ImportController.ts
Normal file
102
src/controllers/ImportController.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import csv from 'csvtojson';
|
||||||
|
import { Authorized, Body, ContentType, Controller, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers';
|
||||||
|
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||||
|
import { RunnerGroupNeededError } from '../errors/RunnerErrors';
|
||||||
|
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
|
||||||
|
import RawBodyMiddleware from '../middlewares/RawBody';
|
||||||
|
import { ImportRunner } from '../models/actions/ImportRunner';
|
||||||
|
import { ResponseRunner } from '../models/responses/ResponseRunner';
|
||||||
|
import { RunnerController } from './RunnerController';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
@Authorized(["RUNNER:IMPORT", "TEAM:IMPORT"])
|
||||||
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
export class ImportController {
|
||||||
|
private runnerController: RunnerController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the repository of this controller's model/entity.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.runnerController = new RunnerController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/runners/import')
|
||||||
|
@ContentType("application/json")
|
||||||
|
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||||
|
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Create new runners from json and insert them into the provided group. <br> If teams/classes are provided alongside the runner's name they'll automaticly be created under the provided org and the runners will be inserted into the teams instead." })
|
||||||
|
async postJSON(@Body({ validate: true, type: ImportRunner }) importRunners: ImportRunner[], @QueryParam("group") groupID: number) {
|
||||||
|
if (!groupID) { throw new RunnerGroupNeededError(); }
|
||||||
|
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
|
||||||
|
for await (let runner of importRunners) {
|
||||||
|
responseRunners.push(await this.runnerController.post(await runner.toCreateRunner(groupID)));
|
||||||
|
}
|
||||||
|
return responseRunners;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/organisations/:id/import')
|
||||||
|
@ContentType("application/json")
|
||||||
|
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||||
|
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Create new runners from json and insert them into the provided org. <br> If teams/classes are provided alongside the runner's name they'll automaticly be created under the provided org and the runners will be inserted into the teams instead." })
|
||||||
|
async postOrgsJSON(@Body({ validate: true, type: ImportRunner }) importRunners: ImportRunner[], @Param('id') id: number) {
|
||||||
|
return await this.postJSON(importRunners, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/teams/:id/import')
|
||||||
|
@ContentType("application/json")
|
||||||
|
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||||
|
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Create new runners from json and insert them into the provided team" })
|
||||||
|
async postTeamsJSON(@Body({ validate: true, type: ImportRunner }) importRunners: ImportRunner[], @Param('id') id: number) {
|
||||||
|
return await this.postJSON(importRunners, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/runners/import/csv')
|
||||||
|
@ContentType("application/json")
|
||||||
|
@UseBefore(RawBodyMiddleware)
|
||||||
|
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||||
|
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Create new runners from csv and insert them into the provided group. <br> If teams/classes are provided alongside the runner's name they'll automaticly be created under the provided org and the runners will be inserted into the teams instead." })
|
||||||
|
async postCSV(@Req() request: any, @QueryParam("group") groupID: number) {
|
||||||
|
let csvParse = await csv({ delimiter: [",", ";"], trim: true }).fromString(request.rawBody.toString());
|
||||||
|
let importRunners: ImportRunner[] = new Array<ImportRunner>();
|
||||||
|
for await (let runner of csvParse) {
|
||||||
|
let newImportRunner = new ImportRunner();
|
||||||
|
newImportRunner.firstname = runner.firstname;
|
||||||
|
newImportRunner.middlename = runner.middlename;
|
||||||
|
newImportRunner.lastname = runner.lastname;
|
||||||
|
if (runner.class === undefined) { newImportRunner.team = runner.team; }
|
||||||
|
else { newImportRunner.class = runner.class; }
|
||||||
|
importRunners.push(newImportRunner);
|
||||||
|
}
|
||||||
|
return await this.postJSON(importRunners, groupID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/organisations/:id/import/csv')
|
||||||
|
@ContentType("application/json")
|
||||||
|
@UseBefore(RawBodyMiddleware)
|
||||||
|
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||||
|
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Create new runners from csv and insert them into the provided org. <br> If teams/classes are provided alongside the runner's name they'll automaticly be created under the provided org and the runners will be inserted into the teams instead." })
|
||||||
|
async postOrgsCSV(@Req() request: any, @Param("id") id: number) {
|
||||||
|
return await this.postCSV(request, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/teams/:id/import/csv')
|
||||||
|
@ContentType("application/json")
|
||||||
|
@UseBefore(RawBodyMiddleware)
|
||||||
|
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||||
|
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Create new runners from csv and insert them into the provided team" })
|
||||||
|
async postTeamsCSV(@Req() request: any, @Param("id") id: number) {
|
||||||
|
return await this.postCSV(request, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/controllers/PermissionController.ts
Normal file
118
src/controllers/PermissionController.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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 { PermissionIdsNotMatchingError, PermissionNeedsPrincipalError, PermissionNotFoundError } from '../errors/PermissionErrors';
|
||||||
|
import { PrincipalNotFoundError } from '../errors/PrincipalErrors';
|
||||||
|
import { CreatePermission } from '../models/actions/CreatePermission';
|
||||||
|
import { UpdatePermission } from '../models/actions/UpdatePermission';
|
||||||
|
import { Permission } from '../models/entities/Permission';
|
||||||
|
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||||
|
import { ResponsePermission } from '../models/responses/ResponsePermission';
|
||||||
|
import { ResponsePrincipal } from '../models/responses/ResponsePrincipal';
|
||||||
|
|
||||||
|
|
||||||
|
@JsonController('/permissions')
|
||||||
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
export class PermissionController {
|
||||||
|
private permissionRepository: Repository<Permission>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the repository of this controller's model/entity.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.permissionRepository = getConnectionManager().get().getRepository(Permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Authorized("PERMISSION:GET")
|
||||||
|
@ResponseSchema(ResponsePermission, { isArray: true })
|
||||||
|
@OpenAPI({ description: 'Lists all permissions for all users and groups.' })
|
||||||
|
async getAll() {
|
||||||
|
let responsePermissions: ResponsePermission[] = new Array<ResponsePermission>();
|
||||||
|
const permissions = await this.permissionRepository.find({ relations: ['principal'] });
|
||||||
|
permissions.forEach(permission => {
|
||||||
|
responsePermissions.push(new ResponsePermission(permission));
|
||||||
|
});
|
||||||
|
return responsePermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
@Authorized("PERMISSION:GET")
|
||||||
|
@ResponseSchema(ResponsePermission)
|
||||||
|
@ResponseSchema(PermissionNotFoundError, { statusCode: 404 })
|
||||||
|
@OnUndefined(PermissionNotFoundError)
|
||||||
|
@OpenAPI({ description: 'Lists all information about the permission whose id got provided.' })
|
||||||
|
async getOne(@Param('id') id: number) {
|
||||||
|
let permission = await this.permissionRepository.findOne({ id: id }, { relations: ['principal'] });
|
||||||
|
if (!permission) { throw new PermissionNotFoundError(); }
|
||||||
|
return new ResponsePermission(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Authorized("PERMISSION:CREATE")
|
||||||
|
@ResponseSchema(ResponsePermission)
|
||||||
|
@ResponseSchema(PrincipalNotFoundError, { statusCode: 404 })
|
||||||
|
@OpenAPI({ description: 'Create a new permission for a existing principal(user/group). <br> If a permission with this target, action and prinicpal already exists that permission will be returned instead of creating a new one.' })
|
||||||
|
async post(@Body({ validate: true }) createPermission: CreatePermission) {
|
||||||
|
let permission;
|
||||||
|
try {
|
||||||
|
permission = await createPermission.toPermission();
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
let existingPermission = await this.permissionRepository.findOne({ target: permission.target, action: permission.action, principal: permission.principal }, { relations: ['principal'] });
|
||||||
|
if (existingPermission) { return new ResponsePermission(existingPermission); }
|
||||||
|
|
||||||
|
permission = await this.permissionRepository.save(permission);
|
||||||
|
permission = await this.permissionRepository.findOne(permission, { relations: ['principal'] });
|
||||||
|
|
||||||
|
return new ResponsePermission(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Put('/:id')
|
||||||
|
@Authorized("PERMISSION:UPDATE")
|
||||||
|
@ResponseSchema(ResponsePrincipal)
|
||||||
|
@ResponseSchema(PermissionNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(PrincipalNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(PermissionIdsNotMatchingError, { statusCode: 406 })
|
||||||
|
@ResponseSchema(PermissionNeedsPrincipalError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Update a permission object. <br> If updateing the permission object would result in duplicate permission (same target, action and principal) this permission will get deleted and the existing permission will be returned. <br> Please remember that ids can't be changed." })
|
||||||
|
async put(@Param('id') id: number, @Body({ validate: true }) permission: UpdatePermission) {
|
||||||
|
let oldPermission = await this.permissionRepository.findOne({ id: id }, { relations: ['principal'] });
|
||||||
|
|
||||||
|
if (!oldPermission) {
|
||||||
|
throw new PermissionNotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
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'] });
|
||||||
|
if (existingPermission) {
|
||||||
|
await this.remove(permission.id, true);
|
||||||
|
return new ResponsePermission(existingPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.permissionRepository.save(await permission.updatePermission(oldPermission));
|
||||||
|
|
||||||
|
return new ResponsePermission(await this.permissionRepository.findOne({ id: permission.id }, { relations: ['principal'] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id')
|
||||||
|
@Authorized("PERMISSION:DELETE")
|
||||||
|
@ResponseSchema(ResponsePermission)
|
||||||
|
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||||
|
@OnUndefined(204)
|
||||||
|
@OpenAPI({ description: 'Deletes the permission whose id you provide. <br> If no permission with this id exists it will just return 204(no content).' })
|
||||||
|
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||||
|
let permission = await this.permissionRepository.findOne({ id: id }, { relations: ['principal'] });
|
||||||
|
if (!permission) { return null; }
|
||||||
|
|
||||||
|
const responsePermission = new ResponsePermission(permission);
|
||||||
|
await this.permissionRepository.delete(permission);
|
||||||
|
return responsePermission;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/controllers/RunnerController.ts
Normal file
106
src/controllers/RunnerController.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
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, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
|
||||||
|
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
|
||||||
|
import { CreateRunner } from '../models/actions/CreateRunner';
|
||||||
|
import { UpdateRunner } from '../models/actions/UpdateRunner';
|
||||||
|
import { Runner } from '../models/entities/Runner';
|
||||||
|
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||||
|
import { ResponseRunner } from '../models/responses/ResponseRunner';
|
||||||
|
|
||||||
|
@JsonController('/runners')
|
||||||
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
export class RunnerController {
|
||||||
|
private runnerRepository: Repository<Runner>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the repository of this controller's model/entity.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.runnerRepository = getConnectionManager().get().getRepository(Runner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@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() {
|
||||||
|
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
|
||||||
|
const runners = await this.runnerRepository.find({ relations: ['scans', 'group'] });
|
||||||
|
runners.forEach(runner => {
|
||||||
|
responseRunners.push(new ResponseRunner(runner));
|
||||||
|
});
|
||||||
|
return responseRunners;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
@Authorized("RUNNER:GET")
|
||||||
|
@ResponseSchema(ResponseRunner)
|
||||||
|
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||||
|
@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'] })
|
||||||
|
if (!runner) { throw new RunnerNotFoundError(); }
|
||||||
|
return new ResponseRunner(runner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Authorized("RUNNER:CREATE")
|
||||||
|
@ResponseSchema(ResponseRunner)
|
||||||
|
@ResponseSchema(RunnerGroupNeededError)
|
||||||
|
@ResponseSchema(RunnerGroupNotFoundError)
|
||||||
|
@OpenAPI({ description: 'Create a new runner. <br> Please remeber to provide the runner\'s group\'s id.' })
|
||||||
|
async post(@Body({ validate: true }) createRunner: CreateRunner) {
|
||||||
|
let runner;
|
||||||
|
try {
|
||||||
|
runner = await createRunner.toRunner();
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
runner = await this.runnerRepository.save(runner)
|
||||||
|
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group'] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:id')
|
||||||
|
@Authorized("RUNNER:UPDATE")
|
||||||
|
@ResponseSchema(ResponseRunner)
|
||||||
|
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(RunnerIdsNotMatchingError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Update the runner whose id you provided. <br> Please remember that ids can't be changed." })
|
||||||
|
async put(@Param('id') id: number, @Body({ validate: true }) runner: UpdateRunner) {
|
||||||
|
let oldRunner = await this.runnerRepository.findOne({ id: id }, { relations: ['group'] });
|
||||||
|
|
||||||
|
if (!oldRunner) {
|
||||||
|
throw new RunnerNotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldRunner.id != runner.id) {
|
||||||
|
throw new RunnerIdsNotMatchingError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.runnerRepository.save(await runner.updateRunner(oldRunner));
|
||||||
|
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group'] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id')
|
||||||
|
@Authorized("RUNNER:DELETE")
|
||||||
|
@ResponseSchema(ResponseRunner)
|
||||||
|
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||||
|
@OnUndefined(204)
|
||||||
|
@OpenAPI({ description: 'Delete the runner whose id you provided. <br> If no runner with this id exists it will just return 204(no content).' })
|
||||||
|
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||||
|
let runner = await this.runnerRepository.findOne({ id: id });
|
||||||
|
if (!runner) { return null; }
|
||||||
|
const responseRunner = await this.runnerRepository.findOne(runner, { relations: ['scans', 'group'] });
|
||||||
|
|
||||||
|
if (!runner) {
|
||||||
|
throw new RunnerNotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.runnerRepository.delete(runner);
|
||||||
|
return new ResponseRunner(responseRunner);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
src/controllers/RunnerOrganisationController.ts
Normal file
127
src/controllers/RunnerOrganisationController.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||||
|
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||||
|
import { getConnectionManager, Repository } from 'typeorm';
|
||||||
|
import { RunnerOrganisationHasRunnersError, RunnerOrganisationHasTeamsError, RunnerOrganisationIdsNotMatchingError, RunnerOrganisationNotFoundError } from '../errors/RunnerOrganisationErrors';
|
||||||
|
import { CreateRunnerOrganisation } from '../models/actions/CreateRunnerOrganisation';
|
||||||
|
import { UpdateRunnerOrganisation } from '../models/actions/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.toRunnerOrganisation();
|
||||||
|
} 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.updateRunnerOrganisation(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/controllers/RunnerTeamController.ts
Normal file
116
src/controllers/RunnerTeamController.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
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 { RunnerTeamHasRunnersError, RunnerTeamIdsNotMatchingError, RunnerTeamNotFoundError } from '../errors/RunnerTeamErrors';
|
||||||
|
import { CreateRunnerTeam } from '../models/actions/CreateRunnerTeam';
|
||||||
|
import { UpdateRunnerTeam } from '../models/actions/UpdateRunnerTeam';
|
||||||
|
import { RunnerTeam } from '../models/entities/RunnerTeam';
|
||||||
|
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||||
|
import { ResponseRunnerTeam } from '../models/responses/ResponseRunnerTeam';
|
||||||
|
import { RunnerController } from './RunnerController';
|
||||||
|
|
||||||
|
|
||||||
|
@JsonController('/teams')
|
||||||
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
export class RunnerTeamController {
|
||||||
|
private runnerTeamRepository: Repository<RunnerTeam>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the repository of this controller's model/entity.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.runnerTeamRepository = getConnectionManager().get().getRepository(RunnerTeam);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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() {
|
||||||
|
let responseTeams: ResponseRunnerTeam[] = new Array<ResponseRunnerTeam>();
|
||||||
|
const runners = await this.runnerTeamRepository.find({ relations: ['parentGroup', 'contact'] });
|
||||||
|
runners.forEach(runner => {
|
||||||
|
responseTeams.push(new ResponseRunnerTeam(runner));
|
||||||
|
});
|
||||||
|
return responseTeams;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
@Authorized("TEAM:GET")
|
||||||
|
@ResponseSchema(ResponseRunnerTeam)
|
||||||
|
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
|
||||||
|
@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'] });
|
||||||
|
if (!runnerTeam) { throw new RunnerTeamNotFoundError(); }
|
||||||
|
return new ResponseRunnerTeam(runnerTeam);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Authorized("TEAM:CREATE")
|
||||||
|
@ResponseSchema(ResponseRunnerTeam)
|
||||||
|
@OpenAPI({ description: 'Create a new organsisation. <br> Please remember to provide it\'s parent group\'s id.' })
|
||||||
|
async post(@Body({ validate: true }) createRunnerTeam: CreateRunnerTeam) {
|
||||||
|
let runnerTeam;
|
||||||
|
try {
|
||||||
|
runnerTeam = await createRunnerTeam.toRunnerTeam();
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
runnerTeam = await this.runnerTeamRepository.save(runnerTeam);
|
||||||
|
runnerTeam = await this.runnerTeamRepository.findOne(runnerTeam, { relations: ['parentGroup', 'contact'] });
|
||||||
|
|
||||||
|
return new ResponseRunnerTeam(runnerTeam);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:id')
|
||||||
|
@Authorized("TEAM:UPDATE")
|
||||||
|
@ResponseSchema(ResponseRunnerTeam)
|
||||||
|
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(RunnerTeamIdsNotMatchingError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Update the team whose id you provided. <br> Please remember that ids can't be changed." })
|
||||||
|
async put(@Param('id') id: number, @Body({ validate: true }) runnerTeam: UpdateRunnerTeam) {
|
||||||
|
let oldRunnerTeam = await this.runnerTeamRepository.findOne({ id: id }, { relations: ['parentGroup', 'contact'] });
|
||||||
|
|
||||||
|
if (!oldRunnerTeam) {
|
||||||
|
throw new RunnerTeamNotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldRunnerTeam.id != runnerTeam.id) {
|
||||||
|
throw new RunnerTeamIdsNotMatchingError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.runnerTeamRepository.save(await runnerTeam.updateRunnerTeam(oldRunnerTeam));
|
||||||
|
|
||||||
|
return new ResponseRunnerTeam(await this.runnerTeamRepository.findOne({ id: runnerTeam.id }, { relations: ['parentGroup', 'contact'] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id')
|
||||||
|
@Authorized("TEAM:DELETE")
|
||||||
|
@ResponseSchema(ResponseRunnerTeam)
|
||||||
|
@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).' })
|
||||||
|
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'] });
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
if (runnerTeam.runners.length != 0) {
|
||||||
|
throw new RunnerTeamHasRunnersError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const runnerController = new RunnerController()
|
||||||
|
for (let runner of runnerTeam.runners) {
|
||||||
|
await runnerController.remove(runner.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseTeam = new ResponseRunnerTeam(runnerTeam);
|
||||||
|
await this.runnerTeamRepository.delete(team);
|
||||||
|
return responseTeam;
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/controllers/StatsClientController.ts
Normal file
75
src/controllers/StatsClientController.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post } from 'routing-controllers';
|
||||||
|
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||||
|
import { getConnectionManager, Repository } from 'typeorm';
|
||||||
|
import { StatsClientNotFoundError } from '../errors/StatsClientErrors';
|
||||||
|
import { TrackNotFoundError } from "../errors/TrackErrors";
|
||||||
|
import { CreateStatsClient } from '../models/actions/CreateStatsClient';
|
||||||
|
import { StatsClient } from '../models/entities/StatsClient';
|
||||||
|
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||||
|
import { ResponseStatsClient } from '../models/responses/ResponseStatsClient';
|
||||||
|
|
||||||
|
@JsonController('/statsclients')
|
||||||
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
export class StatsClientController {
|
||||||
|
private clientRepository: Repository<StatsClient>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the repository of this controller's model/entity.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.clientRepository = getConnectionManager().get().getRepository(StatsClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@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() {
|
||||||
|
let responseClients: ResponseStatsClient[] = new Array<ResponseStatsClient>();
|
||||||
|
const clients = await this.clientRepository.find();
|
||||||
|
clients.forEach(clients => {
|
||||||
|
responseClients.push(new ResponseStatsClient(clients));
|
||||||
|
});
|
||||||
|
return responseClients;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
@Authorized("STATSCLIENT:GET")
|
||||||
|
@ResponseSchema(ResponseStatsClient)
|
||||||
|
@ResponseSchema(StatsClientNotFoundError, { statusCode: 404 })
|
||||||
|
@OnUndefined(StatsClientNotFoundError)
|
||||||
|
@OpenAPI({ description: "Lists all information about the stats client whose id got provided. Please remember that the key can only be viewed on creation." })
|
||||||
|
async getOne(@Param('id') id: number) {
|
||||||
|
let client = await this.clientRepository.findOne({ id: id });
|
||||||
|
if (!client) { throw new TrackNotFoundError(); }
|
||||||
|
return new ResponseStatsClient(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Authorized("STATSCLIENT:CREATE")
|
||||||
|
@ResponseSchema(ResponseStatsClient)
|
||||||
|
@OpenAPI({ description: "Create a new stats client. <br> Please remember that the client\'s key will be generated automaticly and that it can only be viewed on creation." })
|
||||||
|
async post(
|
||||||
|
@Body({ validate: true })
|
||||||
|
client: CreateStatsClient
|
||||||
|
) {
|
||||||
|
let newClient = await this.clientRepository.save(await client.toStatsClient());
|
||||||
|
let responseClient = new ResponseStatsClient(newClient);
|
||||||
|
responseClient.key = newClient.cleartextkey;
|
||||||
|
return responseClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id')
|
||||||
|
@Authorized("STATSCLIENT:DELETE")
|
||||||
|
@ResponseSchema(ResponseStatsClient)
|
||||||
|
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||||
|
@OnUndefined(204)
|
||||||
|
@OpenAPI({ description: "Delete the stats client whose id you provided. <br> If no client with this id exists it will just return 204(no content)." })
|
||||||
|
async remove(@Param("id") id: number) {
|
||||||
|
let client = await this.clientRepository.findOne({ id: id });
|
||||||
|
if (!client) { return null; }
|
||||||
|
|
||||||
|
await this.clientRepository.delete(client);
|
||||||
|
return new ResponseStatsClient(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/controllers/StatsController.ts
Normal file
124
src/controllers/StatsController.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { Get, JsonController, 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 { Runner } from '../models/entities/Runner';
|
||||||
|
import { RunnerOrganisation } from '../models/entities/RunnerOrganisation';
|
||||||
|
import { RunnerTeam } from '../models/entities/RunnerTeam';
|
||||||
|
import { Scan } from '../models/entities/Scan';
|
||||||
|
import { User } from '../models/entities/User';
|
||||||
|
import { ResponseStats } from '../models/responses/ResponseStats';
|
||||||
|
import { ResponseStatsOrgnisation } from '../models/responses/ResponseStatsOrganisation';
|
||||||
|
import { ResponseStatsRunner } from '../models/responses/ResponseStatsRunner';
|
||||||
|
import { ResponseStatsTeam } from '../models/responses/ResponseStatsTeam';
|
||||||
|
|
||||||
|
@JsonController('/stats')
|
||||||
|
export class StatsController {
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@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();
|
||||||
|
let donations = await connection.getRepository(Donation).find({ relations: ['runner', 'runner.scans', 'runner.scans.track'] });
|
||||||
|
return new ResponseStats(runners, teams, orgs, users, scans, donations)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/runners/distance")
|
||||||
|
@UseBefore(StatsAuth)
|
||||||
|
@ResponseSchema(ResponseStatsRunner, { isArray: true })
|
||||||
|
@OpenAPI({ description: "Returns the top ten runners by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
async getTopRunnersByDistance() {
|
||||||
|
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);
|
||||||
|
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
|
||||||
|
topRunners.forEach(runner => {
|
||||||
|
responseRunners.push(new ResponseStatsRunner(runner));
|
||||||
|
});
|
||||||
|
return responseRunners;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/runners/donations")
|
||||||
|
@UseBefore(StatsAuth)
|
||||||
|
@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);
|
||||||
|
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
|
||||||
|
topRunners.forEach(runner => {
|
||||||
|
responseRunners.push(new ResponseStatsRunner(runner));
|
||||||
|
});
|
||||||
|
return responseRunners;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/scans")
|
||||||
|
@UseBefore(StatsAuth)
|
||||||
|
@ResponseSchema(ResponseStatsRunner, { isArray: true })
|
||||||
|
@OpenAPI({ description: "Returns the top ten fastest track times (with their runner and the runner's group).", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
async getTopRunnersByTrackTime() {
|
||||||
|
throw new Error("Not implemented yet.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/teams/distance")
|
||||||
|
@UseBefore(StatsAuth)
|
||||||
|
@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);
|
||||||
|
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
|
||||||
|
topTeams.forEach(team => {
|
||||||
|
responseTeams.push(new ResponseStatsTeam(team));
|
||||||
|
});
|
||||||
|
return responseTeams;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/teams/donations")
|
||||||
|
@UseBefore(StatsAuth)
|
||||||
|
@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);
|
||||||
|
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
|
||||||
|
topTeams.forEach(team => {
|
||||||
|
responseTeams.push(new ResponseStatsTeam(team));
|
||||||
|
});
|
||||||
|
return responseTeams;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/organisations/distance")
|
||||||
|
@UseBefore(StatsAuth)
|
||||||
|
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
|
||||||
|
@OpenAPI({ description: "Returns the top ten organisations 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);
|
||||||
|
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
|
||||||
|
topOrgs.forEach(org => {
|
||||||
|
responseOrgs.push(new ResponseStatsOrgnisation(org));
|
||||||
|
});
|
||||||
|
return responseOrgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/organisations/donations")
|
||||||
|
@UseBefore(StatsAuth)
|
||||||
|
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
|
||||||
|
@OpenAPI({ description: "Returns the top ten organisations 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);
|
||||||
|
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
|
||||||
|
topOrgs.forEach(org => {
|
||||||
|
responseOrgs.push(new ResponseStatsOrgnisation(org));
|
||||||
|
});
|
||||||
|
return responseOrgs;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/controllers/StatusController.ts
Normal file
22
src/controllers/StatusController.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Get, JsonController } from 'routing-controllers';
|
||||||
|
import { OpenAPI } from 'routing-controllers-openapi';
|
||||||
|
import { getConnection } from 'typeorm';
|
||||||
|
|
||||||
|
@JsonController('/status')
|
||||||
|
export class StatusController {
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@OpenAPI({ description: "A very basic status/health endpoint that just checks if the database connection is available. <br> The available information depth will be expanded later." })
|
||||||
|
get() {
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
connection = getConnection();
|
||||||
|
} catch {
|
||||||
|
throw new Error("sth is wrong, i can feel it....");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"controllers": "✔",
|
||||||
|
"database connection": "✔"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,15 @@
|
|||||||
import { JsonController, Param, Body, Get, Post, Put, Delete, NotFoundError, OnUndefined, NotAcceptableError } from 'routing-controllers';
|
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
|
||||||
import { getConnectionManager, Repository } from 'typeorm';
|
|
||||||
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
|
|
||||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||||
import { Track } from '../models/Track';
|
import { getConnectionManager, Repository } from 'typeorm';
|
||||||
import { IsInt, IsNotEmpty, IsPositive, IsString } from 'class-validator';
|
import { TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors";
|
||||||
|
import { CreateTrack } from '../models/actions/CreateTrack';
|
||||||
class CreateTrack {
|
import { UpdateTrack } from '../models/actions/UpdateTrack';
|
||||||
@IsString()
|
import { Track } from '../models/entities/Track';
|
||||||
@IsNotEmpty()
|
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||||
name: string;
|
import { ResponseTrack } from '../models/responses/ResponseTrack';
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@IsPositive()
|
|
||||||
length: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TrackNotFoundError extends NotFoundError {
|
|
||||||
constructor() {
|
|
||||||
super('Track not found!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonController('/tracks')
|
@JsonController('/tracks')
|
||||||
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
export class TrackController {
|
export class TrackController {
|
||||||
private trackRepository: Repository<Track>;
|
private trackRepository: Repository<Track>;
|
||||||
|
|
||||||
@@ -33,59 +21,75 @@ export class TrackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ResponseSchema(Track, { isArray: true })
|
@Authorized("TRACK:GET")
|
||||||
@OpenAPI({description: "Lists all tracks."})
|
@ResponseSchema(ResponseTrack, { isArray: true })
|
||||||
getAll() {
|
@OpenAPI({ description: 'Lists all tracks.' })
|
||||||
return this.trackRepository.find();
|
async getAll() {
|
||||||
|
let responseTracks: ResponseTrack[] = new Array<ResponseTrack>();
|
||||||
|
const tracks = await this.trackRepository.find();
|
||||||
|
tracks.forEach(track => {
|
||||||
|
responseTracks.push(new ResponseTrack(track));
|
||||||
|
});
|
||||||
|
return responseTracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/:id')
|
@Get('/:id')
|
||||||
@ResponseSchema(Track)
|
@Authorized("TRACK:GET")
|
||||||
|
@ResponseSchema(ResponseTrack)
|
||||||
|
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
|
||||||
@OnUndefined(TrackNotFoundError)
|
@OnUndefined(TrackNotFoundError)
|
||||||
@OpenAPI({description: "Returns a track of a specified id (if it exists)"})
|
@OpenAPI({ description: "Lists all information about the track whose id got provided." })
|
||||||
getOne(@Param('id') id: number) {
|
async getOne(@Param('id') id: number) {
|
||||||
return this.trackRepository.findOne({ id: id });
|
let track = await this.trackRepository.findOne({ id: id });
|
||||||
|
if (!track) { throw new TrackNotFoundError(); }
|
||||||
|
return new ResponseTrack(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ResponseSchema(Track)
|
@Authorized("TRACK:CREATE")
|
||||||
@OpenAPI({description: "Create a new track object (id will be generated automagicly)."})
|
@ResponseSchema(ResponseTrack)
|
||||||
post(
|
@ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Create a new track. <br> Please remember that the track\'s distance must be greater than 0." })
|
||||||
|
async post(
|
||||||
@Body({ validate: true })
|
@Body({ validate: true })
|
||||||
track: CreateTrack
|
track: CreateTrack
|
||||||
) {
|
) {
|
||||||
return this.trackRepository.save(track);
|
return new ResponseTrack(await this.trackRepository.save(track.toTrack()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('/:id')
|
@Put('/:id')
|
||||||
@ResponseSchema(Track)
|
@Authorized("TRACK:UPDATE")
|
||||||
@OpenAPI({description: "Update a track object (id can't be changed)."})
|
@ResponseSchema(ResponseTrack)
|
||||||
async put(@Param('id') id: number, @EntityFromBody() track: Track) {
|
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(TrackIdsNotMatchingError, { statusCode: 406 })
|
||||||
|
@ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 })
|
||||||
|
@OpenAPI({ description: "Update the track whose id you provided. <br> Please remember that ids can't be changed." })
|
||||||
|
async put(@Param('id') id: number, @Body({ validate: true }) updateTrack: UpdateTrack) {
|
||||||
let oldTrack = await this.trackRepository.findOne({ id: id });
|
let oldTrack = await this.trackRepository.findOne({ id: id });
|
||||||
|
|
||||||
if (!oldTrack) {
|
if (!oldTrack) {
|
||||||
throw new TrackNotFoundError();
|
throw new TrackNotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(oldTrack.id != track.id){
|
if (oldTrack.id != updateTrack.id) {
|
||||||
throw new NotAcceptableError("The id's don't match!");
|
throw new TrackIdsNotMatchingError();
|
||||||
}
|
}
|
||||||
|
await this.trackRepository.save(await updateTrack.updateTrack(oldTrack));
|
||||||
|
|
||||||
await this.trackRepository.update(oldTrack, track);
|
return new ResponseTrack(await this.trackRepository.findOne({ id: id }));
|
||||||
return track;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('/:id')
|
@Delete('/:id')
|
||||||
@ResponseSchema(Track)
|
@Authorized("TRACK:DELETE")
|
||||||
@OpenAPI({description: "Delete a specified track (if it exists)."})
|
@ResponseSchema(ResponseTrack)
|
||||||
async remove(@Param('id') id: number) {
|
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||||
|
@OnUndefined(204)
|
||||||
|
@OpenAPI({ description: "Delete the track whose id you provided. <br> If no track with this id exists it will just return 204(no content)." })
|
||||||
|
async remove(@Param("id") id: number) {
|
||||||
let track = await this.trackRepository.findOne({ id: id });
|
let track = await this.trackRepository.findOne({ id: id });
|
||||||
|
if (!track) { return null; }
|
||||||
if (!track) {
|
|
||||||
throw new TrackNotFoundError();
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.trackRepository.delete(track);
|
await this.trackRepository.delete(track);
|
||||||
return track;
|
return new ResponseTrack(track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
108
src/controllers/UserController.ts
Normal file
108
src/controllers/UserController.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
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, UserNotFoundError } from '../errors/UserErrors';
|
||||||
|
import { UserGroupNotFoundError } from '../errors/UserGroupErrors';
|
||||||
|
import { CreateUser } from '../models/actions/CreateUser';
|
||||||
|
import { UpdateUser } from '../models/actions/UpdateUser';
|
||||||
|
import { User } from '../models/entities/User';
|
||||||
|
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||||
|
import { ResponseUser } from '../models/responses/ResponseUser';
|
||||||
|
import { PermissionController } from './PermissionController';
|
||||||
|
|
||||||
|
|
||||||
|
@JsonController('/users')
|
||||||
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
export class UserController {
|
||||||
|
private userRepository: Repository<User>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the repository of this controller's model/entity.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.userRepository = getConnectionManager().get().getRepository(User);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Authorized("USER:GET")
|
||||||
|
@ResponseSchema(User, { isArray: true })
|
||||||
|
@OpenAPI({ description: 'Lists all users. <br> This includes their groups and permissions directly granted to them (if existing/associated).' })
|
||||||
|
async getAll() {
|
||||||
|
let responseUsers: ResponseUser[] = new Array<ResponseUser>();
|
||||||
|
const users = await this.userRepository.find({ relations: ['permissions', 'groups'] });
|
||||||
|
users.forEach(user => {
|
||||||
|
responseUsers.push(new ResponseUser(user));
|
||||||
|
});
|
||||||
|
return responseUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
@Authorized("USER:GET")
|
||||||
|
@ResponseSchema(User)
|
||||||
|
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||||
|
@OnUndefined(UserNotFoundError)
|
||||||
|
@OpenAPI({ description: 'Lists all information about the user whose id got provided. <br> Please remember that only permissions granted directly to the user will show up here, not permissions inherited from groups.' })
|
||||||
|
async getOne(@Param('id') id: number) {
|
||||||
|
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] })
|
||||||
|
if (!user) { throw new UserNotFoundError(); }
|
||||||
|
return new ResponseUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Authorized("USER:CREATE")
|
||||||
|
@ResponseSchema(User)
|
||||||
|
@ResponseSchema(UserGroupNotFoundError)
|
||||||
|
@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;
|
||||||
|
try {
|
||||||
|
user = await createUser.toUser();
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
user = await this.userRepository.save(user)
|
||||||
|
return new ResponseUser(await this.userRepository.findOne({ id: user.id }, { relations: ['permissions', 'groups'] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:id')
|
||||||
|
@Authorized("USER:UPDATE")
|
||||||
|
@ResponseSchema(User)
|
||||||
|
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||||
|
@ResponseSchema(UserIdsNotMatchingError, { 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 });
|
||||||
|
|
||||||
|
if (!oldUser) {
|
||||||
|
throw new UserNotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldUser.id != updateUser.id) {
|
||||||
|
throw new UserIdsNotMatchingError();
|
||||||
|
}
|
||||||
|
await this.userRepository.save(await updateUser.updateUser(oldUser));
|
||||||
|
|
||||||
|
return new ResponseUser(await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id')
|
||||||
|
@Authorized("USER:DELETE")
|
||||||
|
@ResponseSchema(User)
|
||||||
|
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||||
|
@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).' })
|
||||||
|
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||||
|
let user = await this.userRepository.findOne({ id: id });
|
||||||
|
if (!user) { return null; }
|
||||||
|
const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] });;
|
||||||
|
|
||||||
|
const permissionControler = new PermissionController();
|
||||||
|
for (let permission of responseUser.permissions) {
|
||||||
|
await permissionControler.remove(permission.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userRepository.delete(user);
|
||||||
|
return new ResponseUser(responseUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/controllers/UserGroupController.ts
Normal file
99
src/controllers/UserGroupController.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
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 { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/UserGroupErrors';
|
||||||
|
import { CreateUserGroup } from '../models/actions/CreateUserGroup';
|
||||||
|
import { UserGroup } from '../models/entities/UserGroup';
|
||||||
|
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||||
|
import { ResponseUserGroup } from '../models/responses/ResponseUserGroup';
|
||||||
|
import { PermissionController } from './PermissionController';
|
||||||
|
|
||||||
|
|
||||||
|
@JsonController('/usergroups')
|
||||||
|
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||||
|
export class UserGroupController {
|
||||||
|
private userGroupsRepository: Repository<UserGroup>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the repository of this controller's model/entity.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.userGroupsRepository = getConnectionManager().get().getRepository(UserGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Authorized("USERGROUP:GET")
|
||||||
|
@ResponseSchema(UserGroup, { 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"] });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
@Authorized("USERGROUP:GET")
|
||||||
|
@ResponseSchema(UserGroup)
|
||||||
|
@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"] });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Authorized("USERGROUP:CREATE")
|
||||||
|
@ResponseSchema(UserGroup)
|
||||||
|
@ResponseSchema(UserGroupNotFoundError)
|
||||||
|
@OpenAPI({ description: 'Create a new group. <br> If you want to grant permissions to the group you have to create them seperately by posting to /api/permissions after creating the group.' })
|
||||||
|
async post(@Body({ validate: true }) createUserGroup: CreateUserGroup) {
|
||||||
|
let userGroup;
|
||||||
|
try {
|
||||||
|
userGroup = await createUserGroup.toUserGroup();
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userGroupsRepository.save(userGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:id')
|
||||||
|
@Authorized("USERGROUP:UPDATE")
|
||||||
|
@ResponseSchema(UserGroup)
|
||||||
|
@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() userGroup: UserGroup) {
|
||||||
|
let oldUserGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
|
||||||
|
|
||||||
|
if (!oldUserGroup) {
|
||||||
|
throw new UserGroupNotFoundError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldUserGroup.id != userGroup.id) {
|
||||||
|
throw new UserGroupIdsNotMatchingError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userGroupsRepository.save(userGroup);
|
||||||
|
return userGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id')
|
||||||
|
@Authorized("USERGROUP:DELETE")
|
||||||
|
@ResponseSchema(ResponseUserGroup)
|
||||||
|
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||||
|
@OnUndefined(204)
|
||||||
|
@OpenAPI({ description: 'Delete the group whose id you provided. <br> If there are any permissions directly granted to the group they will get deleted as well. <br> Users associated with this group won\'t get deleted - just deassociated. <br> If no group with this id exists it will just return 204(no content).' })
|
||||||
|
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||||
|
let group = await this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
|
||||||
|
if (!group) { return null; }
|
||||||
|
const responseGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] });
|
||||||
|
|
||||||
|
const permissionControler = new PermissionController();
|
||||||
|
for (let permission of responseGroup.permissions) {
|
||||||
|
await permissionControler.remove(permission.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userGroupsRepository.delete(group);
|
||||||
|
return new ResponseUserGroup(responseGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/errors/AddressErrors.ts
Normal file
24
src/errors/AddressErrors.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw, when to provided address doesn't belong to the accepted types.
|
||||||
|
*/
|
||||||
|
export class AddressWrongTypeError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "AddressWrongTypeError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The address must be an existing adress's id. \n You provided a object of another type."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw, when a non-existant address get's loaded.
|
||||||
|
*/
|
||||||
|
export class AddressNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "AddressNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The address you provided couldn't be located in the system. \n Please check your request."
|
||||||
|
}
|
||||||
140
src/errors/AuthError.ts
Normal file
140
src/errors/AuthError.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { ForbiddenError, NotAcceptableError, NotFoundError, UnauthorizedError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a jwt could not be parsed.
|
||||||
|
* For example: Wrong signature or expired.
|
||||||
|
*/
|
||||||
|
export class IllegalJWTError extends UnauthorizedError {
|
||||||
|
@IsString()
|
||||||
|
name = "IllegalJWTError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Your provided jwt could not be parsed."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when user is nonexistant or refreshtoken is invalid.
|
||||||
|
* This can happen if someone provides a JWT with a invalid user id or the refreshTokenCount of the user is higher that the provided jwt's is.
|
||||||
|
*/
|
||||||
|
export class UserNonexistantOrRefreshtokenInvalidError extends UnauthorizedError {
|
||||||
|
@IsString()
|
||||||
|
name = "UserNonexistantOrRefreshtokenInvalidError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "User is nonexistant or refreshtoken is invalid."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when provided credentials are invalid.
|
||||||
|
* We don't have seperate errors for username/mail and passwords to protect against guessing attacks.
|
||||||
|
*/
|
||||||
|
export class InvalidCredentialsError extends UnauthorizedError {
|
||||||
|
@IsString()
|
||||||
|
name = "InvalidCredentialsError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Your provided credentials are invalid."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a jwt does not have permission for this route/action.
|
||||||
|
* Mainly used be the @Authorized decorator (via the authchecker).
|
||||||
|
*/
|
||||||
|
export class NoPermissionError extends ForbiddenError {
|
||||||
|
@IsString()
|
||||||
|
name = "NoPermissionError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Your provided jwt does not have permission for this route/ action."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when no username and no email is set.
|
||||||
|
* Because we have to identify users somehow.
|
||||||
|
*/
|
||||||
|
export class UsernameOrEmailNeededError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "UsernameOrEmailNeededError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Auth needs to have email or username set! \n You provided neither."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when no password is provided for a new user.
|
||||||
|
* Passwords are the minimum we need for user security.
|
||||||
|
*/
|
||||||
|
export class PasswordNeededError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "PasswordNeededError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "No password is provided - you need to provide it."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when no user could be found for a certain query.
|
||||||
|
*/
|
||||||
|
export class UserNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "UserNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The user you provided couldn't be located in the system. \n Please check your request."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when no jwt was provided (but one had to be).
|
||||||
|
*/
|
||||||
|
export class JwtNotProvidedError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "JwtNotProvidedError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "No jwt was provided."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when user was not found or the jwt's refresh token count was invalid.
|
||||||
|
*/
|
||||||
|
export class UserNotFoundOrRefreshTokenCountInvalidError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "UserNotFoundOrRefreshTokenCountInvalidError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "User was not found or the refresh token count is invalid."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when refresh token count was invalid
|
||||||
|
*/
|
||||||
|
export class RefreshTokenCountInvalidError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "RefreshTokenCountInvalidError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Refresh token count is invalid."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when someone tryes to reset a user's password more than once in 15 minutes.
|
||||||
|
*/
|
||||||
|
export class ResetAlreadyRequestedError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "ResetAlreadyRequestedError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "You already requested a password reset in the last 15 minutes. \n Please wait until the old reset code expires before requesting a new one."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when someone tries a disabled user's password or login as a disabled user.
|
||||||
|
*/
|
||||||
|
export class UserDisabledError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "UserDisabledError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "This user is currently disabled. \n Please contact your administrator if this is a mistake."
|
||||||
|
}
|
||||||
36
src/errors/DonorErrors.ts
Normal file
36
src/errors/DonorErrors.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a donor couldn't be found.
|
||||||
|
*/
|
||||||
|
export class DonorNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "DonorNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Donor not found!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when two donors' ids don't match.
|
||||||
|
* Usually occurs when a user tries to change a donor's id.
|
||||||
|
*/
|
||||||
|
export class DonorIdsNotMatchingError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "DonorIdsNotMatchingError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The ids don't match! \n And if you wanted to change a donor's id: This isn't allowed!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a donor needs a receipt, but no address is associated with them.
|
||||||
|
*/
|
||||||
|
export class DonorReceiptAddressNeededError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "DonorReceiptAddressNeededError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "An address is needed to create a receipt for a donor. \n You didn't provide one."
|
||||||
|
}
|
||||||
24
src/errors/GroupContactErrors.ts
Normal file
24
src/errors/GroupContactErrors.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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-existant groupContact get's loaded.
|
||||||
|
*/
|
||||||
|
export class GroupContactNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "GroupContactNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The groupContact you provided couldn't be located in the system. \n Please check your request."
|
||||||
|
}
|
||||||
36
src/errors/PermissionErrors.ts
Normal file
36
src/errors/PermissionErrors.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a permission couldn't be found.
|
||||||
|
*/
|
||||||
|
export class PermissionNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "PermissionNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Permission not found!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when two permissions' ids don't match.
|
||||||
|
* Usually occurs when a user tries to change a permission's id.
|
||||||
|
*/
|
||||||
|
export class PermissionIdsNotMatchingError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "PermissionIdsNotMatchingError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The ids don't match! \n And if you wanted to change a permission's id: This isn't allowed!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a permission gets provided without a principal.
|
||||||
|
*/
|
||||||
|
export class PermissionNeedsPrincipalError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "PermissionNeedsPrincipalError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "You provided no principal for this permission."
|
||||||
|
}
|
||||||
24
src/errors/PrincipalErrors.ts
Normal file
24
src/errors/PrincipalErrors.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a user couldn't be found.
|
||||||
|
*/
|
||||||
|
export class PrincipalNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "PrincipalNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Principal not found!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw, when a provided runnerOrganisation doesn't belong to the accepted types.
|
||||||
|
*/
|
||||||
|
export class PrincipalWrongTypeError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "PrincipalWrongTypeError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The princial must have an existing principal's id. \n You provided a object of another type."
|
||||||
|
}
|
||||||
36
src/errors/RunnerErrors.ts
Normal file
36
src/errors/RunnerErrors.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a runner couldn't be found.
|
||||||
|
*/
|
||||||
|
export class RunnerNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "RunnerNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Runner not found!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when two runners' ids don't match.
|
||||||
|
* Usually occurs when a user tries to change a runner's id.
|
||||||
|
*/
|
||||||
|
export class RunnerIdsNotMatchingError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "RunnerIdsNotMatchingError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The ids don't match! \n And if you wanted to change a runner's id: This isn't allowed!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a runner is missing his group association.
|
||||||
|
*/
|
||||||
|
export class RunnerGroupNeededError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "RunnerGroupNeededError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Runner's need to be part of one group (team or organisiation)! \n You provided neither."
|
||||||
|
}
|
||||||
13
src/errors/RunnerGroupErrors.ts
Normal file
13
src/errors/RunnerGroupErrors.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a runner group couldn't be found.
|
||||||
|
*/
|
||||||
|
export class RunnerGroupNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "RunnerGroupNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "RunnerGroup not found!"
|
||||||
|
}
|
||||||
58
src/errors/RunnerOrganisationErrors.ts
Normal file
58
src/errors/RunnerOrganisationErrors.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a runner organisation couldn't be found.
|
||||||
|
*/
|
||||||
|
export class RunnerOrganisationNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "RunnerOrganisationNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "RunnerOrganisation not found!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when two runner organisations' 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."
|
||||||
|
}
|
||||||
47
src/errors/RunnerTeamErrors.ts
Normal file
47
src/errors/RunnerTeamErrors.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a runner team couldn't be found.
|
||||||
|
*/
|
||||||
|
export class RunnerTeamNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "RunnerTeamNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "RunnerTeam not found!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when two runner teams' ids don't match.
|
||||||
|
* Usually occurs when a user tries to change a runner team's id.
|
||||||
|
*/
|
||||||
|
export class RunnerTeamIdsNotMatchingError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "RunnerTeamIdsNotMatchingError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The ids don't match! \n And if you wanted to change a runner's id: This isn't allowed!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a team still has runners associated.
|
||||||
|
*/
|
||||||
|
export class RunnerTeamHasRunnersError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "RunnerTeamHasRunnersError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "This team still has runners associated with it. \n If you want to delete this team with all it's runners and teams add `?force` to your query."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a team still has runners associated.
|
||||||
|
*/
|
||||||
|
export class RunnerTeamNeedsParentError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "RunnerTeamNeedsParentError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "You provided no runner organisation as this team's parent group."
|
||||||
|
}
|
||||||
25
src/errors/StatsClientErrors.ts
Normal file
25
src/errors/StatsClientErrors.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw, when a non-existant stats client get's loaded.
|
||||||
|
*/
|
||||||
|
export class StatsClientNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "StatsClientNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The stats client you provided couldn't be located in the system. \n Please check your request."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when two stats clients' ids don't match.
|
||||||
|
* Usually occurs when a user tries to change a stats client's id.
|
||||||
|
*/
|
||||||
|
export class StatsClientIdsNotMatchingError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "StatsClientIdsNotMatchingError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The ids don't match! \n And if you wanted to change a stats client's id: This isn't allowed!"
|
||||||
|
}
|
||||||
36
src/errors/TrackErrors.ts
Normal file
36
src/errors/TrackErrors.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a track couldn't be found.
|
||||||
|
*/
|
||||||
|
export class TrackNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "TrackNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "Track not found!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when two tracks' ids don't match.
|
||||||
|
* Usually occurs when a user tries to change a track's id.
|
||||||
|
*/
|
||||||
|
export class TrackIdsNotMatchingError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "TrackIdsNotMatchingError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The ids don't match! \n And if you wanted to change a track's id: This isn't allowed"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a track's lap time is set to a negative value.
|
||||||
|
*/
|
||||||
|
export class TrackLapTimeCantBeNegativeError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "TrackLapTimeCantBeNegativeError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The minimum lap time you provided is negative - That isn't possible. \n If you wanted to disable it: Just set it to 0/null."
|
||||||
|
}
|
||||||
38
src/errors/UserErrors.ts
Normal file
38
src/errors/UserErrors.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when no username or email is set.
|
||||||
|
* We somehow need to identify you :)
|
||||||
|
*/
|
||||||
|
export class UsernameOrEmailNeededError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "UsernameOrEmailNeededError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "No username or email is set!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a user couldn't be found.
|
||||||
|
*/
|
||||||
|
export class UserNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "UserNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "User not found!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when two users' ids don't match.
|
||||||
|
* Usually occurs when a user tries to change a user's id.
|
||||||
|
*/
|
||||||
|
export class UserIdsNotMatchingError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "UserIdsNotMatchingError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The ids don't match!! \n And if you wanted to change a user's id: This isn't allowed!"
|
||||||
|
}
|
||||||
36
src/errors/UserGroupErrors.ts
Normal file
36
src/errors/UserGroupErrors.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when no groupname is set.
|
||||||
|
*/
|
||||||
|
export class GroupNameNeededError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "GroupNameNeededError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "No name is set for this group!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when a usergroup couldn't be found.
|
||||||
|
*/
|
||||||
|
export class UserGroupNotFoundError extends NotFoundError {
|
||||||
|
@IsString()
|
||||||
|
name = "UserGroupNotFoundError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "User Group not found!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error to throw when two usergroups' ids don't match.
|
||||||
|
* Usually occurs when a user tries to change a usergroups's id.
|
||||||
|
*/
|
||||||
|
export class UserGroupIdsNotMatchingError extends NotAcceptableError {
|
||||||
|
@IsString()
|
||||||
|
name = "UserGroupIdsNotMatchingError"
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message = "The ids don't match!! \n If you wanted to change a usergroup's id: This isn't allowed!"
|
||||||
|
}
|
||||||
128
src/jwtcreator.ts
Normal file
128
src/jwtcreator.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||||
|
import * as jsonwebtoken from "jsonwebtoken";
|
||||||
|
import { config } from './config';
|
||||||
|
import { User } from './models/entities/User';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for all things JWT creation.
|
||||||
|
*/
|
||||||
|
export class JwtCreator {
|
||||||
|
/**
|
||||||
|
* Creates a new refresh token for a given user
|
||||||
|
* @param user User entity that the refresh token shall be created for
|
||||||
|
* @param expiry_timestamp Timestamp for the token expiry. Will be generated if not provided.
|
||||||
|
*/
|
||||||
|
public static createRefresh(user: User, expiry_timestamp?: number) {
|
||||||
|
if (!expiry_timestamp) { expiry_timestamp = Math.floor(Date.now() / 1000) + 10 * 36000; }
|
||||||
|
return jsonwebtoken.sign({
|
||||||
|
refreshTokenCount: user.refreshTokenCount,
|
||||||
|
id: user.id,
|
||||||
|
exp: expiry_timestamp
|
||||||
|
}, config.jwt_secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new access token for a given user
|
||||||
|
* @param user User entity that the access token shall be created for
|
||||||
|
* @param expiry_timestamp Timestamp for the token expiry. Will be generated if not provided.
|
||||||
|
*/
|
||||||
|
public static createAccess(user: User, expiry_timestamp?: number) {
|
||||||
|
if (!expiry_timestamp) { expiry_timestamp = Math.floor(Date.now() / 1000) + 10 * 36000; }
|
||||||
|
return jsonwebtoken.sign({
|
||||||
|
userdetails: new JwtUser(user),
|
||||||
|
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.
|
||||||
|
* @param user User entity that the password reset token shall be created for
|
||||||
|
*/
|
||||||
|
public static createReset(user: User) {
|
||||||
|
let expiry_timestamp = Math.floor(Date.now() / 1000) + 15 * 60;
|
||||||
|
return jsonwebtoken.sign({
|
||||||
|
id: user.id,
|
||||||
|
refreshTokenCount: user.refreshTokenCount,
|
||||||
|
exp: expiry_timestamp
|
||||||
|
}, config.jwt_secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special variant of the user class that
|
||||||
|
*/
|
||||||
|
export class JwtUser {
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@IsUUID(4)
|
||||||
|
uuid: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
firstname: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
middlename?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
lastname: string;
|
||||||
|
|
||||||
|
permissions: string[];
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@IsNotEmpty()
|
||||||
|
refreshTokenCount?: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
profilePic?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of this class based on a provided user entity.
|
||||||
|
* @param user User entity that shall be encapsulated in a jwt.
|
||||||
|
*/
|
||||||
|
public constructor(user: User) {
|
||||||
|
this.id = user.id;
|
||||||
|
this.firstname = user.firstname;
|
||||||
|
this.middlename = user.middlename;
|
||||||
|
this.lastname = user.lastname;
|
||||||
|
this.username = user.username;
|
||||||
|
this.email = user.email;
|
||||||
|
this.refreshTokenCount = user.refreshTokenCount;
|
||||||
|
this.uuid = user.uuid;
|
||||||
|
this.profilePic = user.profilePic;
|
||||||
|
this.permissions = this.getPermissions(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handels getting the permissions granted to this user (direct or indirect).
|
||||||
|
* @param user User which's permissions shall be gotten.
|
||||||
|
*/
|
||||||
|
public getPermissions(user: User): string[] {
|
||||||
|
let returnPermissions: string[] = new Array<string>();
|
||||||
|
for (let permission of user.permissions) {
|
||||||
|
returnPermissions.push(permission.toString());
|
||||||
|
}
|
||||||
|
for (let group of user.groups) {
|
||||||
|
for (let permission of group.permissions) {
|
||||||
|
returnPermissions.push(permission.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(new Set(returnPermissions));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
import { createConnection } from "typeorm";
|
import { createConnection } from "typeorm";
|
||||||
|
import { runSeeder } from 'typeorm-seeding';
|
||||||
|
import { User } from '../models/entities/User';
|
||||||
|
import SeedUsers from '../seeds/SeedUsers';
|
||||||
|
/**
|
||||||
|
* Loader for the database that creates the database connection and initializes the database tabels.
|
||||||
|
* It also triggers the seeding process if no users got detected in the database.
|
||||||
|
*/
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const connection = await createConnection();
|
const connection = await createConnection();
|
||||||
connection.synchronize();
|
await connection.synchronize();
|
||||||
|
if (await connection.getRepository(User).count() === 0) {
|
||||||
|
await runSeeder(SeedUsers);
|
||||||
|
}
|
||||||
return connection;
|
return connection;
|
||||||
};
|
};
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
|
import cookieParser from "cookie-parser";
|
||||||
import { Application } from "express";
|
import { Application } from "express";
|
||||||
import bodyParser from 'body-parser';
|
/**
|
||||||
import cors from 'cors';
|
* Loader for express related configurations.
|
||||||
|
* Configures proxy trusts, globally used middlewares and other express features.
|
||||||
|
*/
|
||||||
export default async (app: Application) => {
|
export default async (app: Application) => {
|
||||||
app.enable('trust proxy');
|
app.enable('trust proxy');
|
||||||
|
app.disable('x-powered-by');
|
||||||
|
app.disable('x-served-by');
|
||||||
|
app.use(cookieParser());
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import { Application } from "express";
|
||||||
|
import databaseLoader from "./database";
|
||||||
import expressLoader from "./express";
|
import expressLoader from "./express";
|
||||||
import openapiLoader from "./openapi";
|
import openapiLoader from "./openapi";
|
||||||
import databaseLoader from "./database";
|
|
||||||
import { Application } from "express";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index Loader that executes the other loaders in the right order.
|
||||||
|
* This basicly exists for abstraction and a overall better dev experience.
|
||||||
|
*/
|
||||||
export default async (app: Application) => {
|
export default async (app: Application) => {
|
||||||
await databaseLoader();
|
await databaseLoader();
|
||||||
await openapiLoader(app);
|
await openapiLoader(app);
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import { Application } from "express";
|
import { validationMetadatasToSchemas } from "class-validator-jsonschema";
|
||||||
import * as swaggerUiExpress from "swagger-ui-express";
|
import express, { Application } from "express";
|
||||||
|
import path from 'path';
|
||||||
import { getMetadataArgsStorage } from "routing-controllers";
|
import { getMetadataArgsStorage } from "routing-controllers";
|
||||||
import { routingControllersToSpec } from "routing-controllers-openapi";
|
import { routingControllersToSpec } from "routing-controllers-openapi";
|
||||||
import { validationMetadatasToSchemas } from "class-validator-jsonschema";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loader for everything openapi related - from creating the schema to serving it via a static route and swaggerUiExpress.
|
||||||
|
* All auth schema related stuff also has to be configured here
|
||||||
|
*/
|
||||||
export default async (app: Application) => {
|
export default async (app: Application) => {
|
||||||
const storage = getMetadataArgsStorage();
|
const storage = getMetadataArgsStorage();
|
||||||
const schemas = validationMetadatasToSchemas({
|
const schemas = validationMetadatasToSchemas({
|
||||||
refPointerPrefix: "#/components/schemas/",
|
refPointerPrefix: "#/components/schemas/",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Spec creation based on the previously created schemas
|
||||||
const spec = routingControllersToSpec(
|
const spec = routingControllersToSpec(
|
||||||
storage,
|
storage,
|
||||||
{
|
{
|
||||||
@@ -17,24 +23,36 @@ export default async (app: Application) => {
|
|||||||
{
|
{
|
||||||
components: {
|
components: {
|
||||||
schemas,
|
schemas,
|
||||||
|
"securitySchemes": {
|
||||||
|
"AuthToken": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer",
|
||||||
|
"bearerFormat": "JWT",
|
||||||
|
description: "A JWT based access token. Use /api/auth/login or /api/auth/refresh to get one."
|
||||||
|
},
|
||||||
|
"RefreshTokenCookie": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"in": "cookie",
|
||||||
|
"name": "lfk_backend__refresh_token",
|
||||||
|
description: "A cookie containing a JWT based refreh token. Attention: Doesn't work in swagger-ui. Use /api/auth/login or /api/auth/refresh to get one."
|
||||||
|
},
|
||||||
|
"StatsApiToken": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer",
|
||||||
|
description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients)."
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
description: "The the backend API for the LfK! runner system.",
|
description: "The the backend API for the LfK! runner system.",
|
||||||
title: "LfK! Backend API",
|
title: "LfK! Backend API",
|
||||||
version: "1.0.0",
|
version: "0.0.8",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const options = {
|
app.get(["/api/docs/openapi.json", "/api/docs/swagger.json"], (req, res) => {
|
||||||
explorer: true,
|
|
||||||
};
|
|
||||||
app.use(
|
|
||||||
"/api/docs",
|
|
||||||
swaggerUiExpress.serve,
|
|
||||||
swaggerUiExpress.setup(spec, options)
|
|
||||||
);
|
|
||||||
app.get(["/api/openapi.json", "/api/swagger.json"], (req, res) => {
|
|
||||||
res.json(spec);
|
res.json(spec);
|
||||||
});
|
});
|
||||||
|
app.use('/api/docs', express.static(path.join(__dirname, '../static/docs'), { index: "index.html", extensions: ['html'] }));
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|||||||
14
src/middlewares/ErrorHandler.ts
Normal file
14
src/middlewares/ErrorHandler.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { ExpressErrorMiddlewareInterface, Middleware } from "routing-controllers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our Error handling middlware that returns our custom httperrors to the user.
|
||||||
|
*/
|
||||||
|
@Middleware({ type: "after" })
|
||||||
|
export class ErrorHandler implements ExpressErrorMiddlewareInterface {
|
||||||
|
public error(error: any, request: any, response: any, next: (err: any) => any) {
|
||||||
|
if (response.headersSent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.json(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/middlewares/RawBody.ts
Normal file
23
src/middlewares/RawBody.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom express middleware that appends the raw body to the request obeject.
|
||||||
|
* Mainly used for parsing csvs from boddies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RawBodyMiddleware = (req: Request, res: Response, next: () => void) => {
|
||||||
|
const body = []
|
||||||
|
req.on('data', chunk => {
|
||||||
|
body.push(chunk)
|
||||||
|
})
|
||||||
|
req.on('end', () => {
|
||||||
|
const rawBody = Buffer.concat(body)
|
||||||
|
req['rawBody'] = rawBody
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
req.on('error', () => {
|
||||||
|
res.sendStatus(400)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RawBodyMiddleware
|
||||||
65
src/middlewares/StatsAuth.ts
Normal file
65
src/middlewares/StatsAuth.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import * as argon2 from "argon2";
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { StatsClient } from '../models/entities/StatsClient';
|
||||||
|
import authchecker from './authchecker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This middleware handels the authentification of stats client api tokens.
|
||||||
|
* The tokens have to be provided via Bearer auth header.
|
||||||
|
* @param req Express request object.
|
||||||
|
* @param res Express response object.
|
||||||
|
* @param next Next function to call on success.
|
||||||
|
*/
|
||||||
|
const StatsAuth = 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.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
provided_token = provided_token.replace("Bearer ", "");
|
||||||
|
} catch (error) {
|
||||||
|
res.status(401).send("No valid jwt or api token provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix = "";
|
||||||
|
try {
|
||||||
|
prefix = provided_token.split(".")[0];
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (prefix == "" || prefix == undefined || prefix == null) {
|
||||||
|
res.status(401).send("Api token non-existant or invalid syntax.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await getConnectionManager().get().getRepository(StatsClient).findOne({ prefix: prefix });
|
||||||
|
if (!client) {
|
||||||
|
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"]);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (user_authorized == false) {
|
||||||
|
res.status(401).send("Api token non-existant or invalid syntax.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!(await argon2.verify(client.key, provided_token))) {
|
||||||
|
res.status(401).send("Api token invalid.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default StatsAuth;
|
||||||
74
src/middlewares/authchecker.ts
Normal file
74
src/middlewares/authchecker.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import cookie from "cookie";
|
||||||
|
import * as jwt from "jsonwebtoken";
|
||||||
|
import { Action } from "routing-controllers";
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { config } from '../config';
|
||||||
|
import { IllegalJWTError, NoPermissionError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from '../errors/AuthError';
|
||||||
|
import { JwtCreator, JwtUser } from '../jwtcreator';
|
||||||
|
import { User } from '../models/entities/User';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handels authorisation verification via jwt's for all api endpoints using the @Authorized decorator.
|
||||||
|
* @param action Routing-Controllers action object that provides request and response objects among other stuff.
|
||||||
|
* @param permissions The permissions that the endpoint using @Authorized requires.
|
||||||
|
*/
|
||||||
|
const authchecker = async (action: Action, permissions: string[] | string) => {
|
||||||
|
let required_permissions = undefined;
|
||||||
|
if (typeof permissions === "string") {
|
||||||
|
required_permissions = [permissions]
|
||||||
|
} else {
|
||||||
|
required_permissions = permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
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"] }, { relations: ['permissions'] })
|
||||||
|
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
|
||||||
|
if (user.enabled == false) { throw new UserDisabledError(); }
|
||||||
|
if (!jwtPayload["permissions"]) { throw new NoPermissionError(); }
|
||||||
|
|
||||||
|
action.response.local = {}
|
||||||
|
action.response.local.jwtPayload = jwtPayload;
|
||||||
|
for (let required_permission of required_permissions) {
|
||||||
|
if (!(jwtPayload["permissions"].includes(required_permission))) { return false; }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handels 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 authchecker
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
// import bodyParser from 'body-parser';
|
|
||||||
// import cors from 'cors';
|
|
||||||
import * as jwt from "jsonwebtoken";
|
|
||||||
|
|
||||||
export default (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const token = <string>req.headers["auth"];
|
|
||||||
try {
|
|
||||||
const jwtPayload = <any>jwt.verify(token, "secretjwtsecret");
|
|
||||||
// const jwtPayload = <any>jwt.verify(token, process.env.JWT_SECRET);
|
|
||||||
res.locals.jwtPayload = jwtPayload;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return res.status(401).send();
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
|
|
||||||
import {
|
|
||||||
IsInt,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsOptional,
|
|
||||||
IsPositive,
|
|
||||||
IsString,
|
|
||||||
} from "class-validator";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @classdesc Defines a track of given length.
|
|
||||||
* @property {number} id - Autogenerated unique id
|
|
||||||
* @property {string} name - The track's name
|
|
||||||
* @property {number} lenth - The track's length in meters
|
|
||||||
*/
|
|
||||||
@Entity()
|
|
||||||
export class Track {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
@IsOptional()
|
|
||||||
@IsInt()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
@IsInt()
|
|
||||||
@IsPositive()
|
|
||||||
length: number;
|
|
||||||
}
|
|
||||||
70
src/models/actions/CreateAddress.ts
Normal file
70
src/models/actions/CreateAddress.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { IsNotEmpty, IsOptional, IsPostalCode, IsString } from 'class-validator';
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { Address } from '../entities/Address';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new Address entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateAddress {
|
||||||
|
/**
|
||||||
|
* The newaddress's description.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new address's first line.
|
||||||
|
* Containing the street and house number.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
address1: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new address's second line.
|
||||||
|
* Containing optional information.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
address2?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new address's postal code.
|
||||||
|
* This will get checked against the postal code syntax for the configured country.
|
||||||
|
* TODO: Implement the config option.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsPostalCode(config.postalcode_validation_countrycode)
|
||||||
|
postalcode: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new address's city.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
city: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new address's country.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
country: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Address entity from this.
|
||||||
|
*/
|
||||||
|
public toAddress(): 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/models/actions/CreateAuth.ts
Normal file
73
src/models/actions/CreateAuth.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import * as argon2 from "argon2";
|
||||||
|
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { InvalidCredentialsError, PasswordNeededError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
|
||||||
|
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
|
||||||
|
import { JwtCreator } from '../../jwtcreator';
|
||||||
|
import { User } from '../entities/User';
|
||||||
|
import { Auth } from '../responses/ResponseAuth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to create auth credentials based on user credentials provided in a json body (post request).
|
||||||
|
* To be a little bit more exact: Is takes in a username/email + password and creates a new access and refresh token for the user.
|
||||||
|
* It of course checks for user existance, password validity and so on.
|
||||||
|
*/
|
||||||
|
export class CreateAuth {
|
||||||
|
/**
|
||||||
|
* The username of the user that want's to login.
|
||||||
|
* Either username or email have to be provided.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The email address of the user that want's to login.
|
||||||
|
* Either username or email have to be provided.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
@IsString()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's password.
|
||||||
|
* Will be checked against an argon2 hash.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new auth object based on this.
|
||||||
|
*/
|
||||||
|
public async toAuth(): Promise<Auth> {
|
||||||
|
let newAuth: Auth = new Auth();
|
||||||
|
|
||||||
|
if (this.email === undefined && this.username === undefined) {
|
||||||
|
throw new UsernameOrEmailNeededError();
|
||||||
|
}
|
||||||
|
if (!this.password) {
|
||||||
|
throw new PasswordNeededError();
|
||||||
|
}
|
||||||
|
const found_user = await getConnectionManager().get().getRepository(User).findOne({ relations: ['groups', 'permissions', 'groups.permissions'], where: [{ username: this.username }, { email: this.email }] });
|
||||||
|
if (!found_user) {
|
||||||
|
throw new UserNotFoundError();
|
||||||
|
}
|
||||||
|
if (found_user.enabled == false) { throw new UserDisabledError(); }
|
||||||
|
if (!(await argon2.verify(found_user.password, this.password + found_user.uuid))) {
|
||||||
|
throw new InvalidCredentialsError();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create the access token
|
||||||
|
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60
|
||||||
|
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
|
||||||
|
newAuth.refresh_token = JwtCreator.createRefresh(found_user, timestamp_refresh_expiry);
|
||||||
|
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
|
||||||
|
return newAuth;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/models/actions/CreateDonor.ts
Normal file
38
src/models/actions/CreateDonor.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { IsBoolean, IsOptional } from 'class-validator';
|
||||||
|
import { DonorReceiptAddressNeededError } from '../../errors/DonorErrors';
|
||||||
|
import { Donor } from '../entities/Donor';
|
||||||
|
import { CreateParticipant } from './CreateParticipant';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new Donor entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateDonor extends CreateParticipant {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does this donor need a receipt?
|
||||||
|
*/
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
receiptNeeded?: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Donor entity from this.
|
||||||
|
*/
|
||||||
|
public async toDonor(): Promise<Donor> {
|
||||||
|
let newDonor: Donor = new Donor();
|
||||||
|
|
||||||
|
newDonor.firstname = this.firstname;
|
||||||
|
newDonor.middlename = this.middlename;
|
||||||
|
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) {
|
||||||
|
throw new DonorReceiptAddressNeededError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDonor;
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/models/actions/CreateGroupContact.ts
Normal file
85
src/models/actions/CreateGroupContact.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
|
||||||
|
import { Address } from '../entities/Address';
|
||||||
|
import { GroupContact } from '../entities/GroupContact';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new Group entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateGroupContact {
|
||||||
|
/**
|
||||||
|
* The new contact's first name.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
firstname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new contact's middle name.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
middlename?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new contact's last name.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
lastname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new contact's address.
|
||||||
|
* Must be the address's id.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
address?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 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 contact's email address.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the new contact'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 Address entity from this.
|
||||||
|
*/
|
||||||
|
public async toGroupContact(): 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/models/actions/CreateParticipant.ts
Normal file
72
src/models/actions/CreateParticipant.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
|
||||||
|
import { Address } from '../entities/Address';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new Participant entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export abstract class CreateParticipant {
|
||||||
|
/**
|
||||||
|
* The new participant's first name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
firstname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new participant's middle name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
middlename?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new participant's last name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
lastname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new participant's phone number.
|
||||||
|
* This will be validated against the configured country phone numer syntax (default: international).
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@IsPhoneNumber(config.phone_validation_countrycode)
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new participant's e-mail address.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new participant's address.
|
||||||
|
* Must be of type number (address id).
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
address?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the new participant's address by it's address.
|
||||||
|
*/
|
||||||
|
public async getAddress(): Promise<Address> {
|
||||||
|
if (this.address === undefined || this.address === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isNaN(this.address)) {
|
||||||
|
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
|
||||||
|
if (!address) { throw new AddressNotFoundError; }
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AddressWrongTypeError;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/models/actions/CreatePermission.ts
Normal file
60
src/models/actions/CreatePermission.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty
|
||||||
|
} from "class-validator";
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { PrincipalNotFoundError } from '../../errors/PrincipalErrors';
|
||||||
|
import { Permission } from '../entities/Permission';
|
||||||
|
import { Principal } from '../entities/Principal';
|
||||||
|
import { PermissionAction } from '../enums/PermissionAction';
|
||||||
|
import { PermissionTarget } from '../enums/PermissionTargets';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new Permission entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreatePermission {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new permissions's principal's id.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsNotEmpty()
|
||||||
|
principal: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new permissions's target.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsEnum(PermissionTarget)
|
||||||
|
target: PermissionTarget;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new permissions's action.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsEnum(PermissionAction)
|
||||||
|
action: PermissionAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Permission entity from this.
|
||||||
|
*/
|
||||||
|
public async toPermission(): Promise<Permission> {
|
||||||
|
let newPermission: Permission = new Permission();
|
||||||
|
|
||||||
|
newPermission.principal = await this.getPrincipal();
|
||||||
|
newPermission.target = this.target;
|
||||||
|
newPermission.action = this.action;
|
||||||
|
|
||||||
|
return newPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the new permission's principal by it's id.
|
||||||
|
*/
|
||||||
|
public async getPrincipal(): Promise<Principal> {
|
||||||
|
let principal = await getConnectionManager().get().getRepository(Principal).findOne({ id: this.principal })
|
||||||
|
if (!principal) { throw new PrincipalNotFoundError(); }
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/models/actions/CreateResetToken.ts
Normal file
50
src/models/actions/CreateResetToken.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { IsEmail, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { ResetAlreadyRequestedError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
|
||||||
|
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
|
||||||
|
import { JwtCreator } from '../../jwtcreator';
|
||||||
|
import { User } from '../entities/User';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This calss is used to create password reset tokens for users.
|
||||||
|
* These password reset token can be used to set a new password for the user for the next 15mins.
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
@IsEmail()
|
||||||
|
@IsString()
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
let found_user = await getConnectionManager().get().getRepository(User).findOne({ where: [{ username: this.username }, { email: this.email }] });
|
||||||
|
if (!found_user) { throw new UserNotFoundError(); }
|
||||||
|
if (found_user.enabled == false) { throw new UserDisabledError(); }
|
||||||
|
if (found_user.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 15 * 60)) { throw new ResetAlreadyRequestedError(); }
|
||||||
|
|
||||||
|
found_user.refreshTokenCount = found_user.refreshTokenCount + 1;
|
||||||
|
found_user.resetRequestedTimestamp = Math.floor(Date.now() / 1000);
|
||||||
|
await getConnectionManager().get().getRepository(User).save(found_user);
|
||||||
|
|
||||||
|
//Create the reset token
|
||||||
|
let reset_token = JwtCreator.createReset(found_user);
|
||||||
|
|
||||||
|
return reset_token;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/models/actions/CreateRunner.ts
Normal file
53
src/models/actions/CreateRunner.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { IsInt } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { RunnerGroupNotFoundError } from '../../errors/RunnerGroupErrors';
|
||||||
|
import { RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
|
||||||
|
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
|
||||||
|
import { Runner } from '../entities/Runner';
|
||||||
|
import { RunnerGroup } from '../entities/RunnerGroup';
|
||||||
|
import { CreateParticipant } from './CreateParticipant';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new Runner entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateRunner extends CreateParticipant {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new runner's group's id.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
group: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Runner entity from this.
|
||||||
|
*/
|
||||||
|
public async toRunner(): Promise<Runner> {
|
||||||
|
let newRunner: Runner = new Runner();
|
||||||
|
|
||||||
|
newRunner.firstname = this.firstname;
|
||||||
|
newRunner.middlename = this.middlename;
|
||||||
|
newRunner.lastname = this.lastname;
|
||||||
|
newRunner.phone = this.phone;
|
||||||
|
newRunner.email = this.email;
|
||||||
|
newRunner.group = await this.getGroup();
|
||||||
|
newRunner.address = await this.getAddress();
|
||||||
|
|
||||||
|
return newRunner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the new runner's group by it's id.
|
||||||
|
*/
|
||||||
|
public async getGroup(): Promise<RunnerGroup> {
|
||||||
|
if (this.group === undefined || this.group === null) {
|
||||||
|
throw new RunnerTeamNeedsParentError();
|
||||||
|
}
|
||||||
|
if (!isNaN(this.group)) {
|
||||||
|
let group = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: this.group });
|
||||||
|
if (!group) { throw new RunnerGroupNotFoundError; }
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RunnerOrganisationWrongTypeError;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/models/actions/CreateRunnerGroup.ts
Normal file
40
src/models/actions/CreateRunnerGroup.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { GroupContactNotFoundError, GroupContactWrongTypeError } from '../../errors/GroupContactErrors';
|
||||||
|
import { GroupContact } from '../entities/GroupContact';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new RunnerGroup entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export abstract class CreateRunnerGroup {
|
||||||
|
/**
|
||||||
|
* The new group's name.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new group's contact.
|
||||||
|
* Optional
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
contact?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new GroupContactWrongTypeError;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/models/actions/CreateRunnerOrganisation.ts
Normal file
48
src/models/actions/CreateRunnerOrganisation.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { IsInt, IsOptional } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
|
||||||
|
import { Address } from '../entities/Address';
|
||||||
|
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
|
||||||
|
import { CreateRunnerGroup } from './CreateRunnerGroup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new RunnerOrganisation entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateRunnerOrganisation extends CreateRunnerGroup {
|
||||||
|
/**
|
||||||
|
* The new organisation's address.
|
||||||
|
* Must be of type number (address id).
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
address?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the org's address by it's id.
|
||||||
|
*/
|
||||||
|
public async getAddress(): Promise<Address> {
|
||||||
|
if (this.address === undefined || this.address === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isNaN(this.address)) {
|
||||||
|
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
|
||||||
|
if (!address) { throw new AddressNotFoundError; }
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AddressWrongTypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RunnerOrganisation entity from this.
|
||||||
|
*/
|
||||||
|
public async toRunnerOrganisation(): Promise<RunnerOrganisation> {
|
||||||
|
let newRunnerOrganisation: RunnerOrganisation = new RunnerOrganisation();
|
||||||
|
|
||||||
|
newRunnerOrganisation.name = this.name;
|
||||||
|
newRunnerOrganisation.contact = await this.getContact();
|
||||||
|
// newRunnerOrganisation.address = await this.getAddress();
|
||||||
|
|
||||||
|
return newRunnerOrganisation;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/models/actions/CreateRunnerTeam.ts
Normal file
50
src/models/actions/CreateRunnerTeam.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { IsInt, IsNotEmpty } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
|
||||||
|
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
|
||||||
|
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
|
||||||
|
import { RunnerTeam } from '../entities/RunnerTeam';
|
||||||
|
import { CreateRunnerGroup } from './CreateRunnerGroup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new RunnerTeam entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateRunnerTeam extends CreateRunnerGroup {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new team's parent group (organisation).
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsNotEmpty()
|
||||||
|
parentGroup: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the new team's parent org based on it's id.
|
||||||
|
*/
|
||||||
|
public async getParent(): Promise<RunnerOrganisation> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RunnerTeam entity from this.
|
||||||
|
*/
|
||||||
|
public async toRunnerTeam(): Promise<RunnerTeam> {
|
||||||
|
let newRunnerTeam: RunnerTeam = new RunnerTeam();
|
||||||
|
|
||||||
|
newRunnerTeam.name = this.name;
|
||||||
|
newRunnerTeam.parentGroup = await this.getParent();
|
||||||
|
|
||||||
|
newRunnerTeam.contact = await this.getContact()
|
||||||
|
|
||||||
|
return newRunnerTeam;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/models/actions/CreateStatsClient.ts
Normal file
33
src/models/actions/CreateStatsClient.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as argon2 from "argon2";
|
||||||
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
import { StatsClient } from '../entities/StatsClient';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new StatsClient entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateStatsClient {
|
||||||
|
/**
|
||||||
|
* The new client's description.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this to a StatsClient entity.
|
||||||
|
*/
|
||||||
|
public async toStatsClient(): Promise<StatsClient> {
|
||||||
|
let newClient: StatsClient = new StatsClient();
|
||||||
|
|
||||||
|
newClient.description = this.description;
|
||||||
|
|
||||||
|
let newUUID = uuid.v4().toUpperCase();
|
||||||
|
newClient.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
|
||||||
|
newClient.key = await argon2.hash(newClient.prefix + "." + newUUID);
|
||||||
|
newClient.cleartextkey = newClient.prefix + "." + newUUID;
|
||||||
|
|
||||||
|
return newClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/models/actions/CreateTrack.ts
Normal file
46
src/models/actions/CreateTrack.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator';
|
||||||
|
import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors';
|
||||||
|
import { Track } from '../entities/Track';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new Track entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateTrack {
|
||||||
|
/**
|
||||||
|
* The new track's name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new track's distance in meters (must be greater than 0).
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
distance: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum time a runner should take to run a lap on this track (in seconds).
|
||||||
|
* Will be used for fraud detection.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
minimumLapTime: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Track entity from this.
|
||||||
|
*/
|
||||||
|
public toTrack(): Track {
|
||||||
|
let newTrack: Track = new Track();
|
||||||
|
|
||||||
|
newTrack.name = this.name;
|
||||||
|
newTrack.distance = this.distance;
|
||||||
|
newTrack.minimumLapTime = this.minimumLapTime;
|
||||||
|
if (this.minimumLapTime < 0) {
|
||||||
|
throw new TrackLapTimeCantBeNegativeError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return newTrack;
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/models/actions/CreateUser.ts
Normal file
124
src/models/actions/CreateUser.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import * as argon2 from "argon2";
|
||||||
|
import { IsBoolean, IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
|
||||||
|
import { UserGroupNotFoundError } from '../../errors/UserGroupErrors';
|
||||||
|
import { User } from '../entities/User';
|
||||||
|
import { UserGroup } from '../entities/UserGroup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new User entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateUser {
|
||||||
|
/**
|
||||||
|
* The new user's first name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
firstname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new user's middle name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
middlename?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new user's last name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
lastname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new user's username.
|
||||||
|
* You have to provide at least one of: {email, username}.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new user's email address.
|
||||||
|
* You have to provide at least one of: {email, username}.
|
||||||
|
*/
|
||||||
|
@IsEmail()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new user's phone number.
|
||||||
|
* This will be validated against the configured country phone numer syntax (default: international).
|
||||||
|
*/
|
||||||
|
@IsPhoneNumber(config.phone_validation_countrycode)
|
||||||
|
@IsOptional()
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new user's password.
|
||||||
|
* This will of course not be saved in plaintext :)
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will the new user be enabled from the start?
|
||||||
|
* Default: true
|
||||||
|
*/
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
enabled?: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new user's groups' id(s).
|
||||||
|
* You can provide either one groupId or an array of groupIDs.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
groups?: number[] | number
|
||||||
|
|
||||||
|
//TODO: ProfilePics
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this to a User entity.
|
||||||
|
*/
|
||||||
|
public async toUser(): Promise<User> {
|
||||||
|
let newUser: User = new User();
|
||||||
|
|
||||||
|
if (this.email === undefined && this.username === undefined) {
|
||||||
|
throw new UsernameOrEmailNeededError();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.phone = this.phone
|
||||||
|
newUser.password = await argon2.hash(this.password + newUser.uuid);
|
||||||
|
newUser.groups = await this.getGroups();
|
||||||
|
newUser.enabled = this.enabled;
|
||||||
|
//TODO: ProfilePics
|
||||||
|
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get's all groups for this user by their id's;
|
||||||
|
*/
|
||||||
|
public async getGroups() {
|
||||||
|
if (!this.groups) { return null; }
|
||||||
|
let groups = new Array<UserGroup>();
|
||||||
|
if (!Array.isArray(this.groups)) {
|
||||||
|
this.groups = [this.groups]
|
||||||
|
}
|
||||||
|
for (let group of this.groups) {
|
||||||
|
let found = await getConnectionManager().get().getRepository(UserGroup).findOne({ id: group });
|
||||||
|
if (!found) { throw new UserGroupNotFoundError(); }
|
||||||
|
groups.push(found);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/models/actions/CreateUserGroup.ts
Normal file
33
src/models/actions/CreateUserGroup.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
import { UserGroup } from '../entities/UserGroup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This classed is used to create a new UserGroup entity from a json body (post request).
|
||||||
|
*/
|
||||||
|
export class CreateUserGroup {
|
||||||
|
/**
|
||||||
|
* The new group's name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new group's description.
|
||||||
|
* Optinal.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new UserGroup entity from this.
|
||||||
|
*/
|
||||||
|
public async toUserGroup(): Promise<UserGroup> {
|
||||||
|
let newUserGroup: UserGroup = new UserGroup();
|
||||||
|
|
||||||
|
newUserGroup.name = this.name;
|
||||||
|
newUserGroup.description = this.description;
|
||||||
|
|
||||||
|
return newUserGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/models/actions/HandleLogout.ts
Normal file
49
src/models/actions/HandleLogout.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
import * as jsonwebtoken from 'jsonwebtoken';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserNotFoundError } from '../../errors/AuthError';
|
||||||
|
import { User } from '../entities/User';
|
||||||
|
import { Logout } from '../responses/ResponseLogout';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class handels a user logging out of the system.
|
||||||
|
* Of course it check's the user's provided credential (token) before logging him out.
|
||||||
|
*/
|
||||||
|
export class HandleLogout {
|
||||||
|
/**
|
||||||
|
* A stringyfied jwt access token.
|
||||||
|
* Will get checked for validity.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
token?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the user out.
|
||||||
|
* This gets achived by increasing the user's refresh token count, thereby invalidateing all currently existing jwts for that user.
|
||||||
|
*/
|
||||||
|
public async logout(): Promise<Logout> {
|
||||||
|
let logout: Logout = new Logout();
|
||||||
|
if (!this.token || this.token === undefined) {
|
||||||
|
throw new JwtNotProvidedError()
|
||||||
|
}
|
||||||
|
let decoded;
|
||||||
|
try {
|
||||||
|
decoded = jsonwebtoken.verify(this.token, config.jwt_secret)
|
||||||
|
} catch (error) {
|
||||||
|
throw new IllegalJWTError()
|
||||||
|
}
|
||||||
|
logout.timestamp = Math.floor(Date.now() / 1000)
|
||||||
|
let found_user: User = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["id"] });
|
||||||
|
if (!found_user) {
|
||||||
|
throw new UserNotFoundError()
|
||||||
|
}
|
||||||
|
if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) {
|
||||||
|
throw new RefreshTokenCountInvalidError()
|
||||||
|
}
|
||||||
|
found_user.refreshTokenCount++;
|
||||||
|
await getConnectionManager().get().getRepository(User).update({ id: found_user.id }, found_user)
|
||||||
|
return logout;
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/models/actions/ImportRunner.ts
Normal file
97
src/models/actions/ImportRunner.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { RunnerGroupNeededError } from '../../errors/RunnerErrors';
|
||||||
|
import { RunnerOrganisationNotFoundError } from '../../errors/RunnerOrganisationErrors';
|
||||||
|
import { RunnerGroup } from '../entities/RunnerGroup';
|
||||||
|
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
|
||||||
|
import { RunnerTeam } from '../entities/RunnerTeam';
|
||||||
|
import { CreateRunner } from './CreateRunner';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special class used to import runners from csv files - or json arrays created from csv to be exact.
|
||||||
|
* Why you ask? Because the past has shown us that a non excel/csv based workflow is too much for most schools.
|
||||||
|
*/
|
||||||
|
export class ImportRunner {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new runner's first name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
firstname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new runner's middle name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
middlename?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new runner's last name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
lastname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new runner's team's name (if not provided otherwise).
|
||||||
|
* The team will automaticly get generated if it doesn't exist in this org yet.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
team?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just an alias for team, because this is usually only used for importing data from schools.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
public set class(value: string) {
|
||||||
|
this.team = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a CreateRunner object based on this.
|
||||||
|
* @param groupID Either the id of the new runner's group or the id of the org that the new runner's team is a part of.
|
||||||
|
*/
|
||||||
|
public async toCreateRunner(groupID: number): Promise<CreateRunner> {
|
||||||
|
let newRunner: CreateRunner = new CreateRunner();
|
||||||
|
|
||||||
|
newRunner.firstname = this.firstname;
|
||||||
|
newRunner.middlename = this.middlename;
|
||||||
|
newRunner.lastname = this.lastname;
|
||||||
|
newRunner.group = (await this.getGroup(groupID)).id;
|
||||||
|
|
||||||
|
return newRunner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get's the new runners group.
|
||||||
|
* @param groupID Either the id of the new runner's group or the id of the org that the new runner's team is a part of.
|
||||||
|
*/
|
||||||
|
public async getGroup(groupID: number): Promise<RunnerGroup> {
|
||||||
|
if (this.team === undefined && groupID === undefined) {
|
||||||
|
throw new RunnerGroupNeededError();
|
||||||
|
}
|
||||||
|
|
||||||
|
let team = await getConnectionManager().get().getRepository(RunnerTeam).findOne({ id: groupID });
|
||||||
|
if (team) { return team; }
|
||||||
|
|
||||||
|
let org = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: groupID });
|
||||||
|
if (!org) {
|
||||||
|
throw new RunnerOrganisationNotFoundError();
|
||||||
|
}
|
||||||
|
if (this.team === undefined) { return org; }
|
||||||
|
|
||||||
|
team = await getConnectionManager().get().getRepository(RunnerTeam).findOne({ name: this.team, parentGroup: org });
|
||||||
|
if (!team) {
|
||||||
|
let newRunnerTeam: RunnerTeam = new RunnerTeam();
|
||||||
|
newRunnerTeam.name = this.team;
|
||||||
|
newRunnerTeam.parentGroup = org;
|
||||||
|
team = await getConnectionManager().get().getRepository(RunnerTeam).save(newRunnerTeam);
|
||||||
|
}
|
||||||
|
|
||||||
|
return team;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/models/actions/RefreshAuth.ts
Normal file
56
src/models/actions/RefreshAuth.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
import * as jsonwebtoken from 'jsonwebtoken';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
|
||||||
|
import { JwtCreator } from "../../jwtcreator";
|
||||||
|
import { User } from '../entities/User';
|
||||||
|
import { Auth } from '../responses/ResponseAuth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to create refreshed auth credentials.
|
||||||
|
* To be a little bit more exact: Is takes in a refresh token and creates a new access and refresh token for it's user.
|
||||||
|
* It of course checks for user existance, jwt validity and so on.
|
||||||
|
*/
|
||||||
|
export class RefreshAuth {
|
||||||
|
/**
|
||||||
|
* A stringyfied jwt refresh token.
|
||||||
|
* Will get checked for validity.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
token?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new auth object based on this.
|
||||||
|
*/
|
||||||
|
public async toAuth(): Promise<Auth> {
|
||||||
|
let newAuth: Auth = new Auth();
|
||||||
|
if (!this.token || this.token === undefined) {
|
||||||
|
throw new JwtNotProvidedError()
|
||||||
|
}
|
||||||
|
let decoded
|
||||||
|
try {
|
||||||
|
decoded = jsonwebtoken.verify(this.token, config.jwt_secret)
|
||||||
|
} catch (error) {
|
||||||
|
throw new IllegalJWTError()
|
||||||
|
}
|
||||||
|
const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["id"] }, { relations: ['groups', 'permissions', 'groups.permissions'] });
|
||||||
|
if (!found_user) {
|
||||||
|
throw new UserNotFoundError()
|
||||||
|
}
|
||||||
|
if (found_user.enabled == false) { throw new UserDisabledError(); }
|
||||||
|
if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) {
|
||||||
|
throw new RefreshTokenCountInvalidError()
|
||||||
|
}
|
||||||
|
//Create the auth token
|
||||||
|
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 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
|
||||||
|
newAuth.refresh_token = JwtCreator.createRefresh(found_user, timestamp_refresh_expiry);
|
||||||
|
newAuth.refresh_token_expires_at = timestamp_refresh_expiry;
|
||||||
|
return newAuth;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/models/actions/ResetPassword.ts
Normal file
57
src/models/actions/ResetPassword.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import * as argon2 from "argon2";
|
||||||
|
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
import * as jsonwebtoken from 'jsonwebtoken';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { IllegalJWTError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UserNotFoundError } from '../../errors/AuthError';
|
||||||
|
import { User } from '../entities/User';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class can be used to reset a user's password.
|
||||||
|
* To set a new password the user needs to provide a valid password reset token.
|
||||||
|
*/
|
||||||
|
export class ResetPassword {
|
||||||
|
/**
|
||||||
|
* The reset token on which the password reset will be based.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
resetToken?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's new password
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a password reset token based on this.
|
||||||
|
*/
|
||||||
|
public async resetPassword(): Promise<any> {
|
||||||
|
if (!this.resetToken || this.resetToken === undefined) {
|
||||||
|
throw new JwtNotProvidedError()
|
||||||
|
}
|
||||||
|
if (!this.password || this.password === undefined) {
|
||||||
|
throw new PasswordNeededError()
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded;
|
||||||
|
try {
|
||||||
|
decoded = jsonwebtoken.verify(this.resetToken, config.jwt_secret)
|
||||||
|
} catch (error) {
|
||||||
|
throw new IllegalJWTError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["id"] });
|
||||||
|
if (!found_user) { throw new UserNotFoundError(); }
|
||||||
|
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);
|
||||||
|
await getConnectionManager().get().getRepository(User).save(found_user);
|
||||||
|
|
||||||
|
return "password reset successfull";
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/models/actions/UpdateDonor.ts
Normal file
44
src/models/actions/UpdateDonor.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
|
||||||
|
import { DonorReceiptAddressNeededError } from '../../errors/DonorErrors';
|
||||||
|
import { Donor } from '../entities/Donor';
|
||||||
|
import { CreateParticipant } from './CreateParticipant';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to update a Donor entity (via put request).
|
||||||
|
*/
|
||||||
|
export class UpdateDonor extends CreateParticipant {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated donor'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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the updated donor need a receipt?
|
||||||
|
*/
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
receiptNeeded?: boolean;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a provided Donor entity based on this.
|
||||||
|
*/
|
||||||
|
public async updateDonor(donor: Donor): Promise<Donor> {
|
||||||
|
donor.firstname = this.firstname;
|
||||||
|
donor.middlename = this.middlename;
|
||||||
|
donor.lastname = this.lastname;
|
||||||
|
donor.phone = this.phone;
|
||||||
|
donor.email = this.email;
|
||||||
|
donor.receiptNeeded = this.receiptNeeded;
|
||||||
|
donor.address = await this.getAddress();
|
||||||
|
|
||||||
|
if (this.receiptNeeded == true && this.address == null) {
|
||||||
|
throw new DonorReceiptAddressNeededError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return donor;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/models/actions/UpdatePermission.ts
Normal file
68
src/models/actions/UpdatePermission.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { IsInt, IsNotEmpty, IsObject } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { PermissionNeedsPrincipalError } from '../../errors/PermissionErrors';
|
||||||
|
import { PrincipalNotFoundError, PrincipalWrongTypeError } from '../../errors/PrincipalErrors';
|
||||||
|
import { Permission } from '../entities/Permission';
|
||||||
|
import { Principal } from '../entities/Principal';
|
||||||
|
import { PermissionAction } from '../enums/PermissionAction';
|
||||||
|
import { PermissionTarget } from '../enums/PermissionTargets';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to update a Permission entity (via put request).
|
||||||
|
*/
|
||||||
|
export class UpdatePermission {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated permission'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 permissions's principal.
|
||||||
|
* Just has to contain the principal's id -everything else won't be checked or changed.
|
||||||
|
*/
|
||||||
|
@IsObject()
|
||||||
|
@IsNotEmpty()
|
||||||
|
principal: Principal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The permissions's target.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
target: PermissionTarget;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The permissions's action.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
action: PermissionAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a provided Permission entity based on this.
|
||||||
|
*/
|
||||||
|
public async updatePermission(permission: Permission): Promise<Permission> {
|
||||||
|
permission.principal = await this.getPrincipal();
|
||||||
|
permission.target = this.target;
|
||||||
|
permission.action = this.action;
|
||||||
|
|
||||||
|
return permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the updated permission's principal based on it's id.
|
||||||
|
*/
|
||||||
|
public async getPrincipal(): Promise<Principal> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/models/actions/UpdateRunner.ts
Normal file
59
src/models/actions/UpdateRunner.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { IsInt, IsObject } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { RunnerGroupNotFoundError } from '../../errors/RunnerGroupErrors';
|
||||||
|
import { RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
|
||||||
|
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
|
||||||
|
import { Runner } from '../entities/Runner';
|
||||||
|
import { RunnerGroup } from '../entities/RunnerGroup';
|
||||||
|
import { CreateParticipant } from './CreateParticipant';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to update a Runner entity (via put request).
|
||||||
|
*/
|
||||||
|
export class UpdateRunner extends CreateParticipant {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated runner'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 runner's new team/org.
|
||||||
|
* Just has to contain the group's id -everything else won't be checked or changed.
|
||||||
|
*/
|
||||||
|
@IsObject()
|
||||||
|
group: RunnerGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a provided Runner entity based on this.
|
||||||
|
*/
|
||||||
|
public async updateRunner(runner: Runner): Promise<Runner> {
|
||||||
|
runner.firstname = this.firstname;
|
||||||
|
runner.middlename = this.middlename;
|
||||||
|
runner.lastname = this.lastname;
|
||||||
|
runner.phone = this.phone;
|
||||||
|
runner.email = this.email;
|
||||||
|
runner.group = await this.getGroup();
|
||||||
|
runner.address = await this.getAddress();
|
||||||
|
|
||||||
|
return runner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the updated runner's group based on it's id.
|
||||||
|
*/
|
||||||
|
public async getGroup(): Promise<RunnerGroup> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/models/actions/UpdateRunnerOrganisation.ts
Normal file
52
src/models/actions/UpdateRunnerOrganisation.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { IsInt, IsOptional } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { AddressNotFoundError } from '../../errors/AddressErrors';
|
||||||
|
import { Address } from '../entities/Address';
|
||||||
|
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
|
||||||
|
import { CreateRunnerGroup } from './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 updateRunnerOrganisation(organisation: RunnerOrganisation): Promise<RunnerOrganisation> {
|
||||||
|
|
||||||
|
organisation.name = this.name;
|
||||||
|
organisation.contact = await this.getContact();
|
||||||
|
// organisation.address = await this.getAddress();
|
||||||
|
|
||||||
|
return organisation;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/models/actions/UpdateRunnerTeam.ts
Normal file
56
src/models/actions/UpdateRunnerTeam.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { IsInt, IsNotEmpty, IsObject } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
|
||||||
|
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
|
||||||
|
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
|
||||||
|
import { RunnerTeam } from '../entities/RunnerTeam';
|
||||||
|
import { CreateRunnerGroup } from './CreateRunnerGroup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to update a RunnerTeam entity (via put request).
|
||||||
|
*/
|
||||||
|
export class UpdateRunnerTeam extends CreateRunnerGroup {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated team'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 team's parentGroup.
|
||||||
|
* Just has to contain the organisation's id - everything else won't be checked or changed.
|
||||||
|
*/
|
||||||
|
@IsObject()
|
||||||
|
@IsNotEmpty()
|
||||||
|
parentGroup: RunnerOrganisation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the updated teams's parentGroup based on it's id.
|
||||||
|
*/
|
||||||
|
public async getParent(): Promise<RunnerOrganisation> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a provided RunnerTeam entity based on this.
|
||||||
|
*/
|
||||||
|
public async updateRunnerTeam(team: RunnerTeam): Promise<RunnerTeam> {
|
||||||
|
|
||||||
|
team.name = this.name;
|
||||||
|
team.parentGroup = await this.getParent();
|
||||||
|
team.contact = await this.getContact()
|
||||||
|
|
||||||
|
return team;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/models/actions/UpdateTrack.ts
Normal file
50
src/models/actions/UpdateTrack.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator';
|
||||||
|
import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors';
|
||||||
|
import { Track } from '../entities/Track';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to update a Track entity (via put request).
|
||||||
|
*/
|
||||||
|
export class UpdateTrack {
|
||||||
|
/**
|
||||||
|
* The updated track'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;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated track's distance in meters (must be greater than 0).
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
distance: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum time a runner should take to run a lap on this track (in seconds).
|
||||||
|
* Will be used for fraud detection.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
minimumLapTime: number;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a Track entity based on this.
|
||||||
|
* @param track The track that shall be updated.
|
||||||
|
*/
|
||||||
|
public updateTrack(track: Track): Track {
|
||||||
|
track.name = this.name;
|
||||||
|
track.distance = this.distance;
|
||||||
|
track.minimumLapTime = this.minimumLapTime;
|
||||||
|
if (this.minimumLapTime < 0) {
|
||||||
|
throw new TrackLapTimeCantBeNegativeError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
}
|
||||||
130
src/models/actions/UpdateUser.ts
Normal file
130
src/models/actions/UpdateUser.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import * as argon2 from "argon2";
|
||||||
|
import { IsBoolean, IsEmail, IsInt, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { UsernameOrEmailNeededError } from '../../errors/AuthError';
|
||||||
|
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).
|
||||||
|
*/
|
||||||
|
export class UpdateUser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated user'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 user's first name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
firstname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated user's middle name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
middlename?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated user's last name.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
lastname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated user's username.
|
||||||
|
* You have to provide at least one of: {email, username}.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated user's email address.
|
||||||
|
* You have to provide at least one of: {email, username}.
|
||||||
|
*/
|
||||||
|
@IsEmail()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated user's phone number.
|
||||||
|
* This will be validated against the configured country phone numer syntax (default: international).
|
||||||
|
*/
|
||||||
|
@IsPhoneNumber(config.phone_validation_countrycode)
|
||||||
|
@IsOptional()
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The new updated's password.
|
||||||
|
* Only provide it if you want it updated.
|
||||||
|
* Changeing the password will invalidate all of the user's jwts.
|
||||||
|
* This will of course not be saved in plaintext :)
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
password?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the user be enabled?
|
||||||
|
*/
|
||||||
|
@IsBoolean()
|
||||||
|
enabled: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated user's groups.
|
||||||
|
* This just has to contain the group's id - everything else won't be changed.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
groups?: UserGroup[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a provided User entity based on this.
|
||||||
|
*/
|
||||||
|
public async updateUser(user: User): Promise<User> {
|
||||||
|
user.email = this.email;
|
||||||
|
user.username = this.username;
|
||||||
|
if ((user.email === undefined || user.email === null) && (user.username === undefined || user.username === null)) {
|
||||||
|
throw new UsernameOrEmailNeededError();
|
||||||
|
}
|
||||||
|
if (this.password) {
|
||||||
|
user.password = await argon2.hash(this.password + user.uuid);
|
||||||
|
user.refreshTokenCount = user.refreshTokenCount + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.enabled = this.enabled;
|
||||||
|
user.firstname = this.firstname
|
||||||
|
user.middlename = this.middlename
|
||||||
|
user.lastname = this.lastname
|
||||||
|
user.phone = this.phone;
|
||||||
|
user.groups = await this.getGroups();
|
||||||
|
//TODO: ProfilePics
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the updated user's groups based on their ids.
|
||||||
|
*/
|
||||||
|
public async getGroups() {
|
||||||
|
if (!this.groups) { return null; }
|
||||||
|
let groups = new Array<UserGroup>();
|
||||||
|
if (!Array.isArray(this.groups)) {
|
||||||
|
this.groups = [this.groups]
|
||||||
|
}
|
||||||
|
for (let group of this.groups) {
|
||||||
|
let found = await getConnectionManager().get().getRepository(UserGroup).findOne({ id: group.id });
|
||||||
|
if (!found) { throw new UserGroupNotFoundError(); }
|
||||||
|
groups.push(found);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/models/entities/Address.ts
Normal file
83
src/models/entities/Address.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsPostalCode,
|
||||||
|
IsString
|
||||||
|
} from "class-validator";
|
||||||
|
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { IAddressUser } from './IAddressUser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the Address entity.
|
||||||
|
* Implemented this way to prevent any formatting differences.
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
export class Address {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The address's description.
|
||||||
|
* Optional and mostly for UX.
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The address's first line.
|
||||||
|
* Containing the street and house number.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
address1: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The address's second line.
|
||||||
|
* Containing optional information.
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
address2?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The address's postal code.
|
||||||
|
* This will get checked against the postal code syntax for the configured country.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsPostalCode(config.postalcode_validation_countrycode)
|
||||||
|
postalcode: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The address's city.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
city: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The address's country.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
country: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to link the address to participants.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => IAddressUser, addressUser => addressUser.address, { nullable: true })
|
||||||
|
addressUsers: IAddressUser[];
|
||||||
|
}
|
||||||
42
src/models/entities/DistanceDonation.ts
Normal file
42
src/models/entities/DistanceDonation.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { IsInt, IsNotEmpty, IsPositive } from "class-validator";
|
||||||
|
import { ChildEntity, Column, ManyToOne } from "typeorm";
|
||||||
|
import { Donation } from "./Donation";
|
||||||
|
import { Runner } from "./Runner";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the DistanceDonation entity.
|
||||||
|
* For distanceDonations a donor pledges to donate a certain amount for each kilometer ran by a runner.
|
||||||
|
*/
|
||||||
|
@ChildEntity()
|
||||||
|
export class DistanceDonation extends Donation {
|
||||||
|
/**
|
||||||
|
* The donation's associated runner.
|
||||||
|
* Used as the source of the donation's distance.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ManyToOne(() => Runner, runner => runner.distanceDonations)
|
||||||
|
runner: Runner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The donation's amount donated per distance.
|
||||||
|
* The amount the donor set to be donated per kilometer that the runner ran.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
amountPerDistance: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The donation's amount in cents (or whatever your currency's smallest unit is.).
|
||||||
|
* Get's calculated from the runner's distance ran and the amount donated per kilometer.
|
||||||
|
*/
|
||||||
|
public get amount(): number {
|
||||||
|
let calculatedAmount = -1;
|
||||||
|
try {
|
||||||
|
calculatedAmount = this.amountPerDistance * (this.runner.distance / 1000);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return calculatedAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/models/entities/Donation.ts
Normal file
35
src/models/entities/Donation.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty
|
||||||
|
} from "class-validator";
|
||||||
|
import { Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||||
|
import { Donor } from './Donor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the Donation entity.
|
||||||
|
* A donation just associates a donor with a donation amount.
|
||||||
|
* The specifics of the amoun's determination has to be implemented in child classes.
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
@TableInheritance({ column: { name: "type", type: "varchar" } })
|
||||||
|
export abstract class Donation {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The donations's donor.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ManyToOne(() => Donor, donor => donor.donations)
|
||||||
|
donor: Donor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The donation's amount in cents (or whatever your currency's smallest unit is.).
|
||||||
|
* The exact implementation may differ for each type of donation.
|
||||||
|
*/
|
||||||
|
abstract amount: number;
|
||||||
|
}
|
||||||
25
src/models/entities/Donor.ts
Normal file
25
src/models/entities/Donor.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { IsBoolean } from "class-validator";
|
||||||
|
import { ChildEntity, Column, OneToMany } from "typeorm";
|
||||||
|
import { Donation } from './Donation';
|
||||||
|
import { Participant } from "./Participant";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the Donor entity.
|
||||||
|
*/
|
||||||
|
@ChildEntity()
|
||||||
|
export class Donor extends Participant {
|
||||||
|
/**
|
||||||
|
* Does this donor need a receipt?
|
||||||
|
* Will later be used to automaticly generate donation receipts.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsBoolean()
|
||||||
|
receiptNeeded: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to link the participant as the donor of a donation.
|
||||||
|
* Attention: Only runner's can be associated as a distanceDonations distance source.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => Donation, donation => donation.donor, { nullable: true })
|
||||||
|
donations: Donation[];
|
||||||
|
}
|
||||||
19
src/models/entities/FixedDonation.ts
Normal file
19
src/models/entities/FixedDonation.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { IsInt, IsPositive } from "class-validator";
|
||||||
|
import { ChildEntity, Column } from "typeorm";
|
||||||
|
import { Donation } from "./Donation";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the FixedDonation entity.
|
||||||
|
* In the past there was no easy way to track fixed donations (eg. for creating donation receipts).
|
||||||
|
*/
|
||||||
|
@ChildEntity()
|
||||||
|
export class FixedDonation extends Donation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The donation's amount in cents (or whatever your currency's smallest unit is.).
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
84
src/models/entities/GroupContact.ts
Normal file
84
src/models/entities/GroupContact.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsPhoneNumber,
|
||||||
|
|
||||||
|
IsString
|
||||||
|
} from "class-validator";
|
||||||
|
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { Address } from "./Address";
|
||||||
|
import { IAddressUser } from './IAddressUser';
|
||||||
|
import { RunnerGroup } from "./RunnerGroup";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the GroupContact entity.
|
||||||
|
* Mainly it's own class to reduce duplicate code and enable contact's to be associated with multiple groups.
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
export class GroupContact implements IAddressUser {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contact's first name.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
firstname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contact's middle name.
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
middlename?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contact's last name.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
lastname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contact's address.
|
||||||
|
* This is a address object to prevent any formatting differences.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
|
||||||
|
address?: Address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contact's phone number.
|
||||||
|
* This will be validated against the configured country phone numer syntax (default: international).
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsPhoneNumber(config.phone_validation_countrycode)
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contact's email address.
|
||||||
|
* Could later be used to automaticly send mails concerning the contact's associated groups.
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to link contacts to groups.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => RunnerGroup, group => group.contact, { nullable: true })
|
||||||
|
groups: RunnerGroup[];
|
||||||
|
}
|
||||||
15
src/models/entities/IAddressUser.ts
Normal file
15
src/models/entities/IAddressUser.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||||
|
import { Address } from './Address';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interface(tm) all entities using addresses have to implement.
|
||||||
|
* This is a abstract class, because apparently typeorm can't really work with interfaces :/
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
export abstract class IAddressUser {
|
||||||
|
@PrimaryColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
|
||||||
|
address?: Address
|
||||||
|
}
|
||||||
77
src/models/entities/Participant.ts
Normal file
77
src/models/entities/Participant.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsPhoneNumber,
|
||||||
|
|
||||||
|
IsString
|
||||||
|
} from "class-validator";
|
||||||
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { Address } from "./Address";
|
||||||
|
import { IAddressUser } from './IAddressUser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the Participant entity.
|
||||||
|
* Participans can donate and therefor be associated with donation entities.
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
@TableInheritance({ column: { name: "type", type: "varchar" } })
|
||||||
|
export abstract class Participant implements IAddressUser {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participant's first name.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
firstname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participant's middle name.
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
middlename?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participant's last name.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
lastname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participant's address.
|
||||||
|
* This is a address object to prevent any formatting differences.
|
||||||
|
*/
|
||||||
|
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
|
||||||
|
address?: Address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participant's phone number.
|
||||||
|
* This will be validated against the configured country phone numer syntax (default: international).
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsPhoneNumber(config.phone_validation_countrycode)
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participant's email address.
|
||||||
|
* Can be used to contact the participant.
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
54
src/models/entities/Permission.ts
Normal file
54
src/models/entities/Permission.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty
|
||||||
|
} from "class-validator";
|
||||||
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
import { PermissionAction } from '../enums/PermissionAction';
|
||||||
|
import { PermissionTarget } from '../enums/PermissionTargets';
|
||||||
|
import { Principal } from './Principal';
|
||||||
|
/**
|
||||||
|
* Defines the Permission entity.
|
||||||
|
* Permissions can be granted to principals.
|
||||||
|
* The permissions possible targets and actions are defined in enums.
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
export class Permission {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The permission's principal.
|
||||||
|
*/
|
||||||
|
@ManyToOne(() => Principal, principal => principal.permissions)
|
||||||
|
principal: Principal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The permission's target.
|
||||||
|
* This get's stored as the enum value's string representation for compatability reasons.
|
||||||
|
*/
|
||||||
|
@Column({ type: 'varchar' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsEnum(PermissionTarget)
|
||||||
|
target: PermissionTarget;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The permission's action.
|
||||||
|
* This get's stored as the enum value's string representation for compatability reasons.
|
||||||
|
*/
|
||||||
|
@Column({ type: 'varchar' })
|
||||||
|
@IsEnum(PermissionAction)
|
||||||
|
action: PermissionAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn this into a string for exporting and jwts.
|
||||||
|
* Mainly used to shrink the size of jwts (otherwise the would contain entire objects).
|
||||||
|
*/
|
||||||
|
public toString(): string {
|
||||||
|
return this.target + ":" + this.action;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/models/entities/Principal.ts
Normal file
30
src/models/entities/Principal.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { IsInt } from 'class-validator';
|
||||||
|
import { Entity, OneToMany, PrimaryGeneratedColumn, TableInheritance } from 'typeorm';
|
||||||
|
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
|
||||||
|
import { Permission } from './Permission';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the principal entity.
|
||||||
|
* A principal basicly is any entity that can receive permissions for the api (users and their groups).
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
@TableInheritance({ column: { name: "type", type: "varchar" } })
|
||||||
|
export abstract class Principal {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participant's permissions.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => Permission, permission => permission.principal, { nullable: true })
|
||||||
|
permissions: Permission[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns this entity into it's response class.
|
||||||
|
*/
|
||||||
|
public abstract toResponse(): ResponsePrincipal;
|
||||||
|
}
|
||||||
69
src/models/entities/Runner.ts
Normal file
69
src/models/entities/Runner.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { IsInt, IsNotEmpty } from "class-validator";
|
||||||
|
import { ChildEntity, ManyToOne, OneToMany } from "typeorm";
|
||||||
|
import { DistanceDonation } from "./DistanceDonation";
|
||||||
|
import { Participant } from "./Participant";
|
||||||
|
import { RunnerCard } from "./RunnerCard";
|
||||||
|
import { RunnerGroup } from "./RunnerGroup";
|
||||||
|
import { Scan } from "./Scan";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the runner entity.
|
||||||
|
* Runners differ from participants in being able to actually accumulate a ran distance through scans.
|
||||||
|
* Runner's get organized in groups.
|
||||||
|
*/
|
||||||
|
@ChildEntity()
|
||||||
|
export class Runner extends Participant {
|
||||||
|
/**
|
||||||
|
* The runner's associated group.
|
||||||
|
* Can be a runner team or organisation.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ManyToOne(() => RunnerGroup, group => group.runners)
|
||||||
|
group: RunnerGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The runner's associated distanceDonations.
|
||||||
|
* Used to link runners to distanceDonations in order to calculate the donation's amount based on the distance the runner ran.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => DistanceDonation, distanceDonation => distanceDonation.runner, { nullable: true })
|
||||||
|
distanceDonations: DistanceDonation[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The runner's associated cards.
|
||||||
|
* Used to link runners to cards - yes a runner be associated with multiple cards this came in handy in the past.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => RunnerCard, card => card.runner, { nullable: true })
|
||||||
|
cards: RunnerCard[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The runner's associated scans.
|
||||||
|
* Used to link runners to scans (valid and fraudulant).
|
||||||
|
*/
|
||||||
|
@OneToMany(() => Scan, scan => scan.runner, { nullable: true })
|
||||||
|
scans: Scan[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all valid scans associated with this runner.
|
||||||
|
* This is implemented here to avoid duplicate code in other files.
|
||||||
|
*/
|
||||||
|
public get validScans(): Scan[] {
|
||||||
|
return this.scans.filter(scan => { scan.valid === true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total distance ran by this runner based on all his valid scans.
|
||||||
|
* This is implemented here to avoid duplicate code in other files.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
public get distance(): number {
|
||||||
|
return this.validScans.reduce((sum, current) => sum + current.distance, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total donations a runner has collected based on his linked donations and distance ran.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
public get distanceDonationAmount(): number {
|
||||||
|
return this.distanceDonations.reduce((sum, current) => sum + current.amountPerDistance, 0) * this.distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/models/entities/RunnerCard.ts
Normal file
60
src/models/entities/RunnerCard.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsEAN,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString
|
||||||
|
} from "class-validator";
|
||||||
|
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
import { Runner } from "./Runner";
|
||||||
|
import { TrackScan } from "./TrackScan";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the RunnerCard entity.
|
||||||
|
* A runnerCard is a physical representation for a runner.
|
||||||
|
* It can be associated with a runner to create scans via the scan station's.
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
export class RunnerCard {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The card's currently associated runner.
|
||||||
|
* To increase reusability a card can be reassigned.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@ManyToOne(() => Runner, runner => runner.cards, { nullable: true })
|
||||||
|
runner: Runner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The card's code.
|
||||||
|
* This has to be able to being converted to something barcode compatible.
|
||||||
|
* Will get automaticlly generated (not implemented yet).
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsEAN()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the card enabled (for fraud reasons)?
|
||||||
|
* Default: true
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsBoolean()
|
||||||
|
enabled: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The card's associated scans.
|
||||||
|
* Used to link cards to track scans.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
||||||
|
scans: TrackScan[];
|
||||||
|
}
|
||||||
63
src/models/entities/RunnerGroup.ts
Normal file
63
src/models/entities/RunnerGroup.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString
|
||||||
|
} from "class-validator";
|
||||||
|
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||||
|
import { GroupContact } from "./GroupContact";
|
||||||
|
import { Runner } from "./Runner";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the RunnerGroup entity.
|
||||||
|
* This is used to group runners together (as the name suggests).
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
@TableInheritance({ column: { name: "type", type: "varchar" } })
|
||||||
|
export abstract class RunnerGroup {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The group's name.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The group's contact.
|
||||||
|
* This is mostly a feature for the group managers and public relations.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@ManyToOne(() => GroupContact, contact => contact.groups, { nullable: true })
|
||||||
|
contact?: GroupContact;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The group's associated runners.
|
||||||
|
* Used to link runners to a runner group.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => Runner, runner => runner.group, { nullable: true })
|
||||||
|
runners: Runner[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total distance ran by this group's runners based on all their valid scans.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
public get distance(): number {
|
||||||
|
return this.runners.reduce((sum, current) => sum + current.distance, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total donations a runner has collected based on his linked donations and distance ran.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
public get distanceDonationAmount(): number {
|
||||||
|
return this.runners.reduce((sum, current) => sum + current.distanceDonationAmount, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/models/entities/RunnerOrganisation.ts
Normal file
57
src/models/entities/RunnerOrganisation.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { IsInt, IsOptional } from "class-validator";
|
||||||
|
import { ChildEntity, ManyToOne, OneToMany } from "typeorm";
|
||||||
|
import { Address } from './Address';
|
||||||
|
import { IAddressUser } from './IAddressUser';
|
||||||
|
import { Runner } from './Runner';
|
||||||
|
import { RunnerGroup } from "./RunnerGroup";
|
||||||
|
import { RunnerTeam } from "./RunnerTeam";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the RunnerOrganisation entity.
|
||||||
|
* This usually is a school, club or company.
|
||||||
|
*/
|
||||||
|
@ChildEntity()
|
||||||
|
export class RunnerOrganisation extends RunnerGroup implements IAddressUser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The organisations's address.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
|
||||||
|
address?: Address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The organisation's teams.
|
||||||
|
* Used to link teams to a organisation.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => RunnerTeam, team => team.parentGroup, { nullable: true })
|
||||||
|
teams: RunnerTeam[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all runners associated with this organisation (directly or indirectly via teams).
|
||||||
|
*/
|
||||||
|
public get allRunners(): Runner[] {
|
||||||
|
let returnRunners: Runner[] = new Array<Runner>();
|
||||||
|
returnRunners.push(...this.runners);
|
||||||
|
for (let team of this.teams) {
|
||||||
|
returnRunners.push(...team.runners)
|
||||||
|
}
|
||||||
|
return returnRunners;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total distance ran by this group's runners based on all their valid scans.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
public get distance(): number {
|
||||||
|
return this.allRunners.reduce((sum, current) => sum + current.distance, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total donations a runner has collected based on his linked donations and distance ran.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
public get distanceDonationAmount(): number {
|
||||||
|
return this.allRunners.reduce((sum, current) => sum + current.distanceDonationAmount, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/models/entities/RunnerTeam.ts
Normal file
20
src/models/entities/RunnerTeam.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
import { ChildEntity, ManyToOne } from "typeorm";
|
||||||
|
import { RunnerGroup } from "./RunnerGroup";
|
||||||
|
import { RunnerOrganisation } from "./RunnerOrganisation";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the RunnerTeam entity.
|
||||||
|
* This usually is a school class or department in a company.
|
||||||
|
*/
|
||||||
|
@ChildEntity()
|
||||||
|
export class RunnerTeam extends RunnerGroup {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The team's parent group.
|
||||||
|
* Every team has to be part of a runnerOrganisation - this get's checked on creation and update.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ManyToOne(() => RunnerOrganisation, org => org.teams, { nullable: true })
|
||||||
|
parentGroup?: RunnerOrganisation;
|
||||||
|
}
|
||||||
49
src/models/entities/Scan.ts
Normal file
49
src/models/entities/Scan.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
|
||||||
|
IsPositive
|
||||||
|
} from "class-validator";
|
||||||
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
|
||||||
|
import { Runner } from "./Runner";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the Scan entity.
|
||||||
|
* A scan basicly adds a certain distance to a runner's total ran distance.
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
@TableInheritance({ column: { name: "type", type: "varchar" } })
|
||||||
|
export abstract class Scan {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scan's associated runner.
|
||||||
|
* This is important to link ran distances to runners.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ManyToOne(() => Runner, runner => runner.scans, { nullable: false })
|
||||||
|
runner: Runner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scan's distance in meters.
|
||||||
|
* Can be set manually or derived from another object.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
abstract distance: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the scan valid (for fraud reasons).
|
||||||
|
* The determination of validity will work differently for every child class.
|
||||||
|
* Default: true
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsBoolean()
|
||||||
|
valid: boolean = true;
|
||||||
|
}
|
||||||
64
src/models/entities/ScanStation.ts
Normal file
64
src/models/entities/ScanStation.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString
|
||||||
|
} from "class-validator";
|
||||||
|
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
import { Track } from "./Track";
|
||||||
|
import { TrackScan } from "./TrackScan";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the ScanStation entity.
|
||||||
|
* ScanStations get used to create TrackScans for runners based on a scan of their runnerCard.
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
export class ScanStation {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The station's description.
|
||||||
|
* Mostly for better UX when traceing back stuff.
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The track this station is associated with.
|
||||||
|
* All scans created by this station will also be associated with this track.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ManyToOne(() => Track, track => track.stations, { nullable: false })
|
||||||
|
track: Track;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The station's api key.
|
||||||
|
* This is used to authorize a station against the api (not implemented yet).
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the station enabled (for fraud and setup reasons)?
|
||||||
|
* Default: true
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsBoolean()
|
||||||
|
enabled: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to link track scans to a scan station.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
||||||
|
scans: TrackScan[];
|
||||||
|
}
|
||||||
48
src/models/entities/StatsClient.ts
Normal file
48
src/models/entities/StatsClient.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { IsInt, IsOptional, IsString } from "class-validator";
|
||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
/**
|
||||||
|
* Defines the StatsClient entity.
|
||||||
|
* StatsClients can be used to access the protected parts of the stats api (top runners, donators and so on).
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
export class StatsClient {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The clients's description.
|
||||||
|
* Mostly for better UX when traceing back stuff.
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client's api key prefix.
|
||||||
|
* This is used identitfy a client by it's api key.
|
||||||
|
*/
|
||||||
|
@Column({ unique: true })
|
||||||
|
@IsString()
|
||||||
|
prefix: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client's api key hash.
|
||||||
|
* The api key can be used to authenticate against the /stats/** routes.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsString()
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client's api key in plain text.
|
||||||
|
* This will only be used to display the full key on creation and updates.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
cleartextkey?: string;
|
||||||
|
}
|
||||||
64
src/models/entities/Track.ts
Normal file
64
src/models/entities/Track.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsPositive,
|
||||||
|
IsString
|
||||||
|
} from "class-validator";
|
||||||
|
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
import { ScanStation } from "./ScanStation";
|
||||||
|
import { TrackScan } from "./TrackScan";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the Track entity.
|
||||||
|
*/
|
||||||
|
@Entity()
|
||||||
|
export class Track {
|
||||||
|
/**
|
||||||
|
* Autogenerated unique id (primary key).
|
||||||
|
*/
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
@IsInt()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The track's name.
|
||||||
|
* Mainly here for UX.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The track's length/distance in meters.
|
||||||
|
* Will be used to calculate runner's ran distances.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
distance: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum time a runner should take to run a lap on this track (in seconds).
|
||||||
|
* Will be used for fraud detection.
|
||||||
|
*/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
minimumLapTime?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to link scan stations to a certain track.
|
||||||
|
* This makes the configuration of the scan stations easier.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => ScanStation, station => station.track, { nullable: true })
|
||||||
|
stations: ScanStation[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to link track scans to a track.
|
||||||
|
* The scan will derive it's distance from the track's distance.
|
||||||
|
*/
|
||||||
|
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
|
||||||
|
scans: TrackScan[];
|
||||||
|
}
|
||||||
62
src/models/entities/TrackScan.ts
Normal file
62
src/models/entities/TrackScan.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
IsDateString,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
|
||||||
|
IsPositive
|
||||||
|
} from "class-validator";
|
||||||
|
import { ChildEntity, Column, ManyToOne } from "typeorm";
|
||||||
|
import { RunnerCard } from "./RunnerCard";
|
||||||
|
import { Scan } from "./Scan";
|
||||||
|
import { ScanStation } from "./ScanStation";
|
||||||
|
import { Track } from "./Track";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the TrackScan entity.
|
||||||
|
* A track scan usaually get's generated by a scan station.
|
||||||
|
*/
|
||||||
|
@ChildEntity()
|
||||||
|
export class TrackScan extends Scan {
|
||||||
|
/**
|
||||||
|
* The scan's associated track.
|
||||||
|
* This is used to determine the scan's distance.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ManyToOne(() => Track, track => track.scans, { nullable: true })
|
||||||
|
track: Track;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The runnerCard associated with the scan.
|
||||||
|
* This get's saved for documentation and management purposes.
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ManyToOne(() => RunnerCard, card => card.scans, { nullable: true })
|
||||||
|
card: RunnerCard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scanning station that created the scan.
|
||||||
|
* Mainly used for logging and traceing back scans (or errors)
|
||||||
|
*/
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ManyToOne(() => ScanStation, station => station.scans, { nullable: true })
|
||||||
|
station: ScanStation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scan's distance in meters.
|
||||||
|
* This just get's loaded from it's track.
|
||||||
|
*/
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
public get distance(): number {
|
||||||
|
return this.track.distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scan's creation timestamp.
|
||||||
|
* Will be used to implement fraud detection.
|
||||||
|
*/
|
||||||
|
@Column()
|
||||||
|
@IsDateString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user