Compare commits

179 Commits

Author SHA1 Message Date
d3ce7d5ac3 fix: new linting errors
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m34s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m33s
2025-11-05 07:00:49 +01:00
6e5b4a7b3d chore(deps): update node.js to e5bbac0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m31s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m37s
2025-11-05 02:28:24 +00:00
cc76a77b31 chore(deps): update node.js to 69698ef
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m34s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m38s
2025-11-04 23:03:15 +00:00
2ac272582f chore(deps): update node.js to 55b6bbe
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m34s
2025-11-04 14:03:14 +00:00
10240977ca chore(deps): update node.js to 7b3ae90
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m38s
2025-11-04 12:03:19 +00:00
a7258f6c91 chore(deps): update debian:13.1 docker digest to 01a723b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m39s
2025-11-04 11:03:12 +00:00
63701f44f5 chore(deps): update golang:1.25.3 docker digest to 7e3cbcd
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m36s
2025-11-04 10:03:08 +00:00
ad1811e37c chore(deps): update debian:13.1 docker digest to e623a68
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m31s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m36s
2025-11-04 08:03:05 +00:00
689dba2f1a chore(deps): update debian:13.1 docker digest to 5803574
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m33s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m37s
2025-11-04 02:31:55 +00:00
7b5fb9e35a chore(deps): update node.js to v24.11.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m31s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m35s
2025-10-29 04:03:11 +00:00
237d26675d chore(deps): update golang:1.25.3 docker digest to 6bac879
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m33s
2025-10-26 15:02:41 +00:00
7ec60b0f93 chore(deps): update dependency htmx.org to v2.0.8
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m38s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m43s
2025-10-25 03:03:37 +00:00
fcb76ae7a8 chore(deps): update golang:1.25.3 docker digest to dd08f76
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m5s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m24s
2025-10-24 21:03:09 +00:00
37525ac31f chore(deps): update tailwindcss monorepo to v4.1.16
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m29s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m35s
2025-10-23 12:03:41 +00:00
6b11355857 chore(deps): update node.js to 23c24e8
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m34s
2025-10-22 19:03:11 +00:00
7d8f6fd1e5 chore(deps): update golang:1.25.3 docker digest to 8c945d3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m32s
2025-10-22 09:03:16 +00:00
36af297210 chore(deps): update node.js to 91b08ad
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m29s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m33s
2025-10-22 07:03:21 +00:00
b05835dde9 chore(deps): update golang:1.25.3 docker digest to bce1e7e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m33s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m34s
2025-10-22 03:03:51 +00:00
ff8bd828ec chore(deps): update node.js to v22.21.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m36s
2025-10-21 22:03:12 +00:00
4ec8959db8 chore(deps): update node.js to 915acd9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m27s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m27s
2025-10-21 13:03:29 +00:00
1d2a6d1c3a chore(deps): update golang:1.25.3 docker digest to 0d8c14c
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m57s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m23s
2025-10-21 12:03:39 +00:00
67996068d1 chore(deps): update node.js to 6b66300
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m35s
2025-10-21 10:03:20 +00:00
3e8723e359 chore(deps): update golang:1.25.3 docker digest to ffa2e57
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m33s
2025-10-21 09:06:52 +00:00
6c49f0311f chore(deps): update debian:13.1 docker digest to 72547dd
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m31s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m39s
2025-10-21 04:03:20 +00:00
adf68a8e11 chore(deps): update tailwindcss monorepo to v4.1.15
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m35s
2025-10-20 14:08:02 +00:00
a2a88381cb fix(deps): update module github.com/a-h/templ to v0.3.960
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m34s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m35s
2025-10-15 15:05:42 +00:00
e2ddb0b07b chore(deps): update golang:1.25.3 docker digest to 6ea52a0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m35s
2025-10-15 09:04:05 +00:00
1905e5cc03 chore(deps): update golang:1.25.3 docker digest to 2e3aca2
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m2s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m24s
2025-10-15 00:04:03 +00:00
a5e334f4ac chore(deps): update golang docker tag to v1.25.3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m0s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m23s
2025-10-14 00:04:02 +00:00
dc1f9e7a19 chore(deps): update dependency go to v1.25.3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m35s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m35s
2025-10-13 22:04:09 +00:00
cd248472f4 chore(deps): update node.js to v24.10.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m29s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m32s
2025-10-08 23:03:43 +00:00
76311c3603 fix(deps): update module golang.org/x/net to v0.46.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m31s
2025-10-08 22:04:01 +00:00
bba3b32bea fix(deps): update module golang.org/x/crypto to v0.43.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m31s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m33s
2025-10-08 19:10:05 +00:00
73cd04015c chore(deps): update golang docker tag to v1.25.2
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m33s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m24s
2025-10-07 22:03:43 +00:00
a9c4304ef8 fix(deps): update module golang.org/x/net to v0.45.0
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m25s
2025-10-07 20:25:52 +00:00
953d53e884 chore(deps): update node.js to v24.9.0
Some checks failed
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-10-07 20:25:36 +00:00
65b6223256 chore(deps): update node.js to v22.20.0
Some checks failed
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-10-07 20:22:40 +00:00
65fca6390f chore(deps): update tailwindcss monorepo to v4.1.14
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 1m18s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-10-07 20:10:23 +00:00
0394a04c3f chore(deps): update dependency go to v1.25.2
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m43s
2025-10-07 20:03:49 +00:00
29dfd4fa75 chore(deps): update golang:1.25.1 docker digest to d709837
Some checks failed
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-10-07 20:03:34 +00:00
12a0ef8c92 chore(deps): update debian:13.1 docker digest to fd8f5a1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m2s
2025-10-07 19:30:15 +00:00
65d70fd6df fix: linting errors
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m27s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m38s
2025-10-07 18:09:53 +02:00
278630f2e9 chore(deps): update golang:1.25.1 docker digest to 8305f5f
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m39s
2025-09-15 08:26:44 +00:00
bca9563525 chore(deps): update node.js to v24.8.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m35s
2025-09-11 10:08:35 +00:00
8e747efe5f chore(deps): update golang:1.25.1 docker digest to bb979b2
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m43s
2025-09-11 09:16:29 +00:00
37e5348d7e chore(deps): update node.js to afff6d8
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m36s
2025-09-10 10:12:19 +00:00
8e2b4f17aa chore(deps): update golang:1.25.1 docker digest to 1fd7d46
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m21s
2025-09-10 00:08:08 +00:00
d9be074ef3 chore(deps): update golang:1.25.1 docker digest to b773c94
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m41s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m29s
2025-09-09 21:08:04 +00:00
037ae74272 fix(deps): update module golang.org/x/net to v0.44.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m7s
2025-09-09 14:06:40 +00:00
fd42aa1160 chore(deps): update node.js to d6ba961
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m25s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m50s
2025-09-09 13:05:07 +00:00
b00c93262c chore(deps): update node.js to dc08161
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m11s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m58s
2025-09-09 10:08:55 +00:00
68431436fc chore(deps): update golang:1.25.1 docker digest to d6bdb04
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m12s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m6s
2025-09-09 09:05:25 +00:00
e59541a524 chore(deps): update golang:1.25.1 docker digest to 8919d35
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m15s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m11s
2025-09-09 08:05:11 +00:00
101069b2a6 chore(deps): update debian:13.1 docker digest to 833c135
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m20s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m39s
2025-09-09 07:26:18 +00:00
3759fd8d71 chore(deps): update golang:1.25.1 docker digest to 0caf875
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m16s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m44s
2025-09-09 00:06:01 +00:00
aa5636e361 chore(deps): update debian docker tag to v13.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m37s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m26s
2025-09-08 22:05:58 +00:00
ddaf7b8368 chore(deps): update dependency htmx.org to v2.0.7
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m2s
2025-09-08 18:05:33 +00:00
49e9b31a2d fix(deps): update module golang.org/x/crypto to v0.42.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m4s
2025-09-08 16:24:42 +00:00
5944208ca2 chore(deps): update actions/checkout action to v5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m17s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m20s
2025-09-05 22:08:44 +00:00
95767a8127 chore(deps): update golang:1.25.1 docker digest to a5e935d
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m54s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m35s
2025-09-05 18:06:03 +00:00
e16aec5f98 chore(deps): update tailwindcss monorepo to v4.1.13
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m1s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m22s
2025-09-04 18:06:18 +00:00
08dcc486d3 chore(deps): update golang:1.25.1 docker digest to 76a94c4
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m56s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m19s
2025-09-04 00:05:58 +00:00
0e130aeee4 chore(deps): update golang docker tag to v1.25.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m11s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m57s
2025-09-03 19:08:57 +00:00
01101fc2dd chore(deps): update dependency go to v1.25.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 7m17s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m31s
2025-09-03 18:06:29 +00:00
caedc4ce90 fix(deps): update opentelemetry-go monorepo
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m15s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m48s
2025-08-29 23:06:19 +00:00
8a3615b612 fix(deps): update opentelemetry-go-contrib monorepo
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 5m9s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Failing after 3m31s
2025-08-29 22:07:29 +00:00
efb1475f11 fix(deps): update module github.com/golang-migrate/migrate/v4 to v4.19.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m40s
2025-08-29 19:06:15 +00:00
b163495059 chore(deps): update node.js to 6fe2868
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m37s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m10s
2025-08-29 07:05:36 +00:00
24ede772c9 chore(deps): update node.js to c7fe411
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m33s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m13s
2025-08-29 04:05:31 +00:00
3aca37839c fix(deps): update module github.com/stretchr/testify to v1.11.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m17s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m5s
2025-08-29 01:42:53 +00:00
73449d495e chore(deps): update node.js to v22.19.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 11m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m32s
2025-08-29 01:14:29 +00:00
7652f823c8 chore(deps): update node.js to v24.7.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m56s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 7m6s
2025-08-27 22:06:11 +00:00
63c2594cbe chore(deps): update golang:1.25.0 docker digest to 5502b0e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m41s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m57s
2025-08-25 03:05:43 +00:00
52693e2846 fix(deps): update module github.com/stretchr/testify to v1.11.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m52s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m11s
2025-08-24 17:08:43 +00:00
d0faee2950 chore(deps): update golang:1.25.0 docker digest to 4859242
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m24s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m39s
2025-08-22 21:05:43 +00:00
01d459e913 fix: add "?" to validateString
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m52s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m36s
2025-08-20 04:41:29 +02:00
cee533694c chore(deps): update golang:1.25.0 docker digest to 91e2cd4
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m40s
2025-08-18 12:05:26 +00:00
f6283c6ab3 fix(deps): update module github.com/a-h/templ to v0.3.943
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m33s
2025-08-15 16:05:55 +00:00
e48d11b818 chore(deps): update node.js to v24.6.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m38s
2025-08-14 22:06:46 +00:00
b9150334ee chore(deps): update node.js to 3266bc9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m41s
2025-08-14 16:06:16 +00:00
d20981beaa fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.32
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m0s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m34s
2025-08-14 15:06:41 +00:00
3c95abe59c fix(deps): update module github.com/a-h/templ to v0.3.937
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m6s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m30s
2025-08-14 14:10:03 +00:00
fad2bd3928 chore(deps): update tailwindcss monorepo to v4.1.12
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m34s
2025-08-14 13:06:29 +00:00
c75b99ea9d chore(deps): update node.js to 73297e2
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m31s
2025-08-14 07:05:57 +00:00
56737a4156 chore(deps): update golang:1.25.0 docker digest to 9e56f0d
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m58s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m46s
2025-08-14 06:09:06 +00:00
ab425d759c chore(deps): update node.js to 58a2604
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m42s
2025-08-14 01:16:40 +00:00
283679fc4f chore(deps): update golang:1.25.0 docker digest to 10a15b9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m38s
2025-08-14 00:06:49 +00:00
e0802cf232 chore(deps): update dependency go to v1.25.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m19s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m43s
2025-08-13 23:10:18 +00:00
6577dbb297 chore(deps): update golang docker tag to v1.25.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m41s
2025-08-13 22:11:10 +00:00
7e37c24b07 chore(deps): update golang:1.24.6 docker digest to 746a0e9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m41s
2025-08-13 21:06:28 +00:00
3f3edbb8ad chore(deps): update golang:1.24.6 docker digest to 86a999d
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m37s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m22s
2025-08-13 18:06:20 +00:00
16429f1950 chore(deps): update debian docker tag to v13
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m55s
2025-08-13 15:25:14 +00:00
82a9fd8220 chore(deps): update golang:1.24.6 docker digest to 370491a
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 4m50s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-08-13 15:08:39 +00:00
192e6b7f50 chore(deps): update node.js to 5cc5271
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m37s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m33s
2025-08-13 02:06:22 +00:00
57377f9c27 chore(deps): update debian:12.11 docker digest to 731dd13
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m8s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m58s
2025-08-13 01:16:08 +00:00
66227c5818 chore(deps): update golang:1.24.6 docker digest to 0348485
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m44s
2025-08-13 00:08:49 +00:00
6e51e3c8b3 chore(deps): update debian:12.11 docker digest to 5626655
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m57s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m39s
2025-08-12 22:06:39 +00:00
6c916aecb4 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.31
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m0s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m39s
2025-08-11 15:06:14 +00:00
8575fbf56e chore(deps): update actions/checkout digest to 08eba0b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m38s
2025-08-11 11:05:53 +00:00
f820fcdfeb fix(deps): update module golang.org/x/net to v0.43.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m15s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m8s
2025-08-09 20:35:22 +00:00
f6e58b7afc fix(deps): update module golang.org/x/crypto to v0.41.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m2s
2025-08-09 02:06:26 +00:00
93e669b038 chore(deps): update golang docker tag to v1.24.6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m55s
2025-08-09 01:10:39 +00:00
d037317aab chore(deps): update dependency go to v1.24.6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m27s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m50s
2025-08-08 23:09:13 +00:00
0517e7ec89 feat(transaction): #243 add pagination to transactions
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m34s
2025-08-09 00:36:50 +02:00
867c0ca1cd chore(deps): update node.js to 3218f0d
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 4m3s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Failing after 2m50s
2025-08-04 16:07:53 +00:00
ddcbfaa075 chore(deps): update node.js to 0d98a9f
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m41s
2025-08-04 13:04:59 +00:00
4583c0a70e chore(deps): update node.js to v22.18.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m14s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m43s
2025-08-04 10:05:10 +00:00
380854272a chore(deps): update dependency echarts to v6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m9s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m48s
2025-08-01 22:42:20 +02:00
6bc9e0666b feat(layout): #211 optimize the overall layout for mobile
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m10s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m18s
move navigation to aside
proper mobile handling
update logo.svg
remove pirata-one/only use it for the logo
2025-08-01 22:26:17 +02:00
9fa554c60a chore(deps): update node.js to v24.5.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m37s
2025-07-31 22:08:19 +00:00
763c952cbe fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.30
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m7s
2025-07-30 13:06:28 +00:00
fce669146f fix(deps): update module github.com/a-h/templ to v0.3.924
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m16s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m5s
2025-07-26 16:05:39 +00:00
06219d1fd3 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.29
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m19s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m42s
2025-07-25 23:05:50 +00:00
9fac68d7ae chore(deps): update node.js to 37ff334
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 7m58s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m47s
2025-07-25 22:07:02 +00:00
8afd48b981 chore(deps): update golang:1.24.5 docker digest to ef5b4be
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 7m35s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m47s
2025-07-25 21:05:10 +00:00
25568591fd fix: build
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m8s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m39s
2025-07-25 22:47:07 +02:00
e1551c1fa3 chore(deps): update debian:12.11 docker digest to b6507e3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m8s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m49s
2025-07-22 04:06:22 +00:00
e8b3d3e16c fix(deps): update module github.com/a-h/templ to v0.3.920
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m5s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m26s
2025-07-20 19:06:40 +00:00
59288d4544 chore(deps): update node.js to 9e6918e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m59s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m27s
2025-07-17 01:06:01 +00:00
a2d1f22d46 chore(deps): update node.js to 414e20e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m40s
2025-07-16 22:09:53 +00:00
0e150b3d7d chore(deps): update node.js to v22.17.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m24s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m54s
2025-07-16 19:08:44 +00:00
19567313bd chore(deps): update node.js to v24.4.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m36s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m14s
2025-07-15 23:08:46 +00:00
42f1cfc07f fix(deps): update module golang.org/x/net to v0.42.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m3s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m30s
2025-07-10 20:06:21 +00:00
0276bc6a4c fix(deps): update module golang.org/x/crypto to v0.40.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m10s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m28s
2025-07-10 18:06:59 +00:00
d6c8559d4c chore(deps): update golang:1.24.5 docker digest to 14fd8a5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m2s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m2s
2025-07-09 21:05:29 +00:00
38cdd96b6f chore(deps): update golang docker tag to v1.24.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m3s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m0s
2025-07-09 19:05:32 +00:00
cb49494e60 chore(deps): update node.js to v24.4.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m57s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m33s
2025-07-09 13:08:35 +00:00
3ffe7514e2 chore(deps): update dependency go to v1.24.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m0s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m34s
2025-07-08 17:15:13 +00:00
d13a387303 chore(deps): update node.js to 2fa6c97
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m32s
2025-07-08 04:05:16 +00:00
a398d275f5 chore(deps): update node.js to 5307f5f
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m28s
2025-07-08 01:39:32 +00:00
23b97a9cac chore(deps): update node.js to df39165
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m32s
2025-07-07 22:06:05 +00:00
4b74a9b6d4 chore(deps): update golang:1.24.4 docker digest to 20a022e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m6s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m6s
2025-07-02 12:04:58 +00:00
c67f232e9b chore(deps): update golang:1.24.4 docker digest to 764d7e0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m4s
2025-07-02 00:06:32 +00:00
93727ee49a chore(deps): update golang:1.24.4 docker digest to a92f3b1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m1s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m7s
2025-07-01 21:05:23 +00:00
1a79df9423 chore(deps): update golang:1.24.4 docker digest to 1aa97dd
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m50s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m48s
2025-07-01 18:05:24 +00:00
582d265fd5 chore(deps): update golang:1.24.4 docker digest to 1bb140b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m56s
2025-07-01 15:05:25 +00:00
f094767582 chore(deps): update golang:1.24.4 docker digest to 270cd53
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m39s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m18s
2025-07-01 06:54:12 +00:00
440fed9ed1 chore(deps): update debian:12.11 docker digest to d42b86d
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m12s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m55s
2025-07-01 04:06:08 +00:00
6e1d24eef7 feat(dashboard): #191 add development of treasurechests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 12m24s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m53s
2025-06-30 00:39:14 +02:00
f37b50515b fix(deps): update opentelemetry-go-contrib monorepo
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m58s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m25s
2025-06-27 19:06:53 +00:00
b2a512d186 fix(deps): update opentelemetry-go monorepo
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m58s
2025-06-27 18:28:27 +00:00
4a28fb5ca4 chore(deps): update node.js to v24.3.0
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 5m19s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-06-27 18:11:32 +00:00
e5f98c1fb0 chore(deps): update node.js to v22.17.0
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m13s
2025-06-27 17:48:00 +00:00
3072df6507 fix(deps): update module github.com/a-h/templ to v0.3.906
Some checks failed
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-06-27 17:42:29 +00:00
9f35ca7476 chore(deps): update tailwindcss monorepo to v4.1.11
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 5m3s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-06-27 17:24:55 +00:00
472ab68986 chore(deps): update dependency htmx.org to v2.0.6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m21s
2025-06-27 16:33:51 +00:00
2fd2200ac2 fix: enable dot in stings
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m59s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m57s
2025-06-22 19:40:59 +02:00
a58ddb7a1d chore(deps): update dependency htmx.org to v2.0.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m34s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m5s
2025-06-20 22:06:46 +00:00
147d57f6e5 feat: #194 enable sqlite wal mode for better performace
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m57s
2025-06-20 22:55:59 +02:00
d064626197 feat: #193 disable session handling for static content
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m41s
2025-06-20 22:46:13 +02:00
72869e5c68 feat(dashboard): #192 include treemap of treasure chests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m36s
2025-06-20 21:31:37 +02:00
3120c19669 feat(dashboard): #82 add chart for sum of account and savings
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m21s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m33s
2025-06-19 18:05:36 +02:00
c9bf320611 chore: include node version manager
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m51s
2025-06-19 12:31:55 +02:00
3b3343bdb5 fix(observabillity): set service.name resource on metrics and traces
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m23s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m34s
2025-06-17 13:20:13 +02:00
6c92206b3c fix(observabillity): propagate ctx to every log call and add resource to logging
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m5s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m36s
2025-06-17 12:59:43 +02:00
ff3c7bdf52 fix(observabillity): add service_name tag to logs
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m18s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m0s
2025-06-16 23:22:31 +02:00
06a8c80f1d feat(dashboard): #162 include total sum of accounts compared to total savings
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m15s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m7s
2025-06-16 23:00:38 +02:00
596cc602d0 feat(auth): #182 cleanup expired tokens
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m19s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m7s
2025-06-16 22:42:23 +02:00
3df9fab25b fix(dashboard): #163 month selection on first load
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m54s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m56s
2025-06-16 13:00:35 +02:00
6b8059889d feat(dashboard): #163 first summary
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m11s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m6s
2025-06-15 22:04:50 +02:00
935019c1c4 chore(deps): update golang:1.24.4 docker digest to 10c1318
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m44s
2025-06-12 09:05:30 +00:00
a9d8e10592 chore(deps): update node.js to 71bcbb3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m32s
2025-06-12 07:05:32 +00:00
e8a13dc8e7 chore(deps): update node.js to 68cf33c
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m36s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m28s
2025-06-12 04:06:04 +00:00
96b4ac414e chore(deps): update node.js to 2040569
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m25s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m32s
2025-06-12 02:06:05 +00:00
95340547e6 chore(deps): update golang:1.24.4 docker digest to 3178db8
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 5m18s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Failing after 7m28s
2025-06-12 00:06:22 +00:00
58547099bc chore(deps): update tailwindcss monorepo to v4.1.10
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m19s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m17s
2025-06-11 23:06:53 +00:00
67259d5110 chore(deps): update node.js to f627d0e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m14s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m38s
2025-06-11 22:07:17 +00:00
67d10b2b95 chore(deps): update golang:1.24.4 docker digest to 884849e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m20s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m32s
2025-06-11 21:05:47 +00:00
910d8848d8 chore(deps): update golang:1.24.4 docker digest to 8806e87
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m35s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m23s
2025-06-11 18:09:08 +00:00
0fd18fbb4f chore(deps): update tailwindcss monorepo to v4.1.9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m57s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m31s
2025-06-11 16:07:07 +00:00
9843db9402 chore(deps): update golang:1.24.4 docker digest to dc3de88
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m8s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m51s
2025-06-11 15:06:04 +00:00
baf44d680b chore(deps): update node.js to 6a2972b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m2s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m59s
2025-06-11 04:10:15 +00:00
0a6cc5c771 chore(deps): update golang:1.24.4 docker digest to d1db785
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m45s
2025-06-11 03:06:08 +00:00
fa82ce34dc chore(deps): update debian:12.11 docker digest to 0d8498a
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m21s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m49s
2025-06-11 02:09:19 +00:00
c4719db21f chore(deps): update golang:1.24.4 docker digest to 01f861b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m23s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m9s
2025-06-11 00:05:43 +00:00
2e1a0eedd0 fix: sql error
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m11s
2025-06-08 18:34:08 +02:00
11f3bcc89f feat(observabillity): #153 instrument sqlx
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m29s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m49s
2025-06-07 22:04:29 +02:00
c4aca2778f fix(observabillity): update otel-collector endpoint
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m33s
2025-06-07 15:57:13 +02:00
63ade5916e fix(observabillity): include otel logs
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m33s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m4s
2025-06-07 15:32:43 +02:00
75 changed files with 2375 additions and 1274 deletions

View File

@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- run: docker build . -t spend-sparrow-test - run: docker build . -t spend-sparrow-test
- run: docker rmi spend-sparrow-test - run: docker rmi spend-sparrow-test

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- run: docker login git.wundenbergs.de -u tim -p ${{ secrets.DOCKER_GITEA_TOKEN }} - run: docker login git.wundenbergs.de -u tim -p ${{ secrets.DOCKER_GITEA_TOKEN }}
- run: docker build . -t git.wundenbergs.de/x/spend-sparrow:latest -t git.wundenbergs.de/x/spend-sparrow:$GITHUB_SHA - run: docker build . -t git.wundenbergs.de/x/spend-sparrow:latest -t git.wundenbergs.de/x/spend-sparrow:$GITHUB_SHA
- run: docker push git.wundenbergs.de/x/spend-sparrow:latest - run: docker push git.wundenbergs.de/x/spend-sparrow:latest

3
.gitignore vendored
View File

@@ -25,12 +25,13 @@ go.work.sum
# env file # env file
.env .env
*.db data/
secrets/ secrets/
node_modules/ node_modules/
static/css/tailwind.css static/css/tailwind.css
static/js/htmx.min.js static/js/htmx.min.js
static/js/echarts.min.js
tmp/ tmp/
mocks/* mocks/*

View File

@@ -24,6 +24,9 @@ linters:
- cyclop - cyclop
- contextcheck - contextcheck
- bodyclose # i don't care in the tests, the implementation itself doesn't do http requests - bodyclose # i don't care in the tests, the implementation itself doesn't do http requests
- wsl_v5
- noinlineerr
- unqueryvet
settings: settings:
nestif: nestif:
min-complexity: 6 min-complexity: 6

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
24.11.0

View File

@@ -1,4 +1,4 @@
FROM golang:1.24.4@sha256:db5d0afbfb4ab648af2393b92e87eaae9ad5e01132803d80caef91b5752d289c AS builder_go FROM golang:1.25.3@sha256:7e3cbcd2f6af1bebb937462ec29f77ce28b406081af509afed158fa8721f11af AS builder_go
WORKDIR /spend-sparrow WORKDIR /spend-sparrow
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
RUN go install github.com/a-h/templ/cmd/templ@latest RUN go install github.com/a-h/templ/cmd/templ@latest
@@ -13,7 +13,7 @@ RUN golangci-lint run ./...
RUN go build -o /spend-sparrow/spend-sparrow . RUN go build -o /spend-sparrow/spend-sparrow .
FROM node:22.16.0@sha256:0b5b940c21ab03353de9042f9166c75bcfc53c4cd0508c7fd88576646adbf875 AS builder_node FROM node:24.11.0@sha256:e5bbac0e9b8a6e3b96a86a82bbbcf4c533a879694fd613ed616bae5116f6f243 AS builder_node
WORKDIR /spend-sparrow WORKDIR /spend-sparrow
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm clean-install RUN npm clean-install
@@ -21,7 +21,7 @@ COPY . ./
RUN npm run build RUN npm run build
FROM debian:12.11@sha256:bd73076dc2cd9c88f48b5b358328f24f2a4289811bd73787c031e20db9f97123 FROM debian:13.1@sha256:01a723bf5bfb21b9dda0c9a33e0538106e4d02cce8f557e118dd61259553d598
WORKDIR /spend-sparrow WORKDIR /spend-sparrow
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env
COPY migration ./migration COPY migration ./migration

View File

@@ -3,6 +3,15 @@
SpendSparrow is a web app to keep track of expenses and income. It is very opinionated by keeping an keen eye on disciplin of it's users. Every Expense needs to be mapped to a Piggy Bank. For emergencies, funds can be moved between Piggy Banks. SpendSparrow is a web app to keep track of expenses and income. It is very opinionated by keeping an keen eye on disciplin of it's users. Every Expense needs to be mapped to a Piggy Bank. For emergencies, funds can be moved between Piggy Banks.
## Prerequisites
```bash
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
go install github.com/a-h/templ/cmd/templ@latest
go install github.com/vektra/mockery/v2@latest
```
## Design priciples ## Design priciples
The State of the application can always be calculated on the fly. Even though it is not an Event Streaming Application, it is still important to be able to recalculate historic data. The State of the application can always be calculated on the fly. Even though it is not an Event Streaming Application, it is still important to be able to recalculate historic data.
@@ -10,3 +19,4 @@ It may be applicable to do some sort of monthly snapshots to speed up calculatio
This applications uses as little dependencies as feasible, especially on the front end. This applications uses as little dependencies as feasible, especially on the front end.

View File

@@ -0,0 +1,188 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="logo-inkscape.svg"
inkscape:export-filename="static/logo.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="2.5914723"
inkscape:cx="385.10927"
inkscape:cy="275.712"
inkscape:window-width="2252"
inkscape:window-height="1450"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer2" />
<defs
id="defs1">
<rect
x="115.37843"
y="80.263254"
width="470.38898"
height="197.18521"
id="rect2" />
<rect
x="175.18448"
y="463.06726"
width="253.29221"
height="303.50433"
id="rect6" />
</defs>
<g
inkscape:label="Faviocon"
inkscape:groupmode="layer"
id="layer1">
<path
d="m 59.240389,97.978247 c 1.775354,-0.394229 4.087813,-2.156354 4.439709,-3.024187 0.206375,-0.508 -0.822855,-1.30175 -1.098021,-1.621896 -0.629709,-0.73025 -0.375709,-1.090083 -0.132292,-1.960562 0.277813,-0.989542 -0.381,-2.082271 -1.314979,-2.510896 -0.933979,-0.428625 -2.050521,-0.293688 -2.989792,0.124354 -0.939271,0.418042 -1.740958,1.090083 -2.526771,1.751542 -0.574145,-0.36248 -1.489604,-1.963209 -2.97127,-0.923396 -1.023938,0.717021 -1.116542,2.278062 -0.98425,3.52425 0.309562,2.876021 1.018645,4.368271 2.354791,4.770437 1.688042,0.508 3.556,0.240771 5.222875,-0.129646"
style="fill:#ffca28;stroke-width:0.264583"
id="path1-3"
inkscape:export-filename="static/favicon.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<path
d="m 62.124348,89.704727 c -0.224896,3.876145 -4.005792,6.447895 -5.799667,7.580312 l 1.164167,1.000125 c 0,0 0.738187,0.01588 1.748895,-0.306917 1.733021,-0.550333 4.265084,-2.106083 4.439709,-3.024187 0.256646,-1.336146 -1.113896,-1.045104 -1.423459,-2.100792 -0.161395,-0.558271 0.785813,-1.613958 -0.129645,-3.148541 m -6.503459,1.03452 c 0,0 -0.674687,-0.690562 -1.17475,-1.005416 -0.248708,0.468312 -0.425979,0.976312 -0.513291,1.500187 -0.156105,0.92075 0,2.227792 0.36777,3.201459 0.05821,0.150812 0.275167,0.127 0.29898,-0.03175 0.3175,-2.092855 1.021291,-3.66448 1.021291,-3.66448"
style="fill:#e2a610;stroke-width:0.264583"
id="path2-6" />
<path
d="m 50.906014,97.636935 c 0,0 -8.252354,0.891646 -11.975042,8.056565 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490104,-1.30175 6.863292,-4.0005 6.606646,-6.60664 -0.373062,-3.80471 -3.907896,-6.14363 -3.907896,-6.14363 0,0 0.140229,-4.699 -3.505729,-7.749647 -3.235854,-2.709333 -7.289271,-1.486958 -7.289271,-1.486958"
style="fill:#ffca28;stroke-width:0.264583"
id="path3-0" />
<path
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
style="fill:#6d4c41;stroke-width:0.264583"
id="path6-6" />
<path
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
style="fill:#6b4b46;stroke-width:0.264583"
id="path7-2" />
<path
d="m 60.042077,103.11381 c 0.280458,-0.19314 1.222375,0.14023 1.486958,1.98438 0.129646,0.90223 0.169333,1.77535 0.169333,1.77535 0,0 -1.11125,-0.99748 -1.47902,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
style="fill:#e2a610;stroke-width:0.264583"
id="path8-6" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="82.355011"
y="90.66716"
id="text4-9"
inkscape:label="$"
transform="rotate(20.578693)"><tspan
sodipodi:role="line"
id="tspan4-2"
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
x="82.355011"
y="90.66716">$</tspan></text>
</g>
<g
inkscape:label="Logo"
inkscape:groupmode="layer"
id="g7"
transform="translate(1.4293676,-48.496402)">
<g
id="g8"
inkscape:label="Favicon"
transform="translate(-38.797122,-28.178962)"
inkscape:export-filename="../static/logo.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<path
d="m 98.874384,115.02659 c 1.775356,-0.39423 4.087816,-2.15635 4.439706,-3.02419 0.20638,-0.508 -0.82285,-1.30175 -1.09802,-1.62189 -0.62971,-0.73025 -0.37571,-1.09009 -0.13229,-1.96057 0.27781,-0.98954 -0.381,-2.08227 -1.31498,-2.51089 -0.933978,-0.42863 -2.05052,-0.29369 -2.989791,0.12435 -0.939271,0.41804 -1.740958,1.09009 -2.526771,1.75154 -0.574145,-0.36248 -1.489604,-1.96321 -2.97127,-0.92339 -1.023938,0.71702 -1.116542,2.27806 -0.98425,3.52425 0.309562,2.87602 1.018645,4.36827 2.354791,4.77043 1.688042,0.508 3.556,0.24078 5.222875,-0.12964"
style="fill:#ffca28;stroke-width:0.264583"
id="path1-3-3" />
<path
d="m 101.75834,106.75307 c -0.22489,3.87614 -4.005789,6.44789 -5.799664,7.58031 l 1.164167,1.00013 c 0,0 0.738187,0.0159 1.748895,-0.30692 1.733022,-0.55033 4.265082,-2.10608 4.439712,-3.02419 0.25664,-1.33614 -1.1139,-1.0451 -1.42346,-2.10079 -0.1614,-0.55827 0.78581,-1.61396 -0.12965,-3.14854 m -6.503456,1.03452 c 0,0 -0.674687,-0.69056 -1.17475,-1.00542 -0.248708,0.46832 -0.425979,0.97632 -0.513291,1.50019 -0.156105,0.92075 0,2.22779 0.36777,3.20146 0.05821,0.15081 0.275167,0.127 0.29898,-0.0317 0.3175,-2.09286 1.021291,-3.66448 1.021291,-3.66448"
style="fill:#e2a610;stroke-width:0.264583"
id="path2-6-6" />
<path
d="m 90.540009,114.68528 c 0,0 -8.252354,0.89164 -11.975042,8.05656 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490101,-1.30175 6.863291,-4.0005 6.606641,-6.60664 -0.37306,-3.80471 -3.90789,-6.14363 -3.90789,-6.14363 0,0 0.14023,-4.699 -3.50573,-7.74964 -3.235854,-2.70934 -7.289271,-1.48696 -7.289271,-1.48696"
style="fill:#ffca28;stroke-width:0.264583"
id="path3-0-1" />
<path
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
style="fill:#6d4c41;stroke-width:0.264583"
id="path6-6-2" />
<path
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
style="fill:#6b4b46;stroke-width:0.264583"
id="path7-2-9" />
<path
d="m 99.676072,120.16215 c 0.280458,-0.19314 1.222378,0.14023 1.486958,1.98438 0.12965,0.90223 0.16933,1.77535 0.16933,1.77535 0,0 -1.11125,-0.99748 -1.479017,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
style="fill:#e2a610;stroke-width:0.264583"
id="path8-6-3" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="125.45235"
y="92.696564"
id="text4-9-1"
inkscape:label="$"
transform="rotate(20.578693)"><tspan
sodipodi:role="line"
id="tspan4-2-9"
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
x="125.45235"
y="92.696564">$</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Text"
transform="translate(-1.4293676,48.496402)">
<text
xml:space="preserve"
style="font-size:17.6389px;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="57.635151"
y="55.655094"
id="text1"
inkscape:label="SpendSparrow"><tspan
sodipodi:role="line"
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.6389px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:-0.529167px;fill:#4d4d4d;stroke:none;stroke-width:0.264583"
x="57.635151"
y="55.655094">pendSparrow</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#1a1a1a;stroke-width:0.264583"
x="93.314896"
y="91.227318"
id="text5"><tspan
sodipodi:role="line"
id="tspan5"
style="stroke-width:0.264583"
x="93.314896"
y="91.227318" /></text>
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text6"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:74.6667px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-2px;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect6);display:inline;fill:#1a1a1a;stroke:none" />
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text2"
style="fill:#4d4d4d;text-orientation:auto;-inkscape-font-specification:'Pirata One, Normal';font-family:'Pirata One';font-size:74.66666667px;letter-spacing:-2px;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect2)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

6
dev.sh
View File

@@ -1,3 +1,9 @@
#!/bin/sh
set -e
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
go install github.com/a-h/templ/cmd/templ@latest
go install github.com/vektra/mockery/v2@latest
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ." & templ generate --watch --proxy="http://localhost:8080" --cmd="go run ." &
npm run watch npm run watch

65
go.mod
View File

@@ -1,53 +1,54 @@
module spend-sparrow module spend-sparrow
go 1.23.0 go 1.24.0
toolchain go1.24.4 toolchain go1.25.3
require ( require (
github.com/a-h/templ v0.3.898 github.com/a-h/templ v0.3.960
github.com/golang-migrate/migrate/v4 v4.18.3 github.com/golang-migrate/migrate/v4 v4.19.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.28 github.com/mattn/go-sqlite3 v1.14.32
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.11.1
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0 github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2
go.opentelemetry.io/otel v1.36.0 go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0
go.opentelemetry.io/otel/log v0.12.2 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0
go.opentelemetry.io/otel/sdk v1.36.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
go.opentelemetry.io/otel/sdk/log v0.12.2 go.opentelemetry.io/otel/log v0.14.0
go.opentelemetry.io/otel/sdk/metric v1.36.0 go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/trace v1.36.0 go.opentelemetry.io/otel/sdk/log v0.14.0
golang.org/x/crypto v0.39.0 go.opentelemetry.io/otel/sdk/metric v1.38.0
golang.org/x/net v0.41.0 go.opentelemetry.io/otel/trace v1.38.0
golang.org/x/crypto v0.43.0
golang.org/x/net v0.46.0
) )
require ( require (
github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/atomic v1.11.0 // indirect golang.org/x/sys v0.37.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.30.0 // indirect
golang.org/x/text v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect google.golang.org/grpc v1.75.0 // indirect
google.golang.org/grpc v1.72.1 // indirect google.golang.org/protobuf v1.36.8 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

128
go.sum
View File

@@ -1,30 +1,30 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s= github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -41,68 +41,72 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c=
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ=
github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2 h1:zA9ZXfdtowo0EKt+t7uqXNlHxPeygrxuFSIroiBVgPU=
github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2/go.mod h1:ySXmuW9JLCm/TjsQksuMY/7MNiWqfHnhH2xeT34uOLU=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0 h1:EMIiYTms4Z4m3bBuKp1VmMNRLZcl6j4YbvOPL1IhlWo= go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0/go.mod h1:DIEZmUR7tzuOOVUTDKvkGWtYWSHFV18Qg8+GMb8wPJw= go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc= go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E= go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0= go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY= go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0= go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE= go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -3,37 +3,34 @@
@source './static/**/*.js'; @source './static/**/*.js';
@source './template/**/*.templ'; @source './template/**/*.templ';
body {
@apply font-garamond text-gray-700;
}
input:focus {
@apply outline-none ring-0;
}
@font-face {
font-family: "Pirata One";
src: url("/static/font/PirataOne-Regular.woff2") format("woff2");
}
@font-face { @font-face {
font-family: "EB Garamond"; font-family: "EB Garamond";
src: url("/static/font/EBGaramond-VariableFont_wght.woff2") format("woff2"); src: url("/static/font/EBGaramond-VariableFont_wght.woff2") format("woff2");
} }
@theme { body {
--font-pirata: "Pirata One", serif; font-family: "EB Garamond", serif;
--font-garamond: "EB Garamond", serif; @apply text-gray-700;
}
input:focus {
@apply outline-none ring-0;
}
button {
@apply cursor-pointer;
} }
/* Button */ /* Button */
.button { .button {
transition: all 150ms linear; transition: all 150ms linear;
@apply cursor-pointer border-2 rounded-lg border-transparent; @apply cursor-pointer border-2 rounded-lg border-transparent;
} }
.button-primary:hover, .button-primary:hover,
.button-normal:hover { .button-normal:hover {
transform: translate(-0.25rem, -0.25rem); transform: translate(-0.25rem, -0.25rem);
box-shadow: 3px 3px 3px var(--color-gray-200); box-shadow: 3px 3px 3px var(--color-gray-200);
} }
.button-primary { .button-primary {
@@ -61,3 +58,5 @@ input:focus {
box-shadow: 0 0 0 2px var(--color-gray-200); box-shadow: 0 0 0 2px var(--color-gray-200);
} }

View File

@@ -1,6 +1,7 @@
package db package db
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"log/slog" "log/slog"
@@ -13,23 +14,24 @@ import (
) )
type Auth interface { type Auth interface {
InsertUser(user *types.User) error InsertUser(ctx context.Context, user *types.User) error
UpdateUser(user *types.User) error UpdateUser(ctx context.Context, user *types.User) error
GetUserByEmail(email string) (*types.User, error) GetUserByEmail(ctx context.Context, email string) (*types.User, error)
GetUser(userId uuid.UUID) (*types.User, error) GetUser(ctx context.Context, userId uuid.UUID) (*types.User, error)
DeleteUser(userId uuid.UUID) error DeleteUser(ctx context.Context, userId uuid.UUID) error
InsertToken(token *types.Token) error InsertToken(ctx context.Context, token *types.Token) error
GetToken(token string) (*types.Token, error) GetToken(ctx context.Context, token string) (*types.Token, error)
GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error) GetTokensByUserIdAndType(ctx context.Context, userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error)
GetTokensBySessionIdAndType(sessionId string, tokenType types.TokenType) ([]*types.Token, error) GetTokensBySessionIdAndType(ctx context.Context, sessionId string, tokenType types.TokenType) ([]*types.Token, error)
DeleteToken(token string) error DeleteToken(ctx context.Context, token string) error
InsertSession(session *types.Session) error InsertSession(ctx context.Context, session *types.Session) error
GetSession(sessionId string) (*types.Session, error) GetSession(ctx context.Context, sessionId string) (*types.Session, error)
GetSessions(userId uuid.UUID) ([]*types.Session, error) GetSessions(ctx context.Context, userId uuid.UUID) ([]*types.Session, error)
DeleteSession(sessionId string) error DeleteSession(ctx context.Context, sessionId string) error
DeleteOldSessions(userId uuid.UUID) error DeleteOldSessions(ctx context.Context) error
DeleteOldTokens(ctx context.Context) error
} }
type AuthSqlite struct { type AuthSqlite struct {
@@ -40,8 +42,8 @@ func NewAuthSqlite(db *sqlx.DB) *AuthSqlite {
return &AuthSqlite{db: db} return &AuthSqlite{db: db}
} }
func (db AuthSqlite) InsertUser(user *types.User) error { func (db AuthSqlite) InsertUser(ctx context.Context, user *types.User) error {
_, err := db.db.Exec(` _, err := db.db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, email_verified_at, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, email_verified_at, is_admin, password, salt, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
user.Id, user.Email, user.EmailVerified, user.EmailVerifiedAt, user.IsAdmin, user.Password, user.Salt, user.CreateAt) user.Id, user.Email, user.EmailVerified, user.EmailVerifiedAt, user.IsAdmin, user.Password, user.Salt, user.CreateAt)
@@ -51,29 +53,29 @@ func (db AuthSqlite) InsertUser(user *types.User) error {
return ErrAlreadyExists return ErrAlreadyExists
} }
slog.Error("SQL error InsertUser", "err", err) slog.ErrorContext(ctx, "SQL error InsertUser", "err", err)
return types.ErrInternal return types.ErrInternal
} }
return nil return nil
} }
func (db AuthSqlite) UpdateUser(user *types.User) error { func (db AuthSqlite) UpdateUser(ctx context.Context, user *types.User) error {
_, err := db.db.Exec(` _, err := db.db.ExecContext(ctx, `
UPDATE user UPDATE user
SET email_verified = ?, email_verified_at = ?, password = ? SET email_verified = ?, email_verified_at = ?, password = ?
WHERE user_id = ?`, WHERE user_id = ?`,
user.EmailVerified, user.EmailVerifiedAt, user.Password, user.Id) user.EmailVerified, user.EmailVerifiedAt, user.Password, user.Id)
if err != nil { if err != nil {
slog.Error("SQL error UpdateUser", "err", err) slog.ErrorContext(ctx, "SQL error UpdateUser", "err", err)
return types.ErrInternal return types.ErrInternal
} }
return nil return nil
} }
func (db AuthSqlite) GetUserByEmail(email string) (*types.User, error) { func (db AuthSqlite) GetUserByEmail(ctx context.Context, email string) (*types.User, error) {
var ( var (
userId uuid.UUID userId uuid.UUID
emailVerified bool emailVerified bool
@@ -84,7 +86,7 @@ func (db AuthSqlite) GetUserByEmail(email string) (*types.User, error) {
createdAt time.Time createdAt time.Time
) )
err := db.db.QueryRow(` err := db.db.QueryRowContext(ctx, `
SELECT user_id, email_verified, email_verified_at, password, salt, created_at SELECT user_id, email_verified, email_verified_at, password, salt, created_at
FROM user FROM user
WHERE email = ?`, email).Scan(&userId, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt) WHERE email = ?`, email).Scan(&userId, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt)
@@ -92,7 +94,7 @@ func (db AuthSqlite) GetUserByEmail(email string) (*types.User, error) {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound return nil, ErrNotFound
} else { } else {
slog.Error("SQL error GetUser", "err", err) slog.ErrorContext(ctx, "SQL error GetUser", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
} }
@@ -100,7 +102,7 @@ func (db AuthSqlite) GetUserByEmail(email string) (*types.User, error) {
return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
} }
func (db AuthSqlite) GetUser(userId uuid.UUID) (*types.User, error) { func (db AuthSqlite) GetUser(ctx context.Context, userId uuid.UUID) (*types.User, error) {
var ( var (
email string email string
emailVerified bool emailVerified bool
@@ -111,7 +113,7 @@ func (db AuthSqlite) GetUser(userId uuid.UUID) (*types.User, error) {
createdAt time.Time createdAt time.Time
) )
err := db.db.QueryRow(` err := db.db.QueryRowContext(ctx, `
SELECT email, email_verified, email_verified_at, password, salt, created_at SELECT email, email_verified, email_verified_at, password, salt, created_at
FROM user FROM user
WHERE user_id = ?`, userId).Scan(&email, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt) WHERE user_id = ?`, userId).Scan(&email, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt)
@@ -119,7 +121,7 @@ func (db AuthSqlite) GetUser(userId uuid.UUID) (*types.User, error) {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound return nil, ErrNotFound
} else { } else {
slog.Error("SQL error GetUser", "err", err) slog.ErrorContext(ctx, "SQL error GetUser", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
} }
@@ -127,78 +129,78 @@ func (db AuthSqlite) GetUser(userId uuid.UUID) (*types.User, error) {
return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
} }
func (db AuthSqlite) DeleteUser(userId uuid.UUID) error { func (db AuthSqlite) DeleteUser(ctx context.Context, userId uuid.UUID) error {
tx, err := db.db.Begin() tx, err := db.db.BeginTx(ctx, nil)
if err != nil { if err != nil {
slog.Error("Could not start transaction", "err", err) slog.ErrorContext(ctx, "Could not start transaction", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM account WHERE user_id = ?", userId) _, err = tx.ExecContext(ctx, "DELETE FROM account WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
slog.Error("Could not delete accounts", "err", err) slog.ErrorContext(ctx, "Could not delete accounts", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM token WHERE user_id = ?", userId) _, err = tx.ExecContext(ctx, "DELETE FROM token WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
slog.Error("Could not delete user tokens", "err", err) slog.ErrorContext(ctx, "Could not delete user tokens", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM session WHERE user_id = ?", userId) _, err = tx.ExecContext(ctx, "DELETE FROM session WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
slog.Error("Could not delete sessions", "err", err) slog.ErrorContext(ctx, "Could not delete sessions", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM user WHERE user_id = ?", userId) _, err = tx.ExecContext(ctx, "DELETE FROM user WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
slog.Error("Could not delete user", "err", err) slog.ErrorContext(ctx, "Could not delete user", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM treasure_chest WHERE user_id = ?", userId) _, err = tx.ExecContext(ctx, "DELETE FROM treasure_chest WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
slog.Error("Could not delete user", "err", err) slog.ErrorContext(ctx, "Could not delete user", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM \"transaction\" WHERE user_id = ?", userId) _, err = tx.ExecContext(ctx, "DELETE FROM \"transaction\" WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
slog.Error("Could not delete user", "err", err) slog.ErrorContext(ctx, "Could not delete user", "err", err)
return types.ErrInternal return types.ErrInternal
} }
err = tx.Commit() err = tx.Commit()
if err != nil { if err != nil {
slog.Error("Could not commit transaction", "err", err) slog.ErrorContext(ctx, "Could not commit transaction", "err", err)
return types.ErrInternal return types.ErrInternal
} }
return nil return nil
} }
func (db AuthSqlite) InsertToken(token *types.Token) error { func (db AuthSqlite) InsertToken(ctx context.Context, token *types.Token) error {
_, err := db.db.Exec(` _, err := db.db.ExecContext(ctx, `
INSERT INTO token (user_id, session_id, type, token, created_at, expires_at) INSERT INTO token (user_id, session_id, type, token, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?)`, token.UserId, token.SessionId, token.Type, token.Token, token.CreatedAt, token.ExpiresAt) VALUES (?, ?, ?, ?, ?, ?)`, token.UserId, token.SessionId, token.Type, token.Token, token.CreatedAt, token.ExpiresAt)
if err != nil { if err != nil {
slog.Error("Could not insert token", "err", err) slog.ErrorContext(ctx, "Could not insert token", "err", err)
return types.ErrInternal return types.ErrInternal
} }
return nil return nil
} }
func (db AuthSqlite) GetToken(token string) (*types.Token, error) { func (db AuthSqlite) GetToken(ctx context.Context, token string) (*types.Token, error) {
var ( var (
userId uuid.UUID userId uuid.UUID
sessionId string sessionId string
@@ -209,67 +211,67 @@ func (db AuthSqlite) GetToken(token string) (*types.Token, error) {
expiresAt time.Time expiresAt time.Time
) )
err := db.db.QueryRow(` err := db.db.QueryRowContext(ctx, `
SELECT user_id, session_id, type, created_at, expires_at SELECT user_id, session_id, type, created_at, expires_at
FROM token FROM token
WHERE token = ?`, token).Scan(&userId, &sessionId, &tokenType, &createdAtStr, &expiresAtStr) WHERE token = ?`, token).Scan(&userId, &sessionId, &tokenType, &createdAtStr, &expiresAtStr)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
slog.Info("Token not found", "token", token) slog.InfoContext(ctx, "Token not found", "token", token)
return nil, ErrNotFound return nil, ErrNotFound
} else { } else {
slog.Error("Could not get token", "err", err) slog.ErrorContext(ctx, "Could not get token", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
} }
createdAt, err = time.Parse(time.RFC3339, createdAtStr) createdAt, err = time.Parse(time.RFC3339, createdAtStr)
if err != nil { if err != nil {
slog.Error("Could not parse token.created_at", "err", err) slog.ErrorContext(ctx, "Could not parse token.created_at", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr) expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
if err != nil { if err != nil {
slog.Error("Could not parse token.expires_at", "err", err) slog.ErrorContext(ctx, "Could not parse token.expires_at", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
return types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil return types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil
} }
func (db AuthSqlite) GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error) { func (db AuthSqlite) GetTokensByUserIdAndType(ctx context.Context, userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error) {
query, err := db.db.Query(` query, err := db.db.QueryContext(ctx, `
SELECT token, created_at, expires_at SELECT token, created_at, expires_at
FROM token FROM token
WHERE user_id = ? WHERE user_id = ?
AND type = ?`, userId, tokenType) AND type = ?`, userId, tokenType)
if err != nil { if err != nil {
slog.Error("Could not get token", "err", err) slog.ErrorContext(ctx, "Could not get token", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
return getTokensFromQuery(query, userId, "", tokenType) return getTokensFromQuery(ctx, query, userId, "", tokenType)
} }
func (db AuthSqlite) GetTokensBySessionIdAndType(sessionId string, tokenType types.TokenType) ([]*types.Token, error) { func (db AuthSqlite) GetTokensBySessionIdAndType(ctx context.Context, sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
query, err := db.db.Query(` query, err := db.db.QueryContext(ctx, `
SELECT token, created_at, expires_at SELECT token, created_at, expires_at
FROM token FROM token
WHERE session_id = ? WHERE session_id = ?
AND type = ?`, sessionId, tokenType) AND type = ?`, sessionId, tokenType)
if err != nil { if err != nil {
slog.Error("Could not get token", "err", err) slog.ErrorContext(ctx, "Could not get token", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
return getTokensFromQuery(query, uuid.Nil, sessionId, tokenType) return getTokensFromQuery(ctx, query, uuid.Nil, sessionId, tokenType)
} }
func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tokenType types.TokenType) ([]*types.Token, error) { func getTokensFromQuery(ctx context.Context, query *sql.Rows, userId uuid.UUID, sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
var tokens []*types.Token var tokens []*types.Token
hasRows := false hasRows := false
@@ -286,19 +288,19 @@ func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tok
err := query.Scan(&token, &createdAtStr, &expiresAtStr) err := query.Scan(&token, &createdAtStr, &expiresAtStr)
if err != nil { if err != nil {
slog.Error("Could not scan token", "err", err) slog.ErrorContext(ctx, "Could not scan token", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
createdAt, err = time.Parse(time.RFC3339, createdAtStr) createdAt, err = time.Parse(time.RFC3339, createdAtStr)
if err != nil { if err != nil {
slog.Error("Could not parse token.created_at", "err", err) slog.ErrorContext(ctx, "Could not parse token.created_at", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr) expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
if err != nil { if err != nil {
slog.Error("Could not parse token.expires_at", "err", err) slog.ErrorContext(ctx, "Could not parse token.expires_at", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -312,82 +314,92 @@ func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tok
return tokens, nil return tokens, nil
} }
func (db AuthSqlite) DeleteToken(token string) error { func (db AuthSqlite) DeleteToken(ctx context.Context, token string) error {
_, err := db.db.Exec("DELETE FROM token WHERE token = ?", token) _, err := db.db.ExecContext(ctx, "DELETE FROM token WHERE token = ?", token)
if err != nil { if err != nil {
slog.Error("Could not delete token", "err", err) slog.ErrorContext(ctx, "Could not delete token", "err", err)
return types.ErrInternal return types.ErrInternal
} }
return nil return nil
} }
func (db AuthSqlite) InsertSession(session *types.Session) error { func (db AuthSqlite) InsertSession(ctx context.Context, session *types.Session) error {
_, err := db.db.Exec(` _, err := db.db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, ?, ?)`, session.Id, session.UserId, session.CreatedAt, session.ExpiresAt) VALUES (?, ?, ?, ?)`, session.Id, session.UserId, session.CreatedAt, session.ExpiresAt)
if err != nil { if err != nil {
slog.Error("Could not insert new session", "err", err) slog.ErrorContext(ctx, "Could not insert new session", "err", err)
return types.ErrInternal return types.ErrInternal
} }
return nil return nil
} }
func (db AuthSqlite) GetSession(sessionId string) (*types.Session, error) { func (db AuthSqlite) GetSession(ctx context.Context, sessionId string) (*types.Session, error) {
var ( var (
userId uuid.UUID userId uuid.UUID
createdAt time.Time createdAt time.Time
expiresAt time.Time expiresAt time.Time
) )
err := db.db.QueryRow(` err := db.db.QueryRowContext(ctx, `
SELECT user_id, created_at, expires_at SELECT user_id, created_at, expires_at
FROM session FROM session
WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt) WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt)
if err != nil { if err != nil {
slog.Warn("Session not found", "session-id", sessionId, "err", err) slog.WarnContext(ctx, "Session not found", "session-id", sessionId, "err", err)
return nil, ErrNotFound return nil, ErrNotFound
} }
return types.NewSession(sessionId, userId, createdAt, expiresAt), nil return types.NewSession(sessionId, userId, createdAt, expiresAt), nil
} }
func (db AuthSqlite) GetSessions(userId uuid.UUID) ([]*types.Session, error) { func (db AuthSqlite) GetSessions(ctx context.Context, userId uuid.UUID) ([]*types.Session, error) {
var sessions []*types.Session var sessions []*types.Session
err := db.db.Select(&sessions, ` err := db.db.SelectContext(ctx, &sessions, `
SELECT * SELECT *
FROM session FROM session
WHERE user_id = ?`, userId) WHERE user_id = ?`, userId)
if err != nil { if err != nil {
slog.Error("Could not get sessions", "err", err) slog.ErrorContext(ctx, "Could not get sessions", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
return sessions, nil return sessions, nil
} }
func (db AuthSqlite) DeleteOldSessions(userId uuid.UUID) error { func (db AuthSqlite) DeleteSession(ctx context.Context, sessionId string) error {
_, err := db.db.Exec(`
DELETE FROM session
WHERE expires_at < datetime('now')
AND user_id = ?`, userId)
if err != nil {
slog.Error("Could not delete old sessions", "err", err)
return types.ErrInternal
}
return nil
}
func (db AuthSqlite) DeleteSession(sessionId string) error {
if sessionId != "" { if sessionId != "" {
_, err := db.db.Exec("DELETE FROM session WHERE session_id = ?", sessionId) _, err := db.db.ExecContext(ctx, "DELETE FROM session WHERE session_id = ?", sessionId)
if err != nil { if err != nil {
slog.Error("Could not delete session", "err", err) slog.ErrorContext(ctx, "Could not delete session", "err", err)
return types.ErrInternal return types.ErrInternal
} }
} }
return nil return nil
} }
func (db AuthSqlite) DeleteOldSessions(ctx context.Context) error {
_, err := db.db.ExecContext(ctx, `
DELETE FROM session
WHERE expires_at < datetime('now')`)
if err != nil {
slog.ErrorContext(ctx, "Could not delete old sessions", "err", err)
return types.ErrInternal
}
return nil
}
func (db AuthSqlite) DeleteOldTokens(ctx context.Context) error {
_, err := db.db.ExecContext(ctx, `
DELETE FROM token
WHERE expires_at < datetime('now')`)
if err != nil {
slog.ErrorContext(ctx, "Could not delete old tokens", "err", err)
return types.ErrInternal
}
return nil
}

View File

@@ -1,6 +1,7 @@
package db package db
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"log/slog" "log/slog"
@@ -12,24 +13,24 @@ var (
ErrAlreadyExists = errors.New("row already exists") ErrAlreadyExists = errors.New("row already exists")
) )
func TransformAndLogDbError(module string, r sql.Result, err error) error { func TransformAndLogDbError(ctx context.Context, module string, r sql.Result, err error) error {
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound return ErrNotFound
} }
slog.Error("database sql", "module", module, "err", err) slog.ErrorContext(ctx, "database sql", "module", module, "err", err)
return types.ErrInternal return types.ErrInternal
} }
if r != nil { if r != nil {
rows, err := r.RowsAffected() rows, err := r.RowsAffected()
if err != nil { if err != nil {
slog.Error("database rows affected", "module", module, "err", err) slog.ErrorContext(ctx, "database rows affected", "module", module, "err", err)
return types.ErrInternal return types.ErrInternal
} }
if rows == 0 { if rows == 0 {
slog.Info("row not found", "module", module) slog.InfoContext(ctx, "row not found", "module", module)
return ErrNotFound return ErrNotFound
} }
} }

View File

@@ -1,6 +1,7 @@
package db package db
import ( import (
"context"
"errors" "errors"
"log/slog" "log/slog"
"spend-sparrow/internal/types" "spend-sparrow/internal/types"
@@ -20,10 +21,10 @@ func (l migrationLogger) Verbose() bool {
return false return false
} }
func RunMigrations(db *sqlx.DB, pathPrefix string) error { func RunMigrations(ctx context.Context, db *sqlx.DB, pathPrefix string) error {
driver, err := sqlite3.WithInstance(db.DB, &sqlite3.Config{}) driver, err := sqlite3.WithInstance(db.DB, &sqlite3.Config{})
if err != nil { if err != nil {
slog.Error("Could not create Migration instance", "err", err) slog.ErrorContext(ctx, "Could not create Migration instance", "err", err)
return types.ErrInternal return types.ErrInternal
} }
@@ -32,14 +33,14 @@ func RunMigrations(db *sqlx.DB, pathPrefix string) error {
"", "",
driver) driver)
if err != nil { if err != nil {
slog.Error("Could not create migrations instance", "err", err) slog.ErrorContext(ctx, "Could not create migrations instance", "err", err)
return types.ErrInternal return types.ErrInternal
} }
m.Log = migrationLogger{} m.Log = migrationLogger{}
if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
slog.Error("Could not run migrations", "err", err) slog.ErrorContext(ctx, "Could not run migrations", "err", err)
return types.ErrInternal return types.ErrInternal
} }

View File

@@ -39,7 +39,7 @@ func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env fu
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
err = otelShutdown(ctx) err = otelShutdown(ctx)
if err != nil { if err != nil {
slog.Error("error shutting down OpenTelemetry SDK", "err", err) slog.ErrorContext(ctx, "error shutting down OpenTelemetry SDK", "err", err)
} }
cancel() cancel()
}() }()
@@ -47,16 +47,16 @@ func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env fu
slog.SetDefault(log.NewLogPropagator()) slog.SetDefault(log.NewLogPropagator())
} }
slog.Info("Starting server...") slog.InfoContext(ctx, "Starting server...")
// init server settings // init server settings
serverSettings, err := types.NewSettingsFromEnv(env) serverSettings, err := types.NewSettingsFromEnv(ctx, env)
if err != nil { if err != nil {
return err return err
} }
// init db // init db
err = db.RunMigrations(database, migrationsPrefix) err = db.RunMigrations(ctx, database, migrationsPrefix)
if err != nil { if err != nil {
return fmt.Errorf("could not run migrations: %w", err) return fmt.Errorf("could not run migrations: %w", err)
} }
@@ -64,28 +64,29 @@ func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env fu
// init server // init server
httpServer := &http.Server{ httpServer := &http.Server{
Addr: ":" + serverSettings.Port, Addr: ":" + serverSettings.Port,
Handler: createHandler(database, serverSettings), Handler: createHandlerWithServices(ctx, database, serverSettings),
ReadHeaderTimeout: 2 * time.Second, ReadHeaderTimeout: 2 * time.Second,
} }
go startServer(httpServer) go startServer(ctx, httpServer)
// graceful shutdown // graceful shutdown
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
go shutdownServer(httpServer, ctx, &wg) go shutdownServer(ctx, httpServer, &wg)
wg.Wait() wg.Wait()
return nil return nil
} }
func startServer(s *http.Server) { func startServer(ctx context.Context, s *http.Server) {
slog.Info("Starting server", "addr", s.Addr) slog.InfoContext(ctx, "Starting server", "addr", s.Addr)
if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("error listening and serving", "err", err) slog.ErrorContext(ctx, "error listening and serving", "err", err)
} }
} }
func shutdownServer(s *http.Server, ctx context.Context, wg *sync.WaitGroup) { func shutdownServer(ctx context.Context, s *http.Server, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
if s == nil { if s == nil {
return return
@@ -96,13 +97,13 @@ func shutdownServer(s *http.Server, ctx context.Context, wg *sync.WaitGroup) {
shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second) shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second)
defer cancel() defer cancel()
if err := s.Shutdown(shutdownCtx); err != nil { if err := s.Shutdown(shutdownCtx); err != nil {
slog.Error("error shutting down http server", "err", err) slog.ErrorContext(ctx, "error shutting down http server", "err", err)
} else { } else {
slog.Info("Gracefully stopped http server", "addr", s.Addr) slog.InfoContext(ctx, "Gracefully stopped http server", "addr", s.Addr)
} }
} }
func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler { func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *types.Settings) http.Handler {
var router = http.NewServeMux() var router = http.NewServeMux()
authDb := db.NewAuthSqlite(d) authDb := db.NewAuthSqlite(d)
@@ -116,16 +117,21 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler {
treasureChestService := service.NewTreasureChest(d, randomService, clockService) treasureChestService := service.NewTreasureChest(d, randomService, clockService)
transactionService := service.NewTransaction(d, randomService, clockService) transactionService := service.NewTransaction(d, randomService, clockService)
transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService, transactionService) transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService, transactionService)
dashboardService := service.NewDashboard(d)
render := handler.NewRender() render := handler.NewRender()
indexHandler := handler.NewIndex(render) indexHandler := handler.NewIndex(render, clockService)
dashboardHandler := handler.NewDashboard(render, dashboardService, treasureChestService)
authHandler := handler.NewAuth(authService, render) authHandler := handler.NewAuth(authService, render)
accountHandler := handler.NewAccount(accountService, render) accountHandler := handler.NewAccount(accountService, render)
treasureChestHandler := handler.NewTreasureChest(treasureChestService, transactionRecurringService, render) treasureChestHandler := handler.NewTreasureChest(treasureChestService, transactionRecurringService, render)
transactionHandler := handler.NewTransaction(transactionService, accountService, treasureChestService, render) transactionHandler := handler.NewTransaction(transactionService, accountService, treasureChestService, render)
transactionRecurringHandler := handler.NewTransactionRecurring(transactionRecurringService, render) transactionRecurringHandler := handler.NewTransactionRecurring(transactionRecurringService, render)
go dailyTaskTimer(ctx, transactionRecurringService, authService)
indexHandler.Handle(router) indexHandler.Handle(router)
dashboardHandler.Handle(router)
accountHandler.Handle(router) accountHandler.Handle(router)
treasureChestHandler.Handle(router) treasureChestHandler.Handle(router)
authHandler.Handle(router) authHandler.Handle(router)
@@ -137,7 +143,6 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler {
wrapper := middleware.Wrapper( wrapper := middleware.Wrapper(
router, router,
middleware.GenerateRecurringTransactions(transactionRecurringService),
middleware.SecurityHeaders(serverSettings), middleware.SecurityHeaders(serverSettings),
middleware.CacheControl, middleware.CacheControl,
middleware.CrossSiteRequestForgery(authService), middleware.CrossSiteRequestForgery(authService),
@@ -150,3 +155,24 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler {
return wrapper return wrapper
} }
func dailyTaskTimer(ctx context.Context, transactionRecurring service.TransactionRecurring, auth service.Auth) {
runDailyTasks(ctx, transactionRecurring, auth)
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
runDailyTasks(ctx, transactionRecurring, auth)
}
}
}
func runDailyTasks(ctx context.Context, transactionRecurring service.TransactionRecurring, auth service.Auth) {
slog.InfoContext(ctx, "Running daily tasks")
_ = transactionRecurring.GenerateTransactions(ctx)
_ = auth.CleanupSessionsAndTokens(ctx)
}

View File

@@ -44,7 +44,7 @@ func (h AccountImpl) handleAccountPage() http.HandlerFunc {
return return
} }
accounts, err := h.s.GetAll(user) accounts, err := h.s.GetAll(r.Context(), user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -72,7 +72,7 @@ func (h AccountImpl) handleAccountItemComp() http.HandlerFunc {
return return
} }
account, err := h.s.Get(user, id) account, err := h.s.Get(r.Context(), user, id)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -105,13 +105,13 @@ func (h AccountImpl) handleUpdateAccount() http.HandlerFunc {
id := r.PathValue("id") id := r.PathValue("id")
name := r.FormValue("name") name := r.FormValue("name")
if id == "new" { if id == "new" {
account, err = h.s.Add(user, name) account, err = h.s.Add(r.Context(), user, name)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
} else { } else {
account, err = h.s.UpdateName(user, id, name) account, err = h.s.UpdateName(r.Context(), user, id, name)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -135,7 +135,7 @@ func (h AccountImpl) handleDeleteAccount() http.HandlerFunc {
id := r.PathValue("id") id := r.PathValue("id")
err := h.s.Delete(user, id) err := h.s.Delete(r.Context(), user, id)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return

View File

@@ -85,7 +85,7 @@ func (handler AuthImpl) handleSignIn() http.HandlerFunc {
email := r.FormValue("email") email := r.FormValue("email")
password := r.FormValue("password") password := r.FormValue("password")
session, user, err := handler.service.SignIn(session, email, password) session, user, err := handler.service.SignIn(r.Context(), session, email, password)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -98,9 +98,9 @@ func (handler AuthImpl) handleSignIn() http.HandlerFunc {
if err != nil { if err != nil {
if errors.Is(err, service.ErrInvalidCredentials) { if errors.Is(err, service.ErrInvalidCredentials) {
utils.TriggerToastWithStatus(w, r, "error", "Invalid email or password", http.StatusUnauthorized) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Invalid email or password", http.StatusUnauthorized)
} else { } else {
utils.TriggerToastWithStatus(w, r, "error", "An error occurred", http.StatusInternalServerError) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "An error occurred", http.StatusInternalServerError)
} }
return return
} }
@@ -163,11 +163,11 @@ func (handler AuthImpl) handleVerifyResendComp() http.HandlerFunc {
return return
} }
go handler.service.SendVerificationMail(user.Id, user.Email) go handler.service.SendVerificationMail(r.Context(), user.Id, user.Email)
_, err := w.Write([]byte("<p class=\"mt-8\">Verification email sent</p>")) _, err := w.Write([]byte("<p class=\"mt-8\">Verification email sent</p>"))
if err != nil { if err != nil {
slog.Error("Could not write response", "err", err) slog.ErrorContext(r.Context(), "Could not write response", "err", err)
} }
} }
} }
@@ -178,7 +178,7 @@ func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc {
token := r.URL.Query().Get("token") token := r.URL.Query().Get("token")
err := handler.service.VerifyUserEmail(token) err := handler.service.VerifyUserEmail(r.Context(), token)
isVerified := err == nil isVerified := err == nil
comp := auth.VerifyResponseComp(isVerified) comp := auth.VerifyResponseComp(isVerified)
@@ -202,33 +202,33 @@ func (handler AuthImpl) handleSignUp() http.HandlerFunc {
var password = r.FormValue("password") var password = r.FormValue("password")
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) { _, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) {
slog.Info("signing up", "email", email) slog.InfoContext(r.Context(), "signing up", "email", email)
user, err := handler.service.SignUp(email, password) user, err := handler.service.SignUp(r.Context(), email, password)
if err != nil { if err != nil {
return nil, err return nil, err
} }
slog.Info("Sending verification email", "to", user.Email) slog.InfoContext(r.Context(), "Sending verification email", "to", user.Email)
go handler.service.SendVerificationMail(user.Id, user.Email) go handler.service.SendVerificationMail(r.Context(), user.Id, user.Email)
return nil, nil return nil, nil
}) })
if err != nil { if err != nil {
switch { switch {
case errors.Is(err, types.ErrInternal): case errors.Is(err, types.ErrInternal):
utils.TriggerToastWithStatus(w, r, "error", "An error occurred", http.StatusInternalServerError) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "An error occurred", http.StatusInternalServerError)
return return
case errors.Is(err, service.ErrInvalidEmail): case errors.Is(err, service.ErrInvalidEmail):
utils.TriggerToastWithStatus(w, r, "error", "The email provided is invalid", http.StatusBadRequest) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "The email provided is invalid", http.StatusBadRequest)
return return
case errors.Is(err, service.ErrInvalidPassword): case errors.Is(err, service.ErrInvalidPassword):
utils.TriggerToastWithStatus(w, r, "error", service.ErrInvalidPassword.Error(), http.StatusBadRequest) utils.TriggerToastWithStatus(r.Context(), w, r, "error", service.ErrInvalidPassword.Error(), http.StatusBadRequest)
return return
} }
// If err is "service.ErrAccountExists", then just continue // If err is "service.ErrAccountExists", then just continue
} }
utils.TriggerToastWithStatus(w, r, "success", "An activation link has been send to your email", http.StatusOK) utils.TriggerToastWithStatus(r.Context(), w, r, "success", "An activation link has been send to your email", http.StatusOK)
} }
} }
@@ -239,7 +239,7 @@ func (handler AuthImpl) handleSignOut() http.HandlerFunc {
session := middleware.GetSession(r) session := middleware.GetSession(r)
if session != nil { if session != nil {
err := handler.service.SignOut(session.Id) err := handler.service.SignOut(r.Context(), session.Id)
if err != nil { if err != nil {
http.Error(w, "An error occurred", http.StatusInternalServerError) http.Error(w, "An error occurred", http.StatusInternalServerError)
return return
@@ -288,12 +288,12 @@ func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc {
password := r.FormValue("password") password := r.FormValue("password")
err := handler.service.DeleteAccount(user, password) err := handler.service.DeleteAccount(r.Context(), user, password)
if err != nil { if err != nil {
if errors.Is(err, service.ErrInvalidCredentials) { if errors.Is(err, service.ErrInvalidCredentials) {
utils.TriggerToastWithStatus(w, r, "error", "Password not correct", http.StatusBadRequest) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Password not correct", http.StatusBadRequest)
} else { } else {
utils.TriggerToastWithStatus(w, r, "error", "Internal Server Error", http.StatusInternalServerError) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
} }
return return
} }
@@ -327,20 +327,20 @@ func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc {
session := middleware.GetSession(r) session := middleware.GetSession(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if session == nil || user == nil { if session == nil || user == nil {
utils.TriggerToastWithStatus(w, r, "error", "Unathorized", http.StatusUnauthorized) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Unathorized", http.StatusUnauthorized)
return return
} }
currPass := r.FormValue("current-password") currPass := r.FormValue("current-password")
newPass := r.FormValue("new-password") newPass := r.FormValue("new-password")
err := handler.service.ChangePassword(user, session.Id, currPass, newPass) err := handler.service.ChangePassword(r.Context(), user, session.Id, currPass, newPass)
if err != nil { if err != nil {
utils.TriggerToastWithStatus(w, r, "error", err.Error(), http.StatusBadRequest) utils.TriggerToastWithStatus(r.Context(), w, r, "error", err.Error(), http.StatusBadRequest)
return return
} }
utils.TriggerToastWithStatus(w, r, "success", "Password changed", http.StatusOK) utils.TriggerToastWithStatus(r.Context(), w, r, "success", "Password changed", http.StatusOK)
} }
} }
@@ -365,19 +365,19 @@ func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc {
email := r.FormValue("email") email := r.FormValue("email")
if email == "" { if email == "" {
utils.TriggerToastWithStatus(w, r, "error", "Please enter an email", http.StatusBadRequest) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Please enter an email", http.StatusBadRequest)
return return
} }
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) { _, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) {
err := handler.service.SendForgotPasswordMail(email) err := handler.service.SendForgotPasswordMail(r.Context(), email)
return nil, err return nil, err
}) })
if err != nil { if err != nil {
utils.TriggerToastWithStatus(w, r, "error", "Internal Server Error", http.StatusInternalServerError) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
} else { } else {
utils.TriggerToastWithStatus(w, r, "info", "If the address exists, an email has been sent.", http.StatusOK) utils.TriggerToastWithStatus(r.Context(), w, r, "info", "If the address exists, an email has been sent.", http.StatusOK)
} }
} }
} }
@@ -388,19 +388,19 @@ func (handler AuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
pageUrl, err := url.Parse(r.Header.Get("Hx-Current-Url")) pageUrl, err := url.Parse(r.Header.Get("Hx-Current-Url"))
if err != nil { if err != nil {
slog.Error("Could not get current URL", "err", err) slog.ErrorContext(r.Context(), "Could not get current URL", "err", err)
utils.TriggerToastWithStatus(w, r, "error", "Internal Server Error", http.StatusInternalServerError) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
return return
} }
token := pageUrl.Query().Get("token") token := pageUrl.Query().Get("token")
newPass := r.FormValue("new-password") newPass := r.FormValue("new-password")
err = handler.service.ForgotPassword(token, newPass) err = handler.service.ForgotPassword(r.Context(), token, newPass)
if err != nil { if err != nil {
utils.TriggerToastWithStatus(w, r, "error", err.Error(), http.StatusBadRequest) utils.TriggerToastWithStatus(r.Context(), w, r, "error", err.Error(), http.StatusBadRequest)
} else { } else {
utils.TriggerToastWithStatus(w, r, "success", "Password changed", http.StatusOK) utils.TriggerToastWithStatus(r.Context(), w, r, "success", "Password changed", http.StatusOK)
} }
} }
} }

View File

@@ -0,0 +1,254 @@
package handler
import (
"fmt"
"log/slog"
"net/http"
"spend-sparrow/internal/handler/middleware"
"spend-sparrow/internal/service"
"spend-sparrow/internal/template/dashboard"
"spend-sparrow/internal/utils"
"strings"
"time"
"github.com/google/uuid"
)
type Dashboard interface {
Handle(router *http.ServeMux)
}
type DashboardImpl struct {
r *Render
d *service.Dashboard
treasureChest service.TreasureChest
}
func NewDashboard(r *Render, d *service.Dashboard, treasureChest service.TreasureChest) Dashboard {
return DashboardImpl{
r: r,
d: d,
treasureChest: treasureChest,
}
}
func (handler DashboardImpl) Handle(router *http.ServeMux) {
router.Handle("GET /dashboard", handler.handleDashboard())
router.Handle("GET /dashboard/main-chart", handler.handleDashboardMainChart())
router.Handle("GET /dashboard/treasure-chests", handler.handleDashboardTreasureChests())
router.Handle("GET /dashboard/treasure-chest", handler.handleDashboardTreasureChest())
}
func (handler DashboardImpl) handleDashboard() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
treasureChests, err := handler.treasureChest.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
return
}
comp := dashboard.Dashboard(treasureChests)
handler.r.RenderLayoutWithStatus(r, w, comp, user, http.StatusOK)
}
}
func (handler DashboardImpl) handleDashboardMainChart() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r)
series, err := handler.d.MainChart(r.Context(), user)
if err != nil {
handleError(w, r, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
accountBuilder := strings.Builder{}
savingsBuilder := strings.Builder{}
for _, entry := range series {
accountBuilder.WriteString(fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100))
savingsBuilder.WriteString(fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Savings)/100))
}
account := accountBuilder.String()
savings := savingsBuilder.String()
account = account[:len(account)-1]
savings = savings[:len(savings)-1]
_, err = fmt.Fprintf(w, `
{
"aria": {
"enabled": true
},
"tooltip": {
"trigger": "axis",
"formatter": "<updated by client>"
},
"xAxis": {
"type": "time"
},
"yAxis": {
"axisLabel": {
"formatter": "{value} €"
}
},
"series": [
{
"data": [%s],
"type": "line",
"name": "Account Value"
},
{
"data": [%s],
"type": "line",
"name": "Savings"
}
]
}
`, account, savings)
if err != nil {
slog.InfoContext(r.Context(), "could not write response", "err", err)
}
}
}
func (handler DashboardImpl) handleDashboardTreasureChests() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r)
treeList, err := handler.d.TreasureChests(r.Context(), user)
if err != nil {
handleError(w, r, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
dataBuilder := strings.Builder{}
for _, item := range treeList {
childrenBuilder := strings.Builder{}
for _, child := range item.Children {
if child.Value < 0 {
childrenBuilder.WriteString(fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, -child.Value))
} else {
childrenBuilder.WriteString(fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, child.Value))
}
}
children := childrenBuilder.String()
children = children[:len(children)-1]
dataBuilder.WriteString(fmt.Sprintf(`{"name":"%s","children":[%s]},`, item.Name, children))
}
data := dataBuilder.String()
data = data[:len(data)-1]
_, err = fmt.Fprintf(w, `
{
"aria": {
"enabled": true
},
"series": [
{
"data": [%s],
"type": "treemap",
"name": "Savings"
}
]
}
`, data)
if err != nil {
slog.InfoContext(r.Context(), "could not write response", "err", err)
}
}
}
func (handler DashboardImpl) handleDashboardTreasureChest() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r)
var treasureChestId *uuid.UUID
treasureChestStr := r.URL.Query().Get("id")
if treasureChestStr != "" {
id, err := uuid.Parse(treasureChestStr)
if err != nil {
handleError(w, r, fmt.Errorf("could not parse treasure chest: %w", service.ErrBadRequest))
return
}
treasureChestId = &id
}
series, err := handler.d.TreasureChest(r.Context(), user, treasureChestId)
if err != nil {
handleError(w, r, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
valueBuilder := strings.Builder{}
for _, entry := range series {
valueBuilder.WriteString(fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100))
}
value := valueBuilder.String()
if len(value) > 0 {
value = value[:len(value)-1]
}
_, err = fmt.Fprintf(w, `
{
"aria": {
"enabled": true
},
"tooltip": {
"trigger": "axis",
"formatter": "<updated by client>"
},
"xAxis": {
"type": "time"
},
"yAxis": {
"axisLabel": {
"formatter": "{value} €"
}
},
"series": [
{
"data": [%s],
"type": "line",
"name": "Treasure Chest Value"
}
]
}
`, value)
if err != nil {
slog.InfoContext(r.Context(), "could not write response", "err", err)
}
}
}

View File

@@ -15,17 +15,17 @@ import (
func handleError(w http.ResponseWriter, r *http.Request, err error) { func handleError(w http.ResponseWriter, r *http.Request, err error) {
switch { switch {
case errors.Is(err, service.ErrUnauthorized): case errors.Is(err, service.ErrUnauthorized):
utils.TriggerToastWithStatus(w, r, "error", "You are not autorized to perform this operation.", http.StatusUnauthorized) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "You are not autorized to perform this operation.", http.StatusUnauthorized)
return return
case errors.Is(err, service.ErrBadRequest): case errors.Is(err, service.ErrBadRequest):
utils.TriggerToastWithStatus(w, r, "error", extractErrorMessage(err), http.StatusBadRequest) utils.TriggerToastWithStatus(r.Context(), w, r, "error", extractErrorMessage(err), http.StatusBadRequest)
return return
case errors.Is(err, db.ErrNotFound): case errors.Is(err, db.ErrNotFound):
utils.TriggerToastWithStatus(w, r, "error", extractErrorMessage(err), http.StatusNotFound) utils.TriggerToastWithStatus(r.Context(), w, r, "error", extractErrorMessage(err), http.StatusNotFound)
return return
} }
utils.TriggerToastWithStatus(w, r, "error", "Internal Server Error", http.StatusInternalServerError) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
} }
func extractErrorMessage(err error) string { func extractErrorMessage(err error) string {

View File

@@ -3,6 +3,7 @@ package middleware
import ( import (
"context" "context"
"net/http" "net/http"
"strings"
"spend-sparrow/internal/service" "spend-sparrow/internal/service"
"spend-sparrow/internal/types" "spend-sparrow/internal/types"
@@ -16,14 +17,21 @@ var UserKey ContextKey = "user"
func Authenticate(service service.Auth) func(http.Handler) http.Handler { func Authenticate(service service.Auth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if strings.Contains(r.URL.Path, "/static/") {
next.ServeHTTP(w, r.WithContext(ctx))
return
}
sessionId := getSessionID(r) sessionId := getSessionID(r)
session, user, _ := service.SignInSession(sessionId) session, user, _ := service.SignInSession(r.Context(), sessionId)
var err error var err error
// Always sign in anonymous // Always sign in anonymous
// This way, we can always generate csrf tokens // This way, we can always generate csrf tokens
if session == nil { if session == nil {
session, err = service.SignInAnonymous() session, err = service.SignInAnonymous(r.Context())
if err != nil { if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return return
@@ -33,7 +41,6 @@ func Authenticate(service service.Auth) func(http.Handler) http.Handler {
http.SetCookie(w, &cookie) http.SetCookie(w, &cookie)
} }
ctx := r.Context()
ctx = context.WithValue(ctx, UserKey, user) ctx = context.WithValue(ctx, UserKey, user)
ctx = context.WithValue(ctx, SessionKey, session) ctx = context.WithValue(ctx, SessionKey, session)

View File

@@ -4,30 +4,27 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"spend-sparrow/internal/service" "spend-sparrow/internal/service"
"spend-sparrow/internal/types"
"spend-sparrow/internal/utils" "spend-sparrow/internal/utils"
"strings" "strings"
) )
type csrfResponseWriter struct { type csrfResponseWriter struct {
http.ResponseWriter http.ResponseWriter
auth service.Auth
session *types.Session csrfToken string
} }
func newCsrfResponseWriter(w http.ResponseWriter, auth service.Auth, session *types.Session) *csrfResponseWriter { func newCsrfResponseWriter(w http.ResponseWriter, csrfToken string) *csrfResponseWriter {
return &csrfResponseWriter{ return &csrfResponseWriter{
ResponseWriter: w, ResponseWriter: w,
auth: auth, csrfToken: csrfToken,
session: session,
} }
} }
func (rr *csrfResponseWriter) Write(data []byte) (int, error) { func (rr *csrfResponseWriter) Write(data []byte) (int, error) {
dataStr := string(data) dataStr := string(data)
csrfToken, err := rr.auth.GetCsrfToken(rr.session) if rr.csrfToken != "" {
if err == nil { dataStr = strings.ReplaceAll(dataStr, "CSRF_TOKEN", rr.csrfToken)
dataStr = strings.ReplaceAll(dataStr, "CSRF_TOKEN", csrfToken)
} }
return rr.ResponseWriter.Write([]byte(dataStr)) return rr.ResponseWriter.Write([]byte(dataStr))
@@ -36,6 +33,13 @@ func (rr *csrfResponseWriter) Write(data []byte) (int, error) {
func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler { func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if strings.Contains(r.URL.Path, "/static/") {
next.ServeHTTP(w, r.WithContext(ctx))
return
}
session := GetSession(r) session := GetSession(r)
if r.Method == http.MethodPost || if r.Method == http.MethodPost ||
@@ -44,10 +48,10 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler
r.Method == http.MethodPatch { r.Method == http.MethodPatch {
csrfToken := r.Header.Get("Csrf-Token") csrfToken := r.Header.Get("Csrf-Token")
if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) { if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(ctx, csrfToken, session.Id) {
slog.Info("CSRF-Token not correct", "token", csrfToken) slog.InfoContext(ctx, "CSRF-Token not correct", "token", csrfToken)
if r.Header.Get("Hx-Request") == "true" { if r.Header.Get("Hx-Request") == "true" {
utils.TriggerToastWithStatus(w, r, "error", "CSRF-Token not correct", http.StatusBadRequest) utils.TriggerToastWithStatus(ctx, w, r, "error", "CSRF-Token not correct", http.StatusBadRequest)
} else { } else {
http.Error(w, "CSRF-Token not correct", http.StatusBadRequest) http.Error(w, "CSRF-Token not correct", http.StatusBadRequest)
} }
@@ -55,7 +59,17 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler
} }
} }
responseWriter := newCsrfResponseWriter(w, auth, session) token, err := auth.GetCsrfToken(ctx, session)
if err != nil {
if r.Header.Get("Hx-Request") == "true" {
utils.TriggerToastWithStatus(ctx, w, r, "error", "Could not generate CSRF Token", http.StatusBadRequest)
} else {
http.Error(w, "Could not generate CSRF Token", http.StatusBadRequest)
}
return
}
responseWriter := newCsrfResponseWriter(w, token)
next.ServeHTTP(responseWriter, r) next.ServeHTTP(responseWriter, r)
}) })
} }

View File

@@ -1,22 +0,0 @@
package middleware
import (
"net/http"
"spend-sparrow/internal/service"
)
func GenerateRecurringTransactions(transactionRecurring service.TransactionRecurring) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := GetUser(r)
if user == nil || r.Method != http.MethodGet {
next.ServeHTTP(w, r)
return
}
_ = transactionRecurring.GenerateTransactions(user)
next.ServeHTTP(w, r)
})
}
}

View File

@@ -33,7 +33,7 @@ func Gzip(next http.Handler) http.Handler {
err := gz.Close() err := gz.Close()
if err != nil && !errors.Is(err, http.ErrBodyNotAllowed) { if err != nil && !errors.Is(err, http.ErrBodyNotAllowed) {
slog.Error("Gzip: could not close Writer", "err", err) slog.ErrorContext(r.Context(), "Gzip: could not close Writer", "err", err)
} }
}) })
} }

View File

@@ -8,6 +8,7 @@ import (
type WrappedWriter struct { type WrappedWriter struct {
http.ResponseWriter http.ResponseWriter
StatusCode int StatusCode int
} }
@@ -26,7 +27,7 @@ func Log(next http.Handler) http.Handler {
} }
next.ServeHTTP(wrapped, r) next.ServeHTTP(wrapped, r)
slog.Info("request", slog.InfoContext(r.Context(), "request",
"remoteAddr", r.RemoteAddr, "remoteAddr", r.RemoteAddr,
"status", wrapped.StatusCode, "status", wrapped.StatusCode,
"method", r.Method, "method", r.Method,

View File

@@ -2,7 +2,7 @@ package middleware
import "net/http" import "net/http"
// Chain list of handlers together. // Wrapper wraps a list of handlers together.
func Wrapper(next http.Handler, handlers ...func(http.Handler) http.Handler) http.Handler { func Wrapper(next http.Handler, handlers ...func(http.Handler) http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lastHandler := next lastHandler := next

View File

@@ -22,7 +22,7 @@ func (render *Render) RenderWithStatus(r *http.Request, w http.ResponseWriter, c
w.WriteHeader(status) w.WriteHeader(status)
err := comp.Render(r.Context(), w) err := comp.Render(r.Context(), w)
if err != nil { if err != nil {
slog.Error("Failed to render layout", "err", err) slog.ErrorContext(r.Context(), "Failed to render layout", "err", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
} }
} }

View File

@@ -3,7 +3,9 @@ package handler
import ( import (
"net/http" "net/http"
"spend-sparrow/internal/handler/middleware" "spend-sparrow/internal/handler/middleware"
"spend-sparrow/internal/service"
"spend-sparrow/internal/template" "spend-sparrow/internal/template"
"spend-sparrow/internal/utils"
"github.com/a-h/templ" "github.com/a-h/templ"
) )
@@ -13,12 +15,14 @@ type Index interface {
} }
type IndexImpl struct { type IndexImpl struct {
render *Render r *Render
c service.Clock
} }
func NewIndex(render *Render) Index { func NewIndex(r *Render, c service.Clock) Index {
return IndexImpl{ return IndexImpl{
render: render, r: r,
c: c,
} }
} }
@@ -33,6 +37,8 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
user := middleware.GetUser(r) user := middleware.GetUser(r)
htmx := utils.IsHtmx(r)
var comp templ.Component var comp templ.Component
var status int var status int
@@ -41,14 +47,19 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
status = http.StatusNotFound status = http.StatusNotFound
} else { } else {
if user != nil { if user != nil {
comp = template.Dashboard() utils.DoRedirect(w, r, "/dashboard")
return
} else { } else {
comp = template.Index() comp = template.Index()
} }
status = http.StatusOK status = http.StatusOK
} }
handler.render.RenderLayoutWithStatus(r, w, comp, user, status) if htmx {
handler.r.RenderWithStatus(r, w, comp, status)
} else {
handler.r.RenderLayoutWithStatus(r, w, comp, user, status)
}
} }
} }

View File

@@ -14,8 +14,6 @@ import (
"github.com/a-h/templ" "github.com/a-h/templ"
"github.com/google/uuid" "github.com/google/uuid"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
) )
type Transaction interface { type Transaction interface {
@@ -56,28 +54,26 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
return return
} }
currentSpan := trace.SpanFromContext(r.Context())
currentSpan.SetAttributes(attribute.String("", "test"))
filter := types.TransactionItemsFilter{ filter := types.TransactionItemsFilter{
AccountId: r.URL.Query().Get("account-id"), AccountId: r.URL.Query().Get("account-id"),
TreasureChestId: r.URL.Query().Get("treasure-chest-id"), TreasureChestId: r.URL.Query().Get("treasure-chest-id"),
Error: r.URL.Query().Get("error"), Error: r.URL.Query().Get("error"),
Page: r.URL.Query().Get("page"),
} }
transactions, err := h.s.GetAll(user, filter) transactions, err := h.s.GetAll(r.Context(), user, filter)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
accounts, err := h.account.GetAll(user) accounts, err := h.account.GetAll(r.Context(), user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
treasureChests, err := h.treasureChest.GetAll(user) treasureChests, err := h.treasureChest.GetAll(r.Context(), user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -105,13 +101,13 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
return return
} }
accounts, err := h.account.GetAll(user) accounts, err := h.account.GetAll(r.Context(), user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
treasureChests, err := h.treasureChest.GetAll(user) treasureChests, err := h.treasureChest.GetAll(r.Context(), user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -124,7 +120,7 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
return return
} }
transaction, err := h.s.Get(user, id) transaction, err := h.s.Get(r.Context(), user, id)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -212,26 +208,26 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
var transaction *types.Transaction var transaction *types.Transaction
if idStr == "new" { if idStr == "new" {
transaction, err = h.s.Add(nil, user, input) transaction, err = h.s.Add(r.Context(), nil, user, input)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
} else { } else {
transaction, err = h.s.Update(user, input) transaction, err = h.s.Update(r.Context(), user, input)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
} }
accounts, err := h.account.GetAll(user) accounts, err := h.account.GetAll(r.Context(), user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
treasureChests, err := h.treasureChest.GetAll(user) treasureChests, err := h.treasureChest.GetAll(r.Context(), user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -253,13 +249,13 @@ func (h TransactionImpl) handleRecalculate() http.HandlerFunc {
return return
} }
err := h.s.RecalculateBalances(user) err := h.s.RecalculateBalances(r.Context(), user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
utils.TriggerToastWithStatus(w, r, "success", "Balances recalculated, please refresh", http.StatusOK) utils.TriggerToastWithStatus(r.Context(), w, r, "success", "Balances recalculated, please refresh", http.StatusOK)
} }
} }
@@ -275,7 +271,7 @@ func (h TransactionImpl) handleDeleteTransaction() http.HandlerFunc {
id := r.PathValue("id") id := r.PathValue("id")
err := h.s.Delete(user, id) err := h.s.Delete(r.Context(), user, id)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return

View File

@@ -70,13 +70,13 @@ func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.Handle
} }
if input.Id == "new" { if input.Id == "new" {
_, err := h.s.Add(user, input) _, err := h.s.Add(r.Context(), user, input)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
} else { } else {
_, err := h.s.Update(user, input) _, err := h.s.Update(r.Context(), user, input)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -101,7 +101,7 @@ func (h TransactionRecurringImpl) handleDeleteTransactionRecurring() http.Handle
accountId := r.URL.Query().Get("account-id") accountId := r.URL.Query().Get("account-id")
treasureChestId := r.URL.Query().Get("treasure-chest-id") treasureChestId := r.URL.Query().Get("treasure-chest-id")
err := h.s.Delete(user, id) err := h.s.Delete(r.Context(), user, id)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -115,16 +115,16 @@ func (h TransactionRecurringImpl) renderItems(w http.ResponseWriter, r *http.Req
var transactionsRecurring []*types.TransactionRecurring var transactionsRecurring []*types.TransactionRecurring
var err error var err error
if accountId == "" && treasureChestId == "" { if accountId == "" && treasureChestId == "" {
utils.TriggerToastWithStatus(w, r, "error", "Please select an account or treasure chest", http.StatusBadRequest) utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Please select an account or treasure chest", http.StatusBadRequest)
} }
if accountId != "" { if accountId != "" {
transactionsRecurring, err = h.s.GetAllByAccount(user, accountId) transactionsRecurring, err = h.s.GetAllByAccount(r.Context(), user, accountId)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
} else { } else {
transactionsRecurring, err = h.s.GetAllByTreasureChest(user, treasureChestId) transactionsRecurring, err = h.s.GetAllByTreasureChest(r.Context(), user, treasureChestId)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return

View File

@@ -48,13 +48,13 @@ func (h TreasureChestImpl) handleTreasureChestPage() http.HandlerFunc {
return return
} }
treasureChests, err := h.s.GetAll(user) treasureChests, err := h.s.GetAll(r.Context(), user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
transactionsRecurring, err := h.transactionRecurring.GetAll(user) transactionsRecurring, err := h.transactionRecurring.GetAll(r.Context(), user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -77,7 +77,7 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
return return
} }
treasureChests, err := h.s.GetAll(user) treasureChests, err := h.s.GetAll(r.Context(), user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -90,13 +90,13 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
return return
} }
treasureChest, err := h.s.Get(user, id) treasureChest, err := h.s.Get(r.Context(), user, id)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(user, treasureChest.Id.String()) transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(r.Context(), user, treasureChest.Id.String())
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -132,20 +132,20 @@ func (h TreasureChestImpl) handleUpdateTreasureChest() http.HandlerFunc {
parentId := r.FormValue("parent-id") parentId := r.FormValue("parent-id")
name := r.FormValue("name") name := r.FormValue("name")
if id == "new" { if id == "new" {
treasureChest, err = h.s.Add(user, parentId, name) treasureChest, err = h.s.Add(r.Context(), user, parentId, name)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
} else { } else {
treasureChest, err = h.s.Update(user, id, parentId, name) treasureChest, err = h.s.Update(r.Context(), user, id, parentId, name)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
} }
} }
transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(user, treasureChest.Id.String()) transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(r.Context(), user, treasureChest.Id.String())
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return
@@ -171,7 +171,7 @@ func (h TreasureChestImpl) handleDeleteTreasureChest() http.HandlerFunc {
id := r.PathValue("id") id := r.PathValue("id")
err := h.s.Delete(user, id) err := h.s.Delete(r.Context(), user, id)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
return return

View File

@@ -9,10 +9,10 @@ import (
) )
func NewLogPropagator() *slog.Logger { func NewLogPropagator() *slog.Logger {
return slog.New(&logHandler{
console := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}) console: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}),
otel := otelslog.NewHandler("spend-sparrow") otel: otelslog.NewHandler("spend-sparrow"),
return slog.New(&logHandler{console, otel}) })
} }
type logHandler struct { type logHandler struct {

View File

@@ -3,6 +3,7 @@ package internal
import ( import (
"context" "context"
"errors" "errors"
"log/slog"
"time" "time"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
@@ -13,11 +14,13 @@ import (
"go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
) )
var ( var (
otelEndpoint = "192.168.188.155:4317" otelEndpoint = "otel-collector:4317"
) )
// setupOTelSDK bootstraps the OpenTelemetry pipeline. // setupOTelSDK bootstraps the OpenTelemetry pipeline.
@@ -47,8 +50,16 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
prop := newPropagator() prop := newPropagator()
otel.SetTextMapPropagator(prop) otel.SetTextMapPropagator(prop)
resources, err := resource.New(
ctx,
resource.WithAttributes(semconv.ServiceName("spend-sparrow")),
)
if err != nil {
slog.ErrorContext(ctx, "failed to create resource", "error", err)
}
// Set up trace provider. // Set up trace provider.
tracerProvider, err := newTracerProvider(ctx) tracerProvider, err := newTracerProvider(ctx, resources)
if err != nil { if err != nil {
handleErr(ctx, err) handleErr(ctx, err)
return nil, err return nil, err
@@ -57,7 +68,7 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
otel.SetTracerProvider(tracerProvider) otel.SetTracerProvider(tracerProvider)
// Set up meter provider. // Set up meter provider.
meterProvider, err := newMeterProvider(ctx) meterProvider, err := newMeterProvider(ctx, resources)
if err != nil { if err != nil {
handleErr(ctx, err) handleErr(ctx, err)
return nil, err return nil, err
@@ -66,7 +77,7 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
otel.SetMeterProvider(meterProvider) otel.SetMeterProvider(meterProvider)
// Set up logger provider. // Set up logger provider.
loggerProvider, err := newLoggerProvider(ctx) loggerProvider, err := newLoggerProvider(ctx, resources)
if err != nil { if err != nil {
handleErr(ctx, err) handleErr(ctx, err)
return nil, err return nil, err
@@ -84,8 +95,9 @@ func newPropagator() propagation.TextMapPropagator {
) )
} }
func newTracerProvider(ctx context.Context) (*trace.TracerProvider, error) { func newTracerProvider(ctx context.Context, resource *resource.Resource) (*trace.TracerProvider, error) {
exp, err := otlptracegrpc.New(ctx, exp, err := otlptracegrpc.New(
ctx,
otlptracegrpc.WithEndpoint(otelEndpoint), otlptracegrpc.WithEndpoint(otelEndpoint),
otlptracegrpc.WithInsecure(), otlptracegrpc.WithInsecure(),
) )
@@ -93,11 +105,15 @@ func newTracerProvider(ctx context.Context) (*trace.TracerProvider, error) {
return nil, err return nil, err
} }
return trace.NewTracerProvider(trace.WithBatcher(exp)), nil return trace.NewTracerProvider(
trace.WithBatcher(exp),
trace.WithResource(resource),
), nil
} }
func newMeterProvider(ctx context.Context) (*metric.MeterProvider, error) { func newMeterProvider(ctx context.Context, resource *resource.Resource) (*metric.MeterProvider, error) {
exp, err := otlpmetricgrpc.New(ctx, exp, err := otlpmetricgrpc.New(
ctx,
otlpmetricgrpc.WithInsecure(), otlpmetricgrpc.WithInsecure(),
otlpmetricgrpc.WithEndpoint(otelEndpoint)) otlpmetricgrpc.WithEndpoint(otelEndpoint))
if err != nil { if err != nil {
@@ -105,12 +121,12 @@ func newMeterProvider(ctx context.Context) (*metric.MeterProvider, error) {
} }
return metric.NewMeterProvider( return metric.NewMeterProvider(
metric.WithReader( metric.WithReader(metric.NewPeriodicReader(exp, metric.WithInterval(15*time.Second))),
metric.NewPeriodicReader( metric.WithResource(resource),
exp, metric.WithInterval(15*time.Second)))), nil ), nil
} }
func newLoggerProvider(ctx context.Context) (*log.LoggerProvider, error) { func newLoggerProvider(ctx context.Context, resource *resource.Resource) (*log.LoggerProvider, error) {
logExporter, err := otlploggrpc.New( logExporter, err := otlploggrpc.New(
ctx, ctx,
otlploggrpc.WithInsecure(), otlploggrpc.WithInsecure(),
@@ -121,6 +137,7 @@ func newLoggerProvider(ctx context.Context) (*log.LoggerProvider, error) {
loggerProvider := log.NewLoggerProvider( loggerProvider := log.NewLoggerProvider(
log.WithProcessor(log.NewBatchProcessor(logExporter)), log.WithProcessor(log.NewBatchProcessor(logExporter)),
log.WithResource(resource),
) )
return loggerProvider, nil return loggerProvider, nil
} }

View File

@@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -12,11 +13,11 @@ import (
) )
type Account interface { type Account interface {
Add(user *types.User, name string) (*types.Account, error) Add(ctx context.Context, user *types.User, name string) (*types.Account, error)
UpdateName(user *types.User, id string, name string) (*types.Account, error) UpdateName(ctx context.Context, user *types.User, id string, name string) (*types.Account, error)
Get(user *types.User, id string) (*types.Account, error) Get(ctx context.Context, user *types.User, id string) (*types.Account, error)
GetAll(user *types.User) ([]*types.Account, error) GetAll(ctx context.Context, user *types.User) ([]*types.Account, error)
Delete(user *types.User, id string) error Delete(ctx context.Context, user *types.User, id string) error
} }
type AccountImpl struct { type AccountImpl struct {
@@ -33,12 +34,12 @@ func NewAccount(db *sqlx.DB, random Random, clock Clock) Account {
} }
} }
func (s AccountImpl) Add(user *types.User, name string) (*types.Account, error) { func (s AccountImpl) Add(ctx context.Context, user *types.User, name string) (*types.Account, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
newId, err := s.random.UUID() newId, err := s.random.UUID(ctx)
if err != nil { if err != nil {
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -64,10 +65,10 @@ func (s AccountImpl) Add(user *types.User, name string) (*types.Account, error)
UpdatedBy: nil, UpdatedBy: nil,
} }
r, err := s.db.NamedExec(` r, err := s.db.NamedExecContext(ctx, `
INSERT INTO account (id, user_id, name, current_balance, oink_balance, created_at, created_by) INSERT INTO account (id, user_id, name, current_balance, oink_balance, created_at, created_by)
VALUES (:id, :user_id, :name, :current_balance, :oink_balance, :created_at, :created_by)`, account) VALUES (:id, :user_id, :name, :current_balance, :oink_balance, :created_at, :created_by)`, account)
err = db.TransformAndLogDbError("account Insert", r, err) err = db.TransformAndLogDbError(ctx, "account Insert", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -75,7 +76,7 @@ func (s AccountImpl) Add(user *types.User, name string) (*types.Account, error)
return account, nil return account, nil
} }
func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*types.Account, error) { func (s AccountImpl) UpdateName(ctx context.Context, user *types.User, id string, name string) (*types.Account, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -85,12 +86,12 @@ func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*type
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
slog.Error("account update", "err", err) slog.ErrorContext(ctx, "account update", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("account Update", nil, err) err = db.TransformAndLogDbError(ctx, "account Update", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -99,8 +100,8 @@ func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*type
}() }()
var account types.Account var account types.Account
err = tx.Get(&account, `SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid) err = tx.GetContext(ctx, &account, `SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("account Update", nil, err) err = db.TransformAndLogDbError(ctx, "account Update", nil, err)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("account %v not found: %w", id, ErrBadRequest) return nil, fmt.Errorf("account %v not found: %w", id, ErrBadRequest)
@@ -113,7 +114,7 @@ func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*type
account.UpdatedAt = &timestamp account.UpdatedAt = &timestamp
account.UpdatedBy = &user.Id account.UpdatedBy = &user.Id
r, err := tx.NamedExec(` r, err := tx.NamedExecContext(ctx, `
UPDATE account UPDATE account
SET SET
name = :name, name = :name,
@@ -121,13 +122,13 @@ func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*type
updated_by = :updated_by updated_by = :updated_by
WHERE id = :id WHERE id = :id
AND user_id = :user_id`, account) AND user_id = :user_id`, account)
err = db.TransformAndLogDbError("account Update", r, err) err = db.TransformAndLogDbError(ctx, "account Update", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("account Update", nil, err) err = db.TransformAndLogDbError(ctx, "account Update", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -135,37 +136,37 @@ func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*type
return &account, nil return &account, nil
} }
func (s AccountImpl) Get(user *types.User, id string) (*types.Account, error) { func (s AccountImpl) Get(ctx context.Context, user *types.User, id string) (*types.Account, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
slog.Error("account get", "err", err) slog.ErrorContext(ctx, "account get", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
var account types.Account var account types.Account
err = s.db.Get(&account, ` err = s.db.GetContext(ctx, &account, `
SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid) SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("account Get", nil, err) err = db.TransformAndLogDbError(ctx, "account Get", nil, err)
if err != nil { if err != nil {
slog.Error("account get", "err", err) slog.ErrorContext(ctx, "account get", "err", err)
return nil, err return nil, err
} }
return &account, nil return &account, nil
} }
func (s AccountImpl) GetAll(user *types.User) ([]*types.Account, error) { func (s AccountImpl) GetAll(ctx context.Context, user *types.User) ([]*types.Account, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
accounts := make([]*types.Account, 0) accounts := make([]*types.Account, 0)
err := s.db.Select(&accounts, ` err := s.db.SelectContext(ctx, &accounts, `
SELECT * FROM account WHERE user_id = ? ORDER BY name`, user.Id) SELECT * FROM account WHERE user_id = ? ORDER BY name`, user.Id)
err = db.TransformAndLogDbError("account GetAll", nil, err) err = db.TransformAndLogDbError(ctx, "account GetAll", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -173,18 +174,18 @@ func (s AccountImpl) GetAll(user *types.User) ([]*types.Account, error) {
return accounts, nil return accounts, nil
} }
func (s AccountImpl) Delete(user *types.User, id string) error { func (s AccountImpl) Delete(ctx context.Context, user *types.User, id string) error {
if user == nil { if user == nil {
return ErrUnauthorized return ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
slog.Error("account delete", "err", err) slog.ErrorContext(ctx, "account delete", "err", err)
return fmt.Errorf("could not parse Id: %w", ErrBadRequest) return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("account Delete", nil, err) err = db.TransformAndLogDbError(ctx, "account Delete", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -193,8 +194,8 @@ func (s AccountImpl) Delete(user *types.User, id string) error {
}() }()
transactionsCount := 0 transactionsCount := 0
err = tx.Get(&transactionsCount, `SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND account_id = ?`, user.Id, uuid) err = tx.GetContext(ctx, &transactionsCount, `SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND account_id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("account Delete", nil, err) err = db.TransformAndLogDbError(ctx, "account Delete", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -202,14 +203,14 @@ func (s AccountImpl) Delete(user *types.User, id string) error {
return fmt.Errorf("account has transactions, cannot delete: %w", ErrBadRequest) return fmt.Errorf("account has transactions, cannot delete: %w", ErrBadRequest)
} }
res, err := tx.Exec("DELETE FROM account WHERE id = ? and user_id = ?", uuid, user.Id) res, err := tx.ExecContext(ctx, "DELETE FROM account WHERE id = ? and user_id = ?", uuid, user.Id)
err = db.TransformAndLogDbError("account Delete", res, err) err = db.TransformAndLogDbError(ctx, "account Delete", res, err)
if err != nil { if err != nil {
return err return err
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("account Delete", nil, err) err = db.TransformAndLogDbError(ctx, "account Delete", nil, err)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -26,24 +26,26 @@ var (
) )
type Auth interface { type Auth interface {
SignUp(email string, password string) (*types.User, error) SignUp(ctx context.Context, email string, password string) (*types.User, error)
SendVerificationMail(userId uuid.UUID, email string) SendVerificationMail(ctx context.Context, userId uuid.UUID, email string)
VerifyUserEmail(token string) error VerifyUserEmail(ctx context.Context, token string) error
SignIn(session *types.Session, email string, password string) (*types.Session, *types.User, error) SignIn(ctx context.Context, session *types.Session, email string, password string) (*types.Session, *types.User, error)
SignInSession(sessionId string) (*types.Session, *types.User, error) SignInSession(ctx context.Context, sessionId string) (*types.Session, *types.User, error)
SignInAnonymous() (*types.Session, error) SignInAnonymous(ctx context.Context) (*types.Session, error)
SignOut(sessionId string) error SignOut(ctx context.Context, sessionId string) error
DeleteAccount(user *types.User, currPass string) error DeleteAccount(ctx context.Context, user *types.User, currPass string) error
ChangePassword(user *types.User, sessionId string, currPass, newPass string) error ChangePassword(ctx context.Context, user *types.User, sessionId string, currPass, newPass string) error
SendForgotPasswordMail(email string) error SendForgotPasswordMail(ctx context.Context, email string) error
ForgotPassword(token string, newPass string) error ForgotPassword(ctx context.Context, token string, newPass string) error
IsCsrfTokenValid(tokenStr string, sessionId string) bool IsCsrfTokenValid(ctx context.Context, tokenStr string, sessionId string) bool
GetCsrfToken(session *types.Session) (string, error) GetCsrfToken(ctx context.Context, session *types.Session) (string, error)
CleanupSessionsAndTokens(ctx context.Context) error
} }
type AuthImpl struct { type AuthImpl struct {
@@ -64,8 +66,8 @@ func NewAuth(db db.Auth, random Random, clock Clock, mail Mail, serverSettings *
} }
} }
func (service AuthImpl) SignIn(session *types.Session, email string, password string) (*types.Session, *types.User, error) { func (service AuthImpl) SignIn(ctx context.Context, session *types.Session, email string, password string) (*types.Session, *types.User, error) {
user, err := service.db.GetUserByEmail(email) user, err := service.db.GetUserByEmail(ctx, email)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil, nil, ErrInvalidCredentials return nil, nil, ErrInvalidCredentials
@@ -80,30 +82,41 @@ func (service AuthImpl) SignIn(session *types.Session, email string, password st
return nil, nil, ErrInvalidCredentials return nil, nil, ErrInvalidCredentials
} }
err = service.cleanUpSessionWithTokens(session) newSession, err := service.createSession(ctx, user.Id)
if err != nil { if err != nil {
return nil, nil, types.ErrInternal return nil, nil, types.ErrInternal
} }
session, err = service.createSession(user.Id) err = service.db.DeleteSession(ctx, session.Id)
if err != nil { if err != nil {
return nil, nil, types.ErrInternal return nil, nil, types.ErrInternal
} }
return session, user, nil tokens, err := service.db.GetTokensBySessionIdAndType(ctx, session.Id, types.TokenTypeCsrf)
if err != nil {
return nil, nil, types.ErrInternal
}
for _, token := range tokens {
err = service.db.DeleteToken(ctx, token.Token)
if err != nil {
return nil, nil, types.ErrInternal
}
}
return newSession, user, nil
} }
func (service AuthImpl) SignInSession(sessionId string) (*types.Session, *types.User, error) { func (service AuthImpl) SignInSession(ctx context.Context, sessionId string) (*types.Session, *types.User, error) {
if sessionId == "" { if sessionId == "" {
return nil, nil, ErrSessionIdInvalid return nil, nil, ErrSessionIdInvalid
} }
session, err := service.db.GetSession(sessionId) session, err := service.db.GetSession(ctx, sessionId)
if err != nil { if err != nil {
return nil, nil, types.ErrInternal return nil, nil, types.ErrInternal
} }
if session.ExpiresAt.Before(service.clock.Now()) { if session.ExpiresAt.Before(service.clock.Now()) {
_ = service.db.DeleteSession(sessionId) _ = service.db.DeleteSession(ctx, sessionId)
return nil, nil, nil return nil, nil, nil
} }
@@ -111,7 +124,7 @@ func (service AuthImpl) SignInSession(sessionId string) (*types.Session, *types.
return session, nil, nil return session, nil, nil
} }
user, err := service.db.GetUser(session.UserId) user, err := service.db.GetUser(ctx, session.UserId)
if err != nil { if err != nil {
return nil, nil, types.ErrInternal return nil, nil, types.ErrInternal
} }
@@ -119,18 +132,18 @@ func (service AuthImpl) SignInSession(sessionId string) (*types.Session, *types.
return session, user, nil return session, user, nil
} }
func (service AuthImpl) SignInAnonymous() (*types.Session, error) { func (service AuthImpl) SignInAnonymous(ctx context.Context) (*types.Session, error) {
session, err := service.createSession(uuid.Nil) session, err := service.createSession(ctx, uuid.Nil)
if err != nil { if err != nil {
return nil, types.ErrInternal return nil, types.ErrInternal
} }
slog.Info("anonymous session created", "session-id", session.Id) slog.InfoContext(ctx, "anonymous session created", "session-id", session.Id)
return session, nil return session, nil
} }
func (service AuthImpl) SignUp(email string, password string) (*types.User, error) { func (service AuthImpl) SignUp(ctx context.Context, email string, password string) (*types.User, error) {
_, err := mail.ParseAddress(email) _, err := mail.ParseAddress(email)
if err != nil { if err != nil {
return nil, ErrInvalidEmail return nil, ErrInvalidEmail
@@ -140,12 +153,12 @@ func (service AuthImpl) SignUp(email string, password string) (*types.User, erro
return nil, ErrInvalidPassword return nil, ErrInvalidPassword
} }
userId, err := service.random.UUID() userId, err := service.random.UUID(ctx)
if err != nil { if err != nil {
return nil, types.ErrInternal return nil, types.ErrInternal
} }
salt, err := service.random.Bytes(16) salt, err := service.random.Bytes(ctx, 16)
if err != nil { if err != nil {
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -154,7 +167,7 @@ func (service AuthImpl) SignUp(email string, password string) (*types.User, erro
user := types.NewUser(userId, email, false, nil, false, hash, salt, service.clock.Now()) user := types.NewUser(userId, email, false, nil, false, hash, salt, service.clock.Now())
err = service.db.InsertUser(user) err = service.db.InsertUser(ctx, user)
if err != nil { if err != nil {
if errors.Is(err, db.ErrAlreadyExists) { if errors.Is(err, db.ErrAlreadyExists) {
return nil, ErrAccountExists return nil, ErrAccountExists
@@ -166,8 +179,8 @@ func (service AuthImpl) SignUp(email string, password string) (*types.User, erro
return user, nil return user, nil
} }
func (service AuthImpl) SendVerificationMail(userId uuid.UUID, email string) { func (service AuthImpl) SendVerificationMail(ctx context.Context, userId uuid.UUID, email string) {
tokens, err := service.db.GetTokensByUserIdAndType(userId, types.TokenTypeEmailVerify) tokens, err := service.db.GetTokensByUserIdAndType(ctx, userId, types.TokenTypeEmailVerify)
if err != nil && !errors.Is(err, db.ErrNotFound) { if err != nil && !errors.Is(err, db.ErrNotFound) {
return return
} }
@@ -179,7 +192,7 @@ func (service AuthImpl) SendVerificationMail(userId uuid.UUID, email string) {
} }
if token == nil { if token == nil {
newTokenStr, err := service.random.String(32) newTokenStr, err := service.random.String(ctx, 32)
if err != nil { if err != nil {
return return
} }
@@ -192,7 +205,7 @@ func (service AuthImpl) SendVerificationMail(userId uuid.UUID, email string) {
service.clock.Now(), service.clock.Now(),
service.clock.Now().Add(24*time.Hour)) service.clock.Now().Add(24*time.Hour))
err = service.db.InsertToken(token) err = service.db.InsertToken(ctx, token)
if err != nil { if err != nil {
return return
} }
@@ -201,24 +214,24 @@ func (service AuthImpl) SendVerificationMail(userId uuid.UUID, email string) {
var w strings.Builder var w strings.Builder
err = mailTemplate.Register(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &w) err = mailTemplate.Register(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &w)
if err != nil { if err != nil {
slog.Error("Could not render welcome email", "err", err) slog.ErrorContext(ctx, "Could not render welcome email", "err", err)
return return
} }
service.mail.SendMail(email, "Welcome to spend-sparrow", w.String()) service.mail.SendMail(ctx, email, "Welcome to spend-sparrow", w.String())
} }
func (service AuthImpl) VerifyUserEmail(tokenStr string) error { func (service AuthImpl) VerifyUserEmail(ctx context.Context, tokenStr string) error {
if tokenStr == "" { if tokenStr == "" {
return types.ErrInternal return types.ErrInternal
} }
token, err := service.db.GetToken(tokenStr) token, err := service.db.GetToken(ctx, tokenStr)
if err != nil { if err != nil {
return types.ErrInternal return types.ErrInternal
} }
user, err := service.db.GetUser(token.UserId) user, err := service.db.GetUser(ctx, token.UserId)
if err != nil { if err != nil {
return types.ErrInternal return types.ErrInternal
} }
@@ -236,21 +249,21 @@ func (service AuthImpl) VerifyUserEmail(tokenStr string) error {
user.EmailVerified = true user.EmailVerified = true
user.EmailVerifiedAt = &now user.EmailVerifiedAt = &now
err = service.db.UpdateUser(user) err = service.db.UpdateUser(ctx, user)
if err != nil { if err != nil {
return types.ErrInternal return types.ErrInternal
} }
_ = service.db.DeleteToken(token.Token) _ = service.db.DeleteToken(ctx, token.Token)
return nil return nil
} }
func (service AuthImpl) SignOut(sessionId string) error { func (service AuthImpl) SignOut(ctx context.Context, sessionId string) error {
return service.db.DeleteSession(sessionId) return service.db.DeleteSession(ctx, sessionId)
} }
func (service AuthImpl) DeleteAccount(user *types.User, currPass string) error { func (service AuthImpl) DeleteAccount(ctx context.Context, user *types.User, currPass string) error {
userDb, err := service.db.GetUser(user.Id) userDb, err := service.db.GetUser(ctx, user.Id)
if err != nil { if err != nil {
return types.ErrInternal return types.ErrInternal
} }
@@ -260,17 +273,17 @@ func (service AuthImpl) DeleteAccount(user *types.User, currPass string) error {
return ErrInvalidCredentials return ErrInvalidCredentials
} }
err = service.db.DeleteUser(user.Id) err = service.db.DeleteUser(ctx, user.Id)
if err != nil { if err != nil {
return err return err
} }
service.mail.SendMail(user.Email, "Account deleted", "Your account has been deleted") service.mail.SendMail(ctx, user.Email, "Account deleted", "Your account has been deleted")
return nil return nil
} }
func (service AuthImpl) ChangePassword(user *types.User, sessionId string, currPass, newPass string) error { func (service AuthImpl) ChangePassword(ctx context.Context, user *types.User, sessionId string, currPass, newPass string) error {
if !isPasswordValid(newPass) { if !isPasswordValid(newPass) {
return ErrInvalidPassword return ErrInvalidPassword
} }
@@ -288,18 +301,18 @@ func (service AuthImpl) ChangePassword(user *types.User, sessionId string, currP
newHash := GetHashPassword(newPass, user.Salt) newHash := GetHashPassword(newPass, user.Salt)
user.Password = newHash user.Password = newHash
err := service.db.UpdateUser(user) err := service.db.UpdateUser(ctx, user)
if err != nil { if err != nil {
return err return err
} }
sessions, err := service.db.GetSessions(user.Id) sessions, err := service.db.GetSessions(ctx, user.Id)
if err != nil { if err != nil {
return types.ErrInternal return types.ErrInternal
} }
for _, s := range sessions { for _, s := range sessions {
if s.Id != sessionId { if s.Id != sessionId {
err = service.db.DeleteSession(s.Id) err = service.db.DeleteSession(ctx, s.Id)
if err != nil { if err != nil {
return types.ErrInternal return types.ErrInternal
} }
@@ -309,13 +322,13 @@ func (service AuthImpl) ChangePassword(user *types.User, sessionId string, currP
return nil return nil
} }
func (service AuthImpl) SendForgotPasswordMail(email string) error { func (service AuthImpl) SendForgotPasswordMail(ctx context.Context, email string) error {
tokenStr, err := service.random.String(32) tokenStr, err := service.random.String(ctx, 32)
if err != nil { if err != nil {
return err return err
} }
user, err := service.db.GetUserByEmail(email) user, err := service.db.GetUserByEmail(ctx, email)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil return nil
@@ -332,7 +345,7 @@ func (service AuthImpl) SendForgotPasswordMail(email string) error {
service.clock.Now(), service.clock.Now(),
service.clock.Now().Add(15*time.Minute)) service.clock.Now().Add(15*time.Minute))
err = service.db.InsertToken(token) err = service.db.InsertToken(ctx, token)
if err != nil { if err != nil {
return types.ErrInternal return types.ErrInternal
} }
@@ -340,25 +353,25 @@ func (service AuthImpl) SendForgotPasswordMail(email string) error {
var mail strings.Builder var mail strings.Builder
err = mailTemplate.ResetPassword(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &mail) err = mailTemplate.ResetPassword(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &mail)
if err != nil { if err != nil {
slog.Error("Could not render reset password email", "err", err) slog.ErrorContext(ctx, "Could not render reset password email", "err", err)
return types.ErrInternal return types.ErrInternal
} }
service.mail.SendMail(email, "Reset Password", mail.String()) service.mail.SendMail(ctx, email, "Reset Password", mail.String())
return nil return nil
} }
func (service AuthImpl) ForgotPassword(tokenStr string, newPass string) error { func (service AuthImpl) ForgotPassword(ctx context.Context, tokenStr string, newPass string) error {
if !isPasswordValid(newPass) { if !isPasswordValid(newPass) {
return ErrInvalidPassword return ErrInvalidPassword
} }
token, err := service.db.GetToken(tokenStr) token, err := service.db.GetToken(ctx, tokenStr)
if err != nil { if err != nil {
return ErrTokenInvalid return ErrTokenInvalid
} }
err = service.db.DeleteToken(tokenStr) err = service.db.DeleteToken(ctx, tokenStr)
if err != nil { if err != nil {
return err return err
} }
@@ -368,27 +381,27 @@ func (service AuthImpl) ForgotPassword(tokenStr string, newPass string) error {
return ErrTokenInvalid return ErrTokenInvalid
} }
user, err := service.db.GetUser(token.UserId) user, err := service.db.GetUser(ctx, token.UserId)
if err != nil { if err != nil {
slog.Error("Could not get user from token", "err", err) slog.ErrorContext(ctx, "Could not get user from token", "err", err)
return types.ErrInternal return types.ErrInternal
} }
passHash := GetHashPassword(newPass, user.Salt) passHash := GetHashPassword(newPass, user.Salt)
user.Password = passHash user.Password = passHash
err = service.db.UpdateUser(user) err = service.db.UpdateUser(ctx, user)
if err != nil { if err != nil {
return err return err
} }
sessions, err := service.db.GetSessions(user.Id) sessions, err := service.db.GetSessions(ctx, user.Id)
if err != nil { if err != nil {
return types.ErrInternal return types.ErrInternal
} }
for _, session := range sessions { for _, session := range sessions {
err = service.db.DeleteSession(session.Id) err = service.db.DeleteSession(ctx, session.Id)
if err != nil { if err != nil {
return types.ErrInternal return types.ErrInternal
} }
@@ -397,8 +410,8 @@ func (service AuthImpl) ForgotPassword(tokenStr string, newPass string) error {
return nil return nil
} }
func (service AuthImpl) IsCsrfTokenValid(tokenStr string, sessionId string) bool { func (service AuthImpl) IsCsrfTokenValid(ctx context.Context, tokenStr string, sessionId string) bool {
token, err := service.db.GetToken(tokenStr) token, err := service.db.GetToken(ctx, tokenStr)
if err != nil { if err != nil {
return false return false
} }
@@ -412,18 +425,18 @@ func (service AuthImpl) IsCsrfTokenValid(tokenStr string, sessionId string) bool
return true return true
} }
func (service AuthImpl) GetCsrfToken(session *types.Session) (string, error) { func (service AuthImpl) GetCsrfToken(ctx context.Context, session *types.Session) (string, error) {
if session == nil { if session == nil {
return "", types.ErrInternal return "", types.ErrInternal
} }
tokens, _ := service.db.GetTokensBySessionIdAndType(session.Id, types.TokenTypeCsrf) tokens, _ := service.db.GetTokensBySessionIdAndType(ctx, session.Id, types.TokenTypeCsrf)
if len(tokens) > 0 { if len(tokens) > 0 {
return tokens[0].Token, nil return tokens[0].Token, nil
} }
tokenStr, err := service.random.String(32) tokenStr, err := service.random.String(ctx, 32)
if err != nil { if err != nil {
return "", types.ErrInternal return "", types.ErrInternal
} }
@@ -435,47 +448,32 @@ func (service AuthImpl) GetCsrfToken(session *types.Session) (string, error) {
types.TokenTypeCsrf, types.TokenTypeCsrf,
service.clock.Now(), service.clock.Now(),
service.clock.Now().Add(8*time.Hour)) service.clock.Now().Add(8*time.Hour))
err = service.db.InsertToken(token) err = service.db.InsertToken(ctx, token)
if err != nil { if err != nil {
return "", types.ErrInternal return "", types.ErrInternal
} }
slog.Info("CSRF-Token created", "token", tokenStr) slog.InfoContext(ctx, "CSRF-Token created", "token", tokenStr)
return tokenStr, nil return tokenStr, nil
} }
func (service AuthImpl) cleanUpSessionWithTokens(session *types.Session) error { func (service AuthImpl) CleanupSessionsAndTokens(ctx context.Context) error {
if session == nil { err := service.db.DeleteOldSessions(ctx)
return nil
}
err := service.db.DeleteSession(session.Id)
if err != nil { if err != nil {
return types.ErrInternal return types.ErrInternal
} }
tokens, err := service.db.GetTokensBySessionIdAndType(session.Id, types.TokenTypeCsrf) err = service.db.DeleteOldTokens(ctx)
if err != nil { if err != nil {
return types.ErrInternal return types.ErrInternal
} }
for _, token := range tokens {
err = service.db.DeleteToken(token.Token)
if err != nil {
return types.ErrInternal
}
}
return nil return nil
} }
func (service AuthImpl) createSession(userId uuid.UUID) (*types.Session, error) { func (service AuthImpl) createSession(ctx context.Context, userId uuid.UUID) (*types.Session, error) {
sessionId, err := service.random.String(32) sessionId, err := service.random.String(ctx, 32)
if err != nil {
return nil, types.ErrInternal
}
err = service.db.DeleteOldSessions(userId)
if err != nil { if err != nil {
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -485,7 +483,7 @@ func (service AuthImpl) createSession(userId uuid.UUID) (*types.Session, error)
session := types.NewSession(sessionId, userId, createAt, expiresAt) session := types.NewSession(sessionId, userId, createAt, expiresAt)
err = service.db.InsertSession(session) err = service.db.InsertSession(ctx, session)
if err != nil { if err != nil {
return nil, types.ErrInternal return nil, types.ErrInternal
} }

View File

@@ -13,5 +13,5 @@ func NewClock() Clock {
} }
func (c *ClockImpl) Now() time.Time { func (c *ClockImpl) Now() time.Time {
return time.Now() return time.Now().UTC()
} }

View File

@@ -0,0 +1,175 @@
package service
import (
"context"
"spend-sparrow/internal/db"
"spend-sparrow/internal/types"
"time"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
type Dashboard struct {
db *sqlx.DB
}
func NewDashboard(db *sqlx.DB) *Dashboard {
return &Dashboard{
db: db,
}
}
func (s Dashboard) MainChart(
ctx context.Context,
user *types.User,
) ([]types.DashboardMainChartEntry, error) {
if user == nil {
return nil, ErrUnauthorized
}
transactions := make([]types.Transaction, 0)
err := s.db.SelectContext(ctx, &transactions, `
SELECT *
FROM "transaction"
WHERE user_id = ?
ORDER BY timestamp`, user.Id)
err = db.TransformAndLogDbError(ctx, "dashboard Chart", nil, err)
if err != nil {
return nil, err
}
timeEntries := make([]types.DashboardMainChartEntry, 0)
var lastEntry *types.DashboardMainChartEntry
for _, t := range transactions {
if t.Error != nil {
continue
}
newDay := t.Timestamp.Truncate(24 * time.Hour)
if lastEntry == nil {
lastEntry = &types.DashboardMainChartEntry{
Day: newDay,
Value: 0,
Savings: 0,
}
} else if lastEntry.Day != newDay {
timeEntries = append(timeEntries, *lastEntry)
lastEntry = &types.DashboardMainChartEntry{
Day: newDay,
Value: lastEntry.Value,
Savings: lastEntry.Savings,
}
}
if t.AccountId != nil {
lastEntry.Value += t.Value
}
if t.TreasureChestId != nil {
lastEntry.Savings += t.Value
}
}
if lastEntry != nil {
timeEntries = append(timeEntries, *lastEntry)
}
return timeEntries, nil
}
func (s Dashboard) TreasureChests(
ctx context.Context,
user *types.User,
) ([]*types.DashboardTreasureChest, error) {
if user == nil {
return nil, ErrUnauthorized
}
treasureChests := make([]*types.TreasureChest, 0)
err := s.db.SelectContext(ctx, &treasureChests, `SELECT * FROM treasure_chest WHERE user_id = ?`, user.Id)
err = db.TransformAndLogDbError(ctx, "dashboard TreasureChests", nil, err)
if err != nil {
return nil, err
}
treasureChests = sortTreasureChests(treasureChests)
result := make([]*types.DashboardTreasureChest, 0)
for _, t := range treasureChests {
if t.ParentId == nil {
result = append(result, &types.DashboardTreasureChest{
Name: t.Name,
Value: t.CurrentBalance,
Children: make([]types.DashboardTreasureChest, 0),
})
} else {
result[len(result)-1].Children = append(result[len(result)-1].Children, types.DashboardTreasureChest{
Name: t.Name,
Value: t.CurrentBalance,
Children: make([]types.DashboardTreasureChest, 0),
})
}
}
return result, nil
}
func (s Dashboard) TreasureChest(
ctx context.Context,
user *types.User,
treausureChestId *uuid.UUID,
) ([]types.DashboardMainChartEntry, error) {
if user == nil {
return nil, ErrUnauthorized
}
transactions := make([]types.Transaction, 0)
err := s.db.SelectContext(ctx, &transactions, `
SELECT *
FROM "transaction"
WHERE user_id = ?
AND treasure_chest_id = ?
ORDER BY timestamp`, user.Id, treausureChestId)
err = db.TransformAndLogDbError(ctx, "dashboard Chart", nil, err)
if err != nil {
return nil, err
}
timeEntries := make([]types.DashboardMainChartEntry, 0)
var lastEntry *types.DashboardMainChartEntry
for _, t := range transactions {
if t.Error != nil {
continue
}
newDay := t.Timestamp.Truncate(24 * time.Hour)
if lastEntry == nil {
lastEntry = &types.DashboardMainChartEntry{
Day: newDay,
Value: 0,
}
} else if lastEntry.Day != newDay {
timeEntries = append(timeEntries, *lastEntry)
lastEntry = &types.DashboardMainChartEntry{
Day: newDay,
Value: lastEntry.Value,
}
}
if t.TreasureChestId != nil {
lastEntry.Value += t.Value
}
}
if lastEntry != nil {
timeEntries = append(timeEntries, *lastEntry)
}
return timeEntries, nil
}

View File

@@ -10,7 +10,7 @@ const (
) )
var ( var (
safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9ÄÖÜäöüß,&'" -]+$`) safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9ÄÖÜäöüß,&'". \-\?]+$`)
) )
func validateString(value string, fieldName string) error { func validateString(value string, fieldName string) error {

View File

@@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"net/smtp" "net/smtp"
@@ -9,7 +10,7 @@ import (
type Mail interface { type Mail interface {
// Sending an email is a fire and forget operation. Thus no error handling // Sending an email is a fire and forget operation. Thus no error handling
SendMail(to string, subject string, message string) SendMail(ctx context.Context, to string, subject string, message string)
} }
type MailImpl struct { type MailImpl struct {
@@ -20,11 +21,11 @@ func NewMail(server *types.Settings) MailImpl {
return MailImpl{server: server} return MailImpl{server: server}
} }
func (m MailImpl) SendMail(to string, subject string, message string) { func (m MailImpl) SendMail(ctx context.Context, to string, subject string, message string) {
go m.internalSendMail(to, subject, message) go m.internalSendMail(ctx, to, subject, message)
} }
func (m MailImpl) internalSendMail(to string, subject string, message string) { func (m MailImpl) internalSendMail(ctx context.Context, to string, subject string, message string) {
if m.server.Smtp == nil { if m.server.Smtp == nil {
return return
} }
@@ -47,9 +48,9 @@ func (m MailImpl) internalSendMail(to string, subject string, message string) {
subject, subject,
message) message)
slog.Info("sending mail", "to", to) slog.InfoContext(ctx, "sending mail", "to", to)
err := smtp.SendMail(s.Host+":"+s.Port, auth, s.FromMail, []string{to}, []byte(msg)) err := smtp.SendMail(s.Host+":"+s.Port, auth, s.FromMail, []string{to}, []byte(msg))
if err != nil { if err != nil {
slog.Error("Error sending mail", "err", err) slog.ErrorContext(ctx, "Error sending mail", "err", err)
} }
} }

View File

@@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"log/slog" "log/slog"
@@ -10,9 +11,9 @@ import (
) )
type Random interface { type Random interface {
Bytes(size int) ([]byte, error) Bytes(ctx context.Context, size int) ([]byte, error)
String(size int) (string, error) String(ctx context.Context, size int) (string, error)
UUID() (uuid.UUID, error) UUID(ctx context.Context) (uuid.UUID, error)
} }
type RandomImpl struct { type RandomImpl struct {
@@ -22,31 +23,31 @@ func NewRandom() *RandomImpl {
return &RandomImpl{} return &RandomImpl{}
} }
func (r *RandomImpl) Bytes(size int) ([]byte, error) { func (r *RandomImpl) Bytes(ctx context.Context, tsize int) ([]byte, error) {
b := make([]byte, 32) b := make([]byte, 32)
_, err := rand.Read(b) _, err := rand.Read(b)
if err != nil { if err != nil {
slog.Error("Error generating random bytes", "err", err) slog.ErrorContext(ctx, "Error generating random bytes", "err", err)
return []byte{}, types.ErrInternal return []byte{}, types.ErrInternal
} }
return b, nil return b, nil
} }
func (r *RandomImpl) String(size int) (string, error) { func (r *RandomImpl) String(ctx context.Context, size int) (string, error) {
bytes, err := r.Bytes(size) bytes, err := r.Bytes(ctx, size)
if err != nil { if err != nil {
slog.Error("Error generating random string", "err", err) slog.ErrorContext(ctx, "Error generating random string", "err", err)
return "", types.ErrInternal return "", types.ErrInternal
} }
return base64.StdEncoding.EncodeToString(bytes), nil return base64.StdEncoding.EncodeToString(bytes), nil
} }
func (r *RandomImpl) UUID() (uuid.UUID, error) { func (r *RandomImpl) UUID(ctx context.Context) (uuid.UUID, error) {
id, err := uuid.NewRandom() id, err := uuid.NewRandom()
if err != nil { if err != nil {
slog.Error("Error generating random UUID", "err", err) slog.ErrorContext(ctx, "Error generating random UUID", "err", err)
return uuid.Nil, types.ErrInternal return uuid.Nil, types.ErrInternal
} }

View File

@@ -1,25 +1,29 @@
package service package service
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"spend-sparrow/internal/db" "spend-sparrow/internal/db"
"spend-sparrow/internal/types" "spend-sparrow/internal/types"
"strconv"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
type Transaction interface { const page_size = 25
Add(tx *sqlx.Tx, user *types.User, transaction types.Transaction) (*types.Transaction, error)
Update(user *types.User, transaction types.Transaction) (*types.Transaction, error)
Get(user *types.User, id string) (*types.Transaction, error)
GetAll(user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error)
Delete(user *types.User, id string) error
RecalculateBalances(user *types.User) error type Transaction interface {
Add(ctx context.Context, tx *sqlx.Tx, user *types.User, transaction types.Transaction) (*types.Transaction, error)
Update(ctx context.Context, user *types.User, transaction types.Transaction) (*types.Transaction, error)
Get(ctx context.Context, user *types.User, id string) (*types.Transaction, error)
GetAll(ctx context.Context, user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error)
Delete(ctx context.Context, user *types.User, id string) error
RecalculateBalances(ctx context.Context, user *types.User) error
} }
type TransactionImpl struct { type TransactionImpl struct {
@@ -36,7 +40,7 @@ func NewTransaction(db *sqlx.DB, random Random, clock Clock) Transaction {
} }
} }
func (s TransactionImpl) Add(tx *sqlx.Tx, user *types.User, transactionInput types.Transaction) (*types.Transaction, error) { func (s TransactionImpl) Add(ctx context.Context, tx *sqlx.Tx, user *types.User, transactionInput types.Transaction) (*types.Transaction, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -45,8 +49,8 @@ func (s TransactionImpl) Add(tx *sqlx.Tx, user *types.User, transactionInput typ
ownsTransaction := false ownsTransaction := false
if tx == nil { if tx == nil {
ownsTransaction = true ownsTransaction = true
tx, err = s.db.Beginx() tx, err = s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("transaction Add", nil, err) err = db.TransformAndLogDbError(ctx, "transaction Add", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -55,38 +59,38 @@ func (s TransactionImpl) Add(tx *sqlx.Tx, user *types.User, transactionInput typ
}() }()
} }
transaction, err := s.validateAndEnrichTransaction(tx, nil, user.Id, transactionInput) transaction, err := s.validateAndEnrichTransaction(ctx, tx, nil, user.Id, transactionInput)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r, err := tx.NamedExec(` r, err := tx.NamedExecContext(ctx, `
INSERT INTO "transaction" (id, user_id, account_id, treasure_chest_id, value, timestamp, INSERT INTO "transaction" (id, user_id, account_id, treasure_chest_id, value, timestamp,
party, description, error, created_at, created_by) party, description, error, created_at, created_by)
VALUES (:id, :user_id, :account_id, :treasure_chest_id, :value, :timestamp, VALUES (:id, :user_id, :account_id, :treasure_chest_id, :value, :timestamp,
:party, :description, :error, :created_at, :created_by)`, transaction) :party, :description, :error, :created_at, :created_by)`, transaction)
err = db.TransformAndLogDbError("transaction Insert", r, err) err = db.TransformAndLogDbError(ctx, "transaction Insert", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if transaction.Error == nil && transaction.AccountId != nil { if transaction.Error == nil && transaction.AccountId != nil {
r, err = tx.Exec(` r, err = tx.ExecContext(ctx, `
UPDATE account UPDATE account
SET current_balance = current_balance + ? SET current_balance = current_balance + ?
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id) WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
err = db.TransformAndLogDbError("transaction Add", r, err) err = db.TransformAndLogDbError(ctx, "transaction Add", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if transaction.Error == nil && transaction.TreasureChestId != nil { if transaction.Error == nil && transaction.TreasureChestId != nil {
r, err = tx.Exec(` r, err = tx.ExecContext(ctx, `
UPDATE treasure_chest UPDATE treasure_chest
SET current_balance = current_balance + ? SET current_balance = current_balance + ?
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id) WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
err = db.TransformAndLogDbError("transaction Add", r, err) err = db.TransformAndLogDbError(ctx, "transaction Add", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -94,7 +98,7 @@ func (s TransactionImpl) Add(tx *sqlx.Tx, user *types.User, transactionInput typ
if ownsTransaction { if ownsTransaction {
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("transaction Add", nil, err) err = db.TransformAndLogDbError(ctx, "transaction Add", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -103,13 +107,13 @@ func (s TransactionImpl) Add(tx *sqlx.Tx, user *types.User, transactionInput typ
return transaction, nil return transaction, nil
} }
func (s TransactionImpl) Update(user *types.User, input types.Transaction) (*types.Transaction, error) { func (s TransactionImpl) Update(ctx context.Context, user *types.User, input types.Transaction) (*types.Transaction, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("transaction Update", nil, err) err = db.TransformAndLogDbError(ctx, "transaction Update", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -118,8 +122,8 @@ func (s TransactionImpl) Update(user *types.User, input types.Transaction) (*typ
}() }()
transaction := &types.Transaction{} transaction := &types.Transaction{}
err = tx.Get(transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, input.Id) err = tx.GetContext(ctx, transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, input.Id)
err = db.TransformAndLogDbError("transaction Update", nil, err) err = db.TransformAndLogDbError(ctx, "transaction Update", nil, err)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("transaction %v not found: %w", input.Id, ErrBadRequest) return nil, fmt.Errorf("transaction %v not found: %w", input.Id, ErrBadRequest)
@@ -128,53 +132,53 @@ func (s TransactionImpl) Update(user *types.User, input types.Transaction) (*typ
} }
if transaction.Error == nil && transaction.AccountId != nil { if transaction.Error == nil && transaction.AccountId != nil {
r, err := tx.Exec(` r, err := tx.ExecContext(ctx, `
UPDATE account UPDATE account
SET current_balance = current_balance - ? SET current_balance = current_balance - ?
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id) WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
err = db.TransformAndLogDbError("transaction Update", r, err) err = db.TransformAndLogDbError(ctx, "transaction Update", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if transaction.Error == nil && transaction.TreasureChestId != nil { if transaction.Error == nil && transaction.TreasureChestId != nil {
r, err := tx.Exec(` r, err := tx.ExecContext(ctx, `
UPDATE treasure_chest UPDATE treasure_chest
SET current_balance = current_balance - ? SET current_balance = current_balance - ?
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id) WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
err = db.TransformAndLogDbError("transaction Update", r, err) err = db.TransformAndLogDbError(ctx, "transaction Update", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
transaction, err = s.validateAndEnrichTransaction(tx, transaction, user.Id, input) transaction, err = s.validateAndEnrichTransaction(ctx, tx, transaction, user.Id, input)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if transaction.Error == nil && transaction.AccountId != nil { if transaction.Error == nil && transaction.AccountId != nil {
r, err := tx.Exec(` r, err := tx.ExecContext(ctx, `
UPDATE account UPDATE account
SET current_balance = current_balance + ? SET current_balance = current_balance + ?
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id) WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
err = db.TransformAndLogDbError("transaction Update", r, err) err = db.TransformAndLogDbError(ctx, "transaction Update", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if transaction.Error == nil && transaction.TreasureChestId != nil { if transaction.Error == nil && transaction.TreasureChestId != nil {
r, err := tx.Exec(` r, err := tx.ExecContext(ctx, `
UPDATE treasure_chest UPDATE treasure_chest
SET current_balance = current_balance + ? SET current_balance = current_balance + ?
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id) WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
err = db.TransformAndLogDbError("transaction Update", r, err) err = db.TransformAndLogDbError(ctx, "transaction Update", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
r, err := tx.NamedExec(` r, err := tx.NamedExecContext(ctx, `
UPDATE "transaction" UPDATE "transaction"
SET SET
account_id = :account_id, account_id = :account_id,
@@ -188,13 +192,13 @@ func (s TransactionImpl) Update(user *types.User, input types.Transaction) (*typ
updated_by = :updated_by updated_by = :updated_by
WHERE id = :id WHERE id = :id
AND user_id = :user_id`, transaction) AND user_id = :user_id`, transaction)
err = db.TransformAndLogDbError("transaction Update", r, err) err = db.TransformAndLogDbError(ctx, "transaction Update", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("transaction Update", nil, err) err = db.TransformAndLogDbError(ctx, "transaction Update", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -202,19 +206,19 @@ func (s TransactionImpl) Update(user *types.User, input types.Transaction) (*typ
return transaction, nil return transaction, nil
} }
func (s TransactionImpl) Get(user *types.User, id string) (*types.Transaction, error) { func (s TransactionImpl) Get(ctx context.Context, user *types.User, id string) (*types.Transaction, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
slog.Error("transaction get", "err", err) slog.ErrorContext(ctx, "transaction get", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
var transaction types.Transaction var transaction types.Transaction
err = s.db.Get(&transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid) err = s.db.GetContext(ctx, &transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("transaction Get", nil, err) err = db.TransformAndLogDbError(ctx, "transaction Get", nil, err)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest) return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
@@ -225,28 +229,47 @@ func (s TransactionImpl) Get(user *types.User, id string) (*types.Transaction, e
return &transaction, nil return &transaction, nil
} }
func (s TransactionImpl) GetAll(user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error) { func (s TransactionImpl) GetAll(ctx context.Context, user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
var (
page int64
offset int64
err error
)
if filter.Page != "" {
page, err = strconv.ParseInt(filter.Page, 10, 64)
if err != nil {
offset = 0
} else {
offset = page - 1
offset *= page_size
}
}
transactions := make([]*types.Transaction, 0) transactions := make([]*types.Transaction, 0)
err := s.db.Select(&transactions, ` err = s.db.SelectContext(ctx, &transactions, `
SELECT * SELECT *
FROM "transaction" FROM "transaction"
WHERE user_id = ? WHERE user_id = ?
AND (? = '' OR account_id = ?) AND ($1 = '' OR account_id = $1)
AND (? = '' OR treasure_chest_id = ?) AND ($2 = '' OR treasure_chest_id = $2)
AND (? = '' AND ($3 = ''
OR (? = "true" AND error IS NOT NULL) OR ($3 = "true" AND error IS NOT NULL)
OR (? = "false" AND error IS NULL) OR ($3 = "false" AND error IS NULL)
) )
ORDER BY timestamp DESC, created_at DESC`, ORDER BY timestamp DESC, created_at DESC
LIMIT $4 OFFSET $5
`,
user.Id, user.Id,
filter.AccountId, filter.AccountId, filter.AccountId,
filter.TreasureChestId, filter.TreasureChestId, filter.TreasureChestId,
filter.Error, filter.Error, filter.Error) filter.Error,
err = db.TransformAndLogDbError("transaction GetAll", nil, err) page_size,
offset)
err = db.TransformAndLogDbError(ctx, "transaction GetAll", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -254,18 +277,18 @@ func (s TransactionImpl) GetAll(user *types.User, filter types.TransactionItemsF
return transactions, nil return transactions, nil
} }
func (s TransactionImpl) Delete(user *types.User, id string) error { func (s TransactionImpl) Delete(ctx context.Context, user *types.User, id string) error {
if user == nil { if user == nil {
return ErrUnauthorized return ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
slog.Error("transaction delete", "err", err) slog.ErrorContext(ctx, "transaction delete", "err", err)
return fmt.Errorf("could not parse Id: %w", ErrBadRequest) return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("transaction Delete", nil, err) err = db.TransformAndLogDbError(ctx, "transaction Delete", nil, err)
if err != nil { if err != nil {
return nil return nil
} }
@@ -274,44 +297,44 @@ func (s TransactionImpl) Delete(user *types.User, id string) error {
}() }()
var transaction types.Transaction var transaction types.Transaction
err = tx.Get(&transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid) err = tx.GetContext(ctx, &transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("transaction Delete", nil, err) err = db.TransformAndLogDbError(ctx, "transaction Delete", nil, err)
if err != nil { if err != nil {
return err return err
} }
if transaction.Error == nil && transaction.AccountId != nil { if transaction.Error == nil && transaction.AccountId != nil {
r, err := tx.Exec(` r, err := tx.ExecContext(ctx, `
UPDATE account UPDATE account
SET current_balance = current_balance - ? SET current_balance = current_balance - ?
WHERE id = ? WHERE id = ?
AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id) AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
err = db.TransformAndLogDbError("transaction Delete", r, err) err = db.TransformAndLogDbError(ctx, "transaction Delete", r, err)
if err != nil && !errors.Is(err, db.ErrNotFound) { if err != nil && !errors.Is(err, db.ErrNotFound) {
return err return err
} }
} }
if transaction.Error == nil && transaction.TreasureChestId != nil { if transaction.Error == nil && transaction.TreasureChestId != nil {
r, err := tx.Exec(` r, err := tx.ExecContext(ctx, `
UPDATE treasure_chest UPDATE treasure_chest
SET current_balance = current_balance - ? SET current_balance = current_balance - ?
WHERE id = ? WHERE id = ?
AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id) AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
err = db.TransformAndLogDbError("transaction Delete", r, err) err = db.TransformAndLogDbError(ctx, "transaction Delete", r, err)
if err != nil && !errors.Is(err, db.ErrNotFound) { if err != nil && !errors.Is(err, db.ErrNotFound) {
return err return err
} }
} }
r, err := tx.Exec("DELETE FROM \"transaction\" WHERE id = ? AND user_id = ?", uuid, user.Id) r, err := tx.ExecContext(ctx, "DELETE FROM \"transaction\" WHERE id = ? AND user_id = ?", uuid, user.Id)
err = db.TransformAndLogDbError("transaction Delete", r, err) err = db.TransformAndLogDbError(ctx, "transaction Delete", r, err)
if err != nil { if err != nil {
return err return err
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("transaction Delete", nil, err) err = db.TransformAndLogDbError(ctx, "transaction Delete", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -319,13 +342,13 @@ func (s TransactionImpl) Delete(user *types.User, id string) error {
return nil return nil
} }
func (s TransactionImpl) RecalculateBalances(user *types.User) error { func (s TransactionImpl) RecalculateBalances(ctx context.Context, user *types.User) error {
if user == nil { if user == nil {
return ErrUnauthorized return ErrUnauthorized
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("transaction RecalculateBalances", nil, err) err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -333,54 +356,54 @@ func (s TransactionImpl) RecalculateBalances(user *types.User) error {
_ = tx.Rollback() _ = tx.Rollback()
}() }()
r, err := tx.Exec(` r, err := tx.ExecContext(ctx, `
UPDATE account UPDATE account
SET current_balance = 0 SET current_balance = 0
WHERE user_id = ?`, user.Id) WHERE user_id = ?`, user.Id)
err = db.TransformAndLogDbError("transaction RecalculateBalances", r, err) err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", r, err)
if err != nil && !errors.Is(err, db.ErrNotFound) { if err != nil && !errors.Is(err, db.ErrNotFound) {
return err return err
} }
r, err = tx.Exec(` r, err = tx.ExecContext(ctx, `
UPDATE treasure_chest UPDATE treasure_chest
SET current_balance = 0 SET current_balance = 0
WHERE user_id = ?`, user.Id) WHERE user_id = ?`, user.Id)
err = db.TransformAndLogDbError("transaction RecalculateBalances", r, err) err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", r, err)
if err != nil && !errors.Is(err, db.ErrNotFound) { if err != nil && !errors.Is(err, db.ErrNotFound) {
return err return err
} }
rows, err := tx.Queryx(` rows, err := tx.QueryxContext(ctx, `
SELECT * SELECT *
FROM "transaction" FROM "transaction"
WHERE user_id = ?`, user.Id) WHERE user_id = ?`, user.Id)
err = db.TransformAndLogDbError("transaction RecalculateBalances", nil, err) err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", nil, err)
if err != nil && !errors.Is(err, db.ErrNotFound) { if err != nil && !errors.Is(err, db.ErrNotFound) {
return err return err
} }
defer func() { defer func() {
err := rows.Close() err := rows.Close()
if err != nil { if err != nil {
slog.Error("transaction RecalculateBalances", "err", err) slog.ErrorContext(ctx, "transaction RecalculateBalances", "err", err)
} }
}() }()
var transaction types.Transaction var transaction types.Transaction
for rows.Next() { for rows.Next() {
err = rows.StructScan(&transaction) err = rows.StructScan(&transaction)
err = db.TransformAndLogDbError("transaction RecalculateBalances", nil, err) err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", nil, err)
if err != nil { if err != nil {
return err return err
} }
s.updateErrors(&transaction) s.updateErrors(&transaction)
r, err = tx.Exec(` r, err = tx.ExecContext(ctx, `
UPDATE "transaction" UPDATE "transaction"
SET error = ? SET error = ?
WHERE user_id = ? WHERE user_id = ?
AND id = ?`, transaction.Error, user.Id, transaction.Id) AND id = ?`, transaction.Error, user.Id, transaction.Id)
err = db.TransformAndLogDbError("transaction RecalculateBalances", r, err) err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", r, err)
if err != nil { if err != nil {
return err return err
} }
@@ -390,21 +413,21 @@ func (s TransactionImpl) RecalculateBalances(user *types.User) error {
} }
if transaction.AccountId != nil { if transaction.AccountId != nil {
r, err = tx.Exec(` r, err = tx.ExecContext(ctx, `
UPDATE account UPDATE account
SET current_balance = current_balance + ? SET current_balance = current_balance + ?
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id) WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
err = db.TransformAndLogDbError("transaction RecalculateBalances", r, err) err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", r, err)
if err != nil { if err != nil {
return err return err
} }
} }
if transaction.TreasureChestId != nil { if transaction.TreasureChestId != nil {
r, err = tx.Exec(` r, err = tx.ExecContext(ctx, `
UPDATE treasure_chest UPDATE treasure_chest
SET current_balance = current_balance + ? SET current_balance = current_balance + ?
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id) WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
err = db.TransformAndLogDbError("transaction RecalculateBalances", r, err) err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", r, err)
if err != nil { if err != nil {
return err return err
} }
@@ -412,7 +435,7 @@ func (s TransactionImpl) RecalculateBalances(user *types.User) error {
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("transaction RecalculateBalances", nil, err) err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -420,7 +443,7 @@ func (s TransactionImpl) RecalculateBalances(user *types.User) error {
return nil return nil
} }
func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransaction *types.Transaction, userId uuid.UUID, input types.Transaction) (*types.Transaction, error) { func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *sqlx.Tx, oldTransaction *types.Transaction, userId uuid.UUID, input types.Transaction) (*types.Transaction, error) {
var ( var (
id uuid.UUID id uuid.UUID
createdAt time.Time createdAt time.Time
@@ -433,7 +456,7 @@ func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransactio
) )
if oldTransaction == nil { if oldTransaction == nil {
id, err = s.random.UUID() id, err = s.random.UUID(ctx)
if err != nil { if err != nil {
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -449,21 +472,21 @@ func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransactio
} }
if input.AccountId != nil { if input.AccountId != nil {
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, input.AccountId, userId) err = tx.GetContext(ctx, &rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, input.AccountId, userId)
err = db.TransformAndLogDbError("transaction validate", nil, err) err = db.TransformAndLogDbError(ctx, "transaction validate", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rowCount == 0 { if rowCount == 0 {
slog.Error("transaction validate", "err", err) slog.ErrorContext(ctx, "transaction validate", "err", err)
return nil, fmt.Errorf("account not found: %w", ErrBadRequest) return nil, fmt.Errorf("account not found: %w", ErrBadRequest)
} }
} }
if input.TreasureChestId != nil { if input.TreasureChestId != nil {
var treasureChest types.TreasureChest var treasureChest types.TreasureChest
err = tx.Get(&treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, input.TreasureChestId, userId) err = tx.GetContext(ctx, &treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, input.TreasureChestId, userId)
err = db.TransformAndLogDbError("transaction validate", nil, err) err = db.TransformAndLogDbError(ctx, "transaction validate", nil, err)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest) return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest)
@@ -511,27 +534,20 @@ func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransactio
return &transaction, nil return &transaction, nil
} }
func (s TransactionImpl) updateErrors(transaction *types.Transaction) { func (s TransactionImpl) updateErrors(t *types.Transaction) {
errorStr := "" errorStr := ""
switch { switch {
case transaction.Value < 0: case (t.AccountId != nil && t.TreasureChestId != nil && t.Value > 0) ||
if transaction.TreasureChestId == nil { (t.AccountId == nil && t.TreasureChestId == nil):
errorStr = "no treasure chest specified" errorStr = "either an account or a treasure chest needs to be specified"
} case t.Value == 0:
case transaction.Value > 0:
if transaction.AccountId == nil && transaction.TreasureChestId == nil {
errorStr = "either an account or a treasure chest needs to be specified"
} else if transaction.AccountId != nil && transaction.TreasureChestId != nil {
errorStr = "positive amounts can only be applied to either an account or a treasure chest"
}
default:
errorStr = "\"value\" needs to be specified" errorStr = "\"value\" needs to be specified"
} }
if errorStr == "" { if errorStr == "" {
transaction.Error = nil t.Error = nil
} else { } else {
transaction.Error = &errorStr t.Error = &errorStr
} }
} }

View File

@@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -15,14 +16,14 @@ import (
) )
type TransactionRecurring interface { type TransactionRecurring interface {
Add(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error) Add(ctx context.Context, user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error)
Update(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error) Update(ctx context.Context, user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error)
GetAll(user *types.User) ([]*types.TransactionRecurring, error) GetAll(ctx context.Context, user *types.User) ([]*types.TransactionRecurring, error)
GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error) GetAllByAccount(ctx context.Context, user *types.User, accountId string) ([]*types.TransactionRecurring, error)
GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error) GetAllByTreasureChest(ctx context.Context, user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error)
Delete(user *types.User, id string) error Delete(ctx context.Context, user *types.User, id string) error
GenerateTransactions(user *types.User) error GenerateTransactions(ctx context.Context) error
} }
type TransactionRecurringImpl struct { type TransactionRecurringImpl struct {
@@ -41,7 +42,7 @@ func NewTransactionRecurring(db *sqlx.DB, random Random, clock Clock, transactio
} }
} }
func (s TransactionRecurringImpl) Add( func (s TransactionRecurringImpl) Add(ctx context.Context,
user *types.User, user *types.User,
transactionRecurringInput types.TransactionRecurringInput, transactionRecurringInput types.TransactionRecurringInput,
) (*types.TransactionRecurring, error) { ) (*types.TransactionRecurring, error) {
@@ -49,8 +50,8 @@ func (s TransactionRecurringImpl) Add(
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("transactionRecurring Add", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring Add", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -58,24 +59,24 @@ func (s TransactionRecurringImpl) Add(
_ = tx.Rollback() _ = tx.Rollback()
}() }()
transactionRecurring, err := s.validateAndEnrichTransactionRecurring(tx, nil, user.Id, transactionRecurringInput) transactionRecurring, err := s.validateAndEnrichTransactionRecurring(ctx, tx, nil, user.Id, transactionRecurringInput)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r, err := tx.NamedExec(` r, err := tx.NamedExecContext(ctx, `
INSERT INTO "transaction_recurring" (id, user_id, interval_months, INSERT INTO "transaction_recurring" (id, user_id, interval_months,
next_execution, party, description, account_id, treasure_chest_id, value, created_at, created_by) next_execution, party, description, account_id, treasure_chest_id, value, created_at, created_by)
VALUES (:id, :user_id, :interval_months, VALUES (:id, :user_id, :interval_months,
:next_execution, :party, :description, :account_id, :treasure_chest_id, :value, :created_at, :created_by)`, :next_execution, :party, :description, :account_id, :treasure_chest_id, :value, :created_at, :created_by)`,
transactionRecurring) transactionRecurring)
err = db.TransformAndLogDbError("transactionRecurring Insert", r, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring Insert", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("transactionRecurring Add", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring Add", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -83,7 +84,7 @@ func (s TransactionRecurringImpl) Add(
return transactionRecurring, nil return transactionRecurring, nil
} }
func (s TransactionRecurringImpl) Update( func (s TransactionRecurringImpl) Update(ctx context.Context,
user *types.User, user *types.User,
input types.TransactionRecurringInput, input types.TransactionRecurringInput,
) (*types.TransactionRecurring, error) { ) (*types.TransactionRecurring, error) {
@@ -92,12 +93,12 @@ func (s TransactionRecurringImpl) Update(
} }
uuid, err := uuid.Parse(input.Id) uuid, err := uuid.Parse(input.Id)
if err != nil { if err != nil {
slog.Error("transactionRecurring update", "err", err) slog.ErrorContext(ctx, "transactionRecurring update", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("transactionRecurring Update", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring Update", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -106,8 +107,8 @@ func (s TransactionRecurringImpl) Update(
}() }()
transactionRecurring := &types.TransactionRecurring{} transactionRecurring := &types.TransactionRecurring{}
err = tx.Get(transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid) err = tx.GetContext(ctx, transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("transactionRecurring Update", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring Update", nil, err)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("transactionRecurring %v not found: %w", input.Id, ErrBadRequest) return nil, fmt.Errorf("transactionRecurring %v not found: %w", input.Id, ErrBadRequest)
@@ -115,12 +116,12 @@ func (s TransactionRecurringImpl) Update(
return nil, types.ErrInternal return nil, types.ErrInternal
} }
transactionRecurring, err = s.validateAndEnrichTransactionRecurring(tx, transactionRecurring, user.Id, input) transactionRecurring, err = s.validateAndEnrichTransactionRecurring(ctx, tx, transactionRecurring, user.Id, input)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r, err := tx.NamedExec(` r, err := tx.NamedExecContext(ctx, `
UPDATE transaction_recurring UPDATE transaction_recurring
SET SET
interval_months = :interval_months, interval_months = :interval_months,
@@ -134,13 +135,13 @@ func (s TransactionRecurringImpl) Update(
updated_by = :updated_by updated_by = :updated_by
WHERE id = :id WHERE id = :id
AND user_id = :user_id`, transactionRecurring) AND user_id = :user_id`, transactionRecurring)
err = db.TransformAndLogDbError("transactionRecurring Update", r, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring Update", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("transactionRecurring Update", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring Update", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -148,19 +149,19 @@ func (s TransactionRecurringImpl) Update(
return transactionRecurring, nil return transactionRecurring, nil
} }
func (s TransactionRecurringImpl) GetAll(user *types.User) ([]*types.TransactionRecurring, error) { func (s TransactionRecurringImpl) GetAll(ctx context.Context, user *types.User) ([]*types.TransactionRecurring, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
transactionRecurrings := make([]*types.TransactionRecurring, 0) transactionRecurrings := make([]*types.TransactionRecurring, 0)
err := s.db.Select(&transactionRecurrings, ` err := s.db.SelectContext(ctx, &transactionRecurrings, `
SELECT * SELECT *
FROM transaction_recurring FROM transaction_recurring
WHERE user_id = ? WHERE user_id = ?
ORDER BY created_at DESC`, ORDER BY created_at DESC`,
user.Id) user.Id)
err = db.TransformAndLogDbError("transactionRecurring GetAll", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAll", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -168,19 +169,19 @@ func (s TransactionRecurringImpl) GetAll(user *types.User) ([]*types.Transaction
return transactionRecurrings, nil return transactionRecurrings, nil
} }
func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error) { func (s TransactionRecurringImpl) GetAllByAccount(ctx context.Context, user *types.User, accountId string) ([]*types.TransactionRecurring, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
accountUuid, err := uuid.Parse(accountId) accountUuid, err := uuid.Parse(accountId)
if err != nil { if err != nil {
slog.Error("transactionRecurring GetAllByAccount", "err", err) slog.ErrorContext(ctx, "transactionRecurring GetAllByAccount", "err", err)
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByAccount", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -189,8 +190,8 @@ func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId st
}() }()
var rowCount int var rowCount int
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, user.Id) err = tx.GetContext(ctx, &rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, user.Id)
err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByAccount", nil, err)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("account %v not found: %w", accountId, ErrBadRequest) return nil, fmt.Errorf("account %v not found: %w", accountId, ErrBadRequest)
@@ -199,20 +200,20 @@ func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId st
} }
transactionRecurrings := make([]*types.TransactionRecurring, 0) transactionRecurrings := make([]*types.TransactionRecurring, 0)
err = tx.Select(&transactionRecurrings, ` err = tx.SelectContext(ctx, &transactionRecurrings, `
SELECT * SELECT *
FROM transaction_recurring FROM transaction_recurring
WHERE user_id = ? WHERE user_id = ?
AND account_id = ? AND account_id = ?
ORDER BY created_at DESC`, ORDER BY created_at DESC`,
user.Id, accountUuid) user.Id, accountUuid)
err = db.TransformAndLogDbError("transactionRecurring GetAll", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAll", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByAccount", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -220,7 +221,7 @@ func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId st
return transactionRecurrings, nil return transactionRecurrings, nil
} }
func (s TransactionRecurringImpl) GetAllByTreasureChest( func (s TransactionRecurringImpl) GetAllByTreasureChest(ctx context.Context,
user *types.User, user *types.User,
treasureChestId string, treasureChestId string,
) ([]*types.TransactionRecurring, error) { ) ([]*types.TransactionRecurring, error) {
@@ -230,12 +231,12 @@ func (s TransactionRecurringImpl) GetAllByTreasureChest(
treasureChestUuid, err := uuid.Parse(treasureChestId) treasureChestUuid, err := uuid.Parse(treasureChestId)
if err != nil { if err != nil {
slog.Error("transactionRecurring GetAllByTreasureChest", "err", err) slog.ErrorContext(ctx, "transactionRecurring GetAllByTreasureChest", "err", err)
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByTreasureChest", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -244,8 +245,8 @@ func (s TransactionRecurringImpl) GetAllByTreasureChest(
}() }()
var rowCount int var rowCount int
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestId, user.Id) err = tx.GetContext(ctx, &rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestId, user.Id)
err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByTreasureChest", nil, err)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("treasurechest %v not found: %w", treasureChestId, ErrBadRequest) return nil, fmt.Errorf("treasurechest %v not found: %w", treasureChestId, ErrBadRequest)
@@ -254,20 +255,20 @@ func (s TransactionRecurringImpl) GetAllByTreasureChest(
} }
transactionRecurrings := make([]*types.TransactionRecurring, 0) transactionRecurrings := make([]*types.TransactionRecurring, 0)
err = tx.Select(&transactionRecurrings, ` err = tx.SelectContext(ctx, &transactionRecurrings, `
SELECT * SELECT *
FROM transaction_recurring FROM transaction_recurring
WHERE user_id = ? WHERE user_id = ?
AND treasure_chest_id = ? AND treasure_chest_id = ?
ORDER BY created_at DESC`, ORDER BY created_at DESC`,
user.Id, treasureChestUuid) user.Id, treasureChestUuid)
err = db.TransformAndLogDbError("transactionRecurring GetAll", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAll", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByTreasureChest", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -275,18 +276,18 @@ func (s TransactionRecurringImpl) GetAllByTreasureChest(
return transactionRecurrings, nil return transactionRecurrings, nil
} }
func (s TransactionRecurringImpl) Delete(user *types.User, id string) error { func (s TransactionRecurringImpl) Delete(ctx context.Context, user *types.User, id string) error {
if user == nil { if user == nil {
return ErrUnauthorized return ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
slog.Error("transactionRecurring delete", "err", err) slog.ErrorContext(ctx, "transactionRecurring delete", "err", err)
return fmt.Errorf("could not parse Id: %w", ErrBadRequest) return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring Delete", nil, err)
if err != nil { if err != nil {
return nil return nil
} }
@@ -295,20 +296,20 @@ func (s TransactionRecurringImpl) Delete(user *types.User, id string) error {
}() }()
var transactionRecurring types.TransactionRecurring var transactionRecurring types.TransactionRecurring
err = tx.Get(&transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid) err = tx.GetContext(ctx, &transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring Delete", nil, err)
if err != nil { if err != nil {
return err return err
} }
r, err := tx.Exec("DELETE FROM transaction_recurring WHERE id = ? AND user_id = ?", uuid, user.Id) r, err := tx.ExecContext(ctx, "DELETE FROM transaction_recurring WHERE id = ? AND user_id = ?", uuid, user.Id)
err = db.TransformAndLogDbError("transactionRecurring Delete", r, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring Delete", r, err)
if err != nil { if err != nil {
return err return err
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring Delete", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -316,14 +317,11 @@ func (s TransactionRecurringImpl) Delete(user *types.User, id string) error {
return nil return nil
} }
func (s TransactionRecurringImpl) GenerateTransactions(user *types.User) error { func (s TransactionRecurringImpl) GenerateTransactions(ctx context.Context) error {
if user == nil {
return ErrUnauthorized
}
now := s.clock.Now() now := s.clock.Now()
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -332,15 +330,18 @@ func (s TransactionRecurringImpl) GenerateTransactions(user *types.User) error {
}() }()
recurringTransactions := make([]*types.TransactionRecurring, 0) recurringTransactions := make([]*types.TransactionRecurring, 0)
err = tx.Select(&recurringTransactions, ` err = tx.SelectContext(ctx, &recurringTransactions, `
SELECT * FROM transaction_recurring WHERE user_id = ? AND next_execution <= ?`, SELECT * FROM transaction_recurring WHERE next_execution <= ?`,
user.Id, now) now)
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
if err != nil { if err != nil {
return err return err
} }
for _, transactionRecurring := range recurringTransactions { for _, transactionRecurring := range recurringTransactions {
user := &types.User{
Id: transactionRecurring.UserId,
}
transaction := types.Transaction{ transaction := types.Transaction{
Timestamp: *transactionRecurring.NextExecution, Timestamp: *transactionRecurring.NextExecution,
Party: transactionRecurring.Party, Party: transactionRecurring.Party,
@@ -350,22 +351,22 @@ func (s TransactionRecurringImpl) GenerateTransactions(user *types.User) error {
Value: transactionRecurring.Value, Value: transactionRecurring.Value,
} }
_, err = s.transaction.Add(tx, user, transaction) _, err = s.transaction.Add(ctx, tx, user, transaction)
if err != nil { if err != nil {
return err return err
} }
nextExecution := transactionRecurring.NextExecution.AddDate(0, int(transactionRecurring.IntervalMonths), 0) nextExecution := transactionRecurring.NextExecution.AddDate(0, int(transactionRecurring.IntervalMonths), 0)
r, err := tx.Exec(`UPDATE transaction_recurring SET next_execution = ? WHERE id = ? AND user_id = ?`, r, err := tx.ExecContext(ctx, `UPDATE transaction_recurring SET next_execution = ? WHERE id = ? AND user_id = ?`,
nextExecution, transactionRecurring.Id, user.Id) nextExecution, transactionRecurring.Id, user.Id)
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", r, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", r, err)
if err != nil { if err != nil {
return err return err
} }
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -373,6 +374,7 @@ func (s TransactionRecurringImpl) GenerateTransactions(user *types.User) error {
} }
func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring( func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
ctx context.Context,
tx *sqlx.Tx, tx *sqlx.Tx,
oldTransactionRecurring *types.TransactionRecurring, oldTransactionRecurring *types.TransactionRecurring,
userId uuid.UUID, userId uuid.UUID,
@@ -393,7 +395,7 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
) )
if oldTransactionRecurring == nil { if oldTransactionRecurring == nil {
id, err = s.random.UUID() id, err = s.random.UUID(ctx)
if err != nil { if err != nil {
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -413,17 +415,17 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
if input.AccountId != "" { if input.AccountId != "" {
temp, err := uuid.Parse(input.AccountId) temp, err := uuid.Parse(input.AccountId)
if err != nil { if err != nil {
slog.Error("transactionRecurring validate", "err", err) slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
} }
accountUuid = &temp accountUuid = &temp
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, userId) err = tx.GetContext(ctx, &rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, userId)
err = db.TransformAndLogDbError("transactionRecurring validate", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring validate", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rowCount == 0 { if rowCount == 0 {
slog.Error("transactionRecurring validate", "err", err) slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
return nil, fmt.Errorf("account not found: %w", ErrBadRequest) return nil, fmt.Errorf("account not found: %w", ErrBadRequest)
} }
@@ -433,13 +435,13 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
if input.TreasureChestId != "" { if input.TreasureChestId != "" {
temp, err := uuid.Parse(input.TreasureChestId) temp, err := uuid.Parse(input.TreasureChestId)
if err != nil { if err != nil {
slog.Error("transactionRecurring validate", "err", err) slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
} }
treasureChestUuid = &temp treasureChestUuid = &temp
var treasureChest types.TreasureChest var treasureChest types.TreasureChest
err = tx.Get(&treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestUuid, userId) err = tx.GetContext(ctx, &treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestUuid, userId)
err = db.TransformAndLogDbError("transactionRecurring validate", nil, err) err = db.TransformAndLogDbError(ctx, "transactionRecurring validate", nil, err)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest) return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest)
@@ -453,17 +455,17 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
} }
if !hasAccount && !hasTreasureChest { if !hasAccount && !hasTreasureChest {
slog.Error("transactionRecurring validate", "err", err) slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
return nil, fmt.Errorf("either account or treasure chest is required: %w", ErrBadRequest) return nil, fmt.Errorf("either account or treasure chest is required: %w", ErrBadRequest)
} }
if hasAccount && hasTreasureChest { if hasAccount && hasTreasureChest {
slog.Error("transactionRecurring validate", "err", err) slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
return nil, fmt.Errorf("either account or treasure chest is required, not both: %w", ErrBadRequest) return nil, fmt.Errorf("either account or treasure chest is required, not both: %w", ErrBadRequest)
} }
valueFloat, err := strconv.ParseFloat(input.Value, 64) valueFloat, err := strconv.ParseFloat(input.Value, 64)
if err != nil { if err != nil {
slog.Error("transactionRecurring validate", "err", err) slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
} }
value := int64(math.Round(valueFloat * DECIMALS_MULTIPLIER)) value := int64(math.Round(valueFloat * DECIMALS_MULTIPLIER))
@@ -482,18 +484,18 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
} }
intervalMonths, err = strconv.ParseInt(input.IntervalMonths, 10, 0) intervalMonths, err = strconv.ParseInt(input.IntervalMonths, 10, 0)
if err != nil { if err != nil {
slog.Error("transactionRecurring validate", "err", err) slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
return nil, fmt.Errorf("could not parse intervalMonths: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse intervalMonths: %w", ErrBadRequest)
} }
if intervalMonths < 1 { if intervalMonths < 1 {
slog.Error("transactionRecurring validate", "err", err) slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
return nil, fmt.Errorf("intervalMonths needs to be greater than 0: %w", ErrBadRequest) return nil, fmt.Errorf("intervalMonths needs to be greater than 0: %w", ErrBadRequest)
} }
var nextExecution *time.Time = nil var nextExecution *time.Time = nil
if input.NextExecution != "" { if input.NextExecution != "" {
t, err := time.Parse("2006-01-02", input.NextExecution) t, err := time.Parse("2006-01-02", input.NextExecution)
if err != nil { if err != nil {
slog.Error("transaction validate", "err", err) slog.ErrorContext(ctx, "transaction validate", "err", err)
return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
} }

View File

@@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -13,11 +14,11 @@ import (
) )
type TreasureChest interface { type TreasureChest interface {
Add(user *types.User, parentId, name string) (*types.TreasureChest, error) Add(ctx context.Context, user *types.User, parentId, name string) (*types.TreasureChest, error)
Update(user *types.User, id, parentId, name string) (*types.TreasureChest, error) Update(ctx context.Context, user *types.User, id, parentId, name string) (*types.TreasureChest, error)
Get(user *types.User, id string) (*types.TreasureChest, error) Get(ctx context.Context, user *types.User, id string) (*types.TreasureChest, error)
GetAll(user *types.User) ([]*types.TreasureChest, error) GetAll(ctx context.Context, user *types.User) ([]*types.TreasureChest, error)
Delete(user *types.User, id string) error Delete(ctx context.Context, user *types.User, id string) error
} }
type TreasureChestImpl struct { type TreasureChestImpl struct {
@@ -34,12 +35,12 @@ func NewTreasureChest(db *sqlx.DB, random Random, clock Clock) TreasureChest {
} }
} }
func (s TreasureChestImpl) Add(user *types.User, parentId, name string) (*types.TreasureChest, error) { func (s TreasureChestImpl) Add(ctx context.Context, user *types.User, parentId, name string) (*types.TreasureChest, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
newId, err := s.random.UUID() newId, err := s.random.UUID(ctx)
if err != nil { if err != nil {
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -51,7 +52,7 @@ func (s TreasureChestImpl) Add(user *types.User, parentId, name string) (*types.
var parentUuid *uuid.UUID var parentUuid *uuid.UUID
if parentId != "" { if parentId != "" {
parent, err := s.Get(user, parentId) parent, err := s.Get(ctx, user, parentId)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -76,10 +77,10 @@ func (s TreasureChestImpl) Add(user *types.User, parentId, name string) (*types.
UpdatedBy: nil, UpdatedBy: nil,
} }
r, err := s.db.NamedExec(` r, err := s.db.NamedExecContext(ctx, `
INSERT INTO treasure_chest (id, parent_id, user_id, name, current_balance, created_at, created_by) INSERT INTO treasure_chest (id, parent_id, user_id, name, current_balance, created_at, created_by)
VALUES (:id, :parent_id, :user_id, :name, :current_balance, :created_at, :created_by)`, treasureChest) VALUES (:id, :parent_id, :user_id, :name, :current_balance, :created_at, :created_by)`, treasureChest)
err = db.TransformAndLogDbError("treasureChest Insert", r, err) err = db.TransformAndLogDbError(ctx, "treasureChest Insert", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -87,7 +88,7 @@ func (s TreasureChestImpl) Add(user *types.User, parentId, name string) (*types.
return treasureChest, nil return treasureChest, nil
} }
func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string) (*types.TreasureChest, error) { func (s TreasureChestImpl) Update(ctx context.Context, user *types.User, idStr, parentId, name string) (*types.TreasureChest, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -97,12 +98,12 @@ func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string
} }
id, err := uuid.Parse(idStr) id, err := uuid.Parse(idStr)
if err != nil { if err != nil {
slog.Error("treasureChest update", "err", err) slog.ErrorContext(ctx, "treasureChest update", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("treasureChest Update", nil, err) err = db.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -111,8 +112,8 @@ func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string
}() }()
treasureChest := &types.TreasureChest{} treasureChest := &types.TreasureChest{}
err = tx.Get(treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, id) err = tx.GetContext(ctx, treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, id)
err = db.TransformAndLogDbError("treasureChest Update", nil, err) err = db.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("treasureChest %v not found: %w", idStr, err) return nil, fmt.Errorf("treasureChest %v not found: %w", idStr, err)
@@ -122,13 +123,13 @@ func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string
var parentUuid *uuid.UUID var parentUuid *uuid.UUID
if parentId != "" { if parentId != "" {
parent, err := s.Get(user, parentId) parent, err := s.Get(ctx, user, parentId)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var childCount int var childCount int
err = tx.Get(&childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id) err = tx.GetContext(ctx, &childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id)
err = db.TransformAndLogDbError("treasureChest Update", nil, err) err = db.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -145,7 +146,7 @@ func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string
treasureChest.UpdatedAt = &timestamp treasureChest.UpdatedAt = &timestamp
treasureChest.UpdatedBy = &user.Id treasureChest.UpdatedBy = &user.Id
r, err := tx.NamedExec(` r, err := tx.NamedExecContext(ctx, `
UPDATE treasure_chest UPDATE treasure_chest
SET SET
parent_id = :parent_id, parent_id = :parent_id,
@@ -155,13 +156,13 @@ func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string
updated_by = :updated_by updated_by = :updated_by
WHERE id = :id WHERE id = :id
AND user_id = :user_id`, treasureChest) AND user_id = :user_id`, treasureChest)
err = db.TransformAndLogDbError("treasureChest Update", r, err) err = db.TransformAndLogDbError(ctx, "treasureChest Update", r, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("treasureChest Update", nil, err) err = db.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -169,19 +170,19 @@ func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string
return treasureChest, nil return treasureChest, nil
} }
func (s TreasureChestImpl) Get(user *types.User, id string) (*types.TreasureChest, error) { func (s TreasureChestImpl) Get(ctx context.Context, user *types.User, id string) (*types.TreasureChest, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
slog.Error("treasureChest get", "err", err) slog.ErrorContext(ctx, "treasureChest get", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
var treasureChest types.TreasureChest var treasureChest types.TreasureChest
err = s.db.Get(&treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, uuid) err = s.db.GetContext(ctx, &treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("treasureChest Get", nil, err) err = db.TransformAndLogDbError(ctx, "treasureChest Get", nil, err)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound) { if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("treasureChest %v not found: %w", id, err) return nil, fmt.Errorf("treasureChest %v not found: %w", id, err)
@@ -192,33 +193,33 @@ func (s TreasureChestImpl) Get(user *types.User, id string) (*types.TreasureChes
return &treasureChest, nil return &treasureChest, nil
} }
func (s TreasureChestImpl) GetAll(user *types.User) ([]*types.TreasureChest, error) { func (s TreasureChestImpl) GetAll(ctx context.Context, user *types.User) ([]*types.TreasureChest, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
treasureChests := make([]*types.TreasureChest, 0) treasureChests := make([]*types.TreasureChest, 0)
err := s.db.Select(&treasureChests, `SELECT * FROM treasure_chest WHERE user_id = ?`, user.Id) err := s.db.SelectContext(ctx, &treasureChests, `SELECT * FROM treasure_chest WHERE user_id = ?`, user.Id)
err = db.TransformAndLogDbError("treasureChest GetAll", nil, err) err = db.TransformAndLogDbError(ctx, "treasureChest GetAll", nil, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return sortTree(treasureChests), nil return sortTreasureChests(treasureChests), nil
} }
func (s TreasureChestImpl) Delete(user *types.User, idStr string) error { func (s TreasureChestImpl) Delete(ctx context.Context, user *types.User, idStr string) error {
if user == nil { if user == nil {
return ErrUnauthorized return ErrUnauthorized
} }
id, err := uuid.Parse(idStr) id, err := uuid.Parse(idStr)
if err != nil { if err != nil {
slog.Error("treasureChest delete", "err", err) slog.ErrorContext(ctx, "treasureChest delete", "err", err)
return fmt.Errorf("could not parse Id: %w", ErrBadRequest) return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
tx, err := s.db.Beginx() tx, err := s.db.BeginTxx(ctx, nil)
err = db.TransformAndLogDbError("treasureChest Delete", nil, err) err = db.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
if err != nil { if err != nil {
return nil return nil
} }
@@ -227,8 +228,8 @@ func (s TreasureChestImpl) Delete(user *types.User, idStr string) error {
}() }()
childCount := 0 childCount := 0
err = tx.Get(&childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id) err = tx.GetContext(ctx, &childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id)
err = db.TransformAndLogDbError("treasureChest Delete", nil, err) err = db.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -238,10 +239,10 @@ func (s TreasureChestImpl) Delete(user *types.User, idStr string) error {
} }
transactionsCount := 0 transactionsCount := 0
err = tx.Get(&transactionsCount, err = tx.GetContext(ctx, &transactionsCount,
`SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND treasure_chest_id = ?`, `SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND treasure_chest_id = ?`,
user.Id, id) user.Id, id)
err = db.TransformAndLogDbError("treasureChest Delete", nil, err) err = db.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -250,10 +251,10 @@ func (s TreasureChestImpl) Delete(user *types.User, idStr string) error {
} }
recurringCount := 0 recurringCount := 0
err = tx.Get(&recurringCount, ` err = tx.GetContext(ctx, &recurringCount, `
SELECT COUNT(*) FROM transaction_recurring WHERE user_id = ? AND treasure_chest_id = ?`, SELECT COUNT(*) FROM transaction_recurring WHERE user_id = ? AND treasure_chest_id = ?`,
user.Id, id) user.Id, id)
err = db.TransformAndLogDbError("treasureChest Delete", nil, err) err = db.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -261,14 +262,14 @@ func (s TreasureChestImpl) Delete(user *types.User, idStr string) error {
return fmt.Errorf("cannot delete treasure chest with existing recurring transactions: %w", ErrBadRequest) return fmt.Errorf("cannot delete treasure chest with existing recurring transactions: %w", ErrBadRequest)
} }
r, err := tx.Exec(`DELETE FROM treasure_chest WHERE id = ? AND user_id = ?`, id, user.Id) r, err := tx.ExecContext(ctx, `DELETE FROM treasure_chest WHERE id = ? AND user_id = ?`, id, user.Id)
err = db.TransformAndLogDbError("treasureChest Delete", r, err) err = db.TransformAndLogDbError(ctx, "treasureChest Delete", r, err)
if err != nil { if err != nil {
return err return err
} }
err = tx.Commit() err = tx.Commit()
err = db.TransformAndLogDbError("treasureChest Delete", nil, err) err = db.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
if err != nil { if err != nil {
return err return err
} }
@@ -276,7 +277,7 @@ func (s TreasureChestImpl) Delete(user *types.User, idStr string) error {
return nil return nil
} }
func sortTree(nodes []*types.TreasureChest) []*types.TreasureChest { func sortTreasureChests(nodes []*types.TreasureChest) []*types.TreasureChest {
var ( var (
roots []*types.TreasureChest roots []*types.TreasureChest
) )

View File

@@ -1,6 +1,5 @@
package account package account
import "fmt"
import "spend-sparrow/internal/template/svg" import "spend-sparrow/internal/template/svg"
import "spend-sparrow/internal/types" import "spend-sparrow/internal/types"
@@ -67,7 +66,9 @@ templ EditAccount(account *types.Account) {
hx-swap="outerHTML" hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2" class="button button-neglect px-1 flex items-center gap-2"
> >
@svg.Cancel() <span class="h-4 w-4">
@svg.Cancel()
</span>
<span> <span>
Cancel Cancel
</span> </span>
@@ -81,9 +82,9 @@ templ AccountItem(account *types.Account) {
<div class="text-xl flex justify-end gap-4"> <div class="text-xl flex justify-end gap-4">
<p class="mr-auto">{ account.Name }</p> <p class="mr-auto">{ account.Name }</p>
if account.CurrentBalance < 0 { if account.CurrentBalance < 0 {
<p class="mr-20 text-red-700">{ displayBalance(account.CurrentBalance) }</p> <p class="mr-20 text-red-700">{ types.FormatEuros(account.CurrentBalance) }</p>
} else { } else {
<p class="mr-20 text-green-700">{ displayBalance(account.CurrentBalance) }</p> <p class="mr-20 text-green-700">{ types.FormatEuros(account.CurrentBalance) }</p>
} }
<a <a
href={ templ.URL("/transaction?account-id=" + account.Id.String()) } href={ templ.URL("/transaction?account-id=" + account.Id.String()) }
@@ -121,9 +122,3 @@ templ AccountItem(account *types.Account) {
</div> </div>
</div> </div>
} }
func displayBalance(balance int64) string {
euros := float64(balance) / 100
return fmt.Sprintf("%.2f €", euros)
}

View File

@@ -71,10 +71,10 @@ if isSignIn {
Don't have an account? Don't have an account?
Sign Up Sign Up
</a> </a>
<button class="button button-primary font-pirata text-gray-600 text-2xl px-1">Sign In</button> <button class="button button-primary text-gray-600 text-2xl px-1">Sign In</button>
} else { } else {
<a href="/auth/signin" class="text-gray-500 text-sm px-1 button button-neglect">Already have an account? Sign In</a> <a href="/auth/signin" class="text-gray-500 text-sm px-1 button button-neglect">Already have an account? Sign In</a>
<button class="button button-primary font-pirata text-gray-600 text-2xl px-1"> <button class="button button-primary text-gray-600 text-2xl px-1">
Sign Up Sign Up
</button> </button>
} }

View File

@@ -1,7 +1,7 @@
package auth package auth
templ UserComp(user string) { templ UserComp(user string) {
<div id="user-info" class="flex gap-5 items-center"> <div id="user-info" class="flex items-center gap-2 text-nowrap">
if user != "" { if user != "" {
<div class="inline-block group relative"> <div class="inline-block group relative">
<button class="font-semibold py-2 px-4 inline-flex items-center"> <button class="font-semibold py-2 px-4 inline-flex items-center">
@@ -37,8 +37,8 @@ templ UserComp(user string) {
</div> </div>
</div> </div>
} else { } else {
<a href="/auth/signup" class="font-pirata text-xl button px-1 button-neglect">Sign Up</a> <a href="/auth/signup" class="text-xl button px-1 button-neglect">Sign Up</a>
<a href="/auth/signin" class="font-pirata text-xl button px-1 button-neglect">Sign In</a> <a href="/auth/signin" class="text-xl button px-1 button-neglect">Sign In</a>
} }
</div> </div>
} }

View File

@@ -1,9 +0,0 @@
package template
templ Dashboard() {
<div>
<h1 class="text-8xl">
Dashboard
</h1>
</div>
}

View File

@@ -0,0 +1,32 @@
package dashboard
import "spend-sparrow/internal/types"
templ Dashboard(treasureChests []*types.TreasureChest) {
<div class="mt-10 h-full">
<div id="main-chart" class="h-96 mt-10"></div>
<div id="treasure-chests" class="h-96 mt-10"></div>
<section>
<form class="flex items-center justify-end gap-4 mr-40">
<label for="treasure-chest">Treasure Chest:</label>
<select id="treasure-chest-id" name="treasure-chest-id" class="bg-white input">
<option value="">- Select Treasure Chest -</option>
for _, parent := range treasureChests {
if parent.ParentId == nil {
<optgroup label={ parent.Name }>
for _, child := range treasureChests {
if child.ParentId != nil && *child.ParentId == parent.Id {
<option
value={ child.Id.String() }
>{ child.Name }</option>
}
}
</optgroup>
}
}
</select>
</form>
<div id="treasure-chest" class="h-96 mt-10"></div>
</section>
</div>
}

View File

@@ -0,0 +1,2 @@
package dashboard

View File

@@ -1,11 +1,9 @@
package template package template
templ Index() { templ Index() {
<!-- <div class="h-full flex flex-col items-center justify-center"> -->
<div class="h-full flex flex-col items-center justify-center"> <div class="h-full flex flex-col items-center justify-center">
<h1 class="flex gap-2 w-full justify-center"> <h1 class="flex gap-2 w-full justify-center">
<img class="w-24" src="/static/favicon.svg" alt="SpendSparrow logo"/> <img width="600" src="/static/logo.svg" alt="SpendSparrow logo"/>
<span class="text-8xl tracking-tighter font-bold font-pirata">SpendSparrow</span>
</h1> </h1>
<h2 class="text-2xl mt-8 text-gray-800"> <h2 class="text-2xl mt-8 text-gray-800">
Spend your <span class="px-2 text-3xl text-yellow-800">treasure</span> on the important Spend your <span class="px-2 text-3xl text-yellow-800">treasure</span> on the important

View File

@@ -1,10 +1,14 @@
package template package template
import "spend-sparrow/internal/template/svg"
func layoutLinkClass(isActive bool) string { func layoutLinkClass(isActive bool) string {
common := "text-2xl p-2 text-gray-900 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg"
if isActive { if isActive {
return "text-xl hover:bg-gray-100 p-1 duration-100 rounded-xl transition-colors text-gray-900" return common + " " + "underline"
} }
return "text-xl hover:bg-gray-100 hover:text-gray-900 p-1 duration-200 rounded-xl transition-colors text-gray-400"
return common + " " + "hover:underline"
} }
templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path string) { templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path string) {
@@ -22,46 +26,70 @@ templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path str
"includeIndicatorStyles": false, "includeIndicatorStyles": false,
"selfRequestsOnly": true, "selfRequestsOnly": true,
"allowScriptTags": false "allowScriptTags": false
}' }'
/> />
<script src="/static/js/htmx.min.js"></script> <script src="/static/js/htmx.min.js"></script>
<script src="/static/js/toast.js"></script> <script src="/static/js/toast.js"></script>
<script src="/static/js/layout.js"></script>
<script src="/static/js/transaction.js"></script>
<script src="/static/js/time.js"></script> <script src="/static/js/time.js"></script>
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/dashboard.js" defer></script>
</head> </head>
<body class="h-screen flex flex-col" hx-headers='{"Csrf-Token": "CSRF_TOKEN"}'> <body hx-headers='{"Csrf-Token": "CSRF_TOKEN"}'>
// Header <div class="flex flex-col min-h-screen">
<nav class="flex bg-white items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2"> <header class="sticky top-0 z-50 bg-white flex items-center gap-6 p-4 border-b-1 border-gray-200">
<a href="/" class="flex gap-2 mr-20"> <button id="menuButton" class="w-10 h-10 block xl:hidden">
<img class="w-6" src="/static/favicon.svg" alt="SpendSparrow logo"/> @svg.Menu()
<span class="text-4xl font-bold font-pirata">SpendSparrow</span> </button>
</a> <a href="/" class="flex gap-2 -mt-2">
if loggedIn { <img width="150" src="/static/logo.svg" alt="SpendSparrow logo"/>
<a class={ layoutLinkClass(path == "/") } href="/">Dashboard</a> </a>
<a class={ layoutLinkClass(path == "/transaction") } href="/transaction">Transaction</a> <div class="ml-auto">
<a class={ layoutLinkClass(path == "/treasurechest") } href="/treasurechest">Treasure Chest</a> @user
<a class={ layoutLinkClass(path == "/account") } href="/account">Account</a> </div>
} </header>
<div class="ml-auto"> // Content
@user <div class="flex flex-1">
if loggedIn {
<aside class="shrink-0 h-[calc(100vh-4rem)] xl:block hidden sticky top-18 border-r-1 border-gray-200 overflow-y-auto p-4">
@navigation(path)
</aside>
}
<main class="flex-1 p-6">
if slot != nil {
@slot
}
</main>
</div> </div>
</nav> </div>
<div class="h-12 fixed top-12 mr-4 inset-0 bg-linear-0 from-transparent to-white"></div> <dialog id="menu" class="max-h-none w-64 h-screen">
// Content <header class="sticky top-0 z-50 bg-white flex items-center justify-between p-4 border-b-1 border-gray-200">
<main class="flex-1 overflow-auto"> <a href="/" class="flex gap-2 -mt-2">
if slot != nil { <img width="150" src="/static/logo.svg" alt="SpendSparrow logo"/>
@slot </a>
} <button id="menuButtonClose" class="h-6 w-6">
</main> @svg.Cancel()
// Footer </button>
<!-- </div> --> </header>
@navigation(path)
</dialog>
<div id="toasts" class="fixed bottom-4 right-4 ml-4 max-w-96 flex flex-col gap-2 z-50"> <div id="toasts" class="fixed bottom-4 right-4 ml-4 max-w-96 flex flex-col gap-2 z-50">
<div <div
id="toast" id="toast"
class="transition-all duration-300 opacity-0 px-4 py-2 text-lg hidden text-bold rounded bg-amber-900 text-white" class="transition-all duration-300
> opacity-0 px-4 py-2 text-lg hidden text-bold rounded bg-amber-900 text-white"
M ></div>
</div>
</div> </div>
</body> </body>
</html> </html>
} }
templ navigation(path string) {
<nav class="w-64 text-nowrap flex gap-2 flex-col text-lg mt-5 px-5 pt-2">
<a class={ layoutLinkClass(path == "/dashboard") } href="/dashboard">Dashboard</a>
<a class={ layoutLinkClass(path == "/transaction") } href="/transaction">Transaction</a>
<a class={ layoutLinkClass(path == "/treasurechest") } href="/treasurechest">Treasure Chest</a>
<a class={ layoutLinkClass(path == "/account") } href="/account">Account</a>
</nav>
}

View File

@@ -31,7 +31,7 @@ templ Save() {
} }
templ Cancel() { templ Cancel() {
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" class="h-4 w-4 text-gray-500"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" class="text-gray-500">
<path fill="currentColor" d="m654 501l346 346l-154 154l-346-346l-346 346L0 847l346-346L0 155L154 1l346 346L846 1l154 154z"></path> <path fill="currentColor" d="m654 501l346 346l-154 154l-346-346l-346 346L0 847l346-346L0 155L154 1l346 346L846 1l154 154z"></path>
</svg> </svg>
} }
@@ -47,3 +47,13 @@ templ Info() {
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipSInfo0)"></path> <path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipSInfo0)"></path>
</svg> </svg>
} }
templ Menu() {
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="text-gray-500">
<g data-name="1" id="_1">
<path d="M441.13,166.52h-372a15,15,0,1,1,0-30h372a15,15,0,0,1,0,30Z"></path>
<path d="M441.13,279.72h-372a15,15,0,1,1,0-30h372a15,15,0,0,1,0,30Z"></path>
<path d="M441.13,392.92h-372a15,15,0,1,1,0-30h372a15,15,0,0,1,0,30Z"></path>
</g>
</svg>
}

View File

@@ -10,6 +10,7 @@ templ Transaction(items templ.Component, filter types.TransactionItemsFilter, ac
<div class="max-w-6xl mt-10 mx-auto"> <div class="max-w-6xl mt-10 mx-auto">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<form <form
id="transactionFilterForm"
hx-get="/transaction" hx-get="/transaction"
hx-target="#transaction-items" hx-target="#transaction-items"
hx-push-url="true" hx-push-url="true"
@@ -52,6 +53,7 @@ templ Transaction(items templ.Component, filter types.TransactionItemsFilter, ac
selected?={ filter.Error == "false" } selected?={ filter.Error == "false" }
>Has no Errors</option> >Has no Errors</option>
</select> </select>
<input id="page" name="page" type="hidden" value={ filter.Page }/>
</form> </form>
<button <button
hx-get="/transaction/new" hx-get="/transaction/new"
@@ -63,7 +65,25 @@ templ Transaction(items templ.Component, filter types.TransactionItemsFilter, ac
<p>New Transaction</p> <p>New Transaction</p>
</button> </button>
</div> </div>
<div class="flex justify-end items-center gap-5 mt-5">
<button id="pagePrev1" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
&lt;
</button>
<span class="text-gray-400 text-sm">Page: <span class="text-gray-800 text-xl" id="page1">{ getPageNumber(filter.Page) }</span></span>
<button id="pageNext1" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
&gt;
</button>
</div>
@items @items
<div class="flex justify-end items-center gap-5 mt-5">
<button id="pagePrev2" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
&lt;
</button>
<span class="text-gray-400 text-sm">Page: <span class="text-gray-800 text-xl" id="page2">{ getPageNumber(filter.Page) }</span></span>
<button id="pageNext2" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
&gt;
</button>
</div>
</div> </div>
} }
@@ -103,7 +123,7 @@ templ EditTransaction(transaction *types.Transaction, accounts []*types.Account,
if transaction.TreasureChestId != nil { if transaction.TreasureChestId != nil {
treasureChestId = transaction.TreasureChestId.String() treasureChestId = transaction.TreasureChestId.String()
} }
value = displayBalance(transaction.Value) value = formatFloat(transaction.Value)
id = transaction.Id.String() id = transaction.Id.String()
cancelUrl = "/transaction/" + id cancelUrl = "/transaction/" + id
@@ -188,7 +208,9 @@ templ EditTransaction(transaction *types.Transaction, accounts []*types.Account,
hx-swap="outerHTML" hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2" class="button button-neglect px-1 flex items-center gap-2"
> >
@svg.Cancel() <span class="h-4 w-4">
@svg.Cancel()
</span>
<span> <span>
Cancel Cancel
</span> </span>
@@ -250,9 +272,9 @@ templ TransactionItem(transaction *types.Transaction, accounts, treasureChests m
</p> </p>
</div> </div>
if transaction.Value < 0 { if transaction.Value < 0 {
<p class="mr-8 min-w-22 text-right text-red-700">{ displayBalance(transaction.Value)+" €" }</p> <p class="mr-8 min-w-22 text-right text-red-700">{ types.FormatEuros(transaction.Value) }</p>
} else { } else {
<p class="mr-8 w-22 text-right text-green-700">{ displayBalance(transaction.Value)+" €" }</p> <p class="mr-8 w-22 text-right text-green-700">{ types.FormatEuros(transaction.Value) }</p>
} }
<button <button
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" } hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }
@@ -280,11 +302,16 @@ templ TransactionItem(transaction *types.Transaction, accounts, treasureChests m
</div> </div>
} }
func displayBalance(balance int64) string { func formatFloat(balance int64) string {
euros := float64(balance) / 100 euros := float64(balance) / 100
return fmt.Sprintf("%.2f", euros) return fmt.Sprintf("%.2f", euros)
} }
func calculateReferences() { func getPageNumber(page string) string {
if page == "" {
return "1"
} else {
return page
}
} }

View File

@@ -53,9 +53,9 @@ templ TransactionRecurringItem(transactionRecurring *types.TransactionRecurring,
Every <span class="text-xl">{ transactionRecurring.IntervalMonths }</span> month(s) Every <span class="text-xl">{ transactionRecurring.IntervalMonths }</span> month(s)
</p> </p>
if transactionRecurring.Value < 0 { if transactionRecurring.Value < 0 {
<p class="text-right text-red-700">{ displayBalance(transactionRecurring.Value)+" €" }</p> <p class="text-right text-red-700">{ types.FormatEuros(transactionRecurring.Value) }</p>
} else { } else {
<p class="text-right text-green-700">{ displayBalance(transactionRecurring.Value)+" €" }</p> <p class="text-right text-green-700">{ types.FormatEuros(transactionRecurring.Value) }</p>
} }
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@@ -104,7 +104,7 @@ templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring,
} }
party = transactionRecurring.Party party = transactionRecurring.Party
description = transactionRecurring.Description description = transactionRecurring.Description
value = displayBalance(transactionRecurring.Value) value = formatFloat(transactionRecurring.Value)
id = transactionRecurring.Id.String() id = transactionRecurring.Id.String()
} }
@@ -193,7 +193,9 @@ templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring,
hx-swap="outerHTML" hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2" class="button button-neglect px-1 flex items-center gap-2"
> >
@svg.Cancel() <span class="h-4 w-4">
@svg.Cancel()
</span>
<span> <span>
Cancel Cancel
</span> </span>
@@ -201,11 +203,8 @@ templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring,
</div> </div>
} }
func displayBalance(balance int64) string { func formatFloat(balance int64) string {
euros := float64(balance) / 100 euros := float64(balance) / 100
return fmt.Sprintf("%.2f", euros) return fmt.Sprintf("%.2f", euros)
} }
func calculateReferences() {
}

View File

@@ -1,6 +1,5 @@
package treasurechest package treasurechest
import "fmt"
import "spend-sparrow/internal/template/svg" import "spend-sparrow/internal/template/svg"
import "spend-sparrow/internal/types" import "spend-sparrow/internal/types"
import "github.com/google/uuid" import "github.com/google/uuid"
@@ -89,7 +88,9 @@ templ EditTreasureChest(treasureChest *types.TreasureChest, parents []*types.Tre
hx-swap="outerHTML" hx-swap="outerHTML"
class="button button-neglect px-1 flex items-center gap-2" class="button button-neglect px-1 flex items-center gap-2"
> >
@svg.Cancel() <span class="h-4 w-4">
@svg.Cancel()
</span>
<span> <span>
Cancel Cancel
</span> </span>
@@ -130,14 +131,14 @@ templ TreasureChestItem(treasureChest *types.TreasureChest, monthlySums map[uuid
<p class="mr-auto">{ treasureChest.Name }</p> <p class="mr-auto">{ treasureChest.Name }</p>
<p class="mr-20 text-gray-600"> <p class="mr-20 text-gray-600">
if treasureChest.ParentId != nil { if treasureChest.ParentId != nil {
+ { displayBalance(monthlySums[treasureChest.Id]) } <span class="text-gray-500 text-sm">&nbsp;per month</span> + { types.FormatEuros(monthlySums[treasureChest.Id]) } <span class="text-gray-500 text-sm">&nbsp;per month</span>
} }
</p> </p>
if treasureChest.ParentId != nil { if treasureChest.ParentId != nil {
if treasureChest.CurrentBalance < 0 { if treasureChest.CurrentBalance < 0 {
<p class="mr-20 min-w-20 text-right text-red-700">{ displayBalance(treasureChest.CurrentBalance) }</p> <p class="mr-20 min-w-20 text-right text-red-700">{ types.FormatEuros(treasureChest.CurrentBalance) }</p>
} else { } else {
<p class="mr-20 min-w-20 text-right text-green-700">{ displayBalance(treasureChest.CurrentBalance) }</p> <p class="mr-20 min-w-20 text-right text-green-700">{ types.FormatEuros(treasureChest.CurrentBalance) }</p>
} }
} }
<a <a
@@ -187,9 +188,3 @@ func filterNoChildNoSelf(nodes []*types.TreasureChest, selfId string) []*types.T
return result return result
} }
func displayBalance(balance int64) string {
euros := float64(balance) / 100
return fmt.Sprintf("%.2f €", euros)
}

View File

@@ -0,0 +1,30 @@
package types
import "time"
type DashboardMonthlySummary struct {
Month time.Time
// Sum of all Transactions with TreasureChests and no Accounts
Savings int64
// Sum of all Transactions with Accounts and no TreasureChests
Income int64
// Sum of all Transactions with Accounts and TreasureChests
Expenses int64
// Income - Expenses
Total int64
SumOfSavings int64
SumOfAccounts int64
}
type DashboardMainChartEntry struct {
Day time.Time
Value int64
Savings int64
}
type DashboardTreasureChest struct {
Name string
Value int64
Children []DashboardTreasureChest
}

36
internal/types/format.go Normal file
View File

@@ -0,0 +1,36 @@
package types
import (
"fmt"
"strings"
)
func FormatEuros(balance int64) string {
prefix := ""
if balance < 0 {
prefix = "- "
balance = -balance
}
n := float64(balance) / 100
s := fmt.Sprintf("%.2f", n) // "1234567.89"
parts := strings.Split(s, ".")
intPart := parts[0]
fracPart := parts[1]
var result strings.Builder
numberOfSeperators := len(intPart) % 3
if numberOfSeperators == 0 {
result.WriteString(intPart)
} else {
for i := range intPart {
if i > 0 && (i-numberOfSeperators)%3 == 0 {
result.WriteString(",")
}
result.WriteByte(intPart[i])
}
}
return prefix + result.String() + "." + fracPart + " €"
}

View File

@@ -1,6 +1,7 @@
package types package types
import ( import (
"context"
"errors" "errors"
"log/slog" "log/slog"
) )
@@ -26,13 +27,13 @@ type SmtpSettings struct {
FromName string FromName string
} }
func NewSettingsFromEnv(env func(string) string) (*Settings, error) { func NewSettingsFromEnv(ctx context.Context, env func(string) string) (*Settings, error) {
var ( var (
smtp *SmtpSettings smtp *SmtpSettings
err error err error
) )
if env("SMTP_ENABLED") == "true" { if env("SMTP_ENABLED") == "true" {
smtp, err = getSmtpSettings(env) smtp, err = getSmtpSettings(ctx, env)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -46,26 +47,26 @@ func NewSettingsFromEnv(env func(string) string) (*Settings, error) {
} }
if settings.BaseUrl == "" { if settings.BaseUrl == "" {
slog.Error("BASE_URL must be set") slog.ErrorContext(ctx, "BASE_URL must be set")
return nil, ErrMissingConfig return nil, ErrMissingConfig
} }
if settings.Port == "" { if settings.Port == "" {
slog.Error("PORT must be set") slog.ErrorContext(ctx, "PORT must be set")
return nil, ErrMissingConfig return nil, ErrMissingConfig
} }
if settings.Environment == "" { if settings.Environment == "" {
slog.Error("ENVIRONMENT must be set") slog.ErrorContext(ctx, "ENVIRONMENT must be set")
return nil, ErrMissingConfig return nil, ErrMissingConfig
} }
slog.Info("settings read", "BASE_URL", settings.BaseUrl) slog.InfoContext(ctx, "settings read", "BASE_URL", settings.BaseUrl)
slog.Info("settings read", "ENVIRONMENT", settings.Environment) slog.InfoContext(ctx, "settings read", "ENVIRONMENT", settings.Environment)
slog.Info("settings read", "ENVIRONMENT", settings.Environment) slog.InfoContext(ctx, "settings read", "ENVIRONMENT", settings.Environment)
return settings, nil return settings, nil
} }
func getSmtpSettings(env func(string) string) (*SmtpSettings, error) { func getSmtpSettings(ctx context.Context, env func(string) string) (*SmtpSettings, error) {
smtp := SmtpSettings{ smtp := SmtpSettings{
Host: env("SMTP_HOST"), Host: env("SMTP_HOST"),
Port: env("SMTP_PORT"), Port: env("SMTP_PORT"),
@@ -76,27 +77,27 @@ func getSmtpSettings(env func(string) string) (*SmtpSettings, error) {
} }
if smtp.Host == "" { if smtp.Host == "" {
slog.Error("SMTP_HOST must be set") slog.ErrorContext(ctx, "SMTP_HOST must be set")
return nil, ErrMissingConfig return nil, ErrMissingConfig
} }
if smtp.Port == "" { if smtp.Port == "" {
slog.Error("SMTP_PORT must be set") slog.ErrorContext(ctx, "SMTP_PORT must be set")
return nil, ErrMissingConfig return nil, ErrMissingConfig
} }
if smtp.User == "" { if smtp.User == "" {
slog.Error("SMTP_USER must be set") slog.ErrorContext(ctx, "SMTP_USER must be set")
return nil, ErrMissingConfig return nil, ErrMissingConfig
} }
if smtp.Pass == "" { if smtp.Pass == "" {
slog.Error("SMTP_PASS must be set") slog.ErrorContext(ctx, "SMTP_PASS must be set")
return nil, ErrMissingConfig return nil, ErrMissingConfig
} }
if smtp.FromMail == "" { if smtp.FromMail == "" {
slog.Error("SMTP_FROM_MAIL must be set") slog.ErrorContext(ctx, "SMTP_FROM_MAIL must be set")
return nil, ErrMissingConfig return nil, ErrMissingConfig
} }
if smtp.FromName == "" { if smtp.FromName == "" {
slog.Error("SMTP_FROM_NAME must be set") slog.ErrorContext(ctx, "SMTP_FROM_NAME must be set")
return nil, ErrMissingConfig return nil, ErrMissingConfig
} }

View File

@@ -6,13 +6,26 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
// At the center of the application is the transaction. // Transaction is at the center of the application.
// //
// Every piece of data should be calculated based on transactions. // Every piece of data should be calculated based on transactions.
// This means potential calculation errors can be fixed later in time. // This means potential calculation errors can be fixed later in time.
// //
// If it becomes necessary to precalculate snapshots for performance reasons, this can be done in the future. // If it becomes necessary to precalculate snapshots for performance reasons, this can be done in the future.
// But the transaction should always be the source of truth. // But the transaction should always be the source of truth.
//
// There are the following constallations and their explanation:
//
// Account | TreasureChest | Value | Description
// --------|---------------|-------|----------------
// Y | Y | + | Invalid
// Y | Y | - | Expense
// Y | N | + | Deposit
// Y | N | - | Withdrawal (for moving between accounts)
// N | Y | + | Saving
// N | Y | - | Withdrawal (for moving between treasure chests)
// N | N | + | Invalid
// N | N | - | Invalid
type Transaction struct { type Transaction struct {
Id uuid.UUID `db:"id"` Id uuid.UUID `db:"id"`
UserId uuid.UUID `db:"user_id"` UserId uuid.UUID `db:"user_id"`
@@ -38,4 +51,5 @@ type TransactionItemsFilter struct {
AccountId string AccountId string
TreasureChestId string TreasureChestId string
Error string Error string
Page string
} }

View File

@@ -1,6 +1,7 @@
package utils package utils
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -8,16 +9,16 @@ import (
"time" "time"
) )
func TriggerToast(w http.ResponseWriter, r *http.Request, class string, message string) { func TriggerToast(ctx context.Context, w http.ResponseWriter, r *http.Request, class string, message string) {
if IsHtmx(r) { if IsHtmx(r) {
w.Header().Set("Hx-Trigger", fmt.Sprintf(`{"toast": "%v|%v"}`, class, strings.ReplaceAll(message, `"`, `\"`))) w.Header().Set("Hx-Trigger", fmt.Sprintf(`{"toast": "%v|%v"}`, class, strings.ReplaceAll(message, `"`, `\"`)))
} else { } else {
slog.Error("Trying to trigger toast in non-HTMX request") slog.ErrorContext(ctx, "Trying to trigger toast in non-HTMX request")
} }
} }
func TriggerToastWithStatus(w http.ResponseWriter, r *http.Request, class string, message string, statusCode int) { func TriggerToastWithStatus(ctx context.Context, w http.ResponseWriter, r *http.Request, class string, message string, statusCode int) {
TriggerToast(w, r, class, message) TriggerToast(ctx, w, r, class, message)
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
} }
@@ -29,7 +30,7 @@ func DoRedirect(w http.ResponseWriter, r *http.Request, url string) {
} }
} }
func WaitMinimumTime[T interface{}](waitTime time.Duration, f func() (T, error)) (T, error) { func WaitMinimumTime[T any](waitTime time.Duration, f func() (T, error)) (T, error) {
start := time.Now() start := time.Now()
result, err := f() result, err := f()
time.Sleep(waitTime - time.Since(start)) time.Sleep(waitTime - time.Since(start))

17
main.go
View File

@@ -6,31 +6,36 @@ import (
"os" "os"
"spend-sparrow/internal" "spend-sparrow/internal"
"github.com/jmoiron/sqlx"
"github.com/joho/godotenv" "github.com/joho/godotenv"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/uptrace/opentelemetry-go-extra/otelsql"
"github.com/uptrace/opentelemetry-go-extra/otelsqlx"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
) )
func main() { func main() {
ctx := context.Background()
err := godotenv.Load() err := godotenv.Load()
if err != nil { if err != nil {
slog.Error("Error loading .env file") slog.ErrorContext(ctx, "Error loading .env file")
return return
} }
db, err := sqlx.Open("sqlite3", "./data/spend-sparrow.db") db, err := otelsqlx.Open("sqlite3", "./data/spend-sparrow.db?_journal_mode=WAL",
otelsql.WithAttributes(semconv.DBSystemSqlite))
if err != nil { if err != nil {
slog.Error("Could not open Database data.db", "err", err) slog.ErrorContext(ctx, "Could not open Database data.db", "err", err)
return return
} }
defer func() { defer func() {
if err = db.Close(); err != nil { if err = db.Close(); err != nil {
slog.Error("Database close failed", "err", err) slog.ErrorContext(ctx, "Database close failed", "err", err)
} }
}() }()
if err = internal.Run(context.Background(), db, "", os.Getenv); err != nil { if err = internal.Run(context.Background(), db, "", os.Getenv); err != nil {
slog.Error("Error running server", "err", err) slog.ErrorContext(ctx, "Error running server", "err", err)
return return
} }
} }

511
package-lock.json generated
View File

@@ -9,51 +9,32 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@tailwindcss/cli": "4.1.8", "@tailwindcss/cli": "4.1.16",
"htmx.org": "2.0.4", "echarts": "6.0.0",
"tailwindcss": "4.1.8" "htmx.org": "2.0.8",
} "tailwindcss": "4.1.16"
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
} }
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24" "@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
@@ -66,27 +47,17 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25", "version": "0.3.30",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -404,73 +375,68 @@
} }
}, },
"node_modules/@tailwindcss/cli": { "node_modules/@tailwindcss/cli": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.16.tgz",
"integrity": "sha512-+6lkjXSr/68zWiabK3mVYVHmOq/SAHjJ13mR8spyB4LgUWZbWzU9kCSErlAUo+gK5aVfgqe8kY6Ltz9+nz5XYA==", "integrity": "sha512-dsnANPrh2ZooHyZ/8uJhc9ecpcYtufToc21NY09NS9vF16rxPCjJ8dP7TUAtPqlUJTHSmRkN2hCdoYQIlgh4fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@parcel/watcher": "^2.5.1", "@parcel/watcher": "^2.5.1",
"@tailwindcss/node": "4.1.8", "@tailwindcss/node": "4.1.16",
"@tailwindcss/oxide": "4.1.8", "@tailwindcss/oxide": "4.1.16",
"enhanced-resolve": "^5.18.1", "enhanced-resolve": "^5.18.3",
"mri": "^1.2.0", "mri": "^1.2.0",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"tailwindcss": "4.1.8" "tailwindcss": "4.1.16"
}, },
"bin": { "bin": {
"tailwindcss": "dist/index.mjs" "tailwindcss": "dist/index.mjs"
} }
}, },
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
"integrity": "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==", "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.3.0", "@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.1", "enhanced-resolve": "^5.18.3",
"jiti": "^2.4.2", "jiti": "^2.6.1",
"lightningcss": "1.30.1", "lightningcss": "1.30.2",
"magic-string": "^0.30.17", "magic-string": "^0.30.19",
"source-map-js": "^1.2.1", "source-map-js": "^1.2.1",
"tailwindcss": "4.1.8" "tailwindcss": "4.1.16"
} }
}, },
"node_modules/@tailwindcss/oxide": { "node_modules/@tailwindcss/oxide": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz",
"integrity": "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==", "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==",
"dev": true, "dev": true,
"hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": {
"detect-libc": "^2.0.4",
"tar": "^7.4.3"
},
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.8", "@tailwindcss/oxide-android-arm64": "4.1.16",
"@tailwindcss/oxide-darwin-arm64": "4.1.8", "@tailwindcss/oxide-darwin-arm64": "4.1.16",
"@tailwindcss/oxide-darwin-x64": "4.1.8", "@tailwindcss/oxide-darwin-x64": "4.1.16",
"@tailwindcss/oxide-freebsd-x64": "4.1.8", "@tailwindcss/oxide-freebsd-x64": "4.1.16",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.8", "@tailwindcss/oxide-linux-arm64-musl": "4.1.16",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.8", "@tailwindcss/oxide-linux-x64-gnu": "4.1.16",
"@tailwindcss/oxide-linux-x64-musl": "4.1.8", "@tailwindcss/oxide-linux-x64-musl": "4.1.16",
"@tailwindcss/oxide-wasm32-wasi": "4.1.8", "@tailwindcss/oxide-wasm32-wasi": "4.1.16",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.8" "@tailwindcss/oxide-win32-x64-msvc": "4.1.16"
} }
}, },
"node_modules/@tailwindcss/oxide-android-arm64": { "node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz",
"integrity": "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==", "integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -485,9 +451,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-darwin-arm64": { "node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz",
"integrity": "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==", "integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -502,9 +468,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-darwin-x64": { "node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz",
"integrity": "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==", "integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -519,9 +485,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-freebsd-x64": { "node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz",
"integrity": "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==", "integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -536,9 +502,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz",
"integrity": "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==", "integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -553,9 +519,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": { "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz",
"integrity": "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==", "integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -570,9 +536,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-arm64-musl": { "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz",
"integrity": "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==", "integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -587,9 +553,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-x64-gnu": { "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz",
"integrity": "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==", "integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -604,9 +570,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-x64-musl": { "node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz",
"integrity": "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==", "integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -621,9 +587,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi": { "node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz",
"integrity": "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==", "integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==",
"bundleDependencies": [ "bundleDependencies": [
"@napi-rs/wasm-runtime", "@napi-rs/wasm-runtime",
"@emnapi/core", "@emnapi/core",
@@ -639,30 +605,30 @@
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/core": "^1.4.3", "@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.4.3", "@emnapi/runtime": "^1.5.0",
"@emnapi/wasi-threads": "^1.0.2", "@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^0.2.10", "@napi-rs/wasm-runtime": "^1.0.7",
"@tybys/wasm-util": "^0.9.0", "@tybys/wasm-util": "^0.10.1",
"tslib": "^2.8.0" "tslib": "^2.4.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.4.3", "version": "1.5.0",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/wasi-threads": "1.0.2", "@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.4.3", "version": "1.5.0",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
@@ -672,7 +638,7 @@
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.0.2", "version": "1.1.0",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
@@ -682,19 +648,19 @@
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.10", "version": "1.0.7",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/core": "^1.4.3", "@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.4.3", "@emnapi/runtime": "^1.5.0",
"@tybys/wasm-util": "^0.9.0" "@tybys/wasm-util": "^0.10.1"
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.9.0", "version": "0.10.1",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
@@ -704,16 +670,16 @@
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.0", "version": "2.8.1",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "0BSD", "license": "0BSD",
"optional": true "optional": true
}, },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz",
"integrity": "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==", "integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -728,9 +694,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-win32-x64-msvc": { "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz",
"integrity": "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==", "integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -744,16 +710,6 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@tailwindcss/oxide/node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@@ -767,16 +723,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -790,10 +736,21 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.0.0"
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.1", "version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -825,9 +782,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/htmx.org": { "node_modules/htmx.org": {
"version": "2.0.4", "version": "2.0.8",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz", "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.8.tgz",
"integrity": "sha512-HLxMCdfXDOJirs3vBZl/ZLoY+c7PfM4Ahr2Ad4YXh6d22T5ltbTXFFkpx9Tgb2vvmWFMbIc3LqN2ToNkZJvyYQ==", "integrity": "sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==",
"dev": true, "dev": true,
"license": "0BSD" "license": "0BSD"
}, },
@@ -865,9 +822,9 @@
} }
}, },
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.4.2", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -875,9 +832,9 @@
} }
}, },
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"dev": true, "dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
@@ -891,22 +848,44 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
}, },
"optionalDependencies": { "optionalDependencies": {
"lightningcss-darwin-arm64": "1.30.1", "lightningcss-android-arm64": "1.30.2",
"lightningcss-darwin-x64": "1.30.1", "lightningcss-darwin-arm64": "1.30.2",
"lightningcss-freebsd-x64": "1.30.1", "lightningcss-darwin-x64": "1.30.2",
"lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-freebsd-x64": "1.30.2",
"lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.2",
"lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.2",
"lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.2",
"lightningcss-linux-x64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.2",
"lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-linux-x64-musl": "1.30.2",
"lightningcss-win32-x64-msvc": "1.30.1" "lightningcss-win32-arm64-msvc": "1.30.2",
"lightningcss-win32-x64-msvc": "1.30.2"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/lightningcss-darwin-arm64": { "node_modules/lightningcss-darwin-arm64": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -925,9 +904,9 @@
} }
}, },
"node_modules/lightningcss-darwin-x64": { "node_modules/lightningcss-darwin-x64": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -946,9 +925,9 @@
} }
}, },
"node_modules/lightningcss-freebsd-x64": { "node_modules/lightningcss-freebsd-x64": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -967,9 +946,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm-gnueabihf": { "node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -988,9 +967,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm64-gnu": { "node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1009,9 +988,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm64-musl": { "node_modules/lightningcss-linux-arm64-musl": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1030,9 +1009,9 @@
} }
}, },
"node_modules/lightningcss-linux-x64-gnu": { "node_modules/lightningcss-linux-x64-gnu": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1051,9 +1030,9 @@
} }
}, },
"node_modules/lightningcss-linux-x64-musl": { "node_modules/lightningcss-linux-x64-musl": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1072,9 +1051,9 @@
} }
}, },
"node_modules/lightningcss-win32-arm64-msvc": { "node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1093,9 +1072,9 @@
} }
}, },
"node_modules/lightningcss-win32-x64-msvc": { "node_modules/lightningcss-win32-x64-msvc": {
"version": "1.30.1", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1114,9 +1093,9 @@
} }
}, },
"node_modules/lightningcss/node_modules/detect-libc": { "node_modules/lightningcss/node_modules/detect-libc": {
"version": "2.0.4", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -1124,13 +1103,13 @@
} }
}, },
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.19",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/micromatch": { "node_modules/micromatch": {
@@ -1147,45 +1126,6 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mri": { "node_modules/mri": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -1234,40 +1174,22 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.8", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
"integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==", "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tar": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"dev": true,
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -1281,14 +1203,21 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/yallist": { "node_modules/tslib": {
"version": "5.0.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "0BSD"
"engines": { },
"node": ">=18" "node_modules/zrender": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
} }
} }
} }

View File

@@ -1,18 +1,19 @@
{ {
"name": "spend-sparrow", "name": "spend-sparrow",
"version": "1.0.0", "version": "1.0.0",
"description": "Your (almost) independent tech stack to host on a VPC.", "description": "Personal finance tracking done right",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --minify", "build": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && cp -f node_modules/echarts/dist/echarts.min.js static/js/echarts.min.js && tailwindcss -i input.css -o static/css/tailwind.css --minify",
"watch": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --watch" "watch": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && cp -f node_modules/echarts/dist/echarts.min.js static/js/echarts.min.js && tailwindcss -i input.css -o static/css/tailwind.css --watch"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"htmx.org": "2.0.4", "@tailwindcss/cli": "4.1.16",
"tailwindcss": "4.1.8", "htmx.org": "2.0.8",
"@tailwindcss/cli": "4.1.8" "tailwindcss": "4.1.16",
"echarts": "6.0.0"
} }
} }

View File

@@ -1 +1,53 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 128 128"><path d="M93.46 39.45c6.71-1.49 15.45-8.15 16.78-11.43.78-1.92-3.11-4.92-4.15-6.13-2.38-2.76-1.42-4.12-.5-7.41 1.05-3.74-1.44-7.87-4.97-9.49s-7.75-1.11-11.3.47-6.58 4.12-9.55 6.62c-2.17-1.37-5.63-7.42-11.23-3.49-3.87 2.71-4.22 8.61-3.72 13.32 1.17 10.87 3.85 16.51 8.9 18.03 6.38 1.92 13.44.91 19.74-.49" style="fill:#ffca28"/><path d="M104.36 8.18c-.85 14.65-15.14 24.37-21.92 28.65l4.4 3.78s2.79.06 6.61-1.16c6.55-2.08 16.12-7.96 16.78-11.43.97-5.05-4.21-3.95-5.38-7.94-.61-2.11 2.97-6.1-.49-11.9M79.78 12.09s-2.55-2.61-4.44-3.8c-.94 1.77-1.61 3.69-1.94 5.67-.59 3.48 0 8.42 1.39 12.1.22.57 1.04.48 1.13-.12 1.2-7.91 3.86-13.85 3.86-13.85" style="fill:#e2a610"/><path d="M61.96 38.16S30.77 41.53 16.7 68.61s-2.11 43.5 10.55 49.48 44.56 8.09 65.31 3.17 25.94-15.12 24.97-24.97c-1.41-14.38-14.77-23.22-14.77-23.22s.53-17.76-13.25-29.29c-12.23-10.24-27.55-5.62-27.55-5.62" style="fill:#ffca28"/><path d="M74.76 83.73c-6.69-8.44-14.59-9.57-17.12-12.6-1.38-1.65-2.19-3.32-1.88-5.39.33-2.2 2.88-3.72 4.86-4.09 2.31-.44 7.82-.21 12.45 4.2 1.1 1.04.7 2.66.67 4.11-.08 3.11 4.37 6.13 7.97 3.53 3.61-2.61.84-8.42-1.49-11.24-1.76-2.13-8.14-6.82-16.07-7.56-2.23-.21-11.2-1.54-16.38 8.31-1.49 2.83-2.04 9.67 5.76 15.45 1.63 1.21 10.09 5.51 12.44 8.3 4.07 4.83 1.28 9.08-1.9 9.64-8.67 1.52-13.58-3.17-14.49-5.74-.65-1.83.03-3.81-.81-5.53-.86-1.77-2.62-2.47-4.48-1.88-6.1 1.94-4.16 8.61-1.46 12.28 2.89 3.93 6.44 6.3 10.43 7.6 14.89 4.85 22.05-2.81 23.3-8.42.92-4.11.82-7.67-1.8-10.97" style="fill:#6b4b46"/><path d="M71.16 48.99c-12.67 27.06-14.85 61.23-14.85 61.23" style="fill:none;stroke:#6b4b46;stroke-width:5;stroke-miterlimit:10"/><path d="M81.67 31.96c8.44 2.75 10.31 10.38 9.7 12.46-.73 2.44-10.08-7.06-23.98-6.49-4.86.2-3.45-2.78-1.2-4.5 2.97-2.27 7.96-3.91 15.48-1.47" style="fill:#6d4c41"/><path d="M81.67 31.96c8.44 2.75 10.31 10.38 9.7 12.46-.73 2.44-10.08-7.06-23.98-6.49-4.86.2-3.45-2.78-1.2-4.5 2.97-2.27 7.96-3.91 15.48-1.47" style="fill:#6b4b46"/><path d="M96.49 58.86c1.06-.73 4.62.53 5.62 7.5.49 3.41.64 6.71.64 6.71s-4.2-3.77-5.59-6.42c-1.75-3.35-2.43-6.59-.67-7.79" style="fill:#e2a610"/></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="28.384802mm"
height="31.749905mm"
viewBox="0 0 28.384802 31.749905"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-37.253301,-88.598061)">
<path
d="m 59.240389,97.978247 c 1.775354,-0.394229 4.087813,-2.156354 4.439709,-3.024187 0.206375,-0.508 -0.822855,-1.30175 -1.098021,-1.621896 -0.629709,-0.73025 -0.375709,-1.090083 -0.132292,-1.960562 0.277813,-0.989542 -0.381,-2.082271 -1.314979,-2.510896 -0.933979,-0.428625 -2.050521,-0.293688 -2.989792,0.124354 -0.939271,0.418042 -1.740958,1.090083 -2.526771,1.751542 -0.574145,-0.36248 -1.489604,-1.963209 -2.97127,-0.923396 -1.023938,0.717021 -1.116542,2.278062 -0.98425,3.52425 0.309562,2.876021 1.018645,4.368271 2.354791,4.770437 1.688042,0.508 3.556,0.240771 5.222875,-0.129646"
style="fill:#ffca28;stroke-width:0.264583"
id="path1-3" />
<path
d="m 62.124348,89.704727 c -0.224896,3.876145 -4.005792,6.447895 -5.799667,7.580312 l 1.164167,1.000125 c 0,0 0.738187,0.01588 1.748895,-0.306917 1.733021,-0.550333 4.265084,-2.106083 4.439709,-3.024187 0.256646,-1.336146 -1.113896,-1.045104 -1.423459,-2.100792 -0.161395,-0.558271 0.785813,-1.613958 -0.129645,-3.148541 m -6.503459,1.03452 c 0,0 -0.674687,-0.690562 -1.17475,-1.005416 -0.248708,0.468312 -0.425979,0.976312 -0.513291,1.500187 -0.156105,0.92075 0,2.227792 0.36777,3.201459 0.05821,0.150812 0.275167,0.127 0.29898,-0.03175 0.3175,-2.092855 1.021291,-3.66448 1.021291,-3.66448"
style="fill:#e2a610;stroke-width:0.264583"
id="path2-6" />
<path
d="m 50.906014,97.636935 c 0,0 -8.252354,0.891646 -11.975042,8.056565 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490104,-1.30175 6.863292,-4.0005 6.606646,-6.60664 -0.373062,-3.80471 -3.907896,-6.14363 -3.907896,-6.14363 0,0 0.140229,-4.699 -3.505729,-7.749647 -3.235854,-2.709333 -7.289271,-1.486958 -7.289271,-1.486958"
style="fill:#ffca28;stroke-width:0.264583"
id="path3-0" />
<path
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
style="fill:#6d4c41;stroke-width:0.264583"
id="path6-6" />
<path
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
style="fill:#6b4b46;stroke-width:0.264583"
id="path7-2" />
<path
d="m 60.042077,103.11381 c 0.280458,-0.19314 1.222375,0.14023 1.486958,1.98438 0.129646,0.90223 0.169333,1.77535 0.169333,1.77535 0,0 -1.11125,-0.99748 -1.47902,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
style="fill:#e2a610;stroke-width:0.264583"
id="path8-6" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="82.355011"
y="90.66716"
id="text4-9"
transform="rotate(20.578693)"><tspan
id="tspan4-2"
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
x="82.355011"
y="90.66716">$</tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

100
static/js/dashboard.js Normal file
View File

@@ -0,0 +1,100 @@
// Initialize the echarts instance based on the prepared dom
async function initMainChart() {
const element = document.getElementById('main-chart')
if (element === null) {
return;
}
var myChart = echarts.init(element);
window.addEventListener('resize', function() {
myChart.resize();
});
try {
const response = await fetch("/dashboard/main-chart");
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const option = await response.json();
option.tooltip.formatter = function (params) {
return new Date(params[0].data[0]).toLocaleString([], { day: 'numeric', month: 'short', year: 'numeric' }) +
'<br />' +
'Sum of Accounts: <span class="font-bold">' + params[0].data[1] + '</span> € <br />' +
'Sum of Savings: <span class="font-bold">' + params[1].data[1] + '</span> €'
};
myChart.setOption(option);
console.log("initialized main-chart");
} catch (error) {
console.error(error.message);
}
}
async function initTreasureChests() {
const element = document.getElementById('treasure-chests')
if (element === null) {
return;
}
var myChart = echarts.init(element);
window.addEventListener('resize', function() {
myChart.resize();
});
try {
const response = await fetch("/dashboard/treasure-chests");
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const option = await response.json();
myChart.setOption(option);
console.log("initialized treasure-chests");
} catch (error) {
console.error(error.message);
}
}
async function initTreasureChest() {
const element = document.getElementById('treasure-chest')
if (element === null) {
return;
}
var myChart = echarts.init(element);
window.addEventListener('resize', function() {
myChart.resize();
});
const treasureChestSelect = document.getElementById('treasure-chest-id')
treasureChestSelect.addEventListener("change", async (e) => {
try {
const response = await fetch("/dashboard/treasure-chest?id="+e.target.value);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const option = await response.json();
option.tooltip.formatter = function (params) {
return new Date(params[0].data[0]).toLocaleString([], { day: 'numeric', month: 'short', year: 'numeric' }) +
'<br />' +
'Sum of Accounts: <span class="font-bold">' + params[0].data[1] + '</span> €'
};
myChart.setOption(option);
} catch (error) {
console.error(error.message);
}
});
console.log("initialized treasure-chest");
}
initMainChart();
initTreasureChests();
initTreasureChest();

11
static/js/layout.js Normal file
View File

@@ -0,0 +1,11 @@
document.addEventListener("DOMContentLoaded", () => {
menuButton.addEventListener("click", function (e) {
menu.showModal();
});
menuButtonClose.addEventListener("click", function (e) {
menu.close();
});
})

View File

@@ -4,7 +4,6 @@ htmx.on("htmx:afterSwap", () => {
}); });
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded");
updateTime(document); updateTime(document);
}) })
@@ -17,6 +16,7 @@ function updateTime() {
const newDate = value.includes("UTC") ? new Date(value) : value; const newDate = value.includes("UTC") ? new Date(value) : value;
el.valueAsDate = newDate; el.valueAsDate = newDate;
} }
el.classList.remove("datetime");
}) })
} }

43
static/js/transaction.js Normal file
View File

@@ -0,0 +1,43 @@
document.addEventListener("DOMContentLoaded", () => {
if (!page || !page1 || !pagePrev1 || !pageNext1 || !page2 || !pagePrev2 || !pageNext2 || !transactionFilterForm) {
return;
}
const scrollToTop = function() {
window.scrollTo(0, 0);
};
const incPage = function() {
const currPage = Number(page.value);
var nextPage = currPage
if (currPage > 1) {
nextPage -= 1;
page.value = nextPage;
transactionFilterForm.dispatchEvent(new Event('change'));
}
page1.textContent = nextPage;
page2.textContent = nextPage;
scrollToTop();
};
const decPage = function() {
const currPage = Number(page.value);
var nextPage = currPage + 1;
page.value = nextPage;
transactionFilterForm.dispatchEvent(new Event('change'));
page1.textContent = nextPage;
page2.textContent = nextPage;
scrollToTop();
};
pagePrev1.addEventListener("click", incPage);
pagePrev2.addEventListener("click", incPage);
pageNext1.addEventListener("click", decPage);
pageNext2.addEventListener("click", decPage);
console.log("initialized pagination");
})

71
static/logo.svg Normal file
View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="96.820343mm"
height="31.749899mm"
viewBox="0 0 96.820343 31.749899"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g7"
transform="translate(-38.090175,-77.467441)">
<g
id="g8"
transform="translate(-38.797122,-28.178962)">
<path
d="m 98.874384,115.02659 c 1.775356,-0.39423 4.087816,-2.15635 4.439706,-3.02419 0.20638,-0.508 -0.82285,-1.30175 -1.09802,-1.62189 -0.62971,-0.73025 -0.37571,-1.09009 -0.13229,-1.96057 0.27781,-0.98954 -0.381,-2.08227 -1.31498,-2.51089 -0.933978,-0.42863 -2.05052,-0.29369 -2.989791,0.12435 -0.939271,0.41804 -1.740958,1.09009 -2.526771,1.75154 -0.574145,-0.36248 -1.489604,-1.96321 -2.97127,-0.92339 -1.023938,0.71702 -1.116542,2.27806 -0.98425,3.52425 0.309562,2.87602 1.018645,4.36827 2.354791,4.77043 1.688042,0.508 3.556,0.24078 5.222875,-0.12964"
style="fill:#ffca28;stroke-width:0.264583"
id="path1-3-3" />
<path
d="m 101.75834,106.75307 c -0.22489,3.87614 -4.005789,6.44789 -5.799664,7.58031 l 1.164167,1.00013 c 0,0 0.738187,0.0159 1.748895,-0.30692 1.733022,-0.55033 4.265082,-2.10608 4.439712,-3.02419 0.25664,-1.33614 -1.1139,-1.0451 -1.42346,-2.10079 -0.1614,-0.55827 0.78581,-1.61396 -0.12965,-3.14854 m -6.503456,1.03452 c 0,0 -0.674687,-0.69056 -1.17475,-1.00542 -0.248708,0.46832 -0.425979,0.97632 -0.513291,1.50019 -0.156105,0.92075 0,2.22779 0.36777,3.20146 0.05821,0.15081 0.275167,0.127 0.29898,-0.0317 0.3175,-2.09286 1.021291,-3.66448 1.021291,-3.66448"
style="fill:#e2a610;stroke-width:0.264583"
id="path2-6-6" />
<path
d="m 90.540009,114.68528 c 0,0 -8.252354,0.89164 -11.975042,8.05656 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490101,-1.30175 6.863291,-4.0005 6.606641,-6.60664 -0.37306,-3.80471 -3.90789,-6.14363 -3.90789,-6.14363 0,0 0.14023,-4.699 -3.50573,-7.74964 -3.235854,-2.70934 -7.289271,-1.48696 -7.289271,-1.48696"
style="fill:#ffca28;stroke-width:0.264583"
id="path3-0-1" />
<path
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
style="fill:#6d4c41;stroke-width:0.264583"
id="path6-6-2" />
<path
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
style="fill:#6b4b46;stroke-width:0.264583"
id="path7-2-9" />
<path
d="m 99.676072,120.16215 c 0.280458,-0.19314 1.222378,0.14023 1.486958,1.98438 0.12965,0.90223 0.16933,1.77535 0.16933,1.77535 0,0 -1.11125,-0.99748 -1.479017,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
style="fill:#e2a610;stroke-width:0.264583"
id="path8-6-3" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="125.45235"
y="92.696564"
id="text4-9-1"
transform="rotate(20.578693)"><tspan
id="tspan4-2-9"
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
x="125.45235"
y="92.696564">$</tspan></text>
</g>
<g
id="layer2"
transform="translate(-1.4293676,48.496402)">
<text
xml:space="preserve"
style="font-size:17.6389px;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
x="57.635151"
y="55.655094"
id="text1"><tspan
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.6389px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:-0.529167px;fill:#4d4d4d;stroke:none;stroke-width:0.264583"
x="57.635151"
y="55.655094">pendSparrow</tspan></text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -1,6 +1,7 @@
package test_test package test_test
import ( import (
"context"
"spend-sparrow/internal/db" "spend-sparrow/internal/db"
"spend-sparrow/internal/types" "spend-sparrow/internal/types"
"testing" "testing"
@@ -26,7 +27,7 @@ func setupDb(t *testing.T) *sqlx.DB {
} }
}) })
err = db.RunMigrations(d, "../") err = db.RunMigrations(context.Background(), d, "../")
if err != nil { if err != nil {
t.Fatalf("Error running migrations: %v", err) t.Fatalf("Error running migrations: %v", err)
} }
@@ -47,14 +48,14 @@ func TestUser(t *testing.T) {
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
expected := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt) expected := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
err := underTest.InsertUser(expected) err := underTest.InsertUser(context.Background(), expected)
require.NoError(t, err) require.NoError(t, err)
actual, err := underTest.GetUser(expected.Id) actual, err := underTest.GetUser(context.Background(), expected.Id)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expected, actual) assert.Equal(t, expected, actual)
actual, err = underTest.GetUserByEmail(expected.Email) actual, err = underTest.GetUserByEmail(context.Background(), expected.Email)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expected, actual) assert.Equal(t, expected, actual)
}) })
@@ -64,7 +65,7 @@ func TestUser(t *testing.T) {
underTest := db.NewAuthSqlite(d) underTest := db.NewAuthSqlite(d)
_, err := underTest.GetUserByEmail("nonExistentEmail") _, err := underTest.GetUserByEmail(context.Background(), "nonExistentEmail")
assert.Equal(t, db.ErrNotFound, err) assert.Equal(t, db.ErrNotFound, err)
}) })
t.Run("should return ErrUserExist", func(t *testing.T) { t.Run("should return ErrUserExist", func(t *testing.T) {
@@ -77,10 +78,10 @@ func TestUser(t *testing.T) {
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt) user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
err := underTest.InsertUser(user) err := underTest.InsertUser(context.Background(), user)
require.NoError(t, err) require.NoError(t, err)
err = underTest.InsertUser(user) err = underTest.InsertUser(context.Background(), user)
assert.Equal(t, db.ErrAlreadyExists, err) assert.Equal(t, db.ErrAlreadyExists, err)
}) })
t.Run("should return ErrInternal on missing NOT NULL fields", func(t *testing.T) { t.Run("should return ErrInternal on missing NOT NULL fields", func(t *testing.T) {
@@ -92,7 +93,7 @@ func TestUser(t *testing.T) {
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt) user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
err := underTest.InsertUser(user) err := underTest.InsertUser(context.Background(), user)
assert.Equal(t, types.ErrInternal, err) assert.Equal(t, types.ErrInternal, err)
}) })
} }
@@ -110,21 +111,21 @@ func TestToken(t *testing.T) {
expiresAt := createAt.Add(24 * time.Hour) expiresAt := createAt.Add(24 * time.Hour)
expected := types.NewToken(uuid.New(), "sessionId", "token", types.TokenTypeCsrf, createAt, expiresAt) expected := types.NewToken(uuid.New(), "sessionId", "token", types.TokenTypeCsrf, createAt, expiresAt)
err := underTest.InsertToken(expected) err := underTest.InsertToken(context.Background(), expected)
require.NoError(t, err) require.NoError(t, err)
actual, err := underTest.GetToken(expected.Token) actual, err := underTest.GetToken(context.Background(), expected.Token)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expected, actual) assert.Equal(t, expected, actual)
expected.SessionId = "" expected.SessionId = ""
actuals, err := underTest.GetTokensByUserIdAndType(expected.UserId, expected.Type) actuals, err := underTest.GetTokensByUserIdAndType(context.Background(), expected.UserId, expected.Type)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []*types.Token{expected}, actuals) assert.Equal(t, []*types.Token{expected}, actuals)
expected.SessionId = "sessionId" expected.SessionId = "sessionId"
expected.UserId = uuid.Nil expected.UserId = uuid.Nil
actuals, err = underTest.GetTokensBySessionIdAndType(expected.SessionId, expected.Type) actuals, err = underTest.GetTokensBySessionIdAndType(context.Background(), expected.SessionId, expected.Type)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []*types.Token{expected}, actuals) assert.Equal(t, []*types.Token{expected}, actuals)
}) })
@@ -140,14 +141,14 @@ func TestToken(t *testing.T) {
expected1 := types.NewToken(userId, "sessionId", "token1", types.TokenTypeCsrf, createAt, expiresAt) expected1 := types.NewToken(userId, "sessionId", "token1", types.TokenTypeCsrf, createAt, expiresAt)
expected2 := types.NewToken(userId, "sessionId", "token2", types.TokenTypeCsrf, createAt, expiresAt) expected2 := types.NewToken(userId, "sessionId", "token2", types.TokenTypeCsrf, createAt, expiresAt)
err := underTest.InsertToken(expected1) err := underTest.InsertToken(context.Background(), expected1)
require.NoError(t, err) require.NoError(t, err)
err = underTest.InsertToken(expected2) err = underTest.InsertToken(context.Background(), expected2)
require.NoError(t, err) require.NoError(t, err)
expected1.UserId = uuid.Nil expected1.UserId = uuid.Nil
expected2.UserId = uuid.Nil expected2.UserId = uuid.Nil
actuals, err := underTest.GetTokensBySessionIdAndType(expected1.SessionId, expected1.Type) actuals, err := underTest.GetTokensBySessionIdAndType(context.Background(), expected1.SessionId, expected1.Type)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []*types.Token{expected1, expected2}, actuals) assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
@@ -155,7 +156,7 @@ func TestToken(t *testing.T) {
expected2.SessionId = "" expected2.SessionId = ""
expected1.UserId = userId expected1.UserId = userId
expected2.UserId = userId expected2.UserId = userId
actuals, err = underTest.GetTokensByUserIdAndType(userId, expected1.Type) actuals, err = underTest.GetTokensByUserIdAndType(context.Background(), userId, expected1.Type)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []*types.Token{expected1, expected2}, actuals) assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
}) })
@@ -165,13 +166,13 @@ func TestToken(t *testing.T) {
underTest := db.NewAuthSqlite(d) underTest := db.NewAuthSqlite(d)
_, err := underTest.GetToken("nonExistent") _, err := underTest.GetToken(context.Background(), "nonExistent")
assert.Equal(t, db.ErrNotFound, err) assert.Equal(t, db.ErrNotFound, err)
_, err = underTest.GetTokensByUserIdAndType(uuid.New(), types.TokenTypeEmailVerify) _, err = underTest.GetTokensByUserIdAndType(context.Background(), uuid.New(), types.TokenTypeEmailVerify)
assert.Equal(t, db.ErrNotFound, err) assert.Equal(t, db.ErrNotFound, err)
_, err = underTest.GetTokensBySessionIdAndType("sessionId", types.TokenTypeEmailVerify) _, err = underTest.GetTokensBySessionIdAndType(context.Background(), "sessionId", types.TokenTypeEmailVerify)
assert.Equal(t, db.ErrNotFound, err) assert.Equal(t, db.ErrNotFound, err)
}) })
t.Run("should return ErrAlreadyExists", func(t *testing.T) { t.Run("should return ErrAlreadyExists", func(t *testing.T) {
@@ -184,10 +185,10 @@ func TestToken(t *testing.T) {
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt) user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
err := underTest.InsertUser(user) err := underTest.InsertUser(context.Background(), user)
require.NoError(t, err) require.NoError(t, err)
err = underTest.InsertUser(user) err = underTest.InsertUser(context.Background(), user)
assert.Equal(t, db.ErrAlreadyExists, err) assert.Equal(t, db.ErrAlreadyExists, err)
}) })
t.Run("should return ErrInternal on missing NOT NULL fields", func(t *testing.T) { t.Run("should return ErrInternal on missing NOT NULL fields", func(t *testing.T) {
@@ -199,7 +200,7 @@ func TestToken(t *testing.T) {
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC) createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt) user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
err := underTest.InsertUser(user) err := underTest.InsertUser(context.Background(), user)
assert.Equal(t, types.ErrInternal, err) assert.Equal(t, types.ErrInternal, err)
}) })
} }

View File

@@ -1,6 +1,7 @@
package test_test package test_test
import ( import (
"context"
"spend-sparrow/internal/db" "spend-sparrow/internal/db"
"spend-sparrow/internal/service" "spend-sparrow/internal/service"
"spend-sparrow/internal/types" "spend-sparrow/internal/types"
@@ -36,7 +37,7 @@ func TestSignUp(t *testing.T) {
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings) underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
_, err := underTest.SignUp("invalid email address", "SomeStrongPassword123!") _, err := underTest.SignUp(context.Background(), "invalid email address", "SomeStrongPassword123!")
assert.Equal(t, service.ErrInvalidEmail, err) assert.Equal(t, service.ErrInvalidEmail, err)
}) })
@@ -58,7 +59,7 @@ func TestSignUp(t *testing.T) {
} }
for _, password := range weakPasswords { for _, password := range weakPasswords {
_, err := underTest.SignUp("some@valid.email", password) _, err := underTest.SignUp(context.Background(), "some@valid.email", password)
assert.Equal(t, service.ErrInvalidPassword, err) assert.Equal(t, service.ErrInvalidPassword, err)
} }
}) })
@@ -78,13 +79,15 @@ func TestSignUp(t *testing.T) {
expected := types.NewUser(userId, email, false, nil, false, service.GetHashPassword(password, salt), salt, createTime) expected := types.NewUser(userId, email, false, nil, false, service.GetHashPassword(password, salt), salt, createTime)
mockRandom.EXPECT().UUID().Return(userId, nil) ctx := context.Background()
mockRandom.EXPECT().Bytes(16).Return(salt, nil)
mockRandom.EXPECT().UUID(ctx).Return(userId, nil)
mockRandom.EXPECT().Bytes(ctx, 16).Return(salt, nil)
mockClock.EXPECT().Now().Return(createTime) mockClock.EXPECT().Now().Return(createTime)
mockAuthDb.EXPECT().InsertUser(expected).Return(nil) mockAuthDb.EXPECT().InsertUser(context.Background(), expected).Return(nil)
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings) underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
actual, err := underTest.SignUp(email, password) actual, err := underTest.SignUp(context.Background(), email, password)
require.NoError(t, err) require.NoError(t, err)
@@ -105,15 +108,16 @@ func TestSignUp(t *testing.T) {
salt := []byte("salt") salt := []byte("salt")
user := types.NewUser(userId, email, false, nil, false, service.GetHashPassword(password, salt), salt, createTime) user := types.NewUser(userId, email, false, nil, false, service.GetHashPassword(password, salt), salt, createTime)
mockRandom.EXPECT().UUID().Return(user.Id, nil) ctx := context.Background()
mockRandom.EXPECT().Bytes(16).Return(salt, nil) mockRandom.EXPECT().UUID(ctx).Return(user.Id, nil)
mockRandom.EXPECT().Bytes(ctx, 16).Return(salt, nil)
mockClock.EXPECT().Now().Return(createTime) mockClock.EXPECT().Now().Return(createTime)
mockAuthDb.EXPECT().InsertUser(user).Return(db.ErrAlreadyExists) mockAuthDb.EXPECT().InsertUser(context.Background(), user).Return(db.ErrAlreadyExists)
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings) underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
_, err := underTest.SignUp(user.Email, password) _, err := underTest.SignUp(context.Background(), user.Email, password)
assert.Equal(t, service.ErrAccountExists, err) assert.Equal(t, service.ErrAccountExists, err)
}) })
} }
@@ -140,14 +144,14 @@ func TestSendVerificationMail(t *testing.T) {
mockClock := mocks.NewMockClock(t) mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t) mockMail := mocks.NewMockMail(t)
mockAuthDb.EXPECT().GetTokensByUserIdAndType(userId, types.TokenTypeEmailVerify).Return(tokens, nil) ctx := context.Background()
mockAuthDb.EXPECT().GetTokensByUserIdAndType(context.Background(), userId, types.TokenTypeEmailVerify).Return(tokens, nil)
mockMail.EXPECT().SendMail(email, "Welcome to spend-sparrow", mock.MatchedBy(func(message string) bool { mockMail.EXPECT().SendMail(ctx, email, "Welcome to spend-sparrow", mock.MatchedBy(func(message string) bool {
return strings.Contains(message, token.Token) return strings.Contains(message, token.Token)
})).Return() })).Return()
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings) underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
underTest.SendVerificationMail(userId, email) underTest.SendVerificationMail(context.Background(), userId, email)
}) })
} }

View File

@@ -152,7 +152,7 @@ func getTokenAttribute(t *testing.T, data *html.Node) string {
for _, attr := range data.Attr { for _, attr := range data.Attr {
if attr.Key == "hx-headers" { if attr.Key == "hx-headers" {
var data map[string]interface{} var data map[string]any
err := json.Unmarshal([]byte(attr.Val), &data) err := json.Unmarshal([]byte(attr.Val), &data)
require.NoError(t, err) require.NoError(t, err)
result, ok := data["Csrf-Token"].(string) result, ok := data["Csrf-Token"].(string)
@@ -182,16 +182,16 @@ func createValidUserSession(t *testing.T, db *sqlx.DB, add string) (uuid.UUID, s
csrfToken := "my-verifying-token" + add csrfToken := "my-verifying-token" + add
email := add + "mail@mail.de" email := add + "mail@mail.de"
_, err := db.Exec(` _, err := db.ExecContext(context.Background(), `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, ?, TRUE, FALSE, ?, ?, datetime())`, userId, email, pass, []byte("salt")) VALUES (?, ?, TRUE, FALSE, ?, ?, datetime())`, userId, email, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(context.Background(), `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(context.Background(), `
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at) INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
VALUES (?, ?, ?, ?, datetime(), datetime("now", "+1 day"))`, csrfToken, userId, sessionId, types.TokenTypeCsrf) VALUES (?, ?, ?, ?, datetime(), datetime("now", "+1 day"))`, csrfToken, userId, sessionId, types.TokenTypeCsrf)
require.NoError(t, err) require.NoError(t, err)

View File

@@ -112,11 +112,11 @@ func TestIntegrationAuth(t *testing.T) {
sessionId := "session-id" sessionId := "session-id"
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
@@ -138,7 +138,7 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -165,7 +165,7 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -208,7 +208,7 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -248,7 +248,7 @@ func TestIntegrationAuth(t *testing.T) {
db, basePath, ctx := setupIntegrationTest(t) db, basePath, ctx := setupIntegrationTest(t)
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -296,7 +296,7 @@ func TestIntegrationAuth(t *testing.T) {
db, basePath, ctx := setupIntegrationTest(t) db, basePath, ctx := setupIntegrationTest(t)
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -415,7 +415,7 @@ func TestIntegrationAuth(t *testing.T) {
db, basePath, ctx := setupIntegrationTest(t) db, basePath, ctx := setupIntegrationTest(t)
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -451,10 +451,10 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM session WHERE session_id = ?", anonymousSession.Value).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM session WHERE session_id = ?", anonymousSession.Value).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, rows) assert.Equal(t, 0, rows)
err = db.QueryRow("SELECT COUNT(*) FROM token WHERE token = ?", anonymousCsrfToken).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM token WHERE token = ?", anonymousCsrfToken).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, rows) assert.Equal(t, 0, rows)
}) })
@@ -469,11 +469,11 @@ func TestIntegrationAuth(t *testing.T) {
sessionId := "session-id" sessionId := "session-id"
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
@@ -548,7 +548,7 @@ func TestIntegrationAuth(t *testing.T) {
db, basePath, ctx := setupIntegrationTest(t) db, basePath, ctx := setupIntegrationTest(t)
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, uuid.New(), service.GetHashPassword("password", []byte("salt")), []byte("salt")) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, uuid.New(), service.GetHashPassword("password", []byte("salt")), []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -627,11 +627,11 @@ func TestIntegrationAuth(t *testing.T) {
assert.Contains(t, resp.Header.Get("Hx-Trigger"), "An activation link has been send to your email") assert.Contains(t, resp.Header.Get("Hx-Trigger"), "An activation link has been send to your email")
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE email = ? AND email_verified = FALSE", "mail@mail.de").Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE email = ? AND email_verified = FALSE", "mail@mail.de").Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
var token string var token string
err = db.QueryRow("SELECT t.token FROM token t INNER JOIN user u ON u.user_id = t.user_id WHERE u.email = ? AND t.type = ?", "mail@mail.de", types.TokenTypeEmailVerify).Scan(&token) err = db.QueryRowContext(ctx, "SELECT t.token FROM token t INNER JOIN user u ON u.user_id = t.user_id WHERE u.email = ? AND t.type = ?", "mail@mail.de", types.TokenTypeEmailVerify).Scan(&token)
require.NoError(t, err) require.NoError(t, err)
assert.NotEmpty(t, token) assert.NotEmpty(t, token)
}) })
@@ -644,7 +644,7 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -658,7 +658,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = FALSE", userId).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = FALSE", userId).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
}) })
@@ -670,11 +670,11 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
token := "my-outdated-verifying-token" token := "my-outdated-verifying-token"
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO token (token, user_id, type, created_at, expires_at) INSERT INTO token (token, user_id, type, created_at, expires_at)
VALUES (?, ?, ?, datetime("now", "-16 minute"), datetime("now", "-1 minute"))`, token, userId, types.TokenTypeEmailVerify) VALUES (?, ?, ?, datetime("now", "-16 minute"), datetime("now", "-1 minute"))`, token, userId, types.TokenTypeEmailVerify)
require.NoError(t, err) require.NoError(t, err)
@@ -688,7 +688,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = FALSE", userId).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = FALSE", userId).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
}) })
@@ -700,11 +700,11 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
token := "my-verifying-token" token := "my-verifying-token"
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at) INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
VALUES (?, ?, "", ?, datetime("now"), datetime("now", "+15 minute"))`, token, userId, types.TokenTypeEmailVerify) VALUES (?, ?, "", ?, datetime("now"), datetime("now", "+15 minute"))`, token, userId, types.TokenTypeEmailVerify)
require.NoError(t, err) require.NoError(t, err)
@@ -718,7 +718,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = TRUE", userId).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = TRUE", userId).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
}) })
@@ -747,16 +747,16 @@ func TestIntegrationAuth(t *testing.T) {
sessionId := "session-id" sessionId := "session-id"
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/", nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/dashboard", nil)
require.NoError(t, err) require.NoError(t, err)
req.Header.Set("Cookie", "id="+sessionId) req.Header.Set("Cookie", "id="+sessionId)
resp, err := httpClient.Do(req) resp, err := httpClient.Do(req)
@@ -765,7 +765,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
var csrfToken string var csrfToken string
err = db.QueryRow("SELECT token FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypeCsrf).Scan(&csrfToken) err = db.QueryRowContext(ctx, "SELECT token FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypeCsrf).Scan(&csrfToken)
require.NoError(t, err) require.NoError(t, err)
req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signout", nil) req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signout", nil)
@@ -785,7 +785,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, -1, cookie.MaxAge) assert.Equal(t, -1, cookie.MaxAge)
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, rows) assert.Equal(t, 0, rows)
}) })
@@ -825,13 +825,13 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
sessionId := "session-id" sessionId := "session-id"
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
@@ -871,13 +871,13 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
sessionId := "session-id" sessionId := "session-id"
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
@@ -964,22 +964,22 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, rows) assert.Equal(t, 0, rows)
err = db.QueryRow("SELECT COUNT(*) FROM token WHERE user_id = ?", userId).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM token WHERE user_id = ?", userId).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, rows) assert.Equal(t, 0, rows)
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ?", userId).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ?", userId).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, rows) assert.Equal(t, 0, rows)
err = db.QueryRow("SELECT COUNT(*) FROM account WHERE user_id = ?", userId).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM account WHERE user_id = ?", userId).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, rows) assert.Equal(t, 0, rows)
err = db.QueryRow("SELECT COUNT(*) FROM treasure_chest WHERE user_id = ?", userId).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM treasure_chest WHERE user_id = ?", userId).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, rows) assert.Equal(t, 0, rows)
err = db.QueryRow("SELECT COUNT(*) FROM \"transaction\" WHERE user_id = ?", userId).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM \"transaction\" WHERE user_id = ?", userId).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, rows) assert.Equal(t, 0, rows)
}) })
@@ -1040,13 +1040,13 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
sessionId := "session-id" sessionId := "session-id"
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
@@ -1069,7 +1069,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
}) })
@@ -1080,13 +1080,13 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
sessionId := "session-id" sessionId := "session-id"
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
@@ -1119,7 +1119,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
}) })
@@ -1130,13 +1130,13 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
sessionId := "session-id" sessionId := "session-id"
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
@@ -1169,7 +1169,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
}) })
@@ -1181,21 +1181,21 @@ func TestIntegrationAuth(t *testing.T) {
userIdOther := uuid.New() userIdOther := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
sessionId := "session-id" sessionId := "session-id"
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES ("second", ?, datetime(), datetime("now", "+1 day"))`, userId) VALUES ("second", ?, datetime(), datetime("now", "+1 day"))`, userId)
require.NoError(t, err) require.NoError(t, err)
_, err = db.Exec(` _, err = db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES ("other", ?, datetime(), datetime("now", "+1 day"))`, userIdOther) VALUES ("other", ?, datetime(), datetime("now", "+1 day"))`, userIdOther)
require.NoError(t, err) require.NoError(t, err)
@@ -1232,12 +1232,12 @@ func TestIntegrationAuth(t *testing.T) {
pass = service.GetHashPassword("MyNewSecurePassword1!", []byte("salt")) pass = service.GetHashPassword("MyNewSecurePassword1!", []byte("salt"))
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
var sessionIds []string var sessionIds []string
sessions, err := db.Query(`SELECT session_id FROM session WHERE NOT user_id = ? ORDER BY session_id`, uuid.Nil) sessions, err := db.QueryContext(ctx, `SELECT session_id FROM session WHERE NOT user_id = ? ORDER BY session_id`, uuid.Nil)
require.NoError(t, err) require.NoError(t, err)
for sessions.Next() { for sessions.Next() {
var sessionId string var sessionId string
@@ -1260,13 +1260,13 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := d.Exec(` _, err := d.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
sessionId := "session-id" sessionId := "session-id"
_, err = d.Exec(` _, err = d.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES ("session-id", ?, datetime(), datetime("now", "+1 day"))`, userId) VALUES ("session-id", ?, datetime(), datetime("now", "+1 day"))`, userId)
require.NoError(t, err) require.NoError(t, err)
@@ -1288,7 +1288,7 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := d.Exec(` _, err := d.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -1317,7 +1317,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var rows int var rows int
err = d.QueryRow("SELECT COUNT(*) FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypePasswordReset).Scan(&rows) err = d.QueryRowContext(ctx, "SELECT COUNT(*) FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypePasswordReset).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, rows) assert.Equal(t, 0, rows)
}) })
@@ -1363,7 +1363,7 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := db.Exec(` _, err := db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -1399,7 +1399,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Contains(t, resp.Header.Get("Hx-Trigger"), msg) assert.Contains(t, resp.Header.Get("Hx-Trigger"), msg)
var rows int var rows int
err = db.QueryRow("SELECT COUNT(*) FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypePasswordReset).Scan(&rows) err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypePasswordReset).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
}) })
@@ -1413,7 +1413,7 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := d.Exec(` _, err := d.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -1445,7 +1445,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var rows int var rows int
err = d.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) err = d.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
}) })
@@ -1456,7 +1456,7 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := d.Exec(` _, err := d.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -1473,7 +1473,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.NotEmpty(t, anonymousCsrfToken) assert.NotEmpty(t, anonymousCsrfToken)
token := "password-reset-token" token := "password-reset-token"
_, err = d.Exec(` _, err = d.ExecContext(ctx, `
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at) INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
VALUES (?, ?, ?, ?, datetime("now", "-16 minute"), datetime("now", "-1 minute"))`, token, userId, "", types.TokenTypePasswordReset) VALUES (?, ?, ?, ?, datetime("now", "-16 minute"), datetime("now", "-1 minute"))`, token, userId, "", types.TokenTypePasswordReset)
require.NoError(t, err) require.NoError(t, err)
@@ -1494,7 +1494,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var rows int var rows int
err = d.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) err = d.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
}) })
@@ -1505,7 +1505,7 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := d.Exec(` _, err := d.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
@@ -1522,7 +1522,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.NotEmpty(t, anonymousCsrfToken) assert.NotEmpty(t, anonymousCsrfToken)
token := "password-reset-token" token := "password-reset-token"
_, err = d.Exec(` _, err = d.ExecContext(ctx, `
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at) INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
VALUES (?, ?, ?, ?, datetime("now"), datetime("now", "+15 minute"))`, token, userId, "", types.TokenTypePasswordReset) VALUES (?, ?, ?, ?, datetime("now"), datetime("now", "+15 minute"))`, token, userId, "", types.TokenTypePasswordReset)
require.NoError(t, err) require.NoError(t, err)
@@ -1543,7 +1543,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var rows int var rows int
err = d.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows) err = d.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, rows) assert.Equal(t, 1, rows)
}) })
@@ -1554,12 +1554,12 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
pass := service.GetHashPassword("password", []byte("salt")) pass := service.GetHashPassword("password", []byte("salt"))
_, err := d.Exec(` _, err := d.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
_, err = d.Exec(` _, err = d.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES ("session-id", ?, datetime(), datetime("now", "+1 day"))`, userId) VALUES ("session-id", ?, datetime(), datetime("now", "+1 day"))`, userId)
require.NoError(t, err) require.NoError(t, err)
@@ -1590,7 +1590,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
var token string var token string
err = d.QueryRow("SELECT token FROM token WHERE type = ?", types.TokenTypePasswordReset).Scan(&token) err = d.QueryRowContext(ctx, "SELECT token FROM token WHERE type = ?", types.TokenTypePasswordReset).Scan(&token)
require.NoError(t, err) require.NoError(t, err)
formData = url.Values{ formData = url.Values{
@@ -1608,7 +1608,7 @@ func TestIntegrationAuth(t *testing.T) {
_ = resp.Body.Close() _ = resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
sessions, err := d.Query("SELECT session_id FROM session WHERE user_id = ?", userId) sessions, err := d.QueryContext(ctx, "SELECT session_id FROM session WHERE user_id = ?", userId)
require.NoError(t, err) require.NoError(t, err)
assert.False(t, sessions.Next()) assert.False(t, sessions.Next())
}) })
@@ -1623,11 +1623,11 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
sessionId := "session-id" sessionId := "session-id"
_, err := d.Exec(` _, err := d.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
_, err = d.Exec(` _, err = d.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime("now", "-8 hour"), datetime("now", "-1 minute"))`, sessionId, userId) VALUES (?, ?, datetime("now", "-8 hour"), datetime("now", "-1 minute"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
@@ -1643,7 +1643,7 @@ func TestIntegrationAuth(t *testing.T) {
assert.NotEqual(t, sessionId, newSession.Value) assert.NotEqual(t, sessionId, newSession.Value)
var rows int var rows int
err = d.QueryRow("SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows) err = d.QueryRowContext(ctx, "SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, rows) assert.Equal(t, 0, rows)
}) })
@@ -1670,11 +1670,11 @@ func TestIntegrationAuth(t *testing.T) {
userId := uuid.New() userId := uuid.New()
sessionId := "session-id" sessionId := "session-id"
_, err := d.Exec(` _, err := d.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt")) VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
require.NoError(t, err) require.NoError(t, err)
_, err = d.Exec(` _, err = d.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at) INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, datetime("now", "-8 hour"), datetime("now", "-1 minute"))`, sessionId, userId) VALUES (?, ?, datetime("now", "-8 hour"), datetime("now", "-1 minute"))`, sessionId, userId)
require.NoError(t, err) require.NoError(t, err)
@@ -1769,7 +1769,7 @@ func TestIntegrationAccount(t *testing.T) {
_ = resp.Body.Close() _ = resp.Body.Close()
var id uuid.UUID var id uuid.UUID
err = db.Get(&id, "SELECT id FROM account") err = db.GetContext(ctx, &id, "SELECT id FROM account")
require.NoError(t, err) require.NoError(t, err)
// Update // Update
@@ -1862,7 +1862,6 @@ func TestIntegrationAccount(t *testing.T) {
">": 400, ">": 400,
"/": 400, "/": 400,
"\\": 400, "\\": 400,
"?": 400,
":": 400, ":": 400,
"*": 400, "*": 400,
"|": 400, "|": 400,

View File

@@ -22,7 +22,7 @@ func TestTreasureChestShouldNotDeleteIfTransactionRecurringExists(t *testing.T)
assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
var parentId string var parentId string
err := db.Get(&parentId, "SELECT id FROM treasure_chest") err := db.GetContext(ctx, &parentId, "SELECT id FROM treasure_chest")
require.NoError(t, err) require.NoError(t, err)
formData = url.Values{ formData = url.Values{
@@ -33,7 +33,7 @@ func TestTreasureChestShouldNotDeleteIfTransactionRecurringExists(t *testing.T)
assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
var childId string var childId string
err = db.Get(&childId, "SELECT id FROM treasure_chest WHERE parent_id = ?", parentId) err = db.GetContext(ctx, &childId, "SELECT id FROM treasure_chest WHERE parent_id = ?", parentId)
require.NoError(t, err) require.NoError(t, err)
formData = url.Values{ formData = url.Values{