Compare commits

...

6 Commits

Author SHA1 Message Date
6ad98aaad0 template gen 2026-02-01 21:05:29 +02:00
1baa030dc0 wip 2026-02-01 21:01:16 +02:00
b59d27d13d updates 2026-01-24 15:59:05 +02:00
cbb3fb3421 dockerfile for web 2026-01-24 15:28:11 +02:00
cb23f37342 temp fix 2026-01-24 14:59:05 +02:00
e537f77bbc deps 2026-01-24 14:53:37 +02:00
31 changed files with 2060 additions and 970 deletions

38
server/.env.bk Normal file
View File

@@ -0,0 +1,38 @@
# Server
SERVER_PORT=8555
SERVER_ENV=development
# Database
DB_HOST=localhost
DB_PORT=8553
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=biostacker
DB_SSL_MODE=disable
# Redis
REDIS_ADDR=localhost:8554
REDIS_PASSWORD=
REDIS_DB=0
# SMTP
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=your-app-specific-password
SMTP_FROM=noreply@yourapp.com
# JWT
JWT_SECRET=LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ2hLSFdmNmFTUjFjeW8KdHVCNzBEV1J6WEx4V2lZaHgrWXB6QzV2dGxDWkFteU1HM3RxTzd3RjlFczdqVWtjY2dJQ0VzamZHZDQrOEVsaAppSU1DNnN2ZmNpcUZ4YldqbmliY0dOdFV0dmpzU3NxdmhHTDZKVGx
yS3k0NlF4anlHUTRZdm40RVFqb0hkbE5rCnRyMXdVWXdFU0VXYUppQ0MwdTd3SWpoWUh5alJaNUVSWExsa29SUnIxNUt4Z2tTZ1kwUjlURS9LaWVraXZIdXgKT092QWh6UWF6KzdsckpIcWZoL1g5OWtLbjBoeTh4dXFJWXoxUVFnMUQ3S0twcXRnMUFRbDBRSDlBL1hQenhaZgoyWXZodkRrNnVmOHh5RUJZUlFPa2xiTGVEdUVZNXRTVTVaOFFvU2pMQmpwdTBMdC9hM
mYxWlBmWW91Y2svcFZiCk9DVGliOWdyQWdNQkFBRUNnZ0VBUjhYd3FPRVNHWjhaOEZQT0kyWkZ6V011RzE1V1dEb2lnQi8rMkdMZWYxNnMKZ0RPbklkZHJ0RTBxQ21Jd212b05lZVhxenkzQ3BNNDRLRGQzZlorYlg2OEZVQ0dPOVVrMHJsWmxyRk4zQmltRApIbXM3OTRNSGtQcWdzbkw2azZ2ajh0STM1bWFtV0hkeXlZcDNVU1FJVTBucXNhM2tVYzltZWMwTHdmZFN
vdnVqClhndkUxRzhPSGN4cEpLNTZTOHM2RklLTHc4aW1rT1ZSZFVLWGoxdFNyd0VKTHU2emR4UVdWMnQvRXZRakkvVVEKNjZ5MHhiTzRGb1ZvekhYd3NrWlNBbjlkVzcxRHVKVjRrQjZuOTcvaUZFZ3p6dVRjVU1UMS8xTndDS0hlQVo5TgpGbUhnZkxIbTRuRGJQUUpEWHJjY3JwNkJ2OVIrakNVOUlRMG5TTndsbVFLQmdRRFY3bjAzbFZNd1hPUjlZUEhHCjVndkU2d
3JWWmVkL2pRMU9IQnVDbzlEc2Z5ZjI2UTNMMlVITm1QNm44ZnJ0UmVQa3U4YkUySzBHMm03SGsvVG4KbnVndXFUUGtocnlVSGJDeUtkUDRreU5EbXl2bWhhOFVaSXJXNW90cEFTbm8vZHF3SlBOQmpZZ2RnQk1zMGhZago1RHJOYm5hSlFVN3ZKNXRhRkZtSlR5QUc2UUtCZ1FEQTJVMC8xNXg5bWtpUnNyYS8rTklwQUN2YkFGM1NOQTFpCkNUa3NDRDhIejJnaDhtdXl
pRG1zVk9Tc0lEUjkxbk1ZRVpHT2FLbEVpbzE4enBRazRDUWh0MUJkNVhoMnUrc2kKRmluOXlDd0VsSjZrVUFRS2NqZnFoMG5lcnAvZ0YxRXVNcHFCRUhFR0t1WUZ3QlJ6YjIrOTN5Yk8xdndpUW9ORwpCd0s5cEROaDh3S0JnUURReVg0cHRqSEhYSkdmRC9OSGRCTCtiNHBXTkt0WG4vamhSNnROdDhWYVdzdE5QYXk2ClMySGVYemdCL3JjdnhPc2l2R1RFanRkbmZkM
XFLS3QzTm01Unc4OGlkS0V0U1VDKzBQWFFmd0dHcExXV3VOZmoKWmpEZWhZaC94YVA2Z1c1aVJOMm9GNUpGZ0U2MmlwOFREbGFaVWZxY0FFSWlSQnhwTUwwbHRqU0NxUUtCZ0FLUQpJTVd6Y09IK2RlNXh5Sm4ralpSNzZ4bExCUFF4T3VoTnBSUGZ2QzYzWS9QbmkrVGdpSnV3dVNWTWZFWWIzb1c0ClhnM2RlRHB2K1BkcXEyOWVCenpuZWNyMXJNY3ZNaTNPeTVvUzJ
mcnBtcjRtVGhkeGN5ckx4NENOSTVUUDJvVloKcU5JRVRPdy9EN1dOMnZlNXlHdG1sdFp5NXdEeGoxc1Q1c1pzY3o1ZkFvR0FVQ1JLbWRBaVlHMU9UUzk2aHRKWgp1aXl5amtaeGN0Z3RHWGNkNVF4Nm1TY3U0ZC83Snl2SFQyU0x6U0RYTjI1ZWpjSDc0S1B2bU5yYmNNdGNzMi9pCmVRM3pyQWR0T3FmTzZ6OWJHQm5Ya2gxZmFPazZjK092SFZVNUVhWjdia3VkUEdFU
3VtRm9iREh6RzUvUmZpVzYKeFFQak1mMGQ1TUdkNWhqaEFDT0tPTHc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
JWT_EXPIRY=480h
JWT_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFvU2gxbitta2tkWE1xTGJnZTlBMQprYzF5OFZvbUljZm1LY3d1YjdaUW1RSnNqQnQ3YWp1OEJmUkxPNDFKSEhJQ0FoTEkzeG5lUHZCSllZaURBdXJMCjMzSXFoY1cxbzU0bTNCamJWTGI0N0VyS3I0UmkraVU1YXlzdU9
rTVk4aGtPR0w1K0JFSTZCM1pUWkxhOWNGR00KQkVoRm1pWWdndEx1OENJNFdCOG8wV2VSRVZ5NVpLRVVhOWVTc1lKRW9HTkVmVXhQeW9ucElyeDdzVGpyd0ljMApHcy91NWF5UjZuNGYxL2ZaQ3A5SWN2TWJxaUdNOVVFSU5RK3lpcWFyWU5RRUpkRUIvUVAxejg4V1g5bUw0Ync1Ck9ybi9NY2hBV0VVRHBKV3kzZzdoR09iVWxPV2ZFS0VveXdZNmJ0QzdmMnRuOVdUM
zJLTG5KUDZWV3pnazRtL1kKS3dJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==

2
server/.gitignore vendored
View File

@@ -26,3 +26,5 @@ vendor/
*.out
tmp/
*_templ.go

View File

@@ -45,7 +45,16 @@ A scalable backend boilerplate built with **Go**, **Gin**, **GORM**, **PostgreSQ
go run scripts/migrate.go
```
4. Start the server:
4. Generate templates
```bash
go run scripts/migrate.go
go get -tool github.com/a-h/templ/cmd/templ@latest
go -tool templ generate
go tool templ generate
```
5. Start the server:
```bash
go run main.go

View File

@@ -4,11 +4,23 @@ go 1.23.0
toolchain go1.24.5
require github.com/spf13/viper v1.20.1
require (
github.com/oklog/ulid/v2 v2.1.1
github.com/spf13/viper v1.20.1
)
require (
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
github.com/a-h/templ v0.3.977 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cli/browser v1.3.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/natefinch/atomic v1.0.1 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/tools v0.35.0 // indirect
)
require (
@@ -67,3 +79,5 @@ require (
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.1
)
tool github.com/a-h/templ/cmd/templ

View File

@@ -1,3 +1,9 @@
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -7,8 +13,12 @@ github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxl
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
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/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
@@ -17,6 +27,8 @@ 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -72,6 +84,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -79,6 +94,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
@@ -130,10 +147,13 @@ golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
@@ -141,6 +161,8 @@ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
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/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=

View File

@@ -0,0 +1,27 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
type LoginHandler struct{}
func NewLoginHandler() *LoginHandler {
return &LoginHandler{}
}
func (lh *LoginHandler) Login(c *gin.Context) {
email := c.PostForm("email")
password := c.PostForm("password")
// Simple check, replace with real auth
if email == "test@example.com" && password == "password" {
c.Header("Content-Type", "text/html")
c.String(http.StatusOK, "<p>Login successful! <a href='/dashboard'>Go to Dashboard</a></p>")
} else {
c.Header("Content-Type", "text/html")
c.String(http.StatusUnauthorized, "<p>Invalid credentials. Try again.</p>")
}
}

View File

@@ -3,12 +3,15 @@ package routes
import (
"fmt"
"net/http"
"strings"
handlers "go-server/internal/handlers"
"go-server/internal/middleware"
"go-server/internal/repository"
"go-server/internal/service"
"go-server/internal/templates"
"github.com/a-h/templ"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -36,9 +39,28 @@ func SetupRouter(db *gorm.DB) *gin.Engine {
r.Use(middleware.Logger())
r.Use(gin.Recovery())
// Serve simple text at "/"
// Cache headers for static files
r.Use(func(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, "/static/") || c.Request.URL.Path == "/favicon.ico" {
c.Header("Cache-Control", "public, max-age=31536000")
}
c.Next()
})
// Serve static files
r.Static("/static", "./static")
r.StaticFile("/favicon.ico", "./static/favicon.ico")
// Serve home page
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello")
ctx := templ.WithChildren(c.Request.Context(), templates.Home())
templates.Layout("Home").Render(ctx, c.Writer)
})
// Serve login page
r.GET("/login", func(c *gin.Context) {
ctx := templ.WithChildren(c.Request.Context(), templates.Login())
templates.Layout("Login").Render(ctx, c.Writer)
})
// Custom 404 handler
@@ -69,6 +91,7 @@ func SetupRouter(db *gorm.DB) *gin.Engine {
dailyOverviewController := handlers.NewDailyOverviewController(dailyOverviewService)
categoryController := handlers.NewCategoryController(categoryService)
todoController := handlers.NewTodoController(todoService)
loginHandler := handlers.NewLoginHandler()
skipAuth := true
@@ -119,6 +142,11 @@ func SetupRouter(db *gorm.DB) *gin.Engine {
{"GET", "/weekly", todoController.GetWeeklySummary, !skipAuth}, // Get weekly summary
},
},
"user": {
Routes: []Route{
{"POST", "/login", loginHandler.Login, false},
},
},
}
// Register all routes dynamically

View File

@@ -0,0 +1,10 @@
package templates
templ Home() {
<div>
<h1>Welcome to Futur Web App</h1>
<p>This is server-rendered with Templ and HTMX.</p>
<a href="/login">Login</a>
<a href="/dashboard">Dashboard</a>
</div>
}

View File

@@ -0,0 +1,19 @@
package templates
templ Layout(title string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<script src="/static/htmx.2.0.8.min.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
</style>
</head>
<body>
{ children... }
</body>
</html>
}

View File

@@ -0,0 +1,20 @@
package templates
templ Login() {
<div>
<h2>Login</h2>
<form hx-post="/api/user/login" hx-target="#result" hx-swap="innerHTML">
<div>
<label>Email:</label>
<input type="email" name="email" placeholder="Email" required>
</div>
<div>
<label>Password:</label>
<input type="password" name="password" placeholder="Password" required>
</div>
<button type="submit">Login</button>
</form>
<div id="result"></div>
<a href="/">Home</a>
</div>
}

BIN
server/server Executable file

Binary file not shown.

BIN
server/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
server/static/htmx.2.0.8.min.js vendored Normal file

File diff suppressed because one or more lines are too long

48
web-example.Dockerfile Normal file
View File

@@ -0,0 +1,48 @@
ARG NODE_VERSION=24.11.1
FROM node:${NODE_VERSION}-slim AS base
# This Dockerfile is copy-pasted into our main docs at /docs/handbook/deploying-with-docker.
# Make sure you update both files!
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
# Set working directory
WORKDIR /app
# ---
FROM base AS prepare
RUN yarn global add turbo
COPY . .
# Add lockfile and package.json's of isolated subworkspace
RUN turbo prune web --docker
# ---
FROM base AS builder
# First install the dependencies (as they change less often)
COPY --from=prepare /app/out/json/ .
RUN yarn install
# Build the project
COPY --from=prepare /app/out/full/ .
# Uncomment and use build args to enable remote caching
# ARG TURBO_TEAM
# ENV TURBO_TEAM=$TURBO_TEAM
# ARG TURBO_TOKEN
# ENV TURBO_TOKEN=$TURBO_TOKEN
RUN yarn turbo build
# ---
FROM node:${NODE_VERSION}-alpine AS runner
# Don't run production as root for security reasons
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
CMD node apps/web/server.js

15
web/.dockerignore Normal file
View File

@@ -0,0 +1,15 @@
node_modules
npm-debug.log
pnpm-debug.log
.DS_Store
# Turbo cache
.turbo
# IDE specific files
biome.json
# Build output
dist
build
out

51
web/Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
# Use a multi-stage build to optimize the final image size
# Stage 1: Prepare the pruned workspace
FROM node:24-alpine AS prepare
WORKDIR /app
RUN npm install -g pnpm@10.28.1
RUN npm install -g turbo@2.7.5
COPY . .
# Prune the workspace to only include what's needed for the web service
RUN pnpm turbo prune biostacker-frontend --docker
# Stage 2: Build the application
FROM node:24-alpine AS builder
WORKDIR /app
RUN npm install -g pnpm@10.28.1
RUN npm install -g turbo@2.7.5
# First install the dependencies (as they change less often)
COPY --from=prepare /app/out/json/ .
RUN pnpm install --frozen-lockfile
# Build the project
COPY --from=prepare /app/out/full/ .
RUN pwd
RUN ls -la /app
COPY --from=prepare /app/tsconfig.base.json .
RUN pnpm build
RUN ls -la /app
# Stage 3: Create the production image with Caddy as reverse proxy
FROM caddy:2-alpine AS runner
WORKDIR /usr/share/caddy
# Copy the built assets from the builder stage
COPY --from=builder /app/frontend/dist ./dist
# Copy Caddyfile for configuration
COPY --from=builder /app/frontend/Caddyfile ./Caddyfile
EXPOSE 8667
CMD ["caddy", "run", "--config", "/usr/share/caddy/Caddyfile", "--adapter", "caddyfile"]

View File

@@ -8,23 +8,23 @@
},
"dependencies": {
"@greatness/util": "workspace:*",
"@types/node": "24.1.0",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@carbon/icons-react": "11.63.0",
"@types/node": "25.0.10",
"@types/react": "19.2.9",
"@types/react-dom": "19.2.3",
"@carbon/icons-react": "11.73.0",
"@radix-ui/react-select": "2.2.5",
"lucide-react": "0.525.0",
"@hookform/resolvers": "5.2.0",
"@floating-ui/react": "0.27.13",
"lucide-react": "0.563.0",
"@hookform/resolvers": "5.2.2",
"@floating-ui/react": "0.27.16",
"react-jss": "10.10.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-error-boundary": "6.0.0",
"react-router-dom": "7.7.1",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-error-boundary": "6.1.0",
"react-router-dom": "7.13.0",
"clsx": "2.1.1",
"tailwind-merge": "3.3.1",
"react-loading-skeleton": "3.5.0",
"react-hook-form": "7.61.1"
"react-hook-form": "7.71.1"
},
"devDependencies": {
"jest": "30.0.5"

View File

@@ -10,7 +10,7 @@ function ErrorFallback({ error, resetErrorBoundary, onError }: FallbackProps & {
useEffect(() => {
if (onError) {
onError(error);
onError(error as Error);
resetErrorBoundary();
return;
}

10
web/compose.yaml Normal file
View File

@@ -0,0 +1,10 @@
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "8667:8667"
environment:
- NODE_ENV=production
restart: unless-stopped

11
web/frontend/Caddyfile Normal file
View File

@@ -0,0 +1,11 @@
:8667 {
root * /usr/share/caddy/dist
file_server
try_files {path} /index.html
# Enable gzip compression
encode gzip
# Enable brotli compression
encode zstd
}

View File

@@ -22,54 +22,54 @@
"find-deadcode": "ts-prune"
},
"dependencies": {
"@carbon/icons-react": "11.63.0",
"@carbon/icons-react": "11.73.0",
"@greatness/components": "workspace:*",
"@greatness/util": "workspace:*",
"@floating-ui/react": "0.27.13",
"@hookform/resolvers": "5.2.0",
"@radix-ui/react-select": "2.2.5",
"@tailwindcss/vite": "4.1.11",
"@tanstack/react-query": "5.83.0",
"@tanstack/react-query-devtools": "5.83.0",
"@floating-ui/react": "0.27.16",
"@hookform/resolvers": "5.2.2",
"@radix-ui/react-select": "2.2.6",
"@tailwindcss/vite": "4.1.18",
"@tanstack/react-query": "5.90.20",
"@tanstack/react-query-devtools": "5.91.2",
"@types/i18n-js": "3.8.9",
"@types/js-cookie": "3.0.6",
"@types/node": "24.1.0",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@types/node": "25.0.10",
"@types/react": "19.2.9",
"@types/react-dom": "19.2.3",
"awesome-debounce-promise": "2.1.0",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"i18n-js": "3.8.0",
"js-cookie": "3.0.5",
"lucide-react": "0.525.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "7.61.1",
"lucide-react": "0.563.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-error-boundary": "6.1.0",
"react-hook-form": "7.71.1",
"react-jss": "10.10.0",
"react-router-dom": "7.7.1",
"react-router-dom": "7.13.0",
"react-select": "5.10.2",
"recharts": "3.1.0",
"tailwind-merge": "3.3.1",
"tailwindcss": "4.1.11",
"vis-timeline": "8.1.2",
"yup": "1.6.1",
"zod": "3.25.74"
"recharts": "3.7.0",
"tailwind-merge": "3.4.0",
"tailwindcss": "4.1.18",
"vis-timeline": "8.5.0",
"yup": "1.7.1",
"zod": "4.3.6"
},
"devDependencies": {
"@js-temporal/polyfill": "0.5.1",
"@types/jest": "30.0.0",
"@vitejs/plugin-react-swc": "3.11.0",
"@vitejs/plugin-react-swc": "4.2.2",
"depcheck": "1.4.7",
"react-loading-skeleton": "3.5.0",
"rimraf": "6.0.1",
"rollup-plugin-visualizer": "6.0.3",
"sass": "1.89.2",
"rimraf": "6.1.2",
"rollup-plugin-visualizer": "6.0.5",
"sass": "1.97.3",
"ts-prune": "0.10.3",
"tw-animate-css": "1.3.6",
"tw-animate-css": "1.4.0",
"typescript": "5.8.3",
"vite": "7.0.6",
"vite-tsconfig-paths": "5.1.4"
"vite": "7.3.1",
"vite-tsconfig-paths": "6.0.5"
},
"browserslist": [
">1%",

View File

@@ -85,6 +85,7 @@ export default function SessionContextProvider({
logout();
return;
}
// @ts-expect-error ok great
setUser(response.user);
if (location.pathname.endsWith(NavigationPath.Login)) {
@@ -103,6 +104,7 @@ export default function SessionContextProvider({
if (isResponseOk) {
setIsAuthenticated(true);
setToken({
// @ts-expect-error ok great
accessToken: response.accessToken,
});
await checkSession();

View File

@@ -5,7 +5,7 @@ import { useCallback } from "react";
import { useSessionContext } from "@/components/SessionContext";
import createUseStyles from "@/theme/createUseStyles";
import { cn } from "@/util/cn";
import { useDarkMode } from "@/util/useDarkMode";
//import { useDarkMode } from "@/util/useDarkMode";
const useStyles = createUseStyles(({ sizes, media }) => ({
closeButton: {
@@ -44,7 +44,7 @@ export default function PageHeader({
}) {
const classes = useStyles();
const { isAuthenticated, logout } = useSessionContext();
const { isDarkMode, toggleDarkMode } = useDarkMode();
const { isDarkMode, toggleDarkMode } = { isDarkMode: true, toggleDarkMode: () => {}}; //useDarkMode();
const handleClose = useCallback(
() => (isAuthenticated ? logout() : window.close()),

View File

@@ -1,6 +1,6 @@
import DailyNutrientsOverview from "./components/DailyNutrientsOverview";
import NutrientsTable from "./components/NutrientsTable";
import SupplementsTable from "./components/SupplementsTable";
// import NutrientsTable from "./components/NutrientsTable";
// import SupplementsTable from "./components/SupplementsTable";
export default function SupplementsPage() {
return (

View File

@@ -1,7 +1,7 @@
import { useQuery } from "@/api/useQuery";
export default function NutrientsTable() {
const { data, isLoading, error } = useQuery<{ id: string; name: string; description: string }[]>({
const { data } = useQuery<{ id: string; name: string; description: string }[]>({
queryKey: "/nutrient/get-all",
});
return (

View File

@@ -5,7 +5,7 @@ import TableRow from "@greatness/components/src/table/TableRow";
import TableWrapper from "@greatness/components/src/table/TableWrapper";
export default function SupplementsTable() {
const { data, isLoading, error } = useQuery<{ id: string; name: string; description: string }[]>({
const { data } = useQuery<{ id: string; name: string; description: string }[]>({
queryKey: "/supplement/get-all",
});
return (

View File

@@ -1,5 +1,5 @@
import { object, string, Schema } from 'yup';
import { useFormContext, Controller } from 'react-hook-form';
import { useFormContext } from 'react-hook-form';
import { TodoWithStats, CreateTodoRequest, UpdateTodoRequest } from '../../../api/controller/todo';
import Button from '@greatness/components/src/Button';
import Modal from '@greatness/components/src/Modal';

View File

@@ -30,7 +30,7 @@ export default defineConfig(({ command }) => {
},
build: {
outDir: 'dist',
sourcemap: command === 'serve',
sourcemap: false,
commonjsOptions: {
include: [/node_modules/]
},
@@ -40,6 +40,18 @@ export default defineConfig(({ command }) => {
'vendor': [
'react',
'react-dom',
'react-router-dom',
'@tanstack/react-query',
],
'ui': [
'@carbon/icons-react',
'lucide-react',
'react-select',
],
'utils': [
'clsx',
'tailwind-merge',
'class-variance-authority',
],
},
},

View File

@@ -21,10 +21,10 @@
"biome": "biome"
},
"devDependencies": {
"turbo": "2.5.4",
"@biomejs/biome": "2.0.6",
"@types/node": "24.0.10",
"typescript": "5.8.3"
"turbo": "2.7.5",
"@biomejs/biome": "2.3.12",
"@types/node": "25.0.10",
"typescript": "5.9.3"
},
"packageManager": "pnpm@10.12.4"
"packageManager": "pnpm@10.28.1"
}

2567
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,9 @@
"test": "jest"
},
"dependencies": {
"dayjs": "1.11.13"
"dayjs": "1.11.19"
},
"devDependencies": {
"jest": "30.0.5"
"jest": "30.2.0"
}
}