Compare commits

..

1495 Commits

Author SHA1 Message Date
7aaac65af4 chore(release): 1.8.1
Some checks failed
Build release images / build-container (push) Failing after 1m4s
2026-02-20 22:18:36 +01:00
13e0c81957 perf(stats): Cache stats results for 60 seconds 2026-02-20 22:16:14 +01:00
329a29aca7 chore(release): 1.8.0
Some checks failed
Build release images / build-container (push) Failing after 59s
2026-02-20 22:08:57 +01:00
abdadb8e64 refactor(deps): Remove unused glob dependency from package.json and bun.lock 2026-02-20 22:06:21 +01:00
abce517d86 refactor: Replace uuid and dotenv with bun primitives 2026-02-20 22:05:23 +01:00
a1e697acb2 refactor: Switch from official argon2 to Bun's implementation 2026-02-20 21:59:56 +01:00
c9b8614f53 chore(release): 1.7.2
All checks were successful
Build release images / build-container (push) Successful in 2m12s
2026-02-20 21:50:46 +01:00
cbf1da31c9 docs: Added agents file to support ai assisted coding 2026-02-20 21:50:21 +01:00
fd18e56251 refactor(dev): Yeet the funky dev script out of this codebase 2026-02-20 21:49:52 +01:00
3bb8b202b0 fix(dev): We did it funky bun dev workarounds are no more 2026-02-20 21:45:15 +01:00
d1c4744231 chore(release): 1.7.1
All checks were successful
Build release images / build-container (push) Successful in 2m26s
2026-02-20 20:32:25 +01:00
fe90414dd9 fix(ci): Switch to bun in ci 2026-02-20 20:31:23 +01:00
21ceb9fa26 perf(db): Added indexes 2026-02-20 20:31:02 +01:00
5081819281 chore(release): 1.7.0
Some checks failed
Build release images / build-container (push) Failing after 8s
2026-02-20 20:19:31 +01:00
240bd9cba1 refactor: Bun by default 2026-02-20 20:16:37 +01:00
53fb0389cd chore(release): 1.6.0
All checks were successful
Build release images / build-container (push) Successful in 2m48s
2026-02-20 19:46:17 +01:00
d230350027 perf(nats): Implement bulk cache prewarming for runners to optimize startup performance 2026-02-20 19:41:44 +01:00
024e647295 perf(nats): Implement bulk cache prewarming for runners to optimize startup performance 2026-02-20 19:40:02 +01:00
d3e0206a3c refactor(scan): Implement KV-backed scan station submissions and response model 2026-02-20 19:36:38 +01:00
b0c6759813 feat(nats): Implement caching for card, runner, and station entries with improved key management 2026-02-20 19:36:23 +01:00
526738e487 feat(auth): Implement caching for scanauth 2026-02-20 19:27:00 +01:00
778f159405 fix(types): Add custom Express request types for station authentication 2026-02-20 19:24:41 +01:00
2da8247978 chore(deps): Bump typescript and get rid of now legacy imports 2026-02-20 19:18:25 +01:00
bbf6ea6c0f feat(data): Added nats jetstream dependency 2026-02-20 19:15:35 +01:00
3584b3facf feat(auth): Switch scanstation auth from argon2 to sha256 to improve performance 2026-02-20 19:10:15 +01:00
e27e819609 chore(release): 1.5.2
All checks were successful
Build release images / build-container (push) Successful in 1m9s
2025-05-26 19:30:33 +02:00
0f532b139c feat(mailer): Log error message when sending selfservice forgotten mail fails 2025-05-26 19:30:16 +02:00
eebcc2e328 feat(mailer): Add logging for selfservice forgotten mail requests 2025-05-26 19:29:37 +02:00
284954d064 chore(release): 1.5.1
All checks were successful
Build release images / build-container (push) Successful in 1m12s
2025-05-26 19:24:04 +02:00
401ca923a6 feat(mailer): Log error when sending selfservice forgotten mail fails 2025-05-26 19:23:30 +02:00
bf1f6411e0 chore(release): 1.5.0
All checks were successful
Build release images / build-container (push) Successful in 1m17s
2025-05-06 19:41:36 +02:00
f225cc4954 feat(responses): Added created_at/updated_at 2025-05-06 19:38:20 +02:00
728f8a14e9 feat(entities): Added created/updated at to all entities 2025-05-06 19:33:30 +02:00
a4480589a0 feat(participants): Added created/updated at 2025-05-06 19:29:46 +02:00
0ad9eeb52f chore(release): 1.4.3
All checks were successful
Build release images / build-container (push) Successful in 1m15s
2025-05-01 16:02:40 +02:00
4494afc64b feat(runners): Include collected distance donation amount in runner detail 2025-05-01 16:02:28 +02:00
f4747c51de chore(release): 1.4.2
All checks were successful
Build release images / build-container (push) Successful in 1m16s
2025-05-01 15:57:57 +02:00
07a0195f12 fix(donations): Fixed creation bug 2025-05-01 15:56:42 +02:00
7ac98229d1 chore(release): 1.4.1
All checks were successful
Build release images / build-container (push) Successful in 1m19s
2025-04-28 21:36:31 +02:00
dd5b538783 refactor(auth): Increased token timeouts to 24hrs/7days 2025-04-28 21:36:12 +02:00
8e6d67428c chore(release): 1.4.0
All checks were successful
Build release images / build-container (push) Successful in 1m14s
2025-04-28 19:41:55 +02:00
7ffb7523aa Merge branch 'CreateAnonymousDonation-dedicated-enitity-controller' into dev 2025-04-28 19:41:35 +02:00
f4bf309821 feat(donations): Implement response type to indicate possible missing donor 2025-04-28 19:35:07 +02:00
02b1cb9904 refactor(donations): Make anon prepaid 2025-04-28 19:32:06 +02:00
7697acff82 fix(donations): Move donor over to the types that need it 2025-04-28 19:25:41 +02:00
bacfc437f9 chore(release): 1.3.12
All checks were successful
Build release images / build-container (push) Successful in 1m24s
2025-04-28 11:05:10 +02:00
9875b4f392 wip 2025-04-28 11:04:22 +02:00
ce9b765b81 refactor(config): improve consola error logs 2025-04-28 11:03:33 +02:00
2ab6e985e3 refactor: make Donation.donor optional 2025-04-28 10:56:06 +02:00
d06f6a4407 chore(release): 1.3.11
All checks were successful
Build release images / build-container (push) Successful in 1m40s
2025-04-17 20:46:56 +02:00
a50d72f2f5 feat(RunnerController): add selfservice_links parameter to getRunners method 2025-04-17 20:45:28 +02:00
4723d9738e chore(release): 1.3.10
All checks were successful
Build release images / build-container (push) Successful in 1m19s
2025-04-11 12:11:08 +02:00
1a478bd784 feat(RunnerController.getAll): debug created_via query param filter 2025-04-11 12:09:10 +02:00
284cb0f8b3 chore(release): 1.3.9
All checks were successful
Build release images / build-container (push) Successful in 1m14s
2025-04-09 11:38:16 +02:00
6e63c57936 feat(RunnerController.getAll): add created_via query param filter 2025-04-09 11:37:49 +02:00
30b61db2c1 chore(release): 1.3.8
All checks were successful
Build release images / build-container (push) Successful in 1m20s
2025-04-09 10:23:48 +02:00
8237d5f210 feat(RunnerCardController): putByCode 2025-04-09 10:23:01 +02:00
03e0a29096 chore(release): 1.3.7
All checks were successful
Build release images / build-container (push) Successful in 1m16s
2025-04-08 21:15:59 +02:00
a6afba93e2 feat(stats): Publish runners by kiosk stat 2025-04-08 21:15:41 +02:00
a41758cd9c chore(release): 1.3.6
All checks were successful
Build release images / build-container (push) Successful in 1m32s
2025-04-08 21:06:01 +02:00
d6755ed134 feat(runners): Allow created via being set via api 2025-04-08 21:04:38 +02:00
599c75fc00 fix(participant): Switch to correct type 2025-04-08 21:03:23 +02:00
bb213f001e chore(release): 1.3.5
All checks were successful
Build release images / build-container (push) Successful in 1m16s
2025-04-08 20:01:13 +02:00
5415cd38a7 feat(runners): Generate selfservice urls on runner if requested or create/update/get single 2025-04-08 20:00:27 +02:00
175ba52ffa chore(release): 1.3.4
All checks were successful
Build release images / build-container (push) Successful in 1m10s
2025-03-28 21:49:42 +01:00
5c5000a218 feat: add runnersViaSelfservice to statsControllerGet 2025-03-28 21:49:19 +01:00
d559d04031 chore(release): 1.3.3
All checks were successful
Build release images / build-container (push) Successful in 1m11s
2025-03-28 21:20:39 +01:00
2af682d1dd ci: remove "v" prefix from tags 2025-03-28 21:20:30 +01:00
30905e481c chore(release): v1.3.2
All checks were successful
Build release images / build-container (push) Successful in 1m17s
2025-03-28 21:16:41 +01:00
752d405bda ci: pnpm@10.7 2025-03-28 21:16:28 +01:00
8fa4ed7c33 chore(release): v1.3.1
Some checks failed
Build release images / build-container (push) Failing after 6s
2025-03-28 21:15:27 +01:00
c4201e9a68 fix: TypeError: Cannot read properties of undefined (reading 'filter') - when trying to delete a org/team with runners
close #210
2025-03-28 21:14:14 +01:00
78dcad0857 pnpm@10.7, node@23, argon->@node-rs/argon2 2025-03-28 21:04:50 +01:00
93e0cdf577 chore(release): v1.3.0
Some checks failed
Build release images / build-container (push) Failing after 6s
2025-03-28 18:56:01 +01:00
6efcd94726 ci: change release commit message 2025-03-28 18:55:25 +01:00
2e271bcd52 fix: add .created_via to ResponseParticipant constructor 2025-03-28 18:24:53 +01:00
ebde8c6ffd ci: move to gitea workflows 2025-03-28 18:07:10 +01:00
a3639dd89b feat: created_via for tracking how runners got into the system (#212)
close #211

squash merge please:)

Reviewed-on: #212
2025-03-28 17:05:10 +00:00
0a43f1bb5b build: docker "AS" casing 2025-03-28 17:41:20 +01:00
8c6fdb2239 refactor(RunnerController.remove): only load necessary relations 2025-03-28 12:01:15 +01:00
c0d5af5d7a refactor(RunnerTeamController.remove): only load necessary relations 2025-03-28 12:00:55 +01:00
4008a5ee72 chore(release): 1.2.1
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-12-11 22:23:58 +01:00
07bf28b144 refactor: allow selfservice link every 30s
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-12-11 22:22:54 +01:00
6764bf80ea chore(release): 1.2.0
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-11 20:05:31 +01:00
b3a73b25e8 refactor(ci): Switch to new woodpecker 2024-12-11 20:05:07 +01:00
bda1f971d1 Merge pull request 'refactor: move to new mailer' (#209) from refactor/new-mailer into dev
Reviewed-on: #209
Reviewed-by: Nicolai Ort <info@nicolai-ort.com>
2024-12-11 19:04:11 +00:00
765ef84903 SELFSERVICE_URL 2024-12-11 18:43:11 +01:00
296ba8ddab FRONTEND_URL env 2024-12-11 18:40:21 +01:00
6eff243803 feat: middlename 2024-12-11 18:34:40 +01:00
0f4c8b2051 refactor: move to new mailer 2024-12-11 18:26:57 +01:00
d842c14240 chore: update readme 2024-12-11 17:55:04 +01:00
a54cb287a4 🚀Bumped version to v1.1.4 2024-11-20 19:00:19 +01:00
74d334f9b7 fix(dependencies): Switch back to previous class-validator version to produce a working build 2024-11-20 18:59:50 +01:00
cd3cd81360 fix(deps): Bump sqlite3
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2023-11-06 20:32:05 +01:00
cf48c00ddb fix(deps): Bumped argon2 to latest version for arm support
Some checks failed
ci/woodpecker/push/build Pipeline failed
2023-11-06 20:23:32 +01:00
3192365793 feat(ci)!: Switch to woodpecker
Some checks failed
ci/woodpecker/push/build Pipeline failed
2023-11-06 20:15:44 +01:00
075d484f11 ci: drop lfk-client-node 2023-11-06 18:09:31 +01:00
5082b1b8b1 fix: updated README for pnpm, typos 2023-11-06 18:04:32 +01:00
50dd703a1b build: package lock 2023-11-06 18:01:15 +01:00
057a8ee699 🚀Bumped version to v1.1.3
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-10 13:38:14 +02:00
8d9418635d feat(orgs): Also resolve child-teams' distances and add them to org total 2023-05-10 13:37:54 +02:00
f2832a2dae fix(orgs): Removed unused log 2023-05-10 13:36:05 +02:00
0d21596e2b 🚀Bumped version to v1.1.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-10 13:16:33 +02:00
245827e9c6 feat(groups): Resolve the total group distance on group get single (aka get org and get team) 2023-05-10 13:15:59 +02:00
4608a36df6 chore(package): Formatting 2023-05-10 13:15:21 +02:00
cb1305aa77 🚀Bumped version to v1.1.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-19 18:10:52 +02:00
12a9ae2493 feat(donors): Resolve donations with donors via pagination 2023-04-19 18:10:26 +02:00
b9fe9f1c24 🚀Bumped version to v1.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-19 15:48:16 +02:00
b25b0db760 Added hints 2023-04-19 15:47:54 +02:00
fe59e3a557 Added average donation per distance to stats 2023-04-19 15:46:50 +02:00
42c23a5883 Formatting 2023-04-19 15:45:34 +02:00
6ee5328dbc Added calls to controller 2023-04-19 15:41:49 +02:00
6f39ac42da feat(stats): Added donation count and donor count to stats 2023-04-19 15:41:43 +02:00
301f334674 🚀Bumped version to v1.0.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-18 20:09:58 +02:00
fcee3909f4 fix(pagination) page=0 resulted in false thx JS 2023-04-18 20:09:44 +02:00
f0e20e4130 🚀Bumped version to v1.0.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-18 20:03:51 +02:00
80de188565 Merge pull request 'feature/205-pagination' (#206) from feature/205-pagination into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #206
Reviewed-by: Philipp Dormann <philipp@noreply.git.odit.services>
2023-04-18 18:03:20 +00:00
2f305e127c Updated test for attribute
All checks were successful
continuous-integration/drone/pr Build is passing
2023-04-18 20:02:03 +02:00
513d7f6fba usergroup pagination
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:44:15 +02:00
244da61892 users pagination
ref #205
2023-04-18 18:43:13 +02:00
2a72aea10e Track pagination
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:41:57 +02:00
71ebce6f8e statsclient pagination
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:40:45 +02:00
f60025b6de scanstation pagination
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:39:37 +02:00
0fa663a341 RunnerTeam Pagination
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:38:27 +02:00
538622aa18 Added pagination for runner orgs
ref #205
2023-04-18 18:37:09 +02:00
86a21dbfa4 Get all pagination for permissions
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:35:25 +02:00
1e9e24d99d Pagination for group contacts
ref #205
2023-04-18 18:34:08 +02:00
4493c0e3d9 Added pagination for get all donors
Some checks failed
continuous-integration/drone/pr Build is failing
ref #205
2023-04-18 18:30:20 +02:00
f5d48fc638 Added pagination for donations
ref #205
2023-04-18 18:28:55 +02:00
b35a2dd2fa Added pagination for runnercards
ref #205
2023-04-18 18:27:11 +02:00
a28ffe06e5 Formatting
ref #205
2023-04-18 18:21:09 +02:00
d873674819 Added pagination for runners
ref #205
2023-04-18 18:20:56 +02:00
37b2ac974b Added pagination for get all scans
ref #205
2023-04-18 18:17:10 +02:00
81aed1de40 🚀Bumped version to v0.15.4
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 22:52:10 +02:00
0f0c3c7214 Fixed possible null
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 22:51:48 +02:00
3909ed34f7 🚀Bumped version to v0.15.3
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 22:36:09 +02:00
b2ac70e0ae Faster stats (not including donations) 2023-04-15 22:35:55 +02:00
5f17e7f783 🚀Bumped version to v0.15.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 21:16:56 +02:00
a5a56a263a Resolve groups again for card generation 2023-04-15 21:15:29 +02:00
2d8f7528d9 Don't resolve runner group and parten with get all card requests 2023-04-15 21:13:14 +02:00
9581185b24 🚀Bumped version to v0.15.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 21:11:56 +02:00
2905884c02 Log batch time in mass scan script 2023-04-15 21:11:32 +02:00
e9914e317b Faster trackscan creation by only loading the latest scan 2023-04-15 21:08:08 +02:00
702070da66 Dont load cards with get all runners request 2023-04-15 20:55:22 +02:00
cc89ba8afb 🚀Bumped version to v0.15.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 20:51:28 +02:00
7c4ff42a3b More scan request optimizations 2023-04-15 20:51:13 +02:00
8007117434 Added test script for creating mass scans 2023-04-15 20:50:48 +02:00
23fa78eb9d Get all scans speed improvement 2023-04-15 20:31:52 +02:00
3b3e68900b 🚀Bumped version to v0.14.6
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 18:20:54 +02:00
3ff666fd3e Missing orm file 2023-04-15 18:19:47 +02:00
4e4435010f 🚀Bumped version to v0.14.5
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-15 18:16:09 +02:00
de9af5a909 Entrypoint fix 2023-04-15 18:15:57 +02:00
ac631f0af4 Fixed copy 2023-04-15 18:13:58 +02:00
6bbdd5bb04 🚀Bumped version to v0.14.4
Some checks failed
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build is passing
2023-04-15 18:09:49 +02:00
a8fc755840 Back to ean13 based codes
Some checks failed
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is failing
2023-04-15 18:09:24 +02:00
27e74e824c pinned pnpm to 8
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-04-12 14:12:05 +02:00
b5c0a288ac coherent baseimage
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2023-03-29 20:35:16 +02:00
85dc3444ac custom pnpm cache
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2023-03-29 20:29:56 +02:00
d02743984d install prod in first step
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2023-03-29 20:29:08 +02:00
734c826fac added missing ci env
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-29 20:27:01 +02:00
33b25c9743 bumped final pnpm version
Some checks failed
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build is passing
2023-03-29 20:03:38 +02:00
6275aaa326 Switched ci over to pnpm + cache
Some checks failed
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build is failing
2023-03-29 19:56:05 +02:00
2a94bfa622 pinned pnpm version 2023-03-29 19:53:42 +02:00
a64f6c9822 COPY by stage name 2023-03-29 19:52:59 +02:00
93d43b7684 Switched dockerfile to pnpm 8 with cache 2023-03-29 19:52:31 +02:00
16ce0a8480 🚀Bumped version to v0.14.3
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-18 22:15:02 +01:00
9a8d618ae4 Adjusted modulo for new fixed card length 2023-03-18 22:14:50 +01:00
38da2d3318 🚀Bumped version to v0.14.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-18 21:55:23 +01:00
068deb4960 Back to modulo 2023-03-18 21:55:10 +01:00
13f093bb61 🚀Bumped version to v0.14.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-18 21:46:38 +01:00
6289f30740 Switched from card prefix replacement via modulo to regex 2023-03-18 21:46:21 +01:00
6ff764bc34 🚀Bumped version to v0.14.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-15 14:44:34 +01:00
ea87cc793b Updated default length 2023-03-15 14:44:19 +01:00
92517e3653 Removed sqlite journal 2023-03-15 14:39:40 +01:00
ffee887ddf breaking(runnercards): shorter runnercard codes (padding to 12 was a bit tooo ambitious) 2023-03-15 14:39:24 +01:00
3bac75e7ab 🚀Bumped version to v0.13.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-02-15 14:55:41 +01:00
d05eddcae1 Merge pull request 'feature/201-no_citizen-deletion' (#202) from feature/201-no_citizen-deletion into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #202
2023-02-15 13:54:54 +00:00
d5c689d693 Updated tests
All checks were successful
continuous-integration/drone/pr Build is passing
ref #201
2023-02-15 14:35:58 +01:00
8fedd4ef3b Added delete check for citizen org
ref #201
2023-02-15 14:34:12 +01:00
e8b2e6f261 🚀Bumped version to v0.13.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-03 16:12:20 +01:00
39f3b0e01f Merge pull request 'move selfservice magic link endpoint to 15min rate limit' (#200) from feature/runner-selfservice-login-link-rate-limit into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #200
2023-02-03 15:09:34 +00:00
edaf255e8f move to 15min limit
All checks were successful
continuous-integration/drone/pr Build is passing
2023-02-03 14:12:28 +01:00
41c4ed4d0f Merge pull request 'Releases 0.12.0 and 0.13.0' (#199) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #199
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2023-02-03 13:04:39 +00:00
f2bd88aadf 🚀Bumped version to v0.13.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-02-02 16:16:50 +01:00
67a3661448 Updated description 2023-02-02 16:16:36 +01:00
0c763a2dfd 🚀Bumped version to v0.13.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-02-02 12:58:19 +01:00
a7297ff933 Moved changelog generation to package script 2023-02-02 12:58:06 +01:00
4cdba8bc77 Updated readme 2023-02-02 12:56:23 +01:00
77c6303014 Moved license and changelog export to releaseit hooks 2023-02-02 12:55:42 +01:00
2b641faa29 📖New license file version [CI SKIP] [skip ci] 2023-02-02 11:52:17 +00:00
9fa8b93c08 🧾New changelog file version [CI SKIP] [skip ci] 2023-02-02 11:51:42 +00:00
4b676bc853 Merge pull request 'feature/197-duplicate_runner_mail' (#198) from feature/197-duplicate_runner_mail into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #198
2023-02-02 11:51:23 +00:00
4433ddb1e1 Updated logo url
All checks were successful
continuous-integration/drone/pr Build is passing
2023-02-02 11:24:04 +01:00
39aa7598b7 Updated tests for new login in selfservice
All checks were successful
continuous-integration/drone/pr Build is passing
ref #197
2023-02-02 11:18:45 +01:00
19a290c3a9 Fixed typo 2023-02-02 11:17:18 +01:00
9bc80aac8a Updated selfservice tests to prevent email duplication
ref #197
2023-02-02 11:14:48 +01:00
e184673963 Added faker for testing
ref #197
2023-02-02 11:10:04 +01:00
68cd746a9f Added selfservice runner create check to prevent duplicate email
ref #197
2023-02-02 11:08:36 +01:00
69651d9f6c Rename selfservice forgot to login
ref #197
2023-02-02 11:03:12 +01:00
6fd246f43c 📖New license file version [CI SKIP] [skip ci] 2023-02-02 09:24:02 +00:00
ae14d6c74f 🧾New changelog file version [CI SKIP] [skip ci] 2023-02-02 09:23:34 +00:00
2fa56b82d1 Add git for changelog fun
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-02 10:23:11 +01:00
9cc66eebdf depends_on: ["clone"]
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-02 10:21:19 +01:00
4c10e20b91 🚀Bumped version to v0.12.0
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-02 10:19:57 +01:00
9217421221 Enabled tag via release script 2023-02-02 10:19:30 +01:00
4570845b3e Pinned pnpm for builds 2023-02-02 10:18:08 +01:00
0e78951300 Drone -> Kaniko based builds 2023-02-02 10:15:46 +01:00
6ad56b3126 Drone images to odit registry 2023-02-02 10:13:04 +01:00
d95c6d3365 Bumped container base images 2023-02-02 10:10:48 +01:00
1f2c8abb22 Ignore pnpm lock 2023-02-02 10:08:22 +01:00
a6d5693ccd Pinned versions 2023-02-02 10:05:23 +01:00
31b258b4ce 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-22 18:21:52 +00:00
f19f2808d8 Merge pull request 'Release 0.11.1' (#196) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #196
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-04-22 18:21:16 +00:00
3b9cd2e1bb 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-04-22 17:59:02 +00:00
95320ca1bc 🚀Bumped version to v0.11.1
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2021-04-22 19:57:31 +02:00
f2d127fc98 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-04-22 17:53:09 +00:00
eb526fb57f Added fix for the appended 2
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-22 19:52:32 +02:00
348fe52c42 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-22 17:42:09 +00:00
eef0fa6952 Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-22 19:41:21 +02:00
8a82e059b7 Now prefixing runnercards with 2 2021-04-22 19:41:18 +02:00
2229cdf20d 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-14 17:05:15 +00:00
3220b194d4 Merge pull request 'Release 0.11.0' (#195) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #195
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-04-14 17:04:29 +00:00
278c4a6a41 🧾New changelog file version [CI SKIP] [skip ci]
Some checks failed
continuous-integration/drone/pr Build is failing
2021-04-14 16:59:20 +00:00
ec50ac31c4 Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-04-14 18:58:24 +02:00
a2f0d814fc 📖New license file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-04-14 16:58:11 +00:00
6468b35708 Merge branch 'dev' of git.odit.services:lfk/backend into dev 2021-04-14 18:57:45 +02:00
3558e99090 🚀Bumped version to v0.11.0 2021-04-14 18:57:26 +02:00
520608aef0 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-14 16:57:03 +00:00
6df5f634f3 Merge pull request 'Donation payment management feature/193-donation_payments' (#194) from feature/193-donation_payments into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #194
2021-04-14 16:56:22 +00:00
da266a8dd6 Fixed spelling
All checks were successful
continuous-integration/drone/pr Build is passing
ref #193
2021-04-14 18:54:02 +02:00
8ae4b85827 Added payedDonationAmount to donor and responsedonor
All checks were successful
continuous-integration/drone/pr Build is passing
ref #193
2021-04-14 18:49:44 +02:00
8fe3243693 Saved missing file
All checks were successful
continuous-integration/drone/pr Build is passing
ref #193
2021-04-14 18:47:10 +02:00
49b174f29f No longer answering with null, but 0
Some checks failed
continuous-integration/drone/pr Build is failing
ref #193
2021-04-14 18:42:38 +02:00
30c6d3d8db Added status to tests
Some checks failed
continuous-integration/drone/pr Build was killed
ref #193
2021-04-14 18:41:20 +02:00
6c14ed9c89 Added mssing check to tests
ref #193
2021-04-14 18:36:59 +02:00
01ed51489e Updated tests
Some checks failed
continuous-integration/drone/pr Build is failing
ref #193
2021-04-14 18:34:15 +02:00
0636616dad Marked payedAmount as optional during creation and/or update
ref #193
2021-04-14 18:29:40 +02:00
34dbaaafe0 Responses now contain the donation status
ref #193
2021-04-14 18:28:08 +02:00
b4c31ee9b5 Added donation status enum
ref #193
2021-04-14 18:25:42 +02:00
99307423c5 Added payed amount to update classes
ref #193
2021-04-14 18:19:26 +02:00
71542bc388 Added payed amount to crealte classes
ref #193
2021-04-14 18:17:26 +02:00
d64f470b60 Added payed amount to response class
ref #193
2021-04-14 18:15:37 +02:00
b8fbb72fa0 Added payed amount fileld to donation class
ref #193
2021-04-14 18:12:45 +02:00
0c61ff457d 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-07 17:28:37 +00:00
1d82f65b0d Merge pull request 'Release 0.10.2' (#192) from dev into main
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
Reviewed-on: #192
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-04-07 17:27:47 +00:00
610988ec16 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-04-07 17:20:21 +00:00
6e236ede14 🚀Bumped version to v0.10.2
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-07 19:18:21 +02:00
b7ad5d3a31 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-07 17:17:14 +00:00
a694ad225c Merge pull request 'stats/runners/laptime feature/190-runners_laptime' (#191) from feature/190-runners_laptime into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #191
2021-04-07 17:16:33 +00:00
5633e85f41 added new ci secret
All checks were successful
continuous-integration/drone/pr Build is passing
ref #190
2021-04-07 18:13:00 +02:00
95e1eec313 Removed all useless console.logs
All checks were successful
continuous-integration/drone/pr Build is passing
ref #190
2021-04-07 16:41:10 +02:00
377d5dadb2 Potential fix for all remaining errors
All checks were successful
continuous-integration/drone/pr Build is passing
ref #190
2021-04-07 16:38:20 +02:00
4a294b1e17 Now resolving all relations for orgs by distance
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-07 16:34:09 +02:00
720774fcf4 Added temp console log
ref #190
2021-04-07 16:31:36 +02:00
dcdbdd15ac Ptotential fix for stats failing
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-07 16:28:48 +02:00
132b48cf2a Removed console log for passing tests
ref #190
2021-04-07 16:26:12 +02:00
23bd432c5f Resolved missing parentgroup relation
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-07 16:23:13 +02:00
71b33ab05b Removed console logs for now working tests
ref #190
2021-04-07 16:21:01 +02:00
87f444c30d At least one fewer test should fail now
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-07 16:17:37 +02:00
4a73eab134 Added temp console log for ci debugging
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-07 16:10:36 +02:00
f8baca5ab2 Updated default docker-compose
ref #190
2021-04-07 16:10:22 +02:00
10221b9f2e Pinned testing container tag to prod container tag
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-07 16:06:06 +02:00
1d8c8c8e9c Removed console log
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-06 10:45:06 +02:00
4603a84f16 Reverted temp bugfix
ref #190
2021-04-06 10:43:54 +02:00
2cd8f3f7f3 Merge branch 'feature/190-runners_laptime' of git.odit.services:lfk/backend into feature/190-runners_laptime
Some checks failed
continuous-integration/drone/pr Build is failing
2021-04-06 10:15:35 +02:00
107eeeae7f Merge branch 'feature/190-runners_laptime' of git.odit.services:lfk/backend into feature/190-runners_laptime 2021-04-06 10:15:32 +02:00
b8767b8bd4 Merge branch 'feature/190-runners_laptime' of git.odit.services:lfk/backend into feature/190-runners_laptime
Some checks failed
continuous-integration/drone/pr Build is failing
2021-04-06 10:08:00 +02:00
bf686e89e0 Temp test logging workaround
ref #190
2021-04-06 10:07:59 +02:00
6163f0a90b Temp test logging workaround
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-06 10:05:05 +02:00
8f0f795a70 Tried workaround for no availdable stats
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-06 09:34:12 +02:00
22cae39bd3 Added temp console log for test
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-06 09:29:23 +02:00
0b07a53ed2 Temp disabled runners by donations test
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-06 09:27:22 +02:00
d4a02e7db2 Added orgs by donations stats tests
ref #190
2021-04-06 09:26:19 +02:00
b9a7dc84f0 Added teams stats endpoint tests
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-06 09:21:23 +02:00
7111068361 Added runners stats tests
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-06 09:20:09 +02:00
63964fbf2c Removed test for content type
All checks were successful
continuous-integration/drone/pr Build is passing
ref #190
2021-04-06 09:14:45 +02:00
cbcb829fbd Fixed typo in test
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-06 09:12:03 +02:00
057ae0d797 Added first selfservice test
Some checks failed
continuous-integration/drone/pr Build is failing
ref #190
2021-04-06 09:09:01 +02:00
257f320ee3 Now resolving all missing relations
ref #190
2021-04-06 09:02:07 +02:00
7b15c2d88b Fixed sorting
ref #190
2021-04-06 08:58:22 +02:00
988f17a795 Fixed sorting algo
ref #190
2021-04-06 08:56:34 +02:00
4471e57438 First try of the laptime sort
ref #190
2021-04-06 08:44:14 +02:00
51daf969cf Added min laptime to StatsRunner
ref #190
2021-04-06 08:14:02 +02:00
cb71fcd13b Added basic laptime endpoint
ref #190
2021-04-06 08:13:43 +02:00
a6a526dc5d Fixed top-ten bein top 9
ref #190
2021-04-05 17:47:51 +02:00
dd6d799c84 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-03 16:25:14 +00:00
e89e07d0fc Merge pull request 'Release 0.10.1' (#189) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #189
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-04-03 16:24:25 +00:00
c28843c405 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-04-03 16:17:51 +00:00
4834a6698b Removed duplicate openapi statement
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-03 18:16:56 +02:00
69afd4d587 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-03 16:15:38 +00:00
24d152fdc8 🚀Bumped version to v0.10.1
Some checks failed
continuous-integration/drone/push Build was killed
2021-04-03 18:14:47 +02:00
4279e43743 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-03 16:14:17 +00:00
d837654617 Merge pull request 'Selfservice donations reformatting feature/187-selfservice_donation' (#188) from feature/187-selfservice_donation into dev
Some checks failed
continuous-integration/drone/push Build was killed
Reviewed-on: #188
2021-04-03 16:13:34 +00:00
0767943721 Switched selfservice donation.donor from string to object
All checks were successful
continuous-integration/drone/pr Build is passing
ref #187
2021-04-03 17:07:44 +02:00
ca87774767 Adjusted runner property names
ref #187
2021-04-03 17:06:54 +02:00
f693f2cde9 Added new responsetype for new class
ref #187
2021-04-03 17:05:58 +02:00
d70c5b1bbc New class: ResponseSelfServiceDonor
ref #187
2021-04-03 17:05:10 +02:00
71e3d0efe2 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-01 16:39:21 +00:00
b517dff8a8 Merge pull request 'Release 0.10.0' (#186) from dev into main
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: #186
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-04-01 16:38:30 +00:00
114c246ace 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-04-01 16:31:25 +00:00
d7703c9e07 Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-01 18:30:38 +02:00
dc3071f7d2 🚀Bumped version to v0.10.0 2021-04-01 18:30:30 +02:00
5fb355f450 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-01 16:30:20 +00:00
33c13de32c Merge pull request 'Mail locales feature/184-mail_locales' (#185) from feature/184-mail_locales into dev
Some checks failed
continuous-integration/drone/push Build was killed
Reviewed-on: #185
2021-04-01 16:29:39 +00:00
1be073a4fa Added locale to mail related user endpoints
All checks were successful
continuous-integration/drone/pr Build is passing
ref #184
2021-04-01 18:25:09 +02:00
b0d8249452 Merge branch 'feature/184-mail_locales' of git.odit.services:lfk/backend into feature/184-mail_locales 2021-04-01 18:23:21 +02:00
7af883f271 Added locale to mail related runner endpoints
ref #184
2021-04-01 18:23:19 +02:00
f5433076b0 Added locale to mail related runner endpoints
ref #84
2021-04-01 18:23:15 +02:00
6aafe4a6ae 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-29 16:43:52 +00:00
bdeeb03645 Merge pull request 'Release 0.9.2' (#183) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #183
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-29 16:42:59 +00:00
675c8762e8 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-29 16:32:26 +00:00
89e392473c 🚀Bumped version to v0.9.2
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-29 18:31:40 +02:00
6c9b91d75a Fixed bug in return creation 2021-03-29 18:31:27 +02:00
8c00aefd6c 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-29 16:13:02 +00:00
3afd785a54 Merge pull request 'Release v0.9.1' (#182) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #182
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-29 16:12:14 +00:00
8099999e2c 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-29 15:49:57 +00:00
a139554e05 🚀Bumped version to v0.9.1
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-29 17:48:53 +02:00
0290b0e5f5 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-29 15:48:47 +00:00
0f7fa990d4 Merge pull request 'Return cards generated in bulk feature/180-blank_generation_return' (#181) from feature/180-blank_generation_return into dev
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #181
2021-03-29 15:48:05 +00:00
2f568c9cb8 Fixed copy-paste oversight
All checks were successful
continuous-integration/drone/pr Build is passing
ref #180
2021-03-28 18:54:16 +02:00
1cb2dc9d53 Added test for returnCards=true array length
Some checks failed
continuous-integration/drone/pr Build is failing
ref #180
2021-03-28 18:47:32 +02:00
6005b0661f Added test for single card generation with returnCards=true
ref #180
2021-03-28 18:46:25 +02:00
5a36c8dcae Added query param to return created runenrcards
ref #180
2021-03-28 18:44:21 +02:00
58f4d2151f 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-26 20:36:54 +00:00
95135ddc89 Merge pull request 'Release 0.9.0' (#179) from dev into main
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: #179
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-26 20:36:02 +00:00
a7fe1e1759 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-26 20:32:56 +00:00
56a5f41686 🚀Bumped version to v0.9.0
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-26 21:32:11 +01:00
c23b4d907f 🚀Bumped version to v0.8.0 2021-03-26 21:32:02 +01:00
bd7b81efe7 📖New license file version [CI SKIP] [skip ci] 2021-03-26 20:31:19 +00:00
274a146b9b 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-26 20:30:16 +00:00
5a3fc5b2bd Merge pull request 'Password security feature/99-password_checks' (#177) from feature/99-password_checks into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #177
2021-03-26 20:29:35 +00:00
070560e863 Fixed test params
All checks were successful
continuous-integration/drone/pr Build is passing
ref #99
2021-03-26 21:24:53 +01:00
536900091a Fixed empty object getting called
Some checks failed
continuous-integration/drone/pr Build is failing
ref #99
2021-03-26 21:19:58 +01:00
8154e715bb Now forceing user deletion in tests
Some checks failed
continuous-integration/drone/pr Build is failing
ref #99
2021-03-26 21:13:17 +01:00
4c6665062f Reenabled user tests
Some checks failed
continuous-integration/drone/pr Build is failing
ref #99
2021-03-26 21:08:50 +01:00
cb3ea9b1eb Fixed pw not getting hashed currectly;
All checks were successful
continuous-integration/drone/pr Build is passing
ref #99
2021-03-26 21:06:42 +01:00
7a64f23937 Moved to tmp files to better check for other problems
Some checks failed
continuous-integration/drone/pr Build is failing
ref #99
2021-03-26 20:57:42 +01:00
96ba25ec6c No longer using createuser in seeding process
Some checks failed
continuous-integration/drone/pr Build is failing
ref #99
2021-03-26 20:52:58 +01:00
e6a8ebcb5b Added user deletion tests
Some checks failed
continuous-integration/drone/pr Build was killed
ref #99
2021-03-26 20:44:28 +01:00
888cab5898 Added user creation invalid tests
ref #99
2021-03-26 20:41:36 +01:00
383a8095b8 Added user creation valid tests
ref #99
2021-03-26 20:41:25 +01:00
63f6526e4f Updated auth test to comply with the new pw requirements
ref #99
2021-03-26 20:28:08 +01:00
b24e24ff7d Added pw errors to user controller
ref #99
2021-03-26 20:24:08 +01:00
9ce35d8eb7 Added pw errors to me controller
ref #99
2021-03-26 20:23:29 +01:00
48a87e8936 Now checking password rules on user update
ref #99
2021-03-26 20:19:23 +01:00
b8c28ebb08 Formatting
ref #99
2021-03-26 20:18:39 +01:00
5daaa3a73c Now checking password rules on user creation
ref #99
2021-03-26 20:18:08 +01:00
24c38cce26 Added password errors
ref #99
2021-03-26 20:17:00 +01:00
bd00f4f8d5 Added password checker dependency
ref #99
2021-03-26 20:11:22 +01:00
03d76e6d0b 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-26 16:35:23 +00:00
3f8e8ce3a6 Merge pull request 'Release 0.8.0' (#176) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #176
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-26 16:34:31 +00:00
c9bd6de476 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-26 16:19:28 +00:00
e702118d4d Merge pull request 'Selfservice deletion feature/174-selfservice_deletion' (#175) from feature/174-selfservice_deletion into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #175
2021-03-26 16:18:28 +00:00
97159dd9f8 Removed param from test
All checks were successful
continuous-integration/drone/pr Build is passing
ref #174
2021-03-26 16:56:45 +01:00
942d9dbc76 Merge branch 'dev' into feature/174-selfservice_deletion
Some checks failed
continuous-integration/drone/pr Build was killed
2021-03-26 16:54:34 +01:00
88844e1a44 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-26 15:53:45 +00:00
e76a9cef95 Merge pull request 'Release 0.7.1' (#173) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #173
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-26 15:52:41 +00:00
20aeed8778 Added tests for the new endpoint
Some checks failed
continuous-integration/drone/pr Build is failing
ref #174
2021-03-26 16:50:12 +01:00
ccb7ae29a3 Fixed response bug
ref #174
2021-03-26 16:45:30 +01:00
dcb12b0ac2 Added selfservice deletion endpoint
ref #174
2021-03-26 16:42:14 +01:00
dd1258333e Updated old hint
ref #174
2021-03-26 16:42:01 +01:00
3ef3a94b20 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-26 14:24:07 +00:00
135852eb9a 🚀Bumped version to v0.7.1
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-26 15:23:05 +01:00
963253cbc8 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-26 14:22:55 +00:00
539a6509b1 Merge pull request 'RESPONSERUNNERCARD fix bugfix/171-responserunnercards' (#172) from bugfix/171-responserunnercards into dev
Some checks failed
continuous-integration/drone/push Build is failing
Thx @philipp :)

Reviewed-on: #172
2021-03-26 14:22:15 +00:00
f3d73d5346 Tests now keep the group
All checks were successful
continuous-integration/drone/pr Build is passing
ref #171
2021-03-26 15:17:05 +01:00
f159252651 Revert "Set timeout even higher b/c sqlite just kills itself during these tests"
Some checks failed
continuous-integration/drone/pr Build is failing
This reverts commit 6ab60998d4.
2021-03-26 15:14:17 +01:00
6ab60998d4 Set timeout even higher b/c sqlite just kills itself during these tests
Some checks failed
continuous-integration/drone/pr Build is failing
ref #171
2021-03-26 15:13:31 +01:00
30d220bc36 Adjusted jest timeout to mitigate sqlite from invalidateing all tests⏱
Some checks failed
continuous-integration/drone/pr Build is failing
ref #171
2021-03-26 15:06:20 +01:00
24aff3bac4 Now resolveing runnercards
ref #171
2021-03-26 14:56:21 +01:00
ce63043887 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-23 17:50:46 +00:00
e40017a6b8 Merge pull request 'Release 0.7.0' (#170) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #170
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-23 17:49:45 +00:00
e843a464e7 🧾New changelog file version [CI SKIP] [skip ci]
Some checks failed
continuous-integration/drone/pr Build is failing
2021-03-23 17:44:04 +00:00
d0ae50d557 🚀Bumped version to v0.7.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-03-23 18:42:16 +01:00
7a49e7c5c9 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-23 17:42:14 +00:00
1dd64204cc Merge pull request 'Bulk card creation feature/168-runnercards_bulk' (#169) from feature/168-runnercards_bulk into dev
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
Reviewed-on: #169
2021-03-23 17:41:31 +00:00
438ff0fc3f Added bulk card creation tests
All checks were successful
continuous-integration/drone/pr Build is passing
ref #168
2021-03-23 18:19:49 +01:00
c1bbda51f0 Added new "bulk" endpoint
ref #168
2021-03-23 18:16:55 +01:00
4705a39aab 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-19 16:41:43 +00:00
4d721f62d9 Merge pull request 'Release 0.6.4' (#167) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #167
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-19 16:40:45 +00:00
b0328ffdaf 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-19 16:34:14 +00:00
031cede542 🚀Bumped version to v0.6.4
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-19 17:33:17 +01:00
3c69f8c4a8 Adjsuted endpoint 2021-03-19 17:32:46 +01:00
cc6568c381 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-18 15:54:33 +00:00
a3a1395a46 Merge pull request 'Release 0.6.3' (#165) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #165
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-18 15:53:42 +00:00
b08acc6660 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-18 15:11:01 +00:00
7a303c2b2c 🚀Bumped version to v0.6.3
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-18 16:10:02 +01:00
3f9a7049e3 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-18 15:09:50 +00:00
6249419fae Merge pull request 'TrackScan Update bug 🐞bugfix/163-trackscan_updates' (#164) from bugfix/163-trackscan_updates into dev
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #164
2021-03-18 15:09:08 +00:00
f347b7ad49 Updated tests 🧪
All checks were successful
continuous-integration/drone/pr Build is passing
ref #163
2021-03-18 14:05:13 +01:00
74faec85c8 Merge branch 'bugfix/163-trackscan_updates' of git.odit.services:lfk/backend into bugfix/163-trackscan_updates 2021-03-18 14:00:16 +01:00
fbdadbef1f The basic bugfix 🐞
ref #163
2021-03-18 14:00:14 +01:00
c87c97c90f The basic bugfix 🐞
REF '!&§
2021-03-18 14:00:04 +01:00
a6bca59ffe 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-17 18:44:42 +00:00
732a1b88d9 Merge pull request 'Release 0.6.2' (#162) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #162
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-17 18:43:45 +00:00
4c960feeb2 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-17 18:42:50 +00:00
72fee96a08 Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-17 19:41:41 +01:00
fcb43f92b0 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-17 18:41:40 +00:00
5ba8f1dd44 🚀Bumped version to v0.6.2 2021-03-17 19:41:35 +01:00
3d3790c2eb Merge pull request 'Bugfixes for trackscans feature/160-responseTrackScan_total_distance' (#161) from feature/160-responseTrackScan_total_distance into dev
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #161
2021-03-17 18:40:47 +00:00
1fa3fa75ee Fixed wrong error type 👀👀
All checks were successful
continuous-integration/drone/pr Build is passing
ref #160
2021-03-17 19:37:35 +01:00
c8882ae6a1 Removed duplicate openapi declarations 🗑
Some checks failed
continuous-integration/drone/pr Build is failing
ref #160
2021-03-17 19:33:44 +01:00
673e896aa3 Added missing discription
ref #160
2021-03-17 19:31:09 +01:00
0ed7f78b2c Fixed missing renameing🛠
ref #160
2021-03-17 19:30:08 +01:00
1d38d308ad Changed the method of getting a parameter from the headers🛠
ref #160
2021-03-17 19:29:12 +01:00
d709ee7479 Now defining security per endpoint 🔐
ref #160
2021-03-17 19:25:49 +01:00
aae042c041 Now auto-etting the station token🔥🔥🔥
ref #160
2021-03-17 19:24:25 +01:00
ca7a84eb3e Merge branch 'feature/160-responseTrackScan_total_distance' of git.odit.services:lfk/backend into feature/160-responseTrackScan_total_distance 2021-03-17 19:22:09 +01:00
1f32ed0727 Marked station as optional (quality of life improvements incoming)
ref #160
2021-03-17 19:22:04 +01:00
289f9e2196 Added comments✏
ref #160
2021-03-17 19:21:35 +01:00
937a9fad4d Added comments✏
ref #150
2021-03-17 19:21:29 +01:00
7c3a1b8fff Merge branch 'dev' into feature/160-responseTrackScan_total_distance 2021-03-17 19:16:54 +01:00
a8ea4fa659 Fixed trackscan vaildation
ref #160
2021-03-17 19:16:42 +01:00
c1dd4518d1 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-17 17:59:10 +00:00
bdc7bb67e7 Merge pull request 'Release v0.6.0' (#159) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #159
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-17 17:58:20 +00:00
54988ba0fe 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-17 16:41:01 +00:00
ce3ca9f1c8 🚀Bumped version to v0.6.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-03-17 17:40:13 +01:00
46b7aceb0b Scanauth return objects 2021-03-17 17:40:01 +01:00
486e450a58 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-17 16:13:23 +00:00
623b5a1873 🚀Bumped version to v0.6.0
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-17 17:12:01 +01:00
a7958eecd6 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-17 15:52:53 +00:00
13e839902c Merge pull request 'Scanstation "me" endpoint feature/157-scanstation_me' (#158) from feature/157-scanstation_me into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #158
2021-03-17 15:52:02 +00:00
94001a48f1 Updated description
All checks were successful
continuous-integration/drone/pr Build is passing
ref #157
2021-03-17 16:50:48 +01:00
2cb7ec7317 As requested by @philpp
All checks were successful
continuous-integration/drone/pr Build is passing
ref #157
2021-03-17 16:49:19 +01:00
757332ed2b Added tests for the new endpoint
All checks were successful
continuous-integration/drone/pr Build is passing
ref #157
2021-03-17 11:51:31 +01:00
8ba7ee1d48 Now adding station id to headers of request for scan auth
ref #157
2021-03-17 11:45:40 +01:00
c5178e0181 Added scanstation me endpoint
ref #157
2021-03-17 11:44:54 +01:00
a1a94ec9da 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-15 15:03:59 +00:00
f7af777104 Applied Docker MTU fix 🛠
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-15 16:03:15 +01:00
076aa87dba 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-12 19:21:08 +00:00
ca6fa633a1 Revert "Switched normal images to chached registry"
Some checks failed
continuous-integration/drone/push Build was killed
This reverts commit cba4455d53.
2021-03-12 20:20:46 +01:00
641e2aed52 Merge branch 'dev' of git.odit.services:lfk/backend into dev
Some checks failed
continuous-integration/drone/push Build was killed
2021-03-12 20:09:36 +01:00
cba4455d53 Switched normal images to chached registry 2021-03-12 20:09:26 +01:00
d5930f7c46 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-12 18:03:04 +00:00
5541ae6ebd Updated ci with new kubernetes secrets 🚀🚀🚀
Some checks failed
continuous-integration/drone/push Build was killed
ref odit/org#12
2021-03-12 19:02:45 +01:00
6c43872198 Changed ci pipeline type to kubernetes
Some checks failed
continuous-integration/drone/push Build encountered an error
ref odit/org#12 (comment)
2021-03-10 20:07:46 +01:00
e4ed20da3e 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-06 13:22:37 +00:00
cb6e78fc17 Merge pull request 'selfservice forgotten mails feature/154-selfservice_forgotten' (#155) from feature/154-selfservice_forgotten into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #155
2021-03-06 13:22:16 +00:00
bf1ec976e3 Added selfservice forgott positive tests
Some checks failed
continuous-integration/drone/pr Build was killed
ref #154
2021-03-06 14:12:46 +01:00
d0a7e34de8 Added all "negative" tests
ref #154
2021-03-06 14:08:39 +01:00
08957d4dc2 Updated to new responsetype
ref #154
2021-03-06 14:02:58 +01:00
1d762f5662 Renamed test
ref #154
2021-03-06 13:57:25 +01:00
a95a9b4ec4 Added first selfservice forgotten test
ref #154
2021-03-06 13:56:55 +01:00
e5dab3469c Changed endpoint url to avoid conflicts
ref #154
2021-03-06 13:56:34 +01:00
c01233b4d6 Added console logging when a testing env get's discovered
ref #154
2021-03-06 13:51:24 +01:00
92920273be Adjusted tests for the new testing env
ref #154
2021-03-06 13:51:07 +01:00
6bb3ae8ba9 Mailer now ignores mailing erros when env is set to test
ref #154
2021-03-06 13:44:10 +01:00
cedc1750c2 Added readme description for testing env
ref #154
2021-03-06 13:42:10 +01:00
3f372123fd Added testing env check
ref #154
2021-03-06 13:41:10 +01:00
a3437475ca Runner controller now uses the Mailer functions
ref #154
2021-03-06 13:38:37 +01:00
83765136cc Added mailer functions
ref #154
2021-03-06 13:36:12 +01:00
e26b7d4923 Implemented the "real" errors
ref #154
2021-03-06 13:32:36 +01:00
e7f0cb45c9 Added not found error logic
ref #154
2021-03-06 13:29:44 +01:00
ffcd45e572 Updated request timeout
ref #154
2021-03-06 13:24:43 +01:00
d7099717c2 Created basic endpoint for user forgotten mails
ref #154
2021-03-06 13:23:01 +01:00
66d6023335 Added last reset requested timestamp to runners
ref #154
2021-03-06 13:15:27 +01:00
5f5c8a061e 📖New license file version [CI SKIP] [skip ci] 2021-03-04 16:27:55 +00:00
bf71e35ecd 🧾New changelog file version [CI SKIP] [skip ci] 2021-03-04 16:23:18 +00:00
64da0eadb3 Merge pull request 'Alpha Release 0.5.0' (#153) from dev into main
Some checks failed
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build was killed
Reviewed-on: #153
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-04 16:22:36 +00:00
52728290b4 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-03-04 16:07:53 +00:00
3f2a2d2929 🚀Bumped version to v0.5.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build was killed
2021-03-04 17:03:51 +01:00
f1d85cfb85 🚀Bumped version to v0.4.7 2021-03-04 17:03:41 +01:00
15356c1030 Merge pull request 'Features for the new selfservice feature/151-selfservice_scans_mails' (#152) from feature/151-selfservice_scans_mails into dev
Some checks failed
continuous-integration/drone/push Build was killed
Reviewed-on: #152
2021-03-04 16:03:04 +00:00
82c65b632c Added scans returns 200 test
All checks were successful
continuous-integration/drone/pr Build is passing
ref #151
2021-03-04 16:40:05 +01:00
ae7d617690 Updated auth reset test for new mailer
ref #151
2021-03-04 16:35:30 +01:00
bf6b70106e Now generateing bs mailer config in test env
ref #151
2021-03-04 16:32:06 +01:00
33310cdb44 Merge branch 'feature/151-selfservice_scans_mails' of git.odit.services:lfk/backend into feature/151-selfservice_scans_mails 2021-03-04 16:29:46 +01:00
db58a280b3 Updated readme env section
ref #151
2021-03-04 16:29:44 +01:00
149f3a83b2 Updated readme env section
ref #151
2021-03-04 16:28:13 +01:00
a5d2a6ecd3 Added locale to pw reset endpoint
ref #151
2021-03-04 16:25:23 +01:00
bb9bad6d90 Now checking for mails being set
ref #151
2021-03-04 16:19:19 +01:00
ada679823c Removed useless functions and updated comments
ref #151
2021-03-04 16:13:36 +01:00
9a1678acf0 Now using mailer as static funtion
ref #151
2021-03-04 16:11:35 +01:00
485c247cd3 Removed (now useless) mail controller
ref #151
2021-03-04 16:11:21 +01:00
ddea02db57 Added new mailer settings to config
ref #151
2021-03-04 16:11:05 +01:00
1551a444ba Added the new mailer code
ref #151
2021-03-04 16:10:43 +01:00
f289afd8bc Updated mail errors
ref #151
2021-03-04 16:10:26 +01:00
a9e06c9055 Promoted axios to dependency
ref #151
2021-03-04 15:56:24 +01:00
c2fdfeed4f Removed mail templates
ref #151
2021-03-04 15:54:47 +01:00
0342757d92 Removed mail config
ref #151
2021-03-04 15:54:27 +01:00
5833f4218f Removed nodemailer from backend
ref #151
2021-03-04 15:52:20 +01:00
0fcc729b56 Removed old mailer code
ref #151
2021-03-04 15:51:05 +01:00
a2c97a11a3 Laptime is now a part of the response
ref #151
2021-03-01 16:59:36 +01:00
aa833736d3 Trackscans now have a laptime that get's calculated on creation
ref #151
2021-03-01 16:58:34 +01:00
771a205fe6 Added new selfservice scans endpoint
ref #151
2021-03-01 16:49:37 +01:00
6074ac5b3a Added selfservice scan response class
ref #151
2021-03-01 16:47:54 +01:00
030b2255d4 Added another resonse type
ref #151
2021-03-01 16:47:05 +01:00
f7f6df41ff Added new selfservice response type
ref #151
2021-03-01 16:36:36 +01:00
be397c8899 🧾New changelog file version [CI SKIP] [skip ci] 2021-02-26 16:50:45 +00:00
dd3c9275d6 Merge pull request 'Alpha Release 0.4.6' (#148) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #148
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-02-26 16:50:03 +00:00
764b7ffe00 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-26 16:48:33 +00:00
d870b2fd01 Merge pull request 'Fixed wrong body acceptance type' (#150) from bugfix/146-usergroup_update into dev
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: #150
2021-02-26 16:48:16 +00:00
aaec09d2ab Fixed wrong body acceptance type
All checks were successful
continuous-integration/drone/pr Build is passing
ref #146
2021-02-26 17:32:26 +01:00
bce8811925 📖New license file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-25 19:41:04 +00:00
3afc207903 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-25 19:39:39 +00:00
fca997beb8 Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-02-25 20:39:21 +01:00
39ebfbf0b6 Pinned package version to avoid dependency conflicts 📌
yeet
2021-02-25 20:39:16 +01:00
3736b29e54 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-25 19:33:24 +00:00
b4c9369a53 Merge branch 'dev' of git.odit.services:lfk/backend into dev
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2021-02-25 20:33:04 +01:00
5d6c8c957a Quick bugfix 2021-02-25 20:33:00 +01:00
09fe47b9aa 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-25 19:30:37 +00:00
b4acd157fc 🚀Bumped version to v0.4.6
Some checks failed
continuous-integration/drone/push Build is failing
2021-02-25 20:30:16 +01:00
b1fced7764 📖New license file version [CI SKIP] [skip ci] 2021-02-24 20:00:08 +00:00
c0cafb4d51 🧾New changelog file version [CI SKIP] [skip ci] 2021-02-24 19:59:05 +00:00
45d61b487e Merge pull request 'New org selfservice endpoint feature/146-more_selfservice_endpoints' (#147) from feature/146-more_selfservice_endpoints into dev
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #147
2021-02-24 19:58:47 +00:00
28ef139a70 Added tests for the new org selfservice endpoints
All checks were successful
continuous-integration/drone/pr Build is passing
ref #146
2021-02-24 18:32:56 +01:00
656f63dfd5 Added selfservice org info endpoint
ref #146
2021-02-24 18:23:08 +01:00
ba3b5eeefc Added selfservice org response model
ref #146
2021-02-24 18:20:31 +01:00
ba396e0eba Added selfservice team response model
ref #146
2021-02-24 18:18:10 +01:00
3c11d88557 Added new response types
ref #146
2021-02-24 18:17:30 +01:00
305fa0078d 🧾New changelog file version [CI SKIP] [skip ci] 2021-02-09 18:25:18 +00:00
a46d14278b Merge pull request 'Alpha release 0.4.5' (#145) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #145
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-02-09 18:24:30 +00:00
680ae8ebbb 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-09 18:19:21 +00:00
cc869f69ad 🚀Bumped version to v0.4.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-02-09 19:18:46 +01:00
b9aac71676 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-09 18:18:22 +00:00
a30a342e00 Merge pull request 'usergroups/permissions endpoint feature/143-usergroup_permissions_endpoint' (#144) from feature/143-usergroup_permissions_endpoint into dev
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
Reviewed-on: #144
2021-02-09 18:18:04 +00:00
bdcfce88cb Now all /usergroups endpoints return ResponseUserGroup
All checks were successful
continuous-integration/drone/pr Build is passing
ref #143
2021-02-09 18:27:11 +01:00
dd81f4c7e4 Merge branch 'feature/143-usergroup_permissions_endpoint' of git.odit.services:lfk/backend into feature/143-usergroup_permissions_endpoint
# Conflicts:
#	src/models/responses/ResponseUserGroup.ts
2021-02-09 18:26:33 +01:00
416f2a1366 The ResponseUserGroup now returns their permisssions as a string array
ref #143
2021-02-09 18:26:22 +01:00
5e353db206 The ResponseUserGroup now returns their permisssions as a string array
ref #143
2021-02-09 18:26:16 +01:00
0c9867d706 Implemented /groups/permissions endpoint
ref #143
2021-02-09 18:18:00 +01:00
8379c3e29c 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-09 16:47:57 +00:00
c4edccace7 Merge pull request 'Alpha release 0.4.4' (#142) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #142
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-02-09 16:47:31 +00:00
74de6559d7 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-08 16:34:37 +00:00
a6f73c733c 🚀Bumped version to v0.4.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-02-08 17:34:03 +01:00
ca3d093e54 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-08 16:33:37 +00:00
28cfbaa662 Merge pull request 'Expanded runner response feature/140-runner_group_parent' (#141) from feature/140-runner_group_parent into dev
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
Reviewed-on: #141
2021-02-08 16:33:21 +00:00
90e1ad7db7 Adjusted test for the new response depth
All checks were successful
continuous-integration/drone/pr Build is passing
ref #140
2021-02-08 16:51:09 +01:00
906a1dc9e7 The group/runners endpoints now also deliver the runner's group's parentGroup
ref #140
2021-02-08 16:48:20 +01:00
5872c6335b Adjusted test for the new response depth
ref #140
2021-02-08 16:46:07 +01:00
701706c028 Now loading runner's group's parentgroup with every runner controller request
ref #140
2021-02-08 16:40:17 +01:00
09bbc70f5f 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-07 12:43:57 +00:00
dd9cb6d3ef Merge pull request 'Alpha Release 0.4.3' (#139) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #139
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-02-07 12:43:30 +00:00
23c732b690 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-07 12:40:54 +00:00
656d564baa 🚀Bumped version to v0.4.3
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-07 13:40:32 +01:00
f3f5cb462e 🧾New changelog file version [CI SKIP] [skip ci] 2021-02-07 12:38:54 +00:00
9959172f2a Merge pull request 'Bugfix for @lfk/frontend/#43' (#138) from bugfix/118-encode_jwt_in_mail into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #138
2021-02-07 12:38:34 +00:00
8f0a396dd0 Bugfix for @lfk/frontend/#43
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-07 13:37:01 +01:00
a18d4d3cee 🧾New changelog file version [CI SKIP] [skip ci] 2021-02-02 15:02:34 +00:00
390b36dfd4 Merge pull request 'Alpha Release 0.4.2' (#137) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #137
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-02-02 15:02:08 +00:00
3b718f3ce5 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-02-02 14:46:03 +00:00
321b20b073 🚀Bumped version to v0.4.2
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-02 15:45:41 +01:00
f7a0ec7174 🧾New changelog file version [CI SKIP] [skip ci] 2021-02-02 14:45:15 +00:00
110a84783e Merge pull request 'Imprint&Privacy Links feature/135-imprint_and_privacy' (#136) from feature/135-imprint_and_privacy into dev
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #136
2021-02-02 14:45:00 +00:00
333e806da4 Added documentation about the new env vars to the readme
All checks were successful
continuous-integration/drone/pr Build is passing
ref #135
2021-02-02 15:10:04 +01:00
f4f621973a Added imprint and privacy to the api spec
ref #135
2021-02-02 15:07:33 +01:00
bcad691045 Added new url env vars to config
ref #135
2021-02-02 15:04:09 +01:00
74791df68b 📖New license file version [CI SKIP] [skip ci] 2021-01-30 17:13:12 +00:00
8425043099 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-30 17:12:25 +00:00
74b982afba fixed license-exporter call
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-30 18:12:09 +01:00
3aefa75412 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-30 16:00:16 +00:00
71cab4e836 Merge pull request 'Alpha Release 0.4.1' (#134) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #134
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-30 15:59:51 +00:00
4e10077901 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-30 15:54:03 +00:00
c32fa93673 🚀Bumped version to v0.4.1
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-30 16:53:38 +01:00
3d1baae0cc Dependency bump🔝 [skip ci] 2021-01-30 16:53:21 +01:00
94dd7963b7 Deleted useless file [ci skip] 2021-01-30 16:44:39 +01:00
7ba67b9dca 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-30 15:40:39 +00:00
6e5f1bd5ff Merge pull request 'Response object types feature/132-object_types' (#133) from feature/132-object_types into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #133
2021-01-30 15:40:24 +00:00
60ee6ebc1e Merge branch 'dev' into feature/132-object_types
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-30 15:32:20 +00:00
02295346da 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-30 15:27:16 +00:00
c4ea808e06 Merge pull request 'Alpha Release 0.4.0' (#131) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #131
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-30 15:26:48 +00:00
ff7406e71a Cleaned up realations regarding response classes
All checks were successful
continuous-integration/drone/pr Build is passing
ref #132
2021-01-30 16:19:42 +01:00
8dc2810c0c Adjusted tests for the new responseType parameter (part 3)
ref #132
2021-01-30 16:19:20 +01:00
ff8af090e3 Adjusted tests for the new responseType parameter (part 2)
ref #132
2021-01-30 15:39:39 +01:00
bcc15e4286 Adjusted tests for the new responseType parameter (part 1)
ref #132
2021-01-29 18:46:15 +01:00
2a87819486 Fixed typos and missing types
ref #132
2021-01-29 17:06:57 +01:00
9d5e486c6d Implemented the interface in all responses
refr #132
2021-01-29 17:06:43 +01:00
e44cc4c4cb Added a Response interface
ref #132
2021-01-29 16:49:19 +01:00
581ca5ff6c Added Responseobjecttype enum
ref #132
2021-01-29 16:45:23 +01:00
b972395ae8 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-27 16:54:23 +00:00
e5f4f6ee59 🚀Bumped version to v0.4.0
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-27 17:53:58 +01:00
fea4857685 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-27 16:52:45 +00:00
f9e75d06b8 Merge pull request 'Implemented testmail endpoint feature/124-testmail' (#130) from feature/124-testmail into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #130
2021-01-27 16:52:28 +00:00
38223b194b Merge branch 'dev' into feature/124-testmail
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-27 17:47:29 +01:00
09b24aa609 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-27 16:47:07 +00:00
348e6cdec7 Merge pull request 'Email Basics feature/118-emails' (#128) from feature/118-emails into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #128
2021-01-27 16:46:34 +00:00
bd1813a0e8 Merge branch 'dev' into feature/118-emails
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-27 16:45:17 +00:00
e07f258a31 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-27 16:44:56 +00:00
61bbeb0d8f Merge pull request 'Mail+Env documentation feature/123-mail_documentation' (#129) from feature/123-mail_documentation into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #129
2021-01-27 16:44:33 +00:00
650a55e586 Merge branch 'dev' into feature/123-mail_documentation
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-27 16:44:19 +00:00
2071c4db33 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-27 16:43:56 +00:00
80e606aa96 Merge branch 'dev' into feature/123-mail_documentation
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-27 16:43:54 +00:00
20f960ed67 Merge pull request 'Alpha Release 0.3.1' (#127) from dev into main
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is passing
Reviewed-on: #127
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-27 16:43:26 +00:00
e6fe8fcd58 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-27 16:32:04 +00:00
870fd47c83 Merge pull request 'new advanced endpoints feature/125-team_runner' (#126) from feature/125-team_runner into dev
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: #126
2021-01-27 16:31:45 +00:00
644045db44 Merge branch 'dev' into feature/125-team_runner
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-27 16:31:16 +00:00
8611fcb849 Merge branch 'dev' into feature/124-testmail
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-27 11:08:37 +00:00
08e6e59655 Removed the duplicate env copy/create from ci tests
All checks were successful
continuous-integration/drone/pr Build is passing
ref #124
2021-01-27 12:06:00 +01:00
ae74b3963f Added test mail sending test
All checks were successful
continuous-integration/drone/pr Build is passing
ref #124
2021-01-27 12:03:02 +01:00
54ed313342 Implemented the test-mail endpoint via a new mailcontroller
ref #124
2021-01-27 11:59:32 +01:00
ad4b903c25 Added a Mail permisssion target
ref #124
2021-01-27 11:53:33 +01:00
9bd7636a23 Added comments
ref #124
2021-01-27 11:37:46 +01:00
b94179e3ca Added a test mail sending function
ref #124
2021-01-27 11:28:27 +01:00
827002989e Added test mail templates
ref #124
2021-01-27 11:28:02 +01:00
eeff67c192 Merge branch 'dev' into feature/123-mail_documentation
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-26 19:42:59 +00:00
583a4bc0dd Changed order
All checks were successful
continuous-integration/drone/pr Build is passing
ref #123
2021-01-26 20:41:44 +01:00
53fcff77d0 Added a hint to ethereal.email
ref #123
2021-01-26 20:41:08 +01:00
1f0c842d9e Table fix
ref #123
2021-01-26 20:39:38 +01:00
13ccab5e28 Added documentation for the env vars
ref #123
2021-01-26 20:38:53 +01:00
b5018eb114 Added the basics about mail templates to the readme
ref #123
2021-01-26 20:26:25 +01:00
aedfcfcc83 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-26 19:15:59 +00:00
db0876015b 🚀Bumped version to v0.3.1
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-26 20:15:35 +01:00
69417e93c0 Added get runners by team test
All checks were successful
continuous-integration/drone/pr Build is passing
ref #125
2021-01-26 20:13:39 +01:00
f71a22f4dd Added get runners by org test
ref #125
2021-01-26 20:12:14 +01:00
570c34bed0 Created the organizations/runners endpoint
ref #125
2021-01-26 20:06:54 +01:00
7be2971a9e Created the runnerTeam/runners endpoint
ref #125
2021-01-26 19:57:35 +01:00
b92f633d68 Now also sending txt mail body
All checks were successful
continuous-integration/drone/pr Build is passing
ref #118
2021-01-26 18:45:19 +01:00
d3647e3399 Added a txt variant of the pw-reset mail
ref #118
2021-01-26 18:41:50 +01:00
389e423850 Cleaned up the replacements
ref #118
2021-01-26 18:41:21 +01:00
46af786516 Fixed missing app_url protocol
ref #118
2021-01-26 18:31:34 +01:00
b4c117b7dc Fixed wrong file location
ref #118
2021-01-26 18:31:17 +01:00
5cade25eeb Translated the pw reset mail to english
ref #118
2021-01-26 18:17:52 +01:00
fb77f4d798 Renamed the template
ref #118
2021-01-26 18:09:27 +01:00
c116338cd7 Added pw reset template provided by @philipp
ref #118
2021-01-26 18:09:00 +01:00
979d36ea91 Password reset now enforces email
ref #118
2021-01-26 18:07:56 +01:00
c43334bf96 The auth tests now use mail to identify the user
ref #118
2021-01-26 18:07:42 +01:00
71c4caae8b Removed bs console.log
ref #118
2021-01-26 17:55:41 +01:00
536de2a319 Implemented automatic ci env generation
ref #118
2021-01-26 17:54:25 +01:00
e26744b792 Implementes mail sending on pw reset request
ref #118
2021-01-26 17:35:03 +01:00
d02e9dec56 Removed tests working directly with the old pw-reset response
ref #118
2021-01-26 17:28:20 +01:00
637975305f Implemented a basic mailer with reset link sending
ref #118
2021-01-26 17:21:18 +01:00
c418603423 Added the first mail error
ref #118
2021-01-26 17:20:55 +01:00
78d2ac3027 Added nodemailer types
ref #118
2021-01-26 17:20:44 +01:00
470703c4de Added env vars
ref #118
2021-01-26 17:20:33 +01:00
e260e16d66 Merge branch 'feature/118-emails' of git.odit.services:lfk/backend into feature/118-emails 2021-01-26 16:10:26 +01:00
6b0155f014 Added a folder for the mail templates
ref #118
2021-01-26 16:10:24 +01:00
33890b544b Added a folder for the mail templates
ref #118
2021-01-26 16:10:02 +01:00
d7ea928714 Added mail env vars
ref #118
2021-01-26 16:09:11 +01:00
908ac4f1ce Added nodemailer dependecy
ref #118
2021-01-26 16:06:47 +01:00
cf012c0b7e Added a barebones class for handleing mail stuff
ref #118
2021-01-26 16:05:55 +01:00
71898d576c 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-24 17:58:01 +00:00
c964591839 Merge pull request 'Alpha Release 0.3.0' (#122) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #122
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-24 17:57:36 +00:00
cc4bf4451c 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-24 17:53:11 +00:00
7dbbd3780d Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-01-24 18:52:36 +01:00
3697783e19 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-24 17:52:09 +00:00
161feaf364 Merge pull request 'OrganiZation rename feature/117-organization' (#121) from feature/117-organization into dev
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
Reviewed-on: #121
2021-01-24 17:51:50 +00:00
75e2a44c9c 🚀Bumped version to v0.3.0 2021-01-24 18:48:06 +01:00
cd7e9b86b4 Renamedpermisssions from *Organisation* to *Organization*📝
All checks were successful
continuous-integration/drone/pr Build is passing
ref #117
2021-01-24 18:43:29 +01:00
c6c643ecf1 Renamed files and classed from *Organisation* to *Organization*📝
ref #117
2021-01-24 18:40:46 +01:00
ef15d0d576 Changed organisation* to organization* in descriptions, comments and endoints ✏
ref #117
2021-01-24 18:34:15 +01:00
5660aecb50 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-22 14:13:45 +00:00
6a66dd803b Merge pull request 'Self service registration feature/112-selfservice_registration' (#120) from feature/112-selfservice_registration into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #120
2021-01-22 14:13:25 +00:00
b42f0722d7 Merge branch 'dev' into feature/112-selfservice_registration
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-21 20:02:23 +01:00
45c8bb83be Fixed tests testing for a old responseclass
ref #112
2021-01-21 20:01:44 +01:00
6469e3bc97 Fixed wrong error getting thrown
ref #112
2021-01-21 19:51:40 +01:00
10f98e9c99 Bugfix: turned old entity in response to responseclass
ref #112
2021-01-21 19:49:11 +01:00
e5b6f650b2 Added registration invalid company tests
ref #112
2021-01-21 19:48:45 +01:00
3b2ed3f0f2 Resolved missing relation
ref #112
2021-01-21 19:46:46 +01:00
20e102ec5c Added registration valid company tests
ref #112
2021-01-21 19:46:32 +01:00
5a003945ac Updated response schema error to a more fitting one
ref #112
2021-01-21 19:34:27 +01:00
29aeb046de Added registration invalid company tests
ref #112
2021-01-21 19:34:11 +01:00
72941da1cb Added registration valid citizentests
ref #112
2021-01-21 19:33:55 +01:00
81d2197a3e Added registration invalid citizen tests
ref #112
2021-01-21 19:22:26 +01:00
9dd9304a71 Citizen registration now returns tokens
ref #112
2021-01-21 19:17:40 +01:00
0c87906cc3 Added selfservice get positive test
ref #112
2021-01-21 19:17:25 +01:00
1227408407 Fixed fluctuating test bahaviour
ref #112
2021-01-21 19:10:59 +01:00
f8d7544517 Marked param as optional (default: false)
ref #112
2021-01-21 19:08:43 +01:00
a9843ed459 Updates old tests to the new ss-ktokens
ref #112
2021-01-21 19:08:21 +01:00
46f9503543 Fixed typo
ref #112
2021-01-21 18:45:15 +01:00
c5d0646c42 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-21 17:32:17 +00:00
b441658570 Merge pull request 'Alpha Release 0.2.1' (#119) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #119
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-21 17:31:49 +00:00
e95c457e44 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-21 17:15:47 +00:00
6de9d547b7 🚀Bumped version to v0.2.1
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-21 18:15:24 +01:00
3a93c9c078 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-21 17:14:56 +00:00
36d01a0a89 Merge pull request 'Runner scans endpoint feature/113-runner_scans' (#116) from feature/113-runner_scans into dev
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #116
2021-01-21 17:14:41 +00:00
6434b4dfce Added check for empty token for runner self-service get
ref #112
2021-01-21 18:12:53 +01:00
e964a8ed44 Added self-service get invalid tests
ref #112
2021-01-21 18:12:29 +01:00
c39a59e54e Implemented runner selfservice token generation
ref #112
2021-01-21 18:03:48 +01:00
34c852b12a Specified uft-8 format for string
ref #112
2021-01-21 17:54:11 +01:00
7b00b19fce MAde uuid column unique
ref #112
2021-01-21 17:52:03 +01:00
ad446500f9 Implemented registration key generation
ref #112
2021-01-21 17:48:13 +01:00
d490247d1e Implemented a registration key for organisations
ref #112
2021-01-21 17:30:43 +01:00
dee36395a6 Citizen runners now have to provide an email address for verification
ref #112
2021-01-21 17:19:04 +01:00
6df195b6ec Created a citizenrunner selfservice create action
ref #112
2021-01-21 17:18:25 +01:00
946efef252 Updated Method of removeing the team of citizen runners
ref #112
2021-01-21 17:01:56 +01:00
73b1114883 Added openapi description
ref #112
2021-01-21 16:48:53 +01:00
1b5465bea8 Implemented the citizen runner self-registration endpoint
ref #112
2021-01-21 16:47:13 +01:00
5288c701c1 Implemented the basics for the runner selfservice registration endpoint
ref #112
2021-01-21 16:43:04 +01:00
10af1ba341 Implemented a runner selfservice registration creation action
ref #112
2021-01-21 16:40:47 +01:00
26dff4f418 Added get tests for the /runner/scans endpoint
All checks were successful
continuous-integration/drone/pr Build is passing
ref #113
2021-01-21 16:07:11 +01:00
b5f3dec93b Added a "onlyValid" query param
ref #113
2021-01-21 15:57:56 +01:00
a82fc0fb9e Added a /runners/id/scans endpoint
ref #113
2021-01-21 15:55:29 +01:00
e2ec0a3b64 Readme reorganisation [skip ci] 2021-01-21 15:43:11 +01:00
f4668b6e81 Added sqlite as to env.sample db of choice [skip ci] 2021-01-21 15:27:11 +01:00
d5281348b6 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-20 19:14:43 +00:00
1717df113e Merge pull request 'Runner selfservice info endpoint feature/111-runner_selfservic_info' (#115) from feature/111-runner_selfservic_info into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #115
2021-01-20 19:14:19 +00:00
0355bdbbab Merge branch 'dev' into feature/111-runner_selfservic_info
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-20 19:13:18 +00:00
02677de5c0 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-20 19:13:07 +00:00
886c1092d6 Merge pull request 'Implemented more seeding feature/110-seeding' (#114) from feature/110-seeding into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #114
2021-01-20 19:12:49 +00:00
191569792c Updated the openapi description
All checks were successful
continuous-integration/drone/pr Build is passing
ref #111
2021-01-20 20:07:16 +01:00
da1fe34249 Implemented the get part of the runner selfservice (no jwts are availdable yet (tm)
ref #111
2021-01-20 20:05:07 +01:00
4ee807973e Fixed wrong amount calculation
ref #111
2021-01-20 20:02:30 +01:00
c5f7cb2c68 Beautified import
ref #111
2021-01-20 19:44:24 +01:00
88a7089289 Created a donation runner response class for the runner selfservice
ref #111
2021-01-20 19:43:53 +01:00
b89f7ac1b4 Created a donation respoinse class for the runner selfservice
ref #111
2021-01-20 19:43:20 +01:00
8079769881 Implemented a method for getting the runner object from a jwt
ref #110
2021-01-20 19:20:08 +01:00
2274b476d6 Added barebones controller for the runner info selfservice
ref #111
2021-01-20 19:05:59 +01:00
e12aedd1aa Fixed the bool converter for null values
All checks were successful
continuous-integration/drone/pr Build is passing
ref #110
2021-01-20 18:28:41 +01:00
434aaf6136 Merge branch 'dev' into feature/110-seeding
Some checks failed
continuous-integration/drone/pr Build is failing
2021-01-20 17:09:14 +00:00
d8b6669d12 📖New license file version [CI SKIP] [skip ci] 2021-01-20 17:07:14 +00:00
dd3d93edc7 Merge pull request 'Alpha Release 0.2.0' (#109) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #109
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-20 17:05:12 +00:00
7bc603028d The data seeding now only get's triggered on the first time thx to using the key-value
Some checks failed
continuous-integration/drone/pr Build is failing
ref #110
2021-01-20 18:02:08 +01:00
c18012f65a Added bool conversion for testdata seeding env var
ref #110
2021-01-20 17:59:33 +01:00
b15967ff31 Added key-value like db table for config flags
ref #110
2021-01-20 17:58:28 +01:00
2db6510a8a Added a citizen org seeder
ref #110
2021-01-20 17:58:11 +01:00
1837336865 Now creating a test contact
ref #110
2021-01-20 17:38:34 +01:00
eab0e634a2 Now also seeding runners to the test org
ref #110
2021-01-20 17:34:53 +01:00
8870ebdb5e SEED_TEST_DATA is now false by default
ref #110
2021-01-20 17:33:31 +01:00
9df9d9ae80 Added a seeder for runner test data
ref #110
2021-01-20 17:26:04 +01:00
67ba489fe2 Added a config option for test data seeding
ref #110
2021-01-20 17:25:46 +01:00
da9a359251 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-19 19:10:15 +00:00
0661729e5f Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-19 20:09:36 +01:00
ddafd90d3e 🚀Bumped version to v0.2.0 2021-01-19 20:09:30 +01:00
8960aa5545 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-19 19:09:11 +00:00
a0c2b5ade8 Merge pull request 'Implemented group contacts feature/104-contacts' (#108) from feature/104-contacts into dev
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #108
2021-01-19 19:08:53 +00:00
a1acd3519f Adjusted env sample
All checks were successful
continuous-integration/drone/pr Build is passing
ref #104 ref #105
2021-01-19 19:33:11 +01:00
c3d008ec0f Updated contact update tests
All checks were successful
continuous-integration/drone/pr Build is passing
ref #104
2021-01-19 19:32:39 +01:00
8ae53f1c49 Updated contact delete tests
ref #104
2021-01-19 19:12:53 +01:00
179c2a5157 Fixed contact cascading
ref #104
2021-01-19 19:04:46 +01:00
dd7e5dae36 Added contact delete tests
ref #104
2021-01-19 19:04:09 +01:00
e165f01930 Added contact add valid tests
ref #104
2021-01-19 18:48:37 +01:00
940d62cde4 Added contact add invalid tests
ref #104
2021-01-19 18:14:09 +01:00
b002cf2df1 Added contact get tests
ref #104
2021-01-19 18:13:39 +01:00
56c73c2555 Added openapi description about non-deletion
ref #104
2021-01-19 18:03:29 +01:00
28fb9834e1 Implemented contact updateing
ref #104
2021-01-19 18:01:37 +01:00
6b4b16c13b Added missing id property 2021-01-19 18:00:45 +01:00
d743f7ee12 Renamed controller to better fit the overall nameing scheme
ref #104
2021-01-19 17:58:03 +01:00
a4e8311cbd Updated comments
ref #104
2021-01-19 17:57:15 +01:00
c172aa8bf8 Added a contact update class
ref #104
2021-01-19 17:55:56 +01:00
d1926fe372 Merge branch 'feature/104-contacts' of git.odit.services:lfk/backend into feature/104-contacts 2021-01-19 17:53:02 +01:00
2b658ac381 Fixed column not getting resolved
ref #104
2021-01-19 17:52:59 +01:00
321d291b4b Fixed column not getting resolved 2021-01-19 17:52:51 +01:00
2eb26e4e38 Fixed push undefined eror
ref #104
2021-01-19 17:41:00 +01:00
3b06d1a6ef Implemented contact group setting on creation
ref #104
2021-01-19 17:29:52 +01:00
de824375d3 Fixed key null constraint
ref #104
2021-01-19 17:27:43 +01:00
11af9c02d9 Implemented contact posting
ref #104
2021-01-19 17:14:05 +01:00
09e429fc67 Added address to contact response
ref #104
2021-01-19 17:13:46 +01:00
703b4f89a6 Merge branch 'dev' into feature/104-contacts 2021-01-19 16:44:34 +01:00
32e054eb84 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-19 15:37:52 +00:00
5e368552ea Merge pull request 'Fully implemented addresses feature/105-addresses' (#107) from feature/105-addresses into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #107
2021-01-19 15:37:35 +00:00
0379786cbd Implemented contact deletion
ref #104
2021-01-19 16:09:23 +01:00
a9a5eb6735 Updated the contact errors
ref #104
2021-01-19 16:06:42 +01:00
ab70f7e498 Implemented the get endpoints
ref #104
2021-01-19 16:05:35 +01:00
1407fe36f3 Added a contact response class
ref #104
2021-01-19 16:02:13 +01:00
d12801e34d Added contact permission target
ref #104
2021-01-19 15:56:55 +01:00
3e7190e279 Added barebones contact controller from donor-controller
ref #104
2021-01-19 15:56:03 +01:00
41423feffe Merge branch 'dev' into feature/105-addresses
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-19 14:51:07 +00:00
30b585c0c1 Set country code for the ci env to DE
All checks were successful
continuous-integration/drone/pr Build is passing
ref #105
2021-01-19 15:49:35 +01:00
a3c93f0d39 Cleaned up var names
Some checks failed
continuous-integration/drone/pr Build is failing
ref #105
2021-01-19 15:48:06 +01:00
f53894b16a 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-16 20:33:37 +00:00
7533c349ef Merge pull request 'Alpha Release 0.1.1 - Hotfix release' (#106) from dev into main
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: #106
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-16 20:32:39 +00:00
91569ced40 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-16 20:30:47 +00:00
f9ae778b21 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-16 20:30:28 +00:00
427dfaafab Added address update ivalid tests
Some checks failed
continuous-integration/drone/pr Build is failing
ref #105
2021-01-16 21:26:45 +01:00
ae589aeb54 Merge branch 'dev' into feature/105-addresses
# Conflicts:
#	src/errors/AddressErrors.ts
#	src/models/actions/create/CreateAddress.ts
#	src/models/actions/create/CreateDonor.ts
#	src/models/actions/create/CreateGroupContact.ts
#	src/models/actions/create/CreateParticipant.ts
#	src/models/actions/create/CreateRunner.ts
#	src/models/actions/create/CreateRunnerOrganisation.ts
#	src/models/actions/update/UpdateDonor.ts
#	src/models/actions/update/UpdateRunner.ts
#	src/models/actions/update/UpdateRunnerOrganisation.ts
#	src/models/entities/Address.ts
#	src/models/entities/IAddressUser.ts
#	src/models/entities/RunnerOrganisation.ts
#	src/models/responses/ResponseParticipant.ts
#	src/tests/donors/donor_add.spec.ts
#	src/tests/donors/donor_update.spec.ts
#	src/tests/runnerOrgs/org_add.spec.ts
#	src/tests/runnerOrgs/org_delete.spec.ts
#	src/tests/runnerOrgs/org_update.spec.ts
#	src/tests/runnerTeams/team_update.spec.ts
#	src/tests/runners/runner_update.spec.ts
2021-01-16 21:15:02 +01:00
1b9d2969eb 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-16 20:06:31 +00:00
daffbcde72 Merge branch 'dev' of git.odit.services:lfk/backend into dev
Some checks failed
continuous-integration/drone/push Build was killed
# Conflicts:
#	CHANGELOG.md
2021-01-16 21:06:12 +01:00
9445c6f21e 🚀Bumped version to v0.1.1 2021-01-16 21:05:43 +01:00
6febb99499 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-16 20:00:06 +00:00
6e6979cfe3 Hotfix: Missing relation bug
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-16 20:59:48 +01:00
230cdb0e37 Added address update valid tests
ref #105
2021-01-16 20:37:48 +01:00
ce450e9b6d Merge branch 'dev' into feature/105-addresses 2021-01-16 20:33:28 +01:00
de36a24191 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-16 19:32:57 +00:00
b167ba07f7 Hotfix: Missing relation bug
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-16 20:32:40 +01:00
4d40225a44 Added first address update tests
ref #105
2021-01-16 20:26:58 +01:00
57b9c2babc Implemented adress deletion (through reset)
ref #105
2021-01-16 20:19:09 +01:00
9dc9ce37d8 Implemented deep address validation
ref #105
2021-01-16 20:12:17 +01:00
f245840cde Implemented postal code validation for the validaton function
ref #105
2021-01-16 18:59:06 +01:00
4824547dde Fixed donor address check
ref #105
2021-01-16 18:52:57 +01:00
8dbee32eee Test's now accept the new address format
ref #105
2021-01-16 18:34:53 +01:00
ae7c5ff0c3 Added address validity check
ref #105
2021-01-16 18:28:19 +01:00
2a465f88c5 Removed old create address class
ref #105
2021-01-16 17:03:05 +01:00
58ae9b589a Removed the address errors
ref #105
2021-01-16 16:58:55 +01:00
8bc01d3f24 Updated comments
ref #105
2021-01-16 16:57:58 +01:00
d0df5dd641 Switched the update classes over to the new address implementation
ref #105
2021-01-16 16:56:46 +01:00
2cd15d25e9 Switched the create classes over to the new address implementation
ref #105
2021-01-16 16:55:30 +01:00
dafac06bc8 Updated the responseclasses to use the new address implementation
ref #105
2021-01-16 16:53:18 +01:00
e2651728c5 Removed the IAddressUser Interface entity
ref #105 - It was only needed b/c addresses were implemented as their own class
2021-01-16 16:50:04 +01:00
673dea2e57 Removed (now useless) relations
ref #105
2021-01-16 16:48:20 +01:00
7fbe649dc9 Switched Address to embedded entity
ref #105
2021-01-16 16:45:49 +01:00
3766899c83 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-15 21:57:40 +00:00
a6c7d54fe7 Merge pull request 'User self-management feature/100-me_endpoints' (#103) from feature/100-me_endpoints into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #103
2021-01-15 21:57:21 +00:00
79bc04bec1 Merge branch 'dev' into feature/100-me_endpoints
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-15 21:56:57 +00:00
f9834b5f4d Moved the me endpoints to /users/me
All checks were successful
continuous-integration/drone/pr Build is passing
ref #100
2021-01-15 22:45:45 +01:00
fc7b8f4c16 Updated descriptions and responses
All checks were successful
continuous-integration/drone/pr Build is passing
ref #100
2021-01-15 22:43:22 +01:00
4f6e81677c Implemented getting own permissions
ref #100
2021-01-15 22:35:50 +01:00
6b7ecd3044 User deletion now requires confirmation
ref #100
2021-01-15 22:35:23 +01:00
8ef5f90abd Implemented the /me controller that allows a user to get and update themselves
ref #100
2021-01-15 22:28:18 +01:00
a334adffc6 Moved optional param to being optional
ref #100
2021-01-15 22:27:44 +01:00
f1db883609 Implemented a baisc user checker/getter
ref #100
2021-01-15 22:16:28 +01:00
e586a11e2a Created barebones file for the userchecker
ref #100
2021-01-15 21:57:39 +01:00
50b893f537 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-15 20:53:36 +00:00
02efb9a8e5 automaticly merge main into dev after building a latest image
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-15 21:53:20 +01:00
38b9a772cd Merge pull request 'First feature version 0.1.0' (#102) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #102
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-15 19:31:40 +00:00
618430433d 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-15 19:18:43 +00:00
84cd398c09 Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-01-15 20:18:19 +01:00
385a9bba73 Fixed broken pkg stuff
ref #102
2021-01-15 20:18:14 +01:00
8218a452bd 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-15 18:18:39 +00:00
a77e2eb3ad Fixed country code type issue
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
https://ci.odit.services/lfk/backend/252/1/2 ref #102
2021-01-15 19:18:23 +01:00
d1a0bed00e 🧾New changelog file version [CI SKIP] [skip ci]
Some checks failed
continuous-integration/drone/pr Build is failing
2021-01-15 18:02:24 +00:00
66d4770858 Merge branch 'dev' of git.odit.services:lfk/backend into dev
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-01-15 19:01:32 +01:00
80c5f9b84d 🚀Bumped version to v0.1.0 2021-01-15 19:01:05 +01:00
79f46cb745 🧾New changelog file version [CI SKIP] [skip ci]
Some checks failed
continuous-integration/drone/pr Build is failing
2021-01-15 18:01:01 +00:00
de32a9862d 👊 Bumped dependency
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
ref #102
2021-01-15 19:00:45 +01:00
0e119e4834 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-15 17:58:01 +00:00
29c8e00477 Merge pull request 'Switched to accepting ids (numbers/number arrays) feature/90-accept_objects' (#101) from feature/90-accept_objects into dev
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
Reviewed-on: #101
2021-01-15 17:57:45 +00:00
dc6ad9cdd3 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-15 17:53:56 +00:00
dcd754dac8 Merge branch 'dev' into feature/90-accept_objects
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-15 18:53:52 +01:00
d88fb18319 Switched tests over to the new id-only schema
All checks were successful
continuous-integration/drone/pr Build is passing
ref #90
2021-01-15 18:50:35 +01:00
420e9c4662 Updated faulty getter function
ref #90
2021-01-15 18:39:30 +01:00
98d6a1cc64 Fixed old reference
ref #90
2021-01-15 18:39:04 +01:00
09ad081b37 Updated faulty getter function
ref #90
2021-01-15 18:36:57 +01:00
aa0fd9cafd Refactoring: switched update user groups from objects to ids
ref #90
2021-01-15 18:35:21 +01:00
bae8290273 Switched to full update from partial and resolved relation
ref #90
2021-01-15 18:33:53 +01:00
1b799a6973 Clarified comments
ref #90
2021-01-15 18:32:41 +01:00
ed3b55a1e2 Refactoring: switched update team parent from objects to ids
ref #90
2021-01-15 18:31:23 +01:00
97c01ce81a Refactoring: switched update org address from objects to ids
ref #90
2021-01-15 18:30:20 +01:00
e96637219f Refactoring: switched update runner group from objects to ids
ref #90
2021-01-15 18:29:30 +01:00
17244b0006 Clarified comments
ref #90
2021-01-15 18:28:24 +01:00
67a02f06da Merge branch 'feature/90-accept_objects' of git.odit.services:lfk/backend into feature/90-accept_objects
# Conflicts:
#	src/models/actions/update/UpdatePermission.ts
2021-01-15 18:27:32 +01:00
6b6f345618 Refactoring: switched from objects to ids
ref #90
2021-01-15 18:27:21 +01:00
2ac9d3e977 Refactoring: switched from objects to ids
ref #90
2021-01-15 18:26:39 +01:00
93692ec255 Clarified comments
ref #90
2021-01-15 18:25:48 +01:00
99852f591e Clarified comments
ref #90
2021-01-15 18:23:30 +01:00
b89525746d Clarified comments
ref #90
2021-01-15 18:22:26 +01:00
c05834f2a1 Removed useless parts from functions and updated comments
ref #90
2021-01-15 18:20:56 +01:00
9bbfb4763d Clarified comments
ref #90
2021-01-15 18:19:34 +01:00
22e6070e53 Removed useless part from function and updated comments
ref #90
2021-01-15 18:19:26 +01:00
ba218c85e0 Made addresses optional gain 2021-01-15 18:18:26 +01:00
644d2b06ac Removed useless part from function and updated comments
ref #90
2021-01-15 18:13:53 +01:00
8d4c8a4553 Removed useless part from function
ref #90
2021-01-15 18:13:10 +01:00
077174a9a2 Clarified comments
ref #90
2021-01-15 18:12:55 +01:00
ce31b95fb7 Removed todo
#90
2021-01-15 18:10:55 +01:00
881eedbf3a Merge pull request 'Alpha Release 0.0.12' (#98) from dev into main
Some checks failed
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is failing
Reviewed-on: #98
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-13 19:33:07 +00:00
09cb6f7b2b 🧾New changelog file version [CI SKIP] [skip ci]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-13 18:25:55 +00:00
bd091d5cb9 🚀Bumped version to v0.0.12
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-13 19:25:28 +01:00
8cb67a8d20 🧾New changelog file version [CI SKIP] [skip ci] 2021-01-13 18:22:48 +00:00
290bb29e64 Disabled auto clone
All checks were successful
continuous-integration/drone/push Build is passing
ref #63
2021-01-13 19:22:19 +01:00
d0769a5e37 Added secondary full clone for tags
Some checks failed
continuous-integration/drone/push Build is failing
ref #63
2021-01-13 19:20:42 +01:00
c5b28df2ae Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/push Build is passing
# Conflicts:
#	.drone.yml
2021-01-13 19:18:39 +01:00
c108fa509f Updated step order
ref #63
2021-01-13 19:18:31 +01:00
1e5e9801be Updated step order
ref #63
2021-01-13 19:18:08 +01:00
09b16c980b 📖New license file version [CI SKIP] [skip ci] 2021-01-13 18:15:22 +00:00
4c26fc808e Fixed spellings
All checks were successful
continuous-integration/drone/push Build is passing
ref #63
2021-01-13 19:14:16 +01:00
525b11b346 Revert "🚀Bumped version to v0.0.12."
Some checks failed
continuous-integration/drone/push Build encountered an error
This reverts commit 86679b498b.
2021-01-13 19:13:04 +01:00
86679b498b 🚀Bumped version to v0.0.12. 2021-01-13 19:12:50 +01:00
46df8b0528 Updated the release machanics
ref #63
2021-01-13 19:12:43 +01:00
1a4f896a8a Merge branch 'dev' of git.odit.services:lfk/backend into dev 2021-01-13 19:07:57 +01:00
aaaa15a0ef Moved changelog generation to dev build for now
ref #63
2021-01-13 19:07:50 +01:00
de65b1c699 🧾New changelog file version [CI SKIP] [skip ci]
Some checks failed
continuous-integration/drone/pr Build was killed
2021-01-13 17:58:33 +00:00
f9437065ee Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-01-13 18:57:17 +01:00
b495cadae9 Added new ci skipping flags
ref #63
2021-01-13 18:57:11 +01:00
47995b77f7 🧾New changelog file version [CI SKIP]
Some checks failed
continuous-integration/drone/pr Build was killed
2021-01-13 17:55:17 +00:00
bc24ec5272 🧾New changelog file version [CI SKIP]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-13 17:54:26 +00:00
2947c41a72 🧾New changelog file version [CI SKIP]
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-13 17:54:00 +00:00
ef53035f70 Reenabled dev build
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
ref #63
2021-01-13 18:53:38 +01:00
290afc3f8f Disabled verification skip
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
ref #63
2021-01-13 18:50:50 +01:00
d6e89b0880 Merge branch 'dev' of git.odit.services:lfk/backend into dev
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2021-01-13 18:47:03 +01:00
2b72552b1f tmp: skip verification 2021-01-13 18:47:00 +01:00
df69418855 tmp: skip verification
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2021-01-13 18:45:04 +01:00
472e402521 disabled dev build temporary
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
ref #63
2021-01-13 18:42:33 +01:00
a3f282667c Merge branch 'dev' of git.odit.services:lfk/backend into dev
# Conflicts:
#	.drone.yml
2021-01-13 18:42:01 +01:00
b86263d972 Disabled custom clone
ref #63
2021-01-13 18:41:02 +01:00
f278320b93 Disabled custom clone
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
ref #63
2021-01-13 18:38:05 +01:00
6345666ae6 Added new pipeline to automagicly generate changelogs on pr to main
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
ref #63
2021-01-13 18:35:12 +01:00
7b5ebab453 Merge pull request 'New user features feature/93-user_endpoints' (#95) from feature/93-user_endpoints into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #95
2021-01-13 17:30:25 +00:00
d4d713b12d Merge branch 'dev' into feature/93-user_endpoints
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-13 17:21:22 +00:00
ab3af54e15 Merge pull request 'Donation API Endpoint feature/66-donation_api' (#94) from feature/66-donation_api into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #94
2021-01-13 17:20:08 +00:00
b01e1eb8a1 Added a new endpoint that returns a users permissions as objects sorted into two arrays
All checks were successful
continuous-integration/drone/pr Build is passing
ref #93
2021-01-13 18:19:59 +01:00
0724932152 Updated some openapi descriptions
All checks were successful
continuous-integration/drone/pr Build is passing
ref #94
2021-01-13 18:01:53 +01:00
cd7b15aadf First part of resolving user inherited permissions
ref #93
2021-01-13 17:57:42 +01:00
37fc167002 Added '@' as a illegal character for usernames
ref #93
2021-01-13 17:51:42 +01:00
9feeb302e8 Switched emails to being mandetory for users
ref #93
2021-01-13 17:44:22 +01:00
bba35d189e Added donor donation amount to the donor response
Some checks failed
continuous-integration/drone/pr Build is failing
ref #66
2021-01-13 17:32:10 +01:00
cd5e4bbd60 Added donation update validtests
ref #66
2021-01-13 17:19:57 +01:00
a513bf13ca Added donation update invalid tests
ref #66
2021-01-12 20:43:07 +01:00
e3e570e664 Added donation add validtests
ref #66
2021-01-12 20:15:51 +01:00
badff85e28 Fixed typos
ref #66
2021-01-12 20:14:23 +01:00
4a0f75044f Added donation add invalid tests
ref #66
2021-01-12 20:09:00 +01:00
b729a7cead Added cascading runner deletion tests
ref #66
2021-01-12 20:01:56 +01:00
4375ca92d3 Added cascading donor deletion tests
ref #66
2021-01-12 20:00:02 +01:00
71537b283f Added donation delete tests
ref #66
2021-01-12 19:53:03 +01:00
63506dac1c Added donation get tests
ref #66
2021-01-12 19:44:15 +01:00
e716fae1c5 Implmented cascading donation deletion for runners and donors
ref #66
2021-01-12 19:33:54 +01:00
f7370bc802 Implemented distance donation updateing
ref #66
2021-01-12 19:06:26 +01:00
72c3fc78b3 Added the basics for distance donation updateing
ref #66
2021-01-12 19:03:33 +01:00
110387dbd3 Merge branch 'feature/66-donation_api' of git.odit.services:lfk/backend into feature/66-donation_api
# Conflicts:
#	src/controllers/DonationController.ts
2021-01-12 19:01:14 +01:00
2820f151e8 Implemented fixed donation updateing
ref #66
2021-01-12 19:01:03 +01:00
9517df5082 Implemented fixed donation updateing
ref #66
2021-01-12 19:00:35 +01:00
56cedf0144 Fixed typo
ref #66
2021-01-12 18:55:20 +01:00
bbaee7cd4d Added the basics for fixed donation updateing
ref #66
2021-01-12 18:53:59 +01:00
8ee2bdf488 Implemented distance donation creation
ref #66
2021-01-12 18:50:55 +01:00
97ecc83fe4 Implemented fixed donation creation
ref #66
2021-01-12 18:50:47 +01:00
57f62a6087 Implemented donation deletion
ref #66
2021-01-12 18:46:02 +01:00
2e760ff461 Implemented the donation creation action models
ref #66
2021-01-12 18:39:14 +01:00
0df26cbd54 Implemented donation getting
ref #66
2021-01-12 18:29:55 +01:00
5f1ab4a2f3 Added donation errors
ref #66
2021-01-12 18:26:55 +01:00
e1ff8c03e1 Added donation permission target
ref #66
2021-01-12 18:21:52 +01:00
55f72c35a6 Implemented the distance donation response
ref #66
2021-01-12 18:20:36 +01:00
6c53701a59 Implemented the donation response
ref #66
2021-01-12 18:16:09 +01:00
02bb634257 Implemented a response donation interface
ref #66
2021-01-12 18:07:41 +01:00
5581c03f77 Added barebones donation controller
ref #66
2021-01-12 18:01:03 +01:00
cf788fe07b Merge pull request 'Fixed backend version related bugs' (#92) from bugfix/91-backend_version into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #92
closes #91
2021-01-12 16:46:37 +00:00
4bf425e1ca Merge branch 'dev' into bugfix/91-backend_version
All checks were successful
continuous-integration/drone/pr Build is passing
2021-01-12 16:46:08 +00:00
a2f4fd5d9b Introduces a very basic version getting endpoint
All checks were successful
continuous-integration/drone/pr Build is passing
ref #91
2021-01-12 17:41:42 +01:00
295a1524d8 Fixed the version getting process
ref #91
2021-01-12 17:39:40 +01:00
234154255c Merge pull request 'Bugfix: resolved missing relation' (#89) from bugfix/88-user_update into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #89
closes #88
2021-01-12 16:34:36 +00:00
7b087840ec Bugfix: resolved missing relation
All checks were successful
continuous-integration/drone/pr Build is passing
ref #88
2021-01-12 16:53:39 +01:00
16b594ebdd Merge branch 'main' into dev
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-10 18:28:09 +01:00
67b3101fd1 Updated some trone pipeline names and messages
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-10 18:26:11 +01:00
b3ce56c605 Merge pull request 'Alpha Release 0.0.11' (#87) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #87
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-10 17:16:09 +00:00
28cefa792c Version bump
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-01-10 18:13:08 +01:00
0803abc168 Merge pull request 'General cleanup and optimisation feature/76-cleanup' (#86) from feature/76-cleanup into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #86
2021-01-10 17:11:31 +00:00
02ae883fa4 Removed everything comit related from the release-it config
All checks were successful
continuous-integration/drone/pr Build is passing
ref #76
2021-01-10 18:09:57 +01:00
be4050768e Moded group updateing to a updateusergroup action model
All checks were successful
continuous-integration/drone/pr Build is passing
ref #76
2021-01-10 18:01:22 +01:00
dc6ec23cb9 Implmented basic release mgnt
ref #76
2021-01-10 17:47:31 +01:00
1bb98c13d1 Dependency bump
ref #76
2021-01-10 17:29:30 +01:00
bca979bab5 Unified remove parameters
ref #76
2021-01-10 17:16:42 +01:00
e4fafd764c Cleaner implementation of the api version getter
ref #76
2021-01-10 17:14:42 +01:00
172159414b Unified the openapi generation
ref #76
2021-01-10 17:10:25 +01:00
9355138a8c App now automagicly displays the current package version as the openapi version
ref #76
2021-01-10 16:59:39 +01:00
343cd8b772 Merge branch 'feature/76-cleanup' of git.odit.services:lfk/backend into feature/76-cleanup 2021-01-10 16:57:43 +01:00
01e0d5b94d Renamed the auth response call to ResponseAuth
ref #76
2021-01-10 16:57:40 +01:00
ac00667465 Renamed the auth response call to ResponseAuth
ref #76
2021-01-10 16:54:19 +01:00
3deae2bfeb Moved all update() and toEntity action model functions to async
ref #76
2021-01-10 16:53:59 +01:00
3f7b0f6563 Renamed the update>Entity Name>() functiuons to update()
ref #76
2021-01-10 16:35:52 +01:00
e6b9d4f273 Renamed the to>Entity Name>() functiuons to toEntity()
ref #76
2021-01-10 16:31:55 +01:00
a00231dd3c Updated imports
ref #76
2021-01-10 16:23:09 +01:00
3bc172e7e0 Intruduced a new folder structure for action models
ref #76
2021-01-10 16:10:02 +01:00
ee9df21ae5 Fixed some typos in errors
ref #76
2021-01-10 16:07:37 +01:00
f96b256ad3 Fixed some typos and extended comments for the middlewares
ref #76
2021-01-10 16:03:56 +01:00
f2c50e929e Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-09 19:05:00 +01:00
02e3239848 Reverted temporary logging 2021-01-09 19:04:07 +01:00
8a54b027d0 Reverted temporary logging
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-09 18:55:16 +01:00
3b11e896d4 Merge branch 'dev' of git.odit.services:lfk/backend into dev
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-09 18:50:50 +01:00
89926b2c31 Temporary: extended live logging 2021-01-09 18:50:48 +01:00
7b4e89555e Temporary: extended live logging
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-09 18:47:11 +01:00
1e37186247 Revert "Temporary: extended live logging"
This reverts commit 154c763719.
2021-01-09 18:45:44 +01:00
154c763719 Temporary: extended live logging
All checks were successful
continuous-integration/drone/push Build is passing
ref lfk/frontend#28
2021-01-09 18:08:36 +01:00
80197d5834 Merge pull request 'feature/78-trackscan' (#85) from feature/78-trackscan into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #85
2021-01-09 16:33:09 +00:00
7e95103a2d added trackscan update tests
All checks were successful
continuous-integration/drone/pr Build is passing
ref #78
2021-01-09 17:18:33 +01:00
efe1a1f543 added trackscan delete tests
ref #78
2021-01-09 16:56:57 +01:00
4fea690670 Added missing parameter fro negative-test
ref #78
2021-01-09 16:54:19 +01:00
f1dee1061d added trackscan get tests
ref #78
2021-01-09 16:49:17 +01:00
61cf0fc08d Implemented proper scan invalidation
ref #78
2021-01-09 16:47:54 +01:00
0c86e5dae1 added trackscan add tests
ref #78
2021-01-09 16:44:52 +01:00
638898fa28 Implemented trackscan updateing
ref #78
2021-01-09 16:17:50 +01:00
e7cd68e1c8 removed distance checks from tests
ref #78
2021-01-09 15:59:36 +01:00
e40e6faebd Merge branch 'feature/78-trackscan' of git.odit.services:lfk/backend into feature/78-trackscan
# Conflicts:
#	src/controllers/RunnerController.ts
2021-01-09 15:45:35 +01:00
3d07aac944 Implemented cascading scan, track and card deletion
ref #78
2021-01-09 15:45:17 +01:00
1a5493facf Implemented cascading scan, track and card deletion
ref #78
2021-01-09 15:43:52 +01:00
9013b9492c Fixed runner distance resolution
ref #78
2021-01-09 15:25:11 +01:00
188f26ad65 Fixed manual trackscan creation
ref #78
2021-01-09 14:52:08 +01:00
3ceb5a0c0f Removed total distance from tests
ref #78
2021-01-09 14:24:16 +01:00
e1ce052d3c Fixed runner total distance not getting resolved
ref #78
2021-01-09 14:23:47 +01:00
70a379edef Merge pull request 'New feature: runner cards (feature/77-runner_cards)' (#84) from feature/77-runner_cards into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #84
2021-01-09 13:01:50 +00:00
35ea3154d1 Added card update tests
All checks were successful
continuous-integration/drone/pr Build is passing
ref #77
2021-01-09 12:42:41 +01:00
ebf66821a2 Added card delete tests
ref #77
2021-01-09 12:41:59 +01:00
8463bee253 added card add tests
ref #77
2021-01-09 12:32:47 +01:00
860680d001 Implmented the EAN generation
ref #77
2021-01-09 12:24:05 +01:00
df39166279 Added card get tests
ref #77
2021-01-09 11:59:20 +01:00
32fda46f0a Implemented runner updateing
ref #77
2021-01-09 11:55:32 +01:00
36ecae7e6e Added card creation
#17
2021-01-09 11:48:13 +01:00
a5bfe4e3d5 Added card deletion + errors
ref #77
2021-01-09 11:28:59 +01:00
4faeddc3f3 Added runner card get endpoints
ref #77
2021-01-09 11:23:12 +01:00
98f7bf366f Added card permission target
ref #77
2021-01-09 11:21:52 +01:00
af3a9e5ce2 Added basic response calss for runner cards
ref #77
2021-01-09 11:15:29 +01:00
52eb7b1afe Added a barebones runnercard controller
ref #77
2021-01-09 11:10:05 +01:00
490fbd241d Merge pull request 'Alpha Release 0.0.10' (#83) from dev into main
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: #83
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-01-08 20:15:27 +00:00
f132131156 Version bump
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-01-08 21:14:20 +01:00
c1e680a063 Fixed responsescheme for the user controller
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-08 21:13:41 +01:00
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
b6cf3b24d4 Merge pull request 'Disabled the x-served-by and x-powered-by Headers' (#44) from feature/41-owasp_headers into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #44
2020-12-21 17:27:12 +00:00
19422edbae Disabled the x-served-by and x-powered-by Headers
All checks were successful
continuous-integration/drone/pr Build is passing
ref #41
2020-12-21 17:48:04 +01:00
1bf6d3d564 Merge pull request 'Updated the put methods and cleaned up a shitload of comments' (#42) from feature/39-update_puts into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #42
closes #39
2020-12-21 16:39:19 +00:00
7d5f3b092f Reverted simplification that created loops
All checks were successful
continuous-integration/drone/pr Build is passing
ref #39
2020-12-21 17:22:07 +01:00
0ef6d9cc48 Small bugfix
Some checks failed
continuous-integration/drone/pr Build is failing
ref #39
2020-12-21 16:58:51 +01:00
48bef8db60 Second part of the action comment refactoring
Some checks failed
continuous-integration/drone/pr Build is failing
ref #39
2020-12-21 16:21:12 +01:00
1d0d79f3da First part of the action comment refactoring
ref #39
Why did i volunteer for this? It's just a glorified sleeping aid 😴
2020-12-21 16:08:10 +01:00
d20d738218 Code + comment cleanup for the entities
ref #39
2020-12-21 15:29:32 +01:00
a03f1a438d Code + comment cleanup for the enums
ref #39
2020-12-20 19:39:13 +01:00
75332983c2 Code + comment cleanup for the response models
ref #39
2020-12-20 19:38:22 +01:00
a85d52437b Code + comment cleanup for the seeds
ref #39
2020-12-20 19:11:28 +01:00
a88c0389c1 Code + Comment cleanup for the middlewares
ref #39
2020-12-20 19:08:02 +01:00
43a4f1118d Updated loader comments and descriptions
ref #39
2020-12-20 19:01:03 +01:00
de91d491e5 Added a missing poiunt/exclamation mark
ref #39
2020-12-20 18:55:52 +01:00
2199cb0aef Fixed messages and comments for UserGroupErrors
ref #39
2020-12-20 18:50:45 +01:00
ee76f1c0e8 Fixed messages and comments for UserErrors
ref #39
2020-12-20 18:50:06 +01:00
75b6489f8d Fixed messages and comments for TrackErrors + spelling for some other errors
ref #39
2020-12-20 18:48:59 +01:00
389f6347c3 Fixed messages and comments for RunnerTeamErrors
ref #39
2020-12-20 18:47:42 +01:00
37afc10e44 Fixed messages and comments for RunnerOrganisationErrors
ref #39
2020-12-20 18:46:43 +01:00
82ced34750 Fixed messages and comments for RunnerGroupErrors
ref #39
2020-12-20 18:45:33 +01:00
5de81ad093 Fixed messages and comments for RunnerErrors
ref #39
2020-12-20 18:45:10 +01:00
c1d784e29c Fixed messages and comments for PermissionErrors
ref #39
2020-12-20 18:42:57 +01:00
4ca85a1f22 Fixed messages and comments for AuthErrors
ref #39
2020-12-20 18:41:25 +01:00
7a4238f1f7 Fixed some stuff not getting checked against null
ref #39 gosh i sometimes hate js types
2020-12-20 18:18:32 +01:00
fbe2b358bd Moved tracks to the new put mechanism
ref #39
2020-12-20 18:07:45 +01:00
532b5a56a5 Switched runner orgs to the cleaner syntax via a update entity
ref #39
2020-12-20 18:07:33 +01:00
18ede29ea5 Moved usergroups to the new put mechanism
ref #39
2020-12-20 17:37:20 +01:00
ec4d75128b Fixed some weired toString beviour
ref #39 #6
2020-12-20 17:36:22 +01:00
b2bd6173a5 Moved permissions to the new put mechanism
ref #39
2020-12-20 17:29:04 +01:00
cc68948a20 Moved runners to the new put mechanism
ref #39
2020-12-20 17:27:21 +01:00
24de82f6df Moved runner teams to the new put mechanism
ref #39
2020-12-20 17:25:41 +01:00
cf583a22fa Added missing username property to the responseuser
ref #39
2020-12-20 17:19:33 +01:00
b55d210aff Fixed wrong error type
ref #39
2020-12-20 17:18:16 +01:00
adec2bcc5b removed useless deletes
ref #39
2020-12-20 17:17:42 +01:00
3850bd9681 Renamed function to better reflect it's function
ref #39
2020-12-20 17:17:30 +01:00
14b6651f96 Merge pull request 'Fixed a bug concerning user updates' (#38) from bugfix/37-user_update into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #38
closes #37
2020-12-20 15:58:19 +00:00
4a21c1fb5c Updated some nameing to fit with the rest of the models
All checks were successful
continuous-integration/drone/pr Build is passing
ref #37
2020-12-20 16:51:59 +01:00
ca142376b3 Fixed some weired user update behaviour
ref #37
2020-12-20 16:49:05 +01:00
314606addd Merge pull request 'feature/34-status_health' (#36) from feature/34-status_health into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #36
closes #34
2020-12-18 22:02:34 +00:00
a0a08f7724 Formatting implemented for @philipp
All checks were successful
continuous-integration/drone/pr Build is passing
ref #34
2020-12-18 23:00:27 +01:00
cea5993049 added a simple health route
ref #34
2020-12-18 22:58:24 +01:00
3e940c2db5 Merge pull request 'Auth for everything (and everything auth) #6' (#35) from feature/6-api_auth into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #35

closes #6
2020-12-18 21:53:16 +00:00
631310f158 Fixed import for linux
All checks were successful
continuous-integration/drone/pr Build is passing
ref #6
2020-12-18 22:49:01 +01:00
c3e3c6bed1 Manual overwrite
Some checks failed
continuous-integration/drone/pr Build is failing
ref #6
2020-12-18 22:46:59 +01:00
8f48d2593b Merge branch 'feature/6-api_auth' of git.odit.services:lfk/backend into feature/6-api_auth
Some checks failed
continuous-integration/drone/pr Build is failing
2020-12-18 22:44:29 +01:00
23758e7a91 Bugfix for bs file names
ref #6
2020-12-18 22:44:27 +01:00
c7fd0593fb Bugfix for bs file names
Some checks failed
continuous-integration/drone/pr Build is failing
ref #6
2020-12-18 22:42:48 +01:00
b19f18ada1 Added auth to all tests
Some checks failed
continuous-integration/drone/pr Build is failing
ref #6
2020-12-18 22:11:41 +01:00
d742ccd581 Jwt's now feature group permissions and permission deduplication
ref #6
2020-12-18 21:44:30 +01:00
d670b814a4 Fixed the user->Group relation
ref #6
2020-12-18 21:42:43 +01:00
1a9c860188 Formatting #6 2020-12-18 20:53:35 +01:00
f25ae9ba4f Added a admin group with all permissions to seeding 2020-12-18 20:33:27 +01:00
744faba7ee Added auth to all endpoints 2020-12-18 20:33:13 +01:00
cdfd0e0d64 Added the openapi security header to all routes that need some kind of auth
ref #6
2020-12-18 20:07:05 +01:00
e25fc795fe Added additional targets and actions for permissions
ref #6
2020-12-18 20:06:27 +01:00
2240a45a91 Added class validation for the enum
ref #6
2020-12-18 19:49:39 +01:00
595a9213c1 Added comments and formatting to the auth checker
ref #6
2020-12-18 19:42:08 +01:00
428e2c38ce Added coments to the jwt creator 2020-12-18 19:33:10 +01:00
1d54fb085b Shoothed out variable nameing scheme
ref #6
2020-12-18 19:19:47 +01:00
6403e386ab Now with smooth access token refreshing
ref #6
2020-12-18 19:07:31 +01:00
65a8449ea3 Now with 1000% cleaner jwt generation
ref #6
2020-12-18 17:57:48 +01:00
b21dd6f0c0 Added tracks/get as test-route for auth
ref #6
2020-12-18 17:19:02 +01:00
445e96dcdf Added toString for permissions
ref #6
2020-12-18 17:17:02 +01:00
6237e62a03 Reimplmented the old permission checking system
ref #6
2020-12-18 17:15:44 +01:00
b9e91502cd Cleaned up the auth checker a little bit 2020-12-18 17:11:44 +01:00
9dc336f0bb Added permission deletion on group deletion
ref #6
2020-12-18 16:21:59 +01:00
6a7e8ccc37 Now with duplication avoidance
ref #6
2020-12-18 16:10:33 +01:00
882065470a Implemented permission updateing
ref #6
2020-12-18 16:05:25 +01:00
ff3a5b4545 User deletion now also delete's the users permissons
ref #6
2020-12-18 15:49:42 +01:00
d4293c164d Implemented permission deletion
ref #6
2020-12-18 15:37:45 +01:00
145a08b1b4 Now with cleaner participants in the responses
ref #6
2020-12-18 15:26:54 +01:00
dc485c02ea Added Permission creation
ref #11
2020-12-18 15:19:44 +01:00
ebb0c5faca Added specific permission getting
ref #6
2020-12-18 15:16:15 +01:00
d89fcb84a2 Implemented permission getting
ref #6
2020-12-18 15:12:06 +01:00
388fc6ba6a Fixed typo 2020-12-18 14:52:31 +01:00
bb4ea485fd Merge branch 'dev' into feature/6-api_auth 2020-12-18 14:40:40 +01:00
5dc9edfe40 Pulled out some linguini-esc code
ref #6
2020-12-18 14:40:19 +01:00
eb9473e230 Cleaned up relation types
ref #6
2020-12-18 14:29:31 +01:00
476afc6a99 Updated nameing to fit the usual scheme
ref #6
2020-12-18 14:29:17 +01:00
ed53627bbe Merge pull request 'Runner import' (#33) from feature/22-runner_import into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #33
closes #22
2020-12-18 13:24:51 +00:00
efecffb72d Added responseusers
ref #6
2020-12-17 21:12:45 +01:00
3aae8f85c4 Added status codes
All checks were successful
continuous-integration/drone/pr Build is passing
ref #22
2020-12-17 20:54:01 +01:00
cc5a30980a Implemented new Permission system on the DB side.
ref #22
2020-12-17 20:46:54 +01:00
c90f9f1dd4 Fixed path
All checks were successful
continuous-integration/drone/pr Build is passing
2020-12-17 19:27:38 +01:00
15ed9f58d5 Added responseschemas and content types
All checks were successful
continuous-integration/drone/pr Build is passing
ref #22
2020-12-17 19:17:35 +01:00
9db4344153 Expanded API Decriptions
ref #22
2020-12-17 19:15:11 +01:00
03b7e346ab Working csv import
ref #22
2020-12-17 18:36:51 +01:00
0d8fbf1eca Consolidated the json import for a cleaner result
ref #22
2020-12-17 17:25:17 +01:00
71228fbf33 Now organisations and teams can import runners 2020-12-17 17:14:08 +01:00
97494aeaf7 Runners can now be imported into a org 2020-12-17 16:46:00 +01:00
4801e010b4 Removed useless console.log
ref #22
2020-12-17 16:33:54 +01:00
1b59d58c60 Abstracted a little bit more for potential company runner import 2020-12-17 16:32:29 +01:00
cad30c7f63 Fixed the dynamic class creation 2020-12-17 16:26:33 +01:00
a8ec0142b0 Added import-action classes
ref #22
2020-12-17 16:21:02 +01:00
30952aa14f Marked csv import as not implemented 2020-12-17 16:20:20 +01:00
2e4a4f1661 Added endpoints for runner import by json and csv 2020-12-16 19:03:27 +01:00
b9fd2379f4 Added rawbody if needed
ref #22
2020-12-16 19:00:25 +01:00
1b1f8f2b09 Added a basic import controller
ref #22
2020-12-16 18:20:31 +01:00
39b932a81c Merge pull request 'feature/31-lib_generation' (#32) from feature/31-lib_generation into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #32
2020-12-15 15:41:41 +00:00
ec69f6caf3 removed the lib generation part
All checks were successful
continuous-integration/drone/pr Build is passing
2020-12-13 19:20:36 +01:00
ad908a3555 Fixed broken substitution
All checks were successful
continuous-integration/drone/pr Build is passing
ref #31
2020-12-13 12:57:09 +01:00
3e6c7b6302 Cleanup
Some checks failed
continuous-integration/drone/pr Build encountered an error
2020-12-13 10:06:32 +01:00
d0c5323cb6 Push
ref #31
2020-12-13 10:04:09 +01:00
fcb3e35b29 Removed the test pipeline
ref #31
2020-12-13 09:42:29 +01:00
4705b5a0b4 I just need to trigger sth
All checks were successful
continuous-integration/drone/push Build is passing
ref #31
2020-12-13 09:21:50 +01:00
0c6f3d1f12 Added downstream trigger
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-13 09:18:54 +01:00
ff178f9d77 Removed the bs code
ref #31
2020-12-12 22:37:11 +01:00
e59630b17e More switching
Some checks failed
continuous-integration/drone/push Build is failing
ref #31
2020-12-12 22:34:07 +01:00
20ec6e0cd6 fixed command order
Some checks failed
continuous-integration/drone/push Build is failing
ref #31
2020-12-12 22:32:43 +01:00
e10a3947ba fixed duplicate name
Some checks failed
continuous-integration/drone/push Build is failing
ref #31
2020-12-12 22:31:44 +01:00
8d00487359 test drone pipeline
Some checks failed
continuous-integration/drone/push Build encountered an error
ref #31
2020-12-12 22:31:06 +01:00
f304b86cb6 Added lib to gitignore
ref #31
2020-12-12 22:22:33 +01:00
421ddc50ed Added rlly basic lib generation
ref #31
2020-12-12 22:21:45 +01:00
c3aa88c212 Renamed some drone steps [skip-ci}
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-12 20:05:43 +01:00
10dbd233a0 Merge pull request 'feature/24-production_dockerfile' (#30) from feature/24-production_dockerfile into dev
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #30
closes #24
2020-12-12 19:03:22 +00:00
c321da613a Switched env to dev for tests(ci)
All checks were successful
continuous-integration/drone/pr Build is passing
ref #24
2020-12-12 20:01:26 +01:00
ff84209683 Merge branch 'dev' into feature/24-production_dockerfile
Some checks failed
continuous-integration/drone/pr Build was killed
2020-12-12 18:54:02 +00:00
df3c231fd2 Merge pull request 'feature/25-refresh-token-cookie' (#29) from feature/25-refresh-token-cookie into dev
Reviewed-on: #29
closed #25
2020-12-12 18:52:28 +00:00
ac2da0af63 Now w/ working logout
All checks were successful
continuous-integration/drone/pr Build is passing
ref #25
2020-12-12 19:50:12 +01:00
40fb081332 Cleaned up the pipelines
Some checks failed
continuous-integration/drone/pr Build is failing
ref #24
2020-12-12 19:37:21 +01:00
30928180e6 Switched to prefering body provided tokens over cookie tokens
All checks were successful
continuous-integration/drone/pr Build is passing
ref #25
2020-12-12 19:27:56 +01:00
6aa1e0d573 Cleaned up some errors
ref #25
2020-12-12 19:26:04 +01:00
aca3eaaeea Now w/ working cookie based refresh
ref #25
2020-12-12 19:25:40 +01:00
615b54ec4f Removed secure flag and added expiry basd on ht refresh token
ref#25
2020-12-12 19:13:18 +01:00
c07d40ae93 Added cookie-parser to app.use
ref #25
2020-12-12 19:01:31 +01:00
db5da3d3c2 Removed useless return
ref #25
2020-12-12 18:34:22 +01:00
0e003d2dc4 Set cookies to secure
ref #25
2020-12-12 18:32:48 +01:00
a1c3751164 🚀 CI build on feature branch tags
ref #24
2020-12-12 18:13:23 +01:00
359e955926 🚀 CI/CD
ref #24
2020-12-12 17:48:45 +01:00
c391201570 🐳 optimize Dockerfile in speed and size (pnpm + layers)
ref #24
2020-12-12 17:48:32 +01:00
e3980096e2 🚧 move sqlite to to production
ref #24
2020-12-12 17:48:02 +01:00
a7e27c6f6c drop unused packages
ref #24
2020-12-12 13:27:57 +01:00
bcb266e29b move to node:14.15.1-alpine3.12
ref #24
2020-12-12 13:24:27 +01:00
95f40a9c28 🩺🐳 Docker healthcheck
ref #24
2020-12-12 13:15:29 +01:00
8bcaf710ad integrate pm2 process manager to keep the app up and running
ref #24
2020-12-12 13:14:38 +01:00
b8aebc14e8 🐳 working Dockerfile
ref #24
2020-12-12 13:04:37 +01:00
5ccdfe1540 package.json - drop nodemon delay
ref #24
2020-12-12 13:04:20 +01:00
a1e3289a88 🐞 fixed app.ts for production use
ref #24
2020-12-12 12:39:53 +01:00
47e4f6cd7e basic build works 2020-12-12 12:29:14 +01:00
36fbccb286 🚧 implementation in AuthController@login
ref #25
2020-12-12 11:55:45 +01:00
7429407843 Merge pull request 'New Feature: User seeding feature/19-user_seeding' (#26) from feature/19-user_seeding into dev
Reviewed-on: #26
closes #19
2020-12-11 19:40:44 +00:00
10640f40aa Merge branch 'dev' into feature/19-user_seeding
All checks were successful
continuous-integration/drone/pr Build is passing
# Conflicts:
#	package.json
2020-12-11 20:38:33 +01:00
1e625b0775 Merge pull request 'Added drone pipeline that automaticly runs on prs (or at least it should)' (#27) from feature/23-tests_on_pr into dev
Reviewed-on: #27
closes #23
2020-12-11 19:33:40 +00:00
6cfaec8397 Added ci env
All checks were successful
continuous-integration/drone/pr Build is passing
ref #23
2020-12-11 20:29:33 +01:00
0f419625d2 switched to using the ci testing script
Some checks failed
continuous-integration/drone/pr Build was killed
ref #23
2020-12-11 20:25:26 +01:00
a83a23a647 fixed typo
Some checks failed
continuous-integration/drone/pr Build is failing
ref #23
2020-12-11 20:24:03 +01:00
553a35bb8e switched to custom clone logic
Some checks failed
continuous-integration/drone/pr Build is failing
ref #23
2020-12-11 20:23:12 +01:00
ef3fcee2a9 testing branch parameter
Some checks failed
continuous-integration/drone/pr Build is failing
2020-12-11 20:22:31 +01:00
61b2baaee7 renamed step
Some checks failed
continuous-integration/drone/pr Build is failing
ref #23
2020-12-11 20:18:53 +01:00
31e7d074dc Added dedicated clone step
Some checks failed
continuous-integration/drone/pr Build encountered an error
ref #23
2020-12-11 20:18:03 +01:00
1fbddf5ef8 Added source
Some checks failed
continuous-integration/drone/pr Build is failing
2020-12-11 20:13:32 +01:00
7a79f35b58 Test for branch restrictions
All checks were successful
continuous-integration/drone/pr Build is passing
ref #23
2020-12-11 20:07:52 +01:00
79e418f918 Added a test:ci script (for testing in ci enviornments)
Some checks failed
continuous-integration/drone/pr Build is failing
ref #23
2020-12-11 19:56:27 +01:00
abb13045e6 Switched to yarn
Some checks failed
continuous-integration/drone/pr Build is failing
ref #23
2020-12-11 19:47:35 +01:00
d543dfb201 Added drone pipeline that automaticly runs on prs (or at least it should)
Some checks failed
continuous-integration/drone/pr Build is failing
ref #23
2020-12-11 19:45:56 +01:00
5eabc1fe18 Merge branch 'dev' into feature/19-user_seeding
# Conflicts:
#	ormconfig.ts
#	package.json
2020-12-11 19:32:53 +01:00
473033aa50 User seeding now automaticly runs if no users are detected
ref #19
2020-12-11 19:29:23 +01:00
effa79032b Added seed yarn script
ref #19
2020-12-11 19:14:48 +01:00
57f6775140 Merge pull request 'feature/17-automated_tests' (#21) from feature/17-automated_tests into dev
Reviewed-on: #21
closes: #17
2020-12-10 19:36:07 +00:00
5a27689e80 new get test
ref #17
2020-12-10 20:33:36 +01:00
d295100a48 refactoring: cleaned up the names
ref #17
2020-12-10 20:32:57 +01:00
09decd5600 Added first demo seed
ref #19
2020-12-10 20:26:46 +01:00
6eee80d357 Runner update tests now run clean
ref #17
2020-12-10 20:08:20 +01:00
cada962e5a Merge branch 'dev' into feature/17-automated_tests
# Conflicts:
#	src/controllers/RunnerTeamController.ts
2020-12-10 20:05:52 +01:00
721af32989 Bugfix for runner team updates
ref #13 #17
2020-12-10 20:05:20 +01:00
92dee666ee Added team update test
ref #17
2020-12-10 19:53:19 +01:00
6e12b014e7 future proved the group update failture
ref #17
2020-12-10 19:48:50 +01:00
c20f01f485 Added runner update tests 2020-12-10 19:48:03 +01:00
eda8abb668 Merge branch 'dev' into feature/17-automated_tests
# Conflicts:
#	src/controllers/RunnerController.ts
#	src/models/actions/CreateParticipant.ts
2020-12-10 19:37:52 +01:00
02877ece9c Little comment cleanup
ref #13
2020-12-10 19:36:49 +01:00
f3000f14cd Runner updateing now works with it's own class
ref #13
2020-12-10 19:31:48 +01:00
068e5bd72b Merge branch 'dev' into feature/17-automated_tests
# Conflicts:
#	src/models/responses/ResponseEmpty.ts
2020-12-10 18:51:50 +01:00
bd07763455 Fix for the 404 in the swagger doc 2020-12-10 18:50:18 +01:00
64725d9e7a Added basic update test 2020-12-10 18:39:40 +01:00
d2e0384f3c Added runner deletion tests
ref #17
2020-12-10 18:30:26 +01:00
e223c060d4 Fixed runner get test
ref #17
2020-12-10 18:30:09 +01:00
49ac7be367 Moded runner get tests to a new file and added more of them
ref #17
2020-12-10 17:59:12 +01:00
9df86953cf Merge branch 'dev' into feature/17-automated_tests
# Conflicts:
#	src/app.ts
#	src/controllers/RunnerController.ts
#	src/controllers/RunnerOrganisationController.ts
#	src/controllers/RunnerTeamController.ts
#	src/models/actions/CreateParticipant.ts
2020-12-10 17:45:23 +01:00
aaeef4a27e Replaced a console log with a consola.error 2020-12-10 17:44:17 +01:00
d5e5e27ca3 Merge branch 'feature/13-runner_controllers' into dev 2020-12-10 17:43:15 +01:00
7fe9480c94 Removed console logs 2020-12-10 17:43:00 +01:00
bc80be947d Fixed optional property
ref #13 #17
2020-12-10 17:42:49 +01:00
47862f2e1d Added runner creation tests
ref #17
2020-12-10 17:41:15 +01:00
d0d050e6c6 Added basic runner get tests
ref #17
2020-12-10 16:18:23 +01:00
e2feffa1c5 Merge branch 'dev' into feature/17-automated_tests 2020-12-10 16:13:40 +01:00
ff6a4eaca1 Removed sqlite jurnal (however it managed to end up here) 2020-12-10 16:13:05 +01:00
3e961e34a1 Added squlite jurnal tmp file to the gitignore 2020-12-10 16:12:26 +01:00
32a92b1ad7 Added org deletion tests (orgs that still have teams)
ref #17
2020-12-10 16:10:35 +01:00
105efdd454 Added team update tests
ref #17
2020-12-09 20:08:01 +01:00
6e316a7533 Cleanup
ref #17
2020-12-09 19:57:02 +01:00
71e5be2ba4 added non-existant deletion test for teams
ref #17
2020-12-09 19:55:36 +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
80ef7e8c3a Adjustes responsecode 2020-12-09 19:43:51 +01:00
d2898ff60f Merge branch 'dev' into feature/17-automated_tests 2020-12-09 19:42:51 +01:00
429041fef1 Merge branch 'dev' into feature/13-runner_controllers 2020-12-09 19:41:31 +01:00
df5b8ac141 Went back to using id's for deletion (for cleaner query params)
ref #13 #17
2020-12-09 19:41:15 +01:00
cbecff85f0 Merge branch 'feature/18-exported-env-vars' into dev
close #16, close #18
2020-12-09 19:41:09 +01:00
622bdf7a3f move to dotenv + custom env validations
ref #18
2020-12-09 19:40:08 +01:00
a068c4d318 Deletes now return 204 instead of 404 (better rest compatability)
ref #13
2020-12-09 19:34:49 +01:00
6396fffc04 Added test for non-existant deletion
ref #17
2020-12-09 19:20:33 +01:00
5845a91f15 Fixed typos 2020-12-09 19:20:19 +01:00
f4abbfcee4 Added test for getting an non-existant team 2020-12-09 19:16:10 +01:00
c3258b9304 Added team delete test 2020-12-09 19:15:54 +01:00
4cfe9df429 final phone validation move to ZZ default
close #16
2020-12-09 19:09:20 +01:00
0fc0b87c67 Merge branch 'dev' into feature/18-exported-env-vars 2020-12-09 19:04:42 +01:00
ff96ba23d7 Merge branch 'dev' into feature/17-automated_tests 2020-12-09 19:02:29 +01:00
3ae124ef68 Merge branch 'dev' into feature/13-runner_controllers 2020-12-09 19:02:06 +01:00
09f4998499 Merge branch 'dev' of https://git.odit.services/lfk/backend into dev 2020-12-09 19:01:39 +01:00
18ef8df3a9 Merge branch 'feature/12-jwt-creation' into dev
close #12
2020-12-09 19:01:17 +01:00
77b769446f Now throwing errors 2020-12-09 18:59:01 +01:00
00215a81a7 Merge branch 'dev' into feature/17-automated_tests 2020-12-09 18:54:06 +01:00
af1ad482d4 Now throwing errors to the next instance
ref #13
2020-12-09 18:53:36 +01:00
b4b52717fc Added more negative tests for the teams
ref #17
2020-12-09 18:47:15 +01:00
02236caa41 send empty array for user permissions if null
ref #12
2020-12-09 18:46:09 +01:00
2d603a1467 resolve groups + permissions
ref #12
2020-12-09 18:45:39 +01:00
862834c877 Added first team creation tests 2020-12-09 18:42:08 +01:00
76065538c9 Renamed b/c runner teams also need dedicated tests
ref #17
2020-12-09 18:14:29 +01:00
99209981d9 Merge branch 'dev' into feature/17-automated_tests 2020-12-09 18:13:12 +01:00
204e2352a9 Fix for getting one
ref #13
2020-12-09 18:11:23 +01:00
13f96e3190 Added delete test
ref #17
2020-12-09 18:08:45 +01:00
4e3b038dec Added bad test to the put 2020-12-09 17:56:37 +01:00
42d3f9cb98 Merge branch 'dev' into feature/17-automated_tests 2020-12-09 17:52:22 +01:00
0a0050368f Added put tests for runner orgs 2020-12-09 17:52:04 +01:00
e4cb8eba1d Removed relations resolution that broke the update 2020-12-09 17:48:24 +01:00
6da7c23c04 Renamed to better fit the content 2020-12-09 16:28:18 +01:00
db5feb00cc Added more basic tests for the runner orgs
ref #17
2020-12-09 16:27:01 +01:00
381ce9c828 Merge branch 'dev' into feature/17-automated_tests 2020-12-09 16:11:46 +01:00
7bb7da4eed Merge branch 'dev' into feature/13-runner_controllers 2020-12-09 16:11:27 +01:00
4df63a8cc0 Fixxed missing plural
ref #13
2020-12-09 16:10:56 +01:00
34fa94ea4f First tests for orgs
ref #17
2020-12-09 16:10:17 +01:00
fcfc10f7d1 phone countrycode validation in env vars
ref #18 #16
2020-12-06 11:21:10 +01:00
9f7d004c3b Merge branch 'dev' of https://git.odit.services/lfk/backend into dev 2020-12-06 10:48:51 +01:00
39cefbc593 ⚙ use new config loader
ref #18 ,ref #17
2020-12-06 10:48:25 +01:00
2cdc91ec83 Merge branch 'feature/18-exported-env-vars' into feature/17-automated_tests 2020-12-06 10:45:15 +01:00
99d8a0360f 🚚 basic move to config.ts
ref #18
2020-12-06 10:29:56 +01:00
1748fd4034 Merge branch 'feature/17-automated_tests' of https://git.odit.services/lfk/backend into feature/17-automated_tests 2020-12-05 20:45:38 +01:00
34567f24c3 test:watch script 2020-12-05 20:45:22 +01:00
def7ca3eb2 🧪tracks.spec.ts - move to baseurl
ref #17
2020-12-05 20:45:06 +01:00
4dd0217c93 Merge branch 'feature/17-automated_tests' of git.odit.services:lfk/backend into feature/17-automated_tests 2020-12-05 20:33:41 +01:00
a3e8973004 Merge branch 'dev' into feature/17-automated_tests 2020-12-05 20:33:31 +01:00
29acabfca3 🧪tracks.spec.ts - adding + getting + updating tracks
ref #17
2020-12-05 20:32:38 +01:00
4ff6f8c540 ⚙ nodemon config - ignore tests
ref #17
2020-12-05 20:32:15 +01:00
a671bf8bcb 🚧 tracks.spec.ts - sample track adding + getting
ref #17
2020-12-05 20:19:41 +01:00
15e3d04215 🚧tracks.spec.ts - check if track was added
ref #17
2020-12-05 20:17:42 +01:00
07e03ff04e basic track testing
ref #17
2020-12-05 20:11:52 +01:00
5103e8a6e5 Updated folders in the readme 2020-12-05 20:01:06 +01:00
ad6c9e7211 Removed garbage file 2020-12-05 19:15:56 +01:00
1fb09e577c Cleaned up up the middlewares
ref #11
2020-12-05 19:14:04 +01:00
f58a715c45 Cleaned up the loaders
ref #11
2020-12-05 19:09:08 +01:00
a3a809bb40 Merge branch 'feature/11-new_classes' of git.odit.services:lfk/backend into feature/11-new_classes 2020-12-05 19:01:51 +01:00
33b3bcb8c2 Error cleanup
#11 #13 #14
2020-12-05 19:01:48 +01:00
431fd608a6 sample json validation
ref #17
2020-12-05 18:52:36 +01:00
8ae5cea631 basic jest + typescript support
ref #17
2020-12-05 18:49:09 +01:00
278 changed files with 45027 additions and 2180 deletions

9
.env.ci Normal file
View File

@@ -0,0 +1,9 @@
APP_PORT=4010
DB_TYPE=sqlite
DB_HOST=unused
DB_PORT=unused
DB_USER=unused
DB_PASSWORD=bla
DB_NAME=./test.sqlite
NODE_ENV=dev
POSTALCODE_COUNTRYCODE=DE

View File

@@ -1,8 +1,14 @@
APP_PORT=4010
DB_TYPE=bla
DB_TYPE=sqlite
DB_HOST=bla
DB_PORT=bla
DB_USER=bla
DB_PASSWORD=bla
DB_NAME=bla
NODE_ENV=production
DB_NAME=./test.sqlite
NODE_ENV=production
POSTALCODE_COUNTRYCODE=DE
SEED_TEST_DATA=false
SELFSERVICE_URL=bla
STATION_TOKEN_SECRET=<replace-with-random-secret-min-32-chars>
NATS_URL=nats://localhost:4222
NATS_PREWARM=false

View File

@@ -0,0 +1,30 @@
name: Build release images
on:
push:
tags:
- "*.*.*"
jobs:
build-container:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun licenses:export
- name: Login to registry
uses: docker/login-action@v3
with:
registry: registry.odit.services
username: ${{ vars.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
${{ vars.REGISTRY }}/lfk/backend:${{ github.ref_name }}
platforms: linux/amd64,linux/arm64

10
.gitignore vendored
View File

@@ -126,9 +126,17 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Old package manager lockfiles (Bun migration - keep bun.lock)
yarn.lock
package-lock.json
pnpm-lock.yaml
build
*.sqlite
docs
*.sqlite-jurnal
/docs
lib
/oss-attribution
*.tmp

View File

@@ -9,8 +9,7 @@
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features",
"editor.codeActionsOnSave": {
"source.organizeImports": true,
// "source.fixAll": true
"source.organizeImports": "explicit"
}
},
"javascript.preferences.quoteStyle": "single",

282
AGENTS.md Normal file
View File

@@ -0,0 +1,282 @@
# AGENTS.md — LfK Backend
Guidance for agentic coding agents working in this repository.
---
## Project Overview
Express + [`routing-controllers`](https://github.com/typestack/routing-controllers) REST API written in TypeScript. Uses TypeORM for database access (SQLite in dev/test, PostgreSQL or MySQL in production). OpenAPI docs are auto-generated from decorators at startup.
**Runtime & Package Manager**: Bun (replaces Node.js + npm/pnpm).
---
## Build / Run / Test Commands
### Development
```sh
bun run dev # Start dev server with auto-reload (uses Bun's --watch)
```
**Auto-reload**: The `dev` script uses Bun's built-in `--watch` flag, which automatically restarts the server when TypeScript files in `src/` change. Bun runs TypeScript directly - no build step needed.
**Performance**: Bun delivers 8-15% better latency under concurrent load compared to Node.js. See `BUN_BENCHMARK_RESULTS.md` for details.
### Build
```sh
bun run build # rimraf dist && tsc && copy static assets → dist/
```
**Note**: The build script exists for legacy compatibility and type-checking, but is **not required** for development or production. Bun runs TypeScript source files directly.
### Production
```sh
bun start # bun src/app.ts (runs TypeScript directly)
```
### Tests
Tests are **integration tests** that hit a live running server via HTTP. The server must be started before Jest is invoked.
```sh
# Full CI test flow (generates .env, starts server, runs jest):
bun run test:ci
# Run Jest directly (server must already be running):
bun test
# Watch mode:
bun run test:watch
# Run a single test file:
bunx jest src/tests/runners/runner_add.spec.ts
# Run tests matching a name pattern:
bunx jest --testNamePattern="POST /api/runners"
# Run all tests in a subdirectory:
bunx jest src/tests/runners/
```
# Run all tests in a subdirectory:
bunx jest src/tests/runners/
```
> **Important:** `bun test` alone will fail unless the dev server is already running on `http://localhost:<config.internal_port>`. In CI, `start-server-and-test` handles this automatically via `bun run test:ci`.
### Other Utilities
```sh
bun run seed # Sync DB schema and run seeders
bun run openapi:export # Export OpenAPI spec to file
bun run docs # Generate TypeDoc documentation
bun run licenses:export # Export third-party license report
```
---
## TypeScript Configuration
- **Target:** ES2020, **Module:** CommonJS
- **`strict: false`** — TypeScript strictness is disabled; types are used but not exhaustively enforced
- **`experimentalDecorators: true`** and **`emitDecoratorMetadata: true`** — required by `routing-controllers`, `TypeORM`, and `class-validator`
- Spec files (`**/*.spec.ts`) are excluded from compilation
- Source root: `src/`, output: `dist/`
---
## Code Style Guidelines
### No Linter / Formatter Configured
There is no ESLint or Prettier configuration. Follow the patterns already established in the codebase rather than introducing new tooling.
### Imports
- Use named imports for decorator packages: `import { Get, JsonController, Param } from 'routing-controllers'`
- Use named imports for TypeORM: `import { Column, Entity, getConnectionManager } from 'typeorm'`
- Use named imports for class-validator: `import { IsInt, IsOptional, IsString } from 'class-validator'`
- Use `import * as X from 'module'` for modules without clean default exports (e.g., `import * as jwt from 'jsonwebtoken'`)
- Use default imports for simple modules (e.g., `import cookie from 'cookie'`)
- `reflect-metadata` is imported once at the top of `src/app.ts` — do not re-import it
- No barrel/index re-export files; import source files directly by path
### Naming Conventions
| Construct | Convention | Example |
|---|---|---|
| Classes | `PascalCase` | `RunnerController`, `CreateRunner` |
| Files | `PascalCase.ts` matching class name | `RunnerController.ts` |
| Local variables | `camelCase` (some `snake_case` in tests) | `accessToken`, `access_token` |
| DB entity fields | `snake_case` preferred | `created_at`, `updated_at` |
| Controller methods | REST-conventional | `getAll`, `getOne`, `post`, `put`, `remove` |
| Custom errors | `{Entity}{Issue}Error` | `RunnerNotFoundError`, `RunnerIdsNotMatchingError` |
| Response DTOs | `Response{Entity}` | `ResponseRunner`, `ResponseAuth` |
| Create DTOs | `Create{Entity}` | `CreateRunner` |
| Update DTOs | `Update{Entity}` | `UpdateRunner` |
| Enums | `PascalCase` | `ResponseObjectType`, `PermissionAction` |
### Formatting
- 4-space indentation (observed throughout the codebase)
- Single quotes for string literals in most files
- No trailing semicolons style inconsistency — follow what's already in the file you're editing
### Types
- Add TypeScript types to all function parameters and return values
- Use `class-validator` decorators (`@IsString`, `@IsInt`, `@IsOptional`, `@IsUUID`, etc.) on every DTO and response class field — these drive both runtime validation and OpenAPI schema generation
- Use abstract classes for shared entity base types (e.g., `abstract class Participant`)
- Use interfaces for response contracts (e.g., `interface IResponse`)
- Use enums for typed string/number constants
- Avoid `any` where possible; when unavoidable, keep it localised
- `strict` is off — but still annotate types explicitly rather than relying on inference
### Controller Pattern
```typescript
import { Authorized, Body, Delete, Get, JsonController, Param, Post, Put } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
@JsonController('/runners')
@Authorized()
export class RunnerController {
@Get('/')
@OpenAPI({ description: 'Returns all runners' })
@ResponseSchema(ResponseRunner, { isArray: true })
async getAll() { ... }
@Get('/:id')
@ResponseSchema(ResponseRunner)
async getOne(@Param('id') id: number) { ... }
@Post('/')
@ResponseSchema(ResponseRunner)
async post(@Body({ validate: true }) createRunner: CreateRunner) { ... }
@Put('/:id')
@ResponseSchema(ResponseRunner)
async put(@Param('id') id: number, @Body({ validate: true }) updateRunner: UpdateRunner) { ... }
@Delete('/:id')
@ResponseSchema(ResponseRunner)
async remove(@Param('id') id: number) { ... }
}
```
### Error Handling
- Define custom error classes in `src/errors/` extending `routing-controllers` error types (`NotFoundError`, `NotAcceptableError`, etc.)
- Every custom error class must include `@IsString()` decorated `name` and `message` fields for OpenAPI schema generation
- Throw custom errors directly in controllers: `throw new RunnerNotFoundError()`
- Use try/catch in controllers and re-throw meaningful errors; do not swallow errors silently
- The global `ErrorHandler` middleware (registered in `src/middlewares/`) catches all unhandled errors and serialises them as JSON — do not duplicate this logic in controllers
- Auth errors are thrown from `src/middlewares/authchecker.ts`, not from individual controllers
### Entity Pattern (TypeORM)
- Entities live in `src/models/entities/`
- Decorate every entity with `@Entity()` and every column with the appropriate `@Column`, `@PrimaryGeneratedColumn`, etc.
- Use `@CreateDateColumn()` / `@UpdateDateColumn()` for timestamp fields
- Use table inheritance (`@TableInheritance` + `@ChildEntity`) for polymorphic entities
- Access repositories via `getConnectionManager().get().getRepository(EntityClass)` — do not inject repositories as constructor dependencies
- Database schema is synchronised automatically on startup (`connection.synchronize()`) — no manual migration files
### DTO Pattern (Create / Update)
- Create DTOs in `src/models/actions/create/` and `src/models/actions/update/`
- Use `class-validator` decorators for every field
- `@IsOptional()` for fields that are not required on update; all fields on create DTOs should be mandatory unless explicitly optional in the API contract
- Response DTOs live in `src/models/responses/` and follow the `Response{Entity}` naming pattern
---
## Test Style Guidelines
> **IMPORTANT: Do not run existing tests and do not create new tests.** The existing test suite in `src/tests/` is outdated and no longer reflects the current state of the codebase. Ignore all test files when working in this repository. Do not write new tests for any changes or additions.
All tests are integration tests in `src/tests/` organised by domain entity:
```
src/tests/
auth/
auth_login.spec.ts
auth_refresh.spec.ts
runners/
runner_add.spec.ts
runner_get.spec.ts
runner_update.spec.ts
runner_delete.spec.ts
...
```
### Test File Template
```typescript
import axios from 'axios';
import { config } from '../../config';
const base = "http://localhost:" + config.internal_port;
let access_token: string;
let axios_config: object;
beforeAll(async () => {
jest.setTimeout(20000);
const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" });
access_token = res.data["access_token"];
axios_config = {
headers: { "authorization": "Bearer " + access_token },
validateStatus: undefined // prevents axios from throwing on non-2xx responses
};
});
describe('POST /api/runners working', () => {
it('creating a runner with required params should return 200', async () => {
const res = await axios.post(base + '/api/runners', { ... }, axios_config);
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
});
});
describe('POST /api/runners failing', () => {
it('creating a runner without required params should return 400', async () => {
const res = await axios.post(base + '/api/runners', {}, axios_config);
expect(res.status).toEqual(400);
});
});
```
- Always set `validateStatus: undefined` in `axios_config` to prevent axios throwing on error responses
- Group tests by HTTP verb + route in `describe()` blocks; separate "working" and "failing" cases
- Use `jest.setTimeout(20000)` in `beforeAll` for slow integration tests
- Assert both `res.status` and `res.headers['content-type']` on success paths
---
## Environment Configuration
- Copy `.env.example` to `.env` and fill in values before running locally
- Database type is set via `DB_TYPE` env var (`sqlite`, `postgres`, or `mysql`)
- Server port is set via `INTERNAL_PORT` (accessed as `config.internal_port` in code)
- All config values are validated at startup in `src/config.ts`
- CI env is generated by `bun run test:ci:generate_env` (`scripts/create_testenv.ts`)
### NATS Configuration
The backend uses **NATS JetStream** as a KV cache for scan intake performance optimization.
- `NATS_URL` — connection URL for NATS server (default: `nats://localhost:4222`)
- `NATS_PREWARM` — if `true`, preloads all runner state into the KV cache at startup to eliminate DB reads from the first scan onward (default: `false`)
**KV buckets** (auto-created by `NatsClient` at startup):
- `station_state` — station token cache (1-hour TTL)
- `card_state` — card→runner mapping cache (1-hour TTL)
- `runner_state` — runner display name, total distance, latest scan timestamp (no TTL, CAS-based updates)
**Development**: NATS runs in Docker via `docker-compose.yml` (port 4222). The JetStream volume is persisted to `./nats-data/` to survive container restarts.
**Station intake hot path**: `POST /api/scans/trackscans` from scan stations uses a KV-first flow that eliminates DB reads on cache hits and prevents race conditions via compare-and-swap (CAS) updates. See `SCAN_NATS_PLAN.md` for full architecture details.

2100
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,23 @@
FROM node:alpine
# Typescript Build
FROM registry.odit.services/hub/oven/bun:1.3.9-alpine AS build
WORKDIR /app
COPY ./package.json ./
RUN yarn
COPY ./ ./
ENTRYPOINT [ "yarn","dev" ]
COPY package.json bun.lockb* ./
RUN bun install --frozen-lockfile
COPY tsconfig.json ormconfig.js bunfig.toml ./
COPY src ./src
RUN bun run build \
&& rm -rf /app/node_modules \
&& bun install --production --frozen-lockfile
# final image
FROM registry.odit.services/hub/oven/bun:1.3.9-alpine AS final
WORKDIR /app
COPY --from=build /app/package.json /app/package.json
COPY --from=build /app/bun.lockb* /app/
COPY --from=build /app/ormconfig.js /app/ormconfig.js
COPY --from=build /app/bunfig.toml /app/bunfig.toml
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
ENTRYPOINT ["bun", "/app/dist/app.js"]

589
PERFORMANCE_IDEAS.md Normal file
View File

@@ -0,0 +1,589 @@
# Performance Optimization Ideas for LfK Backend
This document outlines potential performance improvements for the LfK backend API, organized by impact and complexity.
---
## ✅ Already Implemented
### 1. Bun Runtime Migration
**Status**: Complete
**Impact**: 8-15% latency improvement
**Details**: Migrated from Node.js to Bun runtime, achieving:
- Parallel throughput: +8.3% (306 → 331 scans/sec)
- Parallel p50 latency: -9.5% (21ms → 19ms)
### 2. NATS KV Cache for Scan Intake
**Status**: Complete (based on code analysis)
**Impact**: Significant reduction in DB reads for hot path
**Details**: `ScanController.stationIntake()` uses NATS JetStream KV store to cache:
- Station tokens (1-hour TTL)
- Card→Runner mappings (1-hour TTL)
- Runner state (no TTL, CAS-based updates)
- Eliminates DB reads on cache hits
- Prevents race conditions via compare-and-swap (CAS)
---
## 🚀 High Impact, Low-Medium Complexity
### 3. Add Database Indexes
**Priority**: HIGH
**Complexity**: Low
**Estimated Impact**: 30-70% query time reduction
**Problem**: TypeORM synchronize() doesn't automatically create indexes on foreign keys or commonly queried fields.
**Observations**:
- Heavy use of `find()` with complex nested relations (e.g., `['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track']`)
- No explicit `@Index()` decorators found in entity files
- Frequent filtering by foreign keys (runner_id, track_id, station_id, card_id)
**Recommended Indexes**:
```typescript
// src/models/entities/Scan.ts
@Index(['runner', 'timestamp']) // For runner scan history queries
@Index(['station', 'timestamp']) // For station-based queries
@Index(['card']) // For card lookup
// src/models/entities/Runner.ts
@Index(['email']) // For authentication/lookup
@Index(['group']) // For group-based queries
// src/models/entities/RunnerCard.ts
@Index(['runner']) // For card→runner lookups
@Index(['code']) // For barcode scans
// src/models/entities/Donation.ts
@Index(['runner']) // For runner donations
@Index(['donor']) // For donor contributions
```
**Implementation Steps**:
1. Audit all entities and add `@Index()` decorators
2. Test query performance with `EXPLAIN` before/after
3. Monitor index usage with database tools
4. Consider composite indexes for frequently combined filters
**Expected Results**:
- 50-70% faster JOIN operations
- 30-50% faster foreign key lookups
- Reduced database CPU usage
---
### 4. Implement Query Result Caching
**Priority**: HIGH
**Complexity**: Medium
**Estimated Impact**: 50-90% latency reduction for repeated queries
**Problem**: Stats endpoints and frequently accessed data (org totals, team rankings, runner lists) are recalculated on every request.
**Observations**:
- `StatsController` methods load entire datasets with deep relations:
- `getRunnerStats()`: loads all runners with scans, groups, donations
- `getTeamStats()`: loads all teams with nested runner data
- `getOrgStats()`: loads all orgs with teams, runners, scans
- Many `find()` calls without any caching layer
- Data changes infrequently (only during scan intake)
**Solution Options**:
**Option A: NATS KV Cache (Recommended)**
```typescript
// src/nats/StatsKV.ts
export async function getOrgStatsCache(): Promise<ResponseOrgStats[] | null> {
const kv = await NatsClient.getKV('stats_cache', { ttl: 60 * 1000 }); // 60s TTL
const entry = await kv.get('org_stats');
return entry ? JSON.parse(entry.string()) : null;
}
export async function setOrgStatsCache(stats: ResponseOrgStats[]): Promise<void> {
const kv = await NatsClient.getKV('stats_cache', { ttl: 60 * 1000 });
await kv.put('org_stats', JSON.stringify(stats));
}
// Invalidate on scan creation
// src/controllers/ScanController.ts (after line 173)
await invalidateStatsCache(); // Clear stats on new scan
```
**Option B: In-Memory Cache with TTL**
```typescript
// src/cache/MemoryCache.ts
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 60 }); // 60s TTL
export function getCached<T>(key: string): T | undefined {
return cache.get<T>(key);
}
export function setCached<T>(key: string, value: T, ttl?: number): void {
cache.set(key, value, ttl);
}
export function invalidatePattern(pattern: string): void {
const keys = cache.keys().filter(k => k.includes(pattern));
cache.del(keys);
}
```
**Option C: Redis Cache** (if Redis is already in stack)
**Recommended Cache Strategy**:
- **TTL**: 30-60 seconds for stats endpoints
- **Invalidation**: On scan creation, runner updates, donation changes
- **Keys**: `stats:org`, `stats:team:${id}`, `stats:runner:${id}`
- **Warm on startup**: Pre-populate cache for critical endpoints
**Expected Results**:
- 80-90% latency reduction for stats endpoints (from ~500ms to ~50ms)
- 70-80% reduction in database load
- Improved user experience for dashboards and leaderboards
---
### 5. Lazy Load Relations & DTOs
**Priority**: HIGH
**Complexity**: Medium
**Estimated Impact**: 40-60% query time reduction
**Problem**: Many queries eagerly load deeply nested relations that aren't always needed.
**Observations**:
```typescript
// Current: Loads everything
scan = await this.scanRepository.findOne(
{ id: scan.id },
{ relations: ['runner', 'track', 'runner.scans', 'runner.group',
'runner.scans.track', 'card', 'station'] }
);
```
**Solutions**:
**A. Create Lightweight Response DTOs**
```typescript
// src/models/responses/ResponseScanLight.ts
export class ResponseScanLight {
@IsInt() id: number;
@IsInt() distance: number;
@IsInt() timestamp: number;
@IsBoolean() valid: boolean;
// Omit nested runner.scans, runner.group, etc.
}
// Use for list views
@Get()
@ResponseSchema(ResponseScanLight, { isArray: true })
async getAll() {
const scans = await this.scanRepository.find({
relations: ['runner', 'track'] // Minimal relations
});
return scans.map(s => new ResponseScanLight(s));
}
// Keep detailed DTO for single-item views
@Get('/:id')
@ResponseSchema(ResponseScan) // Full details
async getOne(@Param('id') id: number) { ... }
```
**B. Use Query Builder for Selective Loading**
```typescript
// Instead of loading all scans with runner relations:
const scans = await this.scanRepository
.createQueryBuilder('scan')
.leftJoinAndSelect('scan.runner', 'runner')
.leftJoinAndSelect('scan.track', 'track')
.select([
'scan.id', 'scan.distance', 'scan.timestamp', 'scan.valid',
'runner.id', 'runner.firstname', 'runner.lastname',
'track.id', 'track.name'
])
.where('scan.id = :id', { id })
.getOne();
```
**C. Implement GraphQL-style Field Selection**
```typescript
@Get()
async getAll(@QueryParam('fields') fields?: string) {
const relations = [];
if (fields?.includes('runner')) relations.push('runner');
if (fields?.includes('track')) relations.push('track');
return this.scanRepository.find({ relations });
}
```
**Expected Results**:
- 40-60% faster list queries
- 50-70% reduction in data transfer size
- Reduced JOIN complexity and memory usage
---
### 6. Pagination Optimization
**Priority**: MEDIUM
**Complexity**: Low
**Estimated Impact**: 20-40% improvement for large result sets
**Problem**: Current pagination uses `skip/take` which becomes slow with large offsets.
**Current Implementation**:
```typescript
// Inefficient for large page numbers (e.g., page=1000)
scans = await this.scanRepository.find({
skip: page * page_size, // Scans 100,000 rows to skip them
take: page_size
});
```
**Solutions**:
**A. Cursor-Based Pagination (Recommended)**
```typescript
@Get()
async getAll(
@QueryParam('cursor') cursor?: number, // Last ID from previous page
@QueryParam('page_size') page_size: number = 100
) {
const query = this.scanRepository.createQueryBuilder('scan')
.orderBy('scan.id', 'ASC')
.take(page_size + 1); // Get 1 extra to determine if more pages exist
if (cursor) {
query.where('scan.id > :cursor', { cursor });
}
const scans = await query.getMany();
const hasMore = scans.length > page_size;
const results = scans.slice(0, page_size);
const nextCursor = hasMore ? results[results.length - 1].id : null;
return {
data: results.map(s => s.toResponse()),
pagination: { nextCursor, hasMore }
};
}
```
**B. Add Total Count Caching**
```typescript
// Cache total counts to avoid expensive COUNT(*) queries
const totalCache = new Map<string, { count: number, expires: number }>();
async function getTotalCount(repo: Repository<any>): Promise<number> {
const cacheKey = repo.metadata.tableName;
const cached = totalCache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return cached.count;
}
const count = await repo.count();
totalCache.set(cacheKey, { count, expires: Date.now() + 60000 }); // 60s TTL
return count;
}
```
**Expected Results**:
- 60-80% faster pagination for large page numbers
- Consistent query performance regardless of offset
- Better mobile app experience with cursor-based loading
---
## 🔧 Medium Impact, Medium Complexity
### 7. Database Connection Pooling Optimization
**Priority**: MEDIUM
**Complexity**: Medium
**Estimated Impact**: 10-20% improvement under load
**Current**: Default TypeORM connection pooling (likely 10 connections)
**Recommendations**:
```typescript
// ormconfig.js
module.exports = {
// ... existing config
extra: {
// PostgreSQL specific
max: 20, // Max pool size (adjust based on load)
min: 5, // Min pool size
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 2000,
// MySQL specific
connectionLimit: 20,
waitForConnections: true,
queueLimit: 0
},
// Enable query logging in dev to identify slow queries
logging: process.env.NODE_ENV !== 'production' ? ['query', 'error'] : ['error'],
maxQueryExecutionTime: 1000, // Log queries taking >1s
};
```
**Monitor**:
- Connection pool exhaustion
- Query execution times
- Active connection count
---
### 8. Bulk Operations for Import
**Priority**: MEDIUM
**Complexity**: Medium
**Estimated Impact**: 50-80% faster imports
**Problem**: Import endpoints likely save entities one-by-one in loops.
**Solution**:
```typescript
// Instead of:
for (const runnerData of importData) {
const runner = await createRunner.toEntity();
await this.runnerRepository.save(runner); // N queries
}
// Use bulk insert:
const runners = await Promise.all(
importData.map(data => createRunner.toEntity())
);
await this.runnerRepository.save(runners); // 1 query
// Or use raw query for massive imports:
await getConnection()
.createQueryBuilder()
.insert()
.into(Runner)
.values(runners)
.execute();
```
---
### 9. Response Compression
**Priority**: MEDIUM
**Complexity**: Low
**Estimated Impact**: 60-80% reduction in response size
**Implementation**:
```typescript
// src/app.ts
import compression from 'compression';
const app = createExpressServer({ ... });
app.use(compression({
level: 6, // Compression level (1-9)
threshold: 1024, // Only compress responses >1KB
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
}
}));
```
**Benefits**:
- 70-80% smaller JSON responses
- Faster transfer times on slow networks
- Reduced bandwidth costs
**Dependencies**: `bun add compression @types/compression`
---
## 🎯 Lower Priority / High Complexity
### 10. Implement Read Replicas
**Priority**: LOW (requires infrastructure)
**Complexity**: High
**Estimated Impact**: 30-50% read query improvement
**When to Consider**:
- Database CPU consistently >70%
- Read-heavy workload (already true for stats endpoints)
- Running PostgreSQL/MySQL in production
**Implementation**:
```typescript
// ormconfig.js
module.exports = {
type: 'postgres',
replication: {
master: {
host: process.env.DB_WRITE_HOST,
port: 5432,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
},
slaves: [
{
host: process.env.DB_READ_REPLICA_1,
port: 5432,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
}
]
}
};
```
---
### 11. Move to Serverless/Edge Functions
**Priority**: LOW (architectural change)
**Complexity**: Very High
**Estimated Impact**: Variable (depends on workload)
**Considerations**:
- Good for: Infrequent workloads, global distribution
- Bad for: High-frequency scan intake (cold starts)
- May conflict with TypeORM's connection model
---
### 12. GraphQL API Layer
**Priority**: LOW (major refactor)
**Complexity**: Very High
**Estimated Impact**: 30-50% for complex queries
**Benefits**:
- Clients request only needed fields
- Single request for complex nested data
- Better mobile app performance
**Trade-offs**:
- Complete rewrite of controller layer
- Learning curve for frontend teams
- More complex caching strategy
---
## 📊 Recommended Implementation Order
**Phase 1: Quick Wins** (1-2 weeks)
1. Add database indexes → Controllers still work, immediate improvement
2. Enable response compression → One-line change in `app.ts`
3. Implement cursor-based pagination → Better mobile UX
**Phase 2: Caching Layer** (2-3 weeks)
4. Add NATS KV cache for stats endpoints
5. Create lightweight response DTOs for list views
6. Cache total counts for pagination
**Phase 3: Query Optimization** (2-3 weeks)
7. Refactor controllers to use query builder with selective loading
8. Optimize database connection pooling
9. Implement bulk operations for imports
**Phase 4: Infrastructure** (ongoing)
10. Monitor query performance and add more indexes as needed
11. Consider read replicas when database becomes bottleneck
---
## 🔍 Performance Monitoring Recommendations
### Add Metrics Endpoint
```typescript
// src/controllers/MetricsController.ts
import { performance } from 'perf_hooks';
const requestMetrics = {
totalRequests: 0,
avgLatency: 0,
p95Latency: 0,
dbQueryCount: 0,
cacheHitRate: 0,
};
@JsonController('/metrics')
export class MetricsController {
@Get()
@Authorized('ADMIN') // Restrict to admins
async getMetrics() {
return requestMetrics;
}
}
```
### Enable Query Logging
```typescript
// ormconfig.js
logging: ['query', 'error'],
maxQueryExecutionTime: 1000, // Warn on queries >1s
```
### Add Request Timing Middleware
```typescript
// src/middlewares/TimingMiddleware.ts
export function timingMiddleware(req: Request, res: Response, next: NextFunction) {
const start = performance.now();
res.on('finish', () => {
const duration = performance.now() - start;
if (duration > 1000) {
consola.warn(`Slow request: ${req.method} ${req.path} took ${duration}ms`);
}
});
next();
}
```
---
## 📝 Performance Testing Commands
```bash
# Run baseline benchmark
bun run benchmark > baseline.txt
# After implementing changes, compare
bun run benchmark > optimized.txt
diff baseline.txt optimized.txt
# Load testing with artillery (if added)
artillery quick --count 100 --num 10 http://localhost:4010/api/runners
# Database query profiling (PostgreSQL)
EXPLAIN ANALYZE SELECT * FROM scan WHERE runner_id = 1;
# Check database indexes
SELECT * FROM pg_indexes WHERE tablename = 'scan';
# Monitor NATS cache hit rate
# (Add custom logging in NATS KV functions)
```
---
## 🎓 Key Principles
1. **Measure first**: Always benchmark before and after changes
2. **Start with indexes**: Biggest impact, lowest risk
3. **Cache strategically**: Stats endpoints benefit most
4. **Lazy load by default**: Only eager load when absolutely needed
5. **Monitor in production**: Use APM tools (New Relic, DataDog, etc.)
---
## 📚 Additional Resources
- [TypeORM Performance Tips](https://typeorm.io/performance)
- [PostgreSQL Index Best Practices](https://www.postgresql.org/docs/current/indexes.html)
- [Bun Performance Benchmarks](https://bun.sh/docs/runtime/performance)
- [NATS JetStream KV Guide](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
---
**Last Updated**: 2026-02-20
**Status**: Ready for review and prioritization

170
README.md
View File

@@ -2,60 +2,140 @@
Backend Server
## Dev Setup 🛠
## Prerequisites
### Local w/ sqlite
1. Create a .env file in the project root containing:
```
APP_PORT=4010
DB_TYPE=sqlite
DB_HOST=bla
DB_PORT=bla
DB_USER=bla
DB_PASSWORD=bla
DB_NAME=./test.sqlite
```
2. Install Dependencies
```bash
yarn
```
3. Start the server
```bash
yarn dev
```
### Generate Docs
```
yarn docs
```
### Docker w/ postgres 🐳
This project uses **Bun** as the runtime and package manager. Install Bun first:
```bash
docker-compose up --build
# macOS/Linux
curl -fsSL https://bun.sh/install | bash
# Windows
powershell -c "irm bun.sh/install.ps1 | iex"
```
Or visit [bun.sh](https://bun.sh) for other installation methods.
## Quickstart 🐳
> Use this to run the backend with a PostgreSQL db in Docker
1. Clone the repo or copy the docker-compose
2. Run in the folder that contains the docker-compose file: `docker-compose up -d`
3. Visit http://127.0.0.1:4010/api/docs to check if the server is running
4. You can now use the default admin user (`demo:demo`)
## Dev Setup 🛠
> Local dev setup utilizing SQLite3 as the database and NATS for caching.
1. Rename the `.env.example` file to `.env` (you can adjust app port and other settings if needed)
2. Start NATS (required for KV cache):
```bash
docker-compose up -d nats
```
3. Install dependencies:
```bash
bun install
```
4. Start the server:
```bash
bun run dev
```
**Note**: Bun cannot run TypeScript source files directly due to circular TypeORM dependencies. The `dev` script automatically builds and runs the compiled output. For hot-reload during development, you may need to rebuild manually after code changes.
### Run Tests
```bash
# Run tests once (server has to be running)
bun test
# Run test in watch mode (reruns on change)
bun run test:watch
# Run test in CI mode (automatically starts the dev server)
bun run test:ci
```
### Run Benchmarks
```bash
# Start the server first
bun run dev
# In another terminal:
bun run benchmark
```
### Generate Docs
```bash
bun run docs
```
### Other Commands
```bash
# Build for production
bun run build
# Start production server
bun start
# Seed database with test data
bun run seed
# Export OpenAPI spec
bun run openapi:export
# Generate license report
bun run licenses:export
# Generate changelog
bun run changelog:export
```
## ENV Vars
> You can provide them via .env file or docker env vars.
> You can use the `test:ci:generate_env` package script to generate an example env (uses placeholder data for test server and ignores the errors).
| Name | Type | Default | Description |
| ------------------------- | ------------------ | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| APP_PORT | Number | 4010 | The port the backend server listens on. Is optional. |
| DB_TYPE | String | N/A | The type of the db you want to use. Supported by TypeORM. Possible: `sqlite`, `mysql`, `postgresql` |
| DB_HOST | String | N/A | The db's host IP address/FQDN or file path for sqlite |
| DB_PORT | String | N/A | The db's port |
| DB_USER | String | N/A | The user for accessing the db |
| DB_PASSWORD | String | N/A | The user's password for accessing the db |
| DB_NAME | String | N/A | The db's name |
| NODE_ENV | String | dev | The app's env - influences debug info. When set to "test", mailing errors get ignored. |
| POSTALCODE_COUNTRYCODE | String/CountryCode | N/A | The country code used to validate address postal codes |
| PHONE_COUNTRYCODE | String/CountryCode | null (international) | The country code used to validate phone numbers |
| SEED_TEST_DATA | Boolean | false | If you want the app to seed example data, set this to true |
| STATION_TOKEN_SECRET | String | N/A | Secret key for HMAC-SHA256 station token generation (min 32 chars). **Required.** |
| NATS_URL | String(URL) | nats://localhost:4222 | NATS server connection URL for KV cache |
| NATS_PREWARM | Boolean | false | Preload all runner state into NATS cache at startup (eliminates DB reads on first scan) |
| MAILER_URL | String(URL) | N/A | The mailer's base URL (no trailing slash) |
| MAILER_KEY | String | N/A | The mailer's API key |
| SELFSERVICE_URL | String(URL) | N/A | The link to selfservice (no trailing slash) |
| IMPRINT_URL | String(URL) | /imprint | The link to an imprint page for the system (defaults to the frontend's imprint) |
| PRIVACY_URL | String(URL) | /privacy | The link to a privacy page for the system (defaults to the frontend's privacy page) |
## Recommended Editor
[Visual Studio Code](https://code.visualstudio.com/)
### Recommended Extensions
- will be automatically recommended via ./vscode/extensions.json
* will be automatically recommended via ./vscode/extensions.json
## Branches
- main: Protected "release" branch
- dev: Current dev branch for merging the different features - only push for merges or minor changes!
- feature/xyz: Feature branches - `feature/issueid-title`
- bugfix/xyz: Branches for bugfixes - `bugfix/issueid-title` (no id for readme changes needed)
## File Structure
- src/models/\* - database models (typeorm entities)
- src/controllers/\* - routing-controllers
- src/loaders/\* - loaders for the different init steps of the api server
- src/routes/\* - express routes for everything we don't do via routing-controllers (shouldn't be much)
- src/middlewares/\* - express middlewares (mainly auth r/n)
- src/errors/* - our custom (http) errors
## Staging
### Branches & Tags
* vX.Y.Z: Release tags created from the main branch
* The version numbers follow the semver standard
* A new release tag automaticly triggers the release ci pipeline
* main: Protected "release" branch
* The latest tag of the docker image get's build from this
* dev: Current dev branch for merging the different feature branches and bugfixes
* New releases get created as tags from this
* The dev tag of the docker image get's build from this
* Only push minor changes to this branch!
* To merge a feature branch into this please create a pull request
* feature/xyz: Feature branches - naming scheme: `feature/issueid-title`
* bugfix/xyz: Branches for bugfixes - naming scheme:`bugfix/issueid-title`

2928
bun.lock Normal file

File diff suppressed because it is too large Load Diff

6
bunfig.toml Normal file
View File

@@ -0,0 +1,6 @@
# Bun configuration
# See: https://bun.sh/docs/runtime/bunfig
[runtime]
# Enable Node.js compatibility mode
bun = true

View File

@@ -1,23 +1,46 @@
version: "3"
services:
backend_server:
build: .
nats:
image: mirror.gcr.io/library/nats:alpine
command: ["--jetstream", "--store_dir", "/data"]
ports:
- 4010:4010
environment:
APP_PORT: 4010
DB_TYPE: postgres
DB_HOST: backend_db
DB_PORT: 5432
DB_USER: lfk
DB_PASSWORD: changeme
DB_NAME: lfk
NODE_ENV: production
backend_db:
image: postgres:11-alpine
environment:
POSTGRES_DB: lfk
POSTGRES_PASSWORD: changeme
POSTGRES_USER: lfk
ports:
- 5432:5432
- "4222:4222"
- "8222:8222"
volumes:
- nats_data:/data
# backend_server:
# build: .
# ports:
# - 4010:4010
# environment:
# APP_PORT: 4010
# DB_TYPE: sqlite
# DB_HOST: bla
# DB_PORT: bla
# DB_USER: bla
# DB_PASSWORD: bla
# DB_NAME: ./db.sqlite
# NODE_ENV: production
# POSTALCODE_COUNTRYCODE: DE
# SEED_TEST_DATA: "true"
# MAILER_URL: https://dev.lauf-fuer-kaya.de/mailer
# MAILER_KEY: asdasd
# APP_PORT: 4010
# DB_TYPE: postgres
# DB_HOST: backend_db
# DB_PORT: 5432
# DB_USER: lfk
# DB_PASSWORD: changeme
# DB_NAME: lfk
# NODE_ENV: production
# backend_db:
# image: postgres:11-alpine
# environment:
# POSTGRES_DB: lfk
# POSTGRES_PASSWORD: changeme
# POSTGRES_USER: lfk
# ports:
# - 5432:5432
volumes:
nats_data:

4
jest.config.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

1605
licenses.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,11 @@
import { config } from 'dotenv-safe';
config();
export default {
module.exports = {
type: process.env.DB_TYPE,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: ["src/models/entities/*.ts"]
// Run directly from TypeScript source (Bun workflow)
entities: ["src/models/entities/**/*.ts"],
seeds: ["src/seeds/**/*.ts"]
};

View File

@@ -1,6 +1,6 @@
{
"name": "@lfk/backend",
"version": "1.0.0",
"name": "@odit/lfk-backend",
"version": "1.8.1",
"main": "src/app.ts",
"repository": "https://git.odit.services/lfk/backend",
"author": {
@@ -22,46 +22,84 @@
],
"license": "CC-BY-NC-SA-4.0",
"dependencies": {
"argon2": "^0.27.0",
"body-parser": "^1.19.0",
"class-transformer": "^0.3.1",
"class-validator": "^0.12.2",
"class-validator-jsonschema": "^2.0.3",
"consola": "^2.15.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"helmet": "^4.2.0",
"jsonwebtoken": "^8.5.1",
"multer": "^1.4.2",
"mysql": "^2.18.1",
"pg": "^8.5.1",
"reflect-metadata": "^0.1.13",
"routing-controllers": "^0.9.0-alpha.6",
"routing-controllers-openapi": "^2.1.0",
"swagger-ui-express": "^4.1.5",
"typeorm": "^0.2.29",
"typeorm-routing-controllers-extensions": "^0.2.0",
"uuid": "^8.3.1"
"@odit/class-validator-jsonschema": "2.1.1",
"axios": "0.21.1",
"body-parser": "1.19.0",
"check-password-strength": "2.0.2",
"class-transformer": "0.3.1",
"class-validator": "0.13.0",
"consola": "2.15.0",
"cookie": "0.4.1",
"cookie-parser": "1.4.5",
"cors": "2.8.5",
"csvtojson": "2.0.10",
"express": "4.17.1",
"jsonwebtoken": "8.5.1",
"libphonenumber-js": "1.9.9",
"mysql": "2.18.1",
"nats": "^2.29.3",
"pg": "8.5.1",
"reflect-metadata": "0.1.13",
"routing-controllers": "0.9.0-alpha.6",
"routing-controllers-openapi": "2.2.0",
"sqlite3": "5.1.7",
"typeorm": "0.2.30",
"typeorm-routing-controllers-extensions": "0.2.0",
"typeorm-seeding": "1.6.1",
"validator": "13.5.2"
},
"devDependencies": {
"@types/cors": "^2.8.8",
"@types/dotenv-safe": "^8.1.1",
"@types/express": "^4.17.9",
"@types/jsonwebtoken": "^8.5.0",
"@types/multer": "^1.4.4",
"@types/node": "^14.14.9",
"@types/swagger-ui-express": "^4.1.2",
"@types/uuid": "^8.3.0",
"dotenv-safe": "^8.2.0",
"nodemon": "^2.0.6",
"sqlite3": "^5.0.0",
"ts-node": "^9.0.0",
"typedoc": "^0.19.2",
"typescript": "^4.1.2"
"@faker-js/faker": "7.6.0",
"@odit/license-exporter": "0.0.9",
"@types/cors": "2.8.19",
"@types/csvtojson": "1.1.5",
"@types/express": "5.0.6",
"@types/jest": "30.0.0",
"@types/jsonwebtoken": "9.0.10",
"@types/node": "25.3.0",
"auto-changelog": "2.4.0",
"cp-cli": "2.0.0",
"jest": "26.6.3",
"release-it": "14.2.2",
"rimraf": "^6.1.3",
"start-server-and-test": "1.11.7",
"ts-jest": "26.5.0",
"typedoc": "0.20.19",
"typescript": "5.9.3"
},
"scripts": {
"dev": "nodemon src/app.ts",
"build": "tsc",
"docs": "typedoc --out docs src"
"dev": "bun --watch src/app.ts",
"start": "bun src/app.ts",
"build": "rimraf ./dist && tsc && cp-cli ./src/static ./dist/static",
"docs": "typedoc --out docs src",
"test": "jest",
"test:watch": "jest --watchAll",
"test:ci:generate_env": "bun scripts/create_testenv.ts",
"test:ci:run": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test",
"test:ci": "bun run test:ci:generate_env && bun run test:ci:run",
"benchmark": "bun scripts/benchmark_scan_intake.ts",
"seed": "bun ./node_modules/typeorm/cli.js schema:sync && bun ./node_modules/typeorm-seeding/dist/cli.js seed",
"openapi:export": "bun scripts/openapi_export.ts",
"licenses:export": "license-exporter --markdown",
"changelog:export": "auto-changelog --commit-limit false -p -u --hide-credit",
"release": "release-it --only-version"
},
"release-it": {
"git": {
"commit": true,
"requireCleanWorkingDir": false,
"commitMessage": "chore(release): ${version}",
"requireBranch": "dev",
"push": true,
"tag": true,
"tagName": "${version}",
"tagAnnotation": "${version}"
},
"npm": {
"publish": false
},
"hooks": {
"after:bump": "bun run changelog:export && bun run licenses:export && git add CHANGELOG.md && git add licenses.md"
}
}
}
}

9427
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- sqlite3

View File

@@ -0,0 +1,367 @@
/**
* Scan Intake Benchmark Script
*
* Measures TrackScan creation performance before and after each optimisation phase.
* Run against a live dev server: bun run dev
*
* Usage:
* bun run benchmark
* bun scripts/benchmark_scan_intake.ts --base http://localhost:4010
*
* What it measures:
* 1. Single sequential scans — baseline latency per request (p50/p95/p99/max)
* 2. Parallel scans (10 stations) — simulates 10 concurrent stations each submitting
* one scan at a time at the expected event rate
* (~1 scan/3s per station = ~3.3 scans/s total)
*
* The script self-provisions all required data (org, runners, cards, track, stations)
* and cleans up after itself. It authenticates via the station token, matching the
* real production auth path exactly.
*
* Output is printed to stdout in a copy-paste-friendly table format so results can
* be compared across phases.
*/
import axios, { AxiosInstance } from 'axios';
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const BASE = (() => {
const idx = process.argv.indexOf('--base');
return idx !== -1 ? process.argv[idx + 1] : 'http://localhost:4010';
})();
const API = `${BASE}/api`;
// Number of simulated scan stations
const STATION_COUNT = 10;
// Sequential benchmark: total number of scans to send, one at a time
const SEQUENTIAL_SCAN_COUNT = 50;
// Parallel benchmark: number of rounds. Each round fires STATION_COUNT scans concurrently.
// 20 rounds × 10 stations = 200 total scans, matching the expected event throughput pattern.
const PARALLEL_ROUNDS = 20;
// Minimum lap time on the test track (seconds). Set low so most scans are valid.
// The benchmark measures submission speed, not business logic.
const TRACK_MINIMUM_LAP_TIME = 1;
// Track distance (metres)
const TRACK_DISTANCE = 400;
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface StationHandle {
id: number;
key: string; // cleartext token, used as Bearer token
cardCode: number; // EAN-13 barcode of the card assigned to this station's runner
axiosInstance: AxiosInstance;
}
interface Percentiles {
p50: number;
p95: number;
p99: number;
max: number;
min: number;
mean: number;
}
interface BenchmarkResult {
label: string;
totalScans: number;
totalTimeMs: number;
scansPerSecond: number;
latencies: Percentiles;
errors: number;
}
// ---------------------------------------------------------------------------
// HTTP helpers
// ---------------------------------------------------------------------------
const adminClient = axios.create({
baseURL: API,
validateStatus: () => true,
});
async function adminLogin(): Promise<string> {
const res = await adminClient.post('/auth/login', { username: 'demo', password: 'demo' });
if (res.status !== 200) {
throw new Error(`Login failed: ${res.status} ${JSON.stringify(res.data)}`);
}
return res.data.access_token;
}
function authedClient(token: string): AxiosInstance {
return axios.create({
baseURL: API,
validateStatus: () => true,
headers: { authorization: `Bearer ${token}` },
});
}
// ---------------------------------------------------------------------------
// Data provisioning
// ---------------------------------------------------------------------------
async function provision(adminToken: string): Promise<{
stations: StationHandle[];
trackId: number;
orgId: number;
cleanup: () => Promise<void>;
}> {
const client = authedClient(adminToken);
const createdIds: { type: string; id: number }[] = [];
const create = async (path: string, body: object): Promise<any> => {
const res = await client.post(path, body);
if (res.status !== 200) {
throw new Error(`POST ${path} failed: ${res.status} ${JSON.stringify(res.data)}`);
}
return res.data;
};
process.stdout.write('Provisioning test data... ');
// Organisation
const org = await create('/organizations', { name: 'benchmark-org' });
createdIds.push({ type: 'organizations', id: org.id });
// Track with a low minimumLapTime so re-scans within the benchmark are mostly valid
const track = await create('/tracks', {
name: 'benchmark-track',
distance: TRACK_DISTANCE,
minimumLapTime: TRACK_MINIMUM_LAP_TIME,
});
createdIds.push({ type: 'tracks', id: track.id });
// One runner + card + station per simulated scan station
const stations: StationHandle[] = [];
for (let i = 0; i < STATION_COUNT; i++) {
const runner = await create('/runners', {
firstname: `Bench`,
lastname: `Runner${i}`,
group: org.id,
});
createdIds.push({ type: 'runners', id: runner.id });
const card = await create('/cards', { runner: runner.id });
createdIds.push({ type: 'cards', id: card.id });
const station = await create('/stations', {
track: track.id,
description: `bench-station-${i}`,
});
createdIds.push({ type: 'stations', id: station.id });
stations.push({
id: station.id,
key: station.key,
cardCode: card.id, // the test spec uses card.id directly as the barcode value
axiosInstance: axios.create({
baseURL: API,
validateStatus: () => true,
headers: { authorization: `Bearer ${station.key}` },
}),
});
}
console.log(`done. (${STATION_COUNT} stations, ${STATION_COUNT} runners, ${STATION_COUNT} cards)`);
const cleanup = async () => {
process.stdout.write('Cleaning up test data... ');
// Delete in reverse-dependency order
for (const item of [...createdIds].reverse()) {
await client.delete(`/${item.type}/${item.id}?force=true`);
}
console.log('done.');
};
return { stations, trackId: track.id, orgId: org.id, cleanup };
}
// ---------------------------------------------------------------------------
// Single scan submission (returns latency in ms)
// ---------------------------------------------------------------------------
async function submitScan(station: StationHandle): Promise<{ latencyMs: number; ok: boolean }> {
const start = performance.now();
const res = await station.axiosInstance.post('/scans/trackscans', {
card: station.cardCode,
station: station.id,
});
const latencyMs = performance.now() - start;
const ok = res.status === 200;
return { latencyMs, ok };
}
// ---------------------------------------------------------------------------
// Statistics
// ---------------------------------------------------------------------------
function percentiles(latencies: number[]): Percentiles {
const sorted = [...latencies].sort((a, b) => a - b);
const at = (pct: number) => sorted[Math.floor((pct / 100) * sorted.length)] ?? sorted[sorted.length - 1];
const mean = sorted.reduce((s, v) => s + v, 0) / sorted.length;
return {
p50: Math.round(at(50)),
p95: Math.round(at(95)),
p99: Math.round(at(99)),
max: Math.round(sorted[sorted.length - 1]),
min: Math.round(sorted[0]),
mean: Math.round(mean),
};
}
// ---------------------------------------------------------------------------
// Benchmark 1 — Sequential (single station, one scan at a time)
// ---------------------------------------------------------------------------
async function benchmarkSequential(station: StationHandle): Promise<BenchmarkResult> {
const latencies: number[] = [];
let errors = 0;
process.stdout.write(` Running ${SEQUENTIAL_SCAN_COUNT} sequential scans`);
const wallStart = performance.now();
for (let i = 0; i < SEQUENTIAL_SCAN_COUNT; i++) {
const { latencyMs, ok } = await submitScan(station);
latencies.push(latencyMs);
if (!ok) errors++;
if ((i + 1) % 10 === 0) process.stdout.write('.');
}
const totalTimeMs = performance.now() - wallStart;
console.log(' done.');
return {
label: 'Sequential (1 station)',
totalScans: SEQUENTIAL_SCAN_COUNT,
totalTimeMs,
scansPerSecond: (SEQUENTIAL_SCAN_COUNT / totalTimeMs) * 1000,
latencies: percentiles(latencies),
errors,
};
}
// ---------------------------------------------------------------------------
// Benchmark 2 — Parallel (10 stations, concurrent rounds)
//
// Models the real event scenario: every ~3 seconds each station submits one scan.
// We don't actually sleep between rounds — we fire each round as fast as the
// previous one completes, which gives us the worst-case sustained throughput
// (all stations submitting at maximum rate simultaneously).
// ---------------------------------------------------------------------------
async function benchmarkParallel(stations: StationHandle[]): Promise<BenchmarkResult> {
const latencies: number[] = [];
let errors = 0;
process.stdout.write(` Running ${PARALLEL_ROUNDS} rounds × ${STATION_COUNT} concurrent stations`);
const wallStart = performance.now();
for (let round = 0; round < PARALLEL_ROUNDS; round++) {
const results = await Promise.all(stations.map(s => submitScan(s)));
for (const { latencyMs, ok } of results) {
latencies.push(latencyMs);
if (!ok) errors++;
}
if ((round + 1) % 4 === 0) process.stdout.write('.');
}
const totalTimeMs = performance.now() - wallStart;
const totalScans = PARALLEL_ROUNDS * STATION_COUNT;
console.log(' done.');
return {
label: `Parallel (${STATION_COUNT} stations concurrent)`,
totalScans,
totalTimeMs,
scansPerSecond: (totalScans / totalTimeMs) * 1000,
latencies: percentiles(latencies),
errors,
};
}
// ---------------------------------------------------------------------------
// Output formatting
// ---------------------------------------------------------------------------
function printResult(result: BenchmarkResult) {
const { label, totalScans, totalTimeMs, scansPerSecond, latencies, errors } = result;
console.log(`\n ${label}`);
console.log(` ${'─'.repeat(52)}`);
console.log(` Total scans : ${totalScans}`);
console.log(` Total time : ${totalTimeMs.toFixed(0)} ms`);
console.log(` Throughput : ${scansPerSecond.toFixed(2)} scans/sec`);
console.log(` Latency min : ${latencies.min} ms`);
console.log(` Latency mean : ${latencies.mean} ms`);
console.log(` Latency p50 : ${latencies.p50} ms`);
console.log(` Latency p95 : ${latencies.p95} ms`);
console.log(` Latency p99 : ${latencies.p99} ms`);
console.log(` Latency max : ${latencies.max} ms`);
console.log(` Errors : ${errors}`);
}
function printSummary(results: BenchmarkResult[]) {
const now = new Date().toISOString();
console.log('\n');
console.log('═'.repeat(60));
console.log(` SCAN INTAKE BENCHMARK RESULTS — ${now}`);
console.log(` Server: ${BASE}`);
console.log('═'.repeat(60));
for (const r of results) {
printResult(r);
}
console.log('\n' + '═'.repeat(60));
console.log(' Copy the block above to compare across phases.');
console.log('═'.repeat(60) + '\n');
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
async function main() {
console.log(`\nScan Intake Benchmark — target: ${BASE}\n`);
let adminToken: string;
try {
adminToken = await adminLogin();
} catch (err) {
console.error(`Could not authenticate. Is the server running at ${BASE}?\n`, err.message);
process.exit(1);
}
const { stations, cleanup } = await provision(adminToken);
const results: BenchmarkResult[] = [];
try {
console.log('\nBenchmark 1 — Sequential');
results.push(await benchmarkSequential(stations[0]));
// Brief pause between benchmarks so the sequential scans don't skew
// the parallel benchmark's first-scan latency (minimumLapTime window)
await new Promise(r => setTimeout(r, (TRACK_MINIMUM_LAP_TIME + 1) * 1000));
console.log('\nBenchmark 2 — Parallel');
results.push(await benchmarkParallel(stations));
} finally {
await cleanup();
}
printSummary(results);
}
main().catch(err => {
console.error('Benchmark failed:', err);
process.exit(1);
});

24
scripts/create_testenv.ts Normal file
View File

@@ -0,0 +1,24 @@
import consola from "consola";
import fs from "fs";
const env = `
APP_PORT=4010
DB_TYPE=sqlite
DB_HOST=bla
DB_PORT=bla
DB_USER=bla
DB_PASSWORD=bla
DB_NAME=./test.sqlite
NODE_ENV=test
POSTALCODE_COUNTRYCODE=DE
SEED_TEST_DATA=true
MAILER_URL=https://dev.lauf-fuer-kaya.de/mailer
MAILER_KEY=asdasd`;
try {
fs.writeFileSync("./.env", env, { encoding: "utf-8" });
consola.success("Exported ci env to .env");
} catch (error) {
consola.error("Couldn't export the ci env");
}

34
scripts/openapi_export.ts Normal file
View File

@@ -0,0 +1,34 @@
import { validationMetadatasToSchemas } from '@odit/class-validator-jsonschema';
import consola from "consola";
import fs from "fs";
import "reflect-metadata";
import { createExpressServer, getMetadataArgsStorage } from "routing-controllers";
import { generateSpec } from '../src/apispec';
import { config } from '../src/config';
import authchecker from "../src/middlewares/authchecker";
import { ErrorHandler } from '../src/middlewares/ErrorHandler';
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
createExpressServer({
authorizationChecker: authchecker,
middlewares: [ErrorHandler],
development: config.development,
cors: true,
routePrefix: "/api",
controllers: [`${__dirname}/../src/controllers/*.${CONTROLLERS_FILE_EXTENSION}`],
});
const storage = getMetadataArgsStorage();
const schemas = validationMetadatasToSchemas({
refPointerPrefix: "#/components/schemas/",
});
//Spec creation based on the previously created schemas
const spec = generateSpec(storage, schemas);
try {
fs.writeFileSync("./openapi.json", JSON.stringify(spec), { encoding: "utf-8" });
consola.success("Exported openapi spec to openapi.json");
} catch (error) {
consola.error("Couldn't export the openapi spec");
}

51
src/apispec.ts Normal file
View File

@@ -0,0 +1,51 @@
import { MetadataArgsStorage } from 'routing-controllers';
import { routingControllersToSpec } from 'routing-controllers-openapi';
import { config } from './config';
/**
* This function generates a the openapi spec from route metadata and type schemas.
* @param storage MetadataArgsStorage object generated by routing-controllers.
* @param schemas MetadataArgsStorage object generated by class-validator-jsonschema.
*/
export function generateSpec(storage: MetadataArgsStorage, schemas) {
return routingControllersToSpec(
storage,
{
routePrefix: "/api"
},
{
components: {
schemas,
"securitySchemes": {
"AuthToken": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
description: "A JWT based access token. Use /api/auth/login or /api/auth/refresh to get one."
},
"RefreshTokenCookie": {
"type": "apiKey",
"in": "cookie",
"name": "lfk_backend__refresh_token",
description: "A cookie containing a JWT based refreh token. Attention: Doesn't work in swagger-ui. Use /api/auth/login or /api/auth/refresh to get one."
},
"StatsApiToken": {
"type": "http",
"scheme": "bearer",
description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients). Only valid for obtaining stats."
},
"StationApiToken": {
"type": "http",
"scheme": "bearer",
description: "Api token that can be obtained by creating a new scan station (post to /api/stations). Only valid for creating scans."
}
}
},
info: {
description: `The the backend API for the LfK! runner system. <br>[Imprint](${config.imprint_url}) & [Privacy](${config.privacy_url})`,
title: "LfK! Backend API",
version: config.version
},
}
);
}

View File

@@ -1,29 +1,79 @@
import consola from "consola";
import * as dotenvSafe from "dotenv-safe";
import "reflect-metadata";
import { createExpressServer } from "routing-controllers";
import authchecker from "./authchecker";
import { config, e as errors } from './config';
import loaders from "./loaders/index";
import authchecker from "./middlewares/authchecker";
import { ErrorHandler } from './middlewares/ErrorHandler';
import UserChecker from './middlewares/UserChecker';
dotenvSafe.config();
const PORT = process.env.APP_PORT || 4010;
// Import all controllers directly to avoid Bun + routing-controllers glob/require issues
import { AuthController } from './controllers/AuthController';
import { DonationController } from './controllers/DonationController';
import { DonorController } from './controllers/DonorController';
import { GroupContactController } from './controllers/GroupContactController';
import { ImportController } from './controllers/ImportController';
import { MeController } from './controllers/MeController';
import { PermissionController } from './controllers/PermissionController';
import { RunnerCardController } from './controllers/RunnerCardController';
import { RunnerController } from './controllers/RunnerController';
import { RunnerOrganizationController } from './controllers/RunnerOrganizationController';
import { RunnerSelfServiceController } from './controllers/RunnerSelfServiceController';
import { RunnerTeamController } from './controllers/RunnerTeamController';
import { ScanController } from './controllers/ScanController';
import { ScanStationController } from './controllers/ScanStationController';
import { StatsClientController } from './controllers/StatsClientController';
import { StatsController } from './controllers/StatsController';
import { StatusController } from './controllers/StatusController';
import { TrackController } from './controllers/TrackController';
import { UserController } from './controllers/UserController';
import { UserGroupController } from './controllers/UserGroupController';
const app = createExpressServer({
authorizationChecker: authchecker,
currentUserChecker: UserChecker,
middlewares: [ErrorHandler],
development: process.env.NODE_ENV === "production",
development: config.development,
cors: true,
routePrefix: "/api",
controllers: [__dirname + "/controllers/*.ts"],
controllers: [
AuthController,
DonationController,
DonorController,
GroupContactController,
ImportController,
MeController,
PermissionController,
RunnerCardController,
RunnerController,
RunnerOrganizationController,
RunnerSelfServiceController,
RunnerTeamController,
ScanController,
ScanStationController,
StatsClientController,
StatsController,
StatusController,
TrackController,
UserController,
UserGroupController,
],
});
async function main() {
await loaders(app);
app.listen(PORT, () => {
if (config.testing) {
consola.info("🛠[config]: Discovered testing env. Mailing errors will get ignored!")
}
app.listen(config.internal_port, () => {
consola.success(
`⚡️[server]: Server is running at http://localhost:${PORT}`
`⚡️[server]: Server is running at http://localhost:${config.internal_port}`
);
});
}
main();
if (errors === 0) {
main();
} else {
consola.error("error");
// something's wrong
}

View File

@@ -1,51 +0,0 @@
import * as jwt from "jsonwebtoken";
import { Action } from "routing-controllers";
import { getConnectionManager } from 'typeorm';
import { IllegalJWTError, NoPermissionError, UserNonexistantOrRefreshtokenInvalidError } from './errors/AuthError';
import { User } from './models/entities/User';
// -----------
const authchecker = async (action: Action, permissions: string | string[]) => {
let required_permissions = undefined
if (typeof permissions === "string") {
required_permissions = [permissions]
} else {
required_permissions = permissions
}
// const token = action.request.headers["authorization"];
const provided_token = action.request.query["auth"];
let jwtPayload = undefined
try {
jwtPayload = <any>jwt.verify(provided_token, "securekey");
} catch (error) {
console.log(error);
throw new IllegalJWTError()
}
const count = await getConnectionManager().get().getRepository(User).count({ id: jwtPayload["userdetails"]["id"], refreshTokenCount: jwtPayload["userdetails"]["refreshTokenCount"] })
if (count !== 1) {
throw new UserNonexistantOrRefreshtokenInvalidError()
}
if (jwtPayload.permissions) {
action.response.local = {}
action.response.local.jwtPayload = jwtPayload.permissions
required_permissions.forEach(r => {
const permission_key = r.split(":")[0]
const actual_accesslevel_for_permission = jwtPayload.permissions[permission_key]
const permission_access_level = r.split(":")[1]
if (actual_accesslevel_for_permission.includes(permission_access_level)) {
return true;
} else {
throw new NoPermissionError()
}
});
} else {
throw new NoPermissionError()
}
//
try {
jwt.verify(provided_token, process.env.JWT_SECRET || "secretjwtsecret");
return true
} catch (error) {
return false
}
}
export default authchecker

59
src/config.ts Normal file
View File

@@ -0,0 +1,59 @@
import consola from 'consola';
import { CountryCode } from 'libphonenumber-js';
import ValidatorJS from 'validator';
export const config = {
internal_port: parseInt(process.env.APP_PORT) || 4010,
development: process.env.NODE_ENV === "production",
testing: process.env.NODE_ENV === "test",
jwt_secret: process.env.JWT_SECRET || "secretjwtsecret",
station_token_secret: process.env.STATION_TOKEN_SECRET || "",
nats_url: process.env.NATS_URL || "nats://localhost:4222",
nats_prewarm: process.env.NATS_PREWARM === "true",
phone_validation_countrycode: getPhoneCodeLocale(),
postalcode_validation_countrycode: getPostalCodeLocale(),
version: process.env.VERSION || require('../package.json').version,
seedTestData: getDataSeeding(),
app_url: process.env.APP_URL || "http://localhost:8080",
privacy_url: process.env.PRIVACY_URL || "/privacy",
imprint_url: process.env.IMPRINT_URL || "/imprint",
mailer_url: process.env.MAILER_URL || "",
mailer_key: process.env.MAILER_KEY || ""
}
let errors = 0
if (typeof config.internal_port !== "number") {
consola.error("Error: APP_PORT is not a number")
errors++
}
if (typeof config.development !== "boolean") {
consola.error("Error: NODE_ENV is not a boolean")
errors++
}
if (config.mailer_url == "" || config.mailer_key == "") {
consola.error("Error: invalid mailer config")
errors++;
}
if (config.station_token_secret.length < 32) {
consola.error("Error: STATION_TOKEN_SECRET must be set and at least 32 characters long")
errors++;
}
function getPhoneCodeLocale(): CountryCode {
return (process.env.PHONE_COUNTRYCODE as CountryCode);
}
function getPostalCodeLocale(): any {
try {
const stringArray: String[] = ValidatorJS.isPostalCodeLocales;
let index = stringArray.indexOf(process.env.POSTALCODE_COUNTRYCODE);
return ValidatorJS.isPostalCodeLocales[index];
} catch (error) {
return null;
}
}
function getDataSeeding(): Boolean {
try {
return JSON.parse(process.env.SEED_TEST_DATA);
} catch (error) {
return false;
}
}
export let e = errors

View File

@@ -1,68 +1,106 @@
import { Body, JsonController, Post } 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 { HandleLogout } from '../models/actions/HandleLogout';
import { RefreshAuth } from '../models/actions/RefreshAuth';
import { Auth } from '../models/responses/ResponseAuth';
import { Logout } from '../models/responses/ResponseLogout';
@JsonController('/auth')
export class AuthController {
constructor() {
}
@Post("/login")
@ResponseSchema(Auth)
@ResponseSchema(InvalidCredentialsError)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@ResponseSchema(PasswordNeededError)
@ResponseSchema(InvalidCredentialsError)
@OpenAPI({ description: 'Create a new access token object' })
async login(@Body({ validate: true }) createAuth: CreateAuth) {
let auth;
try {
auth = await createAuth.toAuth();
} catch (error) {
return error;
}
return auth
}
@Post("/logout")
@ResponseSchema(Logout)
@ResponseSchema(InvalidCredentialsError)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@ResponseSchema(PasswordNeededError)
@ResponseSchema(InvalidCredentialsError)
@OpenAPI({ description: 'Create a new access token object' })
async logout(@Body({ validate: true }) handleLogout: HandleLogout) {
let logout;
try {
logout = await handleLogout.logout()
} catch (error) {
return error;
}
return logout
}
@Post("/refresh")
@ResponseSchema(Auth)
@ResponseSchema(JwtNotProvidedError)
@ResponseSchema(IllegalJWTError)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(RefreshTokenCountInvalidError)
@OpenAPI({ description: 'refresh a access token' })
async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth) {
let auth;
try {
auth = await refreshAuth.toAuth();
} catch (error) {
return error;
}
return auth
}
}
import { Body, CookieParam, JsonController, Param, Post, QueryParam, Req, Res } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError';
import { MailSendingError } from '../errors/MailErrors';
import { UserNotFoundError } from '../errors/UserErrors';
import { Mailer } from '../mailer';
import { CreateAuth } from '../models/actions/create/CreateAuth';
import { CreateResetToken } from '../models/actions/create/CreateResetToken';
import { HandleLogout } from '../models/actions/HandleLogout';
import { RefreshAuth } from '../models/actions/RefreshAuth';
import { ResetPassword } from '../models/actions/ResetPassword';
import { ResponseAuth } from '../models/responses/ResponseAuth';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { Logout } from '../models/responses/ResponseLogout';
@JsonController('/auth')
export class AuthController {
@Post("/login")
@ResponseSchema(ResponseAuth)
@ResponseSchema(InvalidCredentialsError)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@ResponseSchema(PasswordNeededError)
@ResponseSchema(InvalidCredentialsError)
@OpenAPI({ description: 'Login with your username/email and password. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)' })
async login(@Body({ validate: true }) createAuth: CreateAuth, @Res() response: any) {
let auth;
try {
auth = await createAuth.toAuth();
response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
return response.send(auth)
} catch (error) {
throw error;
}
}
@Post("/logout")
@ResponseSchema(Logout)
@ResponseSchema(InvalidCredentialsError)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@ResponseSchema(PasswordNeededError)
@ResponseSchema(InvalidCredentialsError)
@OpenAPI({ description: 'Logout using your refresh token. <br> This instantly invalidates all your access and refresh tokens.', security: [{ "RefreshTokenCookie": [] }] })
async logout(@Body({ validate: true }) handleLogout: HandleLogout, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any) {
if (refresh_token && refresh_token.length != 0 && handleLogout.token == undefined) {
handleLogout.token = refresh_token;
}
let logout;
try {
logout = await handleLogout.logout()
await response.cookie('lfk_backend__refresh_token', "expired", { expires: new Date(Date.now()), httpOnly: true });
response.cookie('lfk_backend__refresh_token_expires_at', "expired", { expires: new Date(Date.now()), httpOnly: true });
} catch (error) {
throw error;
}
return response.send(logout)
}
@Post("/refresh")
@ResponseSchema(ResponseAuth)
@ResponseSchema(JwtNotProvidedError)
@ResponseSchema(IllegalJWTError)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(RefreshTokenCountInvalidError)
@OpenAPI({ description: 'Refresh your access and refresh tokens using a valid refresh token. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)', security: [{ "RefreshTokenCookie": [] }] })
async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any, @Req() req: any) {
if (refresh_token && refresh_token.length != 0 && refreshAuth.token == undefined) {
refreshAuth.token = refresh_token;
}
let auth;
try {
auth = await refreshAuth.toAuth();
response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
} catch (error) {
throw error;
}
return response.send(auth)
}
@Post("/reset")
@ResponseSchema(ResponseEmpty, { statusCode: 200 })
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@ResponseSchema(UsernameOrEmailNeededError, { statusCode: 406 })
@ResponseSchema(MailSendingError, { statusCode: 500 })
@OpenAPI({ description: "Request a password reset token. <br> This will provide you with a reset token that you can use by posting to /api/auth/reset/{token}." })
async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken, @QueryParam("locale") locale: string = "en") {
const reset_token: string = await passwordReset.toResetToken();
await Mailer.sendResetMail(passwordReset.email, reset_token, locale);
return new ResponseEmpty();
}
@Post("/reset/:token")
@ResponseSchema(ResponseAuth)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@OpenAPI({ description: "Reset a user's utilising a valid password reset token. <br> This will set the user's password to the one you provided in the body. <br> To get a reset token post to /api/auth/reset with your username." })
async resetPassword(@Param("token") token: string, @Body({ validate: true }) passwordReset: ResetPassword) {
passwordReset.resetToken = token;
return await passwordReset.resetPassword();
}
}

View File

@@ -0,0 +1,167 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { DonationIdsNotMatchingError, DonationNotFoundError } from '../errors/DonationErrors';
import { DonorNotFoundError } from '../errors/DonorErrors';
import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { CreateAnonymousDonation } from '../models/actions/create/CreateAnonymousDonation';
import { CreateDistanceDonation } from '../models/actions/create/CreateDistanceDonation';
import { CreateFixedDonation } from '../models/actions/create/CreateFixedDonation';
import { UpdateDistanceDonation } from '../models/actions/update/UpdateDistanceDonation';
import { UpdateFixedDonation } from '../models/actions/update/UpdateFixedDonation';
import { DistanceDonation } from '../models/entities/DistanceDonation';
import { Donation } from '../models/entities/Donation';
import { FixedDonation } from '../models/entities/FixedDonation';
import { ResponseAnonymousDonation } from '../models/responses/ResponseAnonymousDonation';
import { ResponseDistanceDonation } from '../models/responses/ResponseDistanceDonation';
import { ResponseDonation } from '../models/responses/ResponseDonation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
@JsonController('/donations')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class DonationController {
private donationRepository: Repository<Donation>;
private distanceDonationRepository: Repository<DistanceDonation>;
private fixedDonationRepository: Repository<FixedDonation>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.donationRepository = getConnectionManager().get().getRepository(Donation);
this.distanceDonationRepository = getConnectionManager().get().getRepository(DistanceDonation);
this.fixedDonationRepository = getConnectionManager().get().getRepository(FixedDonation);
}
@Get()
@Authorized("DONATION:GET")
@ResponseSchema(ResponseDonation, { isArray: true })
@ResponseSchema(ResponseDistanceDonation, { isArray: true })
@ResponseSchema(ResponseAnonymousDonation, { isArray: true })
@OpenAPI({ description: 'Lists all donations (fixed or distance based) from all donors. <br> This includes the donations\'s runner\'s distance ran(if distance donation).' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseDonations: ResponseDonation[] = new Array<ResponseDonation>();
let donations: Array<Donation>;
if (page != undefined) {
donations = await this.donationRepository.find({ relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'], skip: page * page_size, take: page_size });
} else {
donations = await this.donationRepository.find({ relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] });
}
donations.forEach(donation => {
responseDonations.push(donation.toResponse());
});
return responseDonations;
}
@Get('/:id')
@Authorized("DONATION:GET")
@ResponseSchema(ResponseDonation)
@ResponseSchema(ResponseDistanceDonation)
@ResponseSchema(ResponseAnonymousDonation)
@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
@OnUndefined(DonationNotFoundError)
@OpenAPI({ description: 'Lists all information about the donation whose id got provided. This includes the donation\'s runner\'s distance ran (if distance donation).' })
async getOne(@Param('id') id: number) {
let donation = await this.donationRepository.findOne({ id: id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })
if (!donation) { throw new DonationNotFoundError(); }
return donation.toResponse();
}
@Post('/fixed')
@Authorized("DONATION:CREATE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a fixed donation (not distance donation - use /donations/distance instead). <br> Please rmemember to provide the donation\'s donors\'s id and amount.' })
async postFixed(@Body({ validate: true }) createDonation: CreateFixedDonation) {
let donation = await createDonation.toEntity();
donation = await this.fixedDonationRepository.save(donation);
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
}
@Post('/anonymous')
@Authorized("DONATION:CREATE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a anonymous donation' })
async postAnonymous(@Body({ validate: true }) createDonation: CreateAnonymousDonation) {
let donation = await createDonation.toEntity();
donation = await this.fixedDonationRepository.save(donation);
return (await this.donationRepository.findOne({ id: donation.id })).toResponse();
}
@Post('/distance')
@Authorized("DONATION:CREATE")
@ResponseSchema(ResponseDistanceDonation)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a distance donation (not fixed donation - use /donations/fixed instead). <br> Please rmemember to provide the donation\'s donors\'s and runner\s ids and amount per distance (kilometer).' })
async postDistance(@Body({ validate: true }) createDonation: CreateDistanceDonation) {
let donation = await createDonation.toEntity();
donation = await this.distanceDonationRepository.save(donation);
return (await this.distanceDonationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
}
@Put('/fixed/:id')
@Authorized("DONATION:UPDATE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the fixed donation (not distance donation - use /donations/distance instead) whose id you provided. <br> Please remember that ids can't be changed and amounts must be positive." })
async putFixed(@Param('id') id: number, @Body({ validate: true }) donation: UpdateFixedDonation) {
let oldDonation = await this.fixedDonationRepository.findOne({ id: id });
if (!oldDonation) {
throw new DonationNotFoundError();
}
if (oldDonation.id != donation.id) {
throw new DonationIdsNotMatchingError();
}
await this.fixedDonationRepository.save(await donation.update(oldDonation));
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
}
@Put('/distance/:id')
@Authorized("DONATION:UPDATE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the distance donation (not fixed donation - use /donations/fixed instead) whose id you provided. <br> Please remember that ids can't be changed and amountPerDistance must be positive." })
async putDistance(@Param('id') id: number, @Body({ validate: true }) donation: UpdateDistanceDonation) {
let oldDonation = await this.distanceDonationRepository.findOne({ id: id });
if (!oldDonation) {
throw new DonationNotFoundError();
}
if (oldDonation.id != donation.id) {
throw new DonationIdsNotMatchingError();
}
await this.distanceDonationRepository.save(await donation.update(oldDonation));
return (await this.distanceDonationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
}
@Delete('/:id')
@Authorized("DONATION:DELETE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(ResponseDistanceDonation)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the donation whose id you provided. <br> If no donation with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let donation = await this.donationRepository.findOne({ id: id });
if (!donation) { return null; }
const responseScan = await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] });
await this.donationRepository.delete(donation);
return responseScan.toResponse();
}
}

View File

@@ -0,0 +1,120 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { DonorHasDonationsError, DonorIdsNotMatchingError, DonorNotFoundError } from '../errors/DonorErrors';
import { CreateDonor } from '../models/actions/create/CreateDonor';
import { UpdateDonor } from '../models/actions/update/UpdateDonor';
import { Donor } from '../models/entities/Donor';
import { ResponseDonor } from '../models/responses/ResponseDonor';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { DonationController } from './DonationController';
@JsonController('/donors')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class DonorController {
private donorRepository: Repository<Donor>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.donorRepository = getConnectionManager().get().getRepository(Donor);
}
@Get()
@Authorized("DONOR:GET")
@ResponseSchema(ResponseDonor, { isArray: true })
@OpenAPI({ description: 'Lists all donor. <br> This includes the donor\'s current donation amount.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseDonors: ResponseDonor[] = new Array<ResponseDonor>();
let donors: Array<Donor>;
if (page != undefined) {
donors = await this.donorRepository.find({ relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'], skip: page * page_size, take: page_size });
} else {
donors = await this.donorRepository.find({ relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] });
}
donors.forEach(donor => {
responseDonors.push(new ResponseDonor(donor));
});
return responseDonors;
}
@Get('/:id')
@Authorized("DONOR:GET")
@ResponseSchema(ResponseDonor)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@OnUndefined(DonorNotFoundError)
@OpenAPI({ description: 'Lists all information about the donor whose id got provided. <br> This includes the donor\'s current donation amount.' })
async getOne(@Param('id') id: number) {
let donor = await this.donorRepository.findOne({ id: id }, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] })
if (!donor) { throw new DonorNotFoundError(); }
return new ResponseDonor(donor);
}
@Post()
@Authorized("DONOR:CREATE")
@ResponseSchema(ResponseDonor)
@OpenAPI({ description: 'Create a new donor.' })
async post(@Body({ validate: true }) createRunner: CreateDonor) {
let donor;
try {
donor = await createRunner.toEntity();
} catch (error) {
throw error;
}
donor = await this.donorRepository.save(donor)
return new ResponseDonor(await this.donorRepository.findOne(donor, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] }));
}
@Put('/:id')
@Authorized("DONOR:UPDATE")
@ResponseSchema(ResponseDonor)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@ResponseSchema(DonorIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the donor whose id you provided. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) donor: UpdateDonor) {
let oldDonor = await this.donorRepository.findOne({ id: id });
if (!oldDonor) {
throw new DonorNotFoundError();
}
if (oldDonor.id != donor.id) {
throw new DonorIdsNotMatchingError();
}
await this.donorRepository.save(await donor.update(oldDonor));
return new ResponseDonor(await this.donorRepository.findOne({ id: id }, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] }));
}
@Delete('/:id')
@Authorized("DONOR:DELETE")
@ResponseSchema(ResponseDonor)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the donor whose id you provided. <br> If no donor with this id exists it will just return 204(no content). <br> If the donor still has donations associated this will fail, please provide the query param ?force=true to delete the donor with all associated donations.' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let donor = await this.donorRepository.findOne({ id: id });
if (!donor) { return null; }
const responseDonor = await this.donorRepository.findOne(donor, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] });
if (!donor) {
throw new DonorNotFoundError();
}
const donorDonations = (await this.donorRepository.findOne({ id: donor.id }, { relations: ["donations"] })).donations;
if (donorDonations.length > 0 && !force) {
throw new DonorHasDonationsError();
}
const donationController = new DonationController();
for (let donation of donorDonations) {
await donationController.remove(donation.id, force);
}
await this.donorRepository.delete(donor);
return new ResponseDonor(responseDonor);
}
}

View File

@@ -0,0 +1,114 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnection, getConnectionManager } from 'typeorm';
import { GroupContactIdsNotMatchingError, GroupContactNotFoundError } from '../errors/GroupContactErrors';
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
import { CreateGroupContact } from '../models/actions/create/CreateGroupContact';
import { UpdateGroupContact } from '../models/actions/update/UpdateGroupContact';
import { GroupContact } from '../models/entities/GroupContact';
import { RunnerGroup } from '../models/entities/RunnerGroup';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseGroupContact } from '../models/responses/ResponseGroupContact';
@JsonController('/contacts')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class GroupContactController {
private contactRepository: Repository<GroupContact>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.contactRepository = getConnectionManager().get().getRepository(GroupContact);
}
@Get()
@Authorized("CONTACT:GET")
@ResponseSchema(ResponseGroupContact, { isArray: true })
@OpenAPI({ description: 'Lists all contacts. <br> This includes the contact\'s associated groups.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseContacts: ResponseGroupContact[] = new Array<ResponseGroupContact>();
let contacts: Array<GroupContact>;
if (page != undefined) {
contacts = await this.contactRepository.find({ relations: ['groups', 'groups.parentGroup'], skip: page * page_size, take: page_size });
} else {
contacts = await this.contactRepository.find({ relations: ['groups', 'groups.parentGroup'] });
}
contacts.forEach(contact => {
responseContacts.push(contact.toResponse());
});
return responseContacts;
}
@Get('/:id')
@Authorized("CONTACT:GET")
@ResponseSchema(ResponseGroupContact)
@ResponseSchema(GroupContactNotFoundError, { statusCode: 404 })
@OnUndefined(GroupContactNotFoundError)
@OpenAPI({ description: 'Lists all information about the contact whose id got provided. <br> This includes the contact\'s associated groups.' })
async getOne(@Param('id') id: number) {
let contact = await this.contactRepository.findOne({ id: id }, { relations: ['groups', 'groups.parentGroup'] })
if (!contact) { throw new GroupContactNotFoundError(); }
return contact.toResponse();
}
@Post()
@Authorized("CONTACT:CREATE")
@ResponseSchema(ResponseGroupContact)
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new contact.' })
async post(@Body({ validate: true }) createContact: CreateGroupContact) {
let contact;
try {
contact = await createContact.toEntity();
} catch (error) {
throw error;
}
contact = await this.contactRepository.save(contact)
return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups', 'groups.parentGroup'] })).toResponse();
}
@Put('/:id')
@Authorized("CONTACT:UPDATE")
@ResponseSchema(ResponseGroupContact)
@ResponseSchema(GroupContactNotFoundError, { statusCode: 404 })
@ResponseSchema(GroupContactIdsNotMatchingError, { statusCode: 406 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@OpenAPI({ description: "Update the contact whose id you provided. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) contact: UpdateGroupContact) {
let oldContact = await this.contactRepository.findOne({ id: id });
if (!oldContact) {
throw new GroupContactNotFoundError();
}
if (oldContact.id != contact.id) {
throw new GroupContactIdsNotMatchingError();
}
await this.contactRepository.save(await contact.update(oldContact));
return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups', 'groups.parentGroup'] })).toResponse();
}
@Delete('/:id')
@Authorized("CONTACT:DELETE")
@ResponseSchema(ResponseGroupContact)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the contact whose id you provided. <br> If no contact with this id exists it will just return 204(no content). <br> This won\'t delete any groups associated with the contact.' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let contact = await this.contactRepository.findOne({ id: id });
if (!contact) { return null; }
const responseContact = await this.contactRepository.findOne(contact, { relations: ['groups', 'groups.parentGroup'] });
for (let group of responseContact.groups) {
group.contact = null;
await getConnection().getRepository(RunnerGroup).save(group);
}
await this.contactRepository.delete(contact);
return responseContact.toResponse();
}
}

View File

@@ -0,0 +1,102 @@
import csv from 'csvtojson';
import { Authorized, Body, ContentType, Controller, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { RunnerGroupNeededError } from '../errors/RunnerErrors';
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
import RawBodyMiddleware from '../middlewares/RawBody';
import { ImportRunner } from '../models/actions/ImportRunner';
import { ResponseRunner } from '../models/responses/ResponseRunner';
import { RunnerController } from './RunnerController';
@Controller()
@Authorized(["RUNNER:IMPORT", "TEAM:IMPORT"])
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class ImportController {
private runnerController: RunnerController;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerController = new RunnerController();
}
@Post('/runners/import')
@ContentType("application/json")
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from json and insert them into the provided group. <br> If teams/classes are provided alongside the runner's name they'll automaticly be created under the provided org and the runners will be inserted into the teams instead." })
async postJSON(@Body({ validate: true, type: ImportRunner }) importRunners: ImportRunner[], @QueryParam("group") groupID: number) {
if (!groupID) { throw new RunnerGroupNeededError(); }
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
for await (let runner of importRunners) {
responseRunners.push(await this.runnerController.post(await runner.toCreateRunner(groupID)));
}
return responseRunners;
}
@Post('/organizations/:id/import')
@ContentType("application/json")
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from json and insert them into the provided org. <br> If teams/classes are provided alongside the runner's name they'll automaticly be created under the provided org and the runners will be inserted into the teams instead." })
async postOrgsJSON(@Body({ validate: true, type: ImportRunner }) importRunners: ImportRunner[], @Param('id') id: number) {
return await this.postJSON(importRunners, id)
}
@Post('/teams/:id/import')
@ContentType("application/json")
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from json and insert them into the provided team" })
async postTeamsJSON(@Body({ validate: true, type: ImportRunner }) importRunners: ImportRunner[], @Param('id') id: number) {
return await this.postJSON(importRunners, id)
}
@Post('/runners/import/csv')
@ContentType("application/json")
@UseBefore(RawBodyMiddleware)
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from csv and insert them into the provided group. <br> If teams/classes are provided alongside the runner's name they'll automaticly be created under the provided org and the runners will be inserted into the teams instead." })
async postCSV(@Req() request: any, @QueryParam("group") groupID: number) {
let csvParse = await csv({ delimiter: [",", ";"], trim: true }).fromString(request.rawBody.toString());
let importRunners: ImportRunner[] = new Array<ImportRunner>();
for await (let runner of csvParse) {
let newImportRunner = new ImportRunner();
newImportRunner.firstname = runner.firstname;
newImportRunner.middlename = runner.middlename;
newImportRunner.lastname = runner.lastname;
if (runner.class === undefined) { newImportRunner.team = runner.team; }
else { newImportRunner.class = runner.class; }
importRunners.push(newImportRunner);
}
return await this.postJSON(importRunners, groupID);
}
@Post('/organizations/:id/import/csv')
@ContentType("application/json")
@UseBefore(RawBodyMiddleware)
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from csv and insert them into the provided org. <br> If teams/classes are provided alongside the runner's name they'll automaticly be created under the provided org and the runners will be inserted into the teams instead." })
async postOrgsCSV(@Req() request: any, @Param("id") id: number) {
return await this.postCSV(request, id);
}
@Post('/teams/:id/import/csv')
@ContentType("application/json")
@UseBefore(RawBodyMiddleware)
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from csv and insert them into the provided team" })
async postTeamsCSV(@Req() request: any, @Param("id") id: number) {
return await this.postCSV(request, id);
}
}

View File

@@ -0,0 +1,90 @@
import { Body, CurrentUser, Delete, Get, JsonController, OnUndefined, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors';
import { UpdateUser } from '../models/actions/update/UpdateUser';
import { User } from '../models/entities/User';
import { ResponseUser } from '../models/responses/ResponseUser';
import { ResponseUserPermissions } from '../models/responses/ResponseUserPermissions';
import { PermissionController } from './PermissionController';
@JsonController('/users/me')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class MeController {
private userRepository: Repository<User>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.userRepository = getConnectionManager().get().getRepository(User);
}
@Get('/')
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Lists all information about yourself.' })
async get(@CurrentUser() currentUser: User) {
let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
if (!user) { throw new UserNotFoundError(); }
return new ResponseUser(user);
}
@Get('/permissions')
@ResponseSchema(ResponseUserPermissions)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Lists all permissions granted to the you sorted into directly granted and inherited as permission response objects.' })
async getPermissions(@CurrentUser() currentUser: User) {
let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
if (!user) { throw new UserNotFoundError(); }
return new ResponseUserPermissions(user);
}
@Put('/')
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
@OpenAPI({ description: "Update the yourself. <br> You can't edit your own permissions or group memberships here - Please use the /api/users/:id enpoint instead. <br> Please remember that ids can't be changed." })
async put(@CurrentUser() currentUser: User, @Body({ validate: true }) updateUser: UpdateUser) {
let oldUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['groups'] });
updateUser.groups = oldUser.groups.map(g => g.id);
if (!oldUser) {
throw new UserNotFoundError();
}
if (oldUser.id != updateUser.id) {
throw new UserIdsNotMatchingError();
}
await this.userRepository.save(await updateUser.update(oldUser));
return new ResponseUser(await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] }));
}
@Delete('/')
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 })
@OpenAPI({ description: 'Delete yourself. <br> You have to confirm your decision by providing the ?force=true query param. <br> If there are any permissions directly granted to you they will get deleted as well.' })
async remove(@CurrentUser() currentUser: User, @QueryParam("force") force: boolean) {
if (!force) { throw new UserDeletionNotConfirmedError; }
if (!currentUser) { return UserNotFoundError; }
const responseUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] });;
const permissionControler = new PermissionController();
for (let permission of responseUser.permissions) {
await permissionControler.remove(permission.id, true);
}
await this.userRepository.delete(currentUser);
return new ResponseUser(responseUser);
}
}

View File

@@ -0,0 +1,125 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { PermissionIdsNotMatchingError, PermissionNeedsPrincipalError, PermissionNotFoundError } from '../errors/PermissionErrors';
import { PrincipalNotFoundError } from '../errors/PrincipalErrors';
import { CreatePermission } from '../models/actions/create/CreatePermission';
import { UpdatePermission } from '../models/actions/update/UpdatePermission';
import { Permission } from '../models/entities/Permission';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponsePermission } from '../models/responses/ResponsePermission';
import { ResponsePrincipal } from '../models/responses/ResponsePrincipal';
@JsonController('/permissions')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class PermissionController {
private permissionRepository: Repository<Permission>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.permissionRepository = getConnectionManager().get().getRepository(Permission);
}
@Get()
@Authorized("PERMISSION:GET")
@ResponseSchema(ResponsePermission, { isArray: true })
@OpenAPI({ description: 'Lists all permissions for all users and groups.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responsePermissions: ResponsePermission[] = new Array<ResponsePermission>();
let permissions: Array<Permission>;
if (page != undefined) {
permissions = await this.permissionRepository.find({ relations: ['principal'], skip: page * page_size, take: page_size });
} else {
permissions = await this.permissionRepository.find({ relations: ['principal'] });
}
permissions.forEach(permission => {
responsePermissions.push(new ResponsePermission(permission));
});
return responsePermissions;
}
@Get('/:id')
@Authorized("PERMISSION:GET")
@ResponseSchema(ResponsePermission)
@ResponseSchema(PermissionNotFoundError, { statusCode: 404 })
@OnUndefined(PermissionNotFoundError)
@OpenAPI({ description: 'Lists all information about the permission whose id got provided.' })
async getOne(@Param('id') id: number) {
let permission = await this.permissionRepository.findOne({ id: id }, { relations: ['principal'] });
if (!permission) { throw new PermissionNotFoundError(); }
return new ResponsePermission(permission);
}
@Post()
@Authorized("PERMISSION:CREATE")
@ResponseSchema(ResponsePermission)
@ResponseSchema(PrincipalNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new permission for a existing principal(user/group). <br> If a permission with this target, action and prinicpal already exists that permission will be returned instead of creating a new one.' })
async post(@Body({ validate: true }) createPermission: CreatePermission) {
let permission;
try {
permission = await createPermission.toEntity();
} catch (error) {
throw error;
}
let existingPermission = await this.permissionRepository.findOne({ target: permission.target, action: permission.action, principal: permission.principal }, { relations: ['principal'] });
if (existingPermission) { return new ResponsePermission(existingPermission); }
permission = await this.permissionRepository.save(permission);
permission = await this.permissionRepository.findOne(permission, { relations: ['principal'] });
return new ResponsePermission(permission);
}
@Put('/:id')
@Authorized("PERMISSION:UPDATE")
@ResponseSchema(ResponsePrincipal)
@ResponseSchema(PermissionNotFoundError, { statusCode: 404 })
@ResponseSchema(PrincipalNotFoundError, { statusCode: 404 })
@ResponseSchema(PermissionIdsNotMatchingError, { statusCode: 406 })
@ResponseSchema(PermissionNeedsPrincipalError, { statusCode: 406 })
@OpenAPI({ description: "Update a permission object. <br> If updateing the permission object would result in duplicate permission (same target, action and principal) this permission will get deleted and the existing permission will be returned. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) permission: UpdatePermission) {
let oldPermission = await this.permissionRepository.findOne({ id: id }, { relations: ['principal'] });
if (!oldPermission) {
throw new PermissionNotFoundError();
}
if (oldPermission.id != permission.id) {
throw new PermissionIdsNotMatchingError();
}
let existingPermission = await this.permissionRepository.findOne({ target: permission.target, action: permission.action, principal: await permission.getPrincipal() }, { relations: ['principal'] });
if (existingPermission) {
await this.remove(permission.id, true);
return new ResponsePermission(existingPermission);
}
await this.permissionRepository.save(await permission.update(oldPermission));
return new ResponsePermission(await this.permissionRepository.findOne({ id: permission.id }, { relations: ['principal'] }));
}
@Delete('/:id')
@Authorized("PERMISSION:DELETE")
@ResponseSchema(ResponsePermission)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Deletes the permission whose id you provide. <br> If no permission with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let permission = await this.permissionRepository.findOne({ id: id }, { relations: ['principal'] });
if (!permission) { return null; }
const responsePermission = new ResponsePermission(permission);
await this.permissionRepository.delete(permission);
return responsePermission;
}
}

View File

@@ -0,0 +1,164 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { RunnerCardHasScansError, RunnerCardIdsNotMatchingError, RunnerCardNotFoundError } from '../errors/RunnerCardErrors';
import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { deleteCardEntry } from '../nats/CardKV';
import { CreateRunnerCard } from '../models/actions/create/CreateRunnerCard';
import { UpdateRunnerCard } from '../models/actions/update/UpdateRunnerCard';
import { UpdateRunnerCardByCode } from '../models/actions/update/UpdateRunnerCardByCode';
import { RunnerCard } from '../models/entities/RunnerCard';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunnerCard } from '../models/responses/ResponseRunnerCard';
import { ScanController } from './ScanController';
@JsonController('/cards')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class RunnerCardController {
private cardRepository: Repository<RunnerCard>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.cardRepository = getConnectionManager().get().getRepository(RunnerCard);
}
@Get()
@Authorized("CARD:GET")
@ResponseSchema(ResponseRunnerCard, { isArray: true })
@OpenAPI({ description: 'Lists all card.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
let cards: Array<RunnerCard>;
if (page != undefined) {
cards = await this.cardRepository.find({ relations: ['runner', 'runner.group', 'runner.group.parentGroup'], skip: page * page_size, take: page_size });
} else {
cards = await this.cardRepository.find({ relations: ['runner', 'runner.group', 'runner.group.parentGroup'] });
}
cards.forEach(card => {
responseCards.push(new ResponseRunnerCard(card));
});
return responseCards;
}
@Get('/:id')
@Authorized("CARD:GET")
@ResponseSchema(ResponseRunnerCard)
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerCardNotFoundError)
@OpenAPI({ description: "Lists all information about the card whose id got provided." })
async getOne(@Param('id') id: number) {
let card = await this.cardRepository.findOne({ id: id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] });
if (!card) { throw new RunnerCardNotFoundError(); }
return card.toResponse();
}
@Post('/bulk')
@Authorized("CARD:CREATE")
@ResponseSchema(ResponseEmpty, { statusCode: 200 })
@OpenAPI({ description: "Create blank cards in bulk. <br> Just provide the count as a query param and wait for the 200 response. <br> You can provide the 'returnCards' query param if you want to receive the RESPONSERUNNERCARD objects in the response." })
async postBlancoBulk(@QueryParam("count") count: number, @QueryParam("returnCards") returnCards: boolean = false) {
let createPromises = new Array<any>();
for (let index = 0; index < count; index++) {
createPromises.push(this.cardRepository.save({ runner: null, enabled: true }))
}
const cards = await Promise.all(createPromises);
if (returnCards) {
let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
for await (let card of cards) {
let dbCard = await this.cardRepository.findOne({ id: card.id });
responseCards.push(new ResponseRunnerCard(dbCard));
}
return responseCards;
}
let response = new ResponseEmpty();
response.response = `Created ${count} new blanco cards.`
return response;
}
@Post()
@Authorized("CARD:CREATE")
@ResponseSchema(ResponseRunnerCard)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: "Create a new card. <br> You can provide a associated runner by id but you don't have to." })
async post(@Body({ validate: true }) createCard: CreateRunnerCard) {
let card = await createCard.toEntity();
card = await this.cardRepository.save(card);
return (await this.cardRepository.findOne({ id: card.id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] })).toResponse();
}
@Put('/:id')
@Authorized("CARD:UPDATE")
@ResponseSchema(ResponseRunnerCard)
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerCardIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the card whose id you provided. <br> Scans created via this card will still be associated with the old runner. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) card: UpdateRunnerCard) {
let oldCard = await this.cardRepository.findOne({ id: id });
if (!oldCard) {
throw new RunnerCardNotFoundError();
}
if (oldCard.id != card.id) {
throw new RunnerCardIdsNotMatchingError();
}
await this.cardRepository.save(await card.update(oldCard));
await deleteCardEntry(id);
return (await this.cardRepository.findOne({ id: id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] })).toResponse();
}
@Put('/:code')
@Authorized("CARD:UPDATE")
@ResponseSchema(ResponseRunnerCard)
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerCardIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the card whose code you provided." })
async putByCode(@Param('code') code: string, @Body({ validate: true }) card: UpdateRunnerCardByCode) {
let oldCard = await this.cardRepository.findOne({ code: code });
if (!oldCard) {
throw new RunnerCardNotFoundError();
}
if (oldCard.code != card.code) {
throw new RunnerCardIdsNotMatchingError();
}
await this.cardRepository.save(await card.update(oldCard));
return (await this.cardRepository.findOne({ code: code }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] })).toResponse();
}
@Delete('/:id')
@Authorized("CARD:DELETE")
@ResponseSchema(ResponseRunnerCard)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(RunnerCardHasScansError, { statusCode: 406 })
@OnUndefined(204)
@OpenAPI({ description: "Delete the card whose id you provided. <br> If no card with this id exists it will just return 204(no content). <br> If the card still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with by this card - please disable it instead or just remove the runner association)." })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let card = await this.cardRepository.findOne({ id: id });
if (!card) { return null; }
const cardScans = (await this.cardRepository.findOne({ id: id }, { relations: ["scans"] })).scans;
if (cardScans.length != 0 && !force) {
throw new RunnerCardHasScansError();
}
const scanController = new ScanController;
for (let scan of cardScans) {
await scanController.remove(scan.id, force);
}
await deleteCardEntry(id);
await this.cardRepository.delete(card);
return card.toResponse();
}
}

View File

@@ -1,97 +1,174 @@
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { EntityFromBody, EntityFromParam } from 'typeorm-routing-controllers-extensions';
import { RunnerGroupNeededError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
import { Repository, getConnectionManager } from 'typeorm';
import { RunnerGroupNeededError, RunnerHasDistanceDonationsError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
import { CreateRunner } from '../models/actions/CreateRunner';
import { deleteRunnerEntry } from '../nats/RunnerKV';
import { CreateRunner } from '../models/actions/create/CreateRunner';
import { UpdateRunner } from '../models/actions/update/UpdateRunner';
import { Runner } from '../models/entities/Runner';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunner } from '../models/responses/ResponseRunner';
@JsonController('/runners')
//@Authorized('RUNNERS:read')
export class RunnerController {
private runnerRepository: Repository<Runner>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerRepository = getConnectionManager().get().getRepository(Runner);
}
@Get()
@ResponseSchema(ResponseRunner, { isArray: true })
@OpenAPI({ description: 'Lists all runners.' })
async getAll() {
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
const runners = await this.runnerRepository.find({ relations: ['scans', 'group'] });
console.log(runners);
runners.forEach(runner => {
responseRunners.push(new ResponseRunner(runner));
});
return responseRunners;
}
@Get('/:id')
@ResponseSchema(ResponseRunner)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Returns a runner of a specified id (if it exists)' })
async getOne(@Param('id') id: number) {
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group'] }));
}
@Post()
@ResponseSchema(ResponseRunner)
@ResponseSchema(RunnerGroupNeededError)
@ResponseSchema(RunnerGroupNotFoundError)
@OpenAPI({ description: 'Create a new runner object (id will be generated automagicly).' })
async post(@Body({ validate: true }) createRunner: CreateRunner) {
let runner;
try {
runner = await createRunner.toRunner();
} catch (error) {
return error;
}
runner = await this.runnerRepository.save(runner)
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group'] }));
}
@Put('/:id')
@ResponseSchema(ResponseRunner)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update a runner object (id can't be changed)." })
async put(@Param('id') id: number, @EntityFromBody() runner: Runner) {
let oldRunner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group'] });
if (!oldRunner) {
throw new RunnerNotFoundError();
}
if (oldRunner.id != runner.id) {
throw new RunnerIdsNotMatchingError();
}
await this.runnerRepository.update(oldRunner, runner);
return new ResponseRunner(runner);
}
@Delete('/:id')
@ResponseSchema(ResponseRunner)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Delete a specified runner (if it exists).' })
async remove(@EntityFromParam('id') runner: Runner, @QueryParam("force") force: boolean) {
if (!runner) { throw new RunnerNotFoundError(); }
const responseRunner = await this.runnerRepository.findOne(runner, { relations: ['scans', 'group'] });
if (!runner) {
throw new RunnerNotFoundError();
}
await this.runnerRepository.delete(runner);
return new ResponseRunner(responseRunner);
}
}
import { ResponseScan } from '../models/responses/ResponseScan';
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
import { DonationController } from './DonationController';
import { RunnerCardController } from './RunnerCardController';
import { ScanController } from './ScanController';
@JsonController('/runners')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class RunnerController {
private runnerRepository: Repository<Runner>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerRepository = getConnectionManager().get().getRepository(Runner);
}
@Get()
@Authorized("RUNNER:GET")
@ResponseSchema(ResponseRunner, { isArray: true })
@OpenAPI({ description: 'Lists all runners from all teams/orgs. <br> This includes the runner\'s group and distance ran.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100, @QueryParam("created_via", { required: false }) created_via: string = "all", @QueryParam("selfservice_links", { required: false }) selfservice_links: boolean = false) {
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
let runners: Array<Runner>;
console.log("call to RunnerController.getAll() with page: " + page + " and page_size: " + page_size + " and created_via: " + created_via + " and selfservice_links: " + selfservice_links);
if (page != undefined) {
runners = await this.runnerRepository.find({ relations: ['scans', 'group', 'group.parentGroup', 'scans.track'], skip: page * page_size, take: page_size });
} else {
runners = await this.runnerRepository.find({ relations: ['scans', 'group', 'group.parentGroup', 'scans.track'] });
}
runners.forEach(runner => {
if (created_via === "all") {
responseRunners.push(new ResponseRunner(runner, selfservice_links));
} else {
if (runner.created_via === created_via) {
responseRunners.push(new ResponseRunner(runner, selfservice_links));
}
}
});
return responseRunners;
}
@Get('/:id')
@Authorized("RUNNER:GET")
@ResponseSchema(ResponseRunner)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Lists all information about the runner whose id got provided.' })
async getOne(@Param('id') id: number) {
let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations'] })
if (!runner) { throw new RunnerNotFoundError(); }
return new ResponseRunner(runner, true);
}
@Get('/:id/scans')
@Authorized(["RUNNER:GET", "SCAN:GET"])
@ResponseSchema(ResponseScan, { isArray: true })
@ResponseSchema(ResponseTrackScan, { isArray: true })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Lists all scans of the runner whose id got provided. <br> If you only want the valid scans just add the ?onlyValid=true query param.' })
async getScans(@Param('id') id: number, onlyValid?: boolean) {
let responseScans: ResponseScan[] = new Array<ResponseScan>();
let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'scans.track', 'scans.station', 'scans.runner'] })
if (!runner) { throw new RunnerNotFoundError(); }
if (!onlyValid) {
for (let scan of runner.scans) {
responseScans.push(scan.toResponse());
}
}
else {
for (let scan of runner.validScans) {
responseScans.push(scan.toResponse());
}
}
return responseScans;
}
@Post()
@Authorized("RUNNER:CREATE")
@ResponseSchema(ResponseRunner)
@ResponseSchema(RunnerGroupNeededError)
@ResponseSchema(RunnerGroupNotFoundError)
@OpenAPI({ description: 'Create a new runner. <br> Please remeber to provide the runner\'s group\'s id.' })
async post(@Body({ validate: true }) createRunner: CreateRunner) {
let runner;
try {
runner = await createRunner.toEntity();
} catch (error) {
throw error;
}
runner = await this.runnerRepository.save(runner)
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }), true);
}
@Put('/:id')
@Authorized("RUNNER:UPDATE")
@ResponseSchema(ResponseRunner)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the runner whose id you provided. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) runner: UpdateRunner) {
let oldRunner = await this.runnerRepository.findOne({ id: id }, { relations: ['group'] });
if (!oldRunner) {
throw new RunnerNotFoundError();
}
if (oldRunner.id != runner.id) {
throw new RunnerIdsNotMatchingError();
}
await this.runnerRepository.save(await runner.update(oldRunner));
await deleteRunnerEntry(id);
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }), true);
}
@Delete('/:id')
@Authorized("RUNNER:DELETE")
@ResponseSchema(ResponseRunner)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(RunnerHasDistanceDonationsError, { statusCode: 406 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the runner whose id you provided. <br> This will also delete all scans and cards associated with the runner. <br> If no runner with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let runner = await this.runnerRepository.findOne({ id: id });
if (!runner) { return null; }
const responseRunner = await this.runnerRepository.findOne(runner);
if (!runner) {
throw new RunnerNotFoundError();
}
const runnerDonations = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["distanceDonations"] })).distanceDonations;
if (runnerDonations.length > 0 && !force) {
throw new RunnerHasDistanceDonationsError();
}
const donationController = new DonationController();
for (let donation of runnerDonations) {
await donationController.remove(donation.id, force);
}
const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards;
const cardController = new RunnerCardController;
for (let card of runnerCards) {
await cardController.remove(card.id, force);
}
const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans;
const scanController = new ScanController;
for (let scan of runnerScans) {
await scanController.remove(scan.id, force);
}
await this.runnerRepository.delete(runner);
return new ResponseRunner(responseRunner);
}
}

View File

@@ -1,119 +0,0 @@
import { 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, EntityFromParam } from 'typeorm-routing-controllers-extensions';
import { RunnerOrganisationHasRunnersError, RunnerOrganisationHasTeamsError, RunnerOrganisationIdsNotMatchingError, RunnerOrganisationNotFoundError } from '../errors/RunnerOrganisationErrors';
import { CreateRunnerOrganisation } from '../models/actions/CreateRunnerOrganisation';
import { RunnerOrganisation } from '../models/entities/RunnerOrganisation';
import { ResponseRunnerOrganisation } from '../models/responses/ResponseRunnerOrganisation';
import { RunnerController } from './RunnerController';
import { RunnerTeamController } from './RunnerTeamController';
@JsonController('/organisation')
//@Authorized('RUNNERS:read')
export class RunnerOrganisationController {
private runnerOrganisationRepository: Repository<RunnerOrganisation>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerOrganisationRepository = getConnectionManager().get().getRepository(RunnerOrganisation);
}
@Get()
@ResponseSchema(ResponseRunnerOrganisation, { isArray: true })
@OpenAPI({ description: 'Lists all runnerOrganisations.' })
async getAll() {
let responseTeams: ResponseRunnerOrganisation[] = new Array<ResponseRunnerOrganisation>();
const runners = await this.runnerOrganisationRepository.find({ relations: ['address', 'contact', 'teams'] });
console.log(runners);
runners.forEach(runner => {
responseTeams.push(new ResponseRunnerOrganisation(runner));
});
return responseTeams;
}
@Get('/:id')
@ResponseSchema(ResponseRunnerOrganisation)
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerOrganisationNotFoundError)
@OpenAPI({ description: 'Returns a runnerOrganisation of a specified id (if it exists)' })
async getOne(@Param('id') id: number) {
return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne({ id: id }, { relations: ['address', 'contact', 'teams'] }));
}
@Post()
@ResponseSchema(ResponseRunnerOrganisation)
@OpenAPI({ description: 'Create a new runnerOrganisation object (id will be generated automagicly).' })
async post(@Body({ validate: true }) createRunnerOrganisation: CreateRunnerOrganisation) {
let runnerOrganisation;
try {
runnerOrganisation = await createRunnerOrganisation.toRunnerOrganisation();
} catch (error) {
return error;
}
runnerOrganisation = await this.runnerOrganisationRepository.save(runnerOrganisation);
return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(runnerOrganisation, { relations: ['address', 'contact', 'teams'] }));
}
@Put('/:id')
@ResponseSchema(ResponseRunnerOrganisation)
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerOrganisationIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update a runnerOrganisation object (id can't be changed)." })
async put(@Param('id') id: number, @EntityFromBody() runnerOrganisation: RunnerOrganisation) {
let oldRunnerOrganisation = await this.runnerOrganisationRepository.findOne({ id: id }, { relations: ['address', 'contact', 'teams'] });
if (!oldRunnerOrganisation) {
throw new RunnerOrganisationNotFoundError();
}
if (oldRunnerOrganisation.id != runnerOrganisation.id) {
throw new RunnerOrganisationIdsNotMatchingError();
}
await this.runnerOrganisationRepository.update(oldRunnerOrganisation, runnerOrganisation);
runnerOrganisation = await this.runnerOrganisationRepository.findOne(runnerOrganisation, { relations: ['address', 'contact', 'teams'] });
return new ResponseRunnerOrganisation(runnerOrganisation);
}
@Delete('/:id')
@ResponseSchema(ResponseRunnerOrganisation)
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerOrganisationHasTeamsError, { statusCode: 406 })
@ResponseSchema(RunnerOrganisationHasRunnersError, { statusCode: 406 })
@OpenAPI({ description: 'Delete a specified runnerOrganisation (if it exists).' })
async remove(@EntityFromParam('id') organisation: RunnerOrganisation, @QueryParam("force") force: boolean) {
if (!organisation) { throw new RunnerOrganisationNotFoundError() }
let runnerOrganisation = await this.runnerOrganisationRepository.findOne(organisation, { relations: ['address', 'contact', 'runners', 'teams'] });
if (!force) {
if (runnerOrganisation.teams.length != 0) {
throw new RunnerOrganisationHasTeamsError();
}
}
const teamController = new RunnerTeamController()
for (let team of runnerOrganisation.teams) {
await teamController.remove(team, true);
}
if (!force) {
if (runnerOrganisation.runners.length != 0) {
throw new RunnerOrganisationHasRunnersError();
}
}
const runnerController = new RunnerController()
for (let runner of runnerOrganisation.runners) {
await runnerController.remove(runner, true);
}
const responseOrganisation = new ResponseRunnerOrganisation(runnerOrganisation);
await this.runnerOrganisationRepository.delete(organisation);
return responseOrganisation;
}
}

View File

@@ -0,0 +1,156 @@
import { Authorized, BadRequestError, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { RunnerOrganizationHasRunnersError, RunnerOrganizationHasTeamsError, RunnerOrganizationIdsNotMatchingError, RunnerOrganizationNotFoundError } from '../errors/RunnerOrganizationErrors';
import { CreateRunnerOrganization } from '../models/actions/create/CreateRunnerOrganization';
import { UpdateRunnerOrganization } from '../models/actions/update/UpdateRunnerOrganization';
import { Runner } from '../models/entities/Runner';
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunner } from '../models/responses/ResponseRunner';
import { ResponseRunnerOrganization } from '../models/responses/ResponseRunnerOrganization';
import { RunnerController } from './RunnerController';
import { RunnerTeamController } from './RunnerTeamController';
@JsonController('/organizations')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class RunnerOrganizationController {
private runnerOrganizationRepository: Repository<RunnerOrganization>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerOrganizationRepository = getConnectionManager().get().getRepository(RunnerOrganization);
}
@Get()
@Authorized("ORGANIZATION:GET")
@ResponseSchema(ResponseRunnerOrganization, { isArray: true })
@OpenAPI({ description: 'Lists all organizations. <br> This includes their address, contact and teams (if existing/associated).' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseOrgs: ResponseRunnerOrganization[] = new Array<ResponseRunnerOrganization>();
let orgs: Array<RunnerOrganization>;
if (page != undefined) {
orgs = await this.runnerOrganizationRepository.find({ relations: ['contact', 'teams'], skip: page * page_size, take: page_size });
} else {
orgs = await this.runnerOrganizationRepository.find({ relations: ['contact', 'teams'] });
}
orgs.forEach(org => {
responseOrgs.push(new ResponseRunnerOrganization(org));
});
return responseOrgs;
}
@Get('/:id')
@Authorized("ORGANIZATION:GET")
@ResponseSchema(ResponseRunnerOrganization)
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerOrganizationNotFoundError)
@OpenAPI({ description: 'Lists all information about the organization whose id got provided.' })
async getOne(@Param('id') id: number) {
let runnerOrg = await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['contact', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.scans.track', 'runners', 'runners.scans', 'runners.scans.track'] });
if (!runnerOrg) { throw new RunnerOrganizationNotFoundError(); }
return new ResponseRunnerOrganization(runnerOrg);
}
@Get('/:id/runners')
@Authorized(["RUNNER:GET", "SCAN:GET"])
@ResponseSchema(ResponseRunner, { isArray: true })
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Lists all runners from this org and it\'s teams (if you don\'t provide the ?onlyDirect=true param). <br> This includes the runner\'s group and distance ran.' })
async getRunners(@Param('id') id: number, @QueryParam('onlyDirect') onlyDirect: boolean, @QueryParam("selfservice_links", { required: false }) selfservice_links: boolean = false) {
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
let runners: Runner[];
if (!onlyDirect) { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.group', 'teams.runners.group.parentGroup', 'teams.runners.scans', 'teams.runners.scans.track'] })).allRunners; }
else { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners; }
runners.forEach(runner => {
responseRunners.push(new ResponseRunner(runner, selfservice_links));
});
return responseRunners;
}
@Post()
@Authorized("ORGANIZATION:CREATE")
@ResponseSchema(ResponseRunnerOrganization)
@OpenAPI({ description: 'Create a new organsisation.' })
async post(@Body({ validate: true }) createRunnerOrganization: CreateRunnerOrganization) {
let runnerOrganization;
try {
runnerOrganization = await createRunnerOrganization.toEntity();
} catch (error) {
throw error;
}
runnerOrganization = await this.runnerOrganizationRepository.save(runnerOrganization);
return new ResponseRunnerOrganization(await this.runnerOrganizationRepository.findOne(runnerOrganization, { relations: ['contact', 'teams'] }));
}
@Put('/:id')
@Authorized("ORGANIZATION:UPDATE")
@ResponseSchema(ResponseRunnerOrganization)
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerOrganizationIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the organization whose id you provided. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) updateOrganization: UpdateRunnerOrganization) {
let oldRunnerOrganization = await this.runnerOrganizationRepository.findOne({ id: id });
if (!oldRunnerOrganization) {
throw new RunnerOrganizationNotFoundError();
}
if (oldRunnerOrganization.id != updateOrganization.id) {
throw new RunnerOrganizationIdsNotMatchingError();
}
await this.runnerOrganizationRepository.save(await updateOrganization.update(oldRunnerOrganization));
return new ResponseRunnerOrganization(await this.runnerOrganizationRepository.findOne(id, { relations: ['contact', 'teams'] }));
}
@Delete('/:id')
@Authorized("ORGANIZATION:DELETE")
@ResponseSchema(ResponseRunnerOrganization)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(RunnerOrganizationHasTeamsError, { statusCode: 406 })
@ResponseSchema(RunnerOrganizationHasRunnersError, { statusCode: 406 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the organsisation whose id you provided. <br> If the organization still has runners and/or teams associated this will fail. <br> To delete the organization with all associated runners and teams set the force QueryParam to true (cascading deletion might take a while). <br> This won\'t delete the associated contact. <br> If no organization with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
if (id == 1) {
throw new BadRequestError("You can't delete the citizen runner org.");
}
let organization = await this.runnerOrganizationRepository.findOne({ id: id });
if (!organization) { return null; }
let runnerOrganization = await this.runnerOrganizationRepository.findOne(organization, { relations: ['contact', 'runners', 'teams'] });
if (!force) {
if (runnerOrganization.teams.length != 0) {
throw new RunnerOrganizationHasTeamsError();
}
}
const teamController = new RunnerTeamController()
for (let team of runnerOrganization.teams) {
await teamController.remove(team.id, true);
}
if (!force) {
if (runnerOrganization.runners.length != 0) {
throw new RunnerOrganizationHasRunnersError();
}
}
const runnerController = new RunnerController()
for (let runner of runnerOrganization.runners) {
await runnerController.remove(runner.id, true);
}
const responseOrganization = new ResponseRunnerOrganization(runnerOrganization);
await this.runnerOrganizationRepository.delete(organization);
return responseOrganization;
}
}

View File

@@ -0,0 +1,244 @@
import type { Request } from "express";
import * as jwt from "jsonwebtoken";
import { BadRequestError, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { config } from '../config';
import { InvalidCredentialsError, JwtNotProvidedError } from '../errors/AuthError';
import { MailSendingError } from '../errors/MailErrors';
import { RunnerEmailNeededError, RunnerHasDistanceDonationsError, RunnerNotFoundError, RunnerSelfserviceTimeoutError } from '../errors/RunnerErrors';
import { RunnerOrganizationNotFoundError } from '../errors/RunnerOrganizationErrors';
import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
import { JwtCreator } from '../jwtcreator';
import { Mailer } from '../mailer';
import ScanAuth from '../middlewares/ScanAuth';
import { CreateSelfServiceCitizenRunner } from '../models/actions/create/CreateSelfServiceCitizenRunner';
import { CreateSelfServiceRunner } from '../models/actions/create/CreateSelfServiceRunner';
import { Runner } from '../models/entities/Runner';
import { RunnerGroup } from '../models/entities/RunnerGroup';
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
import { ScanStation } from '../models/entities/ScanStation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseScanStation } from '../models/responses/ResponseScanStation';
import { ResponseSelfServiceOrganisation } from '../models/responses/ResponseSelfServiceOrganisation';
import { ResponseSelfServiceRunner } from '../models/responses/ResponseSelfServiceRunner';
import { ResponseSelfServiceScan } from '../models/responses/ResponseSelfServiceScan';
import { DonationController } from './DonationController';
import { RunnerCardController } from './RunnerCardController';
import { ScanController } from './ScanController';
@JsonController()
export class RunnerSelfServiceController {
private runnerRepository: Repository<Runner>;
private orgRepository: Repository<RunnerOrganization>;
private stationRepository: Repository<ScanStation>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerRepository = getConnectionManager().get().getRepository(Runner);
this.orgRepository = getConnectionManager().get().getRepository(RunnerOrganization);
this.stationRepository = getConnectionManager().get().getRepository(ScanStation);
}
@Get('/runners/me/:jwt')
@ResponseSchema(ResponseSelfServiceRunner)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Lists all information about yourself. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please use the forgot endpoint.' })
async get(@Param('jwt') token: string) {
return (new ResponseSelfServiceRunner(await this.getRunner(token)));
}
@Delete('/runners/me/:jwt')
@ResponseSchema(ResponseSelfServiceRunner)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Deletes all information about yourself. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please use the forgot endpoint.' })
async remove(@Param('jwt') token: string, @QueryParam("force") force: boolean) {
const responseRunner = await this.getRunner(token);
let runner = await this.runnerRepository.findOne({ id: responseRunner.id });
if (!runner) { return null; }
if (!runner) {
throw new RunnerNotFoundError();
}
const runnerDonations = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["distanceDonations"] })).distanceDonations;
if (runnerDonations.length > 0 && !force) {
throw new RunnerHasDistanceDonationsError();
}
const donationController = new DonationController();
for (let donation of runnerDonations) {
await donationController.remove(donation.id, force);
}
const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards;
const cardController = new RunnerCardController;
for (let card of runnerCards) {
await cardController.remove(card.id, force);
}
const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans;
const scanController = new ScanController;
for (let scan of runnerScans) {
await scanController.remove(scan.id, force);
}
await this.runnerRepository.delete(runner);
return new ResponseSelfServiceRunner(responseRunner);
}
@Get('/runners/me/:jwt/scans')
@ResponseSchema(ResponseSelfServiceScan, { isArray: true })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Lists all your (runner) scans. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please contact support.' })
async getScans(@Param('jwt') token: string) {
const scans = (await this.getRunner(token)).scans;
let responseScans = new Array<ResponseSelfServiceScan>()
for (let scan of scans) {
responseScans.push(new ResponseSelfServiceScan(scan));
}
return responseScans;
}
@Get('/stations/me')
@UseBefore(ScanAuth)
@ResponseSchema(ResponseScanStation)
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
@OnUndefined(ScanStationNotFoundError)
@OpenAPI({ description: 'Lists basic information about the station whose token got provided. <br> This includes it\'s associated track.', security: [{ "StationApiToken": [] }] })
async getStationMe(@Req() req: Request) {
let scan = await this.stationRepository.findOne({ id: parseInt(req.headers["station_id"].toString()) }, { relations: ['track'] })
if (!scan) { throw new ScanStationNotFoundError(); }
return scan.toResponse();
}
@Post('/runners/login')
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(ResponseEmpty)
@OpenAPI({ description: 'Use this endpoint to reuqest a new selfservice magic-login-link to be sent to your mail address (rate limited to one mail every 15mins).' })
async requestNewToken(@QueryParam('mail') mail: string, @QueryParam("locale") locale: string = "en") {
if (!mail) {
throw new RunnerNotFoundError();
}
const runner = await this.runnerRepository.findOne({ email: mail });
if (!runner) { throw new RunnerNotFoundError(); }
if (runner.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 30)) { throw new RunnerSelfserviceTimeoutError(); }
const token = JwtCreator.createSelfService(runner);
try {
await Mailer.sendSelfserviceForgottenMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, token, locale)
} catch (error) {
throw new MailSendingError();
}
runner.resetRequestedTimestamp = Math.floor(Date.now() / 1000);
await this.runnerRepository.save(runner);
return { token };
}
@Post('/runners/register')
@ResponseSchema(ResponseSelfServiceRunner)
@ResponseSchema(RunnerEmailNeededError, { statusCode: 406 })
@OpenAPI({ description: 'Create a new selfservice runner in the citizen org. <br> This endpoint shoud be used to allow "everyday citizen" to register themselves. <br> You have to provide a mail address, b/c the future we\'ll implement email verification.' })
async registerRunner(@Body({ validate: true }) createRunner: CreateSelfServiceCitizenRunner, @QueryParam("locale") locale: string = "en") {
let runner = await createRunner.toEntity();
if (await this.getRunnerExistsByMail(runner.email)) {
throw new BadRequestError("E-Mail already registered")
}
runner = await this.runnerRepository.save(runner);
let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
response.token = JwtCreator.createSelfService(runner);
try {
await Mailer.sendSelfserviceWelcomeMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, response.token, locale)
} catch (error) {
throw new MailSendingError();
}
return response;
}
@Post('/runners/register/:token')
@ResponseSchema(ResponseSelfServiceRunner)
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new selfservice runner in a provided org. <br> The orgs get provided and authorized via api tokens that can be optained via the /organizations endpoint.' })
async registerOrganizationRunner(@Param('token') token: string, @Body({ validate: true }) createRunner: CreateSelfServiceRunner, @QueryParam("locale") locale: string = "en") {
const org = await this.getOrgansisation(token);
let runner = await createRunner.toEntity(org);
if (await this.getRunnerExistsByMail(runner.email)) {
throw new BadRequestError("E-Mail already registered")
}
runner = await this.runnerRepository.save(runner);
let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
response.token = JwtCreator.createSelfService(runner);
try {
await Mailer.sendSelfserviceWelcomeMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, response.token, locale)
} catch (error) {
throw new MailSendingError();
}
return response;
}
@Get('/organizations/selfservice/:token')
@ResponseSchema(ResponseSelfServiceOrganisation, { isArray: false })
@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Get the basic info and teams for a org.' })
async getSelfserviceOrg(@Param('token') token: string) {
const orgid = (await this.getOrgansisation(token)).id;
const org = await this.orgRepository.findOne({ id: orgid }, { relations: ['teams'] })
return new ResponseSelfServiceOrganisation(<RunnerOrganization>org);
}
/**
* Get's a runner by a provided jwt token.
* @param token The runner jwt provided by the runner to identitfy themselves.
*/
private async getRunner(token: string): Promise<Runner> {
if (token == "") { throw new JwtNotProvidedError(); }
let jwtPayload = undefined
try {
jwtPayload = <any>jwt.verify(token, config.jwt_secret);
} catch (error) {
throw new InvalidCredentialsError();
}
const runner = await this.runnerRepository.findOne({ id: jwtPayload["id"] }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] });
if (!runner) { throw new RunnerNotFoundError() }
return runner;
}
/**
* Get's a runner org by a provided registration api key.
* @param token The organization's registration api token.
*/
private async getOrgansisation(token: string): Promise<RunnerGroup> {
token = Buffer.from(token, 'base64').toString('utf8');
const organization = await this.orgRepository.findOne({ key: token });
if (!organization) { throw new RunnerOrganizationNotFoundError; }
return organization;
}
/**
* Checks if a runner already exists
* @param email The runner's email address
* @returns Boolean (true if exists, false if not)
*/
private async getRunnerExistsByMail(email: string): Promise<boolean> {
const runner = await this.runnerRepository.findOne({ email });
return runner != undefined
}
}

View File

@@ -1,108 +1,138 @@
import { 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, EntityFromParam } from 'typeorm-routing-controllers-extensions';
import { RunnerTeamHasRunnersError, RunnerTeamIdsNotMatchingError, RunnerTeamNotFoundError } from '../errors/RunnerTeamErrors';
import { CreateRunnerTeam } from '../models/actions/CreateRunnerTeam';
import { RunnerTeam } from '../models/entities/RunnerTeam';
import { ResponseRunnerTeam } from '../models/responses/ResponseRunnerTeam';
import { RunnerController } from './RunnerController';
@JsonController('/teams')
//@Authorized('RUNNERS:read')
export class RunnerTeamController {
private runnerTeamRepository: Repository<RunnerTeam>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerTeamRepository = getConnectionManager().get().getRepository(RunnerTeam);
}
@Get()
@ResponseSchema(ResponseRunnerTeam, { isArray: true })
@OpenAPI({ description: 'Lists all runnerTeams.' })
async getAll() {
let responseTeams: ResponseRunnerTeam[] = new Array<ResponseRunnerTeam>();
const runners = await this.runnerTeamRepository.find({ relations: ['parentGroup', 'contact'] });
console.log(runners);
runners.forEach(runner => {
responseTeams.push(new ResponseRunnerTeam(runner));
});
return responseTeams;
}
@Get('/:id')
@ResponseSchema(ResponseRunnerTeam)
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerTeamNotFoundError)
@OpenAPI({ description: 'Returns a runnerTeam of a specified id (if it exists)' })
async getOne(@Param('id') id: number) {
return new ResponseRunnerTeam(await this.runnerTeamRepository.findOne({ id: id }, { relations: ['parentGroup', 'contact'] }));
}
@Post()
@ResponseSchema(ResponseRunnerTeam)
@OpenAPI({ description: 'Create a new runnerTeam object (id will be generated automagicly).' })
async post(@Body({ validate: true }) createRunnerTeam: CreateRunnerTeam) {
let runnerTeam;
try {
runnerTeam = await createRunnerTeam.toRunnerTeam();
} catch (error) {
return error;
}
runnerTeam = await this.runnerTeamRepository.save(runnerTeam);
runnerTeam = await this.runnerTeamRepository.findOne(runnerTeam, { relations: ['parentGroup', 'contact'] });
return new ResponseRunnerTeam(runnerTeam);
}
@Put('/:id')
@ResponseSchema(ResponseRunnerTeam)
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerTeamIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update a runnerTeam object (id can't be changed)." })
async put(@Param('id') id: number, @EntityFromBody() runnerTeam: RunnerTeam) {
let oldRunnerTeam = await this.runnerTeamRepository.findOne({ id: id }, { relations: ['parentGroup', 'contact'] });
if (!oldRunnerTeam) {
throw new RunnerTeamNotFoundError();
}
if (oldRunnerTeam.id != runnerTeam.id) {
throw new RunnerTeamIdsNotMatchingError();
}
await this.runnerTeamRepository.update(oldRunnerTeam, runnerTeam);
runnerTeam = await this.runnerTeamRepository.findOne(runnerTeam, { relations: ['parentGroup', 'contact'] });
return new ResponseRunnerTeam(runnerTeam);
}
@Delete('/:id')
@ResponseSchema(ResponseRunnerTeam)
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerTeamHasRunnersError, { statusCode: 406 })
@OpenAPI({ description: 'Delete a specified runnerTeam (if it exists).' })
async remove(@EntityFromParam('id') team: RunnerTeam, @QueryParam("force") force: boolean) {
if (!team) { throw new RunnerTeamNotFoundError(); }
let runnerTeam = await this.runnerTeamRepository.findOne(team, { relations: ['parentGroup', 'contact', 'runners'] });
if (!force) {
if (runnerTeam.runners.length != 0) {
throw new RunnerTeamHasRunnersError();
}
}
const runnerController = new RunnerController()
for (let runner of runnerTeam.runners) {
await runnerController.remove(runner, true);
}
const responseTeam = new ResponseRunnerTeam(runnerTeam);
await this.runnerTeamRepository.delete(team);
return responseTeam;
}
}
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { RunnerTeamHasRunnersError, RunnerTeamIdsNotMatchingError, RunnerTeamNotFoundError } from '../errors/RunnerTeamErrors';
import { CreateRunnerTeam } from '../models/actions/create/CreateRunnerTeam';
import { UpdateRunnerTeam } from '../models/actions/update/UpdateRunnerTeam';
import { RunnerTeam } from '../models/entities/RunnerTeam';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunner } from '../models/responses/ResponseRunner';
import { ResponseRunnerTeam } from '../models/responses/ResponseRunnerTeam';
import { RunnerController } from './RunnerController';
@JsonController('/teams')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class RunnerTeamController {
private runnerTeamRepository: Repository<RunnerTeam>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerTeamRepository = getConnectionManager().get().getRepository(RunnerTeam);
}
@Get()
@Authorized("TEAM:GET")
@ResponseSchema(ResponseRunnerTeam, { isArray: true })
@OpenAPI({ description: 'Lists all teams. <br> This includes their parent organization and contact (if existing/associated).' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseTeams: ResponseRunnerTeam[] = new Array<ResponseRunnerTeam>();
let teams: Array<RunnerTeam>;
if (page != undefined) {
teams = await this.runnerTeamRepository.find({ relations: ['parentGroup', 'contact'], skip: page * page_size, take: page_size });
} else {
teams = await this.runnerTeamRepository.find({ relations: ['parentGroup', 'contact'] });
}
teams.forEach(team => {
responseTeams.push(new ResponseRunnerTeam(team));
});
return responseTeams;
}
@Get('/:id')
@Authorized("TEAM:GET")
@ResponseSchema(ResponseRunnerTeam)
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerTeamNotFoundError)
@OpenAPI({ description: 'Lists all information about the team whose id got provided.' })
async getOne(@Param('id') id: number) {
let runnerTeam = await this.runnerTeamRepository.findOne({ id: id }, { relations: ['parentGroup', 'contact', 'runners', 'runners.scans', 'runners.scans.track'] });
if (!runnerTeam) { throw new RunnerTeamNotFoundError(); }
return new ResponseRunnerTeam(runnerTeam);
}
@Get('/:id/runners')
@Authorized(["RUNNER:GET", "SCAN:GET"])
@ResponseSchema(ResponseRunner, { isArray: true })
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Lists all runners from this team. <br> This includes the runner\'s group and distance ran.' })
async getRunners(@Param('id') id: number, @QueryParam("selfservice_links", { required: false }) selfservice_links: boolean = false) {
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
const runners = (await this.runnerTeamRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners;
runners.forEach(runner => {
responseRunners.push(new ResponseRunner(runner, selfservice_links));
});
return responseRunners;
}
@Post()
@Authorized("TEAM:CREATE")
@ResponseSchema(ResponseRunnerTeam)
@OpenAPI({ description: 'Create a new organsisation. <br> Please remember to provide it\'s parent group\'s id.' })
async post(@Body({ validate: true }) createRunnerTeam: CreateRunnerTeam) {
let runnerTeam;
try {
runnerTeam = await createRunnerTeam.toEntity();
} catch (error) {
throw error;
}
runnerTeam = await this.runnerTeamRepository.save(runnerTeam);
runnerTeam = await this.runnerTeamRepository.findOne(runnerTeam, { relations: ['parentGroup', 'contact'] });
return new ResponseRunnerTeam(runnerTeam);
}
@Put('/:id')
@Authorized("TEAM:UPDATE")
@ResponseSchema(ResponseRunnerTeam)
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerTeamIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the team whose id you provided. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) runnerTeam: UpdateRunnerTeam) {
let oldRunnerTeam = await this.runnerTeamRepository.findOne({ id: id }, { relations: ['parentGroup', 'contact'] });
if (!oldRunnerTeam) {
throw new RunnerTeamNotFoundError();
}
if (oldRunnerTeam.id != runnerTeam.id) {
throw new RunnerTeamIdsNotMatchingError();
}
await this.runnerTeamRepository.save(await runnerTeam.update(oldRunnerTeam));
return new ResponseRunnerTeam(await this.runnerTeamRepository.findOne({ id: runnerTeam.id }, { relations: ['parentGroup', 'contact'] }));
}
@Delete('/:id')
@Authorized("TEAM:DELETE")
@ResponseSchema(ResponseRunnerTeam)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(RunnerTeamHasRunnersError, { statusCode: 406 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the team whose id you provided. <br> If the team still has runners associated this will fail. <br> To delete the team with all associated runners set the force QueryParam to true (cascading deletion might take a while). <br> This won\'t delete the associated contact.<br> If no team with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let team = await this.runnerTeamRepository.findOne({ id: id });
if (!team) { return null; }
let runnerTeam = await this.runnerTeamRepository.findOne(team, { relations: ['runners'] });
if (!force) {
if (runnerTeam.runners.length != 0) {
throw new RunnerTeamHasRunnersError();
}
}
const runnerController = new RunnerController()
for (let runner of runnerTeam.runners) {
await runnerController.remove(runner.id, true);
}
const responseTeam = new ResponseRunnerTeam(runnerTeam);
await this.runnerTeamRepository.delete(team);
return responseTeam;
}
}

View File

@@ -0,0 +1,254 @@
import type { Request } from "express";
import { Authorized, Body, Delete, Get, HttpError, JsonController, OnUndefined, Param, Post, Put, QueryParam, Req, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnection, getConnectionManager } from 'typeorm';
import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors';
import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
import ScanAuth from '../middlewares/ScanAuth';
import { deleteCardEntry, getCardEntry, setCardEntry } from '../nats/CardKV';
import { deleteRunnerEntry, getRunnerEntry, RunnerKVEntry, setRunnerEntry, warmRunner } from '../nats/RunnerKV';
import { getStationEntryById } from '../nats/StationKV';
import { CreateScan } from '../models/actions/create/CreateScan';
import { CreateTrackScan } from '../models/actions/create/CreateTrackScan';
import { UpdateScan } from '../models/actions/update/UpdateScan';
import { UpdateTrackScan } from '../models/actions/update/UpdateTrackScan';
import { RunnerCard } from '../models/entities/RunnerCard';
import { Scan } from '../models/entities/Scan';
import { TrackScan } from '../models/entities/TrackScan';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseScan } from '../models/responses/ResponseScan';
import { ResponseScanIntake, ResponseScanIntakeRunner } from '../models/responses/ResponseScanIntake';
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
@JsonController('/scans')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class ScanController {
private scanRepository: Repository<Scan>;
private trackScanRepository: Repository<TrackScan>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.scanRepository = getConnectionManager().get().getRepository(Scan);
this.trackScanRepository = getConnectionManager().get().getRepository(TrackScan);
}
@Get()
@Authorized("SCAN:GET")
@ResponseSchema(ResponseScan, { isArray: true })
@ResponseSchema(ResponseTrackScan, { isArray: true })
@OpenAPI({ description: 'Lists all scans (normal or track) from all runners. <br> This includes the scan\'s runner\'s distance ran.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseScans: ResponseScan[] = new Array<ResponseScan>();
let scans: Array<Scan>;
if (page != undefined) {
scans = await this.scanRepository.find({ relations: ['runner', 'track'], skip: page * page_size, take: page_size });
} else {
scans = await this.scanRepository.find({ relations: ['runner', 'track'] });
}
scans.forEach(scan => {
responseScans.push(scan.toResponse());
});
return responseScans;
}
@Get('/:id')
@Authorized("SCAN:GET")
@ResponseSchema(ResponseScan)
@ResponseSchema(ResponseTrackScan)
@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
@OnUndefined(ScanNotFoundError)
@OpenAPI({ description: 'Lists all information about the scan whose id got provided. This includes the scan\'s runner\'s distance ran.' })
async getOne(@Param('id') id: number) {
let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.group', 'card', 'station'] })
if (!scan) { throw new ScanNotFoundError(); }
return scan.toResponse();
}
@Post()
@UseBefore(ScanAuth)
@ResponseSchema(ResponseScan)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new scan (not track scan - use /scans/trackscans instead). <br> Please rmemember to provide the scan\'s runner\'s id and distance.', security: [{ "StationApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async post(@Body({ validate: true }) createScan: CreateScan) {
let scan = await createScan.toEntity();
scan = await this.scanRepository.save(scan);
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
}
@Post("/trackscans")
@UseBefore(ScanAuth)
@ResponseSchema(ResponseTrackScan)
@ResponseSchema(ResponseScanIntake)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new track scan (for "normal" scans use /scans instead). <br> Please remember that to provide the scan\'s card\'s station\'s id.', security: [{ "StationApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan, @Req() req: Request) {
// Station token path — KV-backed intake flow
if (req.isStationAuth) {
return this.stationIntake(createScan.card, req.stationId);
}
// JWT path — existing full flow, unchanged
createScan.station = createScan.station;
let scan = await createScan.toEntity();
scan = await this.trackScanRepository.save(scan);
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
}
/**
* KV-backed hot path for scan station submissions.
* Zero DB reads on a fully warm cache. Fixes the race condition via CAS on the runner KV entry.
*/
private async stationIntake(rawCard: number, stationId: number): Promise<ResponseScanIntake> {
const MAX_RETRIES = 3;
const cardId = rawCard % 200000000000;
// --- Station (already verified by ScanAuth, just need track data) ---
const stationEntry = await getStationEntryById(stationId);
// stationEntry is always populated here — ScanAuth wrote it on the cold path
const trackDistance = stationEntry.trackDistance;
const minimumLapTime = stationEntry.minimumLapTime;
// --- Card ---
let cardEntry = await getCardEntry(cardId);
if (!cardEntry) {
// Cold path: load from DB and cache
const card = await getConnection().getRepository(RunnerCard).findOne({ id: cardId }, { relations: ['runner'] });
if (!card) throw new ScanNotFoundError();
if (!card.runner) throw new RunnerNotFoundError();
cardEntry = {
runnerId: card.runner.id,
runnerDisplayName: `${card.runner.firstname} ${card.runner.lastname}`,
enabled: card.enabled,
};
await setCardEntry(cardId, cardEntry);
}
if (!cardEntry.enabled) throw new HttpError(400, 'Card is disabled.');
const runnerId = cardEntry.runnerId;
// --- Runner state + CAS update (fixes race condition) ---
const now = Math.round(Date.now() / 1000);
let retries = 0;
let response: ResponseScanIntake;
while (retries < MAX_RETRIES) {
// Get current runner state (warm or cold)
let result = await getRunnerEntry(runnerId);
if (!result) {
const warmed = await warmRunner(runnerId);
result = { entry: warmed, revision: undefined };
}
const { entry, revision } = result;
// Compute
const lapTime = entry.latestTimestamp === 0 ? 0 : now - entry.latestTimestamp;
const valid = minimumLapTime === 0 || lapTime > minimumLapTime;
const newDistance = entry.totalDistance + (valid ? trackDistance : 0);
const newTimestamp = valid ? now : entry.latestTimestamp;
const updated: RunnerKVEntry = {
displayName: entry.displayName,
totalDistance: newDistance,
latestTimestamp: newTimestamp,
};
// CAS write — if revision is undefined (warmed this request), plain put
const success = await setRunnerEntry(runnerId, updated, revision);
if (!success) {
retries++;
continue;
}
// DB insert — synchronous, keeps DB as source of truth
const newScan = new TrackScan();
newScan.runner = { id: runnerId } as any;
newScan.card = { id: cardId } as any;
newScan.station = { id: stationId } as any;
newScan.track = { id: stationEntry.trackId } as any;
newScan.timestamp = now;
newScan.lapTime = lapTime;
newScan.valid = valid;
await this.trackScanRepository.save(newScan);
const runnerInfo = new ResponseScanIntakeRunner();
runnerInfo.displayName = entry.displayName;
runnerInfo.totalDistance = newDistance;
response = new ResponseScanIntake();
response.accepted = true;
response.valid = valid;
response.lapTime = lapTime;
response.runner = runnerInfo;
return response;
}
throw new HttpError(409, 'Scan rejected: too many concurrent scans for this runner. Please retry.');
}
@Put('/:id')
@Authorized("SCAN:UPDATE")
@ResponseSchema(ResponseScan)
@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the scan (not track scan use /scans/trackscans/:id instead) whose id you provided. <br> Please remember that ids can't be changed and distances must be positive." })
async put(@Param('id') id: number, @Body({ validate: true }) scan: UpdateScan) {
let oldScan = await this.scanRepository.findOne({ id: id }, { relations: ['runner'] });
if (!oldScan) {
throw new ScanNotFoundError();
}
if (oldScan.id != scan.id) {
throw new ScanIdsNotMatchingError();
}
const runnerId = oldScan.runner?.id;
await this.scanRepository.save(await scan.update(oldScan));
if (runnerId) await deleteRunnerEntry(runnerId);
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
}
@Put('/trackscans/:id')
@Authorized("SCAN:UPDATE")
@ResponseSchema(ResponseTrackScan)
@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: 'Update the track scan (not "normal" scan use /scans/trackscans/:id instead) whose id you provided. <br> Please remember that only the validity, runner and track can be changed.' })
async putTrackScan(@Param('id') id: number, @Body({ validate: true }) scan: UpdateTrackScan) {
let oldScan = await this.trackScanRepository.findOne({ id: id }, { relations: ['runner'] });
if (!oldScan) {
throw new ScanNotFoundError();
}
if (oldScan.id != scan.id) {
throw new ScanIdsNotMatchingError();
}
const runnerId = oldScan.runner?.id;
await this.trackScanRepository.save(await scan.update(oldScan));
if (runnerId) await deleteRunnerEntry(runnerId);
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
}
@Delete('/:id')
@Authorized("SCAN:DELETE")
@ResponseSchema(ResponseScan)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the scan whose id you provided. <br> If no scan with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let scan = await this.scanRepository.findOne({ id: id });
if (!scan) { return null; }
const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] });
await this.scanRepository.delete(scan);
return responseScan.toResponse();
}
}

View File

@@ -0,0 +1,118 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { ScanStationHasScansError, ScanStationIdsNotMatchingError, ScanStationNotFoundError } from '../errors/ScanStationErrors';
import { TrackNotFoundError } from '../errors/TrackErrors';
import { deleteStationEntry } from '../nats/StationKV';
import { CreateScanStation } from '../models/actions/create/CreateScanStation';
import { UpdateScanStation } from '../models/actions/update/UpdateScanStation';
import { ScanStation } from '../models/entities/ScanStation';
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(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseStations: ResponseScanStation[] = new Array<ResponseScanStation>();
let stations: Array<ScanStation>;
if (page != undefined) {
stations = await this.stationRepository.find({ relations: ['track'], skip: page * page_size, take: page_size });
} else {
stations = await this.stationRepository.find({ relations: ['track'] });
}
stations.forEach(station => {
responseStations.push(station.toResponse());
});
return responseStations;
}
@Get('/:id')
@Authorized("STATION:GET")
@ResponseSchema(ResponseScanStation)
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
@OnUndefined(ScanStationNotFoundError)
@OpenAPI({ description: 'Lists all information about the station whose id got provided. <br> This includes it\'s associated track.' })
async getOne(@Param('id') id: number) {
let scan = await this.stationRepository.findOne({ id: id }, { relations: ['track'] })
if (!scan) { throw new ScanStationNotFoundError(); }
return scan.toResponse();
}
@Post()
@Authorized("STATION:CREATE")
@ResponseSchema(ResponseScanStation)
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new station. <br> Please remeber to provide the station\'s track\'s id. <br> Please also remember that the station key is only visibe on creation.' })
async post(@Body({ validate: true }) createStation: CreateScanStation) {
let newStation = await createStation.toEntity();
const station = await this.stationRepository.save(newStation);
let responseStation = (await this.stationRepository.findOne({ id: station.id }, { relations: ['track'] })).toResponse();
responseStation.key = newStation.cleartextkey;
return responseStation;
}
@Put('/:id')
@Authorized("STATION:UPDATE")
@ResponseSchema(ResponseScanStation)
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
@ResponseSchema(ScanStationIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the station whose id you provided. <br> Please remember that only the description and enabled state can be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) station: UpdateScanStation) {
let oldStation = await this.stationRepository.findOne({ id: id });
if (!oldStation) {
throw new ScanStationNotFoundError();
}
if (oldStation.id != station.id) {
throw new ScanStationIdsNotMatchingError();
}
await this.stationRepository.save(await station.update(oldStation));
await deleteStationEntry(oldStation.prefix);
return (await this.stationRepository.findOne({ id: id }, { relations: ['track'] })).toResponse();
}
@Delete('/:id')
@Authorized("STATION:DELETE")
@ResponseSchema(ResponseScanStation)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(ScanStationHasScansError, { statusCode: 406 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the station whose id you provided. <br> If no station with this id exists it will just return 204(no content). <br> If the station still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with/created by this station - please disable it instead).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let station = await this.stationRepository.findOne({ id: id });
if (!station) { return null; }
const stationScans = (await this.stationRepository.findOne({ id: station.id }, { relations: ["scans"] })).scans;
if (stationScans.length != 0 && !force) {
throw new ScanStationHasScansError();
}
const scanController = new ScanController;
for (let scan of stationScans) {
await scanController.remove(scan.id, force);
}
const responseStation = await this.stationRepository.findOne({ id: station.id }, { relations: ["track"] });
await deleteStationEntry(station.prefix);
await this.stationRepository.delete(station);
return responseStation.toResponse();
}
}

View File

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

View File

@@ -0,0 +1,277 @@
import { Get, JsonController, QueryParam, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnection } from 'typeorm';
import StatsAuth from '../middlewares/StatsAuth';
import { Donation } from '../models/entities/Donation';
import { Donor } from '../models/entities/Donor';
import { Runner } from '../models/entities/Runner';
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
import { RunnerTeam } from '../models/entities/RunnerTeam';
import { Scan } from '../models/entities/Scan';
import { TrackScan } from '../models/entities/TrackScan';
import { User } from '../models/entities/User';
import { ResponseStats } from '../models/responses/ResponseStats';
import { ResponseStatsOrgnisation } from '../models/responses/ResponseStatsOrganization';
import { ResponseStatsRunner } from '../models/responses/ResponseStatsRunner';
import { ResponseStatsTeam } from '../models/responses/ResponseStatsTeam';
import { getStatsCache, setStatsCache } from '../nats/StatsKV';
@JsonController('/stats')
export class StatsController {
@Get()
@ResponseSchema(ResponseStats)
@OpenAPI({ description: "A very basic stats endpoint providing basic counters for a dashboard or simmilar" })
async get() {
// Try cache first
const cached = await getStatsCache<ResponseStats>('overview');
if (cached) {
return cached;
}
// Cache miss - compute fresh stats
const connection = getConnection();
const runnersViaSelfservice = await connection.getRepository(Runner).count({ where: { created_via: "selfservice" } });
const runnersViaKiosk = await connection.getRepository(Runner).count({ where: { created_via: "kiosk" } });
const runners = await connection.getRepository(Runner).count();
const teams = await connection.getRepository(RunnerTeam).count();
const orgs = await connection.getRepository(RunnerOrganization).count();
const users = await connection.getRepository(User).count();
const scans = await connection.getRepository(Scan).count({ where: { valid: true } });
const distance_query = await connection.getRepository(Scan).createQueryBuilder('scan')
.leftJoinAndSelect("scan.track", "track").where("scan.valid = TRUE")
.select("SUM(track.distance)", "sum_track").addSelect("SUM(_distance)", "sum_distance")
.getRawOne();
let distace = parseInt(distance_query.sum_track)
if (distance_query.sum_distance) {
distace += parseInt(distance_query.sum_distance)
}
let donations = await connection.getRepository(Donation).find({ relations: ['runner', 'runner.scans', 'runner.scans.track'] });
const donors = await connection.getRepository(Donor).count();
const result = new ResponseStats(runnersViaSelfservice, runners, teams, orgs, users, scans, donations, distace, donors, runnersViaKiosk);
// Store in cache for 60 seconds
await setStatsCache('overview', result);
return result;
}
@Get("/runners/distance")
@UseBefore(StatsAuth)
@ResponseSchema(ResponseStatsRunner, { isArray: true })
@OpenAPI({ description: "Returns the top ten runners by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopRunnersByDistance() {
// Try cache first
const cached = await getStatsCache<ResponseStatsRunner[]>('runners.distance');
if (cached) {
return cached;
}
// Cache miss - compute fresh stats
let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] });
if (!runners || runners.length == 0) {
return [];
}
let topRunners = runners.sort((runner1, runner2) => runner2.distance - runner1.distance).slice(0, 10);
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
topRunners.forEach(runner => {
responseRunners.push(new ResponseStatsRunner(runner));
});
// Store in cache for 60 seconds
await setStatsCache('runners.distance', responseRunners);
return responseRunners;
}
@Get("/runners/donations")
@UseBefore(StatsAuth)
@ResponseSchema(ResponseStatsRunner, { isArray: true })
@OpenAPI({ description: "Returns the top ten runners by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopRunnersByDonations() {
// Try cache first
const cached = await getStatsCache<ResponseStatsRunner[]>('runners.donations');
if (cached) {
return cached;
}
// Cache miss - compute fresh stats
let runners = await getConnection().getRepository(Runner).find({ relations: ['group', 'distanceDonations', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] });
if (!runners || runners.length == 0) {
return [];
}
let topRunners = runners.sort((runner1, runner2) => runner2.distanceDonationAmount - runner1.distanceDonationAmount).slice(0, 10);
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
topRunners.forEach(runner => {
responseRunners.push(new ResponseStatsRunner(runner));
});
// Store in cache for 60 seconds
await setStatsCache('runners.donations', responseRunners);
return responseRunners;
}
@Get("/runners/laptime")
@UseBefore(StatsAuth)
@ResponseSchema(ResponseStatsRunner, { isArray: true })
@OpenAPI({ description: "Returns the top ten runners by fastest laptime on your selected track (track by id).", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopRunnersByLaptime(@QueryParam("track") track: number) {
// Try cache first (cache key includes track id, using dots for NATS KV compatibility)
const cacheKey = `runners.laptime.${track}`;
const cached = await getStatsCache<ResponseStatsRunner[]>(cacheKey);
if (cached) {
return cached;
}
// Cache miss - compute fresh stats
let scans = await getConnection().getRepository(TrackScan).find({ relations: ['track', 'runner', 'runner.group', 'runner.scans', 'runner.scans.track', 'runner.distanceDonations'] });
if (!scans || scans.length == 0) {
return [];
}
scans = scans.filter((s) => { return s.track.id == track && s.valid == true && s.lapTime != 0 }).sort((scan1, scan2) => scan1.lapTime - scan2.lapTime);
let topScans = new Array<TrackScan>();
let knownRunners = new Array<number>();
for (let i = 0; i < scans.length && topScans.length < 10; i++) {
const element = scans[i];
if (!knownRunners.includes(element.runner.id)) {
topScans.push(element);
knownRunners.push(element.runner.id);
}
}
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
topScans.forEach(scan => {
responseRunners.push(new ResponseStatsRunner(scan.runner, scan.lapTime));
});
// Store in cache for 60 seconds
await setStatsCache(cacheKey, responseRunners);
return responseRunners;
}
@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() {
// Try cache first
const cached = await getStatsCache<ResponseStatsTeam[]>('teams.distance');
if (cached) {
return cached;
}
// Cache miss - compute fresh stats
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['parentGroup', 'runners', 'runners.scans', 'runners.scans.track'] });
if (!teams || teams.length == 0) {
return [];
}
let topTeams = teams.sort((team1, team2) => team2.distance - team1.distance).slice(0, 10);
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
topTeams.forEach(team => {
responseTeams.push(new ResponseStatsTeam(team));
});
// Store in cache for 60 seconds
await setStatsCache('teams.distance', responseTeams);
return responseTeams;
}
@Get("/teams/donations")
@UseBefore(StatsAuth)
@ResponseSchema(ResponseStatsTeam, { isArray: true })
@OpenAPI({ description: "Returns the top ten teams by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopTeamsByDonations() {
// Try cache first
const cached = await getStatsCache<ResponseStatsTeam[]>('teams.donations');
if (cached) {
return cached;
}
// Cache miss - compute fresh stats
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['parentGroup', 'runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
if (!teams || teams.length == 0) {
return [];
}
let topTeams = teams.sort((team1, team2) => team2.distanceDonationAmount - team1.distanceDonationAmount).slice(0, 10);
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
topTeams.forEach(team => {
responseTeams.push(new ResponseStatsTeam(team));
});
// Store in cache for 60 seconds
await setStatsCache('teams.donations', responseTeams);
return responseTeams;
}
@Get("/organizations/distance")
@UseBefore(StatsAuth)
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
@OpenAPI({ description: "Returns the top ten organizations by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopOrgsByDistance() {
// Try cache first
const cached = await getStatsCache<ResponseStatsOrgnisation[]>('organizations.distance');
if (cached) {
return cached;
}
// Cache miss - compute fresh stats
let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
if (!orgs || orgs.length == 0) {
return [];
}
let topOrgs = orgs.sort((org1, org2) => org2.distance - org1.distance).slice(0, 10);
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
topOrgs.forEach(org => {
responseOrgs.push(new ResponseStatsOrgnisation(org));
});
// Store in cache for 60 seconds
await setStatsCache('organizations.distance', responseOrgs);
return responseOrgs;
}
@Get("/organizations/donations")
@UseBefore(StatsAuth)
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
@OpenAPI({ description: "Returns the top ten organizations by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async getTopOrgsByDonations() {
// Try cache first
const cached = await getStatsCache<ResponseStatsOrgnisation[]>('organizations.donations');
if (cached) {
return cached;
}
// Cache miss - compute fresh stats
let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.distanceDonations', 'runners.distanceDonations.runner', 'runners.distanceDonations.runner.scans', 'runners.distanceDonations.runner.scans.track', 'teams', 'teams.runners', 'teams.runners.distanceDonations', 'teams.runners.distanceDonations.runner', 'teams.runners.distanceDonations.runner.scans', 'teams.runners.distanceDonations.runner.scans.track'] });
if (!orgs || orgs.length == 0) {
return [];
}
let topOrgs = orgs.sort((org1, org2) => org2.distanceDonationAmount - org1.distanceDonationAmount).slice(0, 10);
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
topOrgs.forEach(org => {
responseOrgs.push(new ResponseStatsOrgnisation(org));
});
// Store in cache for 60 seconds
await setStatsCache('organizations.donations', responseOrgs);
return responseOrgs;
}
}

View File

@@ -0,0 +1,31 @@
import { Get, JsonController } from 'routing-controllers';
import { OpenAPI } from 'routing-controllers-openapi';
import { getConnection } from 'typeorm';
import { config } from '../config';
@JsonController()
export class StatusController {
@Get('/status')
@OpenAPI({ description: "A very basic status/health endpoint that just checks if the database connection is available. <br> The available information depth will be expanded later." })
get() {
let connection;
try {
connection = getConnection();
} catch {
throw new Error("sth is wrong, i can feel it....");
}
return {
"controllers": "✔",
"database connection": "✔"
};
}
@Get('/version')
@OpenAPI({ description: "A very basic endpoint that just returns the curent package version." })
getVersion() {
return {
"version": config.version
}
}
}

View File

@@ -1,14 +1,16 @@
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { EntityFromBody, EntityFromParam } from 'typeorm-routing-controllers-extensions';
import { TrackIdsNotMatchingError, TrackNotFoundError } from "../errors/TrackErrors";
import { CreateTrack } from '../models/actions/CreateTrack';
import { Repository, getConnectionManager } from 'typeorm';
import { TrackHasScanStationsError, TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors";
import { CreateTrack } from '../models/actions/create/CreateTrack';
import { UpdateTrack } from '../models/actions/update/UpdateTrack';
import { Track } from '../models/entities/Track';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseTrack } from '../models/responses/ResponseTrack';
import { ScanStationController } from './ScanStationController';
@JsonController('/tracks')
//@Authorized("TRACKS:read")
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class TrackController {
private trackRepository: Repository<Track>;
@@ -20,11 +22,20 @@ export class TrackController {
}
@Get()
@Authorized("TRACK:GET")
@ResponseSchema(ResponseTrack, { isArray: true })
@OpenAPI({ description: "Lists all tracks." })
async getAll() {
@OpenAPI({ description: 'Lists all tracks.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseTracks: ResponseTrack[] = new Array<ResponseTrack>();
const tracks = await this.trackRepository.find();
let tracks: Array<Track>;
if (page != undefined) {
tracks = await this.trackRepository.find({ skip: page * page_size, take: page_size });
}
else {
tracks = await this.trackRepository.find();
}
tracks.forEach(track => {
responseTracks.push(new ResponseTrack(track));
});
@@ -32,50 +43,69 @@ export class TrackController {
}
@Get('/:id')
@Authorized("TRACK:GET")
@ResponseSchema(ResponseTrack)
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
@OnUndefined(TrackNotFoundError)
@OpenAPI({ description: "Returns a track of a specified id (if it exists)" })
@OpenAPI({ description: "Lists all information about the track whose id got provided." })
async getOne(@Param('id') id: number) {
return new ResponseTrack(await this.trackRepository.findOne({ id: id }));
let track = await this.trackRepository.findOne({ id: id });
if (!track) { throw new TrackNotFoundError(); }
return new ResponseTrack(track);
}
@Post()
@Authorized("TRACK:CREATE")
@ResponseSchema(ResponseTrack)
@OpenAPI({ description: "Create a new track object (id will be generated automagicly)." })
@ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 })
@OpenAPI({ description: "Create a new track. <br> Please remember that the track\'s distance must be greater than 0." })
async post(
@Body({ validate: true })
track: CreateTrack
) {
return new ResponseTrack(await this.trackRepository.save(track.toTrack()));
return new ResponseTrack(await this.trackRepository.save(await track.toEntity()));
}
@Put('/:id')
@Authorized("TRACK:UPDATE")
@ResponseSchema(ResponseTrack)
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
@ResponseSchema(TrackIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update a track object (id can't be changed)." })
async put(@Param('id') id: number, @EntityFromBody() track: Track) {
@ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 })
@OpenAPI({ description: "Update the track whose id you provided. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) updateTrack: UpdateTrack) {
let oldTrack = await this.trackRepository.findOne({ id: id });
if (!oldTrack) {
throw new TrackNotFoundError();
}
if (oldTrack.id != track.id) {
if (oldTrack.id != updateTrack.id) {
throw new TrackIdsNotMatchingError();
}
await this.trackRepository.save(await updateTrack.update(oldTrack));
await this.trackRepository.update(oldTrack, track);
return new ResponseTrack(track);
return new ResponseTrack(await this.trackRepository.findOne({ id: id }));
}
@Delete('/:id')
@Authorized("TRACK:DELETE")
@ResponseSchema(ResponseTrack)
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
@OpenAPI({ description: "Delete a specified track (if it exists)." })
async remove(@EntityFromParam('id') track: Track) {
if (!track) { throw new TrackNotFoundError(); }
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: "Delete the track whose id you provided. <br> If no track with this id exists it will just return 204(no content)." })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let track = await this.trackRepository.findOne({ id: id });
if (!track) { return null; }
const trackStations = (await this.trackRepository.findOne({ id: id }, { relations: ["stations"] })).stations;
if (trackStations.length != 0 && !force) {
throw new TrackHasScanStationsError();
}
const stationController = new ScanStationController;
for (let station of trackStations) {
await stationController.remove(station.id, force);
}
await this.trackRepository.delete(track);
return new ResponseTrack(track);

View File

@@ -1,85 +1,141 @@
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { EntityFromBody, EntityFromParam } from 'typeorm-routing-controllers-extensions';
import { UserIdsNotMatchingError, UserNotFoundError } from '../errors/UserErrors';
import { UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUser } from '../models/actions/CreateUser';
import { User } from '../models/entities/User';
@JsonController('/users')
export class UserController {
private userRepository: Repository<User>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.userRepository = getConnectionManager().get().getRepository(User);
}
@Get()
@ResponseSchema(User, { isArray: true })
@OpenAPI({ description: 'Lists all users.' })
getAll() {
return this.userRepository.find();
}
@Get('/:id')
@ResponseSchema(User)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Returns a user of a specified id (if it exists)' })
getOne(@Param('id') id: number) {
return this.userRepository.findOne({ id: id });
}
@Post()
@ResponseSchema(User)
@ResponseSchema(UserGroupNotFoundError)
@OpenAPI({ description: 'Create a new user object (id will be generated automagicly).' })
async post(@Body({ validate: true }) createUser: CreateUser) {
let user;
try {
user = await createUser.toUser();
} catch (error) {
return error;
}
return this.userRepository.save(user);
}
@Put('/:id')
@ResponseSchema(User)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update a user object (id can't be changed)." })
async put(@Param('id') id: number, @EntityFromBody() user: User) {
let oldUser = await this.userRepository.findOne({ id: id });
if (!oldUser) {
throw new UserNotFoundError();
}
if (oldUser.id != user.id) {
throw new UserIdsNotMatchingError();
}
await this.userRepository.update(oldUser, user);
return user;
}
@Delete('/:id')
@ResponseSchema(User)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Delete a specified runner (if it exists).' })
async remove(@EntityFromParam('id') user: User) {
if (!user) {
throw new UserNotFoundError();
}
await this.userRepository.delete(user);
return user;
}
}
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserDeletionNotConfirmedError, UserIdsNotMatchingError, UserNotFoundError, UsernameContainsIllegalCharacterError } from '../errors/UserErrors';
import { UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUser } from '../models/actions/create/CreateUser';
import { UpdateUser } from '../models/actions/update/UpdateUser';
import { User } from '../models/entities/User';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseUser } from '../models/responses/ResponseUser';
import { ResponseUserPermissions } from '../models/responses/ResponseUserPermissions';
import { PermissionController } from './PermissionController';
@JsonController('/users')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class UserController {
private userRepository: Repository<User>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.userRepository = getConnectionManager().get().getRepository(User);
}
@Get()
@Authorized("USER:GET")
@ResponseSchema(ResponseUser, { isArray: true })
@OpenAPI({ description: 'Lists all users. <br> This includes their groups and permissions granted to them.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseUsers: ResponseUser[] = new Array<ResponseUser>();
let users: Array<User>;
if (page != undefined) {
users = await this.userRepository.find({ relations: ['permissions', 'groups', 'groups.permissions'], skip: page * page_size, take: page_size });
}
else {
users = await this.userRepository.find({ relations: ['permissions', 'groups', 'groups.permissions'] });
}
users.forEach(user => {
responseUsers.push(new ResponseUser(user));
});
return responseUsers;
}
@Get('/:id')
@Authorized("USER:GET")
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Lists all information about the user whose id got provided. <br> Please remember that all permissions granted to the user will show up here.' })
async getOne(@Param('id') id: number) {
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] })
if (!user) { throw new UserNotFoundError(); }
return new ResponseUser(user);
}
@Get('/:id/permissions')
@Authorized("USER:GET")
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Lists all permissions granted to the user sorted into directly granted and inherited as permission response objects.' })
async getPermissions(@Param('id') id: number) {
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
if (!user) { throw new UserNotFoundError(); }
return new ResponseUserPermissions(user);
}
@Post()
@Authorized("USER:CREATE")
@ResponseSchema(ResponseUser)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
@OpenAPI({ description: 'Create a new user. <br> If you want to grant permissions to the user you have to create them seperately by posting to /api/permissions after creating the user.' })
async post(@Body({ validate: true }) createUser: CreateUser) {
let user;
try {
user = await createUser.toEntity();
} catch (error) {
throw error;
}
user = await this.userRepository.save(user)
return new ResponseUser(await this.userRepository.findOne({ id: user.id }, { relations: ['permissions', 'groups', 'groups.permissions'] }));
}
@Put('/:id')
@Authorized("USER:UPDATE")
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
@OpenAPI({ description: "Update the user whose id you provided. <br> To change the permissions directly granted to the user please use /api/permissions instead. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) updateUser: UpdateUser) {
let oldUser = await this.userRepository.findOne({ id: id });
if (!oldUser) {
throw new UserNotFoundError();
}
if (oldUser.id != updateUser.id) {
throw new UserIdsNotMatchingError();
}
await this.userRepository.save(await updateUser.update(oldUser));
return new ResponseUser(await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] }));
}
@Delete('/:id')
@Authorized("USER:DELETE")
@ResponseSchema(ResponseUser)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the user whose id you provided. <br> You have to confirm your decision by providing the ?force=true query param. <br> If there are any permissions directly granted to the user they will get deleted as well. <br> If no user with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
if (!force) { throw new UserDeletionNotConfirmedError; }
let user = await this.userRepository.findOne({ id: id });
if (!user) { return null; }
const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] });;
const permissionControler = new PermissionController();
for (let permission of responseUser.permissions) {
await permissionControler.remove(permission.id, true);
}
await this.userRepository.delete(user);
return new ResponseUser(responseUser);
}
}

View File

@@ -1,84 +1,125 @@
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { EntityFromBody, EntityFromParam } from 'typeorm-routing-controllers-extensions';
import { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUserGroup } from '../models/actions/CreateUserGroup';
import { UserGroup } from '../models/entities/UserGroup';
@JsonController('/usergroups')
export class UserGroupController {
private userGroupsRepository: Repository<UserGroup>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.userGroupsRepository = getConnectionManager().get().getRepository(UserGroup);
}
@Get()
@ResponseSchema(UserGroup, { isArray: true })
@OpenAPI({ description: 'Lists all usergroups.' })
getAll() {
return this.userGroupsRepository.find();
}
@Get('/:id')
@ResponseSchema(UserGroup)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@OnUndefined(UserGroupNotFoundError)
@OpenAPI({ description: 'Returns a usergroup of a specified id (if it exists)' })
getOne(@Param('id') id: number) {
return this.userGroupsRepository.findOne({ id: id });
}
@Post()
@ResponseSchema(UserGroup)
@ResponseSchema(UserGroupNotFoundError)
@OpenAPI({ description: 'Create a new usergroup object (id will be generated automagicly).' })
async post(@Body({ validate: true }) createUserGroup: CreateUserGroup) {
let userGroup;
try {
userGroup = await createUserGroup.toUserGroup();
} catch (error) {
return error;
}
return this.userGroupsRepository.save(userGroup);
}
@Put('/:id')
@ResponseSchema(UserGroup)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(UserGroupIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update a usergroup object (id can't be changed)." })
async put(@Param('id') id: number, @EntityFromBody() userGroup: UserGroup) {
let oldUserGroup = await this.userGroupsRepository.findOne({ id: id });
if (!oldUserGroup) {
throw new UserGroupNotFoundError()
}
if (oldUserGroup.id != userGroup.id) {
throw new UserGroupIdsNotMatchingError();
}
await this.userGroupsRepository.update(oldUserGroup, userGroup);
return userGroup;
}
@Delete('/:id')
@ResponseSchema(UserGroup)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Delete a specified usergroup (if it exists).' })
async remove(@EntityFromParam('id') group: UserGroup) {
if (!group) {
throw new UserGroupNotFoundError();
}
await this.userGroupsRepository.delete(group);
return group;
}
}
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUserGroup } from '../models/actions/create/CreateUserGroup';
import { UpdateUserGroup } from '../models/actions/update/UpdateUserGroup';
import { UserGroup } from '../models/entities/UserGroup';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseUserGroup } from '../models/responses/ResponseUserGroup';
import { ResponseUserGroupPermissions } from '../models/responses/ResponseUserGroupPermissions';
import { PermissionController } from './PermissionController';
@JsonController('/usergroups')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class UserGroupController {
private userGroupsRepository: Repository<UserGroup>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.userGroupsRepository = getConnectionManager().get().getRepository(UserGroup);
}
@Get()
@Authorized("USERGROUP:GET")
@ResponseSchema(ResponseUserGroup, { isArray: true })
@OpenAPI({ description: 'Lists all groups. <br> The information provided might change while the project continues to evolve.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseGroups: ResponseUserGroup[] = new Array<ResponseUserGroup>();
let groups: Array<UserGroup>;
if (page != undefined) {
groups = await this.userGroupsRepository.find({ relations: ['permissions'], skip: page * page_size, take: page_size });
} else {
groups = await this.userGroupsRepository.find({ relations: ['permissions'] });
}
groups.forEach(group => {
responseGroups.push(group.toResponse());
});
return responseGroups;
}
@Get('/:id')
@Authorized("USERGROUP:GET")
@ResponseSchema(ResponseUserGroup)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@OnUndefined(UserGroupNotFoundError)
@OpenAPI({ description: 'Lists all information about the group whose id got provided. <br> The information provided might change while the project continues to evolve.' })
async getOne(@Param('id') id: number) {
return await (await (this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] }))).toResponse();
}
@Get('/:id/permissions')
@Authorized("USERGROUP:GET")
@ResponseSchema(ResponseUserGroupPermissions)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@OnUndefined(UserGroupNotFoundError)
@OpenAPI({ description: 'Lists all permissions granted to the group as permission response objects.' })
async getPermissions(@Param('id') id: number) {
let group = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions', 'permissions.principal'] })
if (!group) { throw new UserGroupNotFoundError(); }
return new ResponseUserGroupPermissions(group);
}
@Post()
@Authorized("USERGROUP:CREATE")
@ResponseSchema(UserGroup)
@ResponseSchema(UserGroupNotFoundError)
@OpenAPI({ description: 'Create a new group. <br> If you want to grant permissions to the group you have to create them seperately by posting to /api/permissions after creating the group.' })
async post(@Body({ validate: true }) createUserGroup: CreateUserGroup) {
let userGroup;
try {
userGroup = await createUserGroup.toEntity();
} catch (error) {
throw error;
}
userGroup = await this.userGroupsRepository.save(userGroup);
return (await (this.userGroupsRepository.findOne({ id: userGroup.id }, { relations: ["permissions"] }))).toResponse();
}
@Put('/:id')
@Authorized("USERGROUP:UPDATE")
@ResponseSchema(UserGroup)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(UserGroupIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the group whose id you provided. <br> To change the permissions granted to the group please use /api/permissions instead. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) updateGroup: UpdateUserGroup) {
let oldGroup = await this.userGroupsRepository.findOne({ id: id });
if (!oldGroup) {
throw new UserGroupNotFoundError();
}
if (oldGroup.id != updateGroup.id) {
throw new UserGroupIdsNotMatchingError();
}
await this.userGroupsRepository.save(await updateGroup.update(oldGroup));
return (await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] })).toResponse();
}
@Delete('/:id')
@Authorized("USERGROUP:DELETE")
@ResponseSchema(ResponseUserGroup)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the group whose id you provided. <br> If there are any permissions directly granted to the group they will get deleted as well. <br> Users associated with this group won\'t get deleted - just deassociated. <br> If no group with this id exists it will just return 204(no content).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let group = await this.userGroupsRepository.findOne({ id: id });
if (!group) { return null; }
const responseGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] });
const permissionController = new PermissionController();
for (let permission of responseGroup.permissions) {
await permissionController.remove(permission.id, true);
}
await this.userGroupsRepository.delete(group);
return new ResponseUserGroup(responseGroup);
}
}

View File

@@ -1,24 +1,57 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
import { BadRequestError } from 'routing-controllers';
/**
* Error to throw, when to provided address doesn't belong to the accepted types.
* Error to throw when an address's postal code fails validation.
*/
export class AddressWrongTypeError extends NotAcceptableError {
export class AddressPostalCodeInvalidError extends BadRequestError {
@IsString()
name = "AddressWrongTypeError"
name = "AddressPostalCodeInvalidError"
@IsString()
message = "The address must be an existing adress's id. \n You provided a object of another type."
message = "The postal code you provided is invalid. \n Please check if your postal code follows the postal code validation guidelines."
}
/**
* Error to throw, when a non-existant address get's loaded.
* Error to throw when an non-empty address's first line isn't set.
*/
export class AddressNotFoundError extends NotFoundError {
export class AddressFirstLineEmptyError extends BadRequestError {
@IsString()
name = "AddressNotFoundError"
name = "AddressFirstLineEmptyError"
@IsString()
message = "The address you provided couldn't be located in the system. \n Please check your request."
message = "You provided a empty first address line. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
}
/**
* Error to throw when an non-empty address's postal code isn't set.
*/
export class AddressPostalCodeEmptyError extends BadRequestError {
@IsString()
name = "AddressPostalCodeEmptyError"
@IsString()
message = "You provided a empty postal code. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
}
/**
* Error to throw when an non-empty address's city isn't set.
*/
export class AddressCityEmptyError extends BadRequestError {
@IsString()
name = "AddressCityEmptyError"
@IsString()
message = "You provided a empty city. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
}
/**
* Error to throw when an non-empty address's country isn't set.
*/
export class AddressCountryEmptyError extends BadRequestError {
@IsString()
name = "AddressCountryEmptyError"
@IsString()
message = "You provided a empty country. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
}

View File

@@ -1,63 +1,57 @@
import { IsString } from 'class-validator';
import { ForbiddenError, NotAcceptableError, NotFoundError, UnauthorizedError } from 'routing-controllers';
/**
* Error to throw when a jwt is expired.
*/
export class ExpiredJWTError extends UnauthorizedError {
@IsString()
name = "ExpiredJWTError"
@IsString()
message = "your provided jwt is expired"
}
/**
* Error to throw when a jwt could not be parsed.
* For example: Wrong signature or expired.
*/
export class IllegalJWTError extends UnauthorizedError {
@IsString()
name = "IllegalJWTError"
@IsString()
message = "your provided jwt could not be parsed"
message = "Your provided jwt could not be parsed."
}
/**
* Error to throw when user is nonexistant or refreshtoken is invalid.
* This can happen if someone provides a JWT with a invalid user id or the refreshTokenCount of the user is higher that the provided jwt's is.
*/
export class UserNonexistantOrRefreshtokenInvalidError extends UnauthorizedError {
@IsString()
name = "UserNonexistantOrRefreshtokenInvalidError"
@IsString()
message = "user is nonexistant or refreshtoken is invalid"
message = "User is nonexistant or refreshtoken is invalid."
}
/**
* Error to throw when provided credentials are invalid.
* We don't have seperate errors for username/mail and passwords to protect against guessing attacks.
*/
export class InvalidCredentialsError extends UnauthorizedError {
@IsString()
name = "InvalidCredentialsError"
@IsString()
message = "your provided credentials are invalid"
message = "Your provided credentials are invalid."
}
/**
* Error to throw when a jwt does not have permission for this route/action.
* Mainly used be the @Authorized decorator (via the authchecker).
*/
export class NoPermissionError extends ForbiddenError {
@IsString()
name = "NoPermissionError"
@IsString()
message = "your provided jwt does not have permission for this route/ action"
message = "Your provided jwt does not have permission for this route/ action."
}
/**
* Error to throw when no username and no email is set.
* Because we have to identify users somehow.
*/
export class UsernameOrEmailNeededError extends NotAcceptableError {
@IsString()
@@ -68,56 +62,79 @@ export class UsernameOrEmailNeededError extends NotAcceptableError {
}
/**
* Error to throw when no password is provided.
* Error to throw when no password is provided for a new user.
* Passwords are the minimum we need for user security.
*/
export class PasswordNeededError extends NotAcceptableError {
@IsString()
name = "PasswordNeededError"
@IsString()
message = "no password is provided - you need to provide it"
message = "No password is provided - you need to provide it."
}
/**
* Error to throw when no user could be found mating the provided credential.
* Error to throw when no user could be found for a certain query.
*/
export class UserNotFoundError extends NotFoundError {
@IsString()
name = "UserNotFoundError"
@IsString()
message = "no user could be found for provided credential"
message = "The user you provided couldn't be located in the system. \n Please check your request."
}
/**
* Error to throw when no jwt token was provided (but one had to be).
* Error to throw when no jwt was provided (but one had to be).
*/
export class JwtNotProvidedError extends NotAcceptableError {
@IsString()
name = "JwtNotProvidedError"
@IsString()
message = "no jwt token was provided"
message = "No jwt was provided."
}
/**
* Error to throw when user was not found or refresh token count was invalid.
* Error to throw when user was not found or the jwt's refresh token count was invalid.
*/
export class UserNotFoundOrRefreshTokenCountInvalidError extends NotAcceptableError {
@IsString()
name = "UserNotFoundOrRefreshTokenCountInvalidError"
@IsString()
message = "user was not found or refresh token count was invalid"
message = "User was not found or the refresh token count is invalid."
}
/**
* Error to thow when refresh token count was invalid
* Error to throw when refresh token count was invalid
*/
export class RefreshTokenCountInvalidError extends NotAcceptableError {
@IsString()
name = "RefreshTokenCountInvalidError"
@IsString()
message = "refresh token count was invalid"
message = "Refresh token count is invalid."
}
/**
* Error to throw when someone tries to reset a user's password more than once in 15 minutes.
*/
export class ResetAlreadyRequestedError extends NotAcceptableError {
@IsString()
name = "ResetAlreadyRequestedError"
@IsString()
message = "You already requested a password reset in the last 15 minutes. \n Please wait until the old reset code expires before requesting a new one."
}
/**
* Error to throw when someone tries a disabled user's password or login as a disabled user.
*/
export class UserDisabledError extends NotAcceptableError {
@IsString()
name = "UserDisabledError"
@IsString()
message = "This user is currently disabled. \n Please contact your administrator if this is a mistake."
}

View File

@@ -0,0 +1,25 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a Donation couldn't be found.
*/
export class DonationNotFoundError extends NotFoundError {
@IsString()
name = "DonationNotFoundError"
@IsString()
message = "Donation not found!"
}
/**
* Error to throw when two Donations' ids don't match.
* Usually occurs when a user tries to change a Donation's id.
*/
export class DonationIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "DonationIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a Donation's id: This isn't allowed!"
}

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

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

View File

@@ -2,18 +2,7 @@ import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw, when a provided groupContact doesn't belong to the accepted types.
*/
export class GroupContactWrongTypeError extends NotAcceptableError {
@IsString()
name = "GroupContactWrongTypeError"
@IsString()
message = "The groupContact must be an existing groupContact's id. \n You provided a object of another type."
}
/**
* Error to throw, when a non-existant groupContact get's loaded.
* Error to throw, when a non-existent contact get's requested.
*/
export class GroupContactNotFoundError extends NotFoundError {
@IsString()
@@ -21,4 +10,16 @@ export class GroupContactNotFoundError extends NotFoundError {
@IsString()
message = "The groupContact you provided couldn't be located in the system. \n Please check your request."
}
}
/**
* Error to throw when two contacts' ids don't match.
* Usually occurs when a user tries to change a contact's id.
*/
export class GroupContactIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "GroupContactIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a contact's id: This isn't allowed!"
}

17
src/errors/MailErrors.ts Normal file
View File

@@ -0,0 +1,17 @@
import { IsString } from 'class-validator';
import { InternalServerError } from 'routing-controllers';
/**
* Error to throw when a permission couldn't be found.
*/
export class MailSendingError extends InternalServerError {
@IsString()
name = "MailSendingError"
@IsString()
message = "We had a problem sending the mail!"
constructor() {
super("We had a problem sending the mail!");
}
}

View File

@@ -0,0 +1,36 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a permission couldn't be found.
*/
export class PermissionNotFoundError extends NotFoundError {
@IsString()
name = "PermissionNotFoundError"
@IsString()
message = "Permission not found!"
}
/**
* Error to throw when two permissions' ids don't match.
* Usually occurs when a user tries to change a permission's id.
*/
export class PermissionIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "PermissionIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a permission's id: This isn't allowed!"
}
/**
* Error to throw when a permission gets provided without a principal.
*/
export class PermissionNeedsPrincipalError extends NotAcceptableError {
@IsString()
name = "PermissionNeedsPrincipalError"
@IsString()
message = "You provided no principal for this permission."
}

View File

@@ -0,0 +1,24 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a user couldn't be found.
*/
export class PrincipalNotFoundError extends NotFoundError {
@IsString()
name = "PrincipalNotFoundError"
@IsString()
message = "Principal not found!"
}
/**
* Error to throw, when a provided runner organization doesn't belong to the accepted types.
*/
export class PrincipalWrongTypeError extends NotAcceptableError {
@IsString()
name = "PrincipalWrongTypeError"
@IsString()
message = "The principal must have an existing principal's id. \n You provided a object of another type."
}

View File

@@ -0,0 +1,48 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a card couldn't be found.
*/
export class RunnerCardNotFoundError extends NotFoundError {
@IsString()
name = "RunnerCardNotFoundError"
@IsString()
message = "Card not found!"
}
/**
* Error to throw when two cards' ids don't match.
* Usually occurs when a user tries to change a card's id.
*/
export class RunnerCardIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "RunnerCardIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a cards's id: This isn't allowed"
}
/**
* Error to throw when a card still has scans associated.
*/
export class RunnerCardHasScansError extends NotAcceptableError {
@IsString()
name = "RunnerCardHasScansError"
@IsString()
message = "This card still has scans associated with it. \n If you want to delete this card with all it's scans add `?force` to your query. \n Otherwise please consider just disabling it."
}
/**
* Error to throw when a card's id is too big to generate a ean-13 barcode for it.
* This error should never reach a end user.
*/
export class RunnerCardIdOutOfRangeError extends Error {
@IsString()
name = "RunnerCardIdOutOfRangeError"
@IsString()
message = "The card's id is too big to fit into a ean-13 barcode. \n This has a very low probability of happening but means that you might want to switch your barcode format for something that can accept numbers over 9999999999."
}

View File

@@ -3,7 +3,6 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a runner couldn't be found.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class RunnerNotFoundError extends NotFoundError {
@IsString()
@@ -16,14 +15,13 @@ export class RunnerNotFoundError extends NotFoundError {
/**
* Error to throw when two runners' ids don't match.
* Usually occurs when a user tries to change a runner's id.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class RunnerIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "RunnerIdsNotMatchingError"
@IsString()
message = "The id's don't match!! \n And if you wanted to change a runner's id: This isn't allowed"
message = "The ids don't match! \n And if you wanted to change a runner's id: This isn't allowed!"
}
/**
@@ -34,5 +32,38 @@ export class RunnerGroupNeededError extends NotAcceptableError {
name = "RunnerGroupNeededError"
@IsString()
message = "Runner's need to be part of one group (team or organisiation)! \n You provided neither."
message = "Runner's need to be part of one group (team or organization)! \n You provided neither."
}
/**
* Error to throw when a citizen runner has no mail-address.
*/
export class RunnerEmailNeededError extends NotAcceptableError {
@IsString()
name = "RunnerEmailNeededError"
@IsString()
message = "Citizenrunners have to provide an email address for verification and contacting."
}
/**
* Error to throw when a runner already requested a new selfservice link in the last 30s.
*/
export class RunnerSelfserviceTimeoutError extends NotAcceptableError {
@IsString()
name = "RunnerSelfserviceTimeoutError"
@IsString()
message = "You can only reqest a new token every 30s."
}
/**
* Error to throw when a runner still has distance donations associated.
*/
export class RunnerHasDistanceDonationsError extends NotAcceptableError {
@IsString()
name = "RunnerHasDistanceDonationsError"
@IsString()
message = "This runner still has distance donations associated with it. \n If you want to delete this runner with all it's donations and teams add `?force` to your query."
}

View File

@@ -3,7 +3,6 @@ import { NotFoundError } from 'routing-controllers';
/**
* Error to throw when a runner group couldn't be found.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class RunnerGroupNotFoundError extends NotFoundError {
@IsString()

View File

@@ -1,62 +0,0 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a runner organisation couldn't be found.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class RunnerOrganisationNotFoundError extends NotFoundError {
@IsString()
name = "RunnerOrganisationNotFoundError"
@IsString()
message = "RunnerOrganisation not found!"
}
/**
* Error to throw when two runner organisations' ids don't match.
* Usually occurs when a user tries to change a runner's id.
* Implemented this way to work with the json-schema conversion for openapi.
*/
export class RunnerOrganisationIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "RunnerOrganisationIdsNotMatchingError"
@IsString()
message = "The id's don't match!! \n And if you wanted to change a runner's id: This isn't allowed"
}
/**
* Error to throw when a organisation still has runners associated.
* Implemented this waysto work with the json-schema conversion for openapi.
*/
export class RunnerOrganisationHasRunnersError extends NotAcceptableError {
@IsString()
name = "RunnerOrganisationHasRunnersError"
@IsString()
message = "This organisation still has runners associated with it. \n If you want to delete this organisation with all it's runners and teams ass `?force` to your query."
}
/**
* Error to throw when a organisation still has runners associated.
* Implemented this waysto work with the json-schema conversion for openapi.
*/
export class RunnerOrganisationHasTeamsError extends NotAcceptableError {
@IsString()
name = "RunnerOrganisationHasTeamsError"
@IsString()
message = "This organisation still has teams associated with it. \n If you want to delete this organisation with all it's runners and teams ass `?force` to your query."
}
/**
* Error to throw, when a provided runnerOrganisation doesn't belong to the accepted types.
*/
export class RunnerOrganisationWrongTypeError extends NotAcceptableError {
@IsString()
name = "RunnerOrganisationWrongTypeError"
@IsString()
message = "The runner organisation must be an existing organisation's id. \n You provided a object of another type."
}

View File

@@ -0,0 +1,58 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a runner organization couldn't be found.
*/
export class RunnerOrganizationNotFoundError extends NotFoundError {
@IsString()
name = "RunnerOrganizationNotFoundError"
@IsString()
message = "RunnerOrganization not found!"
}
/**
* Error to throw when two runner organization's ids don't match.
* Usually occurs when a user tries to change a runner organization's id.
*/
export class RunnerOrganizationIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "RunnerOrganizationIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a runner organization's id: This isn't allowed!"
}
/**
* Error to throw when a organization still has runners associated.
*/
export class RunnerOrganizationHasRunnersError extends NotAcceptableError {
@IsString()
name = "RunnerOrganizationHasRunnersError"
@IsString()
message = "This organization still has runners associated with it. \n If you want to delete this organization with all it's runners and teams add `?force` to your query."
}
/**
* Error to throw when a organization still has teams associated.
*/
export class RunnerOrganizationHasTeamsError extends NotAcceptableError {
@IsString()
name = "RunnerOrganizationHasTeamsError"
@IsString()
message = "This organization still has teams associated with it. \n If you want to delete this organization with all it's runners and teams add `?force` to your query."
}
/**
* Error to throw, when a provided runnerOrganization doesn't belong to the accepted types.
*/
export class RunnerOrganizationWrongTypeError extends NotAcceptableError {
@IsString()
name = "RunnerOrganizationWrongTypeError"
@IsString()
message = "The runner organization must be an existing organization's id. \n You provided a object of another type."
}

View File

@@ -3,7 +3,6 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a runner team couldn't be found.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class RunnerTeamNotFoundError extends NotFoundError {
@IsString()
@@ -15,37 +14,34 @@ export class RunnerTeamNotFoundError extends NotFoundError {
/**
* Error to throw when two runner teams' ids don't match.
* Usually occurs when a user tries to change a runner's id.
* Implemented this way to work with the json-schema conversion for openapi.
* Usually occurs when a user tries to change a runner team's id.
*/
export class RunnerTeamIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "RunnerTeamIdsNotMatchingError"
@IsString()
message = "The id's don't match!! \n And if you wanted to change a runner's id: This isn't allowed"
message = "The ids don't match! \n And if you wanted to change a runner's id: This isn't allowed!"
}
/**
* Error to throw when a team still has runners associated.
* Implemented this waysto work with the json-schema conversion for openapi.
*/
export class RunnerTeamHasRunnersError extends NotAcceptableError {
@IsString()
name = "RunnerTeamHasRunnersError"
@IsString()
message = "This team still has runners associated with it. \n If you want to delete this team with all it's runners and teams ass `?force` to your query."
message = "This team still has runners associated with it. \n If you want to delete this team with all it's runners and teams add `?force` to your query."
}
/**
* Error to throw when a team still has runners associated.
* Implemented this waysto work with the json-schema conversion for openapi.
*/
export class RunnerTeamNeedsParentError extends NotAcceptableError {
@IsString()
name = "RunnerTeamNeedsParentError"
@IsString()
message = "You provided no runner organisation as this team's parent group."
message = "You provided no runner organization as this team's parent group."
}

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-existent scan station get's loaded.
*/
export class ScanStationNotFoundError extends NotFoundError {
@IsString()
name = "ScanStationNotFoundError"
@IsString()
message = "The scan station you provided couldn't be located in the system. \n Please check your request."
}
/**
* Error to throw when two scan stations' ids don't match.
* Usually occurs when a user tries to change a scan station's id.
*/
export class ScanStationIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "ScanStationIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a scan station's id: This isn't allowed!"
}
/**
* Error to throw when a station still has scans associated.
*/
export class ScanStationHasScansError extends NotAcceptableError {
@IsString()
name = "ScanStationHasScansError"
@IsString()
message = "This station still has scans associated with it. \n If you want to delete this station with all it's scans add `?force` to your query."
}

View File

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

View File

@@ -1,9 +1,8 @@
import { JsonController, Param, Body, Get, Post, Put, Delete, NotFoundError, OnUndefined, NotAcceptableError } from 'routing-controllers';
import { IsInt, IsNotEmpty, IsPositive, IsString } from 'class-validator';
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a track couldn't be found.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class TrackNotFoundError extends NotFoundError {
@IsString()
@@ -16,12 +15,30 @@ export class TrackNotFoundError extends NotFoundError {
/**
* Error to throw when two tracks' ids don't match.
* Usually occurs when a user tries to change a track's id.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class TrackIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "TrackIdsNotMatchingError"
@IsString()
message = "The id's don't match!! \n And if you wanted to change a track's id: This isn't allowed"
message = "The ids don't match! \n And if you wanted to change a track's id: This isn't allowed"
}
/**
* Error to throw when a track's lap time is set to a negative value.
*/
export class TrackLapTimeCantBeNegativeError extends NotAcceptableError {
@IsString()
name = "TrackLapTimeCantBeNegativeError"
@IsString()
message = "The minimum lap time you provided is negative - That isn't possible. \n If you wanted to disable it: Just set it to 0/null."
}
export class TrackHasScanStationsError extends NotAcceptableError {
@IsString()
name = "TrackHasScanStationsError"
@IsString()
message = "This track still has stations associated with it. \n If you want to delete this track with all it's stations and scans add `?force` to your query."
}

View File

@@ -3,14 +3,39 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when no username or email is set
* Error to throw when no username or email is set.
* We somehow need to identify you on login.
*/
export class UsernameOrEmailNeededError extends NotFoundError {
@IsString()
name = "UsernameOrEmailNeededError"
@IsString()
message = "no username or email is set!"
message = "No username or email is set!"
}
/**
* Error to throw when no username contains illegal characters.
* Right now the only one is "@" but this could change in the future.
*/
export class UsernameContainsIllegalCharacterError extends NotAcceptableError {
@IsString()
name = "UsernameContainsIllegalCharacterError"
@IsString()
message = "The provided username contains illegal characters! \n Right now the following characters are considered illegal: '@'"
}
/**
* Error to throw when no email is set.
* We somehow need to identify you :)
*/
export class UserEmailNeededError extends NotFoundError {
@IsString()
name = "UserEmailNeededError"
@IsString()
message = "No email is set! \n You have to provide email addresses for users (used for password reset among others)."
}
/**
@@ -33,5 +58,46 @@ export class UserIdsNotMatchingError extends NotAcceptableError {
name = "UserIdsNotMatchingError"
@IsString()
message = "The id's don't match!! \n And if you wanted to change a user's id: This isn't allowed"
message = "The ids don't match!! \n And if you wanted to change a user's id: This isn't allowed!"
}
/**
* Error to throw when two users' ids don't match.
* Usually occurs when a user tries to change a user's id.
*/
export class UserDeletionNotConfirmedError extends NotAcceptableError {
@IsString()
name = "UserDeletionNotConfirmedError"
@IsString()
message = "You are trying to delete a user! \n If you're sure about doing this: provide the ?force=true query param."
}
export class PasswordMustContainUppercaseLetterError extends NotAcceptableError {
@IsString()
name = "PasswordMustContainUppercaseLetterError"
@IsString()
message = "Passwords must contain at least one uppercase letter."
}
export class PasswordMustContainLowercaseLetterError extends NotAcceptableError {
@IsString()
name = "PasswordMustContainLowercaseLetterError"
@IsString()
message = "Passwords must contain at least one lowercase letter."
}
export class PasswordMustContainNumberError extends NotAcceptableError {
@IsString()
name = "PasswordMustContainNumberError"
@IsString()
message = "Passwords must contain at least one number."
}
export class PasswordTooShortError extends NotAcceptableError {
@IsString()
name = "PasswordTooShortError"
@IsString()
message = "Passwords must be at least ten characters long."
}

View File

@@ -2,18 +2,18 @@ import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when no groupname is set
* Error to throw when no group name is set.
*/
export class GroupNameNeededError extends NotFoundError {
@IsString()
name = "GroupNameNeededError"
@IsString()
message = "no groupname is set!"
message = "No name is set for this group!"
}
/**
* Error to throw when a usergroup couldn't be found.
* Error to throw when a user group couldn't be found.
*/
export class UserGroupNotFoundError extends NotFoundError {
@IsString()
@@ -24,13 +24,13 @@ export class UserGroupNotFoundError extends NotFoundError {
}
/**
* Error to throw when two usergroups' ids don't match.
* Usually occurs when a user tries to change a usergroups's id.
* Error to throw when two user groups' ids don't match.
* Usually occurs when a user tries to change a user groups's id.
*/
export class UserGroupIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "UserGroupIdsNotMatchingError"
@IsString()
message = "The id's don't match!! \n If you wanted to change a usergroup's id: This isn't allowed"
message = "The ids don't match!! \n If you wanted to change a user group's id: This isn't allowed!"
}

125
src/jwtcreator.ts Normal file
View File

@@ -0,0 +1,125 @@
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import * as jsonwebtoken from "jsonwebtoken";
import { config } from './config';
import { Runner } from './models/entities/Runner';
import { User } from './models/entities/User';
/**
* This class is responsible for all things JWT creation.
*/
export class JwtCreator {
/**
* Creates a new refresh token for a given user
* @param user User entity that the refresh token shall be created for
* @param expiry_timestamp Timestamp for the token expiry. Will be generated if not provided.
*/
public static createRefresh(user: User, expiry_timestamp?: number) {
if (!expiry_timestamp) { expiry_timestamp = Math.floor(Date.now() / 1000) + 10 * 36000; }
return jsonwebtoken.sign({
refreshTokenCount: user.refreshTokenCount,
id: user.id,
exp: expiry_timestamp
}, config.jwt_secret)
}
/**
* Creates a new access token for a given user
* @param user User entity that the access token shall be created for
* @param expiry_timestamp Timestamp for the token expiry. Will be generated if not provided.
*/
public static createAccess(user: User, expiry_timestamp?: number) {
if (!expiry_timestamp) { expiry_timestamp = Math.floor(Date.now() / 1000) + 10 * 36000; }
return jsonwebtoken.sign({
userdetails: new JwtUser(user),
exp: expiry_timestamp
}, config.jwt_secret)
}
/**
* Creates a new selfservice token for a given runner.
* @param runner Runner entity that the access token shall be created for.
* @param expiry_timestamp Timestamp for the token expiry. Will be set about 9999 years if none provided.
*/
public static createSelfService(runner: Runner, expiry_timestamp?: number) {
if (!expiry_timestamp) { expiry_timestamp = Math.floor(Date.now() / 1000) + 36000 * 60 * 24 * 365 * 9999; }
return jsonwebtoken.sign({
id: runner.id,
exp: expiry_timestamp
}, config.jwt_secret)
}
/**
* Creates a new password reset token for a given user.
* The token is valid for 15 minutes or 1 use - whatever comes first.
* @param user User entity that the password reset token shall be created for
*/
public static createReset(user: User) {
let expiry_timestamp = Math.floor(Date.now() / 1000) + 15 * 60;
return jsonwebtoken.sign({
id: user.id,
refreshTokenCount: user.refreshTokenCount,
exp: expiry_timestamp
}, config.jwt_secret)
}
}
/**
* Special variant of the user class that
*/
export class JwtUser {
@IsInt()
id: number;
@IsUUID(4)
uuid: string;
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@IsString()
username?: string;
@IsString()
@IsNotEmpty()
firstname: string;
@IsString()
@IsOptional()
middlename?: string;
@IsString()
@IsNotEmpty()
lastname: string;
permissions: string[];
@IsBoolean()
enabled: boolean;
@IsInt()
@IsNotEmpty()
refreshTokenCount?: number;
@IsString()
@IsOptional()
profilePic?: string;
/**
* Creates a new instance of this class based on a provided user entity.
* @param user User entity that shall be encapsulated in a jwt.
*/
public constructor(user: User) {
this.id = user.id;
this.firstname = user.firstname;
this.middlename = user.middlename;
this.lastname = user.lastname;
this.username = user.username;
this.email = user.email;
this.refreshTokenCount = user.refreshTokenCount;
this.uuid = user.uuid;
this.profilePic = user.profilePic;
this.permissions = user.allPermissions;
}
}

View File

@@ -1,7 +1,37 @@
import { createConnection } from "typeorm";
import { runSeeder } from 'typeorm-seeding';
import consola from 'consola';
import { config } from '../config';
import { ConfigFlag } from '../models/entities/ConfigFlags';
import SeedPublicOrg from '../seeds/SeedPublicOrg';
import SeedTestRunners from '../seeds/SeedTestRunners';
import SeedUsers from '../seeds/SeedUsers';
/**
* Loader for the database that creates the database connection and initializes the database tabels.
* It also triggers the seeding process if no users got detected in the database.
*/
export default async () => {
const connection = await createConnection();
connection.synchronize();
// Log discovered entities for debugging
consola.info(`TypeORM discovered ${connection.entityMetadatas.length} entities:`);
consola.info(connection.entityMetadatas.map(m => m.name).sort().join(', '));
await connection.synchronize();
//The data seeding part
if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:user", value: "true" }))) {
await runSeeder(SeedUsers);
await connection.getRepository(ConfigFlag).save({ option: "seeded:user", value: "true" });
}
if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:citizenorg", value: "true" }))) {
await runSeeder(SeedPublicOrg);
await connection.getRepository(ConfigFlag).save({ option: "seeded:citizenorg", value: "true" });
}
if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:testdata", value: "true" })) && config.seedTestData == true) {
await runSeeder(SeedTestRunners);
await connection.getRepository(ConfigFlag).save({ option: "seeded:testdata", value: "true" });
}
return connection;
};

View File

@@ -1,8 +1,13 @@
import cookieParser from "cookie-parser";
import { Application } from "express";
import bodyParser from 'body-parser';
import cors from 'cors';
/**
* Loader for express related configurations.
* Configures proxy trusts, globally used middlewares and other express features.
*/
export default async (app: Application) => {
app.enable('trust proxy');
app.disable('x-powered-by');
app.disable('x-served-by');
app.use(cookieParser());
return app;
};

View File

@@ -1,10 +1,28 @@
import { Application } from "express";
import consola from "consola";
import { config } from "../config";
import NatsClient from "../nats/NatsClient";
import { warmAll } from "../nats/RunnerKV";
import databaseLoader from "./database";
import expressLoader from "./express";
import openapiLoader from "./openapi";
import databaseLoader from "./database";
import { Application } from "express";
/**
* Index Loader that executes the other loaders in the right order.
* This basicly exists for abstraction and a overall better dev experience.
*/
export default async (app: Application) => {
await databaseLoader();
await NatsClient.connect();
if (config.nats_prewarm) {
consola.info("Prewarming NATS runner cache...");
const startTime = Date.now();
await warmAll();
const duration = Date.now() - startTime;
consola.success(`NATS runner cache prewarmed in ${duration}ms`);
}
await openapiLoader(app);
await expressLoader(app);
return app;

View File

@@ -1,47 +1,24 @@
import { validationMetadatasToSchemas } from "class-validator-jsonschema";
import { Application } from "express";
import { validationMetadatasToSchemas } from "@odit/class-validator-jsonschema";
import express, { Application } from "express";
import path from 'path';
import { getMetadataArgsStorage } from "routing-controllers";
import { routingControllersToSpec } from "routing-controllers-openapi";
import * as swaggerUiExpress from "swagger-ui-express";
import { generateSpec } from '../apispec';
/**
* Loader for everything openapi related - from creating the schema to serving it via a static route and swaggerUiExpress.
* All auth schema related stuff also has to be configured here
*/
export default async (app: Application) => {
const storage = getMetadataArgsStorage();
const schemas = validationMetadatasToSchemas({
refPointerPrefix: "#/components/schemas/",
});
const spec = routingControllersToSpec(
storage,
{
routePrefix: "/api"
},
{
components: {
schemas,
"securitySchemes": {
"AuthToken": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
info: {
description: "The the backend API for the LfK! runner system.",
title: "LfK! Backend API",
version: "1.0.0",
},
}
);
const options = {
explorer: true,
};
app.use(
"/api/docs",
swaggerUiExpress.serve,
swaggerUiExpress.setup(spec, options)
);
app.get(["/api/openapi.json", "/api/swagger.json"], (req, res) => {
//Spec creation based on the previously created schemas
const spec = generateSpec(storage, schemas);
app.get(["/api/docs/openapi.json", "/api/docs/swagger.json"], (req, res) => {
res.json(spec);
});
app.use('/api/docs', express.static(path.join(__dirname, '../static/docs'), { index: "index.html", extensions: ['html'] }));
return app;
};

118
src/mailer.ts Normal file
View File

@@ -0,0 +1,118 @@
import axios from 'axios';
import { config } from './config';
import { MailSendingError } from './errors/MailErrors';
/**
* This class is responsible for all things mail sending.
* This uses axios to communicate with the mailer api (https://git.odit.services/lfk/mailer).
*/
export class Mailer {
public static base: string = config.mailer_url;
public static key: string = config.mailer_key;
public static testing: boolean = config.testing;
/**
* Function for sending a password reset mail.
* @param to_address The address the mail will be sent to. Should always get pulled from a user object.
* @param token The requested password reset token - will be combined with the app_url to generate a password reset link.
*/
public static async sendResetMail(to_address: string, token: string, locale: string = "en") {
try {
await axios.request({
method: 'POST',
url: `${Mailer.base}/api/v1/email`,
headers: {
authorization: `Bearer ${Mailer.key}`,
'content-type': 'application/json'
},
data: {
to: to_address,
templateName: 'password-reset',
language: locale,
data: { token: token }
}
});
} catch (error) {
if (Mailer.testing) { return true; }
throw new MailSendingError();
}
}
/**
* Function for sending a runner selfservice welcome mail.
* @param to_address The address the mail will be sent to. Should always get pulled from a runner object.
* @param token The requested selfservice token - will be combined with the app_url to generate a selfservice profile link.
*/
public static async sendSelfserviceWelcomeMail(to_address: string, runner_id: number, firstname: string, middlename: string, lastname: string, token: string, locale: string = "en") {
try {
await axios.request({
method: 'POST',
url: `${Mailer.base}/api/v1/email`,
headers: {
authorization: `Bearer ${Mailer.key}`,
'content-type': 'application/json'
},
data: {
to: to_address,
templateName: 'welcome',
language: locale,
data: {
name: `${firstname} ${middlename} ${lastname}`,
barcode_content: `${runner_id}`,
link: `${process.env.SELFSERVICE_URL}/profile/${token}`
}
}
});
} catch (error) {
if (Mailer.testing) { return true; }
throw new MailSendingError();
}
}
/**
* Function for sending a runner selfservice link forgotten mail.
* @param to_address The address the mail will be sent to. Should always get pulled from a runner object.
* @param token The requested selfservice token - will be combined with the app_url to generate a selfservice profile link.
*/
public static async sendSelfserviceForgottenMail(to_address: string, runner_id: number, firstname: string, middlename: string, lastname: string, token: string, locale: string = "en") {
try {
console.log("Mail request", {
to: to_address,
templateName: 'welcome',
language: locale,
data: {
to: to_address,
templateName: 'welcome',
language: locale,
data: {
name: `${firstname} ${middlename} ${lastname}`,
barcode_content: `${runner_id}`,
link: `${process.env.SELFSERVICE_URL}/profile/${token}`
}
}
})
await axios.request({
method: 'POST',
url: `${Mailer.base}/api/v1/email`,
headers: {
authorization: `Bearer ${Mailer.key}`,
'content-type': 'application/json'
},
data: {
to: to_address,
templateName: 'welcome',
language: locale,
data: {
name: `${firstname} ${middlename} ${lastname}`,
barcode_content: `${runner_id}`,
link: `${process.env.SELFSERVICE_URL}/profile/${token}`
}
}
});
} catch (error) {
if (Mailer.testing) { return true; }
console.error("Error while sending selfservice forgotten mail:", error.message);
throw new MailSendingError();
}
}
}

View File

@@ -1,20 +1,14 @@
import {
Middleware,
ExpressErrorMiddlewareInterface
} from "routing-controllers";
import { ExpressErrorMiddlewareInterface, Middleware } from "routing-controllers";
/**
* Our Error handling middlware that returns our custom httperrors to the user.
*/
@Middleware({ type: "after" })
export class ErrorHandler implements ExpressErrorMiddlewareInterface {
public error(
error: any,
request: any,
response: any,
next: (err: any) => any
) {
public error(error: any, request: any, response: any, next: (err: any) => any) {
if (response.headersSent) {
return;
}
response.json(error);
}
}

View File

@@ -0,0 +1,23 @@
import { Request, Response } from 'express';
/**
* Custom express middleware that appends the raw body to the request object.
* Mainly used for parsing csvs from bodies.
*/
const RawBodyMiddleware = (req: Request, res: Response, next: () => void) => {
const body = []
req.on('data', chunk => {
body.push(chunk)
})
req.on('end', () => {
const rawBody = Buffer.concat(body)
req['rawBody'] = rawBody
next()
})
req.on('error', () => {
res.sendStatus(400)
})
}
export default RawBodyMiddleware

129
src/middlewares/ScanAuth.ts Normal file
View File

@@ -0,0 +1,129 @@
import crypto from 'crypto';
import { Request, Response } from 'express';
import { getConnectionManager } from 'typeorm';
import { config } from '../config';
import { deleteStationEntry, getStationEntry, setStationEntry, StationKVEntry } from '../nats/StationKV';
import { ScanStation } from '../models/entities/ScanStation';
import authchecker from './authchecker';
/**
* Computes the HMAC-SHA256 of the provided token using the station token secret.
*/
function computeHmac(token: string): string {
return crypto.createHmac('sha256', config.station_token_secret).update(token).digest('hex');
}
/**
* Constant-time comparison of two hex HMAC strings.
* Returns true if they match.
*/
function verifyHmac(provided_token: string, storedHash: string): boolean {
const expectedHash = computeHmac(provided_token);
const expectedBuf = Buffer.from(expectedHash);
const storedBuf = Buffer.from(storedHash);
return expectedBuf.length === storedBuf.length && crypto.timingSafeEqual(expectedBuf, storedBuf);
}
/**
* This middleware handles the authentication of scan station api tokens.
* The tokens have to be provided via Bearer authorization header.
*
* Auth flow:
* 1. Extract prefix from token (PREFIX.KEY format)
* 2. Try NATS KV cache lookup by prefix — warm path: HMAC verify, no DB
* 3. On cache miss: DB lookup → HMAC verify → write to KV cache
* 4. On no station match at all: fall back to JWT auth (SCAN:CREATE permission)
*
* On success sets req.isStationAuth = true and req.stationId on the request object.
* These are internal server-side properties — not HTTP headers, not spoofable by clients.
*
* You have to manually use this middleware via @UseBefore(ScanAuth) instead of using @Authorized().
* @param req Express request object.
* @param res Express response object.
* @param next Next function to call on success.
*/
const ScanAuth = async (req: Request, res: Response, next: () => void) => {
let provided_token: string = req.headers['authorization'];
if (!provided_token) {
res.status(401).send({ http_code: 401, short: 'no_token', message: 'No api token provided.' });
return;
}
provided_token = provided_token.replace('Bearer ', '');
const prefix = provided_token.split('.')[0];
if (!prefix) {
res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
return;
}
// --- KV cache lookup (warm path) ---
const cached = await getStationEntry(prefix);
if (cached) {
if (!cached.enabled) {
res.status(401).send({ http_code: 401, short: 'station_disabled', message: 'Station is disabled.' });
return;
}
if (!verifyHmac(provided_token, cached.tokenHash)) {
res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
return;
}
req.isStationAuth = true;
req.stationId = cached.id;
next();
return;
}
// --- DB lookup (cold path) ---
const station = await getConnectionManager().get().getRepository(ScanStation).findOne({ prefix }, { relations: ['track'] });
if (!station) {
// No station with this prefix — fall back to JWT auth
let user_authorized = false;
try {
const action = { request: req, response: res, context: null, next };
user_authorized = await authchecker(action, ['SCAN:CREATE']);
} finally {
if (!user_authorized) {
res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
return;
}
next();
}
return;
}
// Station found — verify token before caching
const tokenHash = computeHmac(provided_token);
const storedBuf = Buffer.from(station.key);
const computedBuf = Buffer.from(tokenHash);
const valid = computedBuf.length === storedBuf.length && crypto.timingSafeEqual(computedBuf, storedBuf);
if (!valid) {
res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
return;
}
if (!station.enabled) {
res.status(401).send({ http_code: 401, short: 'station_disabled', message: 'Station is disabled.' });
return;
}
// Write to KV cache for subsequent requests
const entry: StationKVEntry = {
id: station.id,
enabled: station.enabled,
tokenHash,
trackId: station.track.id,
trackDistance: station.track.distance,
minimumLapTime: station.track.minimumLapTime ?? 0,
};
await setStationEntry(prefix, entry);
req.isStationAuth = true;
req.stationId = station.id;
next();
};
export default ScanAuth;
export { deleteStationEntry };

View File

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

View File

@@ -0,0 +1,58 @@
import cookie from "cookie";
import * as jwt from "jsonwebtoken";
import { Action } from 'routing-controllers';
import { getConnectionManager } from 'typeorm';
import { config } from '../config';
import { IllegalJWTError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from '../errors/AuthError';
import { JwtCreator, JwtUser } from '../jwtcreator';
import { User } from '../models/entities/User';
/**
* TODO:
*/
const UserChecker = async (action: Action) => {
let jwtPayload = undefined
try {
let provided_token = "" + action.request.headers["authorization"].replace("Bearer ", "");
jwtPayload = <any>jwt.verify(provided_token, config.jwt_secret);
jwtPayload = jwtPayload["userdetails"];
} catch (error) {
jwtPayload = await refresh(action);
}
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] })
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
if (user.enabled == false) { throw new UserDisabledError(); }
return user;
};
/**
* Handles soft-refreshing of access-tokens.
* @param action Routing-Controllers action object that provides request and response objects among other stuff.
*/
const refresh = async (action: Action) => {
let refresh_token = undefined;
try {
refresh_token = cookie.parse(action.request.headers["cookie"])["lfk_backend__refresh_token"];
}
catch {
throw new IllegalJWTError();
}
let jwtPayload = undefined;
try {
jwtPayload = <any>jwt.verify(refresh_token, config.jwt_secret);
} catch (error) {
throw new IllegalJWTError();
}
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions', 'groups', 'groups.permissions'] })
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
if (user.enabled == false) { throw new UserDisabledError(); }
let newAccess = JwtCreator.createAccess(user);
action.response.header("authorization", "Bearer " + newAccess);
return await new JwtUser(user);
}
export default UserChecker;

View File

@@ -0,0 +1,74 @@
import cookie from "cookie";
import * as jwt from "jsonwebtoken";
import { Action } from "routing-controllers";
import { getConnectionManager } from 'typeorm';
import { config } from '../config';
import { IllegalJWTError, NoPermissionError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from '../errors/AuthError';
import { JwtCreator, JwtUser } from '../jwtcreator';
import { User } from '../models/entities/User';
/**
* Handles authentication via jwt's (Bearer authorization header) for all api endpoints using the @Authorized decorator.
* @param action Routing-Controllers action object that provides request and response objects among other stuff.
* @param permissions The permissions that the endpoint using @Authorized requires.
*/
const authchecker = async (action: Action, permissions: string[] | string) => {
let required_permissions = undefined;
if (typeof permissions === "string") {
required_permissions = [permissions]
} else {
required_permissions = permissions
}
let jwtPayload = undefined
try {
let provided_token = "" + action.request.headers["authorization"].replace("Bearer ", "");
jwtPayload = <any>jwt.verify(provided_token, config.jwt_secret);
jwtPayload = jwtPayload["userdetails"];
} catch (error) {
jwtPayload = await refresh(action);
}
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions'] })
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
if (user.enabled == false) { throw new UserDisabledError(); }
if (!jwtPayload["permissions"]) { throw new NoPermissionError(); }
action.response.local = {}
action.response.local.jwtPayload = jwtPayload;
for (let required_permission of required_permissions) {
if (!(jwtPayload["permissions"].includes(required_permission))) { return false; }
}
return true;
}
/**
* Handles soft-refreshing of access-tokens.
* @param action Routing-Controllers action object that provides request and response objects among other stuff.
*/
const refresh = async (action: Action) => {
let refresh_token = undefined;
try {
refresh_token = cookie.parse(action.request.headers["cookie"])["lfk_backend__refresh_token"];
}
catch {
throw new IllegalJWTError();
}
let jwtPayload = undefined;
try {
jwtPayload = <any>jwt.verify(refresh_token, config.jwt_secret);
} catch (error) {
throw new IllegalJWTError();
}
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions', 'groups', 'groups.permissions'] })
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
if (user.enabled == false) { throw new UserDisabledError(); }
let newAccess = JwtCreator.createAccess(user);
action.response.header("authorization", "Bearer " + newAccess);
return await new JwtUser(user);
}
export default authchecker

View File

@@ -1,64 +0,0 @@
import { IsNotEmpty, IsOptional, IsPostalCode, IsString } from 'class-validator';
import { Address } from '../entities/Address';
export class CreateAddress {
/**
* The address's description.
*/
@IsString()
@IsOptional()
description?: string;
/**
* The address's first line.
* Containing the street and house number.
*/
@IsString()
@IsNotEmpty()
address1: string;
/**
* The address's second line.
* Containing optional information.
*/
@IsString()
@IsOptional()
address2?: string;
/**
* The address's postal code.
*/
@IsString()
@IsNotEmpty()
@IsPostalCode("DE")
postalcode: string;
/**
* The address's city.
*/
@IsString()
@IsNotEmpty()
city: string;
/**
* The address's country.
*/
@IsString()
@IsNotEmpty()
country: string;
/**
* Creates a Address object based on this.
*/
public toAddress(): Address {
let newAddress: Address = new Address();
newAddress.address1 = this.address1;
newAddress.address2 = this.address2;
newAddress.postalcode = this.postalcode;
newAddress.city = this.city;
newAddress.country = this.country;
return newAddress;
}
}

View File

@@ -1,57 +0,0 @@
import * as argon2 from "argon2";
import { IsEmail, IsOptional, IsString } from 'class-validator';
import * as jsonwebtoken from 'jsonwebtoken';
import { getConnectionManager } from 'typeorm';
import { InvalidCredentialsError, PasswordNeededError, UserNotFoundError } from '../../errors/AuthError';
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
import { User } from '../entities/User';
import { Auth } from '../responses/ResponseAuth';
export class CreateAuth {
@IsOptional()
@IsString()
username?: string;
@IsString()
password: string;
@IsOptional()
@IsEmail()
@IsString()
email?: string;
public async toAuth(): Promise<Auth> {
let newAuth: Auth = new Auth();
if (this.email === undefined && this.username === undefined) {
throw new UsernameOrEmailNeededError();
}
if (!this.password) {
throw new PasswordNeededError()
}
const found_users = await getConnectionManager().get().getRepository(User).find({ where: [{ username: this.username }, { email: this.email }] });
if (found_users.length === 0) {
throw new UserNotFoundError()
} else {
const found_user = found_users[0]
if (await argon2.verify(found_user.password, this.password + found_user.uuid)) {
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60
delete found_user.password;
newAuth.access_token = jsonwebtoken.sign({
userdetails: found_user,
exp: timestamp_accesstoken_expiry
}, "securekey")
newAuth.access_token_expires_at = timestamp_accesstoken_expiry
//
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 10 * 36000
newAuth.refresh_token = jsonwebtoken.sign({
refreshtokencount: found_user.refreshTokenCount,
userid: found_user.id,
exp: timestamp_refresh_expiry
}, "securekey")
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
} else {
throw new InvalidCredentialsError()
}
}
return newAuth;
}
}

View File

@@ -1,83 +0,0 @@
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
import { Address } from '../entities/Address';
import { GroupContact } from '../entities/GroupContact';
export class CreateGroupContact {
/**
* The contact's first name.
*/
@IsNotEmpty()
@IsString()
firstname: string;
/**
* The contact's middle name.
* Optional
*/
@IsOptional()
@IsString()
middlename?: string;
/**
* The contact's last name.
*/
@IsNotEmpty()
@IsString()
lastname: string;
/**
* The contact's address.
* Optional
*/
@IsInt()
@IsOptional()
address?: number;
/**
* The contact's phone number.
* Optional
*/
@IsOptional()
@IsPhoneNumber("DE")
phone?: string;
/**
* The contact's email address.
* Optional
*/
@IsOptional()
@IsEmail()
email?: string;
/**
* Get's this participant's address from this.address.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined) {
return null;
}
if (!isNaN(this.address)) {
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
if (!address) { throw new AddressNotFoundError; }
return address;
}
throw new AddressWrongTypeError;
}
/**
* Creates a Address object based on this.
*/
public async toGroupContact(): Promise<GroupContact> {
let contact: GroupContact = new GroupContact();
contact.firstname = this.firstname;
contact.middlename = this.middlename;
contact.lastname = this.lastname;
contact.email = this.email;
contact.phone = this.phone;
contact.address = await this.getAddress();
return null;
}
}

View File

@@ -1,71 +0,0 @@
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
import { Address } from '../entities/Address';
export abstract class CreateParticipant {
/**
* The new participant's first name.
*/
@IsString()
@IsNotEmpty()
firstname: string;
/**
* The new participant's middle name.
* Optional.
*/
@IsString()
@IsNotEmpty()
middlename?: string;
/**
* The new participant's last name.
*/
@IsString()
@IsNotEmpty()
lastname: string;
/**
* The new participant's phone number.
* Optional.
*/
@IsString()
@IsOptional()
@IsPhoneNumber("ZZ")
phone?: string;
/**
* The new participant's e-mail address.
* Optional.
*/
@IsString()
@IsOptional()
@IsEmail()
email?: string;
/**
* The new participant's address.
* Must be of type number (address id), createAddress (new address) or address (existing address)
* Optional.
*/
@IsInt()
@IsOptional()
address?: number;
/**
* Get's this participant's address from this.address.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined) {
return null;
}
if (!isNaN(this.address)) {
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
if (!address) { throw new AddressNotFoundError; }
return address;
}
throw new AddressWrongTypeError;
}
}

View File

@@ -1,37 +0,0 @@
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { GroupContactNotFoundError, GroupContactWrongTypeError } from '../../errors/GroupContactErrors';
import { GroupContact } from '../entities/GroupContact';
export abstract class CreateRunnerGroup {
/**
* The group's name.
*/
@IsNotEmpty()
@IsString()
name: string;
/**
* The group's contact.
* Optional
*/
@IsInt()
@IsOptional()
contact?: number;
/**
* Deals with the contact for groups this.
*/
public async getContact(): Promise<GroupContact> {
if (this.contact === undefined) {
return null;
}
if (!isNaN(this.contact)) {
let address = await getConnectionManager().get().getRepository(GroupContact).findOne({ id: this.contact });
if (!address) { throw new GroupContactNotFoundError; }
return address;
}
throw new GroupContactWrongTypeError;
}
}

View File

@@ -1,46 +0,0 @@
import { IsInt, IsOptional } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
import { Address } from '../entities/Address';
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { CreateRunnerGroup } from './CreateRunnerGroup';
export class CreateRunnerOrganisation extends CreateRunnerGroup {
/**
* The new organisation's address.
* Must be of type number (address id), createAddress (new address) or address (existing address)
* Optional.
*/
@IsInt()
@IsOptional()
address?: number;
/**
* Creates a Participant entity from this.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined) {
return null;
}
if (!isNaN(this.address)) {
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
if (!address) { throw new AddressNotFoundError; }
return address;
}
throw new AddressWrongTypeError;
}
/**
* Creates a RunnerOrganisation entity from this.
*/
public async toRunnerOrganisation(): Promise<RunnerOrganisation> {
let newRunnerOrganisation: RunnerOrganisation = new RunnerOrganisation();
newRunnerOrganisation.name = this.name;
newRunnerOrganisation.contact = await this.getContact();
newRunnerOrganisation.address = await this.getAddress();
return newRunnerOrganisation;
}
}

View File

@@ -1,43 +0,0 @@
import { IsInt, IsNotEmpty } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { RunnerTeam } from '../entities/RunnerTeam';
import { CreateRunnerGroup } from './CreateRunnerGroup';
export class CreateRunnerTeam extends CreateRunnerGroup {
/**
* The team's parent group (organisation).
*/
@IsInt()
@IsNotEmpty()
parentGroup: number;
public async getParent(): Promise<RunnerOrganisation> {
if (this.parentGroup === undefined) {
throw new RunnerTeamNeedsParentError();
}
if (!isNaN(this.parentGroup)) {
let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: this.parentGroup });
if (!parentGroup) { throw new RunnerOrganisationNotFoundError();; }
return parentGroup;
}
throw new RunnerOrganisationWrongTypeError;
}
/**
* Creates a RunnerTeam entity from this.
*/
public async toRunnerTeam(): Promise<RunnerTeam> {
let newRunnerTeam: RunnerTeam = new RunnerTeam();
newRunnerTeam.name = this.name;
newRunnerTeam.parentGroup = await this.getParent();
newRunnerTeam.contact = await this.getContact()
return newRunnerTeam;
}
}

View File

@@ -1,30 +0,0 @@
import { IsInt, IsNotEmpty, IsPositive, IsString } from 'class-validator';
import { Track } from '../entities/Track';
export class CreateTrack {
/**
* The track's name.
*/
@IsString()
@IsNotEmpty()
name: string;
/**
* The track's distance in meters (must be greater 0).
*/
@IsInt()
@IsPositive()
distance: number;
/**
* Converts a Track object based on this.
*/
public toTrack(): Track {
let newTrack: Track = new Track();
newTrack.name = this.name;
newTrack.distance = this.distance;
return newTrack;
}
}

View File

@@ -1,119 +0,0 @@
import * as argon2 from "argon2";
import { IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import * as uuid from 'uuid';
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
import { UserGroupNotFoundError } from '../../errors/UserGroupErrors';
import { User } from '../entities/User';
import { UserGroup } from '../entities/UserGroup';
export class CreateUser {
/**
* The new user's first name.
*/
@IsString()
firstname: string;
/**
* The new user's middle name.
* Optinal.
*/
@IsString()
@IsOptional()
middlename?: string;
/**
* The new user's last name.
*/
@IsString()
lastname: string;
/**
* The new user's username.
* You have to provide at least one of: {email, username}.
*/
@IsOptional()
@IsString()
username?: string;
/**
* The new user's email address.
* You have to provide at least one of: {email, username}.
*/
@IsEmail()
@IsString()
@IsOptional()
email?: string;
/**
* The new user's phone number.
* Optional
*/
@IsPhoneNumber("ZZ")
@IsOptional()
phone?: string;
/**
* The new user's password.
* This will of course not be saved in plaintext :)
*/
@IsString()
password: string;
/**
* The new user's groups' id(s).
* You can provide either one groupId or an array of groupIDs.
* Optional.
*/
@IsOptional()
groupId?: number[] | number
//TODO: ProfilePics
/**
* Converts this to a User Entity.
*/
public async toUser(): Promise<User> {
let newUser: User = new User();
if (this.email === undefined && this.username === undefined) {
throw new UsernameOrEmailNeededError();
}
if (this.groupId) {
if (!Array.isArray(this.groupId)) {
this.groupId = [this.groupId]
}
const groupIDs: number[] = this.groupId
let errors = 0
const validateusergroups = async () => {
let foundgroups = []
for (const g of groupIDs) {
const found = await getConnectionManager().get().getRepository(UserGroup).find({ id: g });
if (found.length === 0) {
errors++
} else {
foundgroups.push(found[0])
}
}
newUser.groups = foundgroups
}
await validateusergroups()
if (errors !== 0) {
throw new UserGroupNotFoundError();
}
}
newUser.email = this.email
newUser.username = this.username
newUser.firstname = this.firstname
newUser.middlename = this.middlename
newUser.lastname = this.lastname
newUser.uuid = uuid.v4()
newUser.phone = this.phone
newUser.password = await argon2.hash(this.password + newUser.uuid);
//TODO: ProfilePics
return newUser;
}
}

View File

@@ -1,14 +1,28 @@
import { IsString } from 'class-validator';
import { IsOptional, IsString } from 'class-validator';
import * as jsonwebtoken from 'jsonwebtoken';
import { getConnectionManager } from 'typeorm';
import { config } from '../../config';
import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserNotFoundError } from '../../errors/AuthError';
import { User } from '../entities/User';
import { Logout } from '../responses/ResponseLogout';
/**
* This class handels a user logging out of the system.
* Of course it check's the user's provided credential (token) before logging him out.
*/
export class HandleLogout {
/**
* A stringyfied jwt access token.
* Will get checked for validity.
*/
@IsString()
token: string;
@IsOptional()
token?: string;
/**
* Logs the user out.
* This gets achived by increasing the user's refresh token count, thereby invalidateing all currently existing jwts for that user.
*/
public async logout(): Promise<Logout> {
let logout: Logout = new Logout();
if (!this.token || this.token === undefined) {
@@ -16,16 +30,16 @@ export class HandleLogout {
}
let decoded;
try {
decoded = jsonwebtoken.verify(this.token, 'securekey')
decoded = jsonwebtoken.verify(this.token, config.jwt_secret)
} catch (error) {
throw new IllegalJWTError()
}
logout.timestamp = Math.floor(Date.now() / 1000)
let found_user: User = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["userid"] });
let found_user: User = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["id"] });
if (!found_user) {
throw new UserNotFoundError()
}
if (found_user.refreshTokenCount !== decoded["refreshtokencount"]) {
if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) {
throw new RefreshTokenCountInvalidError()
}
found_user.refreshTokenCount++;

View File

@@ -0,0 +1,97 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { RunnerGroupNeededError } from '../../errors/RunnerErrors';
import { RunnerOrganizationNotFoundError } from '../../errors/RunnerOrganizationErrors';
import { RunnerGroup } from '../entities/RunnerGroup';
import { RunnerOrganization } from '../entities/RunnerOrganization';
import { RunnerTeam } from '../entities/RunnerTeam';
import { CreateRunner } from './create/CreateRunner';
/**
* Special class used to import runners from csv files - or json arrays created from csv to be exact.
* Why you ask? Because the past has shown us that a non excel/csv based workflow is too much for most schools.
*/
export class ImportRunner {
/**
* The new runner's first name.
*/
@IsString()
@IsNotEmpty()
firstname: string;
/**
* The new runner's middle name.
*/
@IsString()
@IsOptional()
middlename?: string;
/**
* The new runner's last name.
*/
@IsString()
@IsNotEmpty()
lastname: string;
/**
* The new runner's team's name (if not provided otherwise).
* The team will automaticly get generated if it doesn't exist in this org yet.
*/
@IsString()
@IsOptional()
team?: string;
/**
* Just an alias for team, because this is usually only used for importing data from schools.
*/
@IsOptional()
@IsString()
public set class(value: string) {
this.team = value;
}
/**
* Creates a CreateRunner object based on this.
* @param groupID Either the id of the new runner's group or the id of the org that the new runner's team is a part of.
*/
public async toCreateRunner(groupID: number): Promise<CreateRunner> {
let newRunner: CreateRunner = new CreateRunner();
newRunner.firstname = this.firstname;
newRunner.middlename = this.middlename;
newRunner.lastname = this.lastname;
newRunner.group = (await this.getGroup(groupID)).id;
return newRunner;
}
/**
* Get's the new runners group.
* @param groupID Either the id of the new runner's group or the id of the org that the new runner's team is a part of.
*/
public async getGroup(groupID: number): Promise<RunnerGroup> {
if (this.team === undefined && groupID === undefined) {
throw new RunnerGroupNeededError();
}
let team = await getConnectionManager().get().getRepository(RunnerTeam).findOne({ id: groupID });
if (team) { return team; }
let org = await getConnectionManager().get().getRepository(RunnerOrganization).findOne({ id: groupID });
if (!org) {
throw new RunnerOrganizationNotFoundError();
}
if (this.team === undefined) { return org; }
team = await getConnectionManager().get().getRepository(RunnerTeam).findOne({ name: this.team, parentGroup: org });
if (!team) {
let newRunnerTeam: RunnerTeam = new RunnerTeam();
newRunnerTeam.name = this.team;
newRunnerTeam.parentGroup = org;
team = await getConnectionManager().get().getRepository(RunnerTeam).save(newRunnerTeam);
}
return team;
}
}

View File

@@ -1,49 +1,56 @@
import { IsString } from 'class-validator';
import { IsOptional, IsString } from 'class-validator';
import * as jsonwebtoken from 'jsonwebtoken';
import { getConnectionManager } from 'typeorm';
import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserNotFoundError } from '../../errors/AuthError';
import { config } from '../../config';
import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
import { JwtCreator } from "../../jwtcreator";
import { User } from '../entities/User';
import { Auth } from '../responses/ResponseAuth';
import { ResponseAuth } from '../responses/ResponseAuth';
/**
* This class is used to create refreshed auth credentials.
* To be a little bit more exact: Is takes in a refresh token and creates a new access and refresh token for it's user.
* It of course checks for user existance, jwt validity and so on.
*/
export class RefreshAuth {
/**
* A stringyfied jwt refresh token.
* Will get checked for validity.
*/
@IsString()
token: string;
@IsOptional()
token?: string;
public async toAuth(): Promise<Auth> {
let newAuth: Auth = new Auth();
/**
* Creates a new auth object based on this.
*/
public async toAuth(): Promise<ResponseAuth> {
let newAuth: ResponseAuth = new ResponseAuth();
if (!this.token || this.token === undefined) {
throw new JwtNotProvidedError()
}
let decoded
try {
decoded = jsonwebtoken.verify(this.token, 'securekey')
decoded = jsonwebtoken.verify(this.token, config.jwt_secret)
} catch (error) {
throw new IllegalJWTError()
}
const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["userid"] });
const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["id"] }, { relations: ['groups', 'permissions', 'groups.permissions'] });
if (!found_user) {
throw new UserNotFoundError()
}
if (found_user.refreshTokenCount !== decoded["refreshtokencount"]) {
if (found_user.enabled == false) { throw new UserDisabledError(); }
if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) {
throw new RefreshTokenCountInvalidError()
}
delete found_user.password;
//Create the auth token
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60
delete found_user.password;
newAuth.access_token = jsonwebtoken.sign({
userdetails: found_user,
exp: timestamp_accesstoken_expiry
}, "securekey")
newAuth.access_token = JwtCreator.createAccess(found_user, timestamp_accesstoken_expiry);
newAuth.access_token_expires_at = timestamp_accesstoken_expiry
//
//Create the refresh token
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 10 * 36000
newAuth.refresh_token = jsonwebtoken.sign({
refreshtokencount: found_user.refreshTokenCount,
userid: found_user.id,
exp: timestamp_refresh_expiry
}, "securekey")
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
newAuth.refresh_token = JwtCreator.createRefresh(found_user, timestamp_refresh_expiry);
newAuth.refresh_token_expires_at = timestamp_refresh_expiry;
return newAuth;
}
}

View File

@@ -0,0 +1,57 @@
import * as Bun from 'bun';
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 Bun.password.hash(this.password + found_user.uuid);
await getConnectionManager().get().getRepository(User).save(found_user);
return "password reset successfull";
}
}

View File

@@ -0,0 +1,29 @@
import { IsInt, IsPositive } from 'class-validator';
import { FixedDonation } from '../../entities/FixedDonation';
import { CreateDonation } from './CreateDonation';
/**
* This class is used to create a new FixedDonation entity from a json body (post request).
*/
export class CreateAnonymousDonation extends CreateDonation {
/**
* The donation's amount.
* The unit is your currency's smallest unit (default: euro cent).
*/
@IsInt()
@IsPositive()
amount: number;
/**
* Creates a new FixedDonation entity from this.
*/
public async toEntity(): Promise<FixedDonation> {
let newDonation = new FixedDonation;
newDonation.amount = this.amount;
newDonation.paidAmount = this.amount;
return newDonation;
}
}

View File

@@ -0,0 +1,73 @@
import * as Bun from 'bun';
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { InvalidCredentialsError, PasswordNeededError, UserDisabledError, UserNotFoundError } from '../../../errors/AuthError';
import { UsernameOrEmailNeededError } from '../../../errors/UserErrors';
import { JwtCreator } from '../../../jwtcreator';
import { User } from '../../entities/User';
import { ResponseAuth } from '../../responses/ResponseAuth';
/**
* This class is used to create auth credentials based on user credentials provided in a json body (post request).
* To be a little bit more exact: Is takes in a username/email + password and creates a new access and refresh token for the user.
* It of course checks for user existance, password validity and so on.
*/
export class CreateAuth {
/**
* The username of the user that want's to login.
* Either username or email have to be provided.
*/
@IsOptional()
@IsString()
username?: string;
/**
* The email address of the user that want's to login.
* Either username or email have to be provided.
*/
@IsOptional()
@IsEmail()
@IsString()
email?: string;
/**
* The user's password.
* Will be checked against an argon2 hash.
*/
@IsNotEmpty()
@IsString()
password: string;
/**
* Creates a new auth object based on this.
*/
public async toAuth(): Promise<ResponseAuth> {
let newAuth: ResponseAuth = new ResponseAuth();
if (this.email === undefined && this.username === undefined) {
throw new UsernameOrEmailNeededError();
}
if (!this.password) {
throw new PasswordNeededError();
}
const found_user = await getConnectionManager().get().getRepository(User).findOne({ relations: ['groups', 'permissions', 'groups.permissions'], where: [{ username: this.username }, { email: this.email }] });
if (!found_user) {
throw new UserNotFoundError();
}
if (found_user.enabled == false) { throw new UserDisabledError(); }
if (!(await Bun.password.verify(this.password + found_user.uuid, found_user.password))) {
throw new InvalidCredentialsError();
}
//Create the access token
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 24 * 60 * 60
newAuth.access_token = JwtCreator.createAccess(found_user, timestamp_accesstoken_expiry);
newAuth.access_token_expires_at = timestamp_accesstoken_expiry
//Create the refresh token
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
newAuth.refresh_token = JwtCreator.createRefresh(found_user, timestamp_refresh_expiry);
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
return newAuth;
}
}

View File

@@ -0,0 +1,68 @@
import { IsInt, IsOptional, IsPositive } from 'class-validator';
import { getConnection } from 'typeorm';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { DistanceDonation } from '../../entities/DistanceDonation';
import { Runner } from '../../entities/Runner';
import { CreateDonation } from './CreateDonation';
/**
* This class is used to create a new FixedDonation entity from a json body (post request).
*/
export class CreateDistanceDonation extends CreateDonation {
/**
* The donation's associated donor's id.
* This is important to link donations to donors.
*/
@IsInt()
@IsPositive()
donor: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
@IsOptional()
paidAmount?: number;
/**
* The donation's associated runner's id.
* This is important to link the runner's distance ran to the donation.
*/
@IsInt()
@IsPositive()
runner: number;
/**
* The donation's amount per distance (full kilometer aka 1000 meters).
* The unit is your currency's smallest unit (default: euro cent).
*/
@IsInt()
@IsPositive()
amountPerDistance: number;
/**
* Creates a new FixedDonation entity from this.
*/
public async toEntity(): Promise<DistanceDonation> {
let newDonation = new DistanceDonation;
newDonation.amountPerDistance = this.amountPerDistance;
newDonation.paidAmount = this.paidAmount;
newDonation.donor = await this.getDonor();
newDonation.runner = await this.getRunner();
return newDonation;
}
/**
* Gets a runner based on the runner id provided via this.runner.
*/
public async getRunner(): Promise<Runner> {
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
if (!runner) {
throw new RunnerNotFoundError();
}
return runner;
}
}

View File

@@ -0,0 +1,30 @@
import { IsInt, IsOptional } from 'class-validator';
import { getConnection } from 'typeorm';
import { Donation } from '../../entities/Donation';
import { Donor } from '../../entities/Donor';
/**
* This class is used to create a new Donation entity from a json body (post request).
*/
export abstract class CreateDonation {
@IsInt()
@IsOptional()
donor: number;
@IsInt()
@IsOptional()
paidAmount?: number;
/**
* Creates a new Donation entity from this.
*/
public abstract toEntity(): Promise<Donation>;
/**
* Gets a donor based on the donor id provided via this.donor.
*/
public async getDonor(): Promise<Donor> {
const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor });
return donor;
}
}

View File

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

View File

@@ -0,0 +1,44 @@
import { IsInt, IsPositive } from 'class-validator';
import { FixedDonation } from '../../entities/FixedDonation';
import { CreateDonation } from './CreateDonation';
/**
* This class is used to create a new FixedDonation entity from a json body (post request).
*/
export class CreateFixedDonation extends CreateDonation {
/**
* The donation's associated donor's id.
* This is important to link donations to donors.
*/
@IsInt()
@IsPositive()
donor: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
paidAmount?: number;
/**
* The donation's amount.
* The unit is your currency's smallest unit (default: euro cent).
*/
@IsInt()
@IsPositive()
amount: number;
/**
* Creates a new FixedDonation entity from this.
*/
public async toEntity(): Promise<FixedDonation> {
let newDonation = new FixedDonation;
newDonation.amount = this.amount;
newDonation.paidAmount = this.paidAmount;
newDonation.donor = await this.getDonor();
return newDonation;
}
}

View File

@@ -0,0 +1,97 @@
import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { config } from '../../../config';
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
import { Address } from '../../entities/Address';
import { GroupContact } from '../../entities/GroupContact';
import { RunnerGroup } from '../../entities/RunnerGroup';
/**
* This classed is used to create a new GroupContact entity from a json body (post request).
*/
export class CreateGroupContact {
/**
* The new contact's first name.
*/
@IsNotEmpty()
@IsString()
firstname: string;
/**
* The new contact's middle name.
*/
@IsOptional()
@IsString()
middlename?: string;
/**
* The new contact's last name.
*/
@IsNotEmpty()
@IsString()
lastname: string;
/**
* The new contact's address.
*/
@IsOptional()
@IsObject()
address?: Address;
/**
* The contact's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
*/
@IsOptional()
@IsPhoneNumber(config.phone_validation_countrycode)
phone?: string;
/**
* The new contact's email address.
*/
@IsOptional()
@IsEmail()
email?: string;
/**
* The new contacts's groups' ids.
* You can provide either one groupId or an array of groupIDs.
*/
@IsOptional()
groups?: number[] | number
/**
* Get's all groups for this contact by their id's;
*/
public async getGroups(): Promise<RunnerGroup[]> {
if (!this.groups) { return null; }
let groups = new Array<RunnerGroup>();
if (!Array.isArray(this.groups)) {
this.groups = [this.groups]
}
for (let group of this.groups) {
let found = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: group });
if (!found) { throw new RunnerGroupNotFoundError(); }
groups.push(found);
}
return groups;
}
/**
* Creates a new GroupContact entity from this.
*/
public async toEntity(): Promise<GroupContact> {
let newContact: GroupContact = new GroupContact();
newContact.firstname = this.firstname;
newContact.middlename = this.middlename;
newContact.lastname = this.lastname;
newContact.email = this.email;
newContact.phone = this.phone;
newContact.address = this.address;
Address.validate(newContact.address);
newContact.groups = await this.getGroups();
return newContact;
}
}

View File

@@ -0,0 +1,60 @@
import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { config } from '../../../config';
import { Address } from '../../entities/Address';
/**
* This classed is used to create a new Participant entity from a json body (post request).
*/
export abstract class CreateParticipant {
/**
* The new participant's first name.
*/
@IsString()
@IsNotEmpty()
firstname: string;
/**
* The new participant's middle name.
*/
@IsString()
@IsOptional()
middlename?: string;
/**
* The new participant's last name.
*/
@IsString()
@IsNotEmpty()
lastname: string;
/**
* The new participant's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
*/
@IsString()
@IsOptional()
@IsPhoneNumber(config.phone_validation_countrycode)
phone?: string;
/**
* The new participant's e-mail address.
*/
@IsString()
@IsOptional()
@IsEmail()
email?: string;
/**
* The new participant's address.
*/
@IsOptional()
@IsObject()
address?: Address;
/**
* how the participant got into the system
*/
@IsOptional()
@IsString()
created_via?: string;
}

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