305 Commits

Author SHA1 Message Date
8818f70f73 fix(deps): update module golang.org/x/net to v0.43.0
Some checks failed
Build Docker Image / Build-Docker-Image (push) Has been cancelled
2025-08-08 22:11:08 +00:00
739e216106 chore(deps): update node.js to 3218f0d
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m12s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m49s
2025-08-04 16:08:17 +00:00
32093bf087 chore(deps): update node.js to 0d98a9f
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m11s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m1s
2025-08-04 13:05:24 +00:00
09e1ad32a0 chore(deps): update node.js to v22.18.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m4s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m51s
2025-08-04 10:05:49 +00:00
2783a83015 fix(deps): update module github.com/prometheus/client_golang to v1.23.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m39s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m42s
2025-07-31 11:06:56 +00:00
eb33bb51cc 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 3m42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m25s
2025-07-30 13:07:14 +00:00
79980c0df3 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 5m31s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m34s
2025-07-26 16:06:19 +00:00
488d57d0d3 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 3m49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m30s
2025-07-24 14:06:50 +00:00
069e528e7e chore(deps): update node.js to 37ff334
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 3m22s
2025-07-24 07:06:20 +00:00
64d32706ab chore(deps): update golang:1.24.5 docker digest to ef5b4be
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m14s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m10s
2025-07-24 00:06:19 +00:00
d570f73aa1 chore(deps): update node.js to e515259
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m23s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m30s
2025-07-23 22:29:35 +00:00
a97de92b55 chore(deps): update golang:1.24.5 docker digest to a3bb6cd
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m4s
2025-07-23 21:47:50 +00:00
22ad7cd52d chore(deps): update golang:1.24.5 docker digest to fdcd2e5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m59s
2025-07-22 15:09:32 +00:00
4c68b58176 chore(deps): update node.js to 079b6a6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m36s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m45s
2025-07-22 07:06:44 +00:00
180c3b0267 chore(deps): update golang:1.24.5 docker digest to a98400b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m54s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m15s
2025-07-22 06:07:11 +00:00
3b0a08f84c chore(deps): update debian:12.11 docker digest to b6507e3
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 3m40s
2025-07-22 04:06:46 +00:00
354fe8df43 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 3m51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m30s
2025-07-20 19:07:22 +00:00
6d32821058 chore(deps): update node.js to 9e6918e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m52s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m44s
2025-07-17 01:06:26 +00:00
e19e38efe5 chore(deps): update node.js to 414e20e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m55s
2025-07-16 22:10:18 +00:00
56d25e0e51 chore(deps): update node.js to v22.17.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m58s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m47s
2025-07-16 19:09:19 +00:00
4e790e51e7 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 3m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m35s
2025-07-10 20:07:04 +00:00
9ce7c8f37a 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 3m40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m38s
2025-07-10 18:07:40 +00:00
f356f525f9 chore(deps): update golang:1.24.5 docker digest to 14fd8a5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m29s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m57s
2025-07-09 21:05:52 +00:00
36ddf98c0f chore(deps): update golang docker tag to v1.24.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m46s
2025-07-09 19:05:56 +00:00
59b4e332fe chore(deps): update dependency go to v1.24.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m52s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m47s
2025-07-08 17:15:52 +00:00
dc78af68f9 chore(deps): update node.js to 2fa6c97
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m41s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m48s
2025-07-08 04:05:39 +00:00
15495c247e chore(deps): update node.js to 5307f5f
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m34s
2025-07-08 01:39:56 +00:00
799a20ceea chore(deps): update node.js to df39165
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 3m37s
2025-07-07 22:06:36 +00:00
67ce0a351a chore(deps): update golang:1.24.4 docker digest to 20a022e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m21s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m12s
2025-07-02 12:05:21 +00:00
e3b3e4de7e chore(deps): update golang:1.24.4 docker digest to 764d7e0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m14s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m56s
2025-07-02 00:06:48 +00:00
478aebbbc6 chore(deps): update golang:1.24.4 docker digest to a92f3b1
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 3m7s
2025-07-01 21:05:40 +00:00
f68d6c14b4 chore(deps): update golang:1.24.4 docker digest to 1aa97dd
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m14s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m51s
2025-07-01 18:05:40 +00:00
2dc02331d2 chore(deps): update golang:1.24.4 docker digest to 1bb140b
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 2m17s
2025-07-01 15:05:47 +00:00
d884b9066f chore(deps): update golang:1.24.4 docker digest to 270cd53
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 2m19s
2025-07-01 06:54:59 +00:00
dbd03c16b9 chore(deps): update debian:12.11 docker digest to d42b86d
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m4s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m18s
2025-07-01 04:06:33 +00:00
2bc8fd74a7 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 4m50s
2025-06-27 18:01:04 +00:00
69034228f2 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:56:35 +00:00
800a13c558 chore(deps): update tailwindcss monorepo to v4.1.11
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 4m29s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-06-27 17:27:11 +00:00
45ad21080b chore(deps): update dependency htmx.org to v2.0.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 4m16s
2025-06-27 16:35:20 +00:00
43b994cba9 chore(deps): update dependency htmx.org to v2.0.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m19s
2025-06-20 22:07:22 +00:00
6b971f666b chore(deps): update node.js to 71bcbb3
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 4m51s
2025-06-12 10:09:36 +00:00
a96e833000 chore(deps): update golang:1.24.4 docker digest to 10c1318
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m29s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m34s
2025-06-12 09:05:46 +00:00
81b48d9bdb chore(deps): update node.js to 68cf33c
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m29s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m28s
2025-06-12 04:06:22 +00:00
60d39fe764 chore(deps): update node.js to 2040569
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m3s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m15s
2025-06-12 02:06:40 +00:00
95e3d10841 chore(deps): update golang:1.24.4 docker digest to 3178db8
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m23s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m30s
2025-06-12 00:07:00 +00:00
82f0b6ec94 chore(deps): update tailwindcss monorepo to v4.1.10
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m22s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m29s
2025-06-11 23:07:31 +00:00
c0775a5f2d chore(deps): update node.js to f627d0e
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 4m28s
2025-06-11 22:08:48 +00:00
6ec38f2f22 chore(deps): update golang:1.24.4 docker digest to 884849e
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m2s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m24s
2025-06-11 21:07:01 +00:00
4e55c6bf69 chore(deps): update tailwindcss monorepo to v4.1.9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m5s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m23s
2025-06-11 16:07:46 +00:00
1ddb953c59 chore(deps): update golang:1.24.4 docker digest to dc3de88
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m56s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m29s
2025-06-11 15:07:21 +00:00
9c27d2ae8d chore(deps): update node.js to 6a2972b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m29s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m48s
2025-06-11 04:10:49 +00:00
d5839a53b6 chore(deps): update golang:1.24.4 docker digest to d1db785
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 7m5s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m59s
2025-06-11 03:06:41 +00:00
612ac20731 chore(deps): update debian:12.11 docker digest to 0d8498a
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m8s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m20s
2025-06-11 02:18:12 +00:00
b05dd05a28 chore(deps): update golang:1.24.4 docker digest to 01f861b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m31s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m29s
2025-06-11 00:06:00 +00:00
c61911c250 fix(deps): update module github.com/a-h/templ to v0.3.898
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m8s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m29s
2025-06-06 18:06:37 +00:00
e8ace5836a fix(deps): update module golang.org/x/net to v0.41.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m57s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m6s
2025-06-05 22:09:18 +00:00
3b0c80341d chore(deps): update golang docker tag to v1.24.4
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m10s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m18s
2025-06-05 21:08:22 +00:00
00c39436fc fix(deps): update module golang.org/x/crypto to v0.39.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m49s
2025-06-05 20:06:58 +00:00
6894b0d39c chore(deps): update dependency go to v1.24.4
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m27s
2025-06-05 19:06:29 +00:00
a416d829ff fix(deps): update module github.com/a-h/templ to v0.3.894
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 3m26s
2025-06-04 22:06:58 +00:00
c6a884619e fix(deps): update module github.com/a-h/templ to v0.3.887
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m19s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m5s
2025-05-31 21:07:44 +00:00
daa8b93cde chore(deps): update golang:1.24.3 docker digest to 81bf592
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m28s
2025-05-31 03:07:02 +00:00
6c634e49e8 chore(deps): update tailwindcss monorepo to v4.1.8
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m15s
2025-05-28 16:08:11 +00:00
f9a5f1e0fa chore(deps): update node.js to 0b5b940
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 3m37s
2025-05-24 14:08:29 +00:00
8de973be8b chore(deps): update golang:1.24.3 docker digest to 4c0a181
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m5s
2025-05-24 13:01:57 +00:00
2f47b4b91f chore(deps): update node.js to 74066d0
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 4m57s
2025-05-22 10:08:03 +00:00
95df5979c3 chore(deps): update golang:1.24.3 docker digest to 02a2275
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 7m4s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m34s
2025-05-22 09:07:29 +00:00
6468ff7be9 chore(deps): update golang:1.24.3 docker digest to 1bcf884
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m14s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m9s
2025-05-22 03:16:15 +00:00
7ea56f2a40 chore(deps): update debian docker tag to v12.11
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 5m21s
2025-05-22 02:08:28 +00:00
29da9fd893 chore(deps): update node.js to 6e62aab
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 4m45s
2025-05-22 01:20:20 +00:00
183039a261 chore(deps): update node.js to v22.16.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 7m36s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m39s
2025-05-21 22:09:24 +00:00
725e34ad1c chore(deps): update node.js to e558507
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 4m33s
2025-05-16 02:10:47 +00:00
ca06ef7f5b chore(deps): update node.js to v22.15.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m38s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m43s
2025-05-15 22:11:37 +00:00
0021527004 chore(deps): update tailwindcss monorepo to v4.1.7
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m21s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m31s
2025-05-15 15:07:59 +00:00
6d2da1d1ea chore(deps): update golang:1.24.3 docker digest to 86b4cff
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m35s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m22s
2025-05-15 00:07:12 +00:00
3ceceb51f5 chore(deps): update tailwindcss monorepo to v4.1.6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m36s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m34s
2025-05-09 14:08:15 +00:00
680c717745 chore(deps): update golang docker tag to v1.24.3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m53s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m41s
2025-05-06 22:09:10 +00:00
f269fa4fc9 chore(deps): update dependency go to v1.24.3
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 4m40s
2025-05-06 21:08:35 +00:00
5fae3242de fix(deps): update module golang.org/x/net to v0.40.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m54s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m3s
2025-05-05 21:08:40 +00:00
2e3a7d0c8a fix(deps): update module golang.org/x/crypto to v0.38.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 5m57s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m19s
2025-05-05 20:07:58 +00:00
e9c36be023 fix(deps): update module github.com/a-h/templ to v0.3.865
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m23s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m24s
2025-05-02 08:10:34 +00:00
ba3cd45e4a chore(deps): update tailwindcss monorepo to v4.1.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m7s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m28s
2025-04-30 16:10:12 +00:00
11b91add7e chore(deps): update node.js to a1f1274
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m27s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m13s
2025-04-30 10:09:01 +00:00
b4edbcf505 chore(deps): update golang:1.24.2 docker digest to 30baaea
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m24s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m39s
2025-04-30 06:07:27 +00:00
5b4bc21f2c chore(deps): update node.js to c9397a5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m20s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m5s
2025-04-29 22:08:06 +00:00
b7c4393663 chore(deps): update golang:1.24.2 docker digest to 3a060d6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m35s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m26s
2025-04-29 18:07:09 +00:00
39c0be5697 chore(deps): update node.js to f57e74d
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m44s
2025-04-29 16:06:55 +00:00
86aed80f31 chore(deps): update node.js to 012715b
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m27s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m21s
2025-04-29 13:07:32 +00:00
9ce77301e1 chore(deps): update golang:1.24.2 docker digest to f52b85c
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 4m28s
2025-04-29 12:07:01 +00:00
990a310c3a chore(deps): update golang:1.24.2 docker digest to 065cb8c
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m57s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m37s
2025-04-29 09:09:40 +00:00
d7a98b10fe chore(deps): update golang:1.24.2 docker digest to 8131d99
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m9s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m9s
2025-04-29 05:07:44 +00:00
30501b72c2 chore(deps): update node.js to 120a74c
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m19s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m11s
2025-04-29 04:08:12 +00:00
68adbf7216 chore(deps): update debian:12.10 docker digest to 264982f
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 7m50s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m9s
2025-04-29 02:07:06 +00:00
e245bc99ec fix(deps): update module github.com/golang-migrate/migrate/v4 to v4.18.3
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 4m22s
2025-04-24 05:08:14 +00:00
58f70ac285 chore(deps): update node.js to 473b436
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m8s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m17s
2025-04-23 19:07:36 +00:00
c3fd33fd6b chore(deps): update node.js to v22.15.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m23s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m24s
2025-04-23 16:10:58 +00:00
be22d1f14d chore(deps): update golang:1.24.2 docker digest to d9db321
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m21s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m55s
2025-04-18 06:07:41 +00:00
430843e29e fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.28
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m8s
2025-04-16 14:08:31 +00:00
fbb2382bf1 chore(deps): update tailwindcss monorepo to v4.1.4
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 3m45s
2025-04-14 18:08:54 +00:00
dfe3bb9319 chore(deps): update golang:1.24.2 docker digest to 1ecc479
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 5m46s
2025-04-10 21:06:46 +00:00
129ca3c970 chore(deps): update golang:1.24.2 docker digest to 18a1f2d
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 4m24s
2025-04-09 21:06:59 +00:00
f4621eafa2 chore(deps): update golang:1.24.2 docker digest to 1ecc479
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m39s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m46s
2025-04-09 12:10:17 +00:00
404f00de45 chore(deps): update golang:1.24.2 docker digest to 227d106
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m26s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m46s
2025-04-09 06:07:55 +00:00
be01691ecc chore(deps): update node.js to e5ddf89
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m20s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m39s
2025-04-09 04:07:31 +00:00
71d1ee4df6 chore(deps): update golang:1.24.2 docker digest to c0b66cf
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m26s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m40s
2025-04-09 00:07:17 +00:00
a9d948261a chore(deps): update node.js to 4a126f3
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 4m45s
2025-04-08 19:07:40 +00:00
858394672d chore(deps): update golang:1.24.2 docker digest to fb224f9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m29s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m30s
2025-04-08 18:10:46 +00:00
09943c12b2 chore(deps): update node.js to cb930e4
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 4m41s
2025-04-08 16:07:12 +00:00
ae309f4cd1 chore(deps): update golang:1.24.2 docker digest to b51b7be
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 4m41s
2025-04-08 12:07:13 +00:00
1a4660f954 fix(deps): update module github.com/prometheus/client_golang to v1.22.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 4m32s
2025-04-08 10:09:02 +00:00
ff0c16a9d3 chore(deps): update golang:1.24.2 docker digest to 37b19a8
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 9m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m44s
2025-04-08 09:08:13 +00:00
f519383e70 chore(deps): update node.js to 89b8653
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 4m42s
2025-04-08 07:07:40 +00:00
517ba5b632 chore(deps): update golang:1.24.2 docker digest to b665273
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 7m20s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m42s
2025-04-08 06:07:46 +00:00
f780a495a2 chore(deps): update debian:12.10 docker digest to 00cd074
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 4m17s
2025-04-08 04:08:00 +00:00
e6cda6ce14 fix(deps): update module golang.org/x/net to v0.39.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m37s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m51s
2025-04-07 22:07:53 +00:00
9f54d47e2f fix(deps): update module golang.org/x/crypto to v0.37.0
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 3m32s
2025-04-06 17:07:31 +00:00
111410856a chore(deps): update tailwindcss monorepo to v4.1.3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 6m2s
2025-04-04 19:24:44 +00:00
a42cb74355 chore(deps): update tailwindcss monorepo to v4.1.2
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 4m36s
2025-04-03 18:07:27 +00:00
29b96a3fff fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.27
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 4m46s
2025-04-02 17:08:20 +00:00
05e6deeedd chore(deps): update tailwindcss monorepo to v4.1.1
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 4m36s
2025-04-02 10:08:57 +00:00
1f8e1a510f fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.26
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 4m33s
2025-04-02 09:08:10 +00:00
46b1f9d867 chore(deps): update tailwindcss monorepo to v4.1.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m49s
2025-04-01 19:11:17 +00:00
d9eddeed97 chore(deps): update golang docker tag to v1.24.2
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 8m1s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m49s
2025-04-01 18:08:17 +00:00
5345093d50 chore(deps): update dependency go to v1.24.2
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 4m51s
2025-04-01 17:07:37 +00:00
b81d156181 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.25
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 4m37s
2025-04-01 14:07:31 +00:00
1f52a959f2 fix(deps): update module golang.org/x/net to v0.38.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 8m17s
2025-03-30 00:10:40 +00:00
d6aa5c08a2 fix(deps): update module github.com/a-h/templ to v0.3.857
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m37s
2025-03-28 20:14:44 +00:00
7feaa0286b chore(deps): update tailwindcss monorepo to v4.0.17
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 4m18s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-03-28 00:11:25 +00:00
dce993322b fix(deps): update module github.com/a-h/templ to v0.3.856
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m50s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m34s
2025-03-24 00:10:56 +00:00
873dbd00be chore(deps): update tailwindcss monorepo to v4.0.15
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 4m36s
2025-03-21 00:08:25 +00:00
e29b31f25e chore(deps): update debian docker tag to v12.10
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m20s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 5m0s
2025-03-20 11:41:17 +00:00
9534954bcb chore(deps): update node.js to c7fd844
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m8s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m45s
2025-03-20 08:20:55 +00:00
8ae8de3a03 chore(deps): update golang:1.24.1 docker digest to 52ff1b3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m18s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m30s
2025-03-20 00:10:14 +00:00
db8834f9eb chore(deps): update golang:1.24.1 docker digest to fa145a3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m59s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m13s
2025-03-15 00:06:52 +00:00
348082ad96 chore(deps): update tailwindcss monorepo to v4.0.14
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m9s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m54s
2025-03-14 22:27:01 +00:00
b7cd0c5997 chore(deps): update tailwindcss monorepo to v4.0.12
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m6s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m41s
2025-03-08 00:11:05 +00:00
fbb20bada4 fix(deps): update module golang.org/x/net to v0.37.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m26s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m35s
2025-03-07 00:10:32 +00:00
da82680270 fix(deps): update module golang.org/x/crypto to v0.36.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m23s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m50s
2025-03-06 00:10:47 +00:00
1168cb5c9f fix(deps): update module golang.org/x/net to v0.36.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m52s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m4s
2025-03-05 10:00:16 +00:00
b165e29e45 fix(deps): update module github.com/prometheus/client_golang to v1.21.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m53s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m1s
2025-03-05 09:40:58 +00:00
81ec91a73f chore(deps): update golang docker tag to v1.24.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m35s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m53s
2025-03-05 09:06:47 +00:00
8f14b93817 chore(deps): update dependency go to v1.24.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m3s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m51s
2025-03-05 00:09:17 +00:00
02fc5f9baa fix(deps): update module golang.org/x/crypto to v0.35.0
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m32s
2025-03-04 13:23:59 +00:00
27b87dcc12 chore(deps): update dependency go to v1.24.0
Some checks are pending
Build Docker Image / Build-Docker-Image (push) Successful in 3m10s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Waiting to run
2025-03-04 12:44:41 +00:00
ef4e314475 chore(deps): pin golang docker tag to 3f74443
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m38s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m51s
2025-03-04 11:42:42 +00:00
a6794cdfed feat(deps): update go compiler to 1.24
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 2m16s
2025-03-04 12:17:17 +01:00
38b3ad9326 fix(deps): update module golang.org/x/net to v0.35.0
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m4s
2025-03-03 10:02:17 +00:00
a6f5710521 chore(deps): update node.js to v22.14.0
Some checks failed
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-03-03 09:59:59 +00:00
cb0252e1af chore(deps): update tailwindcss monorepo to v4.0.9
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 2m48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-03-03 00:06:37 +00:00
f2937a762e chore(deps): update debian:12.9 docker digest to 3528682
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m12s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m28s
2025-03-02 18:24:21 +00:00
60daac48b4 fix(deps): update module github.com/prometheus/client_golang to v1.21.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 11m2s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 4m24s
2025-02-24 18:28:38 +00:00
b2a655f73a chore(deps): update tailwindcss monorepo to v4.0.8
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 9m58s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Failing after 10m23s
2025-02-24 16:18:54 +00:00
663081d719 chore(deps): update node.js to 5145c88
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 3m39s
2025-02-24 12:34:43 +00:00
28460a6bac fix: make tests more resilient
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 8m28s
2025-02-24 12:58:48 +01:00
3039d66295 feat(docs): update readme
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 2m54s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Failing after 2m16s
2025-02-23 21:46:31 +01:00
9b96e8f0a5 chore(deps): update debian:12.9 docker digest to 4abf773
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m6s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m58s
2025-02-11 14:09:06 +00:00
b86b737a82 chore(deps): update golang:1.23.5 docker digest to e213430
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 6m58s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m14s
2025-02-09 00:11:23 +00:00
f2951985c2 fix(deps): migrate tailwindcss to v4 and remove daisyui
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m37s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m47s
2025-02-03 23:18:51 +01:00
a88ed4bb47 fix(deps): update module github.com/golang-migrate/migrate/v4 to v4.18.2
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 56s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m37s
2025-02-02 10:14:35 +00:00
7ac910aec6 fix(deps): update module github.com/a-h/templ to v0.3.833
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m53s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m3s
2025-02-02 00:07:44 +00:00
15ccd4ef01 chore(deps): update node.js to v22.13.1
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m2s
2025-01-26 01:10:32 +01:00
54f8082430 chore(deps): update golang:1.23.5 docker digest to 8c10f21
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 1m55s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2025-01-26 00:05:02 +00:00
0d5143b91b chore(deps): update golang docker tag to v1.23.5
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2025-01-20 04:52:37 +01:00
3d094154ce chore(deps): update debian docker tag to v12.9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 54s
2025-01-19 21:03:17 +00:00
3d1111256c chore(deps): update golang:1.23.4 docker digest to 9820aca
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m2s
2025-01-19 20:38:44 +01:00
bc82ad123b chore(deps): update dependency go to v1.23.5
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2025-01-19 01:11:32 +01:00
cb01d5e0d4 chore(deps): update node.js to fa54405
Some checks are pending
Build Docker Image / Build-Docker-Image (push) Successful in 49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Waiting to run
2025-01-19 00:05:01 +00:00
7cb46aad36 chore(deps): update node.js to 816f04d
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m50s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 53s
2025-01-14 20:21:01 +00:00
92bb836e87 chore(deps): update node.js to v22.13.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 53s
2025-01-08 23:05:17 +00:00
1d89f45ff9 fix(deps): update module golang.org/x/net to v0.34.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2025-01-07 23:06:18 +00:00
bc70babaca fix(deps): update module golang.org/x/crypto to v0.32.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m20s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 54s
2025-01-06 23:04:59 +00:00
d3700d5a3b fix(deps): update module github.com/a-h/templ to v0.3.819
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m56s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 55s
2025-01-02 23:05:26 +00:00
9a8dfc96db chore: #174 update readme
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 57s
2024-12-31 13:23:01 +01:00
52f6d3d706 chore: #174 make into template
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
2024-12-31 12:25:30 +01:00
508aa3038b feat(observability): #360 remove umami to reduce complexity
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 53s
2024-12-31 12:03:59 +01:00
0b155af4c9 chore(deps): update dependency daisyui to v4.12.23
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 53s
2024-12-27 00:08:13 +01:00
917218da82 chore(deps): update golang:1.23.4 docker digest to 7ea4c9d
Some checks are pending
Build Docker Image / Build-Docker-Image (push) Successful in 48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Waiting to run
2024-12-26 23:02:20 +00:00
fe7f01e035 chore(deps): update node.js to 0e910f4
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-25 23:02:10 +00:00
55408da398 chore(auth): #331 add and fix forgot password actual tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-25 23:13:58 +01:00
b0f183aeed chore(auth): #331 add and fix forgot password tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m9s
2024-12-25 22:58:37 +01:00
42a910df4b chore(deps): update node.js to 7bea049
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-25 22:26:02 +01:00
73333256c5 chore(deps): update golang:1.23.4 docker digest to b01f7c7
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-25 21:21:30 +00:00
14b477f560 chore(auth): #331 add change password tests
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-25 22:20:52 +01:00
87188724ac chore(deps): update debian:12.8 docker digest to b877a1a
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 54s
2024-12-25 20:58:34 +00:00
5ea400352f chore(auth): #331 add sign up verify tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 53s
2024-12-25 21:56:32 +01:00
397442767a chore(auth): #331 implement and fix sign up tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-24 22:39:26 +01:00
9462f8b245 chore(auth): #331 implement and fix fist sign up tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-23 22:57:41 +01:00
7a7d7cf204 chore(auth): #331 remove duplicated/outdated tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-23 22:36:55 +01:00
96b4cc6889 chore(auth): #331 add tests for sign in
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 54s
2024-12-23 22:33:10 +01:00
7a9d34d464 chore(auth): #331 add tests for sign in
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 53s
2024-12-23 13:56:41 +01:00
52cd85d904 chore(auth): #331 add tests for sign out
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
Build Docker Image / Build-Docker-Image (push) Successful in 46s
2024-12-22 23:48:53 +01:00
fb6cc0acda chore(auth): #331 add tests for delete account
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-22 23:07:15 +01:00
6a551929c5 chore(auth): #331 add and fix session tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 48s
2024-12-22 22:33:17 +01:00
ea653f0087 chore(auth): #331 unify existing tests
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-22 21:31:19 +01:00
143662fff0 fix(deps): update module golang.org/x/net to v0.33.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 52s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-18 23:01:53 +00:00
fdb955f20c feat: #337 unify types for auth module
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-18 23:44:59 +01:00
dcc5207272 feat(security): #328 delete old sessions for change and forgot password
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-18 22:56:51 +01:00
43d0a3d022 chore(test): add test for cache control and security headers 2024-12-18 22:56:48 +01:00
c48194c36f chore(deps): update dependency tailwindcss to v3.4.17
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-17 23:02:08 +00:00
23aa3d4b0e chore(test): add test for cache control and security headers
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-16 23:04:22 +01:00
9bb603970d chore(test): fix integration test 'waitForReady'
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-16 22:38:59 +01:00
6d3902e572 chore(test): update integration test setup to automatically generate ports
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-16 17:30:33 +01:00
88892ab6ca chore(deps): update dependency htmx.org to v2.0.4
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 48s
2024-12-13 19:01:26 +00:00
28a97414d4 chore(deps): update dependency daisyui to v4.12.22
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m8s
2024-12-13 02:02:37 +00:00
f0ec293be8 feat(security): #314 include hsts
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 48s
2024-12-12 21:50:32 +01:00
1ad694ce2b feat(security): #314 include all proposed security headers
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 47s
2024-12-12 21:37:23 +01:00
60fe2789cc feat(security): #312 disable autofill for PII information
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-12 21:03:14 +01:00
5d83c9dcc0 chore(deps): update dependency daisyui to v4.12.21
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-12 16:01:32 +00:00
a8937a0e64 chore(deps): update golang:1.23.4 docker digest to 7003184
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-12 01:01:46 +00:00
9629e71962 fix(deps): update module golang.org/x/net to v0.32.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-12-12 00:01:42 +00:00
380dd979f6 feat(security): #305 don't cache sensitive data
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 47s
2024-12-12 00:02:55 +01:00
e81fa4b2b6 fix(deps): update module golang.org/x/crypto to v0.31.0
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-11 23:53:04 +01:00
5579e5da0c chore(deps): update dependency daisyui to v4.12.20
Some checks are pending
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Waiting to run
2024-12-11 22:48:00 +00:00
12d7c13b02 feat(security): #286 use csrf token for delete request
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-11 15:49:11 +01:00
8cf2210aaf feat(security): #286 fix mail sending
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-09 23:36:08 +01:00
eab42c26f8 feat(security): #286 anonymous sign in for csrf token on login form
Some checks failed
Build Docker Image / Build-Docker-Image (push) Failing after 11m7s
2024-12-09 22:39:18 +01:00
57989c9b03 feat(security): #286 implement csrf middleware 2024-12-09 22:39:03 +01:00
bbcdbf7a01 fix(deps): update module golang.org/x/crypto to v0.30.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 47s
2024-12-06 22:04:10 +00:00
93fd45117d chore(deps): update node.js to v22.12.0
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-06 23:01:54 +01:00
fb20672105 chore(deps): update golang docker tag to v1.23.4
Some checks are pending
Build Docker Image / Build-Docker-Image (push) Successful in 1m42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Waiting to run
2024-12-06 21:55:51 +00:00
5ef59df2d0 fix: remove redundante names
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-04 23:34:59 +01:00
2d5f42bb28 fix: move middleware to handlers, as it belongs there
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 48s
2024-12-04 23:12:39 +01:00
17899cbf3d fix: restructure auth handler
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 48s
Build Docker Image / Build-Docker-Image (push) Successful in 41s
2024-12-04 23:04:54 +01:00
adae04b83e chore(deps): update dependency tailwindcss to v3.4.16
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-04 22:26:22 +01:00
832e106b2d chore(deps): update node.js to ec878c7
Some checks are pending
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Waiting to run
2024-12-04 21:20:59 +00:00
3ee26cd32b fix: remove db utils
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-04 22:13:59 +01:00
521119fc02 fix: refine logging
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 48s
2024-12-04 22:04:32 +01:00
5198487feb fix: remove logging from utils
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-12-04 21:23:19 +01:00
9e8e595258 fix: extract html rendering
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 49s
2024-12-04 21:12:01 +01:00
01ceb9eb33 chore(deps): update debian:12.8 docker digest to 17122fe
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-12-03 23:31:48 +01:00
9ed0721b78 chore(deps): update golang:1.23.3 docker digest to e5ca199
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 47s
2024-12-03 22:22:18 +00:00
1f8c4a39b4 fix(quality): extract logic from database layer
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 47s
2024-12-03 23:17:11 +01:00
0accb49871 fix(quality): switch linter and fix errors 2024-12-03 23:17:11 +01:00
7c67720621 chore(deps): remove dependencies from handler package 2024-12-03 23:17:06 +01:00
48ec7b64ac chore(deps): remove dependencies from handler package
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 48s
2024-12-01 21:50:22 +01:00
e201ac7b2c feat: run staticcheck during build and fix errors
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 46s
Build Docker Image / Build-Docker-Image (push) Successful in 41s
2024-11-29 21:24:51 +01:00
a62f0fb037 fix(security): remove sec-fetch filter because it prohibited page reloads
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 38s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 43s
2024-11-24 21:52:34 +01:00
8ee4c1ede4 fix(security): fix sec-fetch filter
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 45s
2024-11-24 21:35:09 +01:00
3188d25d7d fix(deps): update module github.com/stretchr/testify to v1.10.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 43s
2024-11-24 14:55:58 +00:00
983354dec4 chore(build): add mocks package for renovate
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 44s
2024-11-24 15:53:44 +01:00
841c8be63d feat(security): #278 update csp directives
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 39s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 45s
2024-11-23 21:51:20 +01:00
d752de0447 feat(security): #273 enable coop
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 43s
2024-11-23 21:33:13 +01:00
b1af29633a feat(security): #273 filter sec-fetch
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 37s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 43s
2024-11-23 21:30:02 +01:00
6a36eb0580 feat(security): #273 enable corp
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 40s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 43s
2024-11-23 21:26:22 +01:00
ae32bf7232 feat(security): #273 disable frame-ancestors
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 42s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 45s
2024-11-23 12:52:52 +01:00
9ab78b6cc8 fix(middleware): define middleware in a more streamlined way
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m26s
2024-11-20 09:51:19 +01:00
003ccbe035 fix(auth): fix panic due to null reference
Some checks failed
Build Docker Image / Build-Docker-Image (push) Successful in 39s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Has been cancelled
2024-11-20 09:48:41 +01:00
15a53ed8cd chore(build): speed up build by utilizing docker cache
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 38s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 45s
2024-11-19 22:32:36 +01:00
925041ef29 feat(security): #263 securtiy options for htmx
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-11-19 22:25:21 +01:00
35fea60750 feat(security): enable Content-Security-Plolicy for external js
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 51s
2024-11-19 22:04:43 +01:00
641185919c feat(security): enable Content-Security-Plolicy for external js
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s
2024-11-19 22:01:12 +01:00
45a1cbdfd4 feat(security): enable Content-Security-Plolicy #263
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
2024-11-19 21:52:44 +01:00
490d858508 fix(workout): parse date failed
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 55s
2024-11-19 21:45:49 +01:00
6670d1d440 fix(build): fix branch name
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 55s
2024-11-16 21:42:28 +01:00
47c079749b fix(build): use npm clean-install to always use lock file #259
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
2024-11-16 21:40:07 +01:00
3cfa4d4c91 chore(deps): update golang:1.23.3 docker digest to 73f06be
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
2024-11-14 23:01:28 +00:00
c36e1475c8 chore(deps): update dependency tailwindcss to v3.4.15
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 44s
2024-11-14 21:05:54 +00:00
0ad537107b chore(deps): update golang:1.23.3 docker digest to c2d828f
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
2024-11-13 23:08:53 +00:00
1e2ac485c1 chore(deps): update node.js to 5c76d05
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
2024-11-13 23:02:10 +00:00
c971a79675 chore(deps): update debian docker tag to v12.8
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 47s
2024-11-12 12:26:47 +00:00
6cff5aed4d chore(deps): update node.js to f496dba
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
2024-11-12 09:52:41 +01:00
92062d1634 chore(deps): update golang:1.23.3 docker digest to 8956c08
Some checks are pending
Build Docker Image / Build-Docker-Image (push) Waiting to run
2024-11-12 08:45:04 +00:00
1ed504c49b fix: refactor code to be testable #181
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 55s
Build Docker Image / Build-Docker-Image (push) Successful in 45s
2024-11-11 22:01:03 +01:00
9fd9f9649e fix(deps): update module golang.org/x/crypto to v0.29.0
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 52s
Build Docker Image / Build-Docker-Image (push) Successful in 2m9s
2024-11-08 08:47:33 +00:00
1eff0a5060 chore(deps): update golang docker tag to v1.23.3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m36s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 54s
2024-11-07 23:08:17 +00:00
9496b5c687 chore(deps): update node.js to v22.11.0
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m44s
2024-10-30 19:40:32 +01:00
506fc8d248 fix(deps): update module github.com/a-h/templ to v0.2.793
Some checks are pending
Build Docker Image / Build-Docker-Image (push) Successful in 49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Waiting to run
2024-10-30 18:35:20 +00:00
7910196874 chore(deps): update dependency daisyui to v4.12.14
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 54s
2024-10-28 23:02:09 +00:00
96062c6329 chore(deps): update actions/checkout digest to 11bd719
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 54s
2024-10-23 22:01:37 +00:00
5ebe6d5808 chore(deps): update node.js to da53547
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 54s
2024-10-20 00:10:33 +02:00
65d39b0d0c chore(deps): update golang:1.23.2 docker digest to ad5c126
Some checks are pending
Build Docker Image / Build-Docker-Image (push) Successful in 1m43s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Waiting to run
2024-10-19 22:02:47 +00:00
cf0861dde8 chore(deps): update node.js to v22.10.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 57s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 53s
2024-10-18 22:03:54 +00:00
4f6b49e1a0 chore(deps): update golang:1.23.2 docker digest to cc637ce
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m36s
2024-10-17 22:12:55 +00:00
ae0f21ada5 chore(deps): update debian:12.7 docker digest to e11072c
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 54s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 58s
2024-10-17 22:02:07 +00:00
96d7650b2c fix(deps): update module github.com/prometheus/client_golang to v1.20.5
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 56s
2024-10-15 22:52:31 +02:00
d81e09307a chore(deps): update dependency tailwindcss to v3.4.14
Some checks are pending
Build Docker Image / Build-Docker-Image (push) Successful in 50s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Waiting to run
2024-10-15 20:47:17 +00:00
790a3b0575 chore(deps): update golang:1.23.2 docker digest to a7f2fc9
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 50s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 56s
2024-10-11 07:39:16 +00:00
912ac69f1d chore(deps): update dependency daisyui to v4.12.13
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 51s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 55s
2024-10-09 20:03:38 +00:00
6abae81e1c chore(deps): update actions/checkout digest to eef6144
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 49s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 53s
2024-10-08 05:17:16 +00:00
0fab1e1f2e fix: missing service tests #181
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 50s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 2m17s
2024-10-06 10:05:00 +02:00
4dfd29eac1 fix: missing db auth tests #181
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 50s
2024-10-05 23:55:13 +02:00
3232632200 fix: new test and extract time.Now to mockable Clock #181
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 48s
2024-10-05 23:42:38 +02:00
6b033e2c2e fix: create RandomGenerator interface and struct for testing purpose #181 2024-10-05 23:42:38 +02:00
d36f880a01 fix: use testify for assertions #181 2024-10-05 23:42:38 +02:00
5b4160b09f fix: migrate sigin to testable code #181 2024-10-05 23:42:38 +02:00
abd8663cf3 chore: update job name
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 55s
2024-10-05 23:42:10 +02:00
3083ff206a fix(deps): update module golang.org/x/crypto to v0.28.0
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 48s
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 53s
2024-10-05 10:35:46 +00:00
53718d8710 fix: renovate import
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 48s
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 56s
2024-10-05 12:31:52 +02:00
2d56a6a317 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.24
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 49s
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 52s
2024-10-04 22:11:32 +00:00
abb5da3ffd fix: renovate default package name
All checks were successful
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 57s
2024-10-05 00:09:12 +02:00
008f1076da chore(deps): update dependency htmx.org to v2.0.3
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 48s
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 54s
2024-10-04 21:58:10 +00:00
fd6c68a71b fix: switch to mockery instead of handcrafted stubs
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 1m6s
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 55s
2024-10-04 23:54:32 +02:00
4bc17c50a4 chore(deps): update dependency daisyui to v4.12.12
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 48s
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 52s
2024-10-04 13:02:52 +00:00
3486081cf6 chore(deps): update golang docker tag to v1.23.2
All checks were successful
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 54s
2024-10-04 00:28:31 +02:00
f3995fbcfe chore(deps): update dependency daisyui to v4.12.11
Some checks are pending
Build Docker Image / Explore-Gitea-Actions (push) Successful in 47s
Build and Push Docker Image / Explore-Gitea-Actions (push) Waiting to run
2024-10-03 22:23:26 +00:00
cbf5b39294 fix: move signin handler #181
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 47s
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 51s
2024-10-03 23:24:58 +02:00
cc3747b226 chore: extract find cookie #181
All checks were successful
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 53s
Build Docker Image / Explore-Gitea-Actions (push) Successful in 47s
2024-10-03 22:55:12 +02:00
8d90874d04 chore: refine integration test #181 2024-10-03 22:55:09 +02:00
915c9238f6 chore: extract getEnv in integration test #181
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 49s
2024-10-03 12:51:47 +02:00
f2a98e5f49 fix: fist integration test #181
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 47s
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 52s
2024-10-02 23:17:38 +02:00
33380e2124 chore: parametrize port and prometheus #181
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 46s
2024-10-02 18:30:01 +02:00
bddcfc6778 chore: parametrize db path #181
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 48s
2024-10-02 10:41:16 +02:00
a53ad627fc Add renovate.json
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 47s
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 50s
2024-09-30 21:17:21 +00:00
7c1d561bd8 Delete renovate.json
All checks were successful
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 52s
2024-09-30 23:13:15 +02:00
a70138f2f7 fix: restructure env handling for better testing capabillities #181
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 47s
Build and Push Docker Image / Explore-Gitea-Actions (push) Successful in 55s
2024-09-29 23:55:47 +02:00
67 changed files with 5739 additions and 2771 deletions

View File

@@ -3,13 +3,13 @@ on:
push:
branches:
- '**' # matches every branch
- '!master'
- '!prod'
jobs:
Explore-Gitea-Actions:
Build-Docker-Image:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- run: docker build . -t me-fit-test
- run: docker rmi me-fit-test
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- run: docker build . -t web-app-template-test
- run: docker rmi web-app-template-test

View File

@@ -2,17 +2,17 @@ name: Build and Push Docker Image
on:
push:
branches:
- master
- prod
jobs:
Explore-Gitea-Actions:
Build-And-Push-Docker-Image:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- run: docker login git.wundenbergs.de -u tim -p ${{ secrets.DOCKER_GITEA_TOKEN }}
- run: docker build . -t git.wundenbergs.de/x/me-fit:latest -t git.wundenbergs.de/x/me-fit:$GITHUB_SHA
- run: docker push git.wundenbergs.de/x/me-fit:latest
- run: docker push git.wundenbergs.de/x/me-fit:$GITHUB_SHA
- run: docker rmi git.wundenbergs.de/x/me-fit:latest git.wundenbergs.de/x/me-fit:$GITHUB_SHA
- run: docker build . -t git.wundenbergs.de/x/web-app-template:latest -t git.wundenbergs.de/x/web-app-template:$GITHUB_SHA
- run: docker push git.wundenbergs.de/x/web-app-template:latest
- run: docker push git.wundenbergs.de/x/web-app-template:$GITHUB_SHA
- run: docker rmi git.wundenbergs.de/x/web-app-template:latest git.wundenbergs.de/x/web-app-template:$GITHUB_SHA

3
.gitignore vendored
View File

@@ -32,3 +32,6 @@ node_modules/
static/css/tailwind.css
static/js/htmx.min.js
tmp/
mocks/*
!mocks/default.go

13
.mockery.yaml Normal file
View File

@@ -0,0 +1,13 @@
with-expecter: True
dir: mocks/
outpkg: mocks
issue-845-fix: True
packages:
web-app-template/service:
interfaces:
Random:
Clock:
Mail:
web-app-template/db:
interfaces:
Auth:

View File

@@ -1,22 +1,32 @@
FROM golang:1.23.1@sha256:4f063a24d429510e512cc730c3330292ff49f3ade3ae79bda8f84a24fa25ecb0 AS builder_go
WORKDIR /me-fit
FROM golang:1.24.5@sha256:ef5b4be1f94b36c90385abd9b6b4f201723ae28e71acacb76d00687333c17282 AS builder_go
WORKDIR /web-app-template
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
RUN go install github.com/a-h/templ/cmd/templ@latest
RUN go install github.com/vektra/mockery/v2@latest
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
RUN templ generate && go test ./... && go build -o /me-fit/me-fit .
RUN templ generate
RUN mockery --log-level warn
RUN go test ./...
RUN golangci-lint run ./...
RUN go build -o /web-app-template/web-app-template .
FROM node:22.9.0@sha256:69e667a79aa41ec0db50bc452a60e705ca16f35285eaf037ebe627a65a5cdf52 AS builder_node
WORKDIR /me-fit
FROM node:22.18.0@sha256:3218f0d1b9e4b63def322e9ae362d581fbeac1ef21b51fc502ef91386667ce92 AS builder_node
WORKDIR /web-app-template
COPY package.json package-lock.json ./
RUN npm clean-install
COPY . ./
RUN npm install && npm run build
RUN npm run build
FROM debian:12.7@sha256:27586f4609433f2f49a9157405b473c62c3cb28a581c413393975b4e8496d0ab
WORKDIR /me-fit
FROM debian:12.11@sha256:b6507e340c43553136f5078284c8c68d86ec8262b1724dde73c325e8d3dcdeba
WORKDIR /web-app-template
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env
COPY --from=builder_go /me-fit/me-fit ./me-fit
COPY --from=builder_node /me-fit/static ./static
COPY migration ./migration
COPY --from=builder_go /web-app-template/web-app-template ./web-app-template
COPY --from=builder_node /web-app-template/static ./static
EXPOSE 8080
ENTRYPOINT ["/me-fit/me-fit"]
ENTRYPOINT ["/web-app-template/web-app-template"]

View File

@@ -1,44 +1,98 @@
# stackFAST
# Web-App-Template
Your (almost) independent tech stack to host on a VPC.
A basic template with authentication to easily host on a VPC.
## Features
stackFAST includes everything you need to build your App. Focus yourself on developing your idea, instead of "wasting" time on things like setting up auth and observability. This blueprint tries to include as much as possible, but still keep it simple.
This template includes everything essential to build an app. It includes the following features:
The blueprint contains the following features:
- Authentication: Users can login, logout, register and reset their password. For increased security TOTP is available aswell.
- Observability: The stack contains an Grafana+Prometheus instance for basic monitoring. You are able to add alerts and get notified on your phone. For web analytics umami is included, which is an lighweight self hosted alternative to google analytics.
- Authentication: Users can login, logout, register and reset their password. (for increased security TOTP is planned aswell.)
- Observability: The stack contains an Grafana+Prometheus instance for basic monitoring. You are able to add alerts and get notified on your phone.
- Mail: You are able to send mail with SMTP. You still need an external Mail Server, but a guide on how to set that up with a custom domain is included.
- SSL: This is included by using traefik as reverse proxy. It handles SSL certificates automatically. Furthermore all services are accessible through subdomains. Best thing is, you can add your more with 3 lines of code
- Actual Stack: SSG SvelteKit + Tailwindcss + DaisyUI + GO Backend for easy and fast feature development
- SSL: This is included by using traefik as reverse proxy. It handles SSL certificates automatically. Furthermore all services are accessible through subdomains.
- Stack: Tailwindcss + HTMX + GO Backend with templ and sqlite
## Architecture Design Decisions
### Authentication
Authentication is a broad topic. Many people think you should not consider implementing authentication yourself. On the other hand, experts at OWASP don't recommend this in their cheat sheet on that topic. I'm going to explain my criterions and afterwards take a decision.
Authentication is a broad topic. Many people think you should not consider implementing authentication yourself. On the other hand, If only security experts are allowed to write software, what does that result in? I'm going to explain my criterions and afterwards take a decision.
There are a few restrictions I would like to contain:
- I want this blueprint do as much as as possible without relying on external services. This way the things needs to be done on other website are very minimal. Furthermore I would like to take back privacy from BigTech.
- I think most cloud services are overpriced. I want to provide an alternative approach with self holsting. But I don't like the idea to spin up 30 services for a small app with 0 users. It should still be possible to run on a small VPC (2vcpu, 2GB).
- I want this template do as much as as possible without relying on external services. This way the setup cost and dependencies can be minimized.
- It should still be possible to run on a small VPC (2vcpu, 2GB).
- It should be as secure as possible
As of 2024 there are 4 options:
- Implement the authentication myself: If I'm holding thight to the cheat sheet, I "should" be able to doge "most" security risks and attacks according to this topic. Unfortanatly I'm not an expert in this field and will do some errors. If people will buy this blueprint, I probably can't sleep well. Especially if real users start using it. At least this has the advantage of not adding adittional services or configuration to the project.
- Using OAuth2 with Google and Apple: Using OAuth2 is the standard for secure applications. Google and Apple has their experts. They deal with attacks every hour of the day. This has the advantage, that users don't have to create new credentials. The only disatvantage is my personal hate on big tech.
- Using OAuth2 with Keycloak: Same as above, just that the OAuth2 endpoint is another self hosted service. The only advantage is, it's not proprietary and self hosted. But users are not used to get redirected to a key cloak on sign up. They are used to sign in with Google though. Furthermore Google et. al are protecting themselves against credential stuffing attacks etc.
- Firebase, Clerk, etc.: Users have to sign up again AND blueprint users have to setup another project.
I determined 4 options:
1. Implement the authentication myself
2. Using OAuth2 with Keycloak
3. Using OAuth2 with Google and Apple
4. Firebase, Clerk, etc.
Even though I would really implement authentication myself, I think OAuth2 with external providers is the best bet. Especially because my reasoning is privacy, which most people just don't care about enough. Using this approach, adding in a keycloak is possible without breaking changes at a later point, as long as I keep the Google Sign In.
#### 1. Implement the authentication myself
It's always possible to implement it myself. The topic of authentication is something special though.
Pros:
- Great Cheat cheets from OWASP
- No adittional configuration or services needed
- Great learning experience on the topic "security"
Cons:
- Great attack vector
- Introcution of vlunerabillities is possible
- No DDOS protection
#### 2. Using OAuth2 with Google and Apple
Instead of implementing authentication from scratch, an external OAuth2 provider is embedded into the application.
Pros:
- The Systems of BigTech are probably safer. They have security experts employed.
- The other external system is responsible to prevent credential stuffing attacks, etc.
- Users don't have to create new credentials
Cons:
- High dependency on those providers
- Single Point of failure (If your account is banned, your application access get's lost as well.)
- It's possible that these providers ban the whole application (All users lose access)
- There still needs to be implemented some logic
- Full application integration can be difficult
#### 3. Using OAuth2 with Keycloak
This option is almost identical with the previois one, but the provider is self hosted.
Pros:
- Indipendent from 3rd party providers
- The credentials are stored safly
Cons:
- Self hosted (no DDOS protection, etc.)
- There still needs to be implemented some logic server side
- Full application integration can be difficult
#### 4. Firebase, Clerk, etc.
Users can sign in with a seperate sdk on your website
Pros:
- Safe and Sound authentication
Cons:
- Dependent on those providers / adittional setup needed
- Application can be banned
- Still some integration code needed
#### Decision
I've decided on implementing authentication myself, as this is a great learning opportunity. It may not be as secure as other solutions, but if I keep tighly to the OWASP recommendations, it should should good enough.
### Email
For Email verification, etc. a mail server is needed, that can send a whole lot of mails. Aditionally, a mail account is needed for incoming mails. I thought about self hosting, but unfortunatly this is a hastle to maintain. Not only you have to setup a mail server, which is not as easy as it sounds, you also have to "register" your mail server for diffrent providers. Otherwise you are not able to send and receive emails. Thus, the first external service is needed.
For Email verification, etc. a mail server is needed, that can send a whole lot of mails. Aditionally, a mail account is needed for incoming emails. I thought about self hosting, but unfortunatly this is a hastle to maintain. Not only you have to setup a mail server, which is not as easy as it sounds, you also have to "register" your mail server for diffrent providers. Otherwise you are not able to send and receive emails.
In order to not vendor lock in, I decided to use an SMTP relay in favor of a vendor specific API. You are free to choose a transactional mail provider. I chose brevo.com. They have a generous free tier of 300 mails per day. You can either upgrade to a monthly plan 10$ for 20k mails or buy credits for 30$ for 5k mails. Most provider provide 100 mails / day for free.
In order to not vendor lock in, I decided to use an SMTP relay in favor of a vendor specific API. I chose brevo.com. They have a generous free tier of 300 mails per day. You can either upgrade to a monthly plan 10$ for 20k mails or buy credits for 30$ for 5k mails.

View File

@@ -1,60 +1,88 @@
package db
import (
"web-app-template/log"
"web-app-template/types"
"database/sql"
"errors"
"me-fit/types"
"me-fit/utils"
"strings"
"time"
"github.com/google/uuid"
)
var (
ErrUserNotFound = errors.New("User not found")
ErrNotFound = errors.New("value not found")
ErrAlreadyExists = errors.New("row already exists")
)
type User struct {
Id uuid.UUID
Email string
EmailVerified bool
EmailVerifiedAt time.Time
IsAdmin bool
Password []byte
Salt []byte
CreateAt time.Time
type Auth interface {
InsertUser(user *types.User) error
UpdateUser(user *types.User) error
GetUserByEmail(email string) (*types.User, error)
GetUser(userId uuid.UUID) (*types.User, error)
DeleteUser(userId uuid.UUID) error
InsertToken(token *types.Token) error
GetToken(token string) (*types.Token, error)
GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error)
GetTokensBySessionIdAndType(sessionId string, tokenType types.TokenType) ([]*types.Token, error)
DeleteToken(token string) error
InsertSession(session *types.Session) error
GetSession(sessionId string) (*types.Session, error)
GetSessions(userId uuid.UUID) ([]*types.Session, error)
DeleteSession(sessionId string) error
DeleteOldSessions(userId uuid.UUID) error
}
func NewUser(id uuid.UUID, email string, emailVerified bool, emailVerifiedAt time.Time, isAdmin bool, password []byte, salt []byte, createAt time.Time) *User {
return &User{
Id: id,
Email: email,
EmailVerified: emailVerified,
EmailVerifiedAt: emailVerifiedAt,
IsAdmin: isAdmin,
Password: password,
Salt: salt,
CreateAt: createAt,
}
}
type DbAuth interface {
GetUser(email string) (*User, error)
}
type DbAuthSqlite struct {
type AuthSqlite struct {
db *sql.DB
}
func NewDbAuthSqlite(db *sql.DB) *DbAuthSqlite {
return &DbAuthSqlite{db: db}
func NewAuthSqlite(db *sql.DB) *AuthSqlite {
return &AuthSqlite{db: db}
}
func (db DbAuthSqlite) GetUser(email string) (*User, error) {
func (db AuthSqlite) InsertUser(user *types.User) error {
_, err := db.db.Exec(`
INSERT INTO user (user_id, email, email_verified, email_verified_at, is_admin, password, salt, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
user.Id, user.Email, user.EmailVerified, user.EmailVerifiedAt, user.IsAdmin, user.Password, user.Salt, user.CreateAt)
if err != nil {
if strings.Contains(err.Error(), "email") {
return ErrAlreadyExists
}
log.Error("SQL error InsertUser: %v", err)
return types.ErrInternal
}
return nil
}
func (db AuthSqlite) UpdateUser(user *types.User) error {
_, err := db.db.Exec(`
UPDATE user
SET email_verified = ?, email_verified_at = ?, password = ?
WHERE user_id = ?`,
user.EmailVerified, user.EmailVerifiedAt, user.Password, user.Id)
if err != nil {
log.Error("SQL error UpdateUser: %v", err)
return types.ErrInternal
}
return nil
}
func (db AuthSqlite) GetUserByEmail(email string) (*types.User, error) {
var (
userId uuid.UUID
emailVerified bool
emailVerifiedAt time.Time
emailVerifiedAt *time.Time
isAdmin bool
password []byte
salt []byte
@@ -62,17 +90,319 @@ func (db DbAuthSqlite) GetUser(email string) (*User, error) {
)
err := db.db.QueryRow(`
SELECT user_uuid, email_verified, email_verified_at, password, salt, created_at
SELECT user_id, email_verified, email_verified_at, password, salt, created_at
FROM user
WHERE email = ?`, email).Scan(&userId, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, ErrUserNotFound
return nil, ErrNotFound
} else {
utils.LogError("SQL error GetUser", err)
log.Error("SQL error GetUser: %v", err)
return nil, types.ErrInternal
}
}
return 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) {
var (
email string
emailVerified bool
emailVerifiedAt *time.Time
isAdmin bool
password []byte
salt []byte
createdAt time.Time
)
err := db.db.QueryRow(`
SELECT email, email_verified, email_verified_at, password, salt, created_at
FROM user
WHERE user_id = ?`, userId).Scan(&email, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, ErrNotFound
} else {
log.Error("SQL error GetUser %v", err)
return nil, types.ErrInternal
}
}
return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
}
func (db AuthSqlite) DeleteUser(userId uuid.UUID) error {
tx, err := db.db.Begin()
if err != nil {
log.Error("Could not start transaction: %v", err)
return types.ErrInternal
}
_, err = tx.Exec("DELETE FROM workout WHERE user_id = ?", userId)
if err != nil {
_ = tx.Rollback()
log.Error("Could not delete workouts: %v", err)
return types.ErrInternal
}
_, err = tx.Exec("DELETE FROM token WHERE user_id = ?", userId)
if err != nil {
_ = tx.Rollback()
log.Error("Could not delete user tokens: %v", err)
return types.ErrInternal
}
_, err = tx.Exec("DELETE FROM session WHERE user_id = ?", userId)
if err != nil {
_ = tx.Rollback()
log.Error("Could not delete sessions: %v", err)
return types.ErrInternal
}
_, err = tx.Exec("DELETE FROM user WHERE user_id = ?", userId)
if err != nil {
_ = tx.Rollback()
log.Error("Could not delete user: %v", err)
return types.ErrInternal
}
err = tx.Commit()
if err != nil {
log.Error("Could not commit transaction: %v", err)
return types.ErrInternal
}
return nil
}
func (db AuthSqlite) InsertToken(token *types.Token) error {
_, err := db.db.Exec(`
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)
if err != nil {
log.Error("Could not insert token: %v", err)
return types.ErrInternal
}
return nil
}
func (db AuthSqlite) GetToken(token string) (*types.Token, error) {
var (
userId uuid.UUID
sessionId string
tokenType types.TokenType
createdAtStr string
expiresAtStr string
createdAt time.Time
expiresAt time.Time
)
err := db.db.QueryRow(`
SELECT user_id, session_id, type, created_at, expires_at
FROM token
WHERE token = ?`, token).Scan(&userId, &sessionId, &tokenType, &createdAtStr, &expiresAtStr)
if err != nil {
if err == sql.ErrNoRows {
log.Info("Token '%v' not found", token)
return nil, ErrNotFound
} else {
log.Error("Could not get token: %v", err)
return nil, types.ErrInternal
}
}
createdAt, err = time.Parse(time.RFC3339, createdAtStr)
if err != nil {
log.Error("Could not parse token.created_at: %v", err)
return nil, types.ErrInternal
}
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
if err != nil {
log.Error("Could not parse token.expires_at: %v", err)
return nil, types.ErrInternal
}
return types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil
}
func (db AuthSqlite) GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error) {
query, err := db.db.Query(`
SELECT token, created_at, expires_at
FROM token
WHERE user_id = ?
AND type = ?`, userId, tokenType)
if err != nil {
log.Error("Could not get token: %v", err)
return nil, types.ErrInternal
}
return getTokensFromQuery(query, userId, "", tokenType)
}
func (db AuthSqlite) GetTokensBySessionIdAndType(sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
query, err := db.db.Query(`
SELECT token, created_at, expires_at
FROM token
WHERE session_id = ?
AND type = ?`, sessionId, tokenType)
if err != nil {
log.Error("Could not get token: %v", err)
return nil, types.ErrInternal
}
return getTokensFromQuery(query, uuid.Nil, sessionId, tokenType)
}
func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
var tokens []*types.Token
hasRows := false
for query.Next() {
hasRows = true
var (
token string
createdAtStr string
expiresAtStr string
createdAt time.Time
expiresAt time.Time
)
err := query.Scan(&token, &createdAtStr, &expiresAtStr)
if err != nil {
log.Error("Could not scan token: %v", err)
return nil, types.ErrInternal
}
createdAt, err = time.Parse(time.RFC3339, createdAtStr)
if err != nil {
log.Error("Could not parse token.created_at: %v", err)
return nil, types.ErrInternal
}
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
if err != nil {
log.Error("Could not parse token.expires_at: %v", err)
return nil, types.ErrInternal
}
tokens = append(tokens, types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt))
}
if !hasRows {
return nil, ErrNotFound
}
return tokens, nil
}
func (db AuthSqlite) DeleteToken(token string) error {
_, err := db.db.Exec("DELETE FROM token WHERE token = ?", token)
if err != nil {
log.Error("Could not delete token: %v", err)
return types.ErrInternal
}
return nil
}
func (db AuthSqlite) InsertSession(session *types.Session) error {
_, err := db.db.Exec(`
INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, ?, ?)`, session.Id, session.UserId, session.CreatedAt, session.ExpiresAt)
if err != nil {
log.Error("Could not insert new session %v", err)
return types.ErrInternal
}
return nil
}
func (db AuthSqlite) GetSession(sessionId string) (*types.Session, error) {
var (
userId uuid.UUID
createdAt time.Time
expiresAt time.Time
)
err := db.db.QueryRow(`
SELECT user_id, created_at, expires_at
FROM session
WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt)
if err != nil {
log.Warn("Session not found: %v", err)
return nil, ErrNotFound
}
return types.NewSession(sessionId, userId, createdAt, expiresAt), nil
}
func (db AuthSqlite) GetSessions(userId uuid.UUID) ([]*types.Session, error) {
sessions, err := db.db.Query(`
SELECT session_id, created_at, expires_at
FROM session
WHERE user_id = ?`, userId)
if err != nil {
log.Error("Could not get sessions: %v", err)
return nil, types.ErrInternal
}
var result []*types.Session
for sessions.Next() {
var (
sessionId string
createdAt time.Time
expiresAt time.Time
)
err := sessions.Scan(&sessionId, &createdAt, &expiresAt)
if err != nil {
log.Error("Could not scan session: %v", err)
return nil, types.ErrInternal
}
session := types.NewSession(sessionId, userId, createdAt, expiresAt)
result = append(result, session)
}
return result, nil
}
func (db AuthSqlite) DeleteOldSessions(userId uuid.UUID) error {
_, err := db.db.Exec(`
DELETE FROM session
WHERE expires_at < datetime('now')
AND user_id = ?`, userId)
if err != nil {
log.Error("Could not delete old sessions: %v", err)
return types.ErrInternal
}
return nil
}
func (db AuthSqlite) DeleteSession(sessionId string) error {
if sessionId != "" {
_, err := db.db.Exec("DELETE FROM session WHERE session_id = ?", sessionId)
if err != nil {
log.Error("Could not delete session: %v", err)
return types.ErrInternal
}
}
return nil
}

View File

@@ -2,12 +2,12 @@ package db
import (
"database/sql"
"me-fit/utils"
"reflect"
"web-app-template/types"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func setupDb(t *testing.T) *sql.DB {
@@ -15,55 +15,185 @@ func setupDb(t *testing.T) *sql.DB {
if err != nil {
t.Fatalf("Error opening database: %v", err)
}
t.Cleanup(func() {
db.Close()
})
utils.MustRunMigrations(db, "../")
err = RunMigrations(db, "../")
if err != nil {
t.Fatalf("Error running migrations: %v", err)
}
return db
}
func TestGetUser(t *testing.T) {
func TestUser(t *testing.T) {
t.Parallel()
t.Run("should return UserNotFound", func(t *testing.T) {
t.Run("should insert and get the same", func(t *testing.T) {
t.Parallel()
db := setupDb(t)
defer db.Close()
underTest := DbAuthSqlite{db: db}
_, err := underTest.GetUser("someNonExistentEmail")
if err != ErrUserNotFound {
t.Errorf("Expected UserNotFound, got %v", err)
}
})
t.Run("should find user in database", func(t *testing.T) {
t.Parallel()
db := setupDb(t)
defer db.Close()
underTest := DbAuthSqlite{db: db}
underTest := AuthSqlite{db: db}
verifiedAt := time.Date(2020, 1, 5, 13, 0, 0, 0, time.UTC)
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
user := 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 := db.Exec(`
INSERT INTO user (user_uuid, email, email_verified, email_verified_at, is_admin, password, salt, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, user.Id, user.Email, user.EmailVerified, user.EmailVerifiedAt, user.IsAdmin, user.Password, user.Salt, user.CreateAt)
if err != nil {
t.Fatalf("Error inserting user: %v", err)
}
err := underTest.InsertUser(expected)
assert.Nil(t, err)
actual, err := underTest.GetUser(user.Email)
if err != nil {
t.Fatalf("Error getting user: %v", err)
}
actual, err := underTest.GetUser(expected.Id)
assert.Nil(t, err)
assert.Equal(t, expected, actual)
if !reflect.DeepEqual(user, actual) {
t.Errorf("Expected %v, got %v", user, actual)
}
actual, err = underTest.GetUserByEmail(expected.Email)
assert.Nil(t, err)
assert.Equal(t, expected, actual)
})
t.Run("should return ErrNotFound", func(t *testing.T) {
t.Parallel()
db := setupDb(t)
underTest := AuthSqlite{db: db}
_, err := underTest.GetUserByEmail("nonExistentEmail")
assert.Equal(t, ErrNotFound, err)
})
t.Run("should return ErrUserExist", func(t *testing.T) {
t.Parallel()
db := setupDb(t)
underTest := AuthSqlite{db: db}
verifiedAt := time.Date(2020, 1, 5, 13, 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)
err := underTest.InsertUser(user)
assert.Nil(t, err)
err = underTest.InsertUser(user)
assert.Equal(t, ErrAlreadyExists, err)
})
t.Run("should return ErrInternal on missing NOT NULL fields", func(t *testing.T) {
t.Parallel()
db := setupDb(t)
underTest := AuthSqlite{db: db}
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)
err := underTest.InsertUser(user)
assert.Equal(t, types.ErrInternal, err)
})
}
func TestToken(t *testing.T) {
t.Parallel()
t.Run("should insert and get the same", func(t *testing.T) {
t.Parallel()
db := setupDb(t)
underTest := AuthSqlite{db: db}
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
expiresAt := createAt.Add(24 * time.Hour)
expected := types.NewToken(uuid.New(), "sessionId", "token", types.TokenTypeCsrf, createAt, expiresAt)
err := underTest.InsertToken(expected)
assert.Nil(t, err)
actual, err := underTest.GetToken(expected.Token)
assert.Nil(t, err)
assert.Equal(t, expected, actual)
expected.SessionId = ""
actuals, err := underTest.GetTokensByUserIdAndType(expected.UserId, expected.Type)
assert.Nil(t, err)
assert.Equal(t, []*types.Token{expected}, actuals)
expected.SessionId = "sessionId"
expected.UserId = uuid.Nil
actuals, err = underTest.GetTokensBySessionIdAndType(expected.SessionId, expected.Type)
assert.Nil(t, err)
assert.Equal(t, []*types.Token{expected}, actuals)
})
t.Run("should insert and return multiple tokens", func(t *testing.T) {
t.Parallel()
db := setupDb(t)
underTest := AuthSqlite{db: db}
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
expiresAt := createAt.Add(24 * time.Hour)
userId := uuid.New()
expected1 := types.NewToken(userId, "sessionId", "token1", types.TokenTypeCsrf, createAt, expiresAt)
expected2 := types.NewToken(userId, "sessionId", "token2", types.TokenTypeCsrf, createAt, expiresAt)
err := underTest.InsertToken(expected1)
assert.Nil(t, err)
err = underTest.InsertToken(expected2)
assert.Nil(t, err)
expected1.UserId = uuid.Nil
expected2.UserId = uuid.Nil
actuals, err := underTest.GetTokensBySessionIdAndType(expected1.SessionId, expected1.Type)
assert.Nil(t, err)
assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
expected1.SessionId = ""
expected2.SessionId = ""
expected1.UserId = userId
expected2.UserId = userId
actuals, err = underTest.GetTokensByUserIdAndType(userId, expected1.Type)
assert.Nil(t, err)
assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
})
t.Run("should return ErrNotFound", func(t *testing.T) {
t.Parallel()
db := setupDb(t)
underTest := AuthSqlite{db: db}
_, err := underTest.GetToken("nonExistent")
assert.Equal(t, ErrNotFound, err)
_, err = underTest.GetTokensByUserIdAndType(uuid.New(), types.TokenTypeEmailVerify)
assert.Equal(t, ErrNotFound, err)
_, err = underTest.GetTokensBySessionIdAndType("sessionId", types.TokenTypeEmailVerify)
assert.Equal(t, ErrNotFound, err)
})
t.Run("should return ErrAlreadyExists", func(t *testing.T) {
t.Parallel()
db := setupDb(t)
underTest := AuthSqlite{db: db}
verifiedAt := time.Date(2020, 1, 5, 13, 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)
err := underTest.InsertUser(user)
assert.Nil(t, err)
err = underTest.InsertUser(user)
assert.Equal(t, ErrAlreadyExists, err)
})
t.Run("should return ErrInternal on missing NOT NULL fields", func(t *testing.T) {
t.Parallel()
db := setupDb(t)
underTest := AuthSqlite{db: db}
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)
err := underTest.InsertUser(user)
assert.Equal(t, types.ErrInternal, err)
})
}

41
db/default.go Normal file
View File

@@ -0,0 +1,41 @@
package db
import (
"web-app-template/log"
"web-app-template/types"
"database/sql"
"errors"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func RunMigrations(db *sql.DB, pathPrefix string) error {
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
if err != nil {
log.Error("Could not create Migration instance: %v", err)
return types.ErrInternal
}
m, err := migrate.NewWithDatabaseInstance(
"file://"+pathPrefix+"migration/",
"",
driver)
if err != nil {
log.Error("Could not create migrations instance: %v", err)
return types.ErrInternal
}
err = m.Up()
if err != nil {
if !errors.Is(err, migrate.ErrNoChange) {
log.Error("Could not run migrations: %v", err)
return types.ErrInternal
}
}
return nil
}

119
db/workout.go Normal file
View File

@@ -0,0 +1,119 @@
package db
import (
"web-app-template/log"
"web-app-template/types"
"database/sql"
"errors"
"time"
"github.com/google/uuid"
)
var (
ErrWorkoutNotExists = errors.New("Workout does not exist")
)
type WorkoutDb interface {
InsertWorkout(userId uuid.UUID, workout *WorkoutInsert) (*Workout, error)
GetWorkouts(userId uuid.UUID) ([]Workout, error)
DeleteWorkout(userId uuid.UUID, rowId int) error
}
type WorkoutDbSqlite struct {
db *sql.DB
}
func NewWorkoutDbSqlite(db *sql.DB) *WorkoutDbSqlite {
return &WorkoutDbSqlite{db: db}
}
type WorkoutInsert struct {
Date time.Time
Type string
Sets int
Reps int
}
type Workout struct {
RowId int
Date time.Time
Type string
Sets int
Reps int
}
func NewWorkoutInsert(date time.Time, workoutType string, sets int, reps int) *WorkoutInsert {
return &WorkoutInsert{Date: date, Type: workoutType, Sets: sets, Reps: reps}
}
func NewWorkoutFromInsert(rowId int, workoutInsert *WorkoutInsert) *Workout {
return &Workout{RowId: rowId, Date: workoutInsert.Date, Type: workoutInsert.Type, Sets: workoutInsert.Sets, Reps: workoutInsert.Reps}
}
func (db WorkoutDbSqlite) InsertWorkout(userId uuid.UUID, workout *WorkoutInsert) (*Workout, error) {
var rowId int
err := db.db.QueryRow(`
INSERT INTO workout (user_id, date, type, sets, reps)
VALUES (?, ?, ?, ?, ?)
RETURNING rowid`, userId, workout.Date, workout.Type, workout.Sets, workout.Reps).Scan(&rowId)
if err != nil {
log.Error("Error inserting workout: %v", err)
return nil, types.ErrInternal
}
return NewWorkoutFromInsert(rowId, workout), nil
}
func (db WorkoutDbSqlite) GetWorkouts(userId uuid.UUID) ([]Workout, error) {
rows, err := db.db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ? ORDER BY date desc", userId)
if err != nil {
log.Error("Could not get workouts: %v", err)
return nil, types.ErrInternal
}
var workouts = make([]Workout, 0)
for rows.Next() {
var (
workout Workout
date string
)
err = rows.Scan(&workout.RowId, &date, &workout.Type, &workout.Sets, &workout.Reps)
if err != nil {
log.Error("Could not scan workout: %v", err)
return nil, types.ErrInternal
}
workout.Date, err = time.Parse("2006-01-02 15:04:05-07:00", date)
if err != nil {
log.Error("Could not parse date: %v", err)
return nil, types.ErrInternal
}
workouts = append(workouts, workout)
}
return workouts, nil
}
func (db WorkoutDbSqlite) DeleteWorkout(userId uuid.UUID, rowId int) error {
res, err := db.db.Exec("DELETE FROM workout WHERE user_id = ? AND rowid = ?", userId, rowId)
if err != nil {
return types.ErrInternal
}
rows, err := res.RowsAffected()
if err != nil {
return types.ErrInternal
}
if rows == 0 {
return ErrWorkoutNotExists
}
return nil
}

33
go.mod
View File

@@ -1,28 +1,35 @@
module me-fit
module web-app-template
go 1.22.5
go 1.23.0
toolchain go1.24.5
require (
github.com/a-h/templ v0.2.778
github.com/golang-migrate/migrate/v4 v4.18.1
github.com/a-h/templ v0.3.924
github.com/golang-migrate/migrate/v4 v4.18.3
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.23
github.com/prometheus/client_golang v1.20.4
golang.org/x/crypto v0.27.0
github.com/mattn/go-sqlite3 v1.14.30
github.com/prometheus/client_golang v1.23.0
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.41.0
golang.org/x/net v0.43.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/sys v0.25.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
golang.org/x/sys v0.35.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

67
go.sum
View File

@@ -1,15 +1,15 @@
github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/a-h/templ v0.3.924 h1:t5gZqTneXqvehpNZsgtnlOscnBboNh9aASBH2MgV/0k=
github.com/a-h/templ v0.3.924/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -19,35 +19,50 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
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/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
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/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,79 +1,104 @@
package handler
import (
"me-fit/service"
"me-fit/utils"
"time"
"web-app-template/handler/middleware"
"web-app-template/log"
"web-app-template/service"
"web-app-template/template/auth"
"web-app-template/types"
"web-app-template/utils"
"database/sql"
"errors"
"net/http"
"net/url"
"time"
)
type HandlerAuth interface {
handle(router *http.ServeMux)
type Auth interface {
Handle(router *http.ServeMux)
}
type HandlerAuthImpl struct {
db *sql.DB
service service.ServiceAuth
type AuthImpl struct {
service service.Auth
render *Render
}
func NewHandlerAuth(db *sql.DB, service service.ServiceAuth) HandlerAuth {
return HandlerAuthImpl{
db: db,
func NewAuth(service service.Auth, render *Render) Auth {
return AuthImpl{
service: service,
render: render,
}
}
func (handler HandlerAuthImpl) handle(router *http.ServeMux) {
// Don't use auth middleware for these routes, as it makes redirecting very difficult, if the mail is not yet verified
router.Handle("/auth/signin", service.HandleSignInPage(handler.db))
router.Handle("/auth/signup", service.HandleSignUpPage(handler.db))
router.Handle("/auth/verify", service.HandleSignUpVerifyPage(handler.db)) // Hint for the user to verify their email
router.Handle("/auth/delete-account", service.HandleDeleteAccountPage(handler.db))
router.Handle("/auth/verify-email", service.HandleSignUpVerifyResponsePage(handler.db)) // The link contained in the email
router.Handle("/auth/change-password", service.HandleChangePasswordPage(handler.db))
router.Handle("/auth/reset-password", service.HandleResetPasswordPage(handler.db))
router.Handle("/api/auth/signup", service.HandleSignUpComp(handler.db))
router.Handle("/api/auth/signin", handler.handleSignIn())
router.Handle("/api/auth/signout", service.HandleSignOutComp(handler.db))
router.Handle("/api/auth/delete-account", service.HandleDeleteAccountComp(handler.db))
router.Handle("/api/auth/verify-resend", service.HandleVerifyResendComp(handler.db))
router.Handle("/api/auth/change-password", service.HandleChangePasswordComp(handler.db))
router.Handle("/api/auth/reset-password", service.HandleResetPasswordComp(handler.db))
router.Handle("/api/auth/reset-password-actual", service.HandleActualResetPasswordComp(handler.db))
func (handler AuthImpl) Handle(router *http.ServeMux) {
router.Handle("GET /auth/signin", handler.handleSignInPage())
router.Handle("POST /api/auth/signin", handler.handleSignIn())
router.Handle("/auth/signup", handler.handleSignUpPage())
router.Handle("/auth/verify", handler.handleSignUpVerifyPage())
router.Handle("/api/auth/verify-resend", handler.handleVerifyResendComp())
router.Handle("/auth/verify-email", handler.handleSignUpVerifyResponsePage())
router.Handle("/api/auth/signup", handler.handleSignUp())
router.Handle("POST /api/auth/signout", handler.handleSignOut())
router.Handle("/auth/delete-account", handler.handleDeleteAccountPage())
router.Handle("/api/auth/delete-account", handler.handleDeleteAccountComp())
router.Handle("GET /auth/change-password", handler.handleChangePasswordPage())
router.Handle("POST /api/auth/change-password", handler.handleChangePasswordComp())
router.Handle("GET /auth/forgot-password", handler.handleForgotPasswordPage())
router.Handle("POST /api/auth/forgot-password", handler.handleForgotPasswordComp())
router.Handle("POST /api/auth/forgot-password-actual", handler.handleForgotPasswordResponseComp())
}
var (
securityWaitDuration = 250 * time.Millisecond
)
func (handler HandlerAuthImpl) handleSignIn() http.HandlerFunc {
func (handler AuthImpl) handleSignInPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := utils.WaitMinimumTime(securityWaitDuration, func() (*service.User, error) {
var email = r.FormValue("email")
var password = r.FormValue("password")
user := middleware.GetUser(r)
if user != nil {
if !user.EmailVerified {
utils.DoRedirect(w, r, "/auth/verify")
} else {
utils.DoRedirect(w, r, "/")
}
return
}
user, err := handler.service.SignIn(email, password)
comp := auth.SignInOrUpComp(true)
handler.render.RenderLayout(r, w, comp, nil)
}
}
func (handler AuthImpl) handleSignIn() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := utils.WaitMinimumTime(securityWaitDuration, func() (*types.User, error) {
session := middleware.GetSession(r)
email := r.FormValue("email")
password := r.FormValue("password")
session, user, err := handler.service.SignIn(session, email, password)
if err != nil {
return nil, err
}
err = service.TryCreateSessionAndSetCookie(r, w, handler.db, user.Id)
if err != nil {
return nil, err
}
cookie := middleware.CreateSessionCookie(session.Id)
http.SetCookie(w, &cookie)
return user, nil
})
if err != nil {
if err == service.ErrInvaidCredentials {
utils.TriggerToast(w, r, "error", "Invalid email or password")
http.Error(w, "Invalid email or password", http.StatusUnauthorized)
if err == service.ErrInvalidCredentials {
utils.TriggerToast(w, r, "error", "Invalid email or password", http.StatusUnauthorized)
} else {
utils.LogError("Error signing in", err)
http.Error(w, "An error occurred", http.StatusInternalServerError)
utils.TriggerToast(w, r, "error", "An error occurred", http.StatusInternalServerError)
}
return
}
@@ -85,3 +110,273 @@ func (handler HandlerAuthImpl) handleSignIn() http.HandlerFunc {
}
}
}
func (handler AuthImpl) handleSignUpPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user != nil {
if !user.EmailVerified {
utils.DoRedirect(w, r, "/auth/verify")
} else {
utils.DoRedirect(w, r, "/")
}
return
}
signUpComp := auth.SignInOrUpComp(false)
handler.render.RenderLayout(r, w, signUpComp, nil)
}
}
func (handler AuthImpl) handleSignUpVerifyPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
if user.EmailVerified {
utils.DoRedirect(w, r, "/")
return
}
signIn := auth.VerifyComp()
handler.render.RenderLayout(r, w, signIn, user)
}
}
func (handler AuthImpl) handleVerifyResendComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
go handler.service.SendVerificationMail(user.Id, user.Email)
_, err := w.Write([]byte("<p class=\"mt-8\">Verification email sent</p>"))
if err != nil {
log.Error("Could not write response: %v", err)
}
}
}
func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
err := handler.service.VerifyUserEmail(token)
isVerified := err == nil
comp := auth.VerifyResponseComp(isVerified)
var status int
if isVerified {
status = http.StatusOK
} else {
status = http.StatusBadRequest
}
handler.render.RenderLayoutWithStatus(r, w, comp, nil, status)
}
}
func (handler AuthImpl) handleSignUp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var email = r.FormValue("email")
var password = r.FormValue("password")
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (interface{}, error) {
log.Info("Signing up %v", email)
user, err := handler.service.SignUp(email, password)
if err != nil {
return nil, err
}
log.Info("Sending verification email to %v", user.Email)
go handler.service.SendVerificationMail(user.Id, user.Email)
return nil, nil
})
if err != nil {
if errors.Is(err, types.ErrInternal) {
utils.TriggerToast(w, r, "error", "An error occurred", http.StatusInternalServerError)
return
} else if errors.Is(err, service.ErrInvalidEmail) {
utils.TriggerToast(w, r, "error", "The email provided is invalid", http.StatusBadRequest)
return
} else if errors.Is(err, service.ErrInvalidPassword) {
utils.TriggerToast(w, r, "error", service.ErrInvalidPassword.Error(), http.StatusBadRequest)
return
}
// If err is "service.ErrAccountExists", then just continue
}
utils.TriggerToast(w, r, "success", "An activation link has been send to your email", http.StatusOK)
}
}
func (handler AuthImpl) handleSignOut() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session := middleware.GetSession(r)
if session != nil {
err := handler.service.SignOut(session.Id)
if err != nil {
http.Error(w, "An error occurred", http.StatusInternalServerError)
return
}
}
c := http.Cookie{
Name: "id",
Value: "",
MaxAge: -1,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
}
http.SetCookie(w, &c)
utils.DoRedirect(w, r, "/")
}
}
func (handler AuthImpl) handleDeleteAccountPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
comp := auth.DeleteAccountComp()
handler.render.RenderLayout(r, w, comp, user)
}
}
func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
password := r.FormValue("password")
err := handler.service.DeleteAccount(user, password)
if err != nil {
if err == service.ErrInvalidCredentials {
utils.TriggerToast(w, r, "error", "Password not correct", http.StatusBadRequest)
} else {
utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
}
return
}
utils.DoRedirect(w, r, "/")
}
}
func (handler AuthImpl) handleChangePasswordPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
isPasswordReset := r.URL.Query().Has("token")
user := middleware.GetUser(r)
if user == nil && !isPasswordReset {
utils.DoRedirect(w, r, "/auth/signin")
return
}
comp := auth.ChangePasswordComp(isPasswordReset)
handler.render.RenderLayout(r, w, comp, user)
}
}
func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session := middleware.GetSession(r)
user := middleware.GetUser(r)
if session == nil || user == nil {
utils.TriggerToast(w, r, "error", "Unathorized", http.StatusUnauthorized)
return
}
currPass := r.FormValue("current-password")
newPass := r.FormValue("new-password")
err := handler.service.ChangePassword(user, session.Id, currPass, newPass)
if err != nil {
utils.TriggerToast(w, r, "error", "Password not correct", http.StatusBadRequest)
return
}
utils.TriggerToast(w, r, "success", "Password changed", http.StatusOK)
}
}
func (handler AuthImpl) handleForgotPasswordPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user != nil {
utils.DoRedirect(w, r, "/")
return
}
comp := auth.ResetPasswordComp()
handler.render.RenderLayout(r, w, comp, user)
}
}
func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
if email == "" {
utils.TriggerToast(w, r, "error", "Please enter an email", http.StatusBadRequest)
return
}
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (interface{}, error) {
err := handler.service.SendForgotPasswordMail(email)
return nil, err
})
if err != nil {
utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
} else {
utils.TriggerToast(w, r, "info", "If the address exists, an email has been sent.", http.StatusOK)
}
}
}
func (handler AuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
pageUrl, err := url.Parse(r.Header.Get("HX-Current-URL"))
if err != nil {
log.Error("Could not get current URL: %v", err)
utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
return
}
token := pageUrl.Query().Get("token")
newPass := r.FormValue("new-password")
err = handler.service.ForgotPassword(token, newPass)
if err != nil {
utils.TriggerToast(w, r, "error", err.Error(), http.StatusBadRequest)
} else {
utils.TriggerToast(w, r, "success", "Password changed", http.StatusOK)
}
}
}

View File

@@ -1,31 +0,0 @@
package handler
import (
"me-fit/db"
"me-fit/middleware"
"me-fit/service"
"database/sql"
"net/http"
)
func GetHandler(d *sql.DB) http.Handler {
var router = http.NewServeMux()
router.HandleFunc("/", service.HandleIndexAnd404(d))
handlerAuth := NewHandlerAuth(d, service.NewServiceAuthImpl(db.NewDbAuthSqlite(d)))
// Serve static files (CSS, JS and images)
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
handleWorkout(d, router)
handlerAuth.handle(router)
return middleware.Logging(middleware.EnableCors(router))
}
func auth(db *sql.DB, h http.Handler) http.Handler {
return middleware.EnsureValidSession(db, h)
}

50
handler/index_and_404.go Normal file
View File

@@ -0,0 +1,50 @@
package handler
import (
"web-app-template/handler/middleware"
"web-app-template/service"
"web-app-template/template"
"net/http"
"github.com/a-h/templ"
)
type Index interface {
Handle(router *http.ServeMux)
}
type IndexImpl struct {
service service.Auth
render *Render
}
func NewIndex(service service.Auth, render *Render) Index {
return IndexImpl{
service: service,
render: render,
}
}
func (handler IndexImpl) Handle(router *http.ServeMux) {
router.Handle("/", handler.handleIndexAnd404())
}
func (handler IndexImpl) handleIndexAnd404() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
var comp templ.Component
var status int
if r.URL.Path != "/" {
comp = template.NotFound()
status = http.StatusNotFound
} else {
comp = template.Index()
status = http.StatusOK
}
handler.render.RenderLayoutWithStatus(r, w, comp, user, status)
}
}

View File

@@ -0,0 +1,71 @@
package middleware
import (
"context"
"net/http"
"web-app-template/service"
"web-app-template/types"
)
type ContextKey string
var SessionKey ContextKey = "session"
var UserKey ContextKey = "user"
func Authenticate(service service.Auth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sessionId := getSessionID(r)
session, user, _ := service.SignInSession(sessionId)
var err error
// Always sign in anonymous
// This way, we can always generate csrf tokens
if session == nil {
session, err = service.SignInAnonymous()
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
cookie := CreateSessionCookie(session.Id)
http.SetCookie(w, &cookie)
}
ctx := r.Context()
ctx = context.WithValue(ctx, UserKey, user)
ctx = context.WithValue(ctx, SessionKey, session)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func GetUser(r *http.Request) *types.User {
obj := r.Context().Value(UserKey)
if obj == nil {
return nil
}
return obj.(*types.User)
}
func GetSession(r *http.Request) *types.Session {
obj := r.Context().Value(SessionKey)
if obj == nil {
return nil
}
return obj.(*types.Session)
}
func getSessionID(r *http.Request) string {
cookie, err := r.Cookie("id")
if err != nil {
return ""
}
return cookie.Value
}

View File

@@ -0,0 +1,23 @@
package middleware
import (
"net/http"
"strings"
)
func CacheControl(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
cached := false
if strings.HasPrefix(path, "/static") {
cached = true
}
if !cached {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
}
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,74 @@
package middleware
import (
"fmt"
"net/http"
"strings"
"web-app-template/log"
"web-app-template/service"
"web-app-template/types"
"web-app-template/utils"
)
type csrfResponseWriter struct {
http.ResponseWriter
auth service.Auth
session *types.Session
}
func newCsrfResponseWriter(w http.ResponseWriter, auth service.Auth, session *types.Session) *csrfResponseWriter {
return &csrfResponseWriter{
ResponseWriter: w,
auth: auth,
session: session,
}
}
func (rr *csrfResponseWriter) Write(data []byte) (int, error) {
dataStr := string(data)
csrfToken, err := rr.auth.GetCsrfToken(rr.session)
if err == nil {
csrfInput := fmt.Sprintf(`<input type="hidden" name="csrf-token" value="%s" />`, csrfToken)
dataStr = strings.ReplaceAll(dataStr, "</form>", csrfInput+"</form>")
dataStr = strings.ReplaceAll(dataStr, "CSRF_TOKEN", csrfToken)
}
return rr.ResponseWriter.Write([]byte(dataStr))
}
func (rr *csrfResponseWriter) WriteHeader(statusCode int) {
rr.ResponseWriter.WriteHeader(statusCode)
}
func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := GetSession(r)
if r.Method == http.MethodPost ||
r.Method == http.MethodPut ||
r.Method == http.MethodDelete ||
r.Method == http.MethodPatch {
csrfToken := r.FormValue("csrf-token")
if csrfToken == "" {
csrfToken = r.Header.Get("csrf-token")
}
if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) {
log.Info("CSRF-Token not correct")
if r.Header.Get("HX-Request") == "true" {
utils.TriggerToast(w, r, "error", "CSRF-Token not correct", http.StatusBadRequest)
} else {
http.Error(w, "CSRF-Token not correct", http.StatusBadRequest)
}
return
}
}
responseWriter := newCsrfResponseWriter(w, auth, session)
next.ServeHTTP(responseWriter, r)
})
}
}

View File

@@ -0,0 +1,15 @@
package middleware
import "net/http"
func CreateSessionCookie(sessionId string) http.Cookie {
return http.Cookie{
Name: "id",
Value: sessionId,
MaxAge: 60 * 60 * 8, // 8 hours
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
}
}

View File

@@ -1,11 +1,12 @@
package middleware
import (
"log/slog"
"net/http"
"strconv"
"time"
"web-app-template/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
@@ -30,7 +31,7 @@ func (w *WrappedWriter) WriteHeader(code int) {
w.StatusCode = code
}
func Logging(next http.Handler) http.Handler {
func Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
@@ -40,7 +41,7 @@ func Logging(next http.Handler) http.Handler {
}
next.ServeHTTP(wrapped, r)
slog.Info(r.RemoteAddr + " " + strconv.Itoa(wrapped.StatusCode) + " " + r.Method + " " + r.URL.Path + " " + time.Since(start).String())
log.Info(r.RemoteAddr + " " + strconv.Itoa(wrapped.StatusCode) + " " + r.Method + " " + r.URL.Path + " " + time.Since(start).String())
metrics.WithLabelValues(r.URL.Path, r.Method, http.StatusText(wrapped.StatusCode)).Inc()
})
}

View File

@@ -0,0 +1,40 @@
package middleware
import (
"net/http"
"web-app-template/types"
)
func SecurityHeaders(serverSettings *types.Settings) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Access-Control-Allow-Origin", serverSettings.BaseUrl)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")
w.Header().Set("Content-Security-Policy",
"default-src 'none'; "+
"script-src 'self'; "+
"connect-src 'self'; "+
"img-src 'self'; "+
"style-src 'self'; "+
"form-action 'self'; "+
"frame-ancestors 'none'; ",
)
w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
w.Header().Set("Permissions-Policy", "geolocation=(), camera=(), microphone=(), interest-cohort=()")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
}

View File

@@ -0,0 +1,13 @@
package middleware
import "net/http"
func Wrapper(next http.Handler, handlers ...func(http.Handler) http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lastHandler := next
for i := len(handlers) - 1; i >= 0; i-- {
lastHandler = handlers[i](lastHandler)
}
lastHandler.ServeHTTP(w, r)
})
}

53
handler/render.go Normal file
View File

@@ -0,0 +1,53 @@
package handler
import (
"web-app-template/log"
"web-app-template/template"
"web-app-template/template/auth"
"web-app-template/types"
"net/http"
"github.com/a-h/templ"
)
type Render struct {
}
func NewRender() *Render {
return &Render{}
}
func (render *Render) RenderWithStatus(r *http.Request, w http.ResponseWriter, comp templ.Component, status int) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(status)
err := comp.Render(r.Context(), w)
if err != nil {
log.Error("Failed to render layout: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
func (render *Render) Render(r *http.Request, w http.ResponseWriter, comp templ.Component) {
render.RenderWithStatus(r, w, comp, http.StatusOK)
}
func (render *Render) RenderLayout(r *http.Request, w http.ResponseWriter, slot templ.Component, user *types.User) {
render.RenderLayoutWithStatus(r, w, slot, user, http.StatusOK)
}
func (render *Render) RenderLayoutWithStatus(r *http.Request, w http.ResponseWriter, slot templ.Component, user *types.User, status int) {
userComp := render.getUserComp(user)
layout := template.Layout(slot, userComp)
render.RenderWithStatus(r, w, layout, status)
}
func (render *Render) getUserComp(user *types.User) templ.Component {
if user != nil {
return auth.UserComp(user.Email)
} else {
return auth.UserComp("")
}
}

View File

@@ -1,15 +1,129 @@
package handler
import (
"me-fit/service"
"web-app-template/handler/middleware"
"web-app-template/service"
"web-app-template/template/workout"
"web-app-template/utils"
"database/sql"
"net/http"
"strconv"
"time"
)
func handleWorkout(db *sql.DB, router *http.ServeMux) {
router.Handle("/workout", auth(db, service.HandleWorkoutPage(db)))
router.Handle("POST /api/workout", auth(db, service.HandleWorkoutNewComp(db)))
router.Handle("GET /api/workout", auth(db, service.HandleWorkoutGetComp(db)))
router.Handle("DELETE /api/workout/{id}", auth(db, service.HandleWorkoutDeleteComp(db)))
type Workout interface {
Handle(router *http.ServeMux)
}
type WorkoutImpl struct {
service service.Workout
auth service.Auth
render *Render
}
func NewWorkout(service service.Workout, auth service.Auth, render *Render) Workout {
return WorkoutImpl{
service: service,
auth: auth,
render: render,
}
}
func (handler WorkoutImpl) Handle(router *http.ServeMux) {
router.Handle("/workout", handler.handleWorkoutPage())
router.Handle("POST /api/workout", handler.handleAddWorkout())
router.Handle("GET /api/workout", handler.handleGetWorkout())
router.Handle("DELETE /api/workout/{id}", handler.handleDeleteWorkout())
}
func (handler WorkoutImpl) handleWorkoutPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
currentDate := time.Now().Format("2006-01-02")
comp := workout.WorkoutComp(currentDate)
handler.render.RenderLayout(r, w, comp, user)
}
}
func (handler WorkoutImpl) handleAddWorkout() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
var dateStr = r.FormValue("date")
var typeStr = r.FormValue("type")
var setsStr = r.FormValue("sets")
var repsStr = r.FormValue("reps")
wo := service.NewWorkoutDto("", dateStr, typeStr, setsStr, repsStr)
wo, err := handler.service.AddWorkout(user, wo)
if err != nil {
utils.TriggerToast(w, r, "error", "Invalid input values", http.StatusBadRequest)
http.Error(w, "Invalid input values", http.StatusBadRequest)
return
}
wor := workout.Workout{Id: wo.RowId, Date: wo.Date, Type: wo.Type, Sets: wo.Sets, Reps: wo.Reps}
comp := workout.WorkoutItemComp(wor, true)
handler.render.Render(r, w, comp)
}
}
func (handler WorkoutImpl) handleGetWorkout() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
workouts, err := handler.service.GetWorkouts(user)
if err != nil {
return
}
wos := make([]workout.Workout, 0)
for _, wo := range workouts {
wos = append(wos, workout.Workout{Id: wo.RowId, Date: wo.Date, Type: wo.Type, Sets: wo.Sets, Reps: wo.Reps})
}
comp := workout.WorkoutListComp(wos)
handler.render.Render(r, w, comp)
}
}
func (handler WorkoutImpl) handleDeleteWorkout() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
rowId := r.PathValue("id")
if rowId == "" {
utils.TriggerToast(w, r, "error", "Missing ID field", http.StatusBadRequest)
return
}
rowIdInt, err := strconv.Atoi(rowId)
if err != nil {
utils.TriggerToast(w, r, "error", "Invalid ID", http.StatusBadRequest)
return
}
err = handler.service.DeleteWorkout(user, rowIdInt)
if err != nil {
utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
return
}
}
}

18
input.css Normal file
View File

@@ -0,0 +1,18 @@
@import 'tailwindcss';
@source './static/**/*.js';
@source './template/**/*.templ';
@theme {
--animate-fade: fadeOut 0.25s ease-in;
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}

56
log/default.go Normal file
View File

@@ -0,0 +1,56 @@
package log
import (
"fmt"
"log"
"log/slog"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
errorMetric = promauto.NewCounter(
prometheus.CounterOpts{
Name: "mefit_error_total",
Help: "The total number of errors during processing",
},
)
)
func Fatal(message string, args ...interface{}) {
s := format(message, args)
log.Fatal(s)
errorMetric.Inc()
}
func Error(message string, args ...interface{}) {
s := format(message, args)
slog.Error(s)
errorMetric.Inc()
}
func Warn(message string, args ...interface{}) {
s := format(message, args)
slog.Warn(s)
}
func Info(message string, args ...interface{}) {
s := format(message, args)
slog.Info(s)
}
func format(message string, args []interface{}) string {
var w strings.Builder
if len(args) > 0 {
fmt.Fprintf(&w, message, args...)
} else {
w.WriteString(message)
}
return w.String()
}

107
main.go
View File

@@ -1,14 +1,17 @@
package main
import (
"me-fit/handler"
"me-fit/utils"
"web-app-template/db"
"web-app-template/handler"
"web-app-template/handler/middleware"
"web-app-template/log"
"web-app-template/service"
"web-app-template/types"
"context"
"database/sql"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
@@ -20,40 +23,49 @@ import (
)
func main() {
run(context.Background())
}
func run(ctx context.Context) {
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer cancel()
slog.Info("Starting server...")
// init env
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
utils.MustInitEnv()
// init db
db, err := sql.Open("sqlite3", "./data.db")
if err != nil {
log.Fatal("Could not open Database data.db: ", err)
log.Fatal("Could not open Database data.db: %v", err)
}
defer db.Close()
utils.MustRunMigrations(db, "")
run(context.Background(), db, os.Getenv)
}
func run(ctx context.Context, database *sql.DB, env func(string) string) {
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer cancel()
log.Info("Starting server...")
// init server settings
serverSettings := types.NewSettingsFromEnv(env)
// init db
err := db.RunMigrations(database, "")
if err != nil {
log.Fatal("Could not run migrations: %v", err)
}
// init servers
prometheusServer := &http.Server{
Addr: ":8081",
Handler: promhttp.Handler(),
var prometheusServer *http.Server
if serverSettings.PrometheusEnabled {
prometheusServer := &http.Server{
Addr: ":8081",
Handler: promhttp.Handler(),
}
go startServer(prometheusServer)
}
httpServer := &http.Server{
Addr: ":8080",
Handler: handler.GetHandler(db),
Addr: ":" + serverSettings.Port,
Handler: createHandler(database, serverSettings),
}
go startServer(prometheusServer)
go startServer(httpServer)
// graceful shutdown
@@ -65,21 +77,60 @@ func run(ctx context.Context) {
}
func startServer(s *http.Server) {
slog.Info("Starting server on " + s.Addr)
log.Info("Starting server on %q", s.Addr)
if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("error listening and serving: " + err.Error())
log.Error("error listening and serving: %v", err)
}
}
func shutdownServer(s *http.Server, ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
if s == nil {
return
}
<-ctx.Done()
shutdownCtx := context.Background()
shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second)
defer cancel()
if err := s.Shutdown(shutdownCtx); err != nil {
slog.Error("error shutting down http server: " + err.Error())
log.Error("error shutting down http server: %v", err)
} else {
slog.Info("Gracefully stopped http server on " + s.Addr)
log.Info("Gracefully stopped http server on %v", s.Addr)
}
}
func createHandler(d *sql.DB, serverSettings *types.Settings) http.Handler {
var router = http.NewServeMux()
authDb := db.NewAuthSqlite(d)
workoutDb := db.NewWorkoutDbSqlite(d)
randomService := service.NewRandomImpl()
clockService := service.NewClockImpl()
mailService := service.NewMailImpl(serverSettings)
authService := service.NewAuthImpl(authDb, randomService, clockService, mailService, serverSettings)
workoutService := service.NewWorkoutImpl(workoutDb, randomService, clockService, mailService, serverSettings)
render := handler.NewRender()
indexHandler := handler.NewIndex(authService, render)
authHandler := handler.NewAuth(authService, render)
workoutHandler := handler.NewWorkout(workoutService, authService, render)
indexHandler.Handle(router)
workoutHandler.Handle(router)
authHandler.Handle(router)
// Serve static files (CSS, JS and images)
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
return middleware.Wrapper(
router,
middleware.Log,
middleware.CacheControl,
middleware.SecurityHeaders(serverSettings),
middleware.Authenticate(authService),
middleware.CrossSiteRequestForgery(authService),
)
}

1746
main_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
package middleware
import (
"me-fit/utils"
"context"
"database/sql"
"net/http"
)
func EnsureValidSession(db *sql.DB, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := utils.GetUserFromSession(db, r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
if !user.EmailVerified && r.URL.Path != "/auth/verify" {
utils.DoRedirect(w, r, "/auth/verify")
return
}
ctx := context.WithValue(r.Context(), utils.ContextKeyUser, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -1,22 +0,0 @@
package middleware
import (
"me-fit/utils"
"net/http"
)
func EnableCors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", utils.BaseUrl)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -1,4 +1,40 @@
CREATE TABLE user (
user_id TEXT NOT NULL UNIQUE PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
email_verified BOOLEAN NOT NULL,
email_verified_at DATETIME,
is_admin BOOLEAN NOT NULL,
password BLOB NOT NULL,
salt BLOB NOT NULL,
created_at DATETIME NOT NULL
) WITHOUT ROWID;
CREATE TABLE session (
session_id TEXT NOT NULL UNIQUE PRIMARY KEY,
user_id TEXT NOT NULL,
created_at DATETIME NOT NULL,
expires_at DATETIME NOT NULL
) WITHOUT ROWID;
CREATE TABLE token (
token TEXT NOT NULL UNIQUE PRIMARY KEY,
user_id TEXT,
session_id TEXT,
type TEXT NOT NULL,
created_at DATETIME NOT NULL,
expires_at DATETIME
);
CREATE TABLE workout (
user_id INTEGER NOT NULL,
date TEXT NOT NULL,
@@ -6,4 +42,3 @@ CREATE TABLE workout (
sets INTEGER NOT NULL,
reps INTEGER NOT NULL
);

View File

@@ -1,21 +0,0 @@
CREATE TABLE user (
user_uuid TEXT NOT NULL UNIQUE PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
email_verified BOOLEAN NOT NULL,
is_admin BOOLEAN NOT NULL,
password BLOB NOT NULL,
salt BLOB NOT NULL,
created_at DATETIME NOT NULL
) WITHOUT ROWID;
CREATE TABLE session (
session_id TEXT NOT NULL UNIQUE PRIMARY KEY,
user_uuid TEXT NOT NULL,
created_at DATETIME NOT NULL
) WITHOUT ROWID;

View File

@@ -1,2 +0,0 @@
ALTER TABLE user ADD COLUMN email_verified_at DATETIME DEFAULT NULL;

View File

@@ -1,11 +0,0 @@
-- E.G. email-verifications, password-resets, unsubscribe-from-newsletter etc.
CREATE TABLE user_token (
user_uuid TEXT NOT NULL,
type TEXT NOT NULL,
token TEXT NOT NULL UNIQUE PRIMARY KEY,
created_at DATETIME NOT NULL,
expires_at DATETIME
);

1
mocks/default.go Normal file
View File

@@ -0,0 +1 @@
package mocks

2218
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,18 @@
{
"name": "me-fit",
"name": "web-app-template",
"version": "1.0.0",
"description": "Your (almost) independent tech stack to host on a VPC.",
"main": "index.js",
"scripts": {
"build": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss build -o static/css/tailwind.css --minify",
"watch": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss build -o static/css/tailwind.css --watch",
"test": ""
"build": "mkdir -p static/js && 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",
"watch": "mkdir -p static/js && 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"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"htmx.org": "2.0.2",
"tailwindcss": "3.4.13",
"daisyui": "4.12.10"
"htmx.org": "2.0.6",
"tailwindcss": "4.1.11",
"@tailwindcss/cli": "4.1.11"
}
}

View File

@@ -1,10 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:best-practices"
],
"packageRules": [{
"matchUpdateTypes": ["minor", "patch", "digest", "pinDigest"],
"matchCurrentVersion": "!/^0/",
"automerge": true
}]
"local>x/renovate-config"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,107 +1,138 @@
package service
import (
"me-fit/db"
"me-fit/types"
"web-app-template/db"
"web-app-template/mocks"
"web-app-template/types"
"errors"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type DbAuthStub struct {
user *db.User
err error
}
func (d DbAuthStub) GetUser(email string) (*db.User, error) {
return d.user, d.err
}
func TestSignIn(t *testing.T) {
func TestSignUp(t *testing.T) {
t.Parallel()
t.Run("should return user if password is correct", func(t *testing.T) {
t.Run("should check for correct email address", func(t *testing.T) {
t.Parallel()
mockAuthDb := mocks.NewMockAuth(t)
mockRandom := mocks.NewMockRandom(t)
mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t)
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
_, err := underTest.SignUp("invalid email address", "SomeStrongPassword123!")
assert.Equal(t, ErrInvalidEmail, err)
})
t.Run("should check for password complexity", func(t *testing.T) {
t.Parallel()
mockAuthDb := mocks.NewMockAuth(t)
mockRandom := mocks.NewMockRandom(t)
mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t)
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
weakPasswords := []string{
"123!ab", // too short
"no_upper_case_123",
"NO_LOWER_CASE_123",
"noSpecialChar123",
}
for _, password := range weakPasswords {
_, err := underTest.SignUp("some@valid.email", password)
assert.Equal(t, ErrInvalidPassword, err)
}
})
t.Run("should signup correctly", func(t *testing.T) {
t.Parallel()
mockAuthDb := mocks.NewMockAuth(t)
mockRandom := mocks.NewMockRandom(t)
mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t)
userId := uuid.New()
email := "mail@mail.de"
password := "SomeStrongPassword123!"
salt := []byte("salt")
stub := DbAuthStub{
user: db.NewUser(
uuid.New(),
"test@test.de",
true,
time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC),
false,
getHashPassword("password", salt),
salt,
time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
),
err: nil,
}
underTest := NewServiceAuthImpl(stub)
createTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
actualUser, err := underTest.SignIn("test@test.de", "password")
if err != nil {
t.Errorf("Expected nil, got %v", err)
}
expected := types.NewUser(userId, email, false, nil, false, GetHashPassword(password, salt), salt, createTime)
expectedUser := User{
Id: stub.user.Id,
Email: stub.user.Email,
EmailVerified: stub.user.EmailVerified,
}
if *actualUser != expectedUser {
t.Errorf("Expected %v, got %v", expectedUser, actualUser)
}
mockRandom.EXPECT().UUID().Return(userId, nil)
mockRandom.EXPECT().Bytes(16).Return(salt, nil)
mockClock.EXPECT().Now().Return(createTime)
mockAuthDb.EXPECT().InsertUser(expected).Return(nil)
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
actual, err := underTest.SignUp(email, password)
assert.Nil(t, err)
assert.Equal(t, expected, actual)
})
t.Run("should return ErrInvalidCretentials if password is not correct", func(t *testing.T) {
t.Run("should return ErrAccountExists", func(t *testing.T) {
t.Parallel()
mockAuthDb := mocks.NewMockAuth(t)
mockRandom := mocks.NewMockRandom(t)
mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t)
userId := uuid.New()
email := "some@valid.email"
createTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
password := "SomeStrongPassword123!"
salt := []byte("salt")
stub := DbAuthStub{
user: db.NewUser(
uuid.New(),
"test@test.de",
true,
time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC),
false,
getHashPassword("password", salt),
salt,
time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
),
err: nil,
}
underTest := NewServiceAuthImpl(stub)
user := types.NewUser(userId, email, false, nil, false, GetHashPassword(password, salt), salt, createTime)
_, err := underTest.SignIn("test@test.de", "wrong password")
if err != ErrInvaidCredentials {
t.Errorf("Expected %v, got %v", ErrInvaidCredentials, err)
}
})
t.Run("should return ErrInvalidCretentials if user has not been found", func(t *testing.T) {
t.Parallel()
stub := DbAuthStub{
user: nil,
err: db.ErrUserNotFound,
}
underTest := NewServiceAuthImpl(stub)
mockRandom.EXPECT().UUID().Return(user.Id, nil)
mockRandom.EXPECT().Bytes(16).Return(salt, nil)
mockClock.EXPECT().Now().Return(createTime)
_, err := underTest.SignIn("test", "test")
if err != ErrInvaidCredentials {
t.Errorf("Expected %v, got %v", ErrInvaidCredentials, err)
}
})
t.Run("should forward ErrInternal on any other error", func(t *testing.T) {
t.Parallel()
stub := DbAuthStub{
user: nil,
err: errors.New("Some error"),
}
underTest := NewServiceAuthImpl(stub)
mockAuthDb.EXPECT().InsertUser(user).Return(db.ErrAlreadyExists)
_, err := underTest.SignIn("test", "test")
if err != types.ErrInternal {
t.Errorf("Expected %v, got %v", types.ErrInternal, err)
}
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
_, err := underTest.SignUp(user.Email, password)
assert.Equal(t, ErrAccountExists, err)
})
}
func TestSendVerificationMail(t *testing.T) {
t.Parallel()
t.Run("should use stored token and send mail", func(t *testing.T) {
t.Parallel()
token := types.NewToken(uuid.New(), "sessionId", "someRandomTokenToUse", types.TokenTypeEmailVerify, time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC))
tokens := []*types.Token{token}
email := "some@email.de"
userId := uuid.New()
mockAuthDb := mocks.NewMockAuth(t)
mockRandom := mocks.NewMockRandom(t)
mockClock := mocks.NewMockClock(t)
mockMail := mocks.NewMockMail(t)
mockAuthDb.EXPECT().GetTokensByUserIdAndType(userId, types.TokenTypeEmailVerify).Return(tokens, nil)
mockMail.EXPECT().SendMail(email, "Welcome to web-app-template", mock.MatchedBy(func(message string) bool {
return strings.Contains(message, token.Token)
})).Return()
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
underTest.SendVerificationMail(userId, email)
})
}

17
service/clock.go Normal file
View File

@@ -0,0 +1,17 @@
package service
import "time"
type Clock interface {
Now() time.Time
}
type ClockImpl struct{}
func NewClockImpl() Clock {
return &ClockImpl{}
}
func (c *ClockImpl) Now() time.Time {
return time.Now()
}

View File

@@ -1,32 +0,0 @@
package service
import (
"database/sql"
"me-fit/template"
"me-fit/utils"
"net/http"
"github.com/a-h/templ"
)
func HandleIndexAnd404(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := utils.GetUserFromSession(db, r)
var comp templ.Component = nil
userComp := UserInfoComp(user)
if r.URL.Path != "/" {
comp = template.Layout(template.NotFound(), userComp)
w.WriteHeader(http.StatusNotFound)
} else {
comp = template.Layout(template.Index(), userComp)
}
err := comp.Render(r.Context(), w)
if err != nil {
utils.LogError("Failed to render index", err)
http.Error(w, "Failed to render index", http.StatusInternalServerError)
}
}
}

44
service/mail.go Normal file
View File

@@ -0,0 +1,44 @@
package service
import (
"web-app-template/log"
"web-app-template/types"
"fmt"
"net/smtp"
)
type Mail interface {
// Sending an email is a fire and forget operation. Thus no error handling
SendMail(to string, subject string, message string)
}
type MailImpl struct {
server *types.Settings
}
func NewMailImpl(server *types.Settings) MailImpl {
return MailImpl{server: server}
}
func (m MailImpl) SendMail(to string, subject string, message string) {
go m.internalSendMail(to, subject, message)
}
func (m MailImpl) internalSendMail(to string, subject string, message string) {
if m.server.Smtp == nil {
return
}
s := m.server.Smtp
auth := smtp.PlainAuth("", s.User, s.Pass, s.Host)
msg := fmt.Sprintf("From: %v <%v>\nTo: %v\nSubject: %v\nMIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n%v", s.FromName, s.FromMail, to, subject, message)
log.Info("Sending mail to %v", to)
err := smtp.SendMail(s.Host+":"+s.Port, auth, s.FromMail, []string{to}, []byte(msg))
if err != nil {
log.Error("Error sending mail: %v", err)
}
}

View File

@@ -0,0 +1,49 @@
package service
import (
"web-app-template/log"
"web-app-template/types"
"crypto/rand"
"encoding/base64"
"github.com/google/uuid"
)
type Random interface {
Bytes(size int) ([]byte, error)
String(size int) (string, error)
UUID() (uuid.UUID, error)
}
type RandomImpl struct {
}
func NewRandomImpl() *RandomImpl {
return &RandomImpl{}
}
func (r *RandomImpl) Bytes(size int) ([]byte, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
log.Error("Error generating random bytes: %v", err)
return []byte{}, types.ErrInternal
}
return b, nil
}
func (r *RandomImpl) String(size int) (string, error) {
bytes, err := r.Bytes(size)
if err != nil {
log.Error("Error generating random string: %v", err)
return "", types.ErrInternal
}
return base64.StdEncoding.EncodeToString(bytes), nil
}
func (r *RandomImpl) UUID() (uuid.UUID, error) {
return uuid.NewRandom()
}

View File

@@ -1,191 +1,128 @@
package service
import (
"log/slog"
"me-fit/template"
"me-fit/template/workout"
"me-fit/utils"
"web-app-template/db"
"web-app-template/types"
"database/sql"
"net/http"
"errors"
"strconv"
"time"
)
func HandleWorkoutPage(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := utils.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
type Workout interface {
AddWorkout(user *types.User, workoutDto *WorkoutDto) (*WorkoutDto, error)
DeleteWorkout(user *types.User, rowId int) error
GetWorkouts(user *types.User) ([]*WorkoutDto, error)
}
currentDate := time.Now().Format("2006-01-02")
inner := workout.WorkoutComp(currentDate)
userComp := UserInfoComp(user)
err := template.Layout(inner, userComp).Render(r.Context(), w)
if err != nil {
utils.LogError("Failed to render workout page", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
type WorkoutImpl struct {
db db.WorkoutDb
random Random
clock Clock
mail Mail
settings *types.Settings
}
func NewWorkoutImpl(db db.WorkoutDb, random Random, clock Clock, mail Mail, settings *types.Settings) Workout {
return WorkoutImpl{
db: db,
random: random,
clock: clock,
mail: mail,
settings: settings,
}
}
func HandleWorkoutNewComp(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := utils.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
type WorkoutDto struct {
RowId string
Date string
Type string
Sets string
Reps string
}
var dateStr = r.FormValue("date")
var typeStr = r.FormValue("type")
var setsStr = r.FormValue("sets")
var repsStr = r.FormValue("reps")
if dateStr == "" || typeStr == "" || setsStr == "" || repsStr == "" {
utils.TriggerToast(w, r, "error", "Missing required fields")
http.Error(w, "Missing required fields", http.StatusBadRequest)
return
}
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
utils.TriggerToast(w, r, "error", "Invalid date")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sets, err := strconv.Atoi(setsStr)
if err != nil {
utils.TriggerToast(w, r, "error", "Invalid number")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
reps, err := strconv.Atoi(repsStr)
if err != nil {
utils.TriggerToast(w, r, "error", "Invalid number")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var rowId int
err = db.QueryRow("INSERT INTO workout (user_id, date, type, sets, reps) VALUES (?, ?, ?, ?, ?) RETURNING rowid", user.Id, date, typeStr, sets, reps).Scan(&rowId)
if err != nil {
utils.LogError("Could not insert workout", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
wo := workout.Workout{
Id: strconv.Itoa(rowId),
Date: renderDate(date),
Type: r.FormValue("type"),
Sets: r.FormValue("sets"),
Reps: r.FormValue("reps"),
}
err = workout.WorkoutItemComp(wo, true).Render(r.Context(), w)
if err != nil {
utils.LogError("Could not render workoutitem", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
http.Error(w, err.Error(), http.StatusInternalServerError)
}
func NewWorkoutDtoFromDb(workout *db.Workout) *WorkoutDto {
return &WorkoutDto{
RowId: strconv.Itoa(workout.RowId),
Date: renderDate(workout.Date),
Type: workout.Type,
Sets: strconv.Itoa(workout.Sets),
Reps: strconv.Itoa(workout.Reps),
}
}
func NewWorkoutDto(rowId string, date string, workoutType string, sets string, reps string) *WorkoutDto {
return &WorkoutDto{
RowId: rowId,
Date: date,
Type: workoutType,
Sets: sets,
Reps: reps,
}
}
func HandleWorkoutGetComp(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := utils.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
var (
ErrInputValues = errors.New("invalid input values")
)
rows, err := db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ? ORDER BY date desc", user.Id)
if err != nil {
utils.LogError("Could not get workouts", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
func (service WorkoutImpl) AddWorkout(user *types.User, workoutDto *WorkoutDto) (*WorkoutDto, error) {
var workouts = make([]workout.Workout, 0)
for rows.Next() {
var workout workout.Workout
err = rows.Scan(&workout.Id, &workout.Date, &workout.Type, &workout.Sets, &workout.Reps)
if err != nil {
utils.LogError("Could not scan workout", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
workout.Date, err = renderDateStr(workout.Date)
if err != nil {
utils.LogError("Could not render date", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
workouts = append(workouts, workout)
}
workout.WorkoutListComp(workouts).Render(r.Context(), w)
if workoutDto.Date == "" || workoutDto.Type == "" || workoutDto.Sets == "" || workoutDto.Reps == "" {
return nil, ErrInputValues
}
}
func HandleWorkoutDeleteComp(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := utils.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
rowId := r.PathValue("id")
if rowId == "" {
http.Error(w, "Missing required fields", http.StatusBadRequest)
slog.Warn("Missing required fields for workout delete")
utils.TriggerToast(w, r, "error", "Missing ID field")
return
}
res, err := db.Exec("DELETE FROM workout WHERE user_id = ? AND rowid = ?", user.Id, rowId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
utils.LogError("Could not delete workout", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
rows, err := res.RowsAffected()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
utils.LogError("Could not get rows affected", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
if rows == 0 {
http.Error(w, "Not found", http.StatusNotFound)
slog.Warn("Could not find workout to delete")
utils.TriggerToast(w, r, "error", "Not found. Refresh the page.")
return
}
}
}
func renderDateStr(date string) (string, error) {
t, err := time.Parse("2006-01-02 15:04:05-07:00", date)
date, err := time.Parse("2006-01-02", workoutDto.Date)
if err != nil {
return "", err
return nil, ErrInputValues
}
return renderDate(t), nil
sets, err := strconv.Atoi(workoutDto.Sets)
if err != nil {
return nil, ErrInputValues
}
reps, err := strconv.Atoi(workoutDto.Reps)
if err != nil {
return nil, ErrInputValues
}
workoutInsert := db.NewWorkoutInsert(date, workoutDto.Type, sets, reps)
workout, err := service.db.InsertWorkout(user.Id, workoutInsert)
if err != nil {
return nil, err
}
return NewWorkoutDtoFromDb(workout), nil
}
func (service WorkoutImpl) DeleteWorkout(user *types.User, rowId int) error {
if user == nil {
return types.ErrInternal
}
return service.db.DeleteWorkout(user.Id, rowId)
}
func (service WorkoutImpl) GetWorkouts(user *types.User) ([]*WorkoutDto, error) {
if user == nil {
return nil, types.ErrInternal
}
workouts, err := service.db.GetWorkouts(user.Id)
if err != nil {
return nil, err
}
// for _, workout := range workouts {
// workout.Date = renderDate(workout.Date)
// }
workoutsDto := make([]*WorkoutDto, len(workouts))
for i, workout := range workouts {
workoutsDto[i] = NewWorkoutDtoFromDb(&workout)
}
return workoutsDto, nil
}
func renderDate(date time.Time) string {

View File

@@ -1,26 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./template/**/*.templ", "./static/**/*.js"],
theme: {
extend: {
animation: {
fade: 'fadeOut 0.25s ease-in',
},
keyframes: _ => ({
fadeOut: {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
}),
},
},
plugins: [
require('daisyui'),
],
daisyui: {
themes: ["retro"],
},
}

View File

@@ -4,7 +4,7 @@ templ ChangePasswordComp(isPasswordReset bool) {
<form
class="max-w-xl px-2 mx-auto flex flex-col gap-4 h-full justify-center"
if isPasswordReset {
hx-post="/api/auth/reset-password-actual"
hx-post="/api/auth/forgot-password-actual"
} else {
hx-post="/api/auth/change-password"
}
@@ -15,11 +15,29 @@ templ ChangePasswordComp(isPasswordReset bool) {
</h2>
if !isPasswordReset {
<label class="input input-bordered flex items-center gap-2">
<input type="password" class="grow" placeholder="Current Password" name="current-password"/>
<input
type="password"
class="grow"
placeholder="Current Password"
name="current-password"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
</label>
}
<label class="input input-bordered flex items-center gap-2">
<input type="password" class="grow" placeholder="New Password" name="new-password"/>
<input
type="password"
class="grow"
placeholder="New Password"
name="new-password"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
</label>
<button class="btn btn-primary self-end">
Change Password

View File

@@ -12,10 +12,19 @@ templ DeleteAccountComp() {
<p class="text-xl text-red-500 mb-4">
Are you sure you want to delete your account? This action is irreversible.
</p>
<label class="input input-bordered flex items-center gap-2">
<input type="password" class="grow" placeholder="Password" name="password"/>
<label class="flex items-center gap-2">
<input
type="password"
class="grow"
placeholder="Password"
name="password"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
</label>
<button class="btn btn-error self-end">
<button class="self-end">
Delete Account
</button>
</form>

View File

@@ -3,14 +3,23 @@ package auth
templ ResetPasswordComp() {
<form
class="max-w-xl px-2 mx-auto flex flex-col gap-4 h-full justify-center"
hx-post="/api/auth/reset-password"
hx-post="/api/auth/forgot-password"
hx-swap="none"
>
<h2 class="text-6xl mb-10">
Reset Password
</h2>
<label class="input input-bordered flex items-center gap-2">
<input type="email" class="grow" placeholder="E-Mail" name="email"/>
<input
type="email"
class="grow"
placeholder="E-Mail"
name="email"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
</label>
<button class="btn btn-primary self-end">
Request Password Reset

View File

@@ -1,14 +1,18 @@
package auth
templ SignInOrUpComp(isSignIn bool) {
{{
var postUrl string
if isSignIn {
postUrl = "/api/auth/signin"
} else {
postUrl = "/api/auth/signup"
}
}}
<form
class="max-w-xl px-2 mx-auto flex flex-col gap-4 h-full justify-center"
hx-target="#sign-in-or-up-error"
if isSignIn {
hx-post="/api/auth/signin"
} else {
hx-post="/api/auth/signup"
}
hx-post={ postUrl }
>
<h2 class="text-6xl mb-10">
if isSignIn {
@@ -18,12 +22,7 @@ templ SignInOrUpComp(isSignIn bool) {
}
</h2>
<label class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="h-4 w-4 opacity-70">
<path
d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z"
></path>
@@ -31,26 +30,39 @@ templ SignInOrUpComp(isSignIn bool) {
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z"
></path>
</svg>
<input type="text" class="grow" placeholder="Email" name="email"/>
<input
type="text"
class="grow"
placeholder="Email"
name="email"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
</label>
<label class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="h-4 w-4 opacity-70">
<path
fill-rule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clip-rule="evenodd"
></path>
</svg>
<input type="password" class="grow" placeholder="Password" name="password"/>
<input
type="password"
class="grow"
placeholder="Password"
name="password"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
</label>
<div class="flex justify-end items-center gap-2">
if isSignIn {
<a href="/auth/reset-password" class="grow link text-gray-500 text-sm">Forgot Password?</a>
<a href="/auth/forgot-password" class="grow link text-gray-500 text-sm">Forgot Password?</a>
<a href="/auth/signup" class="link text-gray-500 text-sm">Don't have an account? Sign Up</a>
<button class="btn btn-primary">
Sign In

View File

@@ -3,36 +3,28 @@ package auth
templ UserComp(user string) {
<div id="user-info" class="flex gap-5 items-center">
if user != "" {
<div class="group inline-block relative">
<button
class="font-semibold py-2 px-4 inline-flex items-center"
>
<div class="inline-block relative">
<button class="font-semibold py-2 px-4 inline-flex items-center">
<span class="mr-1">{ user }</span>
<svg
class="fill-current h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<path
d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
></path>
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"></path>
</svg>
</button>
<div class="absolute hidden group-hover:block w-full">
<ul class="menu bg-base-300 rounded-box w-fit float-right mr-4 p-3">
<ul class="w-fit float-right mr-4 p-3">
<li class="mb-1">
<a hx-get="/api/auth/signout" hx-target="#user-info">Sign Out</a>
<a hx-post="/api/auth/signout" hx-target="#user-info">Sign Out</a>
</li>
<li class="mb-1">
<a href="/auth/change-password">Change Password</a>
</li>
<li><a href="/auth/delete-account" class="text-error">Delete Account</a></li>
<li><a href="/auth/delete-account" class="">Delete Account</a></li>
</ul>
</div>
</div>
} else {
<a href="/auth/signup" class="btn btn-sm">Sign Up</a>
<a href="/auth/signin" class="btn btn-sm">Sign In</a>
<a href="/auth/signup" class="">Sign Up</a>
<a href="/auth/signin" class="">Sign In</a>
}
</div>
}

View File

@@ -12,7 +12,7 @@ templ VerifyComp() {
<p class="text-lg text-center">
Please check your inbox/spam and click on the link to verify your account.
</p>
<button class="btn mt-8" hx-get="/api/auth/verify-resend" hx-sync="this:drop" hx-swap="outerHTML">
<button class="mt-8" hx-get="/api/auth/verify-resend" hx-sync="this:drop" hx-swap="outerHTML">
resend verification email
</button>
</div>

View File

@@ -0,0 +1,29 @@
package auth
templ VerifyResponseComp(isVerified bool) {
<main>
<div class="flex flex-col items-center justify-center h-screen">
if isVerified {
<h2 class="text-6xl mb-10">
Your email has been verified
</h2>
<p class="text-lg text-center">
You have completed the verification process. Thank you!
</p>
<a class="mt-8" href="/">
Go Home
</a>
} else {
<h2 class="text-6xl mb-10">
Error during verification
</h2>
<p class="text-lg text-center">
Please try again by sign up process
</p>
<a class="mt-8" href="/auth/signup">
Sign Up
</a>
}
</div>
</main>
}

View File

@@ -1,15 +1,15 @@
package template
templ Index() {
<div class="hero bg-base-200 h-full">
<div class="hero-content text-center">
<div class="h-full">
<div class="text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Next Level Workout Tracker</h1>
<p class="py-6">
Ever wanted to track your workouts and see your progress over time? ME-FIT is the perfect
Ever wanted to track your workouts and see your progress over time? web-app-template is the perfect
solution for you.
</p>
<a href="/workout" class="btn btn-primary">Get Started</a>
<a href="/workout" class="">Get Started</a>
</div>
</div>
</div>

View File

@@ -1,28 +1,31 @@
package template
import "me-fit/utils"
templ Layout(slot templ.Component, user templ.Component) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>ME-FIT</title>
<title>web-app-template</title>
<link rel="icon" href="/static/favicon.svg"/>
<link rel="stylesheet" href="/static/css/tailwind.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
if utils.Environment == "prod" {
<script defer src="https://umami.me-fit.eu/script.js" data-website-id="3c8efb09-44e4-4372-8a1e-c3bc675cd89a"></script>
}
<meta
name="htmx-config"
content='{
"includeIndicatorStyles": false,
"selfRequestsOnly": true,
"allowScriptTags": false
}'
/>
<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/toast.js"></script>
</head>
<body>
<body hx-headers='{"csrf-token": "CSRF_TOKEN"}'>
<div class="h-screen flex flex-col">
<div class="flex justify-end items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2 shadow">
<div class="flex justify-end items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2 shadow-sm">
<a href="/" class="flex-1 flex gap-2">
<img src="/static/favicon.svg" alt="ME-FIT logo"/>
<span>ME-FIT</span>
<img src="/static/favicon.svg" alt="web-app-template logo"/>
<span>web-app-template</span>
</a>
@user
</div>
@@ -32,8 +35,8 @@ templ Layout(slot templ.Component, user templ.Component) {
}
</div>
</div>
<div class="toast" id="toasts">
<div class="hidden alert" id="toast">
<div class="" id="toasts">
<div class="hidden" id="toast">
New message arrived.
</div>
</div>

View File

@@ -1,22 +1,23 @@
package mail;
import (
"me-fit/utils"
"net/url"
)
import "net/url"
templ Register(token string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Welcome</title>
</head>
<body>
<h4>Thank you for Sign Up!</h4>
<p>Click <a href={ templ.URL(utils.BaseUrl + "/auth/verify-email?token=" + url.QueryEscape(token)) }>here</a> to verify your account.</p>
<p>Kind regards</p>
</body>
</html>
templ Register(baseUrl string, token string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome</title>
</head>
<body>
<h4>Thank you for Sign Up!</h4>
<p>Click <a href={ templ.URL(baseUrl + "/auth/verify-email?token=" + url.QueryEscape(token)) }>here</a> to finalize
your registration.</p>
<p>Kind regards</p>
</body>
</html>
}

View File

@@ -1,11 +1,8 @@
package mail;
import (
"me-fit/utils"
"net/url"
)
import "net/url"
templ ResetPassword(token string) {
templ ResetPassword(baseUrl string, token string) {
<!DOCTYPE html>
<html lang="en">
<head>
@@ -15,7 +12,7 @@ templ ResetPassword(token string) {
</head>
<body>
<h4>Reset your password</h4>
<p>Click <a href={ templ.URL(utils.BaseUrl + "/auth/change-password?token=" + url.QueryEscape(token)) }>here</a> to change your password.</p>
<p>Click <a href={ templ.URL(baseUrl + "/auth/change-password?token=" + url.QueryEscape(token)) }>here</a> to change your password.</p>
<p>Kind regards</p>
</body>
</html>

View File

@@ -1,11 +1,11 @@
package template
templ NotFound() {
<main class="flex h-full justify-center items-center ">
<div class="bg-error p-16 rounded-lg">
<h1 class="text-4xl text-error-content mb-5">Not Found</h1>
<p class="text-lg text-error-content mb-5">The page you are looking for does not exist.</p>
<a href="/" class="btn btn-lg btn-primary">Go back to home</a>
<main class="flex h-full justify-center items-center">
<div class="p-16 rounded-lg">
<h1 class="text-4xl mb-5">Not Found</h1>
<p class="text-lg mb-5">The page you are looking for does not exist.</p>
<a href="/" class="">Go back to home</a>
</div>
</main>
}

View File

@@ -9,30 +9,14 @@ templ WorkoutComp(currentDate string) {
hx-swap="outerHTML"
>
<h2 class="text-4xl mb-8">Track your workout</h2>
<input
id="date"
type="date"
class="input input-bordered"
value={ currentDate }
name="date"
/>
<select class="select select-bordered w-full" name="type">
<input id="date" type="date" class="" value={ currentDate } name="date"/>
<select class="w-full" name="type">
<option>Push Ups</option>
<option>Pull Ups</option>
</select>
<input
type="number"
class="input input-bordered"
placeholder="Sets"
name="sets"
/>
<input
type="number"
class="input input-bordered"
placeholder="Reps"
name="reps"
/>
<button class="btn btn-primary self-end">Save</button>
<input type="number" class="" placeholder="Sets" name="sets"/>
<input type="number" class="" placeholder="Reps" name="reps"/>
<button class="self-end">Save</button>
</form>
<div hx-get="/api/workout" hx-trigger="load"></div>
</main>
@@ -47,7 +31,7 @@ type Workout struct {
}
templ WorkoutListComp(workouts []Workout) {
<div class="overflow-x-auto mx-auto max-w-screen-lg">
<div class="overflow-x-auto mx-auto max-w-lg">
<h2 class="text-4xl mt-14 mb-8">Workout history</h2>
<table class="table table-auto max-w-full">
<thead>
@@ -80,7 +64,7 @@ templ WorkoutItemComp(w Workout, includePlaceholder bool) {
<th>{ w.Reps }</th>
<th>
<div class="tooltip" data-tip="Delete Entry">
<button hx-delete={ "api/workout/" + w.Id } hx-target="closest tr">
<button hx-delete={ "api/workout/" + w.Id } hx-target="closest tr" type="submit">
Delete
</button>
</div>

75
types/auth.go Normal file
View File

@@ -0,0 +1,75 @@
package types
import (
"time"
"github.com/google/uuid"
)
type User struct {
Id uuid.UUID
Email string
EmailVerified bool
EmailVerifiedAt *time.Time
IsAdmin bool
Password []byte
Salt []byte
CreateAt time.Time
}
func NewUser(id uuid.UUID, email string, emailVerified bool, emailVerifiedAt *time.Time, isAdmin bool, password []byte, salt []byte, createAt time.Time) *User {
return &User{
Id: id,
Email: email,
EmailVerified: emailVerified,
EmailVerifiedAt: emailVerifiedAt,
IsAdmin: isAdmin,
Password: password,
Salt: salt,
CreateAt: createAt,
}
}
type Session struct {
Id string
UserId uuid.UUID
CreatedAt time.Time
ExpiresAt time.Time
}
func NewSession(id string, userId uuid.UUID, createdAt time.Time, expiresAt time.Time) *Session {
return &Session{
Id: id,
UserId: userId,
CreatedAt: createdAt,
ExpiresAt: expiresAt,
}
}
type Token struct {
UserId uuid.UUID
SessionId string
Token string
Type TokenType
CreatedAt time.Time
ExpiresAt time.Time
}
type TokenType string
var (
TokenTypeEmailVerify TokenType = "email_verify"
TokenTypePasswordReset TokenType = "password_reset"
TokenTypeCsrf TokenType = "csrf"
)
func NewToken(userId uuid.UUID, sessionId string, token string, tokenType TokenType, createdAt time.Time, expiresAt time.Time) *Token {
return &Token{
UserId: userId,
SessionId: sessionId,
Token: token,
Type: tokenType,
CreatedAt: createdAt,
ExpiresAt: expiresAt,
}
}

84
types/settings.go Normal file
View File

@@ -0,0 +1,84 @@
package types
import (
"web-app-template/log"
)
type Settings struct {
Port string
PrometheusEnabled bool
BaseUrl string
Environment string
Smtp *SmtpSettings
}
type SmtpSettings struct {
Host string
Port string
User string
Pass string
FromMail string
FromName string
}
func NewSettingsFromEnv(env func(string) string) *Settings {
var smtp *SmtpSettings
if env("SMTP_ENABLED") == "true" {
smtp = &SmtpSettings{
Host: env("SMTP_HOST"),
Port: env("SMTP_PORT"),
User: env("SMTP_USER"),
Pass: env("SMTP_PASS"),
FromMail: env("SMTP_FROM_MAIL"),
FromName: env("SMTP_FROM_NAME"),
}
if smtp.Host == "" {
log.Fatal("SMTP_HOST must be set")
}
if smtp.Port == "" {
log.Fatal("SMTP_PORT must be set")
}
if smtp.User == "" {
log.Fatal("SMTP_USER must be set")
}
if smtp.Pass == "" {
log.Fatal("SMTP_PASS must be set")
}
if smtp.FromMail == "" {
log.Fatal("SMTP_FROM_MAIL must be set")
}
if smtp.FromName == "" {
log.Fatal("SMTP_FROM_NAME must be set")
}
}
settings := &Settings{
Port: env("PORT"),
PrometheusEnabled: env("PROMETHEUS_ENABLED") == "true",
BaseUrl: env("BASE_URL"),
Environment: env("ENVIRONMENT"),
Smtp: smtp,
}
if settings.BaseUrl == "" {
log.Fatal("BASE_URL must be set")
}
if settings.Port == "" {
log.Fatal("PORT must be set")
}
if settings.Environment == "" {
log.Fatal("ENVIRONMENT must be set")
}
if settings.Environment == "prod" && (settings.Smtp == nil || !settings.PrometheusEnabled) {
log.Fatal("SMTP and Prometheus must be enabled in production")
}
log.Info("BASE_URL is %q", settings.BaseUrl)
log.Info("ENVIRONMENT is %q", settings.Environment)
return settings
}

View File

@@ -2,17 +2,8 @@ package types
import (
"errors"
"github.com/google/uuid"
)
var (
ErrInternal = errors.New("Internal server error")
ErrInternal = errors.New("internal server error")
)
type User struct {
Id uuid.UUID
Email string
SessionId string
EmailVerified bool
}

View File

@@ -1,16 +0,0 @@
package utils
import (
"crypto/rand"
"encoding/base64"
)
func RandomToken() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(b), nil
}

View File

@@ -1,32 +0,0 @@
package utils
import (
"database/sql"
"log"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func MustRunMigrations(db *sql.DB, pathPrefix string) {
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
if err != nil {
log.Fatal(err)
}
m, err := migrate.NewWithDatabaseInstance(
"file://"+pathPrefix+"migration/",
"",
driver)
if err != nil {
log.Fatal("Could not create migrations instance: ", err)
}
err = m.Up()
if err != nil {
if err.Error() != "no change" {
log.Fatal("Could not run migrations: ", err)
}
}
}

View File

@@ -1,57 +0,0 @@
package utils
import (
"log"
"log/slog"
"os"
)
var (
SmtpHost string
SmtpPort string
SmtpUser string
SmtpPass string
SmtpFromMail string
SmtpFromName string
BaseUrl string
Environment string
)
func MustInitEnv() {
SmtpHost = os.Getenv("SMTP_HOST")
SmtpPort = os.Getenv("SMTP_PORT")
SmtpUser = os.Getenv("SMTP_USER")
SmtpPass = os.Getenv("SMTP_PASS")
SmtpFromMail = os.Getenv("SMTP_FROM_MAIL")
SmtpFromName = os.Getenv("SMTP_FROM_NAME")
BaseUrl = os.Getenv("BASE_URL")
Environment = os.Getenv("ENVIRONMENT")
if SmtpHost == "" {
log.Fatal("SMTP_HOST must be set")
}
if SmtpPort == "" {
log.Fatal("SMTP_PORT must be set")
}
if SmtpUser == "" {
log.Fatal("SMTP_USER must be set")
}
if SmtpPass == "" {
log.Fatal("SMTP_PASS must be set")
}
if SmtpFromMail == "" {
log.Fatal("SMTP_FROM_MAIL must be set")
}
if SmtpFromName == "" {
log.Fatal("SMTP_FROM_NAME must be set")
}
if BaseUrl == "" {
log.Fatal("BASE_URL must be set")
}
if Environment == "" {
log.Fatal("ENVIRONMENT must be set")
}
slog.Info("BASE_URL is " + BaseUrl)
slog.Info("ENVIRONMENT is " + Environment)
}

View File

@@ -1,46 +1,19 @@
package utils
import (
"database/sql"
"fmt"
"log/slog"
"me-fit/types"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"web-app-template/log"
)
type ContextKey string
const (
ContextKeyUser ContextKey = "user_id"
)
var (
errorMetric = promauto.NewCounter(
prometheus.CounterOpts{
Name: "mefit_error_total",
Help: "The total number of errors during processing",
},
)
)
func LogError(message string, err error) {
slog.Error(message + ": " + err.Error())
errorMetric.Inc()
}
func LogErrorMsg(message string) {
slog.Error(message)
errorMetric.Inc()
}
func TriggerToast(w http.ResponseWriter, r *http.Request, class string, message string) {
func TriggerToast(w http.ResponseWriter, r *http.Request, class string, message string, statusCode int) {
if isHtmx(r) {
w.Header().Set("HX-Trigger", fmt.Sprintf(`{"toast": "%v|%v"}`, class, message))
w.WriteHeader(statusCode)
} else {
LogErrorMsg("Trying to trigger toast in non-HTMX request")
log.Error("Trying to trigger toast in non-HTMX request")
}
}
@@ -52,44 +25,6 @@ func DoRedirect(w http.ResponseWriter, r *http.Request, url string) {
}
}
func GetUser(r *http.Request) *types.User {
user := r.Context().Value(ContextKeyUser)
if user != nil {
return user.(*types.User)
} else {
return nil
}
}
func GetUserFromSession(db *sql.DB, r *http.Request) *types.User {
sessionId := getSessionID(r)
if sessionId == "" {
return nil
}
var user types.User
var createdAt time.Time
user.SessionId = sessionId
err := db.QueryRow(`
SELECT u.user_uuid, u.email, u.email_verified, s.created_at
FROM session s
INNER JOIN user u ON s.user_uuid = u.user_uuid
WHERE session_id = ?`, sessionId).Scan(&user.Id, &user.Email, &user.EmailVerified, &createdAt)
if err != nil {
slog.Warn("Could not verify session: " + err.Error())
return nil
}
if createdAt.Add(time.Duration(8 * time.Hour)).Before(time.Now()) {
return nil
} else {
return &user
}
}
func WaitMinimumTime[T interface{}](waitTime time.Duration, function func() (T, error)) (T, error) {
start := time.Now()
result, err := function()
@@ -97,15 +32,6 @@ func WaitMinimumTime[T interface{}](waitTime time.Duration, function func() (T,
return result, err
}
func getSessionID(r *http.Request) string {
for _, c := range r.Cookies() {
if c.Name == "id" {
return c.Value
}
}
return ""
}
func isHtmx(r *http.Request) bool {
return r.Header.Get("HX-Request") == "true"
}

View File

@@ -1,15 +0,0 @@
package utils
import (
"fmt"
"net/smtp"
)
func SendMail(to string, subject string, message string) error {
auth := smtp.PlainAuth("", SmtpUser, SmtpPass, SmtpHost)
msg := fmt.Sprintf("From: %v <%v>\nTo: %v\nSubject: %v\nMIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n%v", SmtpFromName, SmtpFromMail, to, subject, message)
return smtp.SendMail(SmtpHost+":"+SmtpPort, auth, SmtpFromMail, []string{to}, []byte(msg))
}