Compare commits
615 Commits
ed53627bbe
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| dd3d93edc7 | |||
| da9a359251 | |||
| 0661729e5f | |||
| ddafd90d3e | |||
| 8960aa5545 | |||
| a0c2b5ade8 | |||
| a1acd3519f | |||
| c3d008ec0f | |||
| 8ae53f1c49 | |||
| 179c2a5157 | |||
| dd7e5dae36 | |||
| e165f01930 | |||
| 940d62cde4 | |||
| b002cf2df1 | |||
| 56c73c2555 | |||
| 28fb9834e1 | |||
| 6b4b16c13b | |||
| d743f7ee12 | |||
| a4e8311cbd | |||
| c172aa8bf8 | |||
| d1926fe372 | |||
| 2b658ac381 | |||
| 321d291b4b | |||
| 2eb26e4e38 | |||
| 3b06d1a6ef | |||
| de824375d3 | |||
| 11af9c02d9 | |||
| 09e429fc67 | |||
| 703b4f89a6 | |||
| 32e054eb84 | |||
| 5e368552ea | |||
| 0379786cbd | |||
| a9a5eb6735 | |||
| ab70f7e498 | |||
| 1407fe36f3 | |||
| d12801e34d | |||
| 3e7190e279 | |||
| 41423feffe | |||
| 30b585c0c1 | |||
| a3c93f0d39 | |||
| f53894b16a | |||
| 7533c349ef | |||
| 91569ced40 | |||
| f9ae778b21 | |||
| 427dfaafab | |||
| ae589aeb54 | |||
| 1b9d2969eb | |||
| daffbcde72 | |||
| 9445c6f21e | |||
| 6febb99499 | |||
| 6e6979cfe3 | |||
| 230cdb0e37 | |||
| ce450e9b6d | |||
| de36a24191 | |||
| b167ba07f7 | |||
| 4d40225a44 | |||
| 57b9c2babc | |||
| 9dc9ce37d8 | |||
| f245840cde | |||
| 4824547dde | |||
| 8dbee32eee | |||
| ae7c5ff0c3 | |||
| 2a465f88c5 | |||
| 58ae9b589a | |||
| 8bc01d3f24 | |||
| d0df5dd641 | |||
| 2cd15d25e9 | |||
| dafac06bc8 | |||
| e2651728c5 | |||
| 673dea2e57 | |||
| 7fbe649dc9 | |||
| 3766899c83 | |||
| a6c7d54fe7 | |||
| 79bc04bec1 | |||
| f9834b5f4d | |||
| fc7b8f4c16 | |||
| 4f6e81677c | |||
| 6b7ecd3044 | |||
| 8ef5f90abd | |||
| a334adffc6 | |||
| f1db883609 | |||
| e586a11e2a | |||
| 50b893f537 | |||
| 02efb9a8e5 | |||
| 38b9a772cd | |||
| 618430433d | |||
| 84cd398c09 | |||
| 385a9bba73 | |||
| 8218a452bd | |||
| a77e2eb3ad | |||
| d1a0bed00e | |||
| 66d4770858 | |||
| 80c5f9b84d | |||
| 79f46cb745 | |||
| de32a9862d | |||
| 0e119e4834 | |||
| 29c8e00477 | |||
| dc6ad9cdd3 | |||
| dcd754dac8 | |||
| d88fb18319 | |||
| 420e9c4662 | |||
| 98d6a1cc64 | |||
| 09ad081b37 | |||
| aa0fd9cafd | |||
| bae8290273 | |||
| 1b799a6973 | |||
| ed3b55a1e2 | |||
| 97c01ce81a | |||
| e96637219f | |||
| 17244b0006 | |||
| 67a02f06da | |||
| 6b6f345618 | |||
| 2ac9d3e977 | |||
| 93692ec255 | |||
| 99852f591e | |||
| b89525746d | |||
| c05834f2a1 | |||
| 9bbfb4763d | |||
| 22e6070e53 | |||
| ba218c85e0 | |||
| 644d2b06ac | |||
| 8d4c8a4553 | |||
| 077174a9a2 | |||
| ce31b95fb7 | |||
| 881eedbf3a | |||
| 09cb6f7b2b | |||
| bd091d5cb9 | |||
| 8cb67a8d20 | |||
| 290bb29e64 | |||
| d0769a5e37 | |||
| c5b28df2ae | |||
| c108fa509f | |||
| 1e5e9801be | |||
| 09b16c980b | |||
| 4c26fc808e | |||
| 525b11b346 | |||
| 86679b498b | |||
| 46df8b0528 | |||
| 1a4f896a8a | |||
| aaaa15a0ef | |||
| de65b1c699 | |||
| f9437065ee | |||
| b495cadae9 | |||
| 47995b77f7 | |||
| bc24ec5272 | |||
| 2947c41a72 | |||
| ef53035f70 | |||
| 290afc3f8f | |||
| d6e89b0880 | |||
| 2b72552b1f | |||
| df69418855 | |||
| 472e402521 | |||
| a3f282667c | |||
| b86263d972 | |||
| f278320b93 | |||
| 6345666ae6 | |||
| 7b5ebab453 | |||
| d4d713b12d | |||
| ab3af54e15 | |||
| b01e1eb8a1 | |||
| 0724932152 | |||
| cd7b15aadf | |||
| 37fc167002 | |||
| 9feeb302e8 | |||
| bba35d189e | |||
| cd5e4bbd60 | |||
| a513bf13ca | |||
| e3e570e664 | |||
| badff85e28 | |||
| 4a0f75044f | |||
| b729a7cead | |||
| 4375ca92d3 | |||
| 71537b283f | |||
| 63506dac1c | |||
| e716fae1c5 | |||
| f7370bc802 | |||
| 72c3fc78b3 | |||
| 110387dbd3 | |||
| 2820f151e8 | |||
| 9517df5082 | |||
| 56cedf0144 | |||
| bbaee7cd4d | |||
| 8ee2bdf488 | |||
| 97ecc83fe4 | |||
| 57f62a6087 | |||
| 2e760ff461 | |||
| 0df26cbd54 | |||
| 5f1ab4a2f3 | |||
| e1ff8c03e1 | |||
| 55f72c35a6 | |||
| 6c53701a59 | |||
| 02bb634257 | |||
| 5581c03f77 | |||
| cf788fe07b | |||
| 4bf425e1ca | |||
| a2f4fd5d9b | |||
| 295a1524d8 | |||
| 234154255c | |||
| 7b087840ec | |||
| 16b594ebdd | |||
| 67b3101fd1 | |||
| b3ce56c605 | |||
| 28cefa792c | |||
| 0803abc168 | |||
| 02ae883fa4 | |||
| be4050768e | |||
| dc6ec23cb9 | |||
| 1bb98c13d1 | |||
| bca979bab5 | |||
| e4fafd764c | |||
| 172159414b | |||
| 9355138a8c | |||
| 343cd8b772 | |||
| 01e0d5b94d | |||
| ac00667465 | |||
| 3deae2bfeb | |||
| 3f7b0f6563 | |||
| e6b9d4f273 | |||
| a00231dd3c | |||
| 3bc172e7e0 | |||
| ee9df21ae5 | |||
| f96b256ad3 | |||
| f2c50e929e | |||
| 02e3239848 | |||
| 8a54b027d0 | |||
| 3b11e896d4 | |||
| 89926b2c31 | |||
| 7b4e89555e | |||
| 1e37186247 | |||
| 154c763719 | |||
| 80197d5834 | |||
| 7e95103a2d | |||
| efe1a1f543 | |||
| 4fea690670 | |||
| f1dee1061d | |||
| 61cf0fc08d | |||
| 0c86e5dae1 | |||
| 638898fa28 | |||
| e7cd68e1c8 | |||
| e40e6faebd | |||
| 3d07aac944 | |||
| 1a5493facf | |||
| 9013b9492c | |||
| 188f26ad65 | |||
| 3ceb5a0c0f | |||
| e1ce052d3c | |||
| 70a379edef | |||
| 35ea3154d1 | |||
| ebf66821a2 | |||
| 8463bee253 | |||
| 860680d001 | |||
| df39166279 | |||
| 32fda46f0a | |||
| 36ecae7e6e | |||
| a5bfe4e3d5 | |||
| 4faeddc3f3 | |||
| 98f7bf366f | |||
| af3a9e5ce2 | |||
| 52eb7b1afe | |||
| 490fbd241d | |||
| f132131156 | |||
| c1e680a063 | |||
| c66b06c2c9 | |||
| 65e605cdc4 | |||
| d2fdb4efd9 | |||
| d0deb9d647 | |||
| 5495c90eaf | |||
| bf3ffae67c | |||
| aa0337ea33 | |||
| 4991d735bf | |||
| 398e61bddb | |||
| e6576f4a54 | |||
| c3b9e135b0 | |||
| 3bd4948c43 | |||
| f3cd1380be | |||
| a2c3dfbf85 | |||
| 3c37aafe1f | |||
| c591c182b3 | |||
| 9cc50078d1 | |||
| 7728759bcd | |||
| ce8fed350e | |||
| a005945e9e | |||
| cf86520fae | |||
| db6fdf6baf | |||
| 975ad50afc | |||
| 0c27df7754 | |||
| 102a860ba3 | |||
| 3a886714a0 | |||
| 09ab638239 | |||
| a4f88c78f4 | |||
| ccf2a3b617 | |||
| c8f941a779 | |||
| 5510cbb8e9 | |||
| a434173b54 | |||
| 7387f700fb | |||
| 4f01baaa23 | |||
| 09b37f0ff2 | |||
| 324d5709e3 | |||
| 3f23e4f1f1 | |||
| 9776a35f9f | |||
| 9b9ee70288 | |||
| 2628f69651 | |||
| b9c0a32862 | |||
| 82644a2ff4 | |||
| 3d2c93b5ac | |||
| c447114297 | |||
| 857de9ffcc | |||
| eea656bd7b | |||
| eec5284306 | |||
| 88a6a768c4 | |||
| edac1a224c | |||
| e67d1c5697 | |||
| 30502ec949 | |||
| a2c3913601 | |||
| f1c7713da2 | |||
| d6a41d5a82 | |||
| 72b5ca4153 | |||
| aeec2e1c32 | |||
| f9889bea3d | |||
| 2cad2ac2e9 | |||
| d948fe2631 | |||
| 2b5525323b | |||
| 58156e0d61 | |||
| a4b0dfe43e | |||
| ee2433a5ae | |||
| 2151b8502d | |||
| 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 | |||
| efecffb72d | |||
| cc5a30980a | |||
| e3a5b41b5e |
95
.drone.yml
95
.drone.yml
@@ -11,7 +11,7 @@ steps:
|
||||
- git checkout $DRONE_SOURCE_BRANCH
|
||||
- mv .env.ci .env
|
||||
- name: run tests
|
||||
image: node:alpine
|
||||
image: node:latest
|
||||
commands:
|
||||
- yarn
|
||||
- yarn test:ci
|
||||
@@ -23,8 +23,15 @@ trigger:
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build:dev
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps:
|
||||
- name: clone
|
||||
image: alpine/git
|
||||
commands:
|
||||
- git clone $DRONE_REMOTE_URL .
|
||||
- git checkout dev
|
||||
- name: build dev
|
||||
image: plugins/docker
|
||||
depends_on: [clone]
|
||||
@@ -37,11 +44,41 @@ steps:
|
||||
tags:
|
||||
- dev
|
||||
registry: registry.odit.services
|
||||
when:
|
||||
branch:
|
||||
- dev
|
||||
event:
|
||||
- push
|
||||
- name: run changelog export
|
||||
depends_on: ["clone"]
|
||||
image: node:latest
|
||||
commands:
|
||||
- npx auto-changelog --commit-limit false -p -u --hide-credit
|
||||
- name: push new changelog to repo
|
||||
depends_on: ["run changelog export"]
|
||||
image: appleboy/drone-git-push
|
||||
settings:
|
||||
branch: dev
|
||||
commit: true
|
||||
commit_message: 🧾New changelog file version [CI SKIP] [skip ci]
|
||||
author_email: bot@odit.services
|
||||
remote: git@git.odit.services:lfk/backend.git
|
||||
ssh_key:
|
||||
from_secret: GITLAB_SSHKEY
|
||||
- name: run full license export
|
||||
depends_on: ["clone"]
|
||||
image: node:14.15.1-alpine3.12
|
||||
commands:
|
||||
- yarn
|
||||
- yarn licenses:export
|
||||
- name: push new licenses file to repo
|
||||
depends_on: ["run full license export"]
|
||||
image: appleboy/drone-git-push
|
||||
settings:
|
||||
branch: dev
|
||||
commit: true
|
||||
commit_message: 📖New license file version [CI SKIP] [skip ci]
|
||||
author_email: bot@odit.services
|
||||
remote: git@git.odit.services:lfk/backend.git
|
||||
skip_verify: true
|
||||
ssh_key:
|
||||
from_secret: GITLAB_SSHKEY
|
||||
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
@@ -53,11 +90,20 @@ trigger:
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build:latest
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps:
|
||||
- name: clone
|
||||
image: alpine/git
|
||||
commands:
|
||||
- git clone $DRONE_REMOTE_URL .
|
||||
- git checkout dev
|
||||
- git merge main
|
||||
- git checkout main
|
||||
- name: build latest
|
||||
depends_on: ["clone"]
|
||||
image: plugins/docker
|
||||
depends_on: [clone]
|
||||
settings:
|
||||
username:
|
||||
from_secret: DOCKER_REGISTRY_USER
|
||||
@@ -67,6 +113,15 @@ steps:
|
||||
tags:
|
||||
- latest
|
||||
registry: registry.odit.services
|
||||
- name: push merge to repo
|
||||
depends_on: ["clone"]
|
||||
image: appleboy/drone-git-push
|
||||
settings:
|
||||
branch: dev
|
||||
commit: false
|
||||
remote: git@git.odit.services:lfk/backend.git
|
||||
ssh_key:
|
||||
from_secret: GITLAB_SSHKEY
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
@@ -90,22 +145,20 @@ steps:
|
||||
from_secret: DOCKER_REGISTRY_PASSWORD
|
||||
repo: registry.odit.services/lfk/backend
|
||||
tags:
|
||||
- $DRONE_TAG
|
||||
- '${DRONE_TAG}'
|
||||
registry: registry.odit.services
|
||||
- name: trigger lib build
|
||||
depends_on: [clone]
|
||||
image: plugins/downstream
|
||||
- name: trigger node lib build
|
||||
image: idcooldi/drone-webhook
|
||||
settings:
|
||||
server: https://ci.odit.services/
|
||||
token:
|
||||
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
|
||||
fork: false
|
||||
repositories:
|
||||
- lfk/lib
|
||||
params:
|
||||
- SOURCE_TAG: $DRONE_TAG
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- tag
|
||||
- tag
|
||||
3
.env.ci
3
.env.ci
@@ -5,4 +5,5 @@ DB_PORT=unused
|
||||
DB_USER=unused
|
||||
DB_PASSWORD=bla
|
||||
DB_NAME=./test.sqlite
|
||||
NODE_ENV=dev
|
||||
NODE_ENV=dev
|
||||
POSTALCODE_COUNTRYCODE=DE
|
||||
@@ -5,4 +5,5 @@ DB_PORT=bla
|
||||
DB_USER=bla
|
||||
DB_PASSWORD=bla
|
||||
DB_NAME=bla
|
||||
NODE_ENV=production
|
||||
NODE_ENV=production
|
||||
POSTALCODE_COUNTRYCODE=DE
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -132,5 +132,7 @@ build
|
||||
|
||||
*.sqlite
|
||||
*.sqlite-jurnal
|
||||
docs
|
||||
lib
|
||||
/docs
|
||||
lib
|
||||
/oss-attribution
|
||||
*.tmp
|
||||
1027
CHANGELOG.md
Normal file
1027
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
1296
licenses.md
Normal file
1296
licenses.md
Normal file
File diff suppressed because it is too large
Load Diff
67
package.json
67
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lfk/backend",
|
||||
"version": "1.0.0",
|
||||
"name": "@odit/lfk-backend",
|
||||
"version": "0.2.0",
|
||||
"main": "src/app.ts",
|
||||
"repository": "https://git.odit.services/lfk/backend",
|
||||
"author": {
|
||||
@@ -22,58 +22,77 @@
|
||||
],
|
||||
"license": "CC-BY-NC-SA-4.0",
|
||||
"dependencies": {
|
||||
"argon2": "^0.27.0",
|
||||
"@odit/class-validator-jsonschema": "2.1.1",
|
||||
"argon2": "^0.27.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"class-transformer": "^0.3.1",
|
||||
"class-validator": "^0.12.2",
|
||||
"class-validator-jsonschema": "^2.0.3",
|
||||
"class-transformer": "0.3.1",
|
||||
"class-validator": "^0.13.1",
|
||||
"consola": "^2.15.0",
|
||||
"cookie": "^0.4.1",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"csvtojson": "^2.0.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"libphonenumber-js": "^1.9.7",
|
||||
"mysql": "^2.18.1",
|
||||
"pg": "^8.5.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"routing-controllers": "^0.9.0-alpha.6",
|
||||
"routing-controllers-openapi": "^2.1.0",
|
||||
"sqlite3": "^5.0.0",
|
||||
"swagger-ui-express": "^4.1.5",
|
||||
"routing-controllers-openapi": "^2.2.0",
|
||||
"sqlite3": "5.0.0",
|
||||
"typeorm": "^0.2.29",
|
||||
"typeorm-routing-controllers-extensions": "^0.2.0",
|
||||
"typeorm-seeding": "^1.6.1",
|
||||
"uuid": "^8.3.1"
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "^13.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.8",
|
||||
"@odit/license-exporter": "^0.0.9",
|
||||
"@types/cors": "^2.8.9",
|
||||
"@types/csvtojson": "^1.1.5",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/jest": "^26.0.16",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/node": "^14.14.9",
|
||||
"@types/swagger-ui-express": "^4.1.2",
|
||||
"@types/node": "^14.14.20",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"axios": "^0.21.0",
|
||||
"axios": "^0.21.1",
|
||||
"cp-cli": "^2.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"nodemon": "^2.0.6",
|
||||
"rimraf": "^2.7.1",
|
||||
"start-server-and-test": "^1.11.6",
|
||||
"nodemon": "^2.0.7",
|
||||
"release-it": "^14.2.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"start-server-and-test": "^1.11.7",
|
||||
"ts-jest": "^26.4.4",
|
||||
"ts-node": "^9.0.0",
|
||||
"typedoc": "^0.19.2",
|
||||
"typescript": "^4.1.2"
|
||||
"ts-node": "^9.1.1",
|
||||
"typedoc": "^0.20.14",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"scripts": {
|
||||
"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/openapi.json test",
|
||||
"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 src/openapi_export.ts"
|
||||
"openapi:export": "ts-node scripts/openapi_export.ts",
|
||||
"licenses:export": "license-exporter --md",
|
||||
"release": "release-it --only-version"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
"commit": true,
|
||||
"requireCleanWorkingDir": false,
|
||||
"commitMessage": "🚀Bumped version to v${version}",
|
||||
"requireBranch": "dev",
|
||||
"push": false,
|
||||
"tag": false
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
}
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
@@ -81,4 +100,4 @@
|
||||
"docs/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
scripts/openapi_export.ts
Normal file
34
scripts/openapi_export.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { validationMetadatasToSchemas } from '@odit/class-validator-jsonschema';
|
||||
import consola from "consola";
|
||||
import fs from "fs";
|
||||
import "reflect-metadata";
|
||||
import { createExpressServer, getMetadataArgsStorage } from "routing-controllers";
|
||||
import { generateSpec } from '../src/apispec';
|
||||
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 = generateSpec(storage, schemas);
|
||||
|
||||
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");
|
||||
}
|
||||
51
src/apispec.ts
Normal file
51
src/apispec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { MetadataArgsStorage } from 'routing-controllers';
|
||||
import { routingControllersToSpec } from 'routing-controllers-openapi';
|
||||
import { config } from './config';
|
||||
|
||||
/**
|
||||
* This function generates a the openapi spec from route metadata and type schemas.
|
||||
* @param storage MetadataArgsStorage object generated by routing-controllers.
|
||||
* @param schemas MetadataArgsStorage object generated by class-validator-jsonschema.
|
||||
*/
|
||||
export function generateSpec(storage: MetadataArgsStorage, schemas) {
|
||||
return 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). Only valid for obtaining stats."
|
||||
},
|
||||
"StationApiToken": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
description: "Api token that can be obtained by creating a new scan station (post to /api/stations). Only valid for creating scans."
|
||||
}
|
||||
}
|
||||
},
|
||||
info: {
|
||||
description: "The the backend API for the LfK! runner system.",
|
||||
title: "LfK! Backend API",
|
||||
version: config.version
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import consola from "consola";
|
||||
import "reflect-metadata";
|
||||
import { createExpressServer } from "routing-controllers";
|
||||
import authchecker from "./authchecker";
|
||||
import { config, e as errors } from './config';
|
||||
import loaders from "./loaders/index";
|
||||
import authchecker from "./middlewares/authchecker";
|
||||
import { ErrorHandler } from './middlewares/ErrorHandler';
|
||||
import UserChecker from './middlewares/UserChecker';
|
||||
|
||||
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
|
||||
const app = createExpressServer({
|
||||
authorizationChecker: authchecker,
|
||||
currentUserChecker: UserChecker,
|
||||
middlewares: [ErrorHandler],
|
||||
development: config.development,
|
||||
cors: true,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import { Action } from "routing-controllers";
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { config } from './config';
|
||||
import { IllegalJWTError, NoPermissionError, UserNonexistantOrRefreshtokenInvalidError } from './errors/AuthError';
|
||||
import { User } from './models/entities/User';
|
||||
// -----------
|
||||
const authchecker = async (action: Action, permissions: string | string[]) => {
|
||||
let required_permissions = undefined
|
||||
if (typeof permissions === "string") {
|
||||
required_permissions = [permissions]
|
||||
} else {
|
||||
required_permissions = permissions
|
||||
}
|
||||
// const token = action.request.headers["authorization"];
|
||||
const provided_token = action.request.query["auth"];
|
||||
let jwtPayload = undefined
|
||||
try {
|
||||
jwtPayload = <any>jwt.verify(provided_token, config.jwt_secret);
|
||||
} catch (error) {
|
||||
throw new IllegalJWTError()
|
||||
}
|
||||
const count = await getConnectionManager().get().getRepository(User).count({ id: jwtPayload["userdetails"]["id"], refreshTokenCount: jwtPayload["userdetails"]["refreshTokenCount"] })
|
||||
if (count !== 1) {
|
||||
throw new UserNonexistantOrRefreshtokenInvalidError()
|
||||
}
|
||||
if (jwtPayload.permissions) {
|
||||
action.response.local = {}
|
||||
action.response.local.jwtPayload = jwtPayload.permissions
|
||||
required_permissions.forEach(r => {
|
||||
const permission_key = r.split(":")[0]
|
||||
const actual_accesslevel_for_permission = jwtPayload.permissions[permission_key]
|
||||
const permission_access_level = r.split(":")[1]
|
||||
if (actual_accesslevel_for_permission.includes(permission_access_level)) {
|
||||
return true;
|
||||
} else {
|
||||
throw new NoPermissionError()
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new NoPermissionError()
|
||||
}
|
||||
//
|
||||
try {
|
||||
jwt.verify(provided_token, config.jwt_secret);
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
export default authchecker
|
||||
@@ -1,22 +1,33 @@
|
||||
import { config as configDotenv } from 'dotenv';
|
||||
import { CountryCode } from 'libphonenumber-js';
|
||||
import ValidatorJS from 'validator';
|
||||
|
||||
configDotenv();
|
||||
export const config = {
|
||||
internal_port: parseInt(process.env.APP_PORT) || 4010,
|
||||
development: process.env.NODE_ENV === "production",
|
||||
jwt_secret: process.env.JWT_SECRET || "secretjwtsecret",
|
||||
phone_validation_countrycode: process.env.PHONE_COUNTRYCODE || "ZZ"
|
||||
phone_validation_countrycode: getPhoneCodeLocale(),
|
||||
postalcode_validation_countrycode: getPostalCodeLocale(),
|
||||
version: process.env.VERSION || require('../package.json').version
|
||||
}
|
||||
let errors = 0
|
||||
if (typeof config.internal_port !== "number") {
|
||||
errors++
|
||||
}
|
||||
if (typeof config.phone_validation_countrycode !== "string") {
|
||||
errors++
|
||||
}
|
||||
if (config.phone_validation_countrycode.length !== 2) {
|
||||
errors++
|
||||
}
|
||||
if (typeof config.development !== "boolean") {
|
||||
errors++
|
||||
}
|
||||
function getPhoneCodeLocale(): CountryCode {
|
||||
return (process.env.PHONE_COUNTRYCODE as CountryCode);
|
||||
}
|
||||
function getPostalCodeLocale(): any {
|
||||
try {
|
||||
const stringArray: String[] = ValidatorJS.isPostalCodeLocales;
|
||||
let index = stringArray.indexOf(process.env.POSTALCODE_COUNTRYCODE);
|
||||
return ValidatorJS.isPostalCodeLocales[index];
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export let e = errors
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Body, CookieParam, JsonController, Post, Res } from 'routing-controllers';
|
||||
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 { CreateAuth } from '../models/actions/create/CreateAuth';
|
||||
import { CreateResetToken } from '../models/actions/create/CreateResetToken';
|
||||
import { HandleLogout } from '../models/actions/HandleLogout';
|
||||
import { RefreshAuth } from '../models/actions/RefreshAuth';
|
||||
import { Auth } from '../models/responses/ResponseAuth';
|
||||
import { ResetPassword } from '../models/actions/ResetPassword';
|
||||
import { ResponseAuth } from '../models/responses/ResponseAuth';
|
||||
import { Logout } from '../models/responses/ResponseLogout';
|
||||
|
||||
@JsonController('/auth')
|
||||
@@ -14,13 +16,13 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post("/login")
|
||||
@ResponseSchema(Auth)
|
||||
@ResponseSchema(ResponseAuth)
|
||||
@ResponseSchema(InvalidCredentialsError)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(UsernameOrEmailNeededError)
|
||||
@ResponseSchema(PasswordNeededError)
|
||||
@ResponseSchema(InvalidCredentialsError)
|
||||
@OpenAPI({ description: 'Create a new access token object' })
|
||||
@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 {
|
||||
@@ -40,7 +42,7 @@ export class AuthController {
|
||||
@ResponseSchema(UsernameOrEmailNeededError)
|
||||
@ResponseSchema(PasswordNeededError)
|
||||
@ResponseSchema(InvalidCredentialsError)
|
||||
@OpenAPI({ description: 'Create a new access token object' })
|
||||
@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;
|
||||
@@ -58,13 +60,13 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post("/refresh")
|
||||
@ResponseSchema(Auth)
|
||||
@ResponseSchema(ResponseAuth)
|
||||
@ResponseSchema(JwtNotProvidedError)
|
||||
@ResponseSchema(IllegalJWTError)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(RefreshTokenCountInvalidError)
|
||||
@OpenAPI({ description: 'refresh a access token' })
|
||||
async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any) {
|
||||
@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;
|
||||
}
|
||||
@@ -78,4 +80,24 @@ export class AuthController {
|
||||
}
|
||||
return response.send(auth)
|
||||
}
|
||||
|
||||
@Post("/reset")
|
||||
@ResponseSchema(ResponseAuth)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(UsernameOrEmailNeededError)
|
||||
@OpenAPI({ description: "Request a password reset token. <br> This will provide you with a reset token that you can use by posting to /api/auth/reset/{token}." })
|
||||
async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken) {
|
||||
//This really shouldn't just get returned, but sent via mail or sth like that. But for dev only this is fine.
|
||||
return { "resetToken": await passwordReset.toResetToken() };
|
||||
}
|
||||
|
||||
@Post("/reset/:token")
|
||||
@ResponseSchema(ResponseAuth)
|
||||
@ResponseSchema(UserNotFoundError)
|
||||
@ResponseSchema(UsernameOrEmailNeededError)
|
||||
@OpenAPI({ description: "Reset a user's utilising a valid password reset token. <br> This will set the user's password to the one you provided in the body. <br> To get a reset token post to /api/auth/reset with your username." })
|
||||
async resetPassword(@Param("token") token: string, @Body({ validate: true }) passwordReset: ResetPassword) {
|
||||
passwordReset.resetToken = token;
|
||||
return await passwordReset.resetPassword();
|
||||
}
|
||||
}
|
||||
|
||||
145
src/controllers/DonationController.ts
Normal file
145
src/controllers/DonationController.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
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 { DonationIdsNotMatchingError, DonationNotFoundError } from '../errors/DonationErrors';
|
||||
import { DonorNotFoundError } from '../errors/DonorErrors';
|
||||
import { RunnerNotFoundError } from '../errors/RunnerErrors';
|
||||
import { CreateDistanceDonation } from '../models/actions/create/CreateDistanceDonation';
|
||||
import { CreateFixedDonation } from '../models/actions/create/CreateFixedDonation';
|
||||
import { UpdateDistanceDonation } from '../models/actions/update/UpdateDistanceDonation';
|
||||
import { UpdateFixedDonation } from '../models/actions/update/UpdateFixedDonation';
|
||||
import { DistanceDonation } from '../models/entities/DistanceDonation';
|
||||
import { Donation } from '../models/entities/Donation';
|
||||
import { FixedDonation } from '../models/entities/FixedDonation';
|
||||
import { ResponseDistanceDonation } from '../models/responses/ResponseDistanceDonation';
|
||||
import { ResponseDonation } from '../models/responses/ResponseDonation';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
|
||||
@JsonController('/donations')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class DonationController {
|
||||
private donationRepository: Repository<Donation>;
|
||||
private distanceDonationRepository: Repository<DistanceDonation>;
|
||||
private fixedDonationRepository: Repository<FixedDonation>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.donationRepository = getConnectionManager().get().getRepository(Donation);
|
||||
this.distanceDonationRepository = getConnectionManager().get().getRepository(DistanceDonation);
|
||||
this.fixedDonationRepository = getConnectionManager().get().getRepository(FixedDonation);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("DONATION:GET")
|
||||
@ResponseSchema(ResponseDonation, { isArray: true })
|
||||
@ResponseSchema(ResponseDistanceDonation, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all donations (fixed or distance based) from all donors. <br> This includes the donations\'s runner\'s distance ran(if distance donation).' })
|
||||
async getAll() {
|
||||
let responseDonations: ResponseDonation[] = new Array<ResponseDonation>();
|
||||
const donations = await this.donationRepository.find({ relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] });
|
||||
donations.forEach(donation => {
|
||||
responseDonations.push(donation.toResponse());
|
||||
});
|
||||
return responseDonations;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("DONATION:GET")
|
||||
@ResponseSchema(ResponseDonation)
|
||||
@ResponseSchema(ResponseDistanceDonation)
|
||||
@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(DonationNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about the donation whose id got provided. This includes the donation\'s runner\'s distance ran (if distance donation).' })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let donation = await this.donationRepository.findOne({ id: id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })
|
||||
if (!donation) { throw new DonationNotFoundError(); }
|
||||
return donation.toResponse();
|
||||
}
|
||||
|
||||
@Post('/fixed')
|
||||
@Authorized("DONATION:CREATE")
|
||||
@ResponseSchema(ResponseDonation)
|
||||
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Create a fixed donation (not distance donation - use /donations/distance instead). <br> Please rmemember to provide the donation\'s donors\'s id and amount.' })
|
||||
async postFixed(@Body({ validate: true }) createDonation: CreateFixedDonation) {
|
||||
let donation = await createDonation.toEntity();
|
||||
donation = await this.fixedDonationRepository.save(donation);
|
||||
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
|
||||
}
|
||||
|
||||
@Post('/distance')
|
||||
@Authorized("DONATION:CREATE")
|
||||
@ResponseSchema(ResponseDistanceDonation)
|
||||
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Create a distance donation (not fixed donation - use /donations/fixed instead). <br> Please rmemember to provide the donation\'s donors\'s and runner\s ids and amount per distance (kilometer).' })
|
||||
async postDistance(@Body({ validate: true }) createDonation: CreateDistanceDonation) {
|
||||
let donation = await createDonation.toEntity();
|
||||
donation = await this.distanceDonationRepository.save(donation);
|
||||
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
|
||||
}
|
||||
|
||||
@Put('/fixed/:id')
|
||||
@Authorized("DONATION:UPDATE")
|
||||
@ResponseSchema(ResponseDonation)
|
||||
@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the fixed donation (not distance donation - use /donations/distance instead) whose id you provided. <br> Please remember that ids can't be changed and amounts must be positive." })
|
||||
async putFixed(@Param('id') id: number, @Body({ validate: true }) donation: UpdateFixedDonation) {
|
||||
let oldDonation = await this.fixedDonationRepository.findOne({ id: id });
|
||||
|
||||
if (!oldDonation) {
|
||||
throw new DonationNotFoundError();
|
||||
}
|
||||
|
||||
if (oldDonation.id != donation.id) {
|
||||
throw new DonationIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.fixedDonationRepository.save(await donation.update(oldDonation));
|
||||
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
|
||||
}
|
||||
|
||||
@Put('/distance/:id')
|
||||
@Authorized("DONATION:UPDATE")
|
||||
@ResponseSchema(ResponseDonation)
|
||||
@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the distance donation (not fixed donation - use /donations/fixed instead) whose id you provided. <br> Please remember that ids can't be changed and amountPerDistance must be positive." })
|
||||
async putDistance(@Param('id') id: number, @Body({ validate: true }) donation: UpdateDistanceDonation) {
|
||||
let oldDonation = await this.distanceDonationRepository.findOne({ id: id });
|
||||
|
||||
if (!oldDonation) {
|
||||
throw new DonationNotFoundError();
|
||||
}
|
||||
|
||||
if (oldDonation.id != donation.id) {
|
||||
throw new DonationIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.distanceDonationRepository.save(await donation.update(oldDonation));
|
||||
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("DONATION:DELETE")
|
||||
@ResponseSchema(ResponseDonation)
|
||||
@ResponseSchema(ResponseDistanceDonation)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the donation whose id you provided. <br> If no donation with this id exists it will just return 204(no content).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let donation = await this.donationRepository.findOne({ id: id });
|
||||
if (!donation) { return null; }
|
||||
const responseScan = await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] });
|
||||
|
||||
await this.donationRepository.delete(donation);
|
||||
return responseScan.toResponse();
|
||||
}
|
||||
}
|
||||
113
src/controllers/DonorController.ts
Normal file
113
src/controllers/DonorController.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
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 { DonorHasDonationsError, DonorIdsNotMatchingError, DonorNotFoundError } from '../errors/DonorErrors';
|
||||
import { CreateDonor } from '../models/actions/create/CreateDonor';
|
||||
import { UpdateDonor } from '../models/actions/update/UpdateDonor';
|
||||
import { Donor } from '../models/entities/Donor';
|
||||
import { ResponseDonor } from '../models/responses/ResponseDonor';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { DonationController } from './DonationController';
|
||||
|
||||
@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 donor. <br> This includes the donor\'s current donation amount.' })
|
||||
async getAll() {
|
||||
let responseDonors: ResponseDonor[] = new Array<ResponseDonor>();
|
||||
const donors = await this.donorRepository.find({ relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] });
|
||||
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 donor whose id got provided. <br> This includes the donor\'s current donation amount.' })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let donor = await this.donorRepository.findOne({ id: id }, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] })
|
||||
if (!donor) { throw new DonorNotFoundError(); }
|
||||
return new ResponseDonor(donor);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("DONOR:CREATE")
|
||||
@ResponseSchema(ResponseDonor)
|
||||
@OpenAPI({ description: 'Create a new donor.' })
|
||||
async post(@Body({ validate: true }) createRunner: CreateDonor) {
|
||||
let donor;
|
||||
try {
|
||||
donor = await createRunner.toEntity();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
donor = await this.donorRepository.save(donor)
|
||||
return new ResponseDonor(await this.donorRepository.findOne(donor, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] }));
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("DONOR:UPDATE")
|
||||
@ResponseSchema(ResponseDonor)
|
||||
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(DonorIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the donor 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.update(oldDonor));
|
||||
return new ResponseDonor(await this.donorRepository.findOne({ id: id }, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] }));
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("DONOR:DELETE")
|
||||
@ResponseSchema(ResponseDonor)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the donor whose id you provided. <br> If no donor with this id exists it will just return 204(no content). <br> If the donor still has donations associated this will fail, please provide the query param ?force=true to delete the donor with all associated donations.' })
|
||||
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, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] });
|
||||
|
||||
if (!donor) {
|
||||
throw new DonorNotFoundError();
|
||||
}
|
||||
|
||||
const donorDonations = (await this.donorRepository.findOne({ id: donor.id }, { relations: ["donations"] })).donations;
|
||||
if (donorDonations.length > 0 && !force) {
|
||||
throw new DonorHasDonationsError();
|
||||
}
|
||||
const donationController = new DonationController();
|
||||
for (let donation of donorDonations) {
|
||||
await donationController.remove(donation.id, force);
|
||||
}
|
||||
|
||||
await this.donorRepository.delete(donor);
|
||||
return new ResponseDonor(responseDonor);
|
||||
}
|
||||
}
|
||||
107
src/controllers/GroupContactController.ts
Normal file
107
src/controllers/GroupContactController.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnection, getConnectionManager, Repository } from 'typeorm';
|
||||
import { GroupContactIdsNotMatchingError, GroupContactNotFoundError } from '../errors/GroupContactErrors';
|
||||
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
|
||||
import { CreateGroupContact } from '../models/actions/create/CreateGroupContact';
|
||||
import { UpdateGroupContact } from '../models/actions/update/UpdateGroupContact';
|
||||
import { GroupContact } from '../models/entities/GroupContact';
|
||||
import { RunnerGroup } from '../models/entities/RunnerGroup';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseGroupContact } from '../models/responses/ResponseGroupContact';
|
||||
|
||||
@JsonController('/contacts')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class GroupContactController {
|
||||
private contactRepository: Repository<GroupContact>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.contactRepository = getConnectionManager().get().getRepository(GroupContact);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("CONTACT:GET")
|
||||
@ResponseSchema(ResponseGroupContact, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all contacts. <br> This includes the contact\'s associated groups.' })
|
||||
async getAll() {
|
||||
let responseContacts: ResponseGroupContact[] = new Array<ResponseGroupContact>();
|
||||
const contacts = await this.contactRepository.find({ relations: ['groups'] });
|
||||
contacts.forEach(contact => {
|
||||
responseContacts.push(contact.toResponse());
|
||||
});
|
||||
return responseContacts;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("CONTACT:GET")
|
||||
@ResponseSchema(ResponseGroupContact)
|
||||
@ResponseSchema(GroupContactNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(GroupContactNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about the contact whose id got provided. <br> This includes the contact\'s associated groups.' })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let contact = await this.contactRepository.findOne({ id: id }, { relations: ['groups'] })
|
||||
if (!contact) { throw new GroupContactNotFoundError(); }
|
||||
return contact.toResponse();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("CONTACT:CREATE")
|
||||
@ResponseSchema(ResponseGroupContact)
|
||||
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Create a new contact.' })
|
||||
async post(@Body({ validate: true }) createContact: CreateGroupContact) {
|
||||
let contact;
|
||||
try {
|
||||
contact = await createContact.toEntity();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
contact = await this.contactRepository.save(contact)
|
||||
return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups'] })).toResponse();
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("CONTACT:UPDATE")
|
||||
@ResponseSchema(ResponseGroupContact)
|
||||
@ResponseSchema(GroupContactNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(GroupContactIdsNotMatchingError, { statusCode: 406 })
|
||||
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: "Update the contact whose id you provided. <br> Please remember that ids can't be changed." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) contact: UpdateGroupContact) {
|
||||
let oldContact = await this.contactRepository.findOne({ id: id });
|
||||
|
||||
if (!oldContact) {
|
||||
throw new GroupContactNotFoundError();
|
||||
}
|
||||
|
||||
if (oldContact.id != contact.id) {
|
||||
throw new GroupContactIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.contactRepository.save(await contact.update(oldContact));
|
||||
return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups'] })).toResponse();
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("CONTACT:DELETE")
|
||||
@ResponseSchema(ResponseGroupContact)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the contact whose id you provided. <br> If no contact with this id exists it will just return 204(no content). <br> This won\'t delete any groups associated with the contact.' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let contact = await this.contactRepository.findOne({ id: id });
|
||||
if (!contact) { return null; }
|
||||
const responseContact = await this.contactRepository.findOne(contact, { relations: ['groups'] });
|
||||
for (let group of responseContact.groups) {
|
||||
group.contact = null;
|
||||
await getConnection().getRepository(RunnerGroup).save(group);
|
||||
}
|
||||
|
||||
await this.contactRepository.delete(contact);
|
||||
return responseContact.toResponse();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import csv from 'csvtojson';
|
||||
import { Body, ContentType, Controller, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers';
|
||||
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';
|
||||
@@ -9,7 +9,8 @@ import { ResponseRunner } from '../models/responses/ResponseRunner';
|
||||
import { RunnerController } from './RunnerController';
|
||||
|
||||
@Controller()
|
||||
//@Authorized("IMPORT:read")
|
||||
@Authorized(["RUNNER:IMPORT", "TEAM:IMPORT"])
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class ImportController {
|
||||
private runnerController: RunnerController;
|
||||
|
||||
@@ -25,7 +26,7 @@ export class ImportController {
|
||||
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Create new runners from json and insert them (or their teams) into the provided group" })
|
||||
@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>();
|
||||
@@ -40,7 +41,7 @@ export class ImportController {
|
||||
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Create new runners from json and insert them (or their teams) into the provided org" })
|
||||
@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)
|
||||
}
|
||||
@@ -61,7 +62,7 @@ export class ImportController {
|
||||
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Create new runners from csv and insert them (or their teams) into the provided group" })
|
||||
@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>();
|
||||
@@ -83,7 +84,7 @@ export class ImportController {
|
||||
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
|
||||
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Create new runners from csv and insert them (or their teams) into the provided org" })
|
||||
@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);
|
||||
}
|
||||
|
||||
86
src/controllers/MeController.ts
Normal file
86
src/controllers/MeController.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Body, CurrentUser, Delete, Get, JsonController, OnUndefined, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors';
|
||||
import { UpdateUser } from '../models/actions/update/UpdateUser';
|
||||
import { User } from '../models/entities/User';
|
||||
import { ResponseUser } from '../models/responses/ResponseUser';
|
||||
import { ResponseUserPermissions } from '../models/responses/ResponseUserPermissions';
|
||||
import { PermissionController } from './PermissionController';
|
||||
|
||||
|
||||
@JsonController('/users/me')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class MeController {
|
||||
private userRepository: Repository<User>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.userRepository = getConnectionManager().get().getRepository(User);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(UserNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about yourself.' })
|
||||
async get(@CurrentUser() currentUser: User) {
|
||||
let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
|
||||
if (!user) { throw new UserNotFoundError(); }
|
||||
return new ResponseUser(user);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@ResponseSchema(ResponseUserPermissions)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(UserNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all permissions granted to the you sorted into directly granted and inherited as permission response objects.' })
|
||||
async getPermissions(@CurrentUser() currentUser: User) {
|
||||
let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
|
||||
if (!user) { throw new UserNotFoundError(); }
|
||||
return new ResponseUserPermissions(user);
|
||||
}
|
||||
|
||||
@Put('/')
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
|
||||
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the yourself. <br> You can't edit your own permissions or group memberships here - Please use the /api/users/:id enpoint instead. <br> Please remember that ids can't be changed." })
|
||||
async put(@CurrentUser() currentUser: User, @Body({ validate: true }) updateUser: UpdateUser) {
|
||||
let oldUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['groups'] });
|
||||
updateUser.groups = oldUser.groups.map(g => g.id);
|
||||
|
||||
if (!oldUser) {
|
||||
throw new UserNotFoundError();
|
||||
}
|
||||
|
||||
if (oldUser.id != updateUser.id) {
|
||||
throw new UserIdsNotMatchingError();
|
||||
}
|
||||
await this.userRepository.save(await updateUser.update(oldUser));
|
||||
|
||||
return new ResponseUser(await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] }));
|
||||
}
|
||||
|
||||
@Delete('/')
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 })
|
||||
@OpenAPI({ description: 'Delete yourself. <br> You have to confirm your decision by providing the ?force=true query param. <br> If there are any permissions directly granted to you they will get deleted as well.' })
|
||||
async remove(@CurrentUser() currentUser: User, @QueryParam("force") force: boolean) {
|
||||
if (!force) { throw new UserDeletionNotConfirmedError; }
|
||||
if (!currentUser) { return UserNotFoundError; }
|
||||
const responseUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] });;
|
||||
|
||||
const permissionControler = new PermissionController();
|
||||
for (let permission of responseUser.permissions) {
|
||||
await permissionControler.remove(permission.id, true);
|
||||
}
|
||||
|
||||
await this.userRepository.delete(currentUser);
|
||||
return new ResponseUser(responseUser);
|
||||
}
|
||||
}
|
||||
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/create/CreatePermission';
|
||||
import { UpdatePermission } from '../models/actions/update/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.toEntity();
|
||||
} 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: await permission.getPrincipal() }, { relations: ['principal'] });
|
||||
if (existingPermission) {
|
||||
await this.remove(permission.id, true);
|
||||
return new ResponsePermission(existingPermission);
|
||||
}
|
||||
|
||||
await this.permissionRepository.save(await permission.update(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/RunnerCardController.ts
Normal file
106
src/controllers/RunnerCardController.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 { RunnerCardHasScansError, RunnerCardIdsNotMatchingError, RunnerCardNotFoundError } from '../errors/RunnerCardErrors';
|
||||
import { RunnerNotFoundError } from '../errors/RunnerErrors';
|
||||
import { CreateRunnerCard } from '../models/actions/create/CreateRunnerCard';
|
||||
import { UpdateRunnerCard } from '../models/actions/update/UpdateRunnerCard';
|
||||
import { RunnerCard } from '../models/entities/RunnerCard';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseRunnerCard } from '../models/responses/ResponseRunnerCard';
|
||||
import { ScanController } from './ScanController';
|
||||
|
||||
@JsonController('/cards')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class RunnerCardController {
|
||||
private cardRepository: Repository<RunnerCard>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.cardRepository = getConnectionManager().get().getRepository(RunnerCard);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("CARD:GET")
|
||||
@ResponseSchema(ResponseRunnerCard, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all card.' })
|
||||
async getAll() {
|
||||
let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
|
||||
const cards = await this.cardRepository.find({ relations: ['runner'] });
|
||||
cards.forEach(card => {
|
||||
responseCards.push(new ResponseRunnerCard(card));
|
||||
});
|
||||
return responseCards;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("CARD:GET")
|
||||
@ResponseSchema(ResponseRunnerCard)
|
||||
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerCardNotFoundError)
|
||||
@OpenAPI({ description: "Lists all information about the card whose id got provided." })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let card = await this.cardRepository.findOne({ id: id }, { relations: ['runner'] });
|
||||
if (!card) { throw new RunnerCardNotFoundError(); }
|
||||
return card.toResponse();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("CARD:CREATE")
|
||||
@ResponseSchema(ResponseRunnerCard)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: "Create a new card. <br> You can provide a associated runner by id but you don't have to." })
|
||||
async post(@Body({ validate: true }) createCard: CreateRunnerCard) {
|
||||
let card = await createCard.toEntity();
|
||||
card = await this.cardRepository.save(card);
|
||||
return (await this.cardRepository.findOne({ id: card.id }, { relations: ['runner'] })).toResponse();
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("CARD:UPDATE")
|
||||
@ResponseSchema(ResponseRunnerCard)
|
||||
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerCardIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the card whose id you provided. <br> Scans created via this card will still be associated with the old runner. <br> Please remember that ids can't be changed." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) card: UpdateRunnerCard) {
|
||||
let oldCard = await this.cardRepository.findOne({ id: id });
|
||||
|
||||
if (!oldCard) {
|
||||
throw new RunnerCardNotFoundError();
|
||||
}
|
||||
|
||||
if (oldCard.id != card.id) {
|
||||
throw new RunnerCardIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.cardRepository.save(await card.update(oldCard));
|
||||
return (await this.cardRepository.findOne({ id: id }, { relations: ['runner'] })).toResponse();
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("CARD:DELETE")
|
||||
@ResponseSchema(ResponseRunnerCard)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@ResponseSchema(RunnerCardHasScansError, { statusCode: 406 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: "Delete the card whose id you provided. <br> If no card with this id exists it will just return 204(no content). <br> If the card still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with by this card - please disable it instead or just remove the runner association)." })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let card = await this.cardRepository.findOne({ id: id });
|
||||
if (!card) { return null; }
|
||||
|
||||
const cardScans = (await this.cardRepository.findOne({ id: id }, { relations: ["scans"] })).scans;
|
||||
if (cardScans.length != 0 && !force) {
|
||||
throw new RunnerCardHasScansError();
|
||||
}
|
||||
const scanController = new ScanController;
|
||||
for (let scan of cardScans) {
|
||||
await scanController.remove(scan.id, force);
|
||||
}
|
||||
|
||||
await this.cardRepository.delete(card);
|
||||
return card.toResponse();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { RunnerGroupNeededError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
|
||||
import { RunnerGroupNeededError, RunnerHasDistanceDonationsError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
|
||||
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
|
||||
import { CreateRunner } from '../models/actions/CreateRunner';
|
||||
import { UpdateRunner } from '../models/actions/UpdateRunner';
|
||||
import { CreateRunner } from '../models/actions/create/CreateRunner';
|
||||
import { UpdateRunner } from '../models/actions/update/UpdateRunner';
|
||||
import { Runner } from '../models/entities/Runner';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseRunner } from '../models/responses/ResponseRunner';
|
||||
import { DonationController } from './DonationController';
|
||||
import { RunnerCardController } from './RunnerCardController';
|
||||
import { ScanController } from './ScanController';
|
||||
|
||||
@JsonController('/runners')
|
||||
//@Authorized('RUNNERS:read')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class RunnerController {
|
||||
private runnerRepository: Repository<Runner>;
|
||||
|
||||
@@ -22,11 +25,12 @@ export class RunnerController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("RUNNER:GET")
|
||||
@ResponseSchema(ResponseRunner, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all runners.' })
|
||||
@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'] });
|
||||
const runners = await this.runnerRepository.find({ relations: ['scans', 'group', 'scans.track', 'cards'] });
|
||||
runners.forEach(runner => {
|
||||
responseRunners.push(new ResponseRunner(runner));
|
||||
});
|
||||
@@ -34,38 +38,41 @@ export class RunnerController {
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("RUNNER:GET")
|
||||
@ResponseSchema(ResponseRunner)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerNotFoundError)
|
||||
@OpenAPI({ description: 'Returns a runner of a specified id (if it exists)' })
|
||||
@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'] })
|
||||
let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'scans.track', 'cards'] })
|
||||
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 object (id will be generated automagicly).' })
|
||||
@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();
|
||||
runner = await createRunner.toEntity();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
runner = await this.runnerRepository.save(runner)
|
||||
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group'] }));
|
||||
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'scans.track', 'cards'] }));
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("RUNNER:UPDATE")
|
||||
@ResponseSchema(ResponseRunner)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update a runner object (id can't be changed)." })
|
||||
@OpenAPI({ description: "Update the runner whose id you provided. <br> Please remember that ids can't be changed." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) runner: UpdateRunner) {
|
||||
let oldRunner = await this.runnerRepository.findOne({ id: id }, { relations: ['group'] });
|
||||
|
||||
@@ -77,24 +84,47 @@ export class RunnerController {
|
||||
throw new RunnerIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.runnerRepository.update(oldRunner, await runner.toRunner());
|
||||
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group'] }));
|
||||
await this.runnerRepository.save(await runner.update(oldRunner));
|
||||
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'scans.track', 'cards'] }));
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("RUNNER:DELETE")
|
||||
@ResponseSchema(ResponseRunner)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@ResponseSchema(RunnerHasDistanceDonationsError, { statusCode: 406 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete a specified runner (if it exists).' })
|
||||
@OpenAPI({ description: 'Delete the runner whose id you provided. <br> This will also delete all scans and cards associated with the runner. <br> If no runner with this id exists it will just return 204(no content).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let runner = await this.runnerRepository.findOne({ id: id });
|
||||
if (!runner) { return null; }
|
||||
const responseRunner = await this.runnerRepository.findOne(runner, { relations: ['scans', 'group'] });
|
||||
const responseRunner = await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'scans.track', 'cards'] });
|
||||
|
||||
if (!runner) {
|
||||
throw new RunnerNotFoundError();
|
||||
}
|
||||
|
||||
const runnerDonations = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["distanceDonations"] })).distanceDonations;
|
||||
if (runnerDonations.length > 0 && !force) {
|
||||
throw new RunnerHasDistanceDonationsError();
|
||||
}
|
||||
const donationController = new DonationController();
|
||||
for (let donation of runnerDonations) {
|
||||
await donationController.remove(donation.id, force);
|
||||
}
|
||||
|
||||
const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards;
|
||||
const cardController = new RunnerCardController;
|
||||
for (let card of runnerCards) {
|
||||
await cardController.remove(card.id, force);
|
||||
}
|
||||
|
||||
const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans;
|
||||
const scanController = new ScanController;
|
||||
for (let scan of runnerScans) {
|
||||
await scanController.remove(scan.id, force);
|
||||
}
|
||||
|
||||
await this.runnerRepository.delete(runner);
|
||||
return new ResponseRunner(responseRunner);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
|
||||
import { RunnerOrganisationHasRunnersError, RunnerOrganisationHasTeamsError, RunnerOrganisationIdsNotMatchingError, RunnerOrganisationNotFoundError } from '../errors/RunnerOrganisationErrors';
|
||||
import { CreateRunnerOrganisation } from '../models/actions/CreateRunnerOrganisation';
|
||||
import { CreateRunnerOrganisation } from '../models/actions/create/CreateRunnerOrganisation';
|
||||
import { UpdateRunnerOrganisation } from '../models/actions/update/UpdateRunnerOrganisation';
|
||||
import { RunnerOrganisation } from '../models/entities/RunnerOrganisation';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseRunnerOrganisation } from '../models/responses/ResponseRunnerOrganisation';
|
||||
@@ -12,7 +12,7 @@ import { RunnerTeamController } from './RunnerTeamController';
|
||||
|
||||
|
||||
@JsonController('/organisations')
|
||||
//@Authorized('RUNNERS:read')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class RunnerOrganisationController {
|
||||
private runnerOrganisationRepository: Repository<RunnerOrganisation>;
|
||||
|
||||
@@ -24,11 +24,12 @@ export class RunnerOrganisationController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("ORGANISATION:GET")
|
||||
@ResponseSchema(ResponseRunnerOrganisation, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all runnerOrganisations.' })
|
||||
@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'] });
|
||||
const runners = await this.runnerOrganisationRepository.find({ relations: ['contact', 'teams'] });
|
||||
runners.forEach(runner => {
|
||||
responseTeams.push(new ResponseRunnerOrganisation(runner));
|
||||
});
|
||||
@@ -36,65 +37,68 @@ export class RunnerOrganisationController {
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("ORGANISATION:GET")
|
||||
@ResponseSchema(ResponseRunnerOrganisation)
|
||||
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerOrganisationNotFoundError)
|
||||
@OpenAPI({ description: 'Returns a runnerOrganisation of a specified id (if it exists)' })
|
||||
@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'] });
|
||||
let runnerOrg = await this.runnerOrganisationRepository.findOne({ id: id }, { relations: ['contact', 'teams'] });
|
||||
if (!runnerOrg) { throw new RunnerOrganisationNotFoundError(); }
|
||||
return new ResponseRunnerOrganisation(runnerOrg);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("ORGANISATION:CREATE")
|
||||
@ResponseSchema(ResponseRunnerOrganisation)
|
||||
@OpenAPI({ description: 'Create a new runnerOrganisation object (id will be generated automagicly).' })
|
||||
@OpenAPI({ description: 'Create a new organsisation.' })
|
||||
async post(@Body({ validate: true }) createRunnerOrganisation: CreateRunnerOrganisation) {
|
||||
let runnerOrganisation;
|
||||
try {
|
||||
runnerOrganisation = await createRunnerOrganisation.toRunnerOrganisation();
|
||||
runnerOrganisation = await createRunnerOrganisation.toEntity();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
runnerOrganisation = await this.runnerOrganisationRepository.save(runnerOrganisation);
|
||||
|
||||
return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(runnerOrganisation, { relations: ['address', 'contact', 'teams'] }));
|
||||
return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(runnerOrganisation, { relations: ['contact', 'teams'] }));
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("ORGANISATION:UPDATE")
|
||||
@ResponseSchema(ResponseRunnerOrganisation)
|
||||
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerOrganisationIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update a runnerOrganisation object (id can't be changed)." })
|
||||
async put(@Param('id') id: number, @EntityFromBody() runnerOrganisation: RunnerOrganisation) {
|
||||
@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 != runnerOrganisation.id) {
|
||||
if (oldRunnerOrganisation.id != updateOrganisation.id) {
|
||||
throw new RunnerOrganisationIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.runnerOrganisationRepository.update(oldRunnerOrganisation, runnerOrganisation);
|
||||
await this.runnerOrganisationRepository.save(await updateOrganisation.update(oldRunnerOrganisation));
|
||||
|
||||
runnerOrganisation = await this.runnerOrganisationRepository.findOne(runnerOrganisation, { relations: ['address', 'contact', 'teams'] });
|
||||
return new ResponseRunnerOrganisation(runnerOrganisation);
|
||||
return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(id, { relations: ['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 a specified runnerOrganisation (if it exists).' })
|
||||
@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> This won\'t delete the associated contact. <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'] });
|
||||
let runnerOrganisation = await this.runnerOrganisationRepository.findOne(organisation, { relations: ['contact', 'runners', 'teams'] });
|
||||
|
||||
if (!force) {
|
||||
if (runnerOrganisation.teams.length != 0) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { RunnerTeamHasRunnersError, RunnerTeamIdsNotMatchingError, RunnerTeamNotFoundError } from '../errors/RunnerTeamErrors';
|
||||
import { CreateRunnerTeam } from '../models/actions/CreateRunnerTeam';
|
||||
import { UpdateRunnerTeam } from '../models/actions/UpdateRunnerTeam';
|
||||
import { CreateRunnerTeam } from '../models/actions/create/CreateRunnerTeam';
|
||||
import { UpdateRunnerTeam } from '../models/actions/update/UpdateRunnerTeam';
|
||||
import { RunnerTeam } from '../models/entities/RunnerTeam';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseRunnerTeam } from '../models/responses/ResponseRunnerTeam';
|
||||
@@ -11,7 +11,7 @@ import { RunnerController } from './RunnerController';
|
||||
|
||||
|
||||
@JsonController('/teams')
|
||||
//@Authorized('RUNNERS:read')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class RunnerTeamController {
|
||||
private runnerTeamRepository: Repository<RunnerTeam>;
|
||||
|
||||
@@ -23,8 +23,9 @@ export class RunnerTeamController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("TEAM:GET")
|
||||
@ResponseSchema(ResponseRunnerTeam, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all runnerTeams.' })
|
||||
@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'] });
|
||||
@@ -35,10 +36,11 @@ export class RunnerTeamController {
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("TEAM:GET")
|
||||
@ResponseSchema(ResponseRunnerTeam)
|
||||
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(RunnerTeamNotFoundError)
|
||||
@OpenAPI({ description: 'Returns a runnerTeam of a specified id (if it exists)' })
|
||||
@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(); }
|
||||
@@ -46,12 +48,13 @@ export class RunnerTeamController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("TEAM:CREATE")
|
||||
@ResponseSchema(ResponseRunnerTeam)
|
||||
@OpenAPI({ description: 'Create a new runnerTeam object (id will be generated automagicly).' })
|
||||
@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();
|
||||
runnerTeam = await createRunnerTeam.toEntity();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -63,10 +66,11 @@ export class RunnerTeamController {
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("TEAM:UPDATE")
|
||||
@ResponseSchema(ResponseRunnerTeam)
|
||||
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerTeamIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update a runnerTeam object (id can't be changed)." })
|
||||
@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'] });
|
||||
|
||||
@@ -78,17 +82,18 @@ export class RunnerTeamController {
|
||||
throw new RunnerTeamIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.runnerTeamRepository.update(oldRunnerTeam, await runnerTeam.toRunnerTeam());
|
||||
await this.runnerTeamRepository.save(await runnerTeam.update(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 a specified runnerTeam (if it exists).' })
|
||||
@OpenAPI({ description: 'Delete the team whose id you provided. <br> If the team still has runners associated this will fail. <br> To delete the team with all associated runners set the force QueryParam to true (cascading deletion might take a while). <br> This won\'t delete the associated contact.<br> If no team with this id exists it will just return 204(no content).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let team = await this.runnerTeamRepository.findOne({ id: id });
|
||||
if (!team) { return null; }
|
||||
|
||||
140
src/controllers/ScanController.ts
Normal file
140
src/controllers/ScanController.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam, UseBefore } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { RunnerNotFoundError } from '../errors/RunnerErrors';
|
||||
import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors';
|
||||
import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
|
||||
import ScanAuth from '../middlewares/ScanAuth';
|
||||
import { CreateScan } from '../models/actions/create/CreateScan';
|
||||
import { CreateTrackScan } from '../models/actions/create/CreateTrackScan';
|
||||
import { UpdateScan } from '../models/actions/update/UpdateScan';
|
||||
import { UpdateTrackScan } from '../models/actions/update/UpdateTrackScan';
|
||||
import { Scan } from '../models/entities/Scan';
|
||||
import { TrackScan } from '../models/entities/TrackScan';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseScan } from '../models/responses/ResponseScan';
|
||||
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
|
||||
|
||||
@JsonController('/scans')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class ScanController {
|
||||
private scanRepository: Repository<Scan>;
|
||||
private trackScanRepository: Repository<TrackScan>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.scanRepository = getConnectionManager().get().getRepository(Scan);
|
||||
this.trackScanRepository = getConnectionManager().get().getRepository(TrackScan);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("SCAN:GET")
|
||||
@ResponseSchema(ResponseScan, { isArray: true })
|
||||
@ResponseSchema(ResponseTrackScan, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all scans (normal or track) from all runners. <br> This includes the scan\'s runner\'s distance ran.' })
|
||||
async getAll() {
|
||||
let responseScans: ResponseScan[] = new Array<ResponseScan>();
|
||||
const scans = await this.scanRepository.find({ relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] });
|
||||
scans.forEach(scan => {
|
||||
responseScans.push(scan.toResponse());
|
||||
});
|
||||
return responseScans;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("SCAN:GET")
|
||||
@ResponseSchema(ResponseScan)
|
||||
@ResponseSchema(ResponseTrackScan)
|
||||
@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(ScanNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about the scan whose id got provided. This includes the scan\'s runner\'s distance ran.' })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })
|
||||
if (!scan) { throw new ScanNotFoundError(); }
|
||||
return scan.toResponse();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseBefore(ScanAuth)
|
||||
@ResponseSchema(ResponseScan)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Create a new scan (not track scan - use /scans/trackscans instead). <br> Please rmemember to provide the scan\'s runner\'s id and distance.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async post(@Body({ validate: true }) createScan: CreateScan) {
|
||||
let scan = await createScan.toEntity();
|
||||
scan = await this.scanRepository.save(scan);
|
||||
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
}
|
||||
|
||||
@Post("/trackscans")
|
||||
@UseBefore(ScanAuth)
|
||||
@ResponseSchema(ResponseTrackScan)
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Create a new track scan (for "normal" scans use /scans instead). <br> Please remember that to provide the scan\'s card\'s station\'s id.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan) {
|
||||
let scan = await createScan.toEntity();
|
||||
scan = await this.trackScanRepository.save(scan);
|
||||
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("SCAN:UPDATE")
|
||||
@ResponseSchema(ResponseScan)
|
||||
@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the scan (not track scan use /scans/trackscans/:id instead) whose id you provided. <br> Please remember that ids can't be changed and distances must be positive." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) scan: UpdateScan) {
|
||||
let oldScan = await this.scanRepository.findOne({ id: id });
|
||||
|
||||
if (!oldScan) {
|
||||
throw new ScanNotFoundError();
|
||||
}
|
||||
|
||||
if (oldScan.id != scan.id) {
|
||||
throw new ScanIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.scanRepository.save(await scan.update(oldScan));
|
||||
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
}
|
||||
|
||||
@Put('/trackscans/:id')
|
||||
@Authorized("SCAN:UPDATE")
|
||||
@ResponseSchema(ResponseTrackScan)
|
||||
@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: 'Update the track scan (not "normal" scan use /scans/trackscans/:id instead) whose id you provided. <br> Please remember that only the validity, runner and track can be changed.' })
|
||||
async putTrackScan(@Param('id') id: number, @Body({ validate: true }) scan: UpdateTrackScan) {
|
||||
let oldScan = await this.trackScanRepository.findOne({ id: id });
|
||||
|
||||
if (!oldScan) {
|
||||
throw new ScanNotFoundError();
|
||||
}
|
||||
|
||||
if (oldScan.id != scan.id) {
|
||||
throw new ScanIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.trackScanRepository.save(await scan.update(oldScan));
|
||||
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("SCAN:DELETE")
|
||||
@ResponseSchema(ResponseScan)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the scan whose id you provided. <br> If no scan with this id exists it will just return 204(no content).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let scan = await this.scanRepository.findOne({ id: id });
|
||||
if (!scan) { return null; }
|
||||
const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] });
|
||||
|
||||
await this.scanRepository.delete(scan);
|
||||
return responseScan.toResponse();
|
||||
}
|
||||
}
|
||||
108
src/controllers/ScanStationController.ts
Normal file
108
src/controllers/ScanStationController.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 { ScanStationHasScansError, ScanStationIdsNotMatchingError, ScanStationNotFoundError } from '../errors/ScanStationErrors';
|
||||
import { TrackNotFoundError } from '../errors/TrackErrors';
|
||||
import { CreateScanStation } from '../models/actions/create/CreateScanStation';
|
||||
import { UpdateScanStation } from '../models/actions/update/UpdateScanStation';
|
||||
import { ScanStation } from '../models/entities/ScanStation';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseScanStation } from '../models/responses/ResponseScanStation';
|
||||
import { ScanController } from './ScanController';
|
||||
|
||||
@JsonController('/stations')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class ScanStationController {
|
||||
private stationRepository: Repository<ScanStation>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.stationRepository = getConnectionManager().get().getRepository(ScanStation);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("STATION:GET")
|
||||
@ResponseSchema(ResponseScanStation, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all stations. <br> This includes their associated tracks.' })
|
||||
async getAll() {
|
||||
let responseStations: ResponseScanStation[] = new Array<ResponseScanStation>();
|
||||
const stations = await this.stationRepository.find({ relations: ['track'] });
|
||||
stations.forEach(station => {
|
||||
responseStations.push(station.toResponse());
|
||||
});
|
||||
return responseStations;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("STATION:GET")
|
||||
@ResponseSchema(ResponseScanStation)
|
||||
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(ScanStationNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all information about the station whose id got provided. <br> This includes it\'s associated track.' })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let scan = await this.stationRepository.findOne({ id: id }, { relations: ['track'] })
|
||||
if (!scan) { throw new ScanStationNotFoundError(); }
|
||||
return scan.toResponse();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("STATION:CREATE")
|
||||
@ResponseSchema(ResponseScanStation)
|
||||
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
|
||||
@OpenAPI({ description: 'Create a new station. <br> Please remeber to provide the station\'s track\'s id. <br> Please also remember that the station key is only visibe on creation.' })
|
||||
async post(@Body({ validate: true }) createStation: CreateScanStation) {
|
||||
let newStation = await createStation.toEntity();
|
||||
const station = await this.stationRepository.save(newStation);
|
||||
let responseStation = (await this.stationRepository.findOne({ id: station.id }, { relations: ['track'] })).toResponse();
|
||||
responseStation.key = newStation.cleartextkey;
|
||||
return responseStation;
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("STATION:UPDATE")
|
||||
@ResponseSchema(ResponseScanStation)
|
||||
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(ScanStationIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update the station whose id you provided. <br> Please remember that only the description and enabled state can be changed." })
|
||||
async put(@Param('id') id: number, @Body({ validate: true }) station: UpdateScanStation) {
|
||||
let oldStation = await this.stationRepository.findOne({ id: id });
|
||||
|
||||
if (!oldStation) {
|
||||
throw new ScanStationNotFoundError();
|
||||
}
|
||||
|
||||
if (oldStation.id != station.id) {
|
||||
throw new ScanStationIdsNotMatchingError();
|
||||
}
|
||||
|
||||
await this.stationRepository.save(await station.update(oldStation));
|
||||
return (await this.stationRepository.findOne({ id: id }, { relations: ['track'] })).toResponse();
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("STATION:DELETE")
|
||||
@ResponseSchema(ResponseScanStation)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@ResponseSchema(ScanStationHasScansError, { statusCode: 406 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete the station whose id you provided. <br> If no station with this id exists it will just return 204(no content). <br> If the station still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with/created by this station - please disable it instead).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
let station = await this.stationRepository.findOne({ id: id });
|
||||
if (!station) { return null; }
|
||||
|
||||
const stationScans = (await this.stationRepository.findOne({ id: station.id }, { relations: ["scans"] })).scans;
|
||||
if (stationScans.length != 0 && !force) {
|
||||
throw new ScanStationHasScansError();
|
||||
}
|
||||
const scanController = new ScanController;
|
||||
for (let scan of stationScans) {
|
||||
await scanController.remove(scan.id, force);
|
||||
}
|
||||
|
||||
const responseStation = await this.stationRepository.findOne({ id: station.id }, { relations: ["track"] });
|
||||
await this.stationRepository.delete(station);
|
||||
return responseStation.toResponse();
|
||||
}
|
||||
}
|
||||
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, QueryParam } 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/create/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.toEntity());
|
||||
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, @QueryParam("force") force: boolean) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
31
src/controllers/StatusController.ts
Normal file
31
src/controllers/StatusController.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Get, JsonController } from 'routing-controllers';
|
||||
import { OpenAPI } from 'routing-controllers-openapi';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { config } from '../config';
|
||||
|
||||
@JsonController()
|
||||
export class StatusController {
|
||||
|
||||
@Get('/status')
|
||||
@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": "✔"
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/version')
|
||||
@OpenAPI({ description: "A very basic endpoint that just returns the curent package version." })
|
||||
getVersion() {
|
||||
return {
|
||||
"version": config.version
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
|
||||
import { TrackIdsNotMatchingError, TrackNotFoundError } from "../errors/TrackErrors";
|
||||
import { CreateTrack } from '../models/actions/CreateTrack';
|
||||
import { TrackHasScanStationsError, TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors";
|
||||
import { CreateTrack } from '../models/actions/create/CreateTrack';
|
||||
import { UpdateTrack } from '../models/actions/update/UpdateTrack';
|
||||
import { Track } from '../models/entities/Track';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseTrack } from '../models/responses/ResponseTrack';
|
||||
import { ScanStationController } from './ScanStationController';
|
||||
|
||||
@JsonController('/tracks')
|
||||
//@Authorized("TRACKS:read")
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class TrackController {
|
||||
private trackRepository: Repository<Track>;
|
||||
|
||||
@@ -21,8 +22,9 @@ export class TrackController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("TRACK:GET")
|
||||
@ResponseSchema(ResponseTrack, { isArray: true })
|
||||
@OpenAPI({ description: "Lists all tracks." })
|
||||
@OpenAPI({ description: 'Lists all tracks.' })
|
||||
async getAll() {
|
||||
let responseTracks: ResponseTrack[] = new Array<ResponseTrack>();
|
||||
const tracks = await this.trackRepository.find();
|
||||
@@ -33,10 +35,11 @@ export class TrackController {
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("TRACK:GET")
|
||||
@ResponseSchema(ResponseTrack)
|
||||
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
|
||||
@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." })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let track = await this.trackRepository.findOne({ id: id });
|
||||
if (!track) { throw new TrackNotFoundError(); }
|
||||
@@ -44,44 +47,58 @@ export class TrackController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("TRACK:CREATE")
|
||||
@ResponseSchema(ResponseTrack)
|
||||
@OpenAPI({ description: "Create a new track object (id will be generated automagicly)." })
|
||||
@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 })
|
||||
track: CreateTrack
|
||||
) {
|
||||
return new ResponseTrack(await this.trackRepository.save(track.toTrack()));
|
||||
return new ResponseTrack(await this.trackRepository.save(await track.toEntity()));
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("TRACK:UPDATE")
|
||||
@ResponseSchema(ResponseTrack)
|
||||
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(TrackIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update a track object (id can't be changed)." })
|
||||
async put(@Param('id') id: number, @EntityFromBody() track: Track) {
|
||||
@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 });
|
||||
|
||||
if (!oldTrack) {
|
||||
throw new TrackNotFoundError();
|
||||
}
|
||||
|
||||
if (oldTrack.id != track.id) {
|
||||
if (oldTrack.id != updateTrack.id) {
|
||||
throw new TrackIdsNotMatchingError();
|
||||
}
|
||||
await this.trackRepository.save(await updateTrack.update(oldTrack));
|
||||
|
||||
await this.trackRepository.update(oldTrack, track);
|
||||
return new ResponseTrack(track);
|
||||
return new ResponseTrack(await this.trackRepository.findOne({ id: id }));
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("TRACK:DELETE")
|
||||
@ResponseSchema(ResponseTrack)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: "Delete a specified track (if it exists)." })
|
||||
async remove(@Param("id") id: number) {
|
||||
@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, @QueryParam("force") force: boolean) {
|
||||
let track = await this.trackRepository.findOne({ id: id });
|
||||
if (!track) { return null; }
|
||||
|
||||
const trackStations = (await this.trackRepository.findOne({ id: id }, { relations: ["stations"] })).stations;
|
||||
if (trackStations.length != 0 && !force) {
|
||||
throw new TrackHasScanStationsError();
|
||||
}
|
||||
const stationController = new ScanStationController;
|
||||
for (let station of trackStations) {
|
||||
await stationController.remove(station.id, force);
|
||||
}
|
||||
|
||||
await this.trackRepository.delete(track);
|
||||
return new ResponseTrack(track);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
|
||||
import { UserIdsNotMatchingError, UserNotFoundError } from '../errors/UserErrors';
|
||||
import { UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors';
|
||||
import { UserGroupNotFoundError } from '../errors/UserGroupErrors';
|
||||
import { CreateUser } from '../models/actions/CreateUser';
|
||||
import { CreateUser } from '../models/actions/create/CreateUser';
|
||||
import { UpdateUser } from '../models/actions/update/UpdateUser';
|
||||
import { User } from '../models/entities/User';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseUser } from '../models/responses/ResponseUser';
|
||||
import { ResponseUserPermissions } from '../models/responses/ResponseUserPermissions';
|
||||
import { PermissionController } from './PermissionController';
|
||||
|
||||
|
||||
@JsonController('/users')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class UserController {
|
||||
private userRepository: Repository<User>;
|
||||
|
||||
@@ -21,68 +25,101 @@ export class UserController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ResponseSchema(User, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all users.' })
|
||||
getAll() {
|
||||
return this.userRepository.find();
|
||||
@Authorized("USER:GET")
|
||||
@ResponseSchema(ResponseUser, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all users. <br> This includes their groups and permissions granted to them.' })
|
||||
async getAll() {
|
||||
let responseUsers: ResponseUser[] = new Array<ResponseUser>();
|
||||
const users = await this.userRepository.find({ relations: ['permissions', 'groups', 'groups.permissions'] });
|
||||
users.forEach(user => {
|
||||
responseUsers.push(new ResponseUser(user));
|
||||
});
|
||||
return responseUsers;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@ResponseSchema(User)
|
||||
@Authorized("USER:GET")
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(UserNotFoundError)
|
||||
@OpenAPI({ description: 'Returns a user of a specified id (if it exists)' })
|
||||
getOne(@Param('id') id: number) {
|
||||
return this.userRepository.findOne({ id: id });
|
||||
@OpenAPI({ description: 'Lists all information about the user whose id got provided. <br> Please remember that all permissions granted to the user will show up here.' })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] })
|
||||
if (!user) { throw new UserNotFoundError(); }
|
||||
return new ResponseUser(user);
|
||||
}
|
||||
|
||||
@Get('/:id/permissions')
|
||||
@Authorized("USER:GET")
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(UserNotFoundError)
|
||||
@OpenAPI({ description: 'Lists all permissions granted to the user sorted into directly granted and inherited as permission response objects.' })
|
||||
async getPermissions(@Param('id') id: number) {
|
||||
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
|
||||
if (!user) { throw new UserNotFoundError(); }
|
||||
return new ResponseUserPermissions(user);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ResponseSchema(User)
|
||||
@ResponseSchema(UserGroupNotFoundError)
|
||||
@OpenAPI({ description: 'Create a new user object (id will be generated automagicly).' })
|
||||
@Authorized("USER:CREATE")
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
|
||||
@OpenAPI({ description: 'Create a new user. <br> If you want to grant permissions to the user you have to create them seperately by posting to /api/permissions after creating the user.' })
|
||||
async post(@Body({ validate: true }) createUser: CreateUser) {
|
||||
let user;
|
||||
try {
|
||||
user = await createUser.toUser();
|
||||
user = await createUser.toEntity();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return this.userRepository.save(user);
|
||||
user = await this.userRepository.save(user)
|
||||
return new ResponseUser(await this.userRepository.findOne({ id: user.id }, { relations: ['permissions', 'groups', 'groups.permissions'] }));
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@ResponseSchema(User)
|
||||
@Authorized("USER:UPDATE")
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update a user object (id can't be changed)." })
|
||||
async put(@Param('id') id: number, @EntityFromBody() user: User) {
|
||||
@ResponseSchema(UsernameContainsIllegalCharacterError, { 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 != user.id) {
|
||||
if (oldUser.id != updateUser.id) {
|
||||
throw new UserIdsNotMatchingError();
|
||||
}
|
||||
await this.userRepository.save(await updateUser.update(oldUser));
|
||||
|
||||
await this.userRepository.update(oldUser, user);
|
||||
return user;
|
||||
return new ResponseUser(await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] }));
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@ResponseSchema(User)
|
||||
@Authorized("USER:DELETE")
|
||||
@ResponseSchema(ResponseUser)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete a specified runner (if it exists).' })
|
||||
async remove(@Param("id") id: number) {
|
||||
@OpenAPI({ description: 'Delete the user whose id you provided. <br> You have to confirm your decision by providing the ?force=true query param. <br> If there are any permissions directly granted to the user they will get deleted as well. <br> If no user with this id exists it will just return 204(no content).' })
|
||||
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||
if (!force) { throw new UserDeletionNotConfirmedError; }
|
||||
let user = await this.userRepository.findOne({ id: id });
|
||||
if (!user) {
|
||||
return null;
|
||||
if (!user) { return null; }
|
||||
const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] });;
|
||||
|
||||
const permissionControler = new PermissionController();
|
||||
for (let permission of responseUser.permissions) {
|
||||
await permissionControler.remove(permission.id, true);
|
||||
}
|
||||
|
||||
await this.userRepository.delete(user);
|
||||
return user;
|
||||
return new ResponseUser(responseUser);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
|
||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnectionManager, Repository } from 'typeorm';
|
||||
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
|
||||
import { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/UserGroupErrors';
|
||||
import { CreateUserGroup } from '../models/actions/CreateUserGroup';
|
||||
import { CreateUserGroup } from '../models/actions/create/CreateUserGroup';
|
||||
import { UpdateUserGroup } from '../models/actions/update/UpdateUserGroup';
|
||||
import { UserGroup } from '../models/entities/UserGroup';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseUserGroup } from '../models/responses/ResponseUserGroup';
|
||||
import { PermissionController } from './PermissionController';
|
||||
|
||||
|
||||
@JsonController('/usergroups')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class UserGroupController {
|
||||
private userGroupsRepository: Repository<UserGroup>;
|
||||
|
||||
@@ -20,29 +24,32 @@ export class UserGroupController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("USERGROUP:GET")
|
||||
@ResponseSchema(UserGroup, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all usergroups.' })
|
||||
@OpenAPI({ description: 'Lists all groups. <br> The information provided might change while the project continues to evolve.' })
|
||||
getAll() {
|
||||
return this.userGroupsRepository.find();
|
||||
return this.userGroupsRepository.find({ relations: ["permissions"] });
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("USERGROUP:GET")
|
||||
@ResponseSchema(UserGroup)
|
||||
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(UserGroupNotFoundError)
|
||||
@OpenAPI({ description: 'Returns a usergroup of a specified id (if it exists)' })
|
||||
@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 });
|
||||
return this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("USERGROUP:CREATE")
|
||||
@ResponseSchema(UserGroup)
|
||||
@ResponseSchema(UserGroupNotFoundError)
|
||||
@OpenAPI({ description: 'Create a new usergroup object (id will be generated automagicly).' })
|
||||
@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();
|
||||
userGroup = await createUserGroup.toEntity();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -51,37 +58,43 @@ export class UserGroupController {
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@Authorized("USERGROUP:UPDATE")
|
||||
@ResponseSchema(UserGroup)
|
||||
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
|
||||
@ResponseSchema(UserGroupIdsNotMatchingError, { statusCode: 406 })
|
||||
@OpenAPI({ description: "Update a usergroup object (id can't be changed)." })
|
||||
async put(@Param('id') id: number, @EntityFromBody() userGroup: UserGroup) {
|
||||
let oldUserGroup = await this.userGroupsRepository.findOne({ id: id });
|
||||
@OpenAPI({ description: "Update the group whose id you provided. <br> To change the permissions granted to the group please use /api/permissions instead. <br> Please remember that ids can't be changed." })
|
||||
async put(@Param('id') id: number, @EntityFromBody() updateGroup: UpdateUserGroup) {
|
||||
let oldGroup = await this.userGroupsRepository.findOne({ id: id });
|
||||
|
||||
if (!oldUserGroup) {
|
||||
throw new UserGroupNotFoundError()
|
||||
if (!oldGroup) {
|
||||
throw new UserGroupNotFoundError();
|
||||
}
|
||||
|
||||
if (oldUserGroup.id != userGroup.id) {
|
||||
if (oldGroup.id != updateGroup.id) {
|
||||
throw new UserGroupIdsNotMatchingError();
|
||||
}
|
||||
await this.userGroupsRepository.save(await updateGroup.update(oldGroup));
|
||||
|
||||
await this.userGroupsRepository.update(oldUserGroup, userGroup);
|
||||
return userGroup;
|
||||
return (await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] })).toResponse();
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@ResponseSchema(UserGroup)
|
||||
@Authorized("USERGROUP:DELETE")
|
||||
@ResponseSchema(ResponseUserGroup)
|
||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||
@OnUndefined(204)
|
||||
@OpenAPI({ description: 'Delete a specified usergroup (if it exists).' })
|
||||
async remove(@Param("id") id: number) {
|
||||
let group = await this.userGroupsRepository.findOne({ id: id });
|
||||
if (!group) {
|
||||
return null;
|
||||
@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 permissionController = new PermissionController();
|
||||
for (let permission of responseGroup.permissions) {
|
||||
await permissionController.remove(permission.id, true);
|
||||
}
|
||||
|
||||
await this.userGroupsRepository.delete(group);
|
||||
return group;
|
||||
return new ResponseUserGroup(responseGroup);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,57 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
import { BadRequestError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw, when to provided address doesn't belong to the accepted types.
|
||||
* Error to throw when an address's postal code fails validation.
|
||||
*/
|
||||
export class AddressWrongTypeError extends NotAcceptableError {
|
||||
export class AddressPostalCodeInvalidError extends BadRequestError {
|
||||
@IsString()
|
||||
name = "AddressWrongTypeError"
|
||||
name = "AddressPostalCodeInvalidError"
|
||||
|
||||
@IsString()
|
||||
message = "The address must be an existing adress's id. \n You provided a object of another type."
|
||||
message = "The postal code you provided is invalid. \n Please check if your postal code follows the postal code validation guidelines."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw, when a non-existant address get's loaded.
|
||||
* Error to throw when an non-empty address's first line isn't set.
|
||||
*/
|
||||
export class AddressNotFoundError extends NotFoundError {
|
||||
export class AddressFirstLineEmptyError extends BadRequestError {
|
||||
@IsString()
|
||||
name = "AddressNotFoundError"
|
||||
name = "AddressFirstLineEmptyError"
|
||||
|
||||
@IsString()
|
||||
message = "The address you provided couldn't be located in the system. \n Please check your request."
|
||||
message = "You provided a empty first address line. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when an non-empty address's postal code isn't set.
|
||||
*/
|
||||
export class AddressPostalCodeEmptyError extends BadRequestError {
|
||||
@IsString()
|
||||
name = "AddressPostalCodeEmptyError"
|
||||
|
||||
@IsString()
|
||||
message = "You provided a empty postal code. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when an non-empty address's city isn't set.
|
||||
*/
|
||||
export class AddressCityEmptyError extends BadRequestError {
|
||||
@IsString()
|
||||
name = "AddressCityEmptyError"
|
||||
|
||||
@IsString()
|
||||
message = "You provided a empty city. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when an non-empty address's country isn't set.
|
||||
*/
|
||||
export class AddressCountryEmptyError extends BadRequestError {
|
||||
@IsString()
|
||||
name = "AddressCountryEmptyError"
|
||||
|
||||
@IsString()
|
||||
message = "You provided a empty country. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
|
||||
}
|
||||
@@ -1,63 +1,57 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { ForbiddenError, NotAcceptableError, NotFoundError, UnauthorizedError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a jwt is expired.
|
||||
*/
|
||||
export class ExpiredJWTError extends UnauthorizedError {
|
||||
@IsString()
|
||||
name = "ExpiredJWTError"
|
||||
|
||||
@IsString()
|
||||
message = "your provided jwt is expired"
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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()
|
||||
@@ -68,47 +62,48 @@ export class UsernameOrEmailNeededError extends NotAcceptableError {
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when no password is provided.
|
||||
* 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"
|
||||
message = "No password is provided - you need to provide it."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when no user could be found mating the provided credential.
|
||||
* Error to throw when no user could be found for a certain query.
|
||||
*/
|
||||
export class UserNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
name = "UserNotFoundError"
|
||||
|
||||
@IsString()
|
||||
message = "no user could be found for provided credential"
|
||||
message = "The user you provided couldn't be located in the system. \n Please check your request."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when no jwt token was provided (but one had to be).
|
||||
* 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 token was provided"
|
||||
message = "No jwt was provided."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when user was not found or refresh token count was invalid.
|
||||
* 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 refresh token count was invalid"
|
||||
message = "User was not found or the refresh token count is invalid."
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,5 +114,27 @@ export class RefreshTokenCountInvalidError extends NotAcceptableError {
|
||||
name = "RefreshTokenCountInvalidError"
|
||||
|
||||
@IsString()
|
||||
message = "refresh token count was invalid"
|
||||
message = "Refresh token count is invalid."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when someone tries 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."
|
||||
}
|
||||
25
src/errors/DonationErrors.ts
Normal file
25
src/errors/DonationErrors.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a Donation couldn't be found.
|
||||
*/
|
||||
export class DonationNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
name = "DonationNotFoundError"
|
||||
|
||||
@IsString()
|
||||
message = "Donation not found!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two Donations' ids don't match.
|
||||
* Usually occurs when a user tries to change a Donation's id.
|
||||
*/
|
||||
export class DonationIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "DonationIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The ids don't match! \n And if you wanted to change a Donation's id: This isn't allowed!"
|
||||
}
|
||||
47
src/errors/DonorErrors.ts
Normal file
47
src/errors/DonorErrors.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
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."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a donor still has donations associated.
|
||||
*/
|
||||
export class DonorHasDonationsError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "DonorHasDonationsError"
|
||||
|
||||
@IsString()
|
||||
message = "This donor still has donations associated with it. \n If you want to delete this donor with all it's donations and teams add `?force` to your query."
|
||||
}
|
||||
@@ -2,18 +2,7 @@ import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw, when a provided groupContact doesn't belong to the accepted types.
|
||||
*/
|
||||
export class GroupContactWrongTypeError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "GroupContactWrongTypeError"
|
||||
|
||||
@IsString()
|
||||
message = "The groupContact must be an existing groupContact's id. \n You provided a object of another type."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw, when a non-existant groupContact get's loaded.
|
||||
* Error to throw, when a non-existent contact get's requested.
|
||||
*/
|
||||
export class GroupContactNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
@@ -21,4 +10,16 @@ export class GroupContactNotFoundError extends NotFoundError {
|
||||
|
||||
@IsString()
|
||||
message = "The groupContact you provided couldn't be located in the system. \n Please check your request."
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two contacts' ids don't match.
|
||||
* Usually occurs when a user tries to change a contact's id.
|
||||
*/
|
||||
export class GroupContactIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "GroupContactIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The ids don't match! \n And if you wanted to change a contact's id: This isn't allowed!"
|
||||
}
|
||||
|
||||
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 runner organization doesn't belong to the accepted types.
|
||||
*/
|
||||
export class PrincipalWrongTypeError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "PrincipalWrongTypeError"
|
||||
|
||||
@IsString()
|
||||
message = "The principal must have an existing principal's id. \n You provided a object of another type."
|
||||
}
|
||||
48
src/errors/RunnerCardErrors.ts
Normal file
48
src/errors/RunnerCardErrors.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a card couldn't be found.
|
||||
*/
|
||||
export class RunnerCardNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
name = "RunnerCardNotFoundError"
|
||||
|
||||
@IsString()
|
||||
message = "Card not found!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two cards' ids don't match.
|
||||
* Usually occurs when a user tries to change a card's id.
|
||||
*/
|
||||
export class RunnerCardIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerCardIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The ids don't match! \n And if you wanted to change a cards's id: This isn't allowed"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a card still has scans associated.
|
||||
*/
|
||||
export class RunnerCardHasScansError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerCardHasScansError"
|
||||
|
||||
@IsString()
|
||||
message = "This card still has scans associated with it. \n If you want to delete this card with all it's scans add `?force` to your query. \n Otherwise please consider just disabling it."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a card's id is too big to generate a ean-13 barcode for it.
|
||||
* This error should never reach a end user.
|
||||
*/
|
||||
export class RunnerCardIdOutOfRangeError extends Error {
|
||||
@IsString()
|
||||
name = "RunnerCardIdOutOfRangeError"
|
||||
|
||||
@IsString()
|
||||
message = "The card's id is too big to fit into a ean-13 barcode. \n This has a very low probability of happening but means that you might want to switch your barcode format for something that can accept numbers over 9999999999."
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a runner couldn't be found.
|
||||
* Implemented this ways to work with the json-schema conversion for openapi.
|
||||
*/
|
||||
export class RunnerNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
@@ -16,14 +15,13 @@ export class RunnerNotFoundError extends NotFoundError {
|
||||
/**
|
||||
* Error to throw when two runners' ids don't match.
|
||||
* Usually occurs when a user tries to change a runner's id.
|
||||
* Implemented this ways to work with the json-schema conversion for openapi.
|
||||
*/
|
||||
export class RunnerIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The id's don't match!! \n And if you wanted to change a runner's id: This isn't allowed"
|
||||
message = "The ids don't match! \n And if you wanted to change a runner's id: This isn't allowed!"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,5 +32,16 @@ export class RunnerGroupNeededError extends NotAcceptableError {
|
||||
name = "RunnerGroupNeededError"
|
||||
|
||||
@IsString()
|
||||
message = "Runner's need to be part of one group (team or organisiation)! \n You provided neither."
|
||||
message = "Runner's need to be part of one group (team or organisation)! \n You provided neither."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a runner still has distance donations associated.
|
||||
*/
|
||||
export class RunnerHasDistanceDonationsError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerHasDistanceDonationsError"
|
||||
|
||||
@IsString()
|
||||
message = "This runner still has distance donations associated with it. \n If you want to delete this runner with all it's donations and teams add `?force` to your query."
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a runner group couldn't be found.
|
||||
* Implemented this ways to work with the json-schema conversion for openapi.
|
||||
*/
|
||||
export class RunnerGroupNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
|
||||
@@ -3,7 +3,6 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a runner organisation couldn't be found.
|
||||
* Implemented this ways to work with the json-schema conversion for openapi.
|
||||
*/
|
||||
export class RunnerOrganisationNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
@@ -14,40 +13,37 @@ export class RunnerOrganisationNotFoundError extends NotFoundError {
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two runner organisations' ids don't match.
|
||||
* Usually occurs when a user tries to change a runner's id.
|
||||
* Implemented this way to work with the json-schema conversion for openapi.
|
||||
* Error to throw when two runner organisation's ids don't match.
|
||||
* Usually occurs when a user tries to change a runner organisation's id.
|
||||
*/
|
||||
export class RunnerOrganisationIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerOrganisationIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The id's don't match!! \n And if you wanted to change a runner's id: This isn't allowed"
|
||||
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.
|
||||
* Implemented this waysto work with the json-schema conversion for openapi.
|
||||
*/
|
||||
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 ass `?force` to your query."
|
||||
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 runners associated.
|
||||
* Implemented this waysto work with the json-schema conversion for openapi.
|
||||
* 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 ass `?force` to your query."
|
||||
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."
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a runner team couldn't be found.
|
||||
* Implemented this ways to work with the json-schema conversion for openapi.
|
||||
*/
|
||||
export class RunnerTeamNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
@@ -15,32 +14,29 @@ export class RunnerTeamNotFoundError extends NotFoundError {
|
||||
|
||||
/**
|
||||
* Error to throw when two runner teams' ids don't match.
|
||||
* Usually occurs when a user tries to change a runner's id.
|
||||
* Implemented this way to work with the json-schema conversion for openapi.
|
||||
* Usually occurs when a user tries to change a runner team's id.
|
||||
*/
|
||||
export class RunnerTeamIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "RunnerTeamIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The id's don't match!! \n And if you wanted to change a runner's id: This isn't allowed"
|
||||
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.
|
||||
* Implemented this waysto work with the json-schema conversion for openapi.
|
||||
*/
|
||||
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 ass `?force` to your query."
|
||||
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.
|
||||
* Implemented this waysto work with the json-schema conversion for openapi.
|
||||
*/
|
||||
export class RunnerTeamNeedsParentError extends NotAcceptableError {
|
||||
@IsString()
|
||||
|
||||
25
src/errors/ScanErrors.ts
Normal file
25
src/errors/ScanErrors.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a Scan couldn't be found.
|
||||
*/
|
||||
export class ScanNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
name = "ScanNotFoundError"
|
||||
|
||||
@IsString()
|
||||
message = "Scan not found!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two Scans' ids don't match.
|
||||
* Usually occurs when a user tries to change a Scan's id.
|
||||
*/
|
||||
export class ScanIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "ScanIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The ids don't match! \n And if you wanted to change a Scan's id: This isn't allowed!"
|
||||
}
|
||||
36
src/errors/ScanStationErrors.ts
Normal file
36
src/errors/ScanStationErrors.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw, when a non-existent scan station get's loaded.
|
||||
*/
|
||||
export class ScanStationNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
name = "ScanStationNotFoundError"
|
||||
|
||||
@IsString()
|
||||
message = "The scan station you provided couldn't be located in the system. \n Please check your request."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two scan stations' ids don't match.
|
||||
* Usually occurs when a user tries to change a scan station's id.
|
||||
*/
|
||||
export class ScanStationIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "ScanStationIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The ids don't match! \n And if you wanted to change a scan station's id: This isn't allowed!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a station still has scans associated.
|
||||
*/
|
||||
export class ScanStationHasScansError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "ScanStationHasScansError"
|
||||
|
||||
@IsString()
|
||||
message = "This station still has scans associated with it. \n If you want to delete this station with all it's scans add `?force` to your query."
|
||||
}
|
||||
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-existent 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!"
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { JsonController, Param, Body, Get, Post, Put, Delete, NotFoundError, OnUndefined, NotAcceptableError } from 'routing-controllers';
|
||||
import { IsInt, IsNotEmpty, IsPositive, IsString } from 'class-validator';
|
||||
import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when a track couldn't be found.
|
||||
* Implemented this ways to work with the json-schema conversion for openapi.
|
||||
*/
|
||||
export class TrackNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
@@ -16,12 +15,30 @@ export class TrackNotFoundError extends NotFoundError {
|
||||
/**
|
||||
* Error to throw when two tracks' ids don't match.
|
||||
* Usually occurs when a user tries to change a track's id.
|
||||
* Implemented this ways to work with the json-schema conversion for openapi.
|
||||
*/
|
||||
export class TrackIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "TrackIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The id's don't match!! \n And if you wanted to change a track's id: This isn't allowed"
|
||||
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."
|
||||
}
|
||||
|
||||
export class TrackHasScanStationsError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "TrackHasScanStationsError"
|
||||
|
||||
@IsString()
|
||||
message = "This track still has stations associated with it. \n If you want to delete this track with all it's stations and scans add `?force` to your query."
|
||||
}
|
||||
@@ -3,14 +3,39 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
|
||||
/**
|
||||
* Error to throw when no username or email is set
|
||||
* Error to throw when no username or email is set.
|
||||
* We somehow need to identify you on login.
|
||||
*/
|
||||
export class UsernameOrEmailNeededError extends NotFoundError {
|
||||
@IsString()
|
||||
name = "UsernameOrEmailNeededError"
|
||||
|
||||
@IsString()
|
||||
message = "no username or email is set!"
|
||||
message = "No username or email is set!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when no username contains illegal characters.
|
||||
* Right now the only one is "@" but this could change in the future.
|
||||
*/
|
||||
export class UsernameContainsIllegalCharacterError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "UsernameContainsIllegalCharacterError"
|
||||
|
||||
@IsString()
|
||||
message = "The provided username contains illegal characters! \n Right now the following characters are considered illegal: '@'"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when no email is set.
|
||||
* We somehow need to identify you :)
|
||||
*/
|
||||
export class UserEmailNeededError extends NotFoundError {
|
||||
@IsString()
|
||||
name = "UserEmailNeededError"
|
||||
|
||||
@IsString()
|
||||
message = "No email is set! \n You have to provide email addresses for users (used for password reset among others)."
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,5 +58,17 @@ export class UserIdsNotMatchingError extends NotAcceptableError {
|
||||
name = "UserIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The id's don't match!! \n And if you wanted to change a user's id: This isn't allowed"
|
||||
message = "The ids don't match!! \n And if you wanted to change a user's id: This isn't allowed!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two users' ids don't match.
|
||||
* Usually occurs when a user tries to change a user's id.
|
||||
*/
|
||||
export class UserDeletionNotConfirmedError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "UserDeletionNotConfirmedError"
|
||||
|
||||
@IsString()
|
||||
message = "You are trying to delete a user! \n If you're sure about doing this: provide the ?force=true query param."
|
||||
}
|
||||
@@ -2,18 +2,18 @@ import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw when no groupname is set
|
||||
* Error to throw when no group name is set.
|
||||
*/
|
||||
export class GroupNameNeededError extends NotFoundError {
|
||||
@IsString()
|
||||
name = "GroupNameNeededError"
|
||||
|
||||
@IsString()
|
||||
message = "no groupname is set!"
|
||||
message = "No name is set for this group!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when a usergroup couldn't be found.
|
||||
* Error to throw when a user group couldn't be found.
|
||||
*/
|
||||
export class UserGroupNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
@@ -24,13 +24,13 @@ export class UserGroupNotFoundError extends NotFoundError {
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two usergroups' ids don't match.
|
||||
* Usually occurs when a user tries to change a usergroups's id.
|
||||
* Error to throw when two user groups' ids don't match.
|
||||
* Usually occurs when a user tries to change a user groups's id.
|
||||
*/
|
||||
export class UserGroupIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "UserGroupIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The id's don't match!! \n If you wanted to change a usergroup's id: This isn't allowed"
|
||||
message = "The ids don't match!! \n If you wanted to change a user group's id: This isn't allowed!"
|
||||
}
|
||||
111
src/jwtcreator.ts
Normal file
111
src/jwtcreator.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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 = user.allPermissions;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ 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 () => {
|
||||
const connection = await createConnection();
|
||||
|
||||
@@ -2,10 +2,12 @@ import cookieParser from "cookie-parser";
|
||||
import { Application } from "express";
|
||||
/**
|
||||
* Loader for express related configurations.
|
||||
* Currently only enables the proxy trust.
|
||||
* Configures proxy trusts, globally used middlewares and other express features.
|
||||
*/
|
||||
export default async (app: Application) => {
|
||||
app.enable('trust proxy');
|
||||
app.disable('x-powered-by');
|
||||
app.disable('x-served-by');
|
||||
app.use(cookieParser());
|
||||
return app;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import openapiLoader from "./openapi";
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
await databaseLoader();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { validationMetadatasToSchemas } from "class-validator-jsonschema";
|
||||
import { Application } from "express";
|
||||
import { validationMetadatasToSchemas } from "@odit/class-validator-jsonschema";
|
||||
import express, { Application } from "express";
|
||||
import path from 'path';
|
||||
import { getMetadataArgsStorage } from "routing-controllers";
|
||||
import { routingControllersToSpec } from "routing-controllers-openapi";
|
||||
import * as swaggerUiExpress from "swagger-ui-express";
|
||||
import { generateSpec } from '../apispec';
|
||||
|
||||
/**
|
||||
* Loader for everything openapi related - from creating the schema to serving it via a static route.
|
||||
* 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) => {
|
||||
const storage = getMetadataArgsStorage();
|
||||
@@ -14,41 +15,10 @@ export default async (app: Application) => {
|
||||
});
|
||||
|
||||
//Spec creation based on the previously created schemas
|
||||
const spec = routingControllersToSpec(
|
||||
storage,
|
||||
{
|
||||
routePrefix: "/api"
|
||||
},
|
||||
{
|
||||
components: {
|
||||
schemas,
|
||||
"securitySchemes": {
|
||||
"AuthToken": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}
|
||||
}
|
||||
},
|
||||
info: {
|
||||
description: "The the backend API for the LfK! runner system.",
|
||||
title: "LfK! Backend API",
|
||||
version: "1.0.0",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//Options for swaggerUiExpress
|
||||
const options = {
|
||||
explorer: true,
|
||||
};
|
||||
app.use(
|
||||
"/api/docs",
|
||||
swaggerUiExpress.serve,
|
||||
swaggerUiExpress.setup(spec, options)
|
||||
);
|
||||
app.get(["/api/openapi.json", "/api/swagger.json"], (req, res) => {
|
||||
const spec = generateSpec(storage, schemas);
|
||||
app.get(["/api/docs/openapi.json", "/api/docs/swagger.json"], (req, res) => {
|
||||
res.json(spec);
|
||||
});
|
||||
app.use('/api/docs', express.static(path.join(__dirname, '../static/docs'), { index: "index.html", extensions: ['html'] }));
|
||||
return app;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExpressErrorMiddlewareInterface, Middleware } from "routing-controllers";
|
||||
|
||||
/**
|
||||
* Our Error handling middlware that returns our custom httperrors to the user
|
||||
* Our Error handling middlware that returns our custom httperrors to the user.
|
||||
*/
|
||||
@Middleware({ type: "after" })
|
||||
export class ErrorHandler implements ExpressErrorMiddlewareInterface {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
/**
|
||||
* Custom express middleware that appends the raw body to the request object.
|
||||
* Mainly used for parsing csvs from bodies.
|
||||
*/
|
||||
|
||||
const RawBodyMiddleware = (req: Request, res: Response, next: () => void) => {
|
||||
const body = []
|
||||
req.on('data', chunk => {
|
||||
@@ -8,15 +13,6 @@ const RawBodyMiddleware = (req: Request, res: Response, next: () => void) => {
|
||||
req.on('end', () => {
|
||||
const rawBody = Buffer.concat(body)
|
||||
req['rawBody'] = rawBody
|
||||
/*
|
||||
switch (req.header('content-type')) {
|
||||
case 'application/json':
|
||||
req.body = JSON.parse(rawBody.toString())
|
||||
break
|
||||
// add more body parsing if needs be
|
||||
default:
|
||||
}
|
||||
*/
|
||||
next()
|
||||
})
|
||||
req.on('error', () => {
|
||||
|
||||
69
src/middlewares/ScanAuth.ts
Normal file
69
src/middlewares/ScanAuth.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as argon2 from "argon2";
|
||||
import { Request, Response } from 'express';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { ScanStation } from '../models/entities/ScanStation';
|
||||
import authchecker from './authchecker';
|
||||
|
||||
/**
|
||||
* This middleware handles the authentication of scan station api tokens.
|
||||
* The tokens have to be provided via Bearer authorization header.
|
||||
* You have to manually use this middleware via @UseBefore(ScanAuth) instead of using @Authorized().
|
||||
* @param req Express request object.
|
||||
* @param res Express response object.
|
||||
* @param next Next function to call on success.
|
||||
*/
|
||||
const ScanAuth = async (req: Request, res: Response, next: () => void) => {
|
||||
let provided_token: string = req.headers["authorization"];
|
||||
if (provided_token == "" || provided_token === undefined || provided_token === null) {
|
||||
res.status(401).send("No api token provided.");
|
||||
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-existent or invalid syntax.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const station = await getConnectionManager().get().getRepository(ScanStation).findOne({ prefix: prefix });
|
||||
if (!station) {
|
||||
let user_authorized = false;
|
||||
try {
|
||||
let action = { request: req, response: res, context: null, next: next }
|
||||
user_authorized = await authchecker(action, ["SCAN:CREATE"]);
|
||||
}
|
||||
finally {
|
||||
if (user_authorized == false) {
|
||||
res.status(401).send("Api token non-existent or invalid syntax.");
|
||||
return;
|
||||
}
|
||||
else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (station.enabled == false) {
|
||||
res.status(401).send("Station disabled.");
|
||||
}
|
||||
if (!(await argon2.verify(station.key, provided_token))) {
|
||||
res.status(401).send("Api token invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
export default ScanAuth;
|
||||
66
src/middlewares/StatsAuth.ts
Normal file
66
src/middlewares/StatsAuth.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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 handles the authentication of stats client api tokens.
|
||||
* The tokens have to be provided via Bearer authorization header.
|
||||
* You have to manually use this middleware via @UseBefore(StatsAuth) instead of using @Authorized().
|
||||
* @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;
|
||||
58
src/middlewares/UserChecker.ts
Normal file
58
src/middlewares/UserChecker.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import cookie from "cookie";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import { Action } from 'routing-controllers';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { config } from '../config';
|
||||
import { IllegalJWTError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from '../errors/AuthError';
|
||||
import { JwtCreator, JwtUser } from '../jwtcreator';
|
||||
import { User } from '../models/entities/User';
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
*/
|
||||
const UserChecker = async (action: Action) => {
|
||||
let jwtPayload = undefined
|
||||
try {
|
||||
let provided_token = "" + action.request.headers["authorization"].replace("Bearer ", "");
|
||||
jwtPayload = <any>jwt.verify(provided_token, config.jwt_secret);
|
||||
jwtPayload = jwtPayload["userdetails"];
|
||||
} catch (error) {
|
||||
jwtPayload = await refresh(action);
|
||||
}
|
||||
|
||||
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] })
|
||||
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
|
||||
if (user.enabled == false) { throw new UserDisabledError(); }
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles soft-refreshing of access-tokens.
|
||||
* @param action Routing-Controllers action object that provides request and response objects among other stuff.
|
||||
*/
|
||||
const refresh = async (action: Action) => {
|
||||
let refresh_token = undefined;
|
||||
try {
|
||||
refresh_token = cookie.parse(action.request.headers["cookie"])["lfk_backend__refresh_token"];
|
||||
}
|
||||
catch {
|
||||
throw new IllegalJWTError();
|
||||
}
|
||||
|
||||
let jwtPayload = undefined;
|
||||
try {
|
||||
jwtPayload = <any>jwt.verify(refresh_token, config.jwt_secret);
|
||||
} catch (error) {
|
||||
throw new IllegalJWTError();
|
||||
}
|
||||
|
||||
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions', 'groups', 'groups.permissions'] })
|
||||
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
|
||||
if (user.enabled == false) { throw new UserDisabledError(); }
|
||||
|
||||
let newAccess = JwtCreator.createAccess(user);
|
||||
action.response.header("authorization", "Bearer " + newAccess);
|
||||
|
||||
return await new JwtUser(user);
|
||||
}
|
||||
export default UserChecker;
|
||||
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';
|
||||
|
||||
/**
|
||||
* Handles authentication via jwt's (Bearer authorization header) 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles soft-refreshing of access-tokens.
|
||||
* @param action Routing-Controllers action object that provides request and response objects among other stuff.
|
||||
*/
|
||||
const refresh = async (action: Action) => {
|
||||
let refresh_token = undefined;
|
||||
try {
|
||||
refresh_token = cookie.parse(action.request.headers["cookie"])["lfk_backend__refresh_token"];
|
||||
}
|
||||
catch {
|
||||
throw new IllegalJWTError();
|
||||
}
|
||||
|
||||
let jwtPayload = undefined;
|
||||
try {
|
||||
jwtPayload = <any>jwt.verify(refresh_token, config.jwt_secret);
|
||||
} catch (error) {
|
||||
throw new IllegalJWTError();
|
||||
}
|
||||
|
||||
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions', 'groups', 'groups.permissions'] })
|
||||
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
|
||||
if (user.enabled == false) { throw new UserDisabledError(); }
|
||||
|
||||
let newAccess = JwtCreator.createAccess(user);
|
||||
action.response.header("authorization", "Bearer " + newAccess);
|
||||
|
||||
return await new JwtUser(user);
|
||||
}
|
||||
export default authchecker
|
||||
@@ -1,64 +0,0 @@
|
||||
import { IsNotEmpty, IsOptional, IsPostalCode, IsString } from 'class-validator';
|
||||
import { Address } from '../entities/Address';
|
||||
|
||||
export class CreateAddress {
|
||||
/**
|
||||
* The address's description.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* The address's first line.
|
||||
* Containing the street and house number.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
address1: string;
|
||||
|
||||
/**
|
||||
* The address's second line.
|
||||
* Containing optional information.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
address2?: string;
|
||||
|
||||
/**
|
||||
* The address's postal code.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsPostalCode("DE")
|
||||
postalcode: string;
|
||||
|
||||
/**
|
||||
* The address's city.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
city: string;
|
||||
|
||||
/**
|
||||
* The address's country.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
country: string;
|
||||
|
||||
/**
|
||||
* Creates a Address object based on 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;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import * as argon2 from "argon2";
|
||||
import { IsEmail, IsOptional, IsString } from 'class-validator';
|
||||
import * as jsonwebtoken from 'jsonwebtoken';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { config } from '../../config';
|
||||
import { InvalidCredentialsError, PasswordNeededError, UserNotFoundError } from '../../errors/AuthError';
|
||||
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
|
||||
import { User } from '../entities/User';
|
||||
import { Auth } from '../responses/ResponseAuth';
|
||||
|
||||
export class CreateAuth {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
username?: string;
|
||||
@IsString()
|
||||
password: string;
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
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_users = await getConnectionManager().get().getRepository(User).find({ relations: ['groups', 'permissions'], where: [{ username: this.username }, { email: this.email }] });
|
||||
if (found_users.length === 0) {
|
||||
throw new UserNotFoundError()
|
||||
} else {
|
||||
const found_user = found_users[0]
|
||||
if (await argon2.verify(found_user.password, this.password + found_user.uuid)) {
|
||||
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60
|
||||
found_user.permissions = found_user.permissions || []
|
||||
delete found_user.password;
|
||||
newAuth.access_token = jsonwebtoken.sign({
|
||||
userdetails: found_user,
|
||||
exp: timestamp_accesstoken_expiry
|
||||
}, config.jwt_secret)
|
||||
newAuth.access_token_expires_at = timestamp_accesstoken_expiry
|
||||
//
|
||||
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 10 * 36000
|
||||
newAuth.refresh_token = jsonwebtoken.sign({
|
||||
refreshtokencount: found_user.refreshTokenCount,
|
||||
userid: found_user.id,
|
||||
exp: timestamp_refresh_expiry
|
||||
}, config.jwt_secret)
|
||||
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
|
||||
} else {
|
||||
throw new InvalidCredentialsError()
|
||||
}
|
||||
}
|
||||
return newAuth;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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';
|
||||
|
||||
export class CreateGroupContact {
|
||||
/**
|
||||
* The contact's first name.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
firstname: string;
|
||||
|
||||
/**
|
||||
* The contact's middle name.
|
||||
* Optional
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
middlename?: string;
|
||||
|
||||
/**
|
||||
* The contact's last name.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
lastname: string;
|
||||
|
||||
/**
|
||||
* The contact's address.
|
||||
* Optional
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
address?: number;
|
||||
|
||||
/**
|
||||
* The contact's phone number.
|
||||
* Optional
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsPhoneNumber(config.phone_validation_countrycode)
|
||||
phone?: string;
|
||||
|
||||
/**
|
||||
* The contact's email address.
|
||||
* Optional
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* Get's this participant's address from this.address.
|
||||
*/
|
||||
public async getAddress(): Promise<Address> {
|
||||
if (this.address === undefined) {
|
||||
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 Address object based on 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;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
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';
|
||||
|
||||
export abstract class CreateParticipant {
|
||||
/**
|
||||
* The new participant's first name.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
firstname: string;
|
||||
|
||||
/**
|
||||
* The new participant's middle name.
|
||||
* Optional.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
middlename?: string;
|
||||
|
||||
/**
|
||||
* The new participant's last name.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
lastname: string;
|
||||
|
||||
/**
|
||||
* The new participant's phone number.
|
||||
* Optional.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsPhoneNumber(config.phone_validation_countrycode)
|
||||
phone?: string;
|
||||
|
||||
/**
|
||||
* The new participant's e-mail address.
|
||||
* Optional.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* The new participant's address.
|
||||
* Must be of type number (address id).
|
||||
* Optional.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
address?: number;
|
||||
|
||||
/**
|
||||
* Get's this participant's address from this.address.
|
||||
*/
|
||||
public async getAddress(): Promise<Address> {
|
||||
if (this.address === undefined) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { GroupContactNotFoundError, GroupContactWrongTypeError } from '../../errors/GroupContactErrors';
|
||||
import { GroupContact } from '../entities/GroupContact';
|
||||
|
||||
export abstract class CreateRunnerGroup {
|
||||
/**
|
||||
* The group's name.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The group's contact.
|
||||
* Optional
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
contact?: number;
|
||||
|
||||
/**
|
||||
* Get's this group's contact from this.address.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { IsInt, IsOptional } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
|
||||
import { Address } from '../entities/Address';
|
||||
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
|
||||
import { CreateRunnerGroup } from './CreateRunnerGroup';
|
||||
|
||||
export class CreateRunnerOrganisation extends CreateRunnerGroup {
|
||||
/**
|
||||
* The new organisation's address.
|
||||
* Must be of type number (address id).
|
||||
* Optional.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
address?: number;
|
||||
|
||||
/**
|
||||
* Get's this org's address from this.address.
|
||||
*/
|
||||
public async getAddress(): Promise<Address> {
|
||||
if (this.address === undefined) {
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
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';
|
||||
|
||||
export class CreateRunnerTeam extends CreateRunnerGroup {
|
||||
|
||||
/**
|
||||
* The team's parent group (organisation).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
parentGroup: number;
|
||||
|
||||
public async getParent(): Promise<RunnerOrganisation> {
|
||||
if (this.parentGroup === undefined) {
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { IsInt, IsNotEmpty, IsPositive, IsString } from 'class-validator';
|
||||
import { Track } from '../entities/Track';
|
||||
|
||||
export class CreateTrack {
|
||||
/**
|
||||
* The track's name.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The track's distance in meters (must be greater than 0).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
distance: number;
|
||||
|
||||
/**
|
||||
* Converts a Track object based on this.
|
||||
*/
|
||||
public toTrack(): Track {
|
||||
let newTrack: Track = new Track();
|
||||
|
||||
newTrack.name = this.name;
|
||||
newTrack.distance = this.distance;
|
||||
|
||||
return newTrack;
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import * as argon2 from "argon2";
|
||||
import { 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';
|
||||
|
||||
export class CreateUser {
|
||||
/**
|
||||
* The new user's first name.
|
||||
*/
|
||||
@IsString()
|
||||
firstname: string;
|
||||
|
||||
/**
|
||||
* The new user's middle name.
|
||||
* Optinal.
|
||||
*/
|
||||
@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.
|
||||
* Optional
|
||||
*/
|
||||
@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;
|
||||
|
||||
/**
|
||||
* The new user's groups' id(s).
|
||||
* You can provide either one groupId or an array of groupIDs.
|
||||
* Optional.
|
||||
*/
|
||||
@IsOptional()
|
||||
groupId?: 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();
|
||||
}
|
||||
|
||||
if (this.groupId) {
|
||||
if (!Array.isArray(this.groupId)) {
|
||||
this.groupId = [this.groupId]
|
||||
}
|
||||
const groupIDs: number[] = this.groupId
|
||||
let errors = 0
|
||||
const validateusergroups = async () => {
|
||||
let foundgroups = []
|
||||
for (const g of groupIDs) {
|
||||
const found = await getConnectionManager().get().getRepository(UserGroup).find({ id: g });
|
||||
if (found.length === 0) {
|
||||
errors++
|
||||
} else {
|
||||
foundgroups.push(found[0])
|
||||
}
|
||||
}
|
||||
newUser.groups = foundgroups
|
||||
}
|
||||
await validateusergroups()
|
||||
if (errors !== 0) {
|
||||
throw new UserGroupNotFoundError();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
//TODO: ProfilePics
|
||||
|
||||
return newUser;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,23 @@ import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, Us
|
||||
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) {
|
||||
@@ -23,11 +35,11 @@ export class HandleLogout {
|
||||
throw new IllegalJWTError()
|
||||
}
|
||||
logout.timestamp = Math.floor(Date.now() / 1000)
|
||||
let found_user: User = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["userid"] });
|
||||
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"]) {
|
||||
if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) {
|
||||
throw new RefreshTokenCountInvalidError()
|
||||
}
|
||||
found_user.refreshTokenCount++;
|
||||
|
||||
@@ -5,8 +5,12 @@ import { RunnerOrganisationNotFoundError } from '../../errors/RunnerOrganisation
|
||||
import { RunnerGroup } from '../entities/RunnerGroup';
|
||||
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
|
||||
import { RunnerTeam } from '../entities/RunnerTeam';
|
||||
import { CreateRunner } from './CreateRunner';
|
||||
import { CreateRunner } from './create/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 {
|
||||
|
||||
/**
|
||||
@@ -18,7 +22,6 @@ export class ImportRunner {
|
||||
|
||||
/**
|
||||
* The new runner's middle name.
|
||||
* Optional.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@@ -32,18 +35,26 @@ export class ImportRunner {
|
||||
lastname: string;
|
||||
|
||||
/**
|
||||
* The new runner's class (if not provided otherwise).
|
||||
* 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();
|
||||
|
||||
@@ -55,6 +66,10 @@ export class ImportRunner {
|
||||
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();
|
||||
|
||||
@@ -2,17 +2,30 @@ 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 { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
|
||||
import { JwtCreator } from "../../jwtcreator";
|
||||
import { User } from '../entities/User';
|
||||
import { Auth } from '../responses/ResponseAuth';
|
||||
import { ResponseAuth } 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;
|
||||
|
||||
public async toAuth(): Promise<Auth> {
|
||||
let newAuth: Auth = new Auth();
|
||||
/**
|
||||
* Creates a new auth object based on this.
|
||||
*/
|
||||
public async toAuth(): Promise<ResponseAuth> {
|
||||
let newAuth: ResponseAuth = new ResponseAuth();
|
||||
if (!this.token || this.token === undefined) {
|
||||
throw new JwtNotProvidedError()
|
||||
}
|
||||
@@ -22,31 +35,22 @@ export class RefreshAuth {
|
||||
} catch (error) {
|
||||
throw new IllegalJWTError()
|
||||
}
|
||||
const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["userid"] }, { relations: ['groups', 'permissions'] });
|
||||
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.refreshTokenCount !== decoded["refreshtokencount"]) {
|
||||
if (found_user.enabled == false) { throw new UserDisabledError(); }
|
||||
if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) {
|
||||
throw new RefreshTokenCountInvalidError()
|
||||
}
|
||||
found_user.permissions = found_user.permissions || []
|
||||
delete found_user.password;
|
||||
//Create the auth token
|
||||
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60
|
||||
delete found_user.password;
|
||||
newAuth.access_token = jsonwebtoken.sign({
|
||||
userdetails: found_user,
|
||||
exp: timestamp_accesstoken_expiry
|
||||
}, config.jwt_secret)
|
||||
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 = jsonwebtoken.sign({
|
||||
refreshtokencount: found_user.refreshTokenCount,
|
||||
userid: found_user.id,
|
||||
exp: timestamp_refresh_expiry
|
||||
}, config.jwt_secret)
|
||||
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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';
|
||||
|
||||
export class UpdateRunner extends CreateParticipant {
|
||||
|
||||
/**
|
||||
* The updated runner's id.
|
||||
*/
|
||||
@IsInt()
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The updated runner's new team/org.
|
||||
*/
|
||||
@IsObject()
|
||||
group: RunnerGroup;
|
||||
|
||||
/**
|
||||
* Creates a Runner entity from this.
|
||||
*/
|
||||
public async toRunner(): Promise<Runner> {
|
||||
let newRunner: Runner = new Runner();
|
||||
|
||||
newRunner.id = this.id;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages all the different ways a group can be provided.
|
||||
*/
|
||||
public async getGroup(): Promise<RunnerGroup> {
|
||||
if (this.group === undefined) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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';
|
||||
|
||||
export class UpdateRunnerTeam extends CreateRunnerGroup {
|
||||
|
||||
/**
|
||||
* The updated team's id.
|
||||
*/
|
||||
@IsInt()
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The team's parent group (organisation).
|
||||
*/
|
||||
@IsObject()
|
||||
@IsNotEmpty()
|
||||
parentGroup: RunnerOrganisation;
|
||||
|
||||
public async getParent(): Promise<RunnerOrganisation> {
|
||||
if (this.parentGroup === undefined) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a RunnerTeam entity from this.
|
||||
*/
|
||||
public async toRunnerTeam(): Promise<RunnerTeam> {
|
||||
let newRunnerTeam: RunnerTeam = new RunnerTeam();
|
||||
|
||||
newRunnerTeam.id = this.id;
|
||||
newRunnerTeam.name = this.name;
|
||||
newRunnerTeam.parentGroup = await this.getParent();
|
||||
newRunnerTeam.contact = await this.getContact()
|
||||
|
||||
return newRunnerTeam;
|
||||
}
|
||||
}
|
||||
73
src/models/actions/create/CreateAuth.ts
Normal file
73
src/models/actions/create/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 { ResponseAuth } 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<ResponseAuth> {
|
||||
let newAuth: ResponseAuth = new ResponseAuth();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
52
src/models/actions/create/CreateDistanceDonation.ts
Normal file
52
src/models/actions/create/CreateDistanceDonation.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { IsInt, IsPositive } from 'class-validator';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
|
||||
import { DistanceDonation } from '../../entities/DistanceDonation';
|
||||
import { Runner } from '../../entities/Runner';
|
||||
import { CreateDonation } from './CreateDonation';
|
||||
|
||||
/**
|
||||
* This class is used to create a new FixedDonation entity from a json body (post request).
|
||||
*/
|
||||
export class CreateDistanceDonation extends CreateDonation {
|
||||
|
||||
/**
|
||||
* The donation's associated runner's id.
|
||||
* This is important to link the runner's distance ran to the donation.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
runner: number;
|
||||
|
||||
/**
|
||||
* The donation's amount per distance (full kilometer aka 1000 meters).
|
||||
* The unit is your currency's smallest unit (default: euro cent).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
amountPerDistance: number;
|
||||
|
||||
/**
|
||||
* Creates a new FixedDonation entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<DistanceDonation> {
|
||||
let newDonation = new DistanceDonation;
|
||||
|
||||
newDonation.amountPerDistance = this.amountPerDistance;
|
||||
newDonation.donor = await this.getDonor();
|
||||
newDonation.runner = await this.getRunner();
|
||||
|
||||
return newDonation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a runner based on the runner id provided via this.runner.
|
||||
*/
|
||||
public async getRunner(): Promise<Runner> {
|
||||
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
|
||||
if (!runner) {
|
||||
throw new RunnerNotFoundError();
|
||||
}
|
||||
return runner;
|
||||
}
|
||||
}
|
||||
34
src/models/actions/create/CreateDonation.ts
Normal file
34
src/models/actions/create/CreateDonation.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { IsInt, IsPositive } from 'class-validator';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { DonorNotFoundError } from '../../../errors/DonorErrors';
|
||||
import { Donation } from '../../entities/Donation';
|
||||
import { Donor } from '../../entities/Donor';
|
||||
|
||||
/**
|
||||
* This class is used to create a new Donation entity from a json body (post request).
|
||||
*/
|
||||
export abstract class CreateDonation {
|
||||
/**
|
||||
* The donation's associated donor's id.
|
||||
* This is important to link donations to donors.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
donor: number;
|
||||
|
||||
/**
|
||||
* Creates a new Donation entity from this.
|
||||
*/
|
||||
public abstract toEntity(): Promise<Donation>;
|
||||
|
||||
/**
|
||||
* Gets a donor based on the donor id provided via this.donor.
|
||||
*/
|
||||
public async getDonor(): Promise<Donor> {
|
||||
const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor });
|
||||
if (!donor) {
|
||||
throw new DonorNotFoundError();
|
||||
}
|
||||
return donor;
|
||||
}
|
||||
}
|
||||
39
src/models/actions/create/CreateDonor.ts
Normal file
39
src/models/actions/create/CreateDonor.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { IsBoolean, IsOptional } from 'class-validator';
|
||||
import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { Donor } from '../../entities/Donor';
|
||||
import { CreateParticipant } from './CreateParticipant';
|
||||
|
||||
/**
|
||||
* 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 toEntity(): 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.receiptNeeded = this.receiptNeeded;
|
||||
newDonor.address = this.address;
|
||||
Address.validate(newDonor.address);
|
||||
if (this.receiptNeeded == true && Address.isValidAddress(newDonor.address) == false) {
|
||||
throw new DonorReceiptAddressNeededError()
|
||||
}
|
||||
|
||||
return newDonor;
|
||||
}
|
||||
}
|
||||
28
src/models/actions/create/CreateFixedDonation.ts
Normal file
28
src/models/actions/create/CreateFixedDonation.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { IsInt, IsPositive } from 'class-validator';
|
||||
import { FixedDonation } from '../../entities/FixedDonation';
|
||||
import { CreateDonation } from './CreateDonation';
|
||||
|
||||
/**
|
||||
* This class is used to create a new FixedDonation entity from a json body (post request).
|
||||
*/
|
||||
export class CreateFixedDonation extends CreateDonation {
|
||||
/**
|
||||
* The donation's amount.
|
||||
* The unit is your currency's smallest unit (default: euro cent).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
amount: number;
|
||||
|
||||
/**
|
||||
* Creates a new FixedDonation entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<FixedDonation> {
|
||||
let newDonation = new FixedDonation;
|
||||
|
||||
newDonation.amount = this.amount;
|
||||
newDonation.donor = await this.getDonor();
|
||||
|
||||
return newDonation;
|
||||
}
|
||||
}
|
||||
97
src/models/actions/create/CreateGroupContact.ts
Normal file
97
src/models/actions/create/CreateGroupContact.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { config } from '../../../config';
|
||||
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { GroupContact } from '../../entities/GroupContact';
|
||||
import { RunnerGroup } from '../../entities/RunnerGroup';
|
||||
|
||||
/**
|
||||
* This classed is used to create a new GroupContact 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.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
address?: Address;
|
||||
|
||||
/**
|
||||
* 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 new contact's email address.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* The new contacts's groups' ids.
|
||||
* You can provide either one groupId or an array of groupIDs.
|
||||
*/
|
||||
@IsOptional()
|
||||
groups?: number[] | number
|
||||
|
||||
|
||||
/**
|
||||
* Get's all groups for this contact by their id's;
|
||||
*/
|
||||
public async getGroups(): Promise<RunnerGroup[]> {
|
||||
if (!this.groups) { return null; }
|
||||
let groups = new Array<RunnerGroup>();
|
||||
if (!Array.isArray(this.groups)) {
|
||||
this.groups = [this.groups]
|
||||
}
|
||||
for (let group of this.groups) {
|
||||
let found = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: group });
|
||||
if (!found) { throw new RunnerGroupNotFoundError(); }
|
||||
groups.push(found);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new GroupContact entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<GroupContact> {
|
||||
let newContact: GroupContact = new GroupContact();
|
||||
newContact.firstname = this.firstname;
|
||||
newContact.middlename = this.middlename;
|
||||
newContact.lastname = this.lastname;
|
||||
newContact.email = this.email;
|
||||
newContact.phone = this.phone;
|
||||
newContact.address = this.address;
|
||||
Address.validate(newContact.address);
|
||||
newContact.groups = await this.getGroups();
|
||||
|
||||
return newContact;
|
||||
}
|
||||
}
|
||||
53
src/models/actions/create/CreateParticipant.ts
Normal file
53
src/models/actions/create/CreateParticipant.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||
import { config } from '../../../config';
|
||||
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.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
address?: Address;
|
||||
}
|
||||
60
src/models/actions/create/CreatePermission.ts
Normal file
60
src/models/actions/create/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 toEntity(): 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/create/CreateResetToken.ts
Normal file
50
src/models/actions/create/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;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
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 { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
|
||||
import { RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
|
||||
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { Runner } from '../../entities/Runner';
|
||||
import { RunnerGroup } from '../../entities/RunnerGroup';
|
||||
import { CreateParticipant } from './CreateParticipant';
|
||||
|
||||
/**
|
||||
* This classed is used to create a new Runner entity from a json body (post request).
|
||||
*/
|
||||
export class CreateRunner extends CreateParticipant {
|
||||
|
||||
/**
|
||||
@@ -16,9 +20,9 @@ export class CreateRunner extends CreateParticipant {
|
||||
group: number;
|
||||
|
||||
/**
|
||||
* Creates a Runner entity from this.
|
||||
* Creates a new Runner entity from this.
|
||||
*/
|
||||
public async toRunner(): Promise<Runner> {
|
||||
public async toEntity(): Promise<Runner> {
|
||||
let newRunner: Runner = new Runner();
|
||||
|
||||
newRunner.firstname = this.firstname;
|
||||
@@ -27,16 +31,17 @@ export class CreateRunner extends CreateParticipant {
|
||||
newRunner.phone = this.phone;
|
||||
newRunner.email = this.email;
|
||||
newRunner.group = await this.getGroup();
|
||||
newRunner.address = await this.getAddress();
|
||||
newRunner.address = this.address;
|
||||
Address.validate(newRunner.address);
|
||||
|
||||
return newRunner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages all the different ways a group can be provided.
|
||||
* Gets the new runner's group by it's id.
|
||||
*/
|
||||
public async getGroup(): Promise<RunnerGroup> {
|
||||
if (this.group === undefined) {
|
||||
if (this.group === undefined || this.group === null) {
|
||||
throw new RunnerTeamNeedsParentError();
|
||||
}
|
||||
if (!isNaN(this.group)) {
|
||||
45
src/models/actions/create/CreateRunnerCard.ts
Normal file
45
src/models/actions/create/CreateRunnerCard.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
|
||||
import { Runner } from '../../entities/Runner';
|
||||
import { RunnerCard } from '../../entities/RunnerCard';
|
||||
|
||||
/**
|
||||
* This classed is used to create a new RunnerCard entity from a json body (post request).
|
||||
*/
|
||||
export class CreateRunnerCard {
|
||||
/**
|
||||
* The card's associated runner's id.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
runner?: number;
|
||||
|
||||
/**
|
||||
* Is the new card enabled (for fraud reasons)?
|
||||
* Default: true
|
||||
*/
|
||||
@IsBoolean()
|
||||
enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* Creates a new RunnerCard entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<RunnerCard> {
|
||||
let newCard: RunnerCard = new RunnerCard();
|
||||
|
||||
newCard.enabled = this.enabled;
|
||||
newCard.runner = await this.getRunner();
|
||||
|
||||
return newCard;
|
||||
}
|
||||
|
||||
public async getRunner(): Promise<Runner> {
|
||||
if (!this.runner) { return null; }
|
||||
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
|
||||
if (!runner) {
|
||||
throw new RunnerNotFoundError();
|
||||
}
|
||||
return runner;
|
||||
}
|
||||
}
|
||||
35
src/models/actions/create/CreateRunnerGroup.ts
Normal file
35
src/models/actions/create/CreateRunnerGroup.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { GroupContactNotFoundError } 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's id.
|
||||
* Optional
|
||||
*/
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
contact?: number;
|
||||
|
||||
/**
|
||||
* Gets the new group's contact by it's id.
|
||||
*/
|
||||
public async getContact(): Promise<GroupContact> {
|
||||
if (!this.contact) { return null; }
|
||||
let contact = await getConnectionManager().get().getRepository(GroupContact).findOne({ id: this.contact });
|
||||
if (!contact) { throw new GroupContactNotFoundError; }
|
||||
return contact;
|
||||
|
||||
}
|
||||
}
|
||||
30
src/models/actions/create/CreateRunnerOrganisation.ts
Normal file
30
src/models/actions/create/CreateRunnerOrganisation.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { IsObject, IsOptional } from 'class-validator';
|
||||
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.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
address?: Address;
|
||||
|
||||
/**
|
||||
* Creates a new RunnerOrganisation entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<RunnerOrganisation> {
|
||||
let newRunnerOrganisation: RunnerOrganisation = new RunnerOrganisation();
|
||||
|
||||
newRunnerOrganisation.name = this.name;
|
||||
newRunnerOrganisation.contact = await this.getContact();
|
||||
newRunnerOrganisation.address = this.address;
|
||||
Address.validate(newRunnerOrganisation.address);
|
||||
|
||||
return newRunnerOrganisation;
|
||||
}
|
||||
}
|
||||
45
src/models/actions/create/CreateRunnerTeam.ts
Normal file
45
src/models/actions/create/CreateRunnerTeam.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { IsInt, IsNotEmpty } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { RunnerOrganisationNotFoundError } 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 org's id.
|
||||
*/
|
||||
@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();
|
||||
}
|
||||
let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: this.parentGroup });
|
||||
if (!parentGroup) { throw new RunnerOrganisationNotFoundError();; }
|
||||
return parentGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new RunnerTeam entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<RunnerTeam> {
|
||||
let newRunnerTeam: RunnerTeam = new RunnerTeam();
|
||||
|
||||
newRunnerTeam.name = this.name;
|
||||
newRunnerTeam.parentGroup = await this.getParent();
|
||||
newRunnerTeam.contact = await this.getContact()
|
||||
|
||||
return newRunnerTeam;
|
||||
}
|
||||
}
|
||||
59
src/models/actions/create/CreateScan.ts
Normal file
59
src/models/actions/create/CreateScan.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { IsBoolean, IsInt, IsOptional, IsPositive } from 'class-validator';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
|
||||
import { Runner } from '../../entities/Runner';
|
||||
import { Scan } from '../../entities/Scan';
|
||||
|
||||
/**
|
||||
* This class is used to create a new Scan entity from a json body (post request).
|
||||
*/
|
||||
export abstract class CreateScan {
|
||||
/**
|
||||
* The scan's associated runner's id.
|
||||
* This is important to link ran distances to runners.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
runner: number;
|
||||
|
||||
/**
|
||||
* Is the scan valid (for fraud reasons).
|
||||
* The determination of validity will work differently for every child class.
|
||||
* Default: true
|
||||
*/
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
valid?: boolean = true;
|
||||
|
||||
/**
|
||||
* The scan's distance in meters.
|
||||
* Can be set manually or derived from another object.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
public distance: number;
|
||||
|
||||
/**
|
||||
* Creates a new Scan entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<Scan> {
|
||||
let newScan = new Scan();
|
||||
|
||||
newScan.distance = this.distance;
|
||||
newScan.valid = this.valid;
|
||||
newScan.runner = await this.getRunner();
|
||||
|
||||
return newScan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a runner based on the runner id provided via this.runner.
|
||||
*/
|
||||
public async getRunner(): Promise<Runner> {
|
||||
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
|
||||
if (!runner) {
|
||||
throw new RunnerNotFoundError();
|
||||
}
|
||||
return runner;
|
||||
}
|
||||
}
|
||||
64
src/models/actions/create/CreateScanStation.ts
Normal file
64
src/models/actions/create/CreateScanStation.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as argon2 from "argon2";
|
||||
import { IsBoolean, IsInt, IsOptional, IsPositive, IsString } from 'class-validator';
|
||||
import crypto from 'crypto';
|
||||
import { getConnection } from 'typeorm';
|
||||
import * as uuid from 'uuid';
|
||||
import { TrackNotFoundError } from '../../../errors/TrackErrors';
|
||||
import { ScanStation } from '../../entities/ScanStation';
|
||||
import { Track } from '../../entities/Track';
|
||||
|
||||
/**
|
||||
* This class is used to create a new StatsClient entity from a json body (post request).
|
||||
*/
|
||||
export class CreateScanStation {
|
||||
/**
|
||||
* The new station's description.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* The station's associated track's id.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
track: number;
|
||||
|
||||
/**
|
||||
* Is this station enabled?
|
||||
*/
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
enabled?: boolean = true;
|
||||
|
||||
/**
|
||||
* Converts this to a ScanStation entity.
|
||||
*/
|
||||
public async toEntity(): Promise<ScanStation> {
|
||||
let newStation: ScanStation = new ScanStation();
|
||||
|
||||
newStation.description = this.description;
|
||||
newStation.enabled = this.enabled;
|
||||
newStation.track = await this.getTrack();
|
||||
|
||||
let newUUID = uuid.v4().toUpperCase();
|
||||
newStation.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
|
||||
newStation.key = await argon2.hash(newStation.prefix + "." + newUUID);
|
||||
newStation.cleartextkey = newStation.prefix + "." + newUUID;
|
||||
|
||||
return newStation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's a track by it's id provided via this.track.
|
||||
* Used to link the new station to a track.
|
||||
*/
|
||||
public async getTrack(): Promise<Track> {
|
||||
const track = await getConnection().getRepository(Track).findOne({ id: this.track });
|
||||
if (!track) {
|
||||
throw new TrackNotFoundError();
|
||||
}
|
||||
return track;
|
||||
}
|
||||
}
|
||||
33
src/models/actions/create/CreateStatsClient.ts
Normal file
33
src/models/actions/create/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 toEntity(): 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/create/CreateTrack.ts
Normal file
46
src/models/actions/create/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 toEntity(): 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;
|
||||
}
|
||||
}
|
||||
79
src/models/actions/create/CreateTrackScan.ts
Normal file
79
src/models/actions/create/CreateTrackScan.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { IsInt, IsPositive } from 'class-validator';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { RunnerCardNotFoundError } from '../../../errors/RunnerCardErrors';
|
||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
|
||||
import { ScanStationNotFoundError } from '../../../errors/ScanStationErrors';
|
||||
import { RunnerCard } from '../../entities/RunnerCard';
|
||||
import { ScanStation } from '../../entities/ScanStation';
|
||||
import { TrackScan } from '../../entities/TrackScan';
|
||||
|
||||
/**
|
||||
* This classed is used to create a new Scan entity from a json body (post request).
|
||||
*/
|
||||
export class CreateTrackScan {
|
||||
/**
|
||||
* The id of the runnerCard associated with the scan.
|
||||
* This get's saved for documentation and management purposes.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
card: number;
|
||||
|
||||
/**
|
||||
* The scanning station's id that created the scan.
|
||||
* Mainly used for logging and traceing back scans (or errors).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
station: number;
|
||||
|
||||
/**
|
||||
* Creates a new Track entity from this.
|
||||
*/
|
||||
public async toEntity(): Promise<TrackScan> {
|
||||
let newScan: TrackScan = new TrackScan();
|
||||
|
||||
newScan.station = await this.getStation();
|
||||
newScan.card = await this.getCard();
|
||||
|
||||
newScan.track = newScan.station.track;
|
||||
newScan.runner = newScan.card.runner;
|
||||
|
||||
if (!newScan.runner) {
|
||||
throw new RunnerNotFoundError();
|
||||
}
|
||||
|
||||
newScan.timestamp = Math.round(new Date().getTime() / 1000);
|
||||
newScan.valid = await this.validateScan(newScan);
|
||||
|
||||
return newScan;
|
||||
}
|
||||
|
||||
public async getCard(): Promise<RunnerCard> {
|
||||
const track = await getConnection().getRepository(RunnerCard).findOne({ id: this.card }, { relations: ["runner"] });
|
||||
if (!track) {
|
||||
throw new RunnerCardNotFoundError();
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
public async getStation(): Promise<ScanStation> {
|
||||
const station = await getConnection().getRepository(ScanStation).findOne({ id: this.station }, { relations: ["track"] });
|
||||
if (!station) {
|
||||
throw new ScanStationNotFoundError();
|
||||
}
|
||||
return station;
|
||||
}
|
||||
|
||||
public async validateScan(scan: TrackScan): Promise<boolean> {
|
||||
const scans = await getConnection().getRepository(TrackScan).find({ where: { runner: scan.runner, valid: true }, relations: ["track"] });
|
||||
if (scans.length == 0) { return true; }
|
||||
|
||||
const newestScan = scans[scans.length - 1];
|
||||
if ((scan.timestamp - newestScan.timestamp) > scan.track.minimumLapTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
132
src/models/actions/create/CreateUser.ts
Normal file
132
src/models/actions/create/CreateUser.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import * as argon2 from "argon2";
|
||||
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import * as uuid from 'uuid';
|
||||
import { config } from '../../../config';
|
||||
import { UserEmailNeededError, UsernameContainsIllegalCharacterError } from '../../../errors/UserErrors';
|
||||
import { 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 a email addres, so this is optional.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
username?: string;
|
||||
|
||||
/**
|
||||
* The new user's email address.
|
||||
*/
|
||||
@IsEmail()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
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' ids.
|
||||
* You can provide either one groupId or an array of groupIDs.
|
||||
*/
|
||||
@IsOptional()
|
||||
groups?: number[] | number
|
||||
|
||||
/**
|
||||
* The user's profile pic (or rather a url pointing to it).
|
||||
*/
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
@IsOptional()
|
||||
profilePic?: string;
|
||||
|
||||
/**
|
||||
* Converts this to a User entity.
|
||||
*/
|
||||
public async toEntity(): Promise<User> {
|
||||
let newUser: User = new User();
|
||||
|
||||
if (!this.email) {
|
||||
throw new UserEmailNeededError();
|
||||
}
|
||||
if (this.username.includes("@")) { throw new UsernameContainsIllegalCharacterError(); }
|
||||
|
||||
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;
|
||||
|
||||
if (!this.profilePic) { newUser.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; }
|
||||
else { newUser.profilePic = this.profilePic; }
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { UserGroup } from '../entities/UserGroup';
|
||||
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.
|
||||
@@ -17,9 +20,9 @@ export class CreateUserGroup {
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Converts this to a UserGroup entity.
|
||||
* Creates a new UserGroup entity from this.
|
||||
*/
|
||||
public async toUserGroup(): Promise<UserGroup> {
|
||||
public async toEntity(): Promise<UserGroup> {
|
||||
let newUserGroup: UserGroup = new UserGroup();
|
||||
|
||||
newUserGroup.name = this.name;
|
||||
51
src/models/actions/update/UpdateDistanceDonation.ts
Normal file
51
src/models/actions/update/UpdateDistanceDonation.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { IsInt, IsPositive } from 'class-validator';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
|
||||
import { DistanceDonation } from '../../entities/DistanceDonation';
|
||||
import { Runner } from '../../entities/Runner';
|
||||
import { UpdateDonation } from './UpdateDonation';
|
||||
|
||||
/**
|
||||
* This class is used to update a DistanceDonation entity (via put request).
|
||||
*/
|
||||
export class UpdateDistanceDonation extends UpdateDonation {
|
||||
|
||||
/**
|
||||
* The donation's associated runner's id.
|
||||
* This is important to link the runner's distance ran to the donation.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
runner: number;
|
||||
|
||||
/**
|
||||
* The donation's amount per distance (full kilometer aka 1000 meters).
|
||||
* The unit is your currency's smallest unit (default: euro cent).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
amountPerDistance: number;
|
||||
|
||||
/**
|
||||
* Update a DistanceDonation entity based on this.
|
||||
* @param donation The donation that shall be updated.
|
||||
*/
|
||||
public async update(donation: DistanceDonation): Promise<DistanceDonation> {
|
||||
donation.amountPerDistance = this.amountPerDistance;
|
||||
donation.donor = await this.getDonor();
|
||||
donation.runner = await this.getRunner();
|
||||
|
||||
return donation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a runner based on the runner id provided via this.runner.
|
||||
*/
|
||||
public async getRunner(): Promise<Runner> {
|
||||
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
|
||||
if (!runner) {
|
||||
throw new RunnerNotFoundError();
|
||||
}
|
||||
return runner;
|
||||
}
|
||||
}
|
||||
41
src/models/actions/update/UpdateDonation.ts
Normal file
41
src/models/actions/update/UpdateDonation.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { IsInt, IsPositive } from 'class-validator';
|
||||
import { getConnection } from 'typeorm';
|
||||
import { DonorNotFoundError } from '../../../errors/DonorErrors';
|
||||
import { Donation } from '../../entities/Donation';
|
||||
import { Donor } from '../../entities/Donor';
|
||||
|
||||
/**
|
||||
* This class is used to update a Donation entity (via put request).
|
||||
*/
|
||||
export abstract class UpdateDonation {
|
||||
/**
|
||||
* The updated donation's id.
|
||||
* This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
|
||||
*/
|
||||
@IsInt()
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The updated donation's associated donor's id.
|
||||
* This is important to link donations to donors.
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
donor: number;
|
||||
|
||||
/**
|
||||
* Creates a new Donation entity from this.
|
||||
*/
|
||||
public abstract update(donation: Donation): Promise<Donation>;
|
||||
|
||||
/**
|
||||
* Gets a donor based on the donor id provided via this.donor.
|
||||
*/
|
||||
public async getDonor(): Promise<Donor> {
|
||||
const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor });
|
||||
if (!donor) {
|
||||
throw new DonorNotFoundError();
|
||||
}
|
||||
return donor;
|
||||
}
|
||||
}
|
||||
46
src/models/actions/update/UpdateDonor.ts
Normal file
46
src/models/actions/update/UpdateDonor.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
|
||||
import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors';
|
||||
import { Address } from '../../entities/Address';
|
||||
import { Donor } from '../../entities/Donor';
|
||||
import { CreateParticipant } from '../create/CreateParticipant';
|
||||
|
||||
/**
|
||||
* 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 update(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;
|
||||
if (!this.address) { donor.address.reset(); }
|
||||
else { donor.address = this.address; }
|
||||
Address.validate(donor.address);
|
||||
if (this.receiptNeeded == true && Address.isValidAddress(donor.address) == false) {
|
||||
throw new DonorReceiptAddressNeededError()
|
||||
}
|
||||
|
||||
return donor;
|
||||
}
|
||||
}
|
||||
27
src/models/actions/update/UpdateFixedDonation.ts
Normal file
27
src/models/actions/update/UpdateFixedDonation.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { IsInt, IsPositive } from 'class-validator';
|
||||
import { FixedDonation } from '../../entities/FixedDonation';
|
||||
import { UpdateDonation } from './UpdateDonation';
|
||||
|
||||
/**
|
||||
* This class is used to update a FixedDonation entity (via put request).
|
||||
*/
|
||||
export class UpdateFixedDonation extends UpdateDonation {
|
||||
/**
|
||||
* The updated donation's amount.
|
||||
* The unit is your currency's smallest unit (default: euro cent).
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
amount: number;
|
||||
|
||||
/**
|
||||
* Update a FixedDonation entity based on this.
|
||||
* @param donation The donation that shall be updated.
|
||||
*/
|
||||
public async update(donation: FixedDonation): Promise<FixedDonation> {
|
||||
donation.amount = this.amount;
|
||||
donation.donor = await this.getDonor();
|
||||
|
||||
return donation;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user