Compare commits

...

58 Commits
1.2.0 ... dev

Author SHA1 Message Date
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
67 changed files with 6890 additions and 4642 deletions

View File

@ -0,0 +1,33 @@
name: Build release images
on:
push:
tags:
- "*.*.*"
jobs:
build-container:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 19
- run: npm i -g pnpm@10.7 && pnpm i
- run: pnpm 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

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",

View File

@ -1,33 +0,0 @@
steps:
- name: build latest
image: woodpeckerci/plugin-docker-buildx
settings:
repo: registry.odit.services/lfk/backend
tags:
- latest
registry: registry.odit.services
platforms: linux/amd64,linux/arm64
cache_from: registry.odit.services/lfk/backend:dev
username:
from_secret: odit-registry-builder-username
password:
from_secret: odit-registry-builder-password
when:
branch: main
- name: build dev
image: woodpeckerci/plugin-docker-buildx
settings:
repo: registry.odit.services/lfk/backend
tags:
- dev
registry: registry.odit.services
platforms: linux/amd64,linux/arm64
cache_from: registry.odit.services/lfk/backend:dev
username:
from_secret: odit-registry-builder-username
password:
from_secret: odit-registry-builder-password
when:
branch: dev
when:
event: push

View File

@ -1,17 +0,0 @@
steps:
- name: build tag
image: woodpeckerci/plugin-docker-buildx
settings:
repo: registry.odit.services/lfk/backend
tags:
- "${CI_COMMIT_TAG}"
registry: registry.odit.services
platforms: linux/amd64,linux/arm64
cache_from: registry.odit.services/lfk/backend:dev
username:
from_secret: odit-registry-builder-username
password:
from_secret: odit-registry-builder-password
when:
event:
- tag

View File

@ -2,13 +2,189 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [1.5.2](https://git.odit.services/lfk/backend/compare/1.5.1...1.5.2)
- feat(mailer): Add logging for selfservice forgotten mail requests [`eebcc2e`](https://git.odit.services/lfk/backend/commit/eebcc2e3284230135e3911b4edaecd1a9cfd2100)
- feat(mailer): Log error message when sending selfservice forgotten mail fails [`0f532b1`](https://git.odit.services/lfk/backend/commit/0f532b139c2bc5cd89ca2dbff0867825a9363250)
#### [1.5.1](https://git.odit.services/lfk/backend/compare/1.5.0...1.5.1)
> 26 May 2025
- chore(release): 1.5.1 [`284954d`](https://git.odit.services/lfk/backend/commit/284954d064f09951c13584e9d50a83be2c4b9f72)
- feat(mailer): Log error when sending selfservice forgotten mail fails [`401ca92`](https://git.odit.services/lfk/backend/commit/401ca923a61bc5988e73209c086bc9a5a4fa04f9)
#### [1.5.0](https://git.odit.services/lfk/backend/compare/1.4.3...1.5.0)
> 6 May 2025
- feat(entities): Added created/updated at to all entities [`728f8a1`](https://git.odit.services/lfk/backend/commit/728f8a14e9fb7360fce92640bfa5658af8cadb4f)
- feat(responses): Added created_at/updated_at [`f225cc4`](https://git.odit.services/lfk/backend/commit/f225cc49548605de48cf6c6e6f7c86b163236545)
- feat(participants): Added created/updated at [`a448058`](https://git.odit.services/lfk/backend/commit/a4480589a0e23a4481332ab5efa0777c62bbab56)
- chore(release): 1.5.0 [`bf1f641`](https://git.odit.services/lfk/backend/commit/bf1f6411e0f5113842a537f5bcf632638bdf1048)
#### [1.4.3](https://git.odit.services/lfk/backend/compare/1.4.2...1.4.3)
> 1 May 2025
- feat(runners): Include collected distance donation amount in runner detail [`4494afc`](https://git.odit.services/lfk/backend/commit/4494afc64b433d26b54a293fe156d13c40faad95)
- chore(release): 1.4.3 [`0ad9eeb`](https://git.odit.services/lfk/backend/commit/0ad9eeb52f18af3ea7d86fe1bf15edb04f4cfd2d)
#### [1.4.2](https://git.odit.services/lfk/backend/compare/1.4.1...1.4.2)
> 1 May 2025
- fix(donations): Fixed creation bug [`07a0195`](https://git.odit.services/lfk/backend/commit/07a0195f125519f239d255a0cc081ddbde8f1da3)
- chore(release): 1.4.2 [`f4747c5`](https://git.odit.services/lfk/backend/commit/f4747c51de71d9b28cca1b00a91de3cfd6f0f56e)
#### [1.4.1](https://git.odit.services/lfk/backend/compare/1.4.0...1.4.1)
> 28 April 2025
- chore(release): 1.4.1 [`7ac9822`](https://git.odit.services/lfk/backend/commit/7ac98229d17e7cb019d5dcc5402870490a97f910)
- refactor(auth): Increased token timeouts to 24hrs/7days [`dd5b538`](https://git.odit.services/lfk/backend/commit/dd5b538783f9c806f0c883cd391754fb5c842ec8)
#### [1.4.0](https://git.odit.services/lfk/backend/compare/1.3.12...1.4.0)
> 28 April 2025
- feat(donations): Implement response type to indicate possible missing donor [`f4bf309`](https://git.odit.services/lfk/backend/commit/f4bf309821c140f2bc0ae8b6d96c7458fcc80978)
- wip [`9875b4f`](https://git.odit.services/lfk/backend/commit/9875b4f3926e04b502e7af64c17f54fd3c1d8e3e)
- refactor(donations): Make anon prepaid [`02b1cb9`](https://git.odit.services/lfk/backend/commit/02b1cb9904cc593faeac025ae302a8684f650f5e)
- chore(release): 1.4.0 [`8e6d674`](https://git.odit.services/lfk/backend/commit/8e6d67428c85b6ee504a379ff13a3a951f7b9543)
- fix(donations): Move donor over to the types that need it [`7697acf`](https://git.odit.services/lfk/backend/commit/7697acff82b23d0c05dbbd17fee6e70eb1b7061c)
#### [1.3.12](https://git.odit.services/lfk/backend/compare/1.3.11...1.3.12)
> 28 April 2025
- chore(release): 1.3.12 [`bacfc43`](https://git.odit.services/lfk/backend/commit/bacfc437f97cac6a20c32b79ae2d6391466f78a6)
- refactor: make Donation.donor optional [`2ab6e98`](https://git.odit.services/lfk/backend/commit/2ab6e985e356f0f3d8637d81630d191cc11b8806)
- refactor(config): improve consola error logs [`ce9b765`](https://git.odit.services/lfk/backend/commit/ce9b765b81b014623e79ce64d8d835f1f86cecf3)
#### [1.3.11](https://git.odit.services/lfk/backend/compare/1.3.10...1.3.11)
> 17 April 2025
- feat(RunnerController): add selfservice_links parameter to getRunners method [`a50d72f`](https://git.odit.services/lfk/backend/commit/a50d72f2f5281b8c28ca64a0970161a35a7af95a)
- chore(release): 1.3.11 [`d06f6a4`](https://git.odit.services/lfk/backend/commit/d06f6a44072971d1853411b255f9b49eb423b3a2)
#### [1.3.10](https://git.odit.services/lfk/backend/compare/1.3.9...1.3.10)
> 11 April 2025
- chore(release): 1.3.10 [`4723d97`](https://git.odit.services/lfk/backend/commit/4723d9738eacd63fb41f23c628fbe4181bd126de)
- feat(RunnerController.getAll): debug created_via query param filter [`1a478bd`](https://git.odit.services/lfk/backend/commit/1a478bd784e01b9d5a1c6635d1004a9535c9a0e9)
#### [1.3.9](https://git.odit.services/lfk/backend/compare/1.3.8...1.3.9)
> 9 April 2025
- feat(RunnerController.getAll): add created_via query param filter [`6e63c57`](https://git.odit.services/lfk/backend/commit/6e63c57936f06a29da5f1a94b1141d51b75df5f0)
- chore(release): 1.3.9 [`284cb0f`](https://git.odit.services/lfk/backend/commit/284cb0f8b3955d0d65c2b36d2ec427a39752ffe7)
#### [1.3.8](https://git.odit.services/lfk/backend/compare/1.3.7...1.3.8)
> 9 April 2025
- feat(RunnerCardController): putByCode [`8237d5f`](https://git.odit.services/lfk/backend/commit/8237d5f21067c0872a7eff7c8d1506edf44ec10c)
- chore(release): 1.3.8 [`30b61db`](https://git.odit.services/lfk/backend/commit/30b61db2c160c019bac381f26cefdc6524ea465e)
#### [1.3.7](https://git.odit.services/lfk/backend/compare/1.3.6...1.3.7)
> 8 April 2025
- feat(stats): Publish runners by kiosk stat [`a6afba9`](https://git.odit.services/lfk/backend/commit/a6afba93e243ca419c282a16cad023d06d864e0e)
- chore(release): 1.3.7 [`03e0a29`](https://git.odit.services/lfk/backend/commit/03e0a290965648579956ac1f8e8542c97a667ed8)
#### [1.3.6](https://git.odit.services/lfk/backend/compare/1.3.5...1.3.6)
> 8 April 2025
- chore(release): 1.3.6 [`a41758c`](https://git.odit.services/lfk/backend/commit/a41758cd9c83105c3a4b407744bafe2f0f6fb48a)
- feat(runners): Allow created via being set via api [`d6755ed`](https://git.odit.services/lfk/backend/commit/d6755ed134071df635bc9d5821ceb2396c0f1d22)
- fix(participant): Switch to correct type [`599c75f`](https://git.odit.services/lfk/backend/commit/599c75fc00217eaec3cc87c0de50d059bdde685f)
#### [1.3.5](https://git.odit.services/lfk/backend/compare/1.3.4...1.3.5)
> 8 April 2025
- feat(runners): Generate selfservice urls on runner if requested or create/update/get single [`5415cd3`](https://git.odit.services/lfk/backend/commit/5415cd38a727e76632a01a4d2634a1777df5542c)
- chore(release): 1.3.5 [`bb213f0`](https://git.odit.services/lfk/backend/commit/bb213f001eff2157abf8741128f624f9cc991afe)
#### [1.3.4](https://git.odit.services/lfk/backend/compare/1.3.3...1.3.4)
> 28 March 2025
- feat: add runnersViaSelfservice to statsControllerGet [`5c5000a`](https://git.odit.services/lfk/backend/commit/5c5000a218b47815e6846ac8b857dcd1995bfa6f)
- chore(release): 1.3.4 [`175ba52`](https://git.odit.services/lfk/backend/commit/175ba52ffae8e6ba1fdc1603ac2f5eba15602046)
#### [1.3.3](https://git.odit.services/lfk/backend/compare/v1.3.2...1.3.3)
> 28 March 2025
- chore(release): 1.3.3 [`d559d04`](https://git.odit.services/lfk/backend/commit/d559d0403191c703fd6da0e3f3dab53eec9258c0)
- ci: remove "v" prefix from tags [`2af682d`](https://git.odit.services/lfk/backend/commit/2af682d1dd09df496eb9f3a9111c50c0c4117356)
#### [v1.3.2](https://git.odit.services/lfk/backend/compare/v1.3.1...v1.3.2)
> 28 March 2025
- chore(release): v1.3.2 [`30905e4`](https://git.odit.services/lfk/backend/commit/30905e481c69cfe62b4261544b4277de3a1a43c2)
- ci: pnpm@10.7 [`752d405`](https://git.odit.services/lfk/backend/commit/752d405bda9129f3cd288a956d5444cab316c2af)
#### [v1.3.1](https://git.odit.services/lfk/backend/compare/1.3.0...v1.3.1)
> 28 March 2025
- fix: TypeError: Cannot read properties of undefined (reading 'filter') - when trying to delete a org/team with runners [`#210`](https://git.odit.services/lfk/backend/issues/210)
- pnpm@10.7, node@23, argon->@node-rs/argon2 [`78dcad0`](https://git.odit.services/lfk/backend/commit/78dcad085794c93829499dd550a786c38d6186f5)
- chore(release): v1.3.1 [`8fa4ed7`](https://git.odit.services/lfk/backend/commit/8fa4ed7c3319c3e56a71701ba266ceda64d2ef69)
#### [1.3.0](https://git.odit.services/lfk/backend/compare/1.2.1...1.3.0)
> 28 March 2025
- feat: created_via for tracking how runners got into the system [`#212`](https://git.odit.services/lfk/backend/pull/212)
- feat: created_via for tracking how runners got into the system (#212) [`#211`](https://git.odit.services/lfk/backend/issues/211)
- ci: move to gitea workflows [`ebde8c6`](https://git.odit.services/lfk/backend/commit/ebde8c6ffd8b17c6752da8c4d8eb3095105f6132)
- chore(release): v1.3.0 [`93e0cdf`](https://git.odit.services/lfk/backend/commit/93e0cdf577654898b2d63790d91598c458a2db59)
- build: docker "AS" casing [`0a43f1b`](https://git.odit.services/lfk/backend/commit/0a43f1bb5b26d3acb0d4d91648473f0dc55e8637)
- ci: change release commit message [`6efcd94`](https://git.odit.services/lfk/backend/commit/6efcd94726957b8c527820f1a9b0130151ce22f1)
- refactor(RunnerController.remove): only load necessary relations [`8c6fdb2`](https://git.odit.services/lfk/backend/commit/8c6fdb22390218e385780fadb3bdaf32148ac054)
- refactor(RunnerTeamController.remove): only load necessary relations [`c0d5af5`](https://git.odit.services/lfk/backend/commit/c0d5af5d7ab44cfdf19014e0d774fb560d08f6d7)
- fix: add .created_via to ResponseParticipant constructor [`2e271bc`](https://git.odit.services/lfk/backend/commit/2e271bcd52f02ab7449cd15916b0afc86e8b0a90)
#### [1.2.1](https://git.odit.services/lfk/backend/compare/1.2.0...1.2.1)
> 11 December 2024
- refactor: allow selfservice link every 30s [`07bf28b`](https://git.odit.services/lfk/backend/commit/07bf28b14458849930748ce041fb65e572759482)
- chore(release): 1.2.1 [`4008a5e`](https://git.odit.services/lfk/backend/commit/4008a5ee720b212bac9cba64417058bf4526060b)
#### [1.2.0](https://git.odit.services/lfk/backend/compare/v1.1.4...1.2.0)
> 11 December 2024
- refactor: move to new mailer [`0f4c8b2`](https://git.odit.services/lfk/backend/commit/0f4c8b2051cae17fbdd7e02017ad5b41c61e210c)
- refactor(ci): Switch to new woodpecker [`b3a73b2`](https://git.odit.services/lfk/backend/commit/b3a73b25e80a0466ff83e43481271fc0cd499a0d)
- feat: middlename [`6eff243`](https://git.odit.services/lfk/backend/commit/6eff2438035b368eb45931fad9402a6cb942b350)
- SELFSERVICE_URL [`765ef84`](https://git.odit.services/lfk/backend/commit/765ef849035ca4f8b2253bb76d15be8e9a3e6763)
- FRONTEND_URL env [`296ba8d`](https://git.odit.services/lfk/backend/commit/296ba8ddab1dba46f8201829d9a7e5fc1c88c0f8)
- chore: update readme [`d842c14`](https://git.odit.services/lfk/backend/commit/d842c14240fb4a7f70c66143bbe877f8168ef6d4)
- chore(release): 1.2.0 [`6764bf8`](https://git.odit.services/lfk/backend/commit/6764bf80eac832d186e688319d8a959543a1495f)
- Merge pull request 'refactor: move to new mailer' (#209) from refactor/new-mailer into dev [`bda1f97`](https://git.odit.services/lfk/backend/commit/bda1f971d1a14ea403439533c7ae31280c7df167)
#### [v1.1.4](https://git.odit.services/lfk/backend/compare/v1.1.3...v1.1.4)
> 20 November 2024
- build: package lock [`50dd703`](https://git.odit.services/lfk/backend/commit/50dd703a1bd276a607cc10a087c7e90fd880847a)
- fix(deps): Bump sqlite3 [`cd3cd81`](https://git.odit.services/lfk/backend/commit/cd3cd81360777e8bc4d78e861354e58c8da79cc7)
- feat(ci)!: Switch to woodpecker [`3192365`](https://git.odit.services/lfk/backend/commit/3192365793fae59f2b89e3231db298654f0a28e9)
- fix(deps): Bumped argon2 to latest version for arm support [`cf48c00`](https://git.odit.services/lfk/backend/commit/cf48c00ddb2ac33263549876928db50ae152c12d)
- fix: updated README for pnpm, typos [`5082b1b`](https://git.odit.services/lfk/backend/commit/5082b1b8b1c0ae9e8ffa9c71c4d7923fd9223c87)
- 🚀Bumped version to v1.1.4 [`a54cb28`](https://git.odit.services/lfk/backend/commit/a54cb287a4323ac8de77f51711cc6c52ec290859)
- ci: drop lfk-client-node [`075d484`](https://git.odit.services/lfk/backend/commit/075d484f1169bfc5c5b68cb9712116b0e270b471)
- fix(dependencies): Switch back to previous class-validator version to produce a working build [`74d334f`](https://git.odit.services/lfk/backend/commit/74d334f9b747a77115bd9b97729ef1120822e128)

View File

@ -1,10 +1,12 @@
# Typescript Build
FROM registry.odit.services/hub/library/node:21.1.0-alpine3.18 as build
FROM registry.odit.services/hub/library/node:23.10.0-alpine3.21 AS build
ARG NPM_REGISTRY_URL=https://registry.npmjs.org
WORKDIR /app
COPY package.json ./
RUN npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@8
COPY pnpm-workspace.yaml ./
COPY pnpm-lock.yaml ./
RUN npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@10.7
RUN mkdir /pnpm && pnpm config set store-dir /pnpm && pnpm i
COPY tsconfig.json ormconfig.js ./
@ -14,9 +16,11 @@ RUN pnpm run build \
&& pnpm i --production --prefer-offline
# final image
FROM registry.odit.services/hub/library/node:21.1.0-alpine3.18 as final
FROM registry.odit.services/hub/library/node:23.10.0-alpine3.21 AS final
WORKDIR /app
COPY --from=build /app/package.json /app/package.json
COPY --from=build /app/pnpm-lock.yaml /app/pnpm-lock.yaml
COPY --from=build /app/pnpm-workspace.yaml /app/pnpm-workspace.yaml
COPY --from=build /app/ormconfig.js /app/ormconfig.js
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules

View File

@ -1,4 +1,3 @@
version: "3"
services:
backend_server:
build: .
@ -14,7 +13,7 @@ services:
DB_NAME: ./db.sqlite
NODE_ENV: production
POSTALCODE_COUNTRYCODE: DE
SEED_TEST_DATA: "false"
SEED_TEST_DATA: "true"
MAILER_URL: https://dev.lauf-fuer-kaya.de/mailer
MAILER_KEY: asdasd
# APP_PORT: 4010

View File

@ -1,3 +1,32 @@
# @node-rs/argon2
**Author**: undefined
**Repo**: [object Object]
**License**: MIT
**Description**: RustCrypto: Argon2 binding for Node.js
## License Text
MIT License
Copyright (c) 2020-present LongYinan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# @odit/class-validator-jsonschema
**Author**: Aleksi Pekkala <aleksipekkala@gmail.com>
**Repo**: git@github.com:epiphone/class-validator-jsonschema.git
@ -27,36 +56,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# argon2
**Author**: Ranieri Althoff <ranisalt+argon2@gmail.com>
**Repo**: [object Object]
**License**: MIT
**Description**: An Argon2 library for Node
## License Text
The MIT License (MIT)
Copyright (c) 2015 Ranieri Althoff
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# axios
**Author**: Matt Zabriskie
**Repo**: [object Object]

View File

@ -1,11 +1,8 @@
{
"name": "@odit/lfk-backend",
"version": "1.2.0",
"version": "1.5.2",
"main": "src/app.ts",
"repository": "https://git.odit.services/lfk/backend",
"engines": {
"pnpm": "8"
},
"author": {
"name": "ODIT.Services",
"email": "info@odit.services",
@ -25,8 +22,8 @@
],
"license": "CC-BY-NC-SA-4.0",
"dependencies": {
"@node-rs/argon2": "^2.0.2",
"@odit/class-validator-jsonschema": "2.1.1",
"argon2": "0.31.2",
"axios": "0.21.1",
"body-parser": "1.19.0",
"check-password-strength": "2.0.2",
@ -46,7 +43,7 @@
"reflect-metadata": "0.1.13",
"routing-controllers": "0.9.0-alpha.6",
"routing-controllers-openapi": "2.2.0",
"sqlite3": "5.1.6",
"sqlite3": "5.1.7",
"typeorm": "0.2.30",
"typeorm-routing-controllers-extensions": "0.2.0",
"typeorm-seeding": "1.6.1",
@ -94,12 +91,12 @@
"git": {
"commit": true,
"requireCleanWorkingDir": false,
"commitMessage": "🚀Bumped version to v${version}",
"commitMessage": "chore(release): ${version}",
"requireBranch": "dev",
"push": true,
"tag": true,
"tagName": "v${version}",
"tagAnnotation": "v${version}"
"tagName": "${version}",
"tagAnnotation": "${version}"
},
"npm": {
"publish": false
@ -114,4 +111,4 @@
"docs/*"
]
}
}
}

10304
pnpm-lock.yaml generated

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

@ -1,3 +1,4 @@
import consola from 'consola';
import { config as configDotenv } from 'dotenv';
import { CountryCode } from 'libphonenumber-js';
import ValidatorJS from 'validator';
@ -20,12 +21,15 @@ export const config = {
}
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++;
}
function getPhoneCodeLocale(): CountryCode {

View File

@ -4,6 +4,7 @@ import { Repository, getConnectionManager } from 'typeorm';
import { DonationIdsNotMatchingError, DonationNotFoundError } from '../errors/DonationErrors';
import { DonorNotFoundError } from '../errors/DonorErrors';
import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { CreateAnonymousDonation } from '../models/actions/create/CreateAnonymousDonation';
import { CreateDistanceDonation } from '../models/actions/create/CreateDistanceDonation';
import { CreateFixedDonation } from '../models/actions/create/CreateFixedDonation';
import { UpdateDistanceDonation } from '../models/actions/update/UpdateDistanceDonation';
@ -11,6 +12,7 @@ import { UpdateFixedDonation } from '../models/actions/update/UpdateFixedDonatio
import { DistanceDonation } from '../models/entities/DistanceDonation';
import { Donation } from '../models/entities/Donation';
import { FixedDonation } from '../models/entities/FixedDonation';
import { ResponseAnonymousDonation } from '../models/responses/ResponseAnonymousDonation';
import { ResponseDistanceDonation } from '../models/responses/ResponseDistanceDonation';
import { ResponseDonation } from '../models/responses/ResponseDonation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
@ -35,6 +37,7 @@ export class DonationController {
@Authorized("DONATION:GET")
@ResponseSchema(ResponseDonation, { isArray: true })
@ResponseSchema(ResponseDistanceDonation, { isArray: true })
@ResponseSchema(ResponseAnonymousDonation, { isArray: true })
@OpenAPI({ description: 'Lists all donations (fixed or distance based) from all donors. <br> This includes the donations\'s runner\'s distance ran(if distance donation).' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
let responseDonations: ResponseDonation[] = new Array<ResponseDonation>();
@ -56,6 +59,7 @@ export class DonationController {
@Authorized("DONATION:GET")
@ResponseSchema(ResponseDonation)
@ResponseSchema(ResponseDistanceDonation)
@ResponseSchema(ResponseAnonymousDonation)
@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
@OnUndefined(DonationNotFoundError)
@OpenAPI({ description: 'Lists all information about the donation whose id got provided. This includes the donation\'s runner\'s distance ran (if distance donation).' })
@ -76,6 +80,17 @@ export class DonationController {
return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
}
@Post('/anonymous')
@Authorized("DONATION:CREATE")
@ResponseSchema(ResponseDonation)
@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a anonymous donation' })
async postAnonymous(@Body({ validate: true }) createDonation: CreateAnonymousDonation) {
let donation = await createDonation.toEntity();
donation = await this.fixedDonationRepository.save(donation);
return (await this.donationRepository.findOne({ id: donation.id })).toResponse();
}
@Post('/distance')
@Authorized("DONATION:CREATE")
@ResponseSchema(ResponseDistanceDonation)

View File

@ -5,6 +5,7 @@ import { RunnerCardHasScansError, RunnerCardIdsNotMatchingError, RunnerCardNotFo
import { RunnerNotFoundError } from '../errors/RunnerErrors';
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';
@ -112,6 +113,28 @@ export class RunnerCardController {
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)

View File

@ -30,10 +30,11 @@ export class RunnerController {
@Authorized("RUNNER:GET")
@ResponseSchema(ResponseRunner, { isArray: true })
@OpenAPI({ description: 'Lists all runners from all teams/orgs. <br> This includes the runner\'s group and distance ran.' })
async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
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 {
@ -41,7 +42,13 @@ export class RunnerController {
}
runners.forEach(runner => {
responseRunners.push(new ResponseRunner(runner));
if (created_via === "all") {
responseRunners.push(new ResponseRunner(runner, selfservice_links));
} else {
if (runner.created_via === created_via) {
responseRunners.push(new ResponseRunner(runner, selfservice_links));
}
}
});
return responseRunners;
}
@ -53,9 +60,9 @@ export class RunnerController {
@OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Lists all information about the runner whose id got provided.' })
async getOne(@Param('id') id: number) {
let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] })
let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations'] })
if (!runner) { throw new RunnerNotFoundError(); }
return new ResponseRunner(runner);
return new ResponseRunner(runner, true);
}
@Get('/:id/scans')
@ -98,7 +105,7 @@ export class RunnerController {
}
runner = await this.runnerRepository.save(runner)
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }));
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }), true);
}
@Put('/:id')
@ -119,7 +126,7 @@ export class RunnerController {
}
await this.runnerRepository.save(await runner.update(oldRunner));
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }));
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }), true);
}
@Delete('/:id')
@ -132,7 +139,7 @@ export class RunnerController {
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let runner = await this.runnerRepository.findOne({ id: id });
if (!runner) { return null; }
const responseRunner = await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] });
const responseRunner = await this.runnerRepository.findOne(runner);
if (!runner) {
throw new RunnerNotFoundError();

View File

@ -62,13 +62,13 @@ export class RunnerOrganizationController {
@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) {
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));
responseRunners.push(new ResponseRunner(runner, selfservice_links));
});
return responseRunners;
}

View File

@ -127,7 +127,7 @@ export class RunnerSelfServiceController {
const runner = await this.runnerRepository.findOne({ email: mail });
if (!runner) { throw new RunnerNotFoundError(); }
if (runner.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 60 * 15)) { throw new RunnerSelfserviceTimeoutError(); }
if (runner.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 30)) { throw new RunnerSelfserviceTimeoutError(); }
const token = JwtCreator.createSelfService(runner);
try {

View File

@ -60,11 +60,11 @@ export class RunnerTeamController {
@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) {
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));
responseRunners.push(new ResponseRunner(runner, selfservice_links));
});
return responseRunners;
}
@ -119,7 +119,7 @@ export class RunnerTeamController {
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let team = await this.runnerTeamRepository.findOne({ id: id });
if (!team) { return null; }
let runnerTeam = await this.runnerTeamRepository.findOne(team, { relations: ['parentGroup', 'contact', 'runners'] });
let runnerTeam = await this.runnerTeamRepository.findOne(team, { relations: ['runners'] });
if (!force) {
if (runnerTeam.runners.length != 0) {

View File

@ -23,6 +23,8 @@ export class StatsController {
@OpenAPI({ description: "A very basic stats endpoint providing basic counters for a dashboard or simmilar" })
async get() {
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();
@ -41,7 +43,7 @@ export class StatsController {
let donations = await connection.getRepository(Donation).find({ relations: ['runner', 'runner.scans', 'runner.scans.track'] });
const donors = await connection.getRepository(Donor).count();
return new ResponseStats(runners, teams, orgs, users, scans, donations, distace, donors)
return new ResponseStats(runnersViaSelfservice, runners, teams, orgs, users, scans, donations, distace, donors, runnersViaKiosk)
}
@Get("/runners/distance")

View File

@ -47,14 +47,14 @@ export class RunnerEmailNeededError extends NotAcceptableError {
}
/**
* Error to throw when a runner already requested a new selfservice link in the last 24hrs.
* 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 24hrs."
message = "You can only reqest a new token every 30s."
}
/**

View File

@ -76,6 +76,21 @@ export class Mailer {
*/
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`,
@ -96,6 +111,7 @@ export class Mailer {
});
} catch (error) {
if (Mailer.testing) { return true; }
console.error("Error while sending selfservice forgotten mail:", error.message);
throw new MailSendingError();
}
}

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { verify } from '@node-rs/argon2';
import { Request, Response } from 'express';
import { getConnectionManager } from 'typeorm';
import { ScanStation } from '../models/entities/ScanStation';
@ -58,7 +58,7 @@ const ScanAuth = async (req: Request, res: Response, next: () => void) => {
if (station.enabled == false) {
res.status(401).send({ http_code: 401, short: "station_disabled", message: "Station is disabled." });
}
if (!(await argon2.verify(station.key, provided_token))) {
if (!(await verify(station.key, provided_token))) {
res.status(401).send({ http_code: 401, short: "invalid_token", message: "Api token non-existent or invalid syntax." });
return;
}

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { verify } from '@node-rs/argon2';
import { Request, Response } from 'express';
import { getConnectionManager } from 'typeorm';
import { StatsClient } from '../models/entities/StatsClient';
@ -55,7 +55,7 @@ const StatsAuth = async (req: Request, res: Response, next: () => void) => {
}
}
else {
if (!(await argon2.verify(client.key, provided_token))) {
if (!(await verify(client.key, provided_token))) {
res.status(401).send("Api token invalid.");
return;
}

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from '@node-rs/argon2';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import * as jsonwebtoken from 'jsonwebtoken';
import { getConnectionManager } from 'typeorm';
@ -49,7 +49,7 @@ export class ResetPassword {
if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) { throw new RefreshTokenCountInvalidError(); }
found_user.refreshTokenCount = found_user.refreshTokenCount + 1;
found_user.password = await argon2.hash(this.password + found_user.uuid);
found_user.password = await 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

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { verify } from '@node-rs/argon2';
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { InvalidCredentialsError, PasswordNeededError, UserDisabledError, UserNotFoundError } from '../../../errors/AuthError';
@ -56,16 +56,16 @@ export class CreateAuth {
throw new UserNotFoundError();
}
if (found_user.enabled == false) { throw new UserDisabledError(); }
if (!(await argon2.verify(found_user.password, this.password + found_user.uuid))) {
if (!(await verify(found_user.password, this.password + found_user.uuid))) {
throw new InvalidCredentialsError();
}
//Create the access token
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 24 * 60 * 60
newAuth.access_token = JwtCreator.createAccess(found_user, timestamp_accesstoken_expiry);
newAuth.access_token_expires_at = timestamp_accesstoken_expiry
//Create the refresh token
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 10 * 36000
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
newAuth.refresh_token = JwtCreator.createRefresh(found_user, timestamp_refresh_expiry);
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
return newAuth;

View File

@ -1,4 +1,4 @@
import { IsInt, IsPositive } from 'class-validator';
import { IsInt, IsOptional, IsPositive } from 'class-validator';
import { getConnection } from 'typeorm';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { DistanceDonation } from '../../entities/DistanceDonation';
@ -10,6 +10,21 @@ import { CreateDonation } from './CreateDonation';
*/
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.

View File

@ -1,6 +1,5 @@
import { IsInt, IsOptional, IsPositive } from 'class-validator';
import { IsInt, IsOptional } from 'class-validator';
import { getConnection } from 'typeorm';
import { DonorNotFoundError } from '../../../errors/DonorErrors';
import { Donation } from '../../entities/Donation';
import { Donor } from '../../entities/Donor';
@ -8,17 +7,10 @@ import { Donor } from '../../entities/Donor';
* This class is used to create a new Donation entity from a json body (post request).
*/
export abstract class CreateDonation {
/**
* The donation's associated donor's id.
* This is important to link donations to donors.
*/
@IsInt()
@IsPositive()
@IsOptional()
donor: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
@IsOptional()
paidAmount?: number;
@ -33,9 +25,6 @@ export abstract class CreateDonation {
*/
public async getDonor(): Promise<Donor> {
const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor });
if (!donor) {
throw new DonorNotFoundError();
}
return donor;
}
}

View File

@ -6,6 +6,21 @@ import { CreateDonation } from './CreateDonation';
* This class is used to create a new FixedDonation entity from a json body (post request).
*/
export class CreateFixedDonation extends CreateDonation {
/**
* The donation's associated donor's id.
* This is important to link donations to donors.
*/
@IsInt()
@IsPositive()
donor: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
paidAmount?: number;
/**
* The donation's amount.
* The unit is your currency's smallest unit (default: euro cent).

View File

@ -50,4 +50,11 @@ export abstract class CreateParticipant {
@IsOptional()
@IsObject()
address?: Address;
/**
* how the participant got into the system
*/
@IsOptional()
@IsString()
created_via?: string;
}

View File

@ -32,6 +32,9 @@ export class CreateRunner extends CreateParticipant {
newRunner.email = this.email;
newRunner.group = await this.getGroup();
newRunner.address = this.address;
if (this.created_via) {
newRunner.created_via = this.created_via;
}
Address.validate(newRunner.address);
return newRunner;

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from '@node-rs/argon2';
import { IsBoolean, IsInt, IsOptional, IsPositive, IsString } from 'class-validator';
import crypto from 'crypto';
import { getConnection } from 'typeorm';
@ -44,7 +44,7 @@ export class CreateScanStation {
let newUUID = uuid.v4().toUpperCase();
newStation.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
newStation.key = await argon2.hash(newStation.prefix + "." + newUUID);
newStation.key = await hash(newStation.prefix + "." + newUUID);
newStation.cleartextkey = newStation.prefix + "." + newUUID;
return newStation;

View File

@ -26,6 +26,7 @@ export class CreateSelfServiceCitizenRunner extends CreateParticipant {
public async toEntity(): Promise<Runner> {
let newRunner: Runner = new Runner();
newRunner.created_via = "selfservice";
newRunner.firstname = this.firstname;
newRunner.middlename = this.middlename;
newRunner.lastname = this.lastname;

View File

@ -28,6 +28,7 @@ export class CreateSelfServiceRunner extends CreateParticipant {
public async toEntity(group: RunnerGroup): Promise<Runner> {
let newRunner: Runner = new Runner();
newRunner.created_via = "selfservice";
newRunner.firstname = this.firstname;
newRunner.middlename = this.middlename;
newRunner.lastname = this.lastname;

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from '@node-rs/argon2';
import { IsOptional, IsString } from 'class-validator';
import crypto from 'crypto';
import * as uuid from 'uuid';
@ -25,7 +25,7 @@ export class CreateStatsClient {
let newUUID = uuid.v4().toUpperCase();
newClient.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
newClient.key = await argon2.hash(newClient.prefix + "." + newUUID);
newClient.key = await hash(newClient.prefix + "." + newUUID);
newClient.cleartextkey = newClient.prefix + "." + newUUID;
return newClient;

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from "@node-rs/argon2";
import { passwordStrength } from "check-password-strength";
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
@ -110,7 +110,7 @@ export class CreateUser {
newUser.lastname = this.lastname
newUser.uuid = uuid.v4()
newUser.phone = this.phone
newUser.password = await argon2.hash(this.password + newUser.uuid);
newUser.password = await hash(this.password + newUser.uuid);
newUser.groups = await this.getGroups();
newUser.enabled = this.enabled;

View File

@ -0,0 +1,50 @@
import { IsBoolean, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnection } from 'typeorm';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { Runner } from '../../entities/Runner';
import { RunnerCard } from '../../entities/RunnerCard';
/**
* This class is used to update a RunnerCard entity (via put request).
*/
export class UpdateRunnerCardByCode {
/**
* The card's code.
*/
@IsString()
@IsNotEmpty()
code?: string;
/**
* The runner's id.
*/
@IsInt()
@IsOptional()
runner?: number;
/**
* Is the updated card enabled (for fraud reasons)?
* Default: true
*/
@IsBoolean()
enabled: boolean = true;
/**
* Creates a new RunnerCard entity from this.
*/
public async update(card: RunnerCard): Promise<RunnerCard> {
card.enabled = this.enabled;
card.runner = await this.getRunner();
return card;
}
public async getRunner(): Promise<Runner> {
if (!this.runner) { return null; }
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
if (!runner) {
throw new RunnerNotFoundError();
}
return runner;
}
}

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from '@node-rs/argon2';
import { passwordStrength } from "check-password-strength";
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
@ -111,7 +111,7 @@ export class UpdateUser {
if (!password_strength.contains.includes("lowercase")) { throw new PasswordMustContainLowercaseLetterError(); }
if (!password_strength.contains.includes("number")) { throw new PasswordMustContainNumberError(); }
if (!(password_strength.length > 9)) { throw new PasswordTooShortError(); }
user.password = await argon2.hash(this.password + user.uuid);
user.password = await hash(this.password + user.uuid);
user.refreshTokenCount = user.refreshTokenCount + 1;
}

View File

@ -1,8 +1,10 @@
import {
IsInt,
IsNotEmpty,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, PrimaryColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, PrimaryColumn } from "typeorm";
/**
* Defines the ConfigFlag entity.
@ -24,4 +26,25 @@ export class ConfigFlag {
@IsString()
@IsNotEmpty()
value: string;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
}

View File

@ -1,8 +1,8 @@
import {
IsInt,
IsNotEmpty
IsPositive
} from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseDonation } from '../responses/ResponseDonation';
import { Donor } from './Donor';
@ -24,7 +24,6 @@ export abstract class Donation {
/**
* The donations's donor.
*/
@IsNotEmpty()
@ManyToOne(() => Donor, donor => donor.donations)
donor: Donor;
@ -42,6 +41,27 @@ export abstract class Donation {
@IsInt()
paidAmount: number;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -5,9 +5,11 @@ import {
IsOptional,
IsPhoneNumber,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { config } from '../../config';
import { ResponseGroupContact } from '../responses/ResponseGroupContact';
import { Address } from "./Address";
@ -81,6 +83,27 @@ export class GroupContact {
@OneToMany(() => RunnerGroup, group => group.contact, { nullable: true })
groups: RunnerGroup[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -5,9 +5,11 @@ import {
IsOptional,
IsPhoneNumber,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { config } from '../../config';
import { ResponseParticipant } from '../responses/ResponseParticipant';
import { Address } from "./Address";
@ -75,6 +77,35 @@ export abstract class Participant {
@IsEmail()
email?: string;
/**
* how the participant got into the system
*/
@Column({ nullable: true, default: "backend" })
@IsOptional()
@IsString()
created_via?: string;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -1,9 +1,10 @@
import {
IsEnum,
IsInt,
IsNotEmpty
IsNotEmpty,
IsPositive
} from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { PermissionAction } from '../enums/PermissionAction';
import { PermissionTarget } from '../enums/PermissionTargets';
import { ResponsePermission } from '../responses/ResponsePermission';
@ -45,6 +46,27 @@ export class Permission {
@IsEnum(PermissionAction)
action: PermissionAction;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turn this into a string for exporting and jwts.
* Mainly used to shrink the size of jwts (otherwise the would contain entire objects).

View File

@ -1,5 +1,5 @@
import { IsInt } from 'class-validator';
import { Entity, OneToMany, PrimaryGeneratedColumn, TableInheritance } from 'typeorm';
import { IsInt, IsPositive } from 'class-validator';
import { BeforeInsert, BeforeUpdate, Column, Entity, OneToMany, PrimaryGeneratedColumn, TableInheritance } from 'typeorm';
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
import { Permission } from './Permission';
@ -23,6 +23,27 @@ export abstract class Principal {
@OneToMany(() => Permission, permission => permission.principal, { nullable: true })
permissions: Permission[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -57,7 +57,10 @@ export class Runner extends Participant {
* This is implemented here to avoid duplicate code in other files.
*/
public get validScans(): Scan[] {
return this.scans.filter(scan => scan.valid == true);
if (this.scans) {
return this.scans.filter(scan => scan.valid == true);
}
return []
}
/**
@ -81,6 +84,6 @@ export class Runner extends Participant {
* Turns this entity into it's response class.
*/
public toResponse(): ResponseRunner {
return new ResponseRunner(this);
return new ResponseRunner(this, true);
}
}

View File

@ -3,9 +3,10 @@ import {
IsInt,
IsOptional
IsOptional,
IsPositive
} from "class-validator";
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { RunnerCardIdOutOfRangeError } from '../../errors/RunnerCardErrors';
import { ResponseRunnerCard } from '../responses/ResponseRunnerCard';
import { Runner } from "./Runner";
@ -48,6 +49,27 @@ export class RunnerCard {
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Generates a ean-13 compliant string for barcode generation.
*/

View File

@ -2,9 +2,10 @@ import {
IsInt,
IsNotEmpty,
IsOptional,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseRunnerGroup } from '../responses/ResponseRunnerGroup';
import { GroupContact } from "./GroupContact";
import { Runner } from "./Runner";
@ -46,6 +47,27 @@ export abstract class RunnerGroup {
@OneToMany(() => Runner, runner => runner.group, { nullable: true })
runners: Runner[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Returns the total distance ran by this group's runners based on all their valid scans.
*/

View File

@ -5,7 +5,7 @@ import {
IsPositive
} from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseScan } from '../responses/ResponseScan';
import { Runner } from "./Runner";
@ -40,6 +40,27 @@ export class Scan {
@IsBoolean()
valid: boolean = true;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* The scan's distance in meters.
* This is the "real" value used by "normal" scans..

View File

@ -3,9 +3,10 @@ import {
IsInt,
IsNotEmpty,
IsOptional,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { ResponseScanStation } from '../responses/ResponseScanStation';
import { Track } from "./Track";
import { TrackScan } from "./TrackScan";
@ -78,6 +79,27 @@ export class ScanStation {
@IsBoolean()
enabled?: boolean = true;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -1,5 +1,5 @@
import { IsInt, IsOptional, IsString } from "class-validator";
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { IsInt, IsOptional, IsPositive, IsString } from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { ResponseStatsClient } from '../responses/ResponseStatsClient';
/**
* Defines the StatsClient entity.
@ -47,6 +47,27 @@ export class StatsClient {
@IsOptional()
cleartextkey?: string;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -5,7 +5,7 @@ import {
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { ResponseTrack } from '../responses/ResponseTrack';
import { ScanStation } from "./ScanStation";
import { TrackScan } from "./TrackScan";
@ -63,6 +63,27 @@ export class Track {
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -3,9 +3,10 @@ import {
IsInt,
IsNotEmpty,
IsOptional,
IsPositive,
IsString
} from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { PermissionAction } from '../enums/PermissionAction';
import { User } from './User';
@ -53,6 +54,27 @@ export class UserAction {
@IsString()
changed: string;
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()
@IsPositive()
created_at: number;
@Column({ type: 'bigint', nullable: true })
@IsInt()
@IsPositive()
updated_at: number;
@BeforeInsert()
public setCreatedAt() {
this.created_at = Math.floor(Date.now() / 1000);
this.updated_at = Math.floor(Date.now() / 1000);
}
@BeforeUpdate()
public setUpdatedAt() {
this.updated_at = Math.floor(Date.now() / 1000);
}
/**
* Turns this entity into it's response class.
*/

View File

@ -0,0 +1,68 @@
import { IsInt, IsPositive } from "class-validator";
import { Donation } from '../entities/Donation';
import { DonationStatus } from '../enums/DonationStatus';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
/**
* Defines the donation response.
*/
export class ResponseAnonymousDonation implements IResponse {
/**
* The responseType.
* This contains the type of class/entity this response contains.
*/
responseType: ResponseObjectType = ResponseObjectType.DONATION;
/**
* The donation's payment status.
* Provides you with a quick indicator of it's payment status.
*/
status: DonationStatus;
/**
* The donation's id.
*/
@IsInt()
@IsPositive()
id: number;
/**
* The donation's amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
amount: number;
/**
* The donation's paid amount in the smalles unit of your currency (default: euro cent).
*/
@IsInt()
paidAmount: number;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseDonation object from a scan.
* @param donation The donation the response shall be build for.
*/
public constructor(donation: Donation) {
this.id = donation.id;
this.amount = donation.amount;
this.paidAmount = donation.paidAmount || 0;
if (this.paidAmount < this.amount) {
this.status = DonationStatus.OPEN;
}
else {
this.status = DonationStatus.PAID;
}
this.created_at = donation.created_at;
this.updated_at = donation.updated_at;
}
}

View File

@ -47,6 +47,14 @@ export class ResponseDonation implements IResponse {
@IsInt()
paidAmount: number;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseDonation object from a scan.
* @param donation The donation the response shall be build for.
@ -64,5 +72,7 @@ export class ResponseDonation implements IResponse {
else {
this.status = DonationStatus.PAID;
}
this.created_at = donation.created_at;
this.updated_at = donation.updated_at;
}
}

View File

@ -1,4 +1,4 @@
import { IsInt, IsObject, IsString } from "class-validator";
import { IsInt, IsObject, IsPositive, IsString } from "class-validator";
import { Address } from '../entities/Address';
import { GroupContact } from '../entities/GroupContact';
import { ResponseObjectType } from '../enums/ResponseObjectType';
@ -64,6 +64,14 @@ export class ResponseGroupContact implements IResponse {
@IsObject()
address?: Address;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseGroupContact object from a contact.
* @param contact The contact the response shall be build for.
@ -82,5 +90,7 @@ export class ResponseGroupContact implements IResponse {
this.groups.push(group.toResponse());
}
}
this.created_at = contact.created_at;
this.updated_at = contact.updated_at;
}
}

View File

@ -1,4 +1,4 @@
import { IsInt, IsObject, IsOptional, IsString } from "class-validator";
import { IsInt, IsObject, IsOptional, IsPositive, IsString } from "class-validator";
import { Address } from '../entities/Address';
import { Participant } from '../entities/Participant';
import { ResponseObjectType } from '../enums/ResponseObjectType';
@ -50,6 +50,12 @@ export abstract class ResponseParticipant implements IResponse {
@IsString()
email?: string;
/**
* how the participant got into the system
*/
@IsString()
created_via?: string;
/**
* The participant's address.
*/
@ -57,6 +63,14 @@ export abstract class ResponseParticipant implements IResponse {
@IsObject()
address?: Address;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseParticipant object from a participant.
* @param participant The participant the response shall be build for.
@ -64,10 +78,13 @@ export abstract class ResponseParticipant implements IResponse {
public constructor(participant: Participant) {
this.id = participant.id;
this.firstname = participant.firstname;
this.created_via = participant.created_via;
this.middlename = participant.middlename;
this.lastname = participant.lastname;
this.phone = participant.phone;
this.email = participant.email;
this.address = participant.address;
this.created_at = participant.created_at;
this.updated_at = participant.updated_at;
}
}

View File

@ -2,7 +2,8 @@ import {
IsEnum,
IsInt,
IsNotEmpty,
IsObject
IsObject,
IsPositive
} from "class-validator";
import { Permission } from '../entities/Permission';
import { PermissionAction } from '../enums/PermissionAction';
@ -48,6 +49,14 @@ export class ResponsePermission implements IResponse {
@IsEnum(PermissionAction)
action: PermissionAction;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponsePermission object from a permission.
* @param permission The permission the response shall be build for.
@ -57,5 +66,7 @@ export class ResponsePermission implements IResponse {
this.principal = permission.principal.toResponse();
this.target = permission.target;
this.action = permission.action;
this.created_at = permission.created_at;
this.updated_at = permission.updated_at;
}
}

View File

@ -1,5 +1,6 @@
import {
IsInt
IsInt,
IsPositive
} from "class-validator";
import { Principal } from '../entities/Principal';
import { ResponseObjectType } from '../enums/ResponseObjectType';
@ -22,11 +23,21 @@ export abstract class ResponsePrincipal implements IResponse {
@IsInt()
id: number;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponsePrincipal object from a principal.
* @param principal The principal the response shall be build for.
*/
public constructor(principal: Principal) {
this.id = principal.id;
this.created_at = principal.created_at;
this.updated_at = principal.updated_at;
}
}

View File

@ -1,7 +1,10 @@
import {
IsInt,
IsObject
IsObject,
IsOptional,
IsString
} from "class-validator";
import { JwtCreator } from '../../jwtcreator';
import { Runner } from '../entities/Runner';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
@ -24,20 +27,43 @@ export class ResponseRunner extends ResponseParticipant implements IResponse {
@IsInt()
distance: number;
/**
* The runner's current donation amount based on distance.
* Only available for queries for single runners.
*/
@IsInt()
donationAmount: number;
/**
* The runner's group.
*/
@IsObject()
group: ResponseRunnerGroup;
/**
* A selfservice link for our new runner.
*/
@IsOptional()
@IsString()
selfserviceLink: string;
/**
* Creates a ResponseRunner object from a runner.
* @param runner The user the response shall be build for.
*/
public constructor(runner: Runner) {
public constructor(runner: Runner, generateSelfServiceLink: boolean = false) {
super(runner);
if (!runner.scans) { this.distance = 0 }
else { this.distance = runner.validScans.reduce((sum, current) => sum + current.distance, 0); }
if (runner.group) { this.group = runner.group.toResponse(); }
if (runner.distanceDonations) {
this.donationAmount = runner.distanceDonations.reduce((sum, current) => sum + (current.amountPerDistance * runner.distance / 1000), 0);
}
if (generateSelfServiceLink) {
const token = JwtCreator.createSelfService(runner);
this.selfserviceLink = `${process.env.SELFSERVICE_URL}/profile/${token}`;
}
}
}

View File

@ -1,4 +1,4 @@
import { IsBoolean, IsEAN, IsInt, IsNotEmpty, IsObject, IsString } from "class-validator";
import { IsBoolean, IsEAN, IsInt, IsNotEmpty, IsObject, IsPositive, IsString } from "class-validator";
import { RunnerCard } from '../entities/RunnerCard';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
@ -42,6 +42,14 @@ export class ResponseRunnerCard implements IResponse {
@IsBoolean()
enabled: boolean = true;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseRunnerCard object from a runner card.
* @param card The card the response shall be build for.
@ -57,5 +65,7 @@ export class ResponseRunnerCard implements IResponse {
}
this.enabled = card.enabled;
this.created_at = card.created_at;
this.updated_at = card.updated_at;
}
}

View File

@ -1,4 +1,4 @@
import { IsInt, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString } from "class-validator";
import { IsInt, IsNotEmpty, IsNumber, IsObject, IsOptional, IsPositive, IsString } from "class-validator";
import { RunnerGroup } from '../entities/RunnerGroup';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
@ -40,6 +40,14 @@ export abstract class ResponseRunnerGroup implements IResponse {
@IsNumber()
total_distance: number
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseRunnerGroup object from a runnerGroup.
* @param group The runnerGroup the response shall be build for.
@ -49,5 +57,7 @@ export abstract class ResponseRunnerGroup implements IResponse {
this.name = group.name;
if (group.contact) { this.contact = group.contact.toResponse(); };
if (group.runners) { this.total_distance = group.runners.reduce((p, c) => p + c.distance, 0) }
this.created_at = group.created_at;
this.updated_at = group.updated_at;
}
}

View File

@ -41,6 +41,14 @@ export class ResponseScan implements IResponse {
@IsPositive()
distance: number;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseScan object from a scan.
* @param scan The scan the response shall be build for.
@ -50,5 +58,7 @@ export class ResponseScan implements IResponse {
if (scan.runner) { this.runner = scan.runner.toResponse(); }
this.distance = scan.distance;
this.valid = scan.valid;
this.created_at = scan.created_at;
this.updated_at = scan.updated_at;
}
}

View File

@ -1,5 +1,4 @@
import {
IsBoolean,
IsInt,
@ -8,6 +7,7 @@ import {
IsObject,
IsOptional,
IsPositive,
IsString
} from "class-validator";
import { ScanStation } from '../entities/ScanStation';
@ -63,6 +63,14 @@ export class ResponseScanStation implements IResponse {
@IsBoolean()
enabled?: boolean = true;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseStatsClient object from a statsClient.
* @param client The statsClient the response shall be build for.
@ -74,5 +82,7 @@ export class ResponseScanStation implements IResponse {
this.key = "Only visible on creation.";
if (station.track) { this.track = station.track.toResponse(); }
this.enabled = station.enabled;
this.created_at = station.created_at;
this.updated_at = station.updated_at;
}
}

View File

@ -16,6 +16,18 @@ export class ResponseStats implements IResponse {
*/
responseType: ResponseObjectType = ResponseObjectType.STATS;
/**
* The amount of runners registered via selfservice.
*/
@IsInt()
runnersViaSelfservice: number;
/**
* The amount of runners registered via kiosk.
*/
@IsInt()
runnersViaKiosk: number;
/**
* The amount of runners registered in the system.
*/
@ -84,14 +96,16 @@ export class ResponseStats implements IResponse {
/**
* Creates a new stats response containing some basic statistics for a dashboard or public display.
* @param runners Array containing all runners - the following relations have to be resolved: scans, scans.track
* @param teams Array containing all teams - no relations have to be resolved.
* @param orgs Array containing all orgs - no relations have to be resolved.
* @param users Array containing all users - no relations have to be resolved.
* @param scans Array containing all scans - no relations have to be resolved.
* @param runnersViaSelfservice number of runners registered via selfservice
* @param runners number of runners
* @param teams number of teams - no relations have to be resolved.
* @param orgs number of orgs - no relations have to be resolved.
* @param users number of users - no relations have to be resolved.
* @param scans number of scans - no relations have to be resolved.
* @param donations Array containing all donations - the following relations have to be resolved: runner, runner.scans, runner.scans.track
*/
public constructor(runners: number, teams: number, orgs: number, users: number, scans: number, donations: Donation[], distance: number, donors: number) {
public constructor(runnersViaSelfservice: number, runners: number, teams: number, orgs: number, users: number, scans: number, donations: Donation[], distance: number, donors: number, runnersViaKiosk: number) {
this.runnersViaSelfservice = runnersViaSelfservice;
this.total_runners = runners;
this.total_teams = teams;
this.total_orgs = orgs;
@ -103,5 +117,6 @@ export class ResponseStats implements IResponse {
this.average_donation = this.total_donation / this.total_donations
this.total_donors = donors;
this.average_distance = this.total_distance / this.total_runners;
this.runnersViaKiosk = runnersViaKiosk;
}
}

View File

@ -1,10 +1,10 @@
import {
IsInt,
IsNotEmpty,
IsOptional,
IsPositive,
IsString
} from "class-validator";
import { StatsClient } from '../entities/StatsClient';
@ -49,6 +49,14 @@ export class ResponseStatsClient implements IResponse {
@IsNotEmpty()
prefix: string;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseStatsClient object from a statsClient.
* @param client The statsClient the response shall be build for.
@ -58,5 +66,7 @@ export class ResponseStatsClient implements IResponse {
this.description = client.description;
this.prefix = client.prefix;
this.key = "Only visible on creation.";
this.created_at = client.created_at;
this.updated_at = client.updated_at;
}
}

View File

@ -1,4 +1,4 @@
import { IsInt, IsOptional, IsString } from "class-validator";
import { IsInt, IsOptional, IsPositive, IsString } from "class-validator";
import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors';
import { Track } from '../entities/Track';
import { ResponseObjectType } from '../enums/ResponseObjectType';
@ -40,6 +40,14 @@ export class ResponseTrack implements IResponse {
@IsOptional()
minimumLapTime?: number;
@IsInt()
@IsPositive()
created_at: number;
@IsInt()
@IsPositive()
updated_at: number;
/**
* Creates a ResponseTrack object from a track.
* @param track The track the response shall be build for.
@ -52,5 +60,7 @@ export class ResponseTrack implements IResponse {
if (this.minimumLapTime < 0) {
throw new TrackLapTimeCantBeNegativeError();
}
this.created_at = track.created_at;
this.updated_at = track.updated_at;
}
}

View File

@ -1,4 +1,4 @@
import * as argon2 from "argon2";
import { hash } from '@node-rs/argon2';
import { Connection } from 'typeorm';
import { Factory, Seeder } from 'typeorm-seeding';
import * as uuid from 'uuid';
@ -33,7 +33,7 @@ export default class SeedUsers implements Seeder {
initialUser.lastname = "demo";
initialUser.username = "demo";
initialUser.uuid = uuid.v4();
initialUser.password = await argon2.hash("demo" + initialUser.uuid);
initialUser.password = await hash("demo" + initialUser.uuid);
initialUser.email = "demo@dev.lauf-fuer-kaya.de"
initialUser.groups = [group];
return await connection.getRepository(User).save(initialUser);