Compare commits

...

272 Commits

Author SHA1 Message Date
c66b06c2c9 Merge pull request 'Alpha Release 0.0.9' (#82) from dev into main
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: #82
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-08 19:52:55 +00:00
65e605cdc4 Version bump
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-01-08 20:49:19 +01:00
d2fdb4efd9 Merge pull request 'All users get profile pics feature/79-profile_pics' (#81) from feature/79-profile_pics into dev
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #81
closes #79
2021-01-08 19:48:22 +00:00
d0deb9d647 Fixed wrong relation getting resolved
All checks were successful
continuous-integration/drone/pr Build is passing
ref #79
2021-01-08 20:40:15 +01:00
5495c90eaf Merge branch 'dev' into feature/79-profile_pics
Some checks failed
continuous-integration/drone/pr Build is failing
2021-01-08 20:19:08 +01:00
bf3ffae67c Merge pull request 'Added scan (station) apis feature/67-scan_apis' (#80) from feature/67-scan_apis into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #80
closes #67
2021-01-08 19:18:39 +00:00
aa0337ea33 Fixed getting all permissions for users
ref #79
2021-01-08 20:17:05 +01:00
4991d735bf Pinned sqlite3 to 5.0.0 as a temporary bugfix
All checks were successful
continuous-integration/drone/pr Build is passing
ref #67
2021-01-08 20:04:04 +01:00
398e61bddb Merge branch 'feature/67-scan_apis' of git.odit.services:lfk/backend into feature/67-scan_apis
Some checks failed
continuous-integration/drone/pr Build is failing
# Conflicts:
#	.drone.yml
2021-01-08 19:37:25 +01:00
e6576f4a54 Finned node version for ci
ref #67
2021-01-08 19:37:13 +01:00
c3b9e135b0 Finned node version for ci
Some checks failed
continuous-integration/drone/pr Build is failing
ref #67
2021-01-08 19:34:39 +01:00
3bd4948c43 Merge branch 'feature/79-profile_pics' of git.odit.services:lfk/backend into feature/79-profile_pics 2021-01-08 19:32:13 +01:00
f3cd1380be First part of the permission return (buggy!)
ref #79
2021-01-08 19:32:11 +01:00
a2c3dfbf85 First part of the permission return (buggy!)
ref #71
2021-01-08 19:32:04 +01:00
3c37aafe1f Added profile pics to all user related models
ref #79
2021-01-08 19:11:50 +01:00
c591c182b3 Updated comments
Some checks failed
continuous-integration/drone/pr Build is failing
ref #67
2021-01-08 18:37:33 +01:00
9cc50078d1 Merge branch 'dev' into feature/67-scan_apis 2021-01-08 18:29:33 +01:00
7728759bcd Added openapi sec scheme for the scan station auth
ref #67
2021-01-08 18:28:35 +01:00
ce8fed350e Updated OPENAPI Descriptions for the new controllers
ref #67
2021-01-08 18:25:29 +01:00
a005945e9e Added scan add tests with the station based auth
ref #67
2021-01-08 18:09:47 +01:00
cf86520fae Fixed wrong auth type being used
ref #67
2021-01-08 18:08:13 +01:00
db6fdf6baf Implemented scan auth middleware
ref #67
2021-01-08 17:50:29 +01:00
975ad50afc Added scan update tests
ref #67
2021-01-08 17:42:05 +01:00
0c27df7754 Added scan add tests
ref #67
2021-01-08 17:27:56 +01:00
102a860ba3 Added scan delete tests
ref #67
2021-01-08 16:47:52 +01:00
3a886714a0 Merge branch 'feature/67-scan_apis' of git.odit.services:lfk/backend into feature/67-scan_apis
# Conflicts:
#	src/tests/scanstations/scanstations_delete.spec.ts
2021-01-07 20:35:05 +01:00
09ab638239 Added scan station delete tests
ref #67
2021-01-07 20:34:48 +01:00
a4f88c78f4 Added scan station delete tests
ref #67
2021-01-07 20:34:36 +01:00
ccf2a3b617 Added scan station update tests
ref #67
2021-01-07 20:31:29 +01:00
c8f941a779 Fixed wrong error getting thrown
ref #67
2021-01-07 20:22:58 +01:00
5510cbb8e9 Added scan station add tests
ref #67
2021-01-07 20:16:14 +01:00
a434173b54 Added scan station get tests
ref #67
2021-01-07 20:04:15 +01:00
7387f700fb Added alias for posting track scans
ref #67
2021-01-07 19:46:20 +01:00
4f01baaa23 Added the enabled flag for scanstations
ref #67
2021-01-07 19:37:15 +01:00
09b37f0ff2 Fixed typo
ref #67
2021-01-07 19:36:57 +01:00
324d5709e3 Added tmp files to gitignore
ref #67
2021-01-07 19:19:21 +01:00
3f23e4f1f1 Added scan get tests
ref #67
2021-01-07 19:18:26 +01:00
9776a35f9f Track deletion now recognizes associated stations
ref #67
2021-01-07 18:53:09 +01:00
9b9ee70288 Implemented cascading station deletion
ref #67
2021-01-07 18:48:58 +01:00
2628f69651 Implemented scan station creation
ref #67
2021-01-07 18:39:38 +01:00
b9c0a32862 Implemented single scan station get +e errors
ref #67
2021-01-07 18:35:19 +01:00
82644a2ff4 Implmented getting all scan stations
ref #67
2021-01-07 18:05:54 +01:00
3d2c93b5ac Added (scan) stations as a new permission target
ref #67
2021-01-07 17:35:36 +01:00
c447114297 Added a ScanStation response class
ref #67
2021-01-07 17:31:44 +01:00
857de9ffcc Added Creation class for ScanSatations
ref #67
2021-01-07 17:29:22 +01:00
eea656bd7b Added a barebones scanstation controller
ref #67
2021-01-07 17:16:36 +01:00
eec5284306 Implemented "normal" scan updateing
ref #67
2021-01-07 17:12:12 +01:00
88a6a768c4 Implemented scan deletion
ref #67
2021-01-07 17:03:40 +01:00
edac1a224c Fixed runner scan validation bug
ref #67
2021-01-07 16:59:57 +01:00
e67d1c5697 Fixed scan runner in response
ref #67
2021-01-07 16:38:41 +01:00
30502ec949 Fixed Creation of normal scans
ref #67
2021-01-07 16:32:16 +01:00
a2c3913601 Merge branch 'feature/67-scan_apis' of git.odit.services:lfk/backend into feature/67-scan_apis 2021-01-07 16:13:44 +01:00
f1c7713da2 Adusted the way scan distances are implemented
ref #67
2021-01-07 16:13:41 +01:00
d6a41d5a82 Ajusted the way scan distances are implemented 2021-01-07 16:13:31 +01:00
72b5ca4153 Added basics for scan creation (to be tested after scanstations got added)
ref #67
2021-01-06 19:44:20 +01:00
aeec2e1c32 Added single scan get w/ errors
ref #67
2021-01-03 19:36:38 +01:00
f9889bea3d Implemented scans get including the response classes
ref #67
2021-01-03 19:26:06 +01:00
2cad2ac2e9 Implemented the second round of the toResponse normalisationf for all classes
ref #67
2021-01-03 19:18:31 +01:00
d948fe2631 Merge pull request 'Fixed relative paths not being updated + version bump for bugfix release' (#75) from dev into main
Some checks failed
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is failing
Reviewed-on: #75
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-03 18:13:10 +00:00
2b5525323b Fixed relative paths not being updated + version bump for bugfix release
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-01-03 19:10:46 +01:00
58156e0d61 Implemented the first route of the toResponse normalisationf for all classes
ref #67
2021-01-03 19:09:06 +01:00
a4b0dfe43e Defined responses for scans and trackscans
ref #67
2021-01-03 19:02:06 +01:00
ee2433a5ae Added barebones scans controller
ref #67
2021-01-03 18:49:33 +01:00
2151b8502d Added Scan permission target
ref #67
2021-01-03 18:48:05 +01:00
b57fde9b0a Merge pull request 'Bugfix for the openapi exporter' (#74) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #74
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-03 17:29:35 +00:00
86706f9422 Merge branch 'main' into dev
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-01-03 17:29:24 +00:00
0687f268fc Fixed switch up between node/js and ts-node/ts
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-01-03 18:27:58 +01:00
bc426831db Merge pull request 'Alpha Release 0.0.7' (#73) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #73
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-03 17:22:25 +00:00
276e553e13 Version bump
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ref #73
2021-01-03 18:18:51 +01:00
e7ab302c61 Merge pull request 'Minimum lap times for tracks feature/71-track_times' (#72) from feature/71-track_times into dev
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
Reviewed-on: #72
closes #71
2021-01-03 17:17:59 +00:00
a5d70ce4b5 Removed useless console.log
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-03 18:13:53 +01:00
d67be313e6 Added track update tests
All checks were successful
continuous-integration/drone/pr Build is passing
ref #71
2021-01-03 18:08:04 +01:00
15d2d029dc Added track delete tests
ref #71
2021-01-03 18:07:33 +01:00
b6ea5e6549 Fixed copy-paste mistake
ref #71
2021-01-03 18:01:21 +01:00
f378b0651a Added helpful comment about the tracktime's unit
ref #71
2021-01-03 17:52:36 +01:00
1a0573e0d0 Added track add tests
ref #71
2021-01-03 17:52:16 +01:00
9f103d8df1 Added track get tests
ref #71
2021-01-03 17:49:55 +01:00
daa899a1ef Removed the old basic test class
ref #71
2021-01-03 17:49:44 +01:00
59cb72a11d Implemented track upodates using the "new" method
ref #71
2021-01-03 17:30:17 +01:00
28c1b6d31d Improved error handling for negative lap times
ref #71
2021-01-03 17:21:53 +01:00
dcb791c9a2 Added the laptime to the track response
ref #71
2021-01-03 17:06:57 +01:00
907259bf73 Added the laptime to createtrack
ref #71
2021-01-03 17:05:43 +01:00
02f7ddbb37 Marked property as optional
ref #71
2021-01-03 17:04:09 +01:00
63b1ca9b56 Added the minimum lap time to the track entity
ref #71
2021-01-03 16:51:36 +01:00
39857cf6e6 Merge pull request 'New Feature: Donor endpoints feature/65-donor_controllers' (#69) from feature/65-donor_controllers into dev
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Reviewed-on: #69
closes #65

Donors go 💲💲💲
2021-01-02 21:06:08 +00:00
3090ae69f3 Merge branch 'dev' into feature/65-donor_controllers
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-02 20:55:31 +00:00
92186a86cc Merge pull request 'bugfix/68-address_circular_dependencies' (#70) from bugfix/68-address_circular_dependencies into feature/65-donor_controllers
All checks were successful
continuous-integration/drone/pr Build is passing
Reviewed-on: #70
closes #68
2021-01-02 20:55:02 +00:00
97e8470b0d Change requested by @philipp
All checks were successful
continuous-integration/drone/pr Build is passing
ref #70
2021-01-02 21:53:21 +01:00
6b0e3503a7 Dependency: Bumped license-exporter version
All checks were successful
continuous-integration/drone/push Build is passing
ref odit/license-exporter#1 odit/license-exporter#3
2021-01-02 20:49:20 +01:00
1e2de7656e Reenabled addresses in org responses
All checks were successful
continuous-integration/drone/pr Build is passing
ref #68
2021-01-02 20:03:02 +01:00
56c6a7efb0 Revert "Removed addresses from tests until the circular dependencies are solved"
This reverts commit 599296c4e3.
2021-01-02 19:57:55 +01:00
9c4e54fc6e Added comments to the bugfix 2021-01-02 19:57:33 +01:00
2c47436259 Implemented a possible bugfix
ref #68
2021-01-02 19:56:04 +01:00
9b5d16ae92 Added todo relateing to the bugfix issue
All checks were successful
continuous-integration/drone/pr Build is passing
ref #65 #68
2021-01-02 19:39:02 +01:00
deb13674b2 Added donor put (update) tests
ref #65
2021-01-02 19:25:58 +01:00
17c82ff409 Added donor delete tests
ref #65
2021-01-02 19:13:59 +01:00
f9e314bf9f Added donor add test for address needed error
ref #65
2021-01-02 19:12:02 +01:00
e4c1930dd1 Added donor post (add) tests
ref #65
2021-01-02 19:10:23 +01:00
b337ab424d Added donor get tests
ref #65
2021-01-02 19:02:31 +01:00
82a0e194cb Updated track tests for paralellism
ref #65
2021-01-02 19:02:16 +01:00
599296c4e3 Removed addresses from tests until the circular dependencies are solved
ref #65
2021-01-02 19:01:55 +01:00
2594a607dc Added address check for donors that want a receipt on update
ref #65
2021-01-02 18:30:03 +01:00
335d4e24da Added address check for donors that want a receipt
ref #65
2021-01-02 18:28:22 +01:00
becc277123 Merge branch 'feature/65-donor_controllers' of git.odit.services:lfk/backend into feature/65-donor_controllers 2021-01-02 18:19:51 +01:00
52cdd41ec8 Fixed not null constraint
ref #65
2021-01-02 18:19:45 +01:00
53548ba7a6 Fixed not null constraint
ref #56
2021-01-02 18:19:40 +01:00
1dc438beb2 Mitigated circular dependency (to be fixed)
ref #65
2021-01-02 18:12:18 +01:00
c9ba69792f Extended todo w/ issue link
ref #65
2021-01-02 17:07:17 +01:00
ab67e5f4aa Added basic runner updateing
ref #65
2021-01-02 16:55:27 +01:00
557608e318 Added everything for basic donor creation
ref #65
2021-01-02 16:51:33 +01:00
a83fedc9b8 Added first donor-specific errors
ref #65
2021-01-02 16:47:06 +01:00
61a17b198f Implemented basic donor deletion
ref #65
2021-01-02 16:45:01 +01:00
3df1db4ad8 Added the base logic for donor getters
ref #65
2021-01-02 16:42:55 +01:00
e46cfa0d77 Added donor response class
ref #65
2021-01-02 16:40:38 +01:00
4126d31a5e Added copy of runnerController with some stuff reanames for donors
ref #65
2021-01-02 16:38:07 +01:00
9d9549cdd4 Added new donor permission target
ref #65
2021-01-02 16:37:17 +01:00
eb40de6eb4 Removed legacy license txt file
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-31 18:05:21 +01:00
6efd09db73 new license file version [CI SKIP] 2020-12-31 17:02:56 +00:00
3f09e3d387 Merge pull request 'Automatic and manual license collection 📖' (#62) from feature/59-license_collection into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #62
closes #59
2020-12-31 17:02:08 +00:00
05868e0e00 Bumped license lib version
All checks were successful
continuous-integration/drone/pr Build is passing
ref #59
2020-12-31 18:00:46 +01:00
580a73f9a5 Switched to automatic license attribution generation via oss-attribution-generator
All checks were successful
continuous-integration/drone/pr Build is passing
#59
2020-12-31 15:14:51 +01:00
ab7110d49f Merge branch 'dev' into feature/59-license_collection
All checks were successful
continuous-integration/drone/pr Build is passing
# Conflicts:
#	.drone.yml
2020-12-30 21:26:07 +01:00
875781335c Removed the testing pipeline and updated the dev license pipeline
ref #59
2020-12-30 21:24:51 +01:00
625340cf8a Merge branch 'feature/59-license_collection' of git.odit.services:lfk/backend into feature/59-license_collection
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-30 21:23:37 +01:00
8d9dbc3957 Merge branch 'feature/59-license_collection' of git.odit.services:lfk/backend into feature/59-license_collection 2020-12-30 21:23:33 +01:00
07d813082b Merge branch 'feature/59-license_collection' of git.odit.services:lfk/backend into feature/59-license_collection 2020-12-30 21:23:04 +01:00
a684f60252 Added secondary dependency for piupeline
ref #59
2020-12-30 21:22:59 +01:00
931cae3c98 new license file version [CI SKIP] 2020-12-30 20:22:35 +00:00
dfd82a6293 Merge branch 'feature/59-license_collection' of git.odit.services:lfk/backend into feature/59-license_collection
All checks were successful
continuous-integration/drone/push Build is passing
# Conflicts:
#	.drone.yml
2020-12-30 21:21:50 +01:00
82d4b11de3 Adjusted ci dependencies
ref #59
2020-12-30 21:21:40 +01:00
75473937cf Adjusted ci dependencies
ref #59
2020-12-30 21:21:08 +01:00
a68bbab8ab Canged drone branch
All checks were successful
continuous-integration/drone/push Build is passing
ref #59
2020-12-30 21:18:40 +01:00
5cfd2c9a52 Revert "Added license exporter (to json)"
All checks were successful
continuous-integration/drone/push Build is passing
This reverts commit 84a0bd2cd9.
2020-12-30 21:17:27 +01:00
6c7b31d76c Revert "Moved package script related files to their own folder"
This reverts commit 395b0101a8.
2020-12-30 21:17:23 +01:00
2924ac2900 Revert "Added automatic license export on dev push/merge"
This reverts commit 18e3ef9a79.
2020-12-30 21:17:18 +01:00
a501625dd6 Revert "Added --full option for the license exporter to export the license path and text as well"
This reverts commit 62c7f26540.
2020-12-30 21:17:13 +01:00
cc64ce4498 Revert "Added test pipeline for automatic license export"
This reverts commit c9378e6cae.
2020-12-30 21:17:09 +01:00
c9378e6cae Added test pipeline for automatic license export
All checks were successful
continuous-integration/drone/push Build is passing
ref #59
2020-12-30 21:13:32 +01:00
62c7f26540 Added --full option for the license exporter to export the license path and text as well
ref #59
2020-12-30 21:05:16 +01:00
18e3ef9a79 Added automatic license export on dev push/merge
ref #59
2020-12-30 20:40:28 +01:00
395b0101a8 Moved package script related files to their own folder
ref #59
2020-12-30 20:22:18 +01:00
84a0bd2cd9 Added license exporter (to json)
ref #59
2020-12-30 20:18:28 +01:00
9cd181c5b8 Merge pull request 'Merge for alpha 0.0.6' (#61) from dev into main
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: #61
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2020-12-30 17:58:24 +00:00
41828a6e41 Version bump
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2020-12-30 18:55:16 +01:00
356e398caf Merge pull request 'feature/56-stats_endpoint' (#60) from feature/56-stats_endpoint into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #60
closes #56
2020-12-30 16:49:18 +00:00
6cb978df98 Updated security for the stats endpoints
All checks were successful
continuous-integration/drone/pr Build is passing
ref #56 requested by @philipp
2020-12-30 17:40:18 +01:00
4cb0efa6bd Added response schemas
All checks were successful
continuous-integration/drone/pr Build is passing
ref #56
2020-12-30 17:35:21 +01:00
e0fa58da57 Added some comments
All checks were successful
continuous-integration/drone/pr Build is passing
ref #56
2020-12-30 17:27:24 +01:00
5d31d8d1a2 Added stats and stats responses for orgs
ref #56
2020-12-30 16:59:07 +01:00
53a01ad977 Added stats response
ref #56
2020-12-30 16:31:18 +01:00
d7791756dc Added mission relation resolving
ref #56
2020-12-30 16:13:57 +01:00
dd48ee2f7e Added ResponseSchemas and fixed donation resolution bug
ref #56
2020-12-30 15:07:13 +01:00
ec64ec3d63 Added a response class for team stats
ref #56
2020-12-30 14:41:07 +01:00
35dbfeb5e7 Added donation amount to the stats runner response
ref #56
2020-12-30 14:34:10 +01:00
a9ecfccfd2 Added response schemas
ref #56
2020-12-30 14:31:07 +01:00
d850650aeb Added response class for the runner stats
ref #56
2020-12-30 14:30:31 +01:00
43e256f38c Impelemented stats api auth via token or the usual auth (jwt with get for runners, teams and orgs).
ref #56
2020-12-30 14:19:54 +01:00
b5f9cf201d Moved the authchecker to the middleware folder (b/c it pretty much is a glolified middleware)
ref #56
2020-12-30 14:01:37 +01:00
6e121a3ce2 Implemented more stats endpoints
ref #56
2020-12-29 22:17:29 +01:00
555e37eaf7 Added authed stats routes
ref #56
2020-12-29 21:48:21 +01:00
9675e79441 Added openapi scheme for the stats api tokens.
ref #56
2020-12-29 21:38:48 +01:00
345851bf1d Added example endpoint for stats auth 2020-12-29 21:34:49 +01:00
7c5a3893ef Added basic status api key checking middleware
ref #56
2020-12-29 21:32:45 +01:00
b53b5cf91f Update: keys cant be updated (for security reasons)
ref #56
2020-12-29 21:00:43 +01:00
04813173e4 Updated the method of api key creation.
ref #56
2020-12-29 20:49:45 +01:00
c4270b0839 Adapted the new async behaviour
ref #56
2020-12-29 20:21:45 +01:00
bb24ed53a4 Switched to hased tokens based on uuid (to be canged)
ref #56
2020-12-29 20:20:59 +01:00
1b74b21420 Renamed class
ref #56
2020-12-29 20:07:43 +01:00
b7cbe2a0b4 Adjusted the validation type
ref #56
2020-12-29 20:05:35 +01:00
500b94b44a Added a controller for stats clients (todo: put)
ref #56
2020-12-29 20:01:40 +01:00
641466a731 Added basic errors for stats clients
ref #56
2020-12-29 20:00:31 +01:00
e3ea83bb47 Removed async flag, b/c this doesn't need to perform anything async
ref #56
2020-12-29 19:57:19 +01:00
b6043744a9 Added STATSCLIENT as a new permission target
ref #56
2020-12-29 19:48:35 +01:00
2b38044271 Created a response for the statsClient
ref #56
2020-12-29 19:45:30 +01:00
4c3d2643c1 Added enabled flag for the stats clients
ref #56
2020-12-29 19:37:55 +01:00
e2cc0c0b80 Added Create action for the statsclients
ref #56
2020-12-29 19:34:14 +01:00
ce55dce011 Removed abstract flag from class
ref #56
2020-12-29 19:32:20 +01:00
a738c19316 Added the new statsClient class for stats api auth
ref #56
2020-12-29 19:29:16 +01:00
63b8176bdf Merge branch 'dev' into feature/56-stats_endpoint 2020-12-29 19:18:56 +01:00
bc76afafce Merge pull request 'Updates for the tag build pipeline' (#58) from dev into main
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: #58
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2020-12-29 18:08:09 +00:00
1f49ad43a1 Merge branch 'main' into dev
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2020-12-29 18:07:47 +00:00
6a762f570d Added team and org stats
ref #56
2020-12-29 16:08:50 +01:00
1b7424f750 Added stats endpoint with some basic stats (more to come) - to be tested
ref #56
2020-12-29 15:25:40 +01:00
bdd4f705be Adjusted return type, since async is no longer needed here (thanks to db relations)
ref #56
2020-12-29 15:23:29 +01:00
ded14b1b3b Changed method of triggering lib builds
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2020-12-29 14:57:00 +01:00
fbd3f615ad Changed docker image tag
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-29 14:56:37 +01:00
a22a7a19c2 Merge pull request 'Final fix for the tag pipeline triggers' (#57) from dev into main
Some checks failed
continuous-integration/drone/tag Build was killed
Reviewed-on: #57
Reviewed-by: odit_bot <bot@odit.services>
2020-12-29 13:18:39 +00:00
2d263814db Merge branch 'main' into dev 2020-12-29 13:17:33 +00:00
a79bed259b Moved to the official tag recognition
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-29 14:13:48 +01:00
f2970f4cd8 Added branch to when
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-23 19:09:01 +01:00
b3f741234e Back to when syntax for triggering tag builds
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-23 18:51:07 +01:00
6a8247f88a Now using the exact trigger snytax the gitea project uses
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-23 18:46:38 +01:00
b737fe6a08 Set trigger to ref tags only
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-23 18:39:44 +01:00
607630c4f9 Tried switching to global when
Some checks failed
continuous-integration/drone/push Build was killed
2020-12-23 18:38:01 +01:00
a7976c0ee2 Switched from trigger to when
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-23 18:36:37 +01:00
b51da15007 Added pushing to tags as trigger
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-23 18:33:56 +01:00
5ed5f181d1 Added tag as ref to tag build
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-23 18:32:32 +01:00
e33076c04d Removed push from tag build triggers
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-23 18:30:08 +01:00
ae35f50da2 Added push as drone tag build event trigger
Some checks failed
continuous-integration/drone/push Build was killed
2020-12-23 18:28:10 +01:00
cc5d90cb4f Merge pull request 'Bugfix for the release pipeline (no other changes)' (#55) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #55
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2020-12-23 17:18:29 +00:00
c33236c516 Merge branch 'main' into dev
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2020-12-23 18:11:47 +01:00
eee2bbcac7 Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-23 18:11:23 +01:00
519d11beef Removed the branch requirements from dev
ref #47
2020-12-23 18:11:20 +01:00
cbed5fc0b2 Merge pull request 'Merge alpha 0.0.5 to master' (#54) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #54
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>

ref #47
2020-12-23 17:05:32 +00:00
59fdfe9f40 Merge branch 'main' into dev
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2020-12-23 17:02:12 +00:00
c93e93be31 Set package version (+openapi version)
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ref #47
2020-12-23 18:00:53 +01:00
d3760f7b80 Merge pull request 'feature/52-alternative_openapi_viewers' (#53) from feature/52-alternative_openapi_viewers into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #53
closes #52
2020-12-23 16:57:17 +00:00
11c7d041ef 🎨 fixed landing html + styling
All checks were successful
continuous-integration/drone/pr Build is passing
ref #52
2020-12-23 17:55:44 +01:00
9ab6eb5314 Added tests for the api docs
All checks were successful
continuous-integration/drone/pr Build is passing
ref #52
2020-12-23 17:01:18 +01:00
ce0500ef8c Removed the firsttests jest tests (they were redundant)
ref #52
2020-12-23 17:01:03 +01:00
0b4d30b3f3 Updated the openapi json path for the ci testing script
Some checks failed
continuous-integration/drone/pr Build is failing
ref #52
2020-12-23 16:54:45 +01:00
bb70bf58fb Added the static files to the build step
Some checks failed
continuous-integration/drone/pr Build is failing
ref #52
2020-12-23 15:43:03 +01:00
9fc282d858 Removed everything concerning the swaggerUI express middleware
ref #52
2020-12-23 15:21:55 +01:00
39ad43bbb2 switched over to using the static deployment of swaggerUI
ref #52
2020-12-23 15:20:06 +01:00
bd46a48f76 Merge branch 'feature/52-alternative_openapi_viewers' of git.odit.services:lfk/backend into feature/52-alternative_openapi_viewers 2020-12-23 15:11:17 +01:00
ebedea97ed Added very basic api doc chooser
ref #52
2020-12-23 15:11:14 +01:00
5c3c3eb167 Added very basic api doc chooser
ref #52
2020-12-23 15:11:04 +01:00
d8e38f404d Renamed the package to fit the scheme for the project
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-22 20:43:49 +01:00
aa1042ca51 Merge pull request 'feature/49-openapi_cookie_schema' (#51) from feature/49-openapi_cookie_schema into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #51
closes #49
2020-12-22 19:33:03 +00:00
9994f8ddc4 Merge branch 'dev' into feature/49-openapi_cookie_schema
All checks were successful
continuous-integration/drone/pr Build is passing
2020-12-22 19:32:35 +00:00
3ac536ef23 Merge pull request 'feature/45-auth_tests' (#50) from feature/45-auth_tests into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #50
closes #45
2020-12-22 19:32:20 +00:00
5d75f70296 fixed typo
All checks were successful
continuous-integration/drone/pr Build is passing
ref #49
2020-12-22 20:29:18 +01:00
c34bde7d4f Fixed typo
All checks were successful
continuous-integration/drone/pr Build is passing
ref #49
2020-12-22 20:19:28 +01:00
1f061c7ea6 Updated the openapi descriptions for all group routes
ref #49
2020-12-22 20:18:30 +01:00
578f9301db Updated the openapi descriptions for all user routes
ref #49
2020-12-22 20:13:16 +01:00
9b47f3ab05 Updated the openapi descriptions for all track routes
ref #49
2020-12-22 20:07:41 +01:00
84b97bee8d Updated the openapi descriptions for all status routes
ref #49
2020-12-22 20:05:29 +01:00
767841d405 Merge branch 'feature/49-openapi_cookie_schema' of git.odit.services:lfk/backend into feature/49-openapi_cookie_schema
# Conflicts:
#	src/controllers/RunnerTeamController.ts
2020-12-22 20:04:19 +01:00
16e5b6921d Updated the openapi descriptions for all team routes
ref #49
2020-12-22 20:04:08 +01:00
58a12c7fa1 Updated the openapi descriptions for all team routes
ref #49
2020-12-22 20:03:49 +01:00
f256dec121 Updated the openapi descriptions for all organisation routes
ref #49
2020-12-22 20:01:25 +01:00
9bb4865b2d Merge branch 'feature/49-openapi_cookie_schema' of git.odit.services:lfk/backend into feature/49-openapi_cookie_schema
# Conflicts:
#	src/controllers/RunnerController.ts
2020-12-22 19:58:25 +01:00
66631f5e0a Updated the openapi descriptions for all runner routes
ref #49
2020-12-22 19:57:46 +01:00
8de35f3431 Updated the openapi descriptions for all runner routes
ref #49
2020-12-22 19:55:37 +01:00
05319e6f6e Updated the openapi descriptions for all permission routes
ref #49
2020-12-22 19:51:37 +01:00
b7827fef54 Updated the openapi descriptions for all import routes
ref #49
2020-12-22 19:45:09 +01:00
a4ddeee8e4 Fixed uniqueness error
All checks were successful
continuous-integration/drone/pr Build is passing
ref #45
2020-12-22 19:38:12 +01:00
50f2462eb9 Updated the openapi descriptions for all auth routes
ref #49
2020-12-22 19:23:35 +01:00
dae51cfd47 Added openapi cookie security schema
ref #49
2020-12-22 19:13:20 +01:00
e1341fc126 Merge branch 'dev' into feature/45-auth_tests
Some checks failed
continuous-integration/drone/pr Build is failing
2020-12-22 18:50:13 +01:00
a9dbf1d0d2 Added login test after logout
Some checks failed
continuous-integration/drone/pr Build is failing
ref #45
2020-12-22 18:49:10 +01:00
c6ecde29b5 Added auth reset tests
ref #45
2020-12-22 18:48:54 +01:00
13949af938 Added auth refresh tests
ref #45
2020-12-22 18:29:23 +01:00
3c003a60b2 Added logut tests
ref #45
2020-12-22 18:26:20 +01:00
69796a888f Added wron password auth test
ref #45
2020-12-22 17:04:22 +01:00
a85e914759 Added validator as a explicit dependency, b/c pnpm doesn't fallback to peer dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-22 16:57:01 +01:00
af2744885f added the first login tests
ref #45
2020-12-22 16:56:02 +01:00
8d73a9dd59 Merge pull request 'feature/40-pw_reset' (#48) from feature/40-pw_reset into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #48
closes #40
2020-12-22 15:29:43 +00:00
853876a09c Merge branch 'dev' into feature/40-pw_reset
All checks were successful
continuous-integration/drone/pr Build is passing
2020-12-22 16:05:27 +01:00
cdc90b0770 Merge pull request 'feature/43-postal_from_env' (#46) from feature/43-postal_from_env into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #46
closes #43
2020-12-22 14:55:37 +00:00
d0cfc16f8b Merge branch 'dev' into feature/43-postal_from_env
All checks were successful
continuous-integration/drone/pr Build is passing
2020-12-22 15:55:12 +01:00
ce5f4b467d Updated ci to trigger the builds for the new libs
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-22 15:44:00 +01:00
84a7f30a60 Merge branch 'dev' into feature/43-postal_from_env
All checks were successful
continuous-integration/drone/pr Build is passing
2020-12-22 12:41:15 +01:00
f3008979f3 Added the POSTALCODE_COUNTRYCODE to the sample and ci env files
ref #43
2020-12-22 12:40:11 +01:00
b8c93bf476 Implemented the getter for loading the postalcodelocale from env
ref #43
2020-12-22 12:38:53 +01:00
146787fd66 Added comments
All checks were successful
continuous-integration/drone/pr Build is passing
ref #40
2020-12-22 11:48:06 +01:00
9458b774ea Removed the user disableing
ref #40
2020-12-22 11:35:33 +01:00
bf4250babd All things auth now check if the user is disabled
ref #40
2020-12-22 11:29:52 +01:00
a16c4c564a Users now can be disabled from the start
ref #40
2020-12-22 11:27:21 +01:00
8d860cb2e1 Fixed weired query behaviour
ref #40
2020-12-22 11:26:45 +01:00
2f7b0d5606 Removed bs enabled check
ref #40
2020-12-22 11:20:11 +01:00
4b9bfe3b79 Now disableing users while they're in the process of resetting their password
ref #40
2020-12-22 11:18:31 +01:00
17ee682029 Implemented a password reset timeout
ref #40
2020-12-22 11:12:24 +01:00
48685451be Set reset token expiry to 15 mins
rer #40
2020-12-22 11:07:01 +01:00
5aad581c2d Implemented toe password reset route
ref #40
2020-12-22 10:57:25 +01:00
caeb17311b Implemented basic password reset
ref #40
2020-12-22 10:57:08 +01:00
5aa83fe2f0 Renamed the return variable to fit the class
ref #40
2020-12-22 10:44:43 +01:00
aef8485f59 Renamed the password reset token creation class to better fit the scheme
ref #40
2020-12-22 10:39:42 +01:00
61aff5e629 Added a password reset token request route
ref #40
2020-12-22 10:39:17 +01:00
aa146cd6c1 Added a basic pw reset action
ref #40
2020-12-22 10:38:48 +01:00
6042089074 Added pw reset jwt generation
ref #40
2020-12-22 10:24:25 +01:00
e3a5b41b5e Merge pull request 'latest work' (#20) from dev into main
Reviewed-on: #20
Reviewed-by: Nicolai Ort <info@nicolai-ort.com>
2020-12-09 18:49:30 +00:00
122 changed files with 20448 additions and 369 deletions

View File

@@ -11,7 +11,7 @@ steps:
- git checkout $DRONE_SOURCE_BRANCH
- mv .env.ci .env
- name: run tests
image: node:alpine
image: node:14.15.1-alpine3.12
commands:
- yarn
- yarn test:ci
@@ -37,11 +37,23 @@ steps:
tags:
- dev
registry: registry.odit.services
when:
branch:
- dev
event:
- push
- 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]
author_email: bot@odit.services
remote: git@git.odit.services:lfk/backend.git
ssh_key:
from_secret: GITLAB_SSHKEY
trigger:
branch:
@@ -90,22 +102,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

View File

@@ -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=null

View File

@@ -5,4 +5,5 @@ DB_PORT=bla
DB_USER=bla
DB_PASSWORD=bla
DB_NAME=bla
NODE_ENV=production
NODE_ENV=production
POSTALCODE_COUNTRYCODE=null

6
.gitignore vendored
View File

@@ -132,5 +132,7 @@ build
*.sqlite
*.sqlite-jurnal
docs
lib
/docs
lib
/oss-attribution
*.tmp

1296
licenses.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@lfk/backend",
"version": "1.0.0",
"name": "@odit/lfk-backend",
"version": "0.0.9",
"main": "src/app.ts",
"repository": "https://git.odit.services/lfk/backend",
"author": {
@@ -40,23 +40,24 @@
"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",
"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.1",
"validator": "^13.5.2"
},
"devDependencies": {
"@odit/license-exporter": "^0.0.8",
"@types/cors": "^2.8.8",
"@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/uuid": "^8.3.0",
"axios": "^0.21.0",
"cp-cli": "^2.0.0",
"jest": "^26.6.3",
"nodemon": "^2.0.6",
"rimraf": "^2.7.1",
@@ -68,13 +69,14 @@
},
"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"
},
"nodemonConfig": {
"ignore": [
@@ -82,4 +84,4 @@
"docs/*"
]
}
}
}

View File

@@ -4,9 +4,9 @@ import fs from "fs";
import "reflect-metadata";
import { createExpressServer, getMetadataArgsStorage } from "routing-controllers";
import { routingControllersToSpec } from 'routing-controllers-openapi';
import authchecker from "./authchecker";
import { config } from './config';
import { ErrorHandler } from './middlewares/ErrorHandler';
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({
@@ -15,7 +15,7 @@ createExpressServer({
development: config.development,
cors: true,
routePrefix: "/api",
controllers: [`${__dirname}/controllers/*.${CONTROLLERS_FILE_EXTENSION}`],
controllers: [`${__dirname}/../src/controllers/*.${CONTROLLERS_FILE_EXTENSION}`],
});
const storage = getMetadataArgsStorage();
@@ -36,14 +36,31 @@ const spec = routingControllersToSpec(
"AuthToken": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
"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: "1.0.0",
version: "0.0.8",
},
}
);

View File

@@ -1,9 +1,9 @@
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';
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';

View File

@@ -1,10 +1,13 @@
import { config as configDotenv } from 'dotenv';
import ValidatorJS from 'validator';
configDotenv();
export const config = {
internal_port: parseInt(process.env.APP_PORT) || 4010,
development: process.env.NODE_ENV === "production",
jwt_secret: process.env.JWT_SECRET || "secretjwtsecret",
phone_validation_countrycode: process.env.PHONE_COUNTRYCODE || "ZZ"
phone_validation_countrycode: process.env.PHONE_COUNTRYCODE || "ZZ",
postalcode_validation_countrycode: getPostalCodeLocale()
}
let errors = 0
if (typeof config.internal_port !== "number") {
@@ -19,4 +22,13 @@ if (config.phone_validation_countrycode.length !== 2) {
if (typeof config.development !== "boolean") {
errors++
}
function getPostalCodeLocale(): any {
try {
const stringArray: String[] = ValidatorJS.isPostalCodeLocales;
let index = stringArray.indexOf(process.env.POSTALCODE_COUNTRYCODE);
return ValidatorJS.isPostalCodeLocales[index];
} catch (error) {
return null;
}
}
export let e = errors

View File

@@ -1,10 +1,12 @@
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 { CreateResetToken } from '../models/actions/CreateResetToken';
import { HandleLogout } from '../models/actions/HandleLogout';
import { RefreshAuth } from '../models/actions/RefreshAuth';
import { ResetPassword } from '../models/actions/ResetPassword';
import { Auth } from '../models/responses/ResponseAuth';
import { Logout } from '../models/responses/ResponseLogout';
@@ -20,7 +22,7 @@ export class AuthController {
@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;
@@ -63,8 +65,8 @@ export class AuthController {
@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(Auth)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@OpenAPI({ description: "Request a password reset token. <br> This will provide you with a reset token that you can use by posting to /api/auth/reset/{token}." })
async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken) {
//This really shouldn't just get returned, but sent via mail or sth like that. But for dev only this is fine.
return { "resetToken": await passwordReset.toResetToken() };
}
@Post("/reset/:token")
@ResponseSchema(Auth)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@OpenAPI({ description: "Reset a user's utilising a valid password reset token. <br> This will set the user's password to the one you provided in the body. <br> To get a reset token post to /api/auth/reset with your username." })
async resetPassword(@Param("token") token: string, @Body({ validate: true }) passwordReset: ResetPassword) {
passwordReset.resetToken = token;
return await passwordReset.resetPassword();
}
}

View File

@@ -0,0 +1,105 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { DonorIdsNotMatchingError, DonorNotFoundError } from '../errors/DonorErrors';
import { CreateDonor } from '../models/actions/CreateDonor';
import { UpdateDonor } from '../models/actions/UpdateDonor';
import { Donor } from '../models/entities/Donor';
import { ResponseDonor } from '../models/responses/ResponseDonor';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
@JsonController('/donors')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class DonorController {
private donorRepository: Repository<Donor>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.donorRepository = getConnectionManager().get().getRepository(Donor);
}
@Get()
@Authorized("DONOR:GET")
@ResponseSchema(ResponseDonor, { isArray: true })
@OpenAPI({ description: 'Lists all runners from all teams/orgs. <br> This includes the runner\'s group and distance ran.' })
async getAll() {
let responseDonors: ResponseDonor[] = new Array<ResponseDonor>();
const donors = await this.donorRepository.find();
donors.forEach(donor => {
responseDonors.push(new ResponseDonor(donor));
});
return responseDonors;
}
@Get('/:id')
@Authorized("DONOR:GET")
@ResponseSchema(ResponseDonor)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@OnUndefined(DonorNotFoundError)
@OpenAPI({ description: 'Lists all information about the runner whose id got provided.' })
async getOne(@Param('id') id: number) {
let donor = await this.donorRepository.findOne({ id: id })
if (!donor) { throw new DonorNotFoundError(); }
return new ResponseDonor(donor);
}
@Post()
@Authorized("DONOR:CREATE")
@ResponseSchema(ResponseDonor)
@OpenAPI({ description: 'Create a new runner. <br> Please remeber to provide the runner\'s group\'s id.' })
async post(@Body({ validate: true }) createRunner: CreateDonor) {
let donor;
try {
donor = await createRunner.toDonor();
} catch (error) {
throw error;
}
donor = await this.donorRepository.save(donor)
return new ResponseDonor(await this.donorRepository.findOne(donor));
}
@Put('/:id')
@Authorized("DONOR:UPDATE")
@ResponseSchema(ResponseDonor)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@ResponseSchema(DonorIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the runner whose id you provided. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) donor: UpdateDonor) {
let oldDonor = await this.donorRepository.findOne({ id: id });
if (!oldDonor) {
throw new DonorNotFoundError();
}
if (oldDonor.id != donor.id) {
throw new DonorIdsNotMatchingError();
}
await this.donorRepository.save(await donor.updateDonor(oldDonor));
return new ResponseDonor(await this.donorRepository.findOne({ id: id }));
}
@Delete('/:id')
@Authorized("DONOR:DELETE")
@ResponseSchema(ResponseDonor)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the runner whose id you provided. <br> If no runner with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let donor = await this.donorRepository.findOne({ id: id });
if (!donor) { return null; }
const responseDonor = await this.donorRepository.findOne(donor);
if (!donor) {
throw new DonorNotFoundError();
}
//TODO: DELETE DONATIONS AND WARN FOR FORCE (https://git.odit.services/lfk/backend/issues/66)
await this.donorRepository.delete(donor);
return new ResponseDonor(responseDonor);
}
}

View File

@@ -10,7 +10,7 @@ import { RunnerController } from './RunnerController';
@Controller()
@Authorized(["RUNNER:IMPORT", "TEAM:IMPORT"])
@OpenAPI({ security: [{ "AuthToken": [] }] })
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class ImportController {
private runnerController: RunnerController;
@@ -26,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>();
@@ -41,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)
}
@@ -62,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>();
@@ -84,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);
}

View File

@@ -12,7 +12,7 @@ import { ResponsePrincipal } from '../models/responses/ResponsePrincipal';
@JsonController('/permissions')
@OpenAPI({ security: [{ "AuthToken": [] }] })
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class PermissionController {
private permissionRepository: Repository<Permission>;
@@ -26,7 +26,7 @@ export class PermissionController {
@Get()
@Authorized("PERMISSION:GET")
@ResponseSchema(ResponsePermission, { isArray: true })
@OpenAPI({ description: 'Lists all permissions.' })
@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'] });
@@ -42,7 +42,7 @@ export class PermissionController {
@ResponseSchema(ResponsePermission)
@ResponseSchema(PermissionNotFoundError, { statusCode: 404 })
@OnUndefined(PermissionNotFoundError)
@OpenAPI({ description: 'Returns a permissions of a specified id (if it exists)' })
@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(); }
@@ -54,7 +54,7 @@ export class PermissionController {
@Authorized("PERMISSION:CREATE")
@ResponseSchema(ResponsePermission)
@ResponseSchema(PrincipalNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new runnerTeam object (id will be generated automagicly).' })
@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 {
@@ -79,7 +79,7 @@ export class PermissionController {
@ResponseSchema(PrincipalNotFoundError, { statusCode: 404 })
@ResponseSchema(PermissionIdsNotMatchingError, { statusCode: 406 })
@ResponseSchema(PermissionNeedsPrincipalError, { statusCode: 406 })
@OpenAPI({ description: "Update a permission object (id can't be changed)." })
@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'] });
@@ -106,7 +106,7 @@ export class PermissionController {
@ResponseSchema(ResponsePermission)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete a specified permission (if it exists).' })
@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; }

View File

@@ -10,7 +10,7 @@ import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunner } from '../models/responses/ResponseRunner';
@JsonController('/runners')
@OpenAPI({ security: [{ "AuthToken": [] }] })
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class RunnerController {
private runnerRepository: Repository<Runner>;
@@ -24,7 +24,7 @@ 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'] });
@@ -39,7 +39,7 @@ export class RunnerController {
@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'] })
if (!runner) { throw new RunnerNotFoundError(); }
@@ -51,7 +51,7 @@ export class RunnerController {
@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 {
@@ -69,7 +69,7 @@ export class RunnerController {
@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'] });
@@ -90,7 +90,7 @@ export class RunnerController {
@ResponseSchema(ResponseRunner)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete a specified runner (if it exists).' })
@OpenAPI({ description: 'Delete the runner whose id you provided. <br> If no runner with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let runner = await this.runnerRepository.findOne({ id: id });
if (!runner) { return null; }

View File

@@ -12,7 +12,7 @@ import { RunnerTeamController } from './RunnerTeamController';
@JsonController('/organisations')
@OpenAPI({ security: [{ "AuthToken": [] }] })
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class RunnerOrganisationController {
private runnerOrganisationRepository: Repository<RunnerOrganisation>;
@@ -26,7 +26,7 @@ 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'] });
@@ -41,7 +41,7 @@ export class RunnerOrganisationController {
@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'] });
if (!runnerOrg) { throw new RunnerOrganisationNotFoundError(); }
@@ -51,7 +51,7 @@ export class RunnerOrganisationController {
@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 {
@@ -70,7 +70,7 @@ export class RunnerOrganisationController {
@ResponseSchema(ResponseRunnerOrganisation)
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerOrganisationIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update a runnerOrganisation object (id can't be changed)." })
@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 });
@@ -94,7 +94,7 @@ export class RunnerOrganisationController {
@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> 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; }

View File

@@ -11,7 +11,7 @@ import { RunnerController } from './RunnerController';
@JsonController('/teams')
@OpenAPI({ security: [{ "AuthToken": [] }] })
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class RunnerTeamController {
private runnerTeamRepository: Repository<RunnerTeam>;
@@ -25,7 +25,7 @@ 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'] });
@@ -40,7 +40,7 @@ export class RunnerTeamController {
@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(); }
@@ -50,7 +50,7 @@ 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 {
@@ -70,7 +70,7 @@ export class RunnerTeamController {
@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'] });
@@ -93,7 +93,7 @@ export class RunnerTeamController {
@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> 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; }

View File

@@ -0,0 +1,110 @@
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 ScanAuth from '../middlewares/ScanAuth';
import { CreateScan } from '../models/actions/CreateScan';
import { CreateTrackScan } from '../models/actions/CreateTrackScan';
import { UpdateScan } from '../models/actions/UpdateScan';
import { Scan } from '../models/entities/Scan';
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>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.scanRepository = getConnectionManager().get().getRepository(Scan);
}
@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', 'runner.scans', 'runner.scans.track'] });
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', 'runner.scans', 'runner.scans.track'] })
if (!scan) { throw new ScanNotFoundError(); }
return scan.toResponse();
}
@Post()
@UseBefore(ScanAuth)
@ResponseSchema(ResponseScan)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new scan. <br> Please remeber to provide the scan\'s runner\'s id and distance for normal scans.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async post(@Body({ validate: true }) createScan: CreateScan) {
let scan = await createScan.toScan();
scan = await this.scanRepository.save(scan);
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner'] })).toResponse();
}
@Post("/trackscans")
@UseBefore(ScanAuth)
@ResponseSchema(ResponseScan)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new track scan. <br> This is just a alias for posting /scans', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan) {
return this.post(createScan);
}
@Put('/:id')
@Authorized("SCAN:UPDATE")
@ResponseSchema(ResponseScan)
@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the scan 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.updateScan(oldScan));
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner'] })).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"] });
await this.scanRepository.delete(scan);
return responseScan.toResponse();
}
}

View 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/CreateScanStation';
import { UpdateScanStation } from '../models/actions/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.updateStation(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) {
scanController.remove(scan.id, force);
}
const responseStation = await this.stationRepository.findOne({ id: station.id }, { relations: ["track"] });
await this.stationRepository.delete(station);
return responseStation.toResponse();
}
}

View File

@@ -0,0 +1,75 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { StatsClientNotFoundError } from '../errors/StatsClientErrors';
import { TrackNotFoundError } from "../errors/TrackErrors";
import { CreateStatsClient } from '../models/actions/CreateStatsClient';
import { StatsClient } from '../models/entities/StatsClient';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseStatsClient } from '../models/responses/ResponseStatsClient';
@JsonController('/statsclients')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class StatsClientController {
private clientRepository: Repository<StatsClient>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.clientRepository = getConnectionManager().get().getRepository(StatsClient);
}
@Get()
@Authorized("STATSCLIENT:GET")
@ResponseSchema(ResponseStatsClient, { isArray: true })
@OpenAPI({ description: 'Lists all stats clients. Please remember that the key can only be viewed on creation.' })
async getAll() {
let responseClients: ResponseStatsClient[] = new Array<ResponseStatsClient>();
const clients = await this.clientRepository.find();
clients.forEach(clients => {
responseClients.push(new ResponseStatsClient(clients));
});
return responseClients;
}
@Get('/:id')
@Authorized("STATSCLIENT:GET")
@ResponseSchema(ResponseStatsClient)
@ResponseSchema(StatsClientNotFoundError, { statusCode: 404 })
@OnUndefined(StatsClientNotFoundError)
@OpenAPI({ description: "Lists all information about the stats client whose id got provided. Please remember that the key can only be viewed on creation." })
async getOne(@Param('id') id: number) {
let client = await this.clientRepository.findOne({ id: id });
if (!client) { throw new TrackNotFoundError(); }
return new ResponseStatsClient(client);
}
@Post()
@Authorized("STATSCLIENT:CREATE")
@ResponseSchema(ResponseStatsClient)
@OpenAPI({ description: "Create a new stats client. <br> Please remember that the client\'s key will be generated automaticly and that it can only be viewed on creation." })
async post(
@Body({ validate: true })
client: CreateStatsClient
) {
let newClient = await this.clientRepository.save(await client.toStatsClient());
let responseClient = new ResponseStatsClient(newClient);
responseClient.key = newClient.cleartextkey;
return responseClient;
}
@Delete('/:id')
@Authorized("STATSCLIENT:DELETE")
@ResponseSchema(ResponseStatsClient)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: "Delete the stats client whose id you provided. <br> If no client with this id exists it will just return 204(no content)." })
async remove(@Param("id") id: number) {
let client = await this.clientRepository.findOne({ id: id });
if (!client) { return null; }
await this.clientRepository.delete(client);
return new ResponseStatsClient(client);
}
}

View 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;
}
}

View File

@@ -6,7 +6,7 @@ import { getConnection } from 'typeorm';
export class StatusController {
@Get()
@OpenAPI({ description: "Lists all tracks." })
@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 {

View File

@@ -1,15 +1,16 @@
import { Authorized, 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 { TrackHasScanStationsError, TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors";
import { CreateTrack } from '../models/actions/CreateTrack';
import { UpdateTrack } from '../models/actions/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')
@OpenAPI({ security: [{ "AuthToken": [] }] })
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class TrackController {
private trackRepository: Repository<Track>;
@@ -23,6 +24,7 @@ export class TrackController {
@Get()
@Authorized("TRACK:GET")
@ResponseSchema(ResponseTrack, { isArray: true })
@OpenAPI({ description: 'Lists all tracks.' })
async getAll() {
let responseTracks: ResponseTrack[] = new Array<ResponseTrack>();
const tracks = await this.trackRepository.find();
@@ -37,7 +39,7 @@ export class TrackController {
@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(); }
@@ -47,7 +49,8 @@ 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
@@ -60,20 +63,21 @@ export class TrackController {
@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.updateTrack(oldTrack));
await this.trackRepository.save(track);
return new ResponseTrack(track);
return new ResponseTrack(await this.trackRepository.findOne({ id: id }));
}
@Delete('/:id')
@@ -81,11 +85,20 @@ export class TrackController {
@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 scanController = new ScanStationController;
for (let station of trackStations) {
scanController.remove(station.id, force);
}
await this.trackRepository.delete(track);
return new ResponseTrack(track);
}

View File

@@ -12,7 +12,7 @@ import { PermissionController } from './PermissionController';
@JsonController('/users')
@OpenAPI({ security: [{ "AuthToken": [] }] })
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class UserController {
private userRepository: Repository<User>;
@@ -26,10 +26,10 @@ export class UserController {
@Get()
@Authorized("USER:GET")
@ResponseSchema(User, { isArray: true })
@OpenAPI({ description: 'Lists all users.' })
@OpenAPI({ description: 'Lists all users. <br> This includes their groups and permissions directly granted to them (if existing/associated).' })
async getAll() {
let responseUsers: ResponseUser[] = new Array<ResponseUser>();
const users = await this.userRepository.find({ relations: ['permissions', 'groups'] });
const users = await this.userRepository.find({ relations: ['permissions', 'groups', 'groups.permissions'] });
users.forEach(user => {
responseUsers.push(new ResponseUser(user));
});
@@ -41,9 +41,9 @@ export class UserController {
@ResponseSchema(User)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Returns a user of a specified id (if it exists)' })
@OpenAPI({ description: 'Lists all information about the user whose id got provided. <br> Please remember that only permissions granted directly to the user will show up here, not permissions inherited from groups.' })
async getOne(@Param('id') id: number) {
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] })
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] })
if (!user) { throw new UserNotFoundError(); }
return new ResponseUser(user);
}
@@ -52,7 +52,7 @@ export class UserController {
@Authorized("USER:CREATE")
@ResponseSchema(User)
@ResponseSchema(UserGroupNotFoundError)
@OpenAPI({ description: 'Create a new user object (id will be generated automagicly).' })
@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 {
@@ -62,7 +62,7 @@ export class UserController {
}
user = await this.userRepository.save(user)
return new ResponseUser(await this.userRepository.findOne(user, { relations: ['permissions', 'groups'] }));
return new ResponseUser(await this.userRepository.findOne({ id: user.id }, { relations: ['permissions', 'groups'] }));
}
@Put('/:id')
@@ -70,7 +70,7 @@ export class UserController {
@ResponseSchema(User)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update a user object (id can't be changed)." })
@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 });
@@ -91,7 +91,7 @@ export class UserController {
@ResponseSchema(User)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete a user runner (if it exists).' })
@OpenAPI({ description: 'Delete the user whose id you provided. <br> If there are any permissions directly granted to the user they will get deleted as well. <br> If no user with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let user = await this.userRepository.findOne({ id: id });
if (!user) { return null; }

View File

@@ -11,7 +11,7 @@ import { PermissionController } from './PermissionController';
@JsonController('/usergroups')
@OpenAPI({ security: [{ "AuthToken": [] }] })
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class UserGroupController {
private userGroupsRepository: Repository<UserGroup>;
@@ -25,9 +25,9 @@ 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')
@@ -35,16 +35,16 @@ export class UserGroupController {
@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 {
@@ -61,9 +61,9 @@ export class UserGroupController {
@ResponseSchema(UserGroup)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(UserGroupIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update a usergroup object (id can't be changed)." })
@OpenAPI({ description: "Update the group whose id you provided. <br> To change the permissions granted to the group please use /api/permissions instead. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @EntityFromBody() userGroup: UserGroup) {
let oldUserGroup = await this.userGroupsRepository.findOne({ id: id });
let oldUserGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
if (!oldUserGroup) {
throw new UserGroupNotFoundError()
@@ -82,11 +82,11 @@ export class UserGroupController {
@ResponseSchema(ResponseUserGroup)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete a specified usergroup (if it exists).' })
@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 });
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 responseGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] });
const permissionControler = new PermissionController();
for (let permission of responseGroup.permissions) {

View File

@@ -115,4 +115,26 @@ export class RefreshTokenCountInvalidError extends NotAcceptableError {
@IsString()
message = "Refresh token count is invalid."
}
/**
* Error to throw when someone tryes to reset a user's password more than once in 15 minutes.
*/
export class ResetAlreadyRequestedError extends NotAcceptableError {
@IsString()
name = "ResetAlreadyRequestedError"
@IsString()
message = "You already requested a password reset in the last 15 minutes. \n Please wait until the old reset code expires before requesting a new one."
}
/**
* Error to throw when someone tries a disabled user's password or login as a disabled user.
*/
export class UserDisabledError extends NotAcceptableError {
@IsString()
name = "UserDisabledError"
@IsString()
message = "This user is currently disabled. \n Please contact your administrator if this is a mistake."
}

36
src/errors/DonorErrors.ts Normal file
View File

@@ -0,0 +1,36 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a donor couldn't be found.
*/
export class DonorNotFoundError extends NotFoundError {
@IsString()
name = "DonorNotFoundError"
@IsString()
message = "Donor not found!"
}
/**
* Error to throw when two donors' ids don't match.
* Usually occurs when a user tries to change a donor's id.
*/
export class DonorIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "DonorIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a donor's id: This isn't allowed!"
}
/**
* Error to throw when a donor needs a receipt, but no address is associated with them.
*/
export class DonorReceiptAddressNeededError extends NotAcceptableError {
@IsString()
name = "DonorReceiptAddressNeededError"
@IsString()
message = "An address is needed to create a receipt for a donor. \n You didn't provide one."
}

25
src/errors/ScanErrors.ts Normal file
View 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!"
}

View File

@@ -0,0 +1,36 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw, when a non-existant 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."
}

View File

@@ -0,0 +1,25 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw, when a non-existant stats client get's loaded.
*/
export class StatsClientNotFoundError extends NotFoundError {
@IsString()
name = "StatsClientNotFoundError"
@IsString()
message = "The stats client you provided couldn't be located in the system. \n Please check your request."
}
/**
* Error to throw when two stats clients' ids don't match.
* Usually occurs when a user tries to change a stats client's id.
*/
export class StatsClientIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "StatsClientIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a stats client's id: This isn't allowed!"
}

View File

@@ -22,4 +22,23 @@ export class TrackIdsNotMatchingError extends NotAcceptableError {
@IsString()
message = "The ids don't match! \n And if you wanted to change a track's id: This isn't allowed"
}
/**
* Error to throw when a track's lap time is set to a negative value.
*/
export class TrackLapTimeCantBeNegativeError extends NotAcceptableError {
@IsString()
name = "TrackLapTimeCantBeNegativeError"
@IsString()
message = "The minimum lap time you provided is negative - That isn't possible. \n If you wanted to disable it: Just set it to 0/null."
}
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."
}

View File

@@ -33,6 +33,20 @@ export class JwtCreator {
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)
}
}
/**
@@ -92,23 +106,6 @@ export class JwtUser {
this.refreshTokenCount = user.refreshTokenCount;
this.uuid = user.uuid;
this.profilePic = user.profilePic;
this.permissions = this.getPermissions(user);
}
/**
* Handels getting the permissions granted to this user (direct or indirect).
* @param user User which's permissions shall be gotten.
*/
public getPermissions(user: User): string[] {
let returnPermissions: string[] = new Array<string>();
for (let permission of user.permissions) {
returnPermissions.push(permission.toString());
}
for (let group of user.groups) {
for (let permission of group.permissions) {
returnPermissions.push(permission.toString());
}
}
return Array.from(new Set(returnPermissions));
this.permissions = user.allPermissions;
}
}

View File

@@ -1,8 +1,8 @@
import { validationMetadatasToSchemas } from "class-validator-jsonschema";
import { Application } from "express";
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";
/**
* Loader for everything openapi related - from creating the schema to serving it via a static route and swaggerUiExpress.
@@ -29,28 +29,35 @@ export default async (app: Application) => {
"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: "1.0.0",
version: "0.0.8",
},
}
);
//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) => {
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;
};

View File

@@ -0,0 +1,68 @@
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 handels the authentification of scan station api tokens.
* The tokens have to be provided via Bearer auth header.
* @param req Express request object.
* @param res Express response object.
* @param next Next function to call on success.
*/
const 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-existant 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-existant 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;

View File

@@ -0,0 +1,65 @@
import * as argon2 from "argon2";
import { Request, Response } from 'express';
import { getConnectionManager } from 'typeorm';
import { StatsClient } from '../models/entities/StatsClient';
import authchecker from './authchecker';
/**
* This middleware handels the authentification of stats client api tokens.
* The tokens have to be provided via Bearer auth header.
* @param req Express request object.
* @param res Express response object.
* @param next Next function to call on success.
*/
const StatsAuth = async (req: Request, res: Response, next: () => void) => {
let provided_token: string = req.headers["authorization"];
if (provided_token == "" || provided_token === undefined || provided_token === null) {
res.status(401).send("No api token provided.");
return;
}
try {
provided_token = provided_token.replace("Bearer ", "");
} catch (error) {
res.status(401).send("No valid jwt or api token provided.");
return;
}
let prefix = "";
try {
prefix = provided_token.split(".")[0];
}
finally {
if (prefix == "" || prefix == undefined || prefix == null) {
res.status(401).send("Api token non-existant or invalid syntax.");
return;
}
}
const client = await getConnectionManager().get().getRepository(StatsClient).findOne({ prefix: prefix });
if (!client) {
let user_authorized = false;
try {
let action = { request: req, response: res, context: null, next: next }
user_authorized = await authchecker(action, ["RUNNER:GET", "TEAM:GET", "ORGANISATION:GET"]);
}
finally {
if (user_authorized == false) {
res.status(401).send("Api token non-existant or invalid syntax.");
return;
}
else {
next();
}
}
}
else {
if (!(await argon2.verify(client.key, provided_token))) {
res.status(401).send("Api token invalid.");
return;
}
next();
}
}
export default StatsAuth;

View File

@@ -2,10 +2,10 @@ 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, UserNonexistantOrRefreshtokenInvalidError } from './errors/AuthError';
import { JwtCreator, JwtUser } from './jwtcreator';
import { User } from './models/entities/User';
import { config } from '../config';
import { IllegalJWTError, NoPermissionError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from '../errors/AuthError';
import { JwtCreator, JwtUser } from '../jwtcreator';
import { User } from '../models/entities/User';
/**
* Handels authorisation verification via jwt's for all api endpoints using the @Authorized decorator.
@@ -31,6 +31,7 @@ const authchecker = async (action: Action, permissions: string[] | string) => {
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 = {}
@@ -63,6 +64,7 @@ const refresh = async (action: Action) => {
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);

View File

@@ -1,4 +1,5 @@
import { IsNotEmpty, IsOptional, IsPostalCode, IsString } from 'class-validator';
import { config } from '../../config';
import { Address } from '../entities/Address';
/**
@@ -35,7 +36,7 @@ export class CreateAddress {
*/
@IsString()
@IsNotEmpty()
@IsPostalCode("DE")
@IsPostalCode(config.postalcode_validation_countrycode)
postalcode: string;
/**

View File

@@ -1,7 +1,7 @@
import * as argon2 from "argon2";
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { InvalidCredentialsError, PasswordNeededError, UserNotFoundError } from '../../errors/AuthError';
import { InvalidCredentialsError, PasswordNeededError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
import { JwtCreator } from '../../jwtcreator';
import { User } from '../entities/User';
@@ -55,6 +55,7 @@ export class CreateAuth {
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();
}

View File

@@ -0,0 +1,38 @@
import { IsBoolean, IsOptional } from 'class-validator';
import { DonorReceiptAddressNeededError } from '../../errors/DonorErrors';
import { Donor } from '../entities/Donor';
import { CreateParticipant } from './CreateParticipant';
/**
* This classed is used to create a new Donor entity from a json body (post request).
*/
export class CreateDonor extends CreateParticipant {
/**
* Does this donor need a receipt?
*/
@IsBoolean()
@IsOptional()
receiptNeeded?: boolean = false;
/**
* Creates a new Donor entity from this.
*/
public async toDonor(): Promise<Donor> {
let newDonor: Donor = new Donor();
newDonor.firstname = this.firstname;
newDonor.middlename = this.middlename;
newDonor.lastname = this.lastname;
newDonor.phone = this.phone;
newDonor.email = this.email;
newDonor.address = await this.getAddress();
newDonor.receiptNeeded = this.receiptNeeded;
if (this.receiptNeeded == true && this.address == null) {
throw new DonorReceiptAddressNeededError()
}
return newDonor;
}
}

View 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;
}
}

View File

@@ -41,7 +41,7 @@ export class CreateRunnerOrganisation extends CreateRunnerGroup {
newRunnerOrganisation.name = this.name;
newRunnerOrganisation.contact = await this.getContact();
newRunnerOrganisation.address = await this.getAddress();
// newRunnerOrganisation.address = await this.getAddress();
return newRunnerOrganisation;
}

View 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.
* 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 toScan(): 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;
}
}

View 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.
*/
@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;
}
}

View File

@@ -0,0 +1,33 @@
import * as argon2 from "argon2";
import { IsOptional, IsString } from 'class-validator';
import crypto from 'crypto';
import * as uuid from 'uuid';
import { StatsClient } from '../entities/StatsClient';
/**
* This classed is used to create a new StatsClient entity from a json body (post request).
*/
export class CreateStatsClient {
/**
* The new client's description.
*/
@IsString()
@IsOptional()
description?: string;
/**
* Converts this to a StatsClient entity.
*/
public async toStatsClient(): Promise<StatsClient> {
let newClient: StatsClient = new StatsClient();
newClient.description = this.description;
let newUUID = uuid.v4().toUpperCase();
newClient.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
newClient.key = await argon2.hash(newClient.prefix + "." + newUUID);
newClient.cleartextkey = newClient.prefix + "." + newUUID;
return newClient;
}
}

View File

@@ -1,4 +1,5 @@
import { IsInt, IsNotEmpty, IsPositive, IsString } from 'class-validator';
import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator';
import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors';
import { Track } from '../entities/Track';
/**
@@ -19,6 +20,14 @@ export class CreateTrack {
@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.
*/
@@ -27,6 +36,10 @@ export class CreateTrack {
newTrack.name = this.name;
newTrack.distance = this.distance;
newTrack.minimumLapTime = this.minimumLapTime;
if (this.minimumLapTime < 0) {
throw new TrackLapTimeCantBeNegativeError();
}
return newTrack;
}

View File

@@ -0,0 +1,84 @@
import { IsNotEmpty } from 'class-validator';
import { getConnection } from 'typeorm';
import { RunnerNotFoundError } from '../../errors/RunnerErrors';
import { RunnerCard } from '../entities/RunnerCard';
import { ScanStation } from '../entities/ScanStation';
import { TrackScan } from '../entities/TrackScan';
import { CreateScan } from './CreateScan';
/**
* This classed is used to create a new Scan entity from a json body (post request).
*/
export class CreateTrackScan extends CreateScan {
/**
* The scan's associated track.
* This is used to determine the scan's distance.
*/
@IsNotEmpty()
track: number;
/**
* The runnerCard associated with the scan.
* This get's saved for documentation and management purposes.
*/
@IsNotEmpty()
card: number;
/**
* The scanning station that created the scan.
* Mainly used for logging and traceing back scans (or errors)
*/
@IsNotEmpty()
station: number;
/**
* Creates a new Track entity from this.
*/
public async toScan(): 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 = new Date(Date.now()).toString();
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 Error();
}
return track;
}
public async getStation(): Promise<ScanStation> {
const track = await getConnection().getRepository(ScanStation).findOne({ id: this.card }, { relations: ["track"] });
if (!track) {
throw new Error();
}
return track;
}
public async validateScan(scan: TrackScan): Promise<boolean> {
const scans = await getConnection().getRepository(TrackScan).find({ where: { runner: scan.runner }, relations: ["track"] });
if (scans.length == 0) { return true; }
const newestScan = scans[0];
if ((new Date(scan.timestamp).getTime() - new Date(newestScan.timestamp).getTime()) > scan.track.minimumLapTime) {
return true;
}
return false;
}
}

View File

@@ -1,5 +1,5 @@
import * as argon2 from "argon2";
import { IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { IsBoolean, IsEmail, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import * as uuid from 'uuid';
import { config } from '../../config';
@@ -63,6 +63,14 @@ export class CreateUser {
@IsString()
password: string;
/**
* Will the new user be enabled from the start?
* Default: true
*/
@IsBoolean()
@IsOptional()
enabled?: boolean = true;
/**
* The new user's groups' id(s).
* You can provide either one groupId or an array of groupIDs.
@@ -70,7 +78,13 @@ export class CreateUser {
@IsOptional()
groups?: number[] | number
//TODO: ProfilePics
/**
* The user's profile pic (or rather a url pointing to it).
*/
@IsString()
@IsUrl()
@IsOptional()
profilePic?: string;
/**
* Converts this to a User entity.
@@ -91,7 +105,10 @@ export class CreateUser {
newUser.phone = this.phone
newUser.password = await argon2.hash(this.password + newUser.uuid);
newUser.groups = await this.getGroups();
//TODO: ProfilePics
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;
}

View File

@@ -2,7 +2,7 @@ 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';
@@ -39,6 +39,7 @@ export class RefreshAuth {
if (!found_user) {
throw new UserNotFoundError()
}
if (found_user.enabled == false) { throw new UserDisabledError(); }
if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) {
throw new RefreshTokenCountInvalidError()
}

View 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";
}
}

View File

@@ -0,0 +1,44 @@
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
import { DonorReceiptAddressNeededError } from '../../errors/DonorErrors';
import { Donor } from '../entities/Donor';
import { CreateParticipant } from './CreateParticipant';
/**
* This class is used to update a Donor entity (via put request).
*/
export class UpdateDonor extends CreateParticipant {
/**
* The updated donor's id.
* This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
*/
@IsInt()
id: number;
/**
* Does the updated donor need a receipt?
*/
@IsBoolean()
@IsOptional()
receiptNeeded?: boolean;
/**
* Updates a provided Donor entity based on this.
*/
public async updateDonor(donor: Donor): Promise<Donor> {
donor.firstname = this.firstname;
donor.middlename = this.middlename;
donor.lastname = this.lastname;
donor.phone = this.phone;
donor.email = this.email;
donor.receiptNeeded = this.receiptNeeded;
donor.address = await this.getAddress();
if (this.receiptNeeded == true && this.address == null) {
throw new DonorReceiptAddressNeededError()
}
return donor;
}
}

View File

@@ -45,7 +45,7 @@ export class UpdateRunnerOrganisation extends CreateRunnerGroup {
organisation.name = this.name;
organisation.contact = await this.getContact();
organisation.address = await this.getAddress();
// organisation.address = await this.getAddress();
return organisation;
}

View File

@@ -0,0 +1,62 @@
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 update a Scan entity (via put request)
*/
export abstract class UpdateScan {
/**
* The updated scan'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 scan's associated runner.
* This is important to link ran distances to runners.
*/
@IsInt()
@IsPositive()
runner: number;
/**
* Is the updated scan valid (for fraud reasons).
*/
@IsBoolean()
@IsOptional()
valid?: boolean = true;
/**
* The updated scan's distance in meters.
*/
@IsInt()
@IsPositive()
public distance: number;
/**
* Update a Scan entity based on this.
* @param scan The scan that shall be updated.
*/
public async updateScan(scan: Scan): Promise<Scan> {
scan.distance = this.distance;
scan.valid = this.valid;
scan.runner = await this.getRunner();
return scan;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,39 @@
import { IsBoolean, IsInt, IsOptional, IsString } from 'class-validator';
import { ScanStation } from '../entities/ScanStation';
/**
* This class is used to update a ScanStation entity (via put request)
*/
export class UpdateScanStation {
/**
* The updated station'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 station's description.
*/
@IsString()
@IsOptional()
description?: string;
/**
* Is this station enabled?
*/
@IsBoolean()
@IsOptional()
enabled?: boolean = true;
/**
* Update a ScanStation entity based on this.
* @param station The station that shall be updated.
*/
public async updateStation(station: ScanStation): Promise<ScanStation> {
station.description = this.description;
station.enabled = this.enabled;
return station;
}
}

View File

@@ -0,0 +1,50 @@
import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator';
import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors';
import { Track } from '../entities/Track';
/**
* This class is used to update a Track entity (via put request).
*/
export class UpdateTrack {
/**
* The updated track's id.
* This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
*/
@IsInt()
id: number;
@IsString()
@IsNotEmpty()
name: string;
/**
* The updated track's distance in meters (must be greater than 0).
*/
@IsInt()
@IsPositive()
distance: number;
/**
* The minimum time a runner should take to run a lap on this track (in seconds).
* Will be used for fraud detection.
*/
@IsInt()
@IsOptional()
minimumLapTime: number;
/**
* Update a Track entity based on this.
* @param track The track that shall be updated.
*/
public updateTrack(track: Track): Track {
track.name = this.name;
track.distance = this.distance;
track.minimumLapTime = this.minimumLapTime;
if (this.minimumLapTime < 0) {
throw new TrackLapTimeCantBeNegativeError();
}
return track;
}
}

View File

@@ -1,5 +1,5 @@
import * as argon2 from "argon2";
import { IsBoolean, IsEmail, IsInt, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { IsBoolean, IsEmail, IsInt, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { config } from '../../config';
import { UsernameOrEmailNeededError } from '../../errors/AuthError';
@@ -87,7 +87,16 @@ export class UpdateUser {
groups?: UserGroup[]
/**
* Updates a provided User entity based on this.
* The user's profile pic (or rather a url pointing to it).
*/
@IsString()
@IsUrl()
@IsOptional()
profilePic?: string;
/**
* Updates a user entity based on this.
* @param user The user that shall be updated.
*/
public async updateUser(user: User): Promise<User> {
user.email = this.email;
@@ -106,7 +115,9 @@ export class UpdateUser {
user.lastname = this.lastname
user.phone = this.phone;
user.groups = await this.getGroups();
//TODO: ProfilePics
if (!this.profilePic) { user.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; }
else { user.profilePic = this.profilePic; }
return user;
}

View File

@@ -6,8 +6,8 @@ import {
IsString
} from "class-validator";
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { Participant } from "./Participant";
import { RunnerOrganisation } from "./RunnerOrganisation";
import { config } from '../../config';
import { IAddressUser } from './IAddressUser';
/**
* Defines the Address entity.
@@ -52,12 +52,11 @@ export class Address {
/**
* The address's postal code.
* This will get checked against the postal code syntax for the configured country.
* TODO: Implement the config option.
*/
@Column()
@IsString()
@IsNotEmpty()
@IsPostalCode("DE")
@IsPostalCode(config.postalcode_validation_countrycode)
postalcode: string;
/**
@@ -79,12 +78,13 @@ export class Address {
/**
* Used to link the address to participants.
*/
@OneToMany(() => Participant, participant => participant.address, { nullable: true })
participants: Participant[];
@OneToMany(() => IAddressUser, addressUser => addressUser.address, { nullable: true })
addressUsers: IAddressUser[];
/**
* Used to link the address to runner groups.
* Turns this entity into it's response class.
*/
@OneToMany(() => RunnerOrganisation, group => group.address, { nullable: true })
groups: RunnerOrganisation[];
public toResponse() {
return new Error("NotImplemented");
}
}

View File

@@ -39,4 +39,11 @@ export class DistanceDonation extends Donation {
}
return calculatedAmount;
}
/**
* Turns this entity into it's response class.
*/
public toResponse() {
return new Error("NotImplemented");
}
}

View File

@@ -3,7 +3,7 @@ import {
IsNotEmpty
} from "class-validator";
import { Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { Participant } from "./Participant";
import { Donor } from './Donor';
/**
* Defines the Donation entity.
@@ -24,12 +24,19 @@ export abstract class Donation {
* The donations's donor.
*/
@IsNotEmpty()
@ManyToOne(() => Participant, donor => donor.donations)
donor: Participant;
@ManyToOne(() => Donor, donor => donor.donations)
donor: Donor;
/**
* The donation's amount in cents (or whatever your currency's smallest unit is.).
* The exact implementation may differ for each type of donation.
*/
abstract amount: number | Promise<number>;
abstract amount: number;
/**
* Turns this entity into it's response class.
*/
public toResponse() {
return new Error("NotImplemented");
}
}

View File

@@ -1,5 +1,7 @@
import { IsBoolean } from "class-validator";
import { ChildEntity, Column } from "typeorm";
import { ChildEntity, Column, OneToMany } from "typeorm";
import { ResponseDonor } from '../responses/ResponseDonor';
import { Donation } from './Donation';
import { Participant } from "./Participant";
/**
@@ -14,4 +16,18 @@ export class Donor extends Participant {
@Column()
@IsBoolean()
receiptNeeded: boolean = false;
/**
* Used to link the participant as the donor of a donation.
* Attention: Only runner's can be associated as a distanceDonations distance source.
*/
@OneToMany(() => Donation, donation => donation.donor, { nullable: true })
donations: Donation[];
/**
* Turns this entity into it's response class.
*/
public toResponse(): ResponseDonor {
return new ResponseDonor(this);
}
}

View File

@@ -16,4 +16,11 @@ export class FixedDonation extends Donation {
@IsInt()
@IsPositive()
amount: number;
/**
* Turns this entity into it's response class.
*/
public toResponse() {
return new Error("NotImplemented");
}
}

View File

@@ -10,6 +10,7 @@ import {
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { config } from '../../config';
import { Address } from "./Address";
import { IAddressUser } from './IAddressUser';
import { RunnerGroup } from "./RunnerGroup";
/**
@@ -17,7 +18,7 @@ import { RunnerGroup } from "./RunnerGroup";
* Mainly it's own class to reduce duplicate code and enable contact's to be associated with multiple groups.
*/
@Entity()
export class GroupContact {
export class GroupContact implements IAddressUser {
/**
* Autogenerated unique id (primary key).
*/
@@ -54,7 +55,7 @@ export class GroupContact {
* This is a address object to prevent any formatting differences.
*/
@IsOptional()
@ManyToOne(() => Address, address => address.participants, { nullable: true })
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
address?: Address;
/**
@@ -80,4 +81,11 @@ export class GroupContact {
*/
@OneToMany(() => RunnerGroup, group => group.contact, { nullable: true })
groups: RunnerGroup[];
/**
* Turns this entity into it's response class.
*/
public toResponse() {
return new Error("NotImplemented");
}
}

View File

@@ -0,0 +1,20 @@
import { Entity, ManyToOne, PrimaryColumn } from 'typeorm';
import { Address } from './Address';
/**
* The interface(tm) all entities using addresses have to implement.
* This is a abstract class, because apparently typeorm can't really work with interfaces :/
*/
@Entity()
export abstract class IAddressUser {
@PrimaryColumn()
id: number;
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
address?: Address
/**
* Turns this entity into it's response class.
*/
public abstract toResponse();
}

View File

@@ -7,10 +7,11 @@ import {
IsString
} from "class-validator";
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { config } from '../../config';
import { ResponseParticipant } from '../responses/ResponseParticipant';
import { Address } from "./Address";
import { Donation } from "./Donation";
import { IAddressUser } from './IAddressUser';
/**
* Defines the Participant entity.
@@ -18,7 +19,7 @@ import { Donation } from "./Donation";
*/
@Entity()
@TableInheritance({ column: { name: "type", type: "varchar" } })
export abstract class Participant {
export abstract class Participant implements IAddressUser {
/**
* Autogenerated unique id (primary key).
*/
@@ -54,7 +55,7 @@ export abstract class Participant {
* The participant's address.
* This is a address object to prevent any formatting differences.
*/
@ManyToOne(() => Address, address => address.participants, { nullable: true })
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
address?: Address;
/**
@@ -76,9 +77,7 @@ export abstract class Participant {
email?: string;
/**
* Used to link the participant as the donor of a donation.
* Attention: Only runner's can be associated as a distanceDonations distance source.
* Turns this entity into it's response class.
*/
@OneToMany(() => Donation, donation => donation.donor, { nullable: true })
donations: Donation[];
public abstract toResponse(): ResponseParticipant;
}

View File

@@ -6,6 +6,7 @@ import {
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { PermissionAction } from '../enums/PermissionAction';
import { PermissionTarget } from '../enums/PermissionTargets';
import { ResponsePermission } from '../responses/ResponsePermission';
import { Principal } from './Principal';
/**
* Defines the Permission entity.
@@ -51,4 +52,11 @@ export class Permission {
public toString(): string {
return this.target + ":" + this.action;
}
/**
* Turns this entity into it's response class.
*/
public toResponse(): ResponsePermission {
return new ResponsePermission(this);
}
}

View File

@@ -1,5 +1,6 @@
import { IsInt, IsNotEmpty } from "class-validator";
import { ChildEntity, ManyToOne, OneToMany } from "typeorm";
import { ResponseRunner } from '../responses/ResponseRunner';
import { DistanceDonation } from "./DistanceDonation";
import { Participant } from "./Participant";
import { RunnerCard } from "./RunnerCard";
@@ -18,7 +19,7 @@ export class Runner extends Participant {
* Can be a runner team or organisation.
*/
@IsNotEmpty()
@ManyToOne(() => RunnerGroup, group => group.runners, { nullable: false })
@ManyToOne(() => RunnerGroup, group => group.runners)
group: RunnerGroup;
/**
@@ -47,7 +48,7 @@ export class Runner extends Participant {
* This is implemented here to avoid duplicate code in other files.
*/
public get validScans(): Scan[] {
return this.scans.filter(scan => { scan.valid === true });
return this.scans.filter(scan => scan.valid == true);
}
/**
@@ -58,4 +59,19 @@ export class Runner extends Participant {
public get distance(): number {
return this.validScans.reduce((sum, current) => sum + current.distance, 0);
}
/**
* Returns the total donations a runner has collected based on his linked donations and distance ran.
*/
@IsInt()
public get distanceDonationAmount(): number {
return this.distanceDonations.reduce((sum, current) => sum + current.amountPerDistance, 0) * this.distance;
}
/**
* Turns this entity into it's response class.
*/
public toResponse(): ResponseRunner {
return new ResponseRunner(this);
}
}

View File

@@ -57,4 +57,11 @@ export class RunnerCard {
*/
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];
/**
* Turns this entity into it's response class.
*/
public toResponse() {
return new Error("NotImplemented");
}
}

View File

@@ -5,6 +5,7 @@ import {
IsString
} from "class-validator";
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseRunnerGroup } from '../responses/ResponseRunnerGroup';
import { GroupContact } from "./GroupContact";
import { Runner } from "./Runner";
@@ -44,4 +45,25 @@ export abstract class RunnerGroup {
*/
@OneToMany(() => Runner, runner => runner.group, { nullable: true })
runners: Runner[];
/**
* Returns the total distance ran by this group's runners based on all their valid scans.
*/
@IsInt()
public get distance(): number {
return this.runners.reduce((sum, current) => sum + current.distance, 0);
}
/**
* Returns the total donations a runner has collected based on his linked donations and distance ran.
*/
@IsInt()
public get distanceDonationAmount(): number {
return this.runners.reduce((sum, current) => sum + current.distanceDonationAmount, 0);
}
/**
* Turns this entity into it's response class.
*/
public abstract toResponse(): ResponseRunnerGroup;
}

View File

@@ -1,6 +1,9 @@
import { IsOptional } from "class-validator";
import { IsInt, IsOptional } from "class-validator";
import { ChildEntity, ManyToOne, OneToMany } from "typeorm";
import { Address } from "./Address";
import { ResponseRunnerOrganisation } from '../responses/ResponseRunnerOrganisation';
import { Address } from './Address';
import { IAddressUser } from './IAddressUser';
import { Runner } from './Runner';
import { RunnerGroup } from "./RunnerGroup";
import { RunnerTeam } from "./RunnerTeam";
@@ -9,13 +12,13 @@ import { RunnerTeam } from "./RunnerTeam";
* This usually is a school, club or company.
*/
@ChildEntity()
export class RunnerOrganisation extends RunnerGroup {
export class RunnerOrganisation extends RunnerGroup implements IAddressUser {
/**
* The organisations's address.
*/
@IsOptional()
@ManyToOne(() => Address, address => address.groups, { nullable: true })
@ManyToOne(() => Address, address => address.addressUsers, { nullable: true })
address?: Address;
/**
@@ -24,4 +27,39 @@ export class RunnerOrganisation extends RunnerGroup {
*/
@OneToMany(() => RunnerTeam, team => team.parentGroup, { nullable: true })
teams: RunnerTeam[];
/**
* Returns all runners associated with this organisation (directly or indirectly via teams).
*/
public get allRunners(): Runner[] {
let returnRunners: Runner[] = new Array<Runner>();
returnRunners.push(...this.runners);
for (let team of this.teams) {
returnRunners.push(...team.runners)
}
return returnRunners;
}
/**
* Returns the total distance ran by this group's runners based on all their valid scans.
*/
@IsInt()
public get distance(): number {
return this.allRunners.reduce((sum, current) => sum + current.distance, 0);
}
/**
* Returns the total donations a runner has collected based on his linked donations and distance ran.
*/
@IsInt()
public get distanceDonationAmount(): number {
return this.allRunners.reduce((sum, current) => sum + current.distanceDonationAmount, 0);
}
/**
* Turns this entity into it's response class.
*/
public toResponse(): ResponseRunnerOrganisation {
return new ResponseRunnerOrganisation(this);
}
}

View File

@@ -1,5 +1,6 @@
import { IsNotEmpty } from "class-validator";
import { ChildEntity, ManyToOne } from "typeorm";
import { ResponseRunnerTeam } from '../responses/ResponseRunnerTeam';
import { RunnerGroup } from "./RunnerGroup";
import { RunnerOrganisation } from "./RunnerOrganisation";
@@ -17,4 +18,11 @@ export class RunnerTeam extends RunnerGroup {
@IsNotEmpty()
@ManyToOne(() => RunnerOrganisation, org => org.teams, { nullable: true })
parentGroup?: RunnerOrganisation;
/**
* Turns this entity into it's response class.
*/
public toResponse(): ResponseRunnerTeam {
return new ResponseRunnerTeam(this);
}
}

View File

@@ -6,6 +6,7 @@ import {
IsPositive
} from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseScan } from '../responses/ResponseScan';
import { Runner } from "./Runner";
/**
@@ -14,7 +15,7 @@ import { Runner } from "./Runner";
*/
@Entity()
@TableInheritance({ column: { name: "type", type: "varchar" } })
export abstract class Scan {
export class Scan {
/**
* Autogenerated unique id (primary key).
*/
@@ -30,14 +31,6 @@ export abstract class Scan {
@ManyToOne(() => Runner, runner => runner.scans, { nullable: false })
runner: Runner;
/**
* The scan's distance in meters.
* Can be set manually or derived from another object.
*/
@IsInt()
@IsPositive()
abstract distance: number;
/**
* Is the scan valid (for fraud reasons).
* The determination of validity will work differently for every child class.
@@ -46,4 +39,37 @@ export abstract class Scan {
@Column()
@IsBoolean()
valid: boolean = true;
/**
* The scan's distance in meters.
* This is the "real" value used by "normal" scans..
*/
@Column({ nullable: true })
@IsInt()
private _distance?: number;
/**
* The scan's distance in meters.
* Can be set manually or derived from another object.
*/
@IsInt()
@IsPositive()
public get distance(): number {
return this._distance;
}
/**
* The scan's distance in meters.
* Can be set manually or derived from another object.
*/
public set distance(value: number) {
this._distance = value;
}
/**
* Turns this entity into it's response class.
*/
public toResponse(): ResponseScan {
return new ResponseScan(this);
}
}

View File

@@ -6,6 +6,7 @@ import {
IsString
} from "class-validator";
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { ResponseScanStation } from '../responses/ResponseScanStation';
import { Track } from "./Track";
import { TrackScan } from "./TrackScan";
@@ -39,6 +40,14 @@ export class ScanStation {
@ManyToOne(() => Track, track => track.stations, { nullable: false })
track: Track;
/**
* The client's api key prefix.
* This is used identitfy a client by it's api key.
*/
@Column({ unique: true })
@IsString()
prefix: string;
/**
* The station's api key.
* This is used to authorize a station against the api (not implemented yet).
@@ -49,16 +58,30 @@ export class ScanStation {
key: string;
/**
* Is the station enabled (for fraud and setup reasons)?
* Default: true
* The client's api key in plain text.
* This will only be used to display the full key on creation and updates.
*/
@Column()
@IsBoolean()
enabled: boolean = true;
@IsString()
@IsOptional()
cleartextkey?: string;
/**
* Used to link track scans to a scan station.
*/
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];
/**
* Is this station enabled?
*/
@Column({ nullable: true })
@IsBoolean()
enabled?: boolean = true;
/**
* Turns this entity into it's response class.
*/
public toResponse(): ResponseScanStation {
return new ResponseScanStation(this);
}
}

View File

@@ -0,0 +1,56 @@
import { IsInt, IsOptional, IsString } from "class-validator";
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { ResponseStatsClient } from '../responses/ResponseStatsClient';
/**
* Defines the StatsClient entity.
* StatsClients can be used to access the protected parts of the stats api (top runners, donators and so on).
*/
@Entity()
export class StatsClient {
/**
* Autogenerated unique id (primary key).
*/
@PrimaryGeneratedColumn()
@IsInt()
id: number;
/**
* The clients's description.
* Mostly for better UX when traceing back stuff.
*/
@Column({ nullable: true })
@IsOptional()
@IsString()
description?: string;
/**
* The client's api key prefix.
* This is used identitfy a client by it's api key.
*/
@Column({ unique: true })
@IsString()
prefix: string;
/**
* The client's api key hash.
* The api key can be used to authenticate against the /stats/** routes.
*/
@Column()
@IsString()
key: string;
/**
* The client's api key in plain text.
* This will only be used to display the full key on creation and updates.
*/
@IsString()
@IsOptional()
cleartextkey?: string;
/**
* Turns this entity into it's response class.
*/
public toResponse(): ResponseStatsClient {
return new ResponseStatsClient(this);
}
}

View File

@@ -1,10 +1,12 @@
import {
IsInt,
IsNotEmpty,
IsOptional,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { ResponseTrack } from '../responses/ResponseTrack';
import { ScanStation } from "./ScanStation";
import { TrackScan } from "./TrackScan";
@@ -18,7 +20,7 @@ export class Track {
*/
@PrimaryGeneratedColumn()
@IsInt()
id: number;;
id: number;
/**
* The track's name.
@@ -38,6 +40,15 @@ export class Track {
@IsPositive()
distance: number;
/**
* The minimum time a runner should take to run a lap on this track (in seconds).
* Will be used for fraud detection.
*/
@Column({ nullable: true })
@IsInt()
@IsOptional()
minimumLapTime?: number;
/**
* Used to link scan stations to a certain track.
* This makes the configuration of the scan stations easier.
@@ -51,4 +62,11 @@ export class Track {
*/
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];
/**
* Turns this entity into it's response class.
*/
public toResponse(): ResponseTrack {
return new ResponseTrack(this);
}
}

View File

@@ -6,6 +6,7 @@ import {
IsPositive
} from "class-validator";
import { ChildEntity, Column, ManyToOne } from "typeorm";
import { ResponseTrackScan } from '../responses/ResponseTrackScan';
import { RunnerCard } from "./RunnerCard";
import { Scan } from "./Scan";
import { ScanStation } from "./ScanStation";
@@ -59,4 +60,11 @@ export class TrackScan extends Scan {
@IsDateString()
@IsNotEmpty()
timestamp: string;
/**
* Turns this entity into it's response class.
*/
public toResponse(): ResponseTrackScan {
return new ResponseTrackScan(this);
}
}

View File

@@ -1,4 +1,4 @@
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUUID } from "class-validator";
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl, IsUUID } from "class-validator";
import { ChildEntity, Column, JoinTable, ManyToMany, OneToMany } from "typeorm";
import { config } from '../../config';
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
@@ -106,10 +106,19 @@ export class User extends Principal {
* The user's profile picture.
* We haven't decided yet if this will be a bas64 encoded image or just a link to the profile picture.
*/
@Column({ nullable: true, unique: true })
@Column({ nullable: false, unique: false })
@IsString()
@IsUrl()
profilePic: string;
/**
* The last time the user requested a password reset.
* Used to prevent spamming of the password reset route.
*/
@Column({ nullable: true, unique: false })
@IsString()
@IsOptional()
profilePic?: string;
resetRequestedTimestamp?: number;
/**
* The actions performed by this user.
@@ -119,6 +128,26 @@ export class User extends Principal {
@OneToMany(() => UserAction, action => action.user, { nullable: true })
actions: UserAction[]
/**
* Resolves all permissions granted to this user through groups or directly to the string enum format.
*/
public get allPermissions(): string[] {
let returnPermissions: string[] = new Array<string>();
if (!this.permissions) { return returnPermissions; }
for (let permission of this.permissions) {
returnPermissions.push(permission.toString());
}
if (!this.groups) { return returnPermissions; }
for (let group of this.groups) {
for (let permission of group.permissions) {
returnPermissions.push(permission.toString());
}
}
return Array.from(new Set(returnPermissions));
}
/**
* Turns this entity into it's response class.
*/

View File

@@ -52,4 +52,11 @@ export class UserAction {
@IsOptional()
@IsString()
changed: string;
/**
* Turns this entity into it's response class.
*/
public toResponse() {
return new Error("NotImplemented");
}
}

View File

@@ -8,5 +8,9 @@ export enum PermissionTarget {
TRACK = 'TRACK',
USER = 'USER',
USERGROUP = 'USERGROUP',
PERMISSION = 'PERMISSION'
PERMISSION = 'PERMISSION',
STATSCLIENT = 'STATSCLIENT',
DONOR = 'DONOR',
SCAN = 'SCAN',
STATION = 'STATION'
}

View File

@@ -0,0 +1,26 @@
import {
IsBoolean
} from "class-validator";
import { Donor } from '../entities/Donor';
import { ResponseParticipant } from './ResponseParticipant';
/**
* Defines the donor response.
*/
export class ResponseDonor extends ResponseParticipant {
/**
* Does this donor need a receipt?
*/
@IsBoolean()
receiptNeeded: boolean;
/**
* Creates a ResponseRunner object from a runner.
* @param runner The user the response shall be build for.
*/
public constructor(donor: Donor) {
super(donor);
this.receiptNeeded = donor.receiptNeeded;
}
}

View File

@@ -29,7 +29,8 @@ export class ResponseRunner extends ResponseParticipant {
*/
public constructor(runner: Runner) {
super(runner);
this.distance = runner.scans.filter(scan => { scan.valid === true }).reduce((sum, current) => sum + current.distance, 0);
if (!runner.scans) { this.distance = 0 }
else { this.distance = runner.validScans.reduce((sum, current) => sum + current.distance, 0); }
this.group = runner.group;
}
}

View File

@@ -1,7 +1,8 @@
import {
IsArray,
IsNotEmpty,
IsObject
IsObject,
IsOptional
} from "class-validator";
import { Address } from '../entities/Address';
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
@@ -17,7 +18,7 @@ export class ResponseRunnerOrganisation extends ResponseRunnerGroup {
* The runnerOrganisation's address.
*/
@IsObject()
@IsNotEmpty()
@IsOptional()
address?: Address;
/**

View File

@@ -0,0 +1,46 @@
import { IsBoolean, IsInt, IsNotEmpty, IsPositive } from "class-validator";
import { Scan } from '../entities/Scan';
import { ResponseRunner } from './ResponseRunner';
/**
* Defines the scan response.
*/
export class ResponseScan {
/**
* The scans's id.
*/
@IsInt()
id: number;;
/**
* The scan's associated runner.
* This is important to link ran distances to runners.
*/
@IsNotEmpty()
runner: ResponseRunner;
/**
* Is the scan valid (for fraud reasons).
* The determination of validity will work differently for every child class.
*/
@IsBoolean()
valid: boolean = true;
/**
* The scans's length/distance in meters.
*/
@IsInt()
@IsPositive()
distance: number;
/**
* Creates a ResponseScan object from a scan.
* @param scan The scan the response shall be build for.
*/
public constructor(scan: Scan) {
this.id = scan.id;
this.runner = scan.runner.toResponse();
this.distance = scan.distance;
this.valid = scan.valid;
}
}

View File

@@ -0,0 +1,70 @@
import {
IsBoolean,
IsInt,
IsNotEmpty,
IsObject,
IsOptional,
IsString
} from "class-validator";
import { ScanStation } from '../entities/ScanStation';
import { ResponseTrack } from './ResponseTrack';
/**
* Defines the statsClient response.
*/
export class ResponseScanStation {
/**
* The client's id.
*/
@IsInt()
id: number;
/**
* The client's description.
*/
@IsString()
@IsOptional()
description?: string;
/**
* The client's api key.
* Only visible on creation or regeneration.
*/
@IsString()
@IsOptional()
key: string;
/**
* The client's api key prefix.
*/
@IsString()
@IsNotEmpty()
prefix: string;
@IsObject()
@IsNotEmpty()
track: ResponseTrack;
/**
* Is this station enabled?
*/
@IsBoolean()
enabled?: boolean = true;
/**
* Creates a ResponseStatsClient object from a statsClient.
* @param client The statsClient the response shall be build for.
*/
public constructor(station: ScanStation) {
this.id = station.id;
this.description = station.description;
this.prefix = station.prefix;
this.key = "Only visible on creation.";
this.track = station.track;
this.enabled = station.enabled;
}
}

View File

@@ -0,0 +1,83 @@
import {
IsInt
} from "class-validator";
import { Donation } from '../entities/Donation';
import { Runner } from '../entities/Runner';
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { RunnerTeam } from '../entities/RunnerTeam';
import { Scan } from '../entities/Scan';
import { User } from '../entities/User';
/**
* Defines the stats response.
* The stats response calculates some basic stats for a dashboard or public display.
*/
export class ResponseStats {
/**
* The amount of runners registered in the system.
*/
@IsInt()
total_runners: number;
/**
* The amount of teams registered in the system.
*/
@IsInt()
total_teams: number;
/**
* The amount of organisations registered in the system.
*/
@IsInt()
total_orgs: number;
/**
* The amount of users registered in the system.
*/
@IsInt()
total_users: number;
/**
* The amount of valid scans registered in the system.
*/
@IsInt()
total_scans: number;
/**
* The total distance that all runners ran combined.
*/
@IsInt()
total_distance: number;
/**
* The total donation amount.
*/
@IsInt()
total_donation: number;
/**
* The average distance ran per runner.
*/
@IsInt()
average_distance: number;
/**
* Creates a new stats response containing some basic statistics for a dashboard or public display.
* @param runners Array containing all runners - the following relations have to be resolved: scans, scans.track
* @param teams Array containing all teams - no relations have to be resolved.
* @param orgs Array containing all orgs - no relations have to be resolved.
* @param users Array containing all users - no relations have to be resolved.
* @param scans Array containing all scans - no relations have to be resolved.
* @param donations Array containing all donations - the following relations have to be resolved: runner, runner.scans, runner.scans.track
*/
public constructor(runners: Runner[], teams: RunnerTeam[], orgs: RunnerOrganisation[], users: User[], scans: Scan[], donations: Donation[]) {
this.total_runners = runners.length;
this.total_teams = teams.length;
this.total_orgs = orgs.length;
this.total_users = users.length;
this.total_scans = scans.filter(scan => { scan.valid === true }).length;
this.total_distance = runners.reduce((sum, current) => sum + current.distance, 0);
this.total_donation = donations.reduce((sum, current) => sum + current.amount, 0);
this.average_distance = this.total_distance / this.total_runners;
}
}

View File

@@ -0,0 +1,54 @@
import {
IsInt,
IsNotEmpty,
IsOptional,
IsString
} from "class-validator";
import { StatsClient } from '../entities/StatsClient';
/**
* Defines the statsClient response.
*/
export class ResponseStatsClient {
/**
* The client's id.
*/
@IsInt()
id: number;
/**
* The client's description.
*/
@IsString()
@IsOptional()
description?: string;
/**
* The client's api key.
* Only visible on creation or regeneration.
*/
@IsString()
@IsOptional()
key: string;
/**
* The client's api key prefix.
*/
@IsString()
@IsNotEmpty()
prefix: string;
/**
* Creates a ResponseStatsClient object from a statsClient.
* @param client The statsClient the response shall be build for.
*/
public constructor(client: StatsClient) {
this.id = client.id;
this.description = client.description;
this.prefix = client.prefix;
this.key = "Only visible on creation.";
}
}

View File

@@ -0,0 +1,47 @@
import {
IsInt,
IsString
} from "class-validator";
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
/**
* Defines the org stats response.
* This differs from the normal org responce.
*/
export class ResponseStatsOrgnisation {
/**
* The orgs's id.
*/
@IsInt()
id: number;
/**
* The orgs's name.
*/
@IsString()
name: string;
/**
* The orgs's runner's currently ran distance in meters.
*/
@IsInt()
distance: number;
/**
* The orgs's currently collected donations.
*/
@IsInt()
donationAmount: number;
/**
* Creates a new organisation stats response from a organisation
* @param org The organisation whoes response shall be generated - the following relations have to be resolved: runners, runners.scans, runners.distanceDonations, runners.scans.track, teams, teams.runners, teams.runners.scans, teams.runners.distanceDonations, teams.runners.scans.track
*/
public constructor(org: RunnerOrganisation) {
this.name = org.name;
this.id = org.id;
this.distance = org.distance;
this.donationAmount = org.distanceDonationAmount;
}
}

View File

@@ -0,0 +1,69 @@
import {
IsInt,
IsObject,
IsString
} from "class-validator";
import { Runner } from '../entities/Runner';
import { RunnerGroup } from '../entities/RunnerGroup';
/**
* Defines the runner stats response.
* This differs from the normal runner responce.
*/
export class ResponseStatsRunner {
/**
* The runner's id.
*/
@IsInt()
id: number;
/**
* The runner's first name.
*/
@IsString()
firstname: string;
/**
* The runner's middle name.
*/
@IsString()
middlename?: string;
/**
* The runner's last name.
*/
@IsString()
lastname: string;
/**
* The runner's currently ran distance in meters.
*/
@IsInt()
distance: number;
/**
* The runner's currently collected donations.
*/
@IsInt()
donationAmount: number;
/**
* The runner's group.
*/
@IsObject()
group: RunnerGroup;
/**
* Creates a new runner stats response from a runner
* @param runner The runner whoes response shall be generated - the following relations have to be resolved: scans, group, distanceDonations, scans.track
*/
public constructor(runner: Runner) {
this.id = runner.id;
this.firstname = runner.firstname;
this.middlename = runner.middlename;
this.lastname = runner.lastname;
this.distance = runner.distance;
this.donationAmount = runner.distanceDonationAmount;
this.group = runner.group;
}
}

View File

@@ -0,0 +1,55 @@
import {
IsInt,
IsObject,
IsString
} from "class-validator";
import { RunnerGroup } from '../entities/RunnerGroup';
import { RunnerTeam } from '../entities/RunnerTeam';
/**
* Defines the team stats response.
* This differs from the normal team responce.
*/
export class ResponseStatsTeam {
/**
* The team's id.
*/
@IsInt()
id: number;
/**
* The team's name.
*/
@IsString()
name: string;
/**
* The teams's currently ran distance in meters.
*/
@IsInt()
distance: number;
/**
* The teams's currently collected donations.
*/
@IsInt()
donationAmount: number;
/**
* The teams's parent group.
*/
@IsObject()
parent: RunnerGroup;
/**
* Creates a new team stats response from a team
* @param team The team whoes response shall be generated - the following relations have to be resolved: runners, runners.scans, runners.distanceDonations, runners.scans.track
*/
public constructor(team: RunnerTeam) {
this.name = team.name;
this.id = team.id;
this.parent = team.parentGroup;
this.distance = team.distance;
this.donationAmount = team.distanceDonationAmount;
}
}

View File

@@ -1,4 +1,5 @@
import { IsInt, IsString } from "class-validator";
import { IsInt, IsOptional, IsString } from "class-validator";
import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors';
import { Track } from '../entities/Track';
/**
@@ -23,6 +24,14 @@ export class ResponseTrack {
@IsInt()
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 ResponseTrack object from a track.
* @param track The track the response shall be build for.
@@ -31,5 +40,9 @@ export class ResponseTrack {
this.id = track.id;
this.name = track.name;
this.distance = track.distance;
this.minimumLapTime = track.minimumLapTime;
if (this.minimumLapTime < 0) {
throw new TrackLapTimeCantBeNegativeError();
}
}
}

View File

@@ -0,0 +1,48 @@
import { IsDateString, IsNotEmpty } from "class-validator";
import { RunnerCard } from '../entities/RunnerCard';
import { ScanStation } from '../entities/ScanStation';
import { TrackScan } from '../entities/TrackScan';
import { ResponseScan } from './ResponseScan';
import { ResponseTrack } from './ResponseTrack';
/**
* Defines the trackScan response.
*/
export class ResponseTrackScan extends ResponseScan {
/**
* The scan's associated track.
*/
@IsNotEmpty()
track: ResponseTrack;
/**
* The runnerCard associated with the scan.
*/
@IsNotEmpty()
card: RunnerCard;
/**
* The scanning station that created the scan.
*/
@IsNotEmpty()
station: ScanStation;
/**
* The scan's creation timestamp.
*/
@IsDateString()
@IsNotEmpty()
timestamp: string;
/**
* Creates a ResponseTrackScan object from a scan.
* @param scan The trackSscan the response shall be build for.
*/
public constructor(scan: TrackScan) {
super(scan);
this.track = new ResponseTrack(scan.track);
this.card = scan.card;
this.station = scan.station;
this.timestamp = scan.timestamp;
}
}

View File

@@ -5,7 +5,6 @@ import {
IsOptional,
IsString
} from "class-validator";
import { Permission } from '../entities/Permission';
import { User } from '../entities/User';
import { UserGroup } from '../entities/UserGroup';
import { ResponsePrincipal } from './ResponsePrincipal';
@@ -57,11 +56,10 @@ export class ResponseUser extends ResponsePrincipal {
enabled: boolean = true;
/**
* The user's profile pic.
* The user's profile pic (or rather a url pointing to it).
*/
@IsString()
@IsOptional()
profilePic?: string;
profilePic: string;
/**
* The groups that the user is a part of.
@@ -75,7 +73,7 @@ export class ResponseUser extends ResponsePrincipal {
*/
@IsArray()
@IsOptional()
permissions: Permission[];
permissions: string[];
/**
* Creates a ResponseUser object from a user.
@@ -92,6 +90,7 @@ export class ResponseUser extends ResponsePrincipal {
this.enabled = user.enabled;
this.profilePic = user.profilePic;
this.groups = user.groups;
this.permissions = user.permissions;
this.permissions = user.allPermissions;
this.groups.forEach(function (g) { delete g.permissions });
}
}

156
src/static/docs/index.html Normal file
View File

@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Docs</title>
<style>
:root {
--bg-color: #fff;
--bg-secondary-color: #f3f3f6;
--color-primary: #14854f;
--color-lightGrey: #d2d6dd;
--color-grey: #747681;
--color-darkGrey: #3f4144;
--color-error: #d43939;
--color-success: #28bd14;
--grid-maxWidth: 120rem;
--grid-gutter: 2rem;
--font-size: 1.6rem;
--font-color: #333;
--font-family-sans: -apple-system, BlinkMacSystemFont, Avenir, "Avenir Next", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
--font-family-mono: monaco, "Consolas", "Lucida Console", monospace
}
html {
-webkit-box-sizing: border-box;
box-sizing: border-box;
font-size: 62.5%;
line-height: 1.15;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%
}
*,
:after,
:before {
-webkit-box-sizing: inherit;
box-sizing: inherit
}
body {
background-color: var(--bg-color);
line-height: 1.6;
font-size: var(--font-size);
color: var(--font-color);
font-family: Segoe UI, Helvetica Neue, sans-serif;
font-family: var(--font-family-sans);
margin: 0;
padding: 0
}
h3 {
font-weight: 500;
margin: .35em 0 .7em
}
h3 {
font-size: 1.5em
}
a {
color: var(--color-primary);
text-decoration: none
}
a:hover:not(.button) {
opacity: .75
}
input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]):not(:disabled):hover {
border-color: var(--color-grey)
}
::-webkit-input-placeholder {
color: #bdbfc4
}
::-moz-placeholder {
color: #bdbfc4
}
:-ms-input-placeholder {
color: #bdbfc4
}
::-ms-input-placeholder {
color: #bdbfc4
}
.tabs {
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
.tabs a {
text-decoration: none
}
.tabs>a {
padding: 1rem 2rem;
-webkit-box-flex: 0;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
color: var(--color-darkGrey);
border-bottom: 2px solid var(--color-lightGrey);
text-align: center
}
.tabs>a:hover {
opacity: 1;
border-bottom: 2px solid var(--color-darkGrey)
}
.is-vertical-align {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center
}
.is-center {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center
}
.is-center {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center
}
</style>
</head>
<body>
<div class="hero">
<div class="logo is-center is-vertical-align">
<h3>API Docs</h3>
</div>
<nav class="tabs is-center">
<a href="./redoc">ReDoc</a>
<a href="./swaggerui">SwaggerUI</a>
<a href="./rapidoc">RapiDoc</a>
<a href="./openapi.json">Raw Spec (json)</a>
</nav>
</div>
</body>
</html>

220
src/static/docs/rapidoc-min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script type="module" src="./rapidoc-min.js"></script>
</head>
<body>
<rapi-doc
spec-url="/api/docs/openapi.json"
> </rapi-doc>
</body>
</html>

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>ReDoc</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='/api/docs/openapi.json'></redoc>
<script src="./redoc.standalone.js"> </script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "/api/docs/openapi.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>

View File

@@ -0,0 +1,34 @@
import axios from 'axios';
import { config } from '../config';
const base = "http://localhost:" + config.internal_port
describe('GET /api/docs/openapi.json', () => {
it('OpenAPI Spec is availdable 200', async () => {
const res = await axios.get(base + '/api/docs/openapi.json');
expect(res.status).toEqual(200);
});
});
describe('GET /api/docs/swagger.json', () => {
it('OpenAPI Spec is availdable 200', async () => {
const res = await axios.get(base + '/api/docs/swagger.json');
expect(res.status).toEqual(200);
});
});
describe('GET /api/docs/swaggerui', () => {
it('swaggerui is availdable 200', async () => {
const res = await axios.get(base + '/api/docs/swaggerui');
expect(res.status).toEqual(200);
});
});
describe('GET /api/docs/redoc', () => {
it('redoc is availdable 200', async () => {
const res = await axios.get(base + '/api/docs/redoc');
expect(res.status).toEqual(200);
});
});
describe('GET /api/docs/rapidoc', () => {
it('rapidoc is availdable 200', async () => {
const res = await axios.get(base + '/api/docs/rapidoc');
expect(res.status).toEqual(200);
});
});

View File

@@ -0,0 +1,56 @@
import axios from 'axios';
import { config } from '../../config';
const base = "http://localhost:" + config.internal_port
let axios_config;
beforeAll(async () => {
axios_config = {
validateStatus: undefined
};
});
describe('POST /api/auth/login valid', () => {
it('valid login should return 200', async () => {
const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }, axios_config);
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json")
});
});
// ---------------
describe('POST /api/auth/login invalid body', () => {
it('Loging without a body should return 400', async () => {
const res = await axios.post(base + '/api/auth/login', null, axios_config);
expect(res.status).toEqual(400);
});
it('Loging without a password should return 400', async () => {
const res = await axios.post(base + '/api/auth/login', { username: "demo" }, axios_config);
expect(res.status).toEqual(400);
});
it('Loging with invalid mail format should return 400', async () => {
const res = await axios.post(base + '/api/auth/login', { email: "demo", password: "demo" }, axios_config);
expect(res.status).toEqual(400);
});
it('Loging without a username/mail should return 404', async () => {
const res = await axios.post(base + '/api/auth/login', { password: "demo" }, axios_config);
expect(res.status).toEqual(404);
});
});
// ---------------
describe('POST /api/auth/login nonexistant user', () => {
it('login with nonexistant username should return 404', async () => {
const res = await axios.post(base + '/api/auth/login', { username: "-1", password: "demo" }, axios_config);
expect(res.status).toEqual(404);
});
it('login with nonexistant mail should return 404', async () => {
const res = await axios.post(base + '/api/auth/login', { email: "test@example.com", password: "demo" }, axios_config);
expect(res.status).toEqual(404);
});
});
// ---------------
describe('POST /api/auth/login wrong password', () => {
it('login with wrong password should return 401', async () => {
const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "totallynotthecorrectpassword" }, axios_config);
expect(res.status).toEqual(401);
});
});

Some files were not shown because too many files have changed in this diff Show More