47 Commits

Author SHA1 Message Date
1c091dc924 wip
Some checks failed
Build Docker Image / Build-Docker-Image (push) Failing after 42s
2025-12-24 08:13:05 +01:00
1db5c48553 wip
Some checks failed
Build Docker Image / Build-Docker-Image (push) Has been cancelled
2025-12-24 08:12:26 +01:00
f9a5a9e5f9 feat: extract account to domain package
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m17s
2025-12-24 07:45:44 +01:00
1e61b765ae fix: resolve hints
All checks were successful
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 3m35s
2025-12-23 20:17:47 +01:00
677c6b795e chore(deps): update golang:1.25.5 docker digest to 36b4f45
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m13s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m14s
2025-12-15 10:06:47 +00:00
3b0ba91b73 chore(deps): update tailwindcss monorepo to v4.1.18
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
Build Docker Image / Build-Docker-Image (push) Successful in 1m12s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m16s
2025-12-14 17:04:15 +00:00
74b63bc494 chore(deps): update node.js to v24.12.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m13s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m21s
2025-12-11 23:04:19 +00:00
480f311856 chore(deps): update node.js to v24.12.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m11s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m15s
2025-12-10 17:03:18 +00:00
1a0524a24b chore(deps): update golang:1.25.5 docker digest to a22b2e6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m12s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m18s
2025-12-09 22:03:27 +00:00
2e641a1db5 chore(deps): update node.js to 9a2ed90
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m11s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m18s
2025-12-09 11:07:18 +00:00
d952956a8d chore(deps): update golang:1.25.5 docker digest to 0ece421
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m12s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m17s
2025-12-09 10:03:47 +00:00
b28b41aff4 chore(deps): update node.js to 822e393
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m12s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m15s
2025-12-09 08:04:14 +00:00
18e651babf chore(deps): update node.js to ef9b3fb
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m10s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m19s
2025-12-09 06:03:56 +00:00
5e992873cc chore(deps): update debian:13.2 docker digest to 0d01188
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m6s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m16s
2025-12-09 05:04:48 +00:00
26b75d3db9 chore(deps): update golang:1.25.5 docker digest to 68ee6df
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m9s
2025-12-09 04:06:32 +00:00
772e3e5c2e chore(deps): update debian:13.2 docker digest to f54909a
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m12s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m25s
2025-12-09 02:11:27 +00:00
fcba476a88 fix(deps): update module golang.org/x/net to v0.48.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m17s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m15s
2025-12-08 22:06:49 +00:00
d1bdf38227 fix(deps): update opentelemetry-go-contrib monorepo
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m15s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m14s
2025-12-08 21:03:53 +00:00
dea1b9027b fix(deps): update module golang.org/x/crypto to v0.46.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m16s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m13s
2025-12-08 20:13:25 +00:00
ee9ef98fa3 fix(deps): update opentelemetry-go monorepo
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 1m14s
2025-12-08 18:03:46 +00:00
9a48f23a2c fix(deps): update opentelemetry-go monorepo
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m9s
2025-12-08 17:03:42 +00:00
99d52aa505 chore(deps): update golang docker tag to v1.25.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 2m47s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m7s
2025-12-02 19:03:45 +00:00
8833147278 chore(deps): update dependency go to v1.25.5
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m16s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m17s
2025-12-02 17:13:28 +00:00
69727339aa chore(deps): update actions/checkout digest to 8e8c483
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m21s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m24s
2025-12-02 17:09:18 +00:00
e97b7c3069 fix(deps): update module github.com/golang-migrate/migrate/v4 to v4.19.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m50s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m13s
2025-11-29 22:03:46 +00:00
492baab18b Merge branch 'prod' into renovate/actions-checkout-6.x
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m52s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m48s
2025-11-23 20:14:09 +00:00
263ea213cd chore(deps): update golang:1.25.4 docker digest to 6981837
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m32s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m36s
2025-11-23 01:03:54 +00:00
6881a64691 chore(deps): update actions/checkout action to v6
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m26s
2025-11-20 17:49:21 +00:00
5efd5f9bbc fix(deps): update module golang.org/x/crypto to v0.45.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m39s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m35s
2025-11-19 21:03:39 +00:00
396c97516d chore(deps): update node.js to aa648b3
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m46s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m30s
2025-11-18 17:06:35 +00:00
88caf44fc5 chore(deps): update node.js to d5a23e0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m28s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m32s
2025-11-18 15:05:03 +00:00
c8dce6f33a chore(deps): update golang:1.25.4 docker digest to f60eaa8
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 4m14s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m29s
2025-11-18 13:06:57 +00:00
12affdff43 chore(deps): update node.js to c5453ea
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m34s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m42s
2025-11-18 11:11:41 +00:00
9f3fcc0171 chore(deps): update golang:1.25.4 docker digest to 2948461
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m35s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m33s
2025-11-18 10:12:40 +00:00
f5dd96cf9f chore(deps): update golang:1.25.4 docker digest to 3976069
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m27s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m31s
2025-11-18 07:03:51 +00:00
d3900957c9 chore(deps): update debian docker tag to v13.2
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m35s
2025-11-18 05:04:12 +00:00
f90c5f83e1 chore(deps): update actions/checkout digest to 93cb6ef
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m24s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m31s
2025-11-17 18:12:21 +00:00
a5246e523c chore(deps): update node.js to v24.11.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m27s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m32s
2025-11-13 05:02:44 +00:00
11a620b73a chore(deps): update node.js to v24.11.1
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m19s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m29s
2025-11-11 23:03:17 +00:00
5db923d438 chore(deps): update golang:1.25.4 docker digest to e68f6a0
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 1m22s
2025-11-11 22:04:13 +00:00
e1c4eeb51d fix(deps): update module golang.org/x/net to v0.47.0
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m35s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m32s
2025-11-11 20:03:38 +00:00
cf728abe11 fix(deps): update module golang.org/x/crypto to v0.44.0
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 1m33s
2025-11-11 19:06:35 +00:00
84f72a1e25 chore(deps): update golang:1.25.4 docker digest to 6ca9eb0
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 1m21s
2025-11-08 10:03:26 +00:00
3d2dbaebc7 chore(deps): update tailwindcss monorepo to v4.1.17
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m30s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m33s
2025-11-06 16:06:38 +00:00
0b9d1d31e4 chore(deps): update golang docker tag to v1.25.4
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 3m25s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m19s
2025-11-05 22:06:01 +00:00
d3daa4e5ba chore(deps): update dependency go to v1.25.4
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m31s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m31s
2025-11-05 20:06:26 +00:00
2839a2c4c3 chore(deps): update golang:1.25.3 docker digest to 6d4e5e7
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 1m25s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 1m32s
2025-11-05 06:05:58 +00:00
50 changed files with 656 additions and 675 deletions

View File

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

View File

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

2
.nvmrc
View File

@@ -1 +1 @@
24.11.0
24.12.0

View File

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

59
go.mod
View File

@@ -2,11 +2,11 @@ module spend-sparrow
go 1.24.0
toolchain go1.25.3
toolchain go1.25.5
require (
github.com/a-h/templ v0.3.960
github.com/golang-migrate/migrate/v4 v4.19.0
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1
@@ -14,41 +14,40 @@ require (
github.com/stretchr/testify v1.11.1
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2
github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
go.opentelemetry.io/otel/log v0.14.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/sdk/log v0.14.0
go.opentelemetry.io/otel/sdk/metric v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
golang.org/x/crypto v0.43.0
golang.org/x/net v0.46.0
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0
go.opentelemetry.io/otel/log v0.15.0
go.opentelemetry.io/otel/sdk v1.39.0
go.opentelemetry.io/otel/sdk/log v0.15.0
go.opentelemetry.io/otel/sdk/metric v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
golang.org/x/crypto v0.46.0
golang.org/x/net v0.48.0
)
require (
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/objx v0.5.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

119
go.sum
View File

@@ -4,8 +4,10 @@ github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -15,21 +17,16 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@@ -43,10 +40,10 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
@@ -55,58 +52,58 @@ github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/Oa
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ=
github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2 h1:zA9ZXfdtowo0EKt+t7uqXNlHxPeygrxuFSIroiBVgPU=
github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2/go.mod h1:ySXmuW9JLCm/TjsQksuMY/7MNiWqfHnhH2xeT34uOLU=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM=
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY=
go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE=
go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=

View File

@@ -1,44 +1,36 @@
package handler
package account
import (
"net/http"
"spend-sparrow/internal/handler/middleware"
"spend-sparrow/internal/service"
t "spend-sparrow/internal/template/account"
"spend-sparrow/internal/types"
"spend-sparrow/internal/utils"
"github.com/a-h/templ"
"net/http"
"spend-sparrow/internal/core"
"spend-sparrow/internal/utils"
)
type Account interface {
Handle(router *http.ServeMux)
type Handler struct {
s Service
r *core.Render
}
type AccountImpl struct {
s service.Account
r *Render
}
func NewAccount(s service.Account, r *Render) Account {
return AccountImpl{
func NewHandler(s Service, r *core.Render) Handler {
return Handler{
s: s,
r: r,
}
}
func (h AccountImpl) Handle(r *http.ServeMux) {
func (h Handler) Handle(r *http.ServeMux) {
r.Handle("GET /account", h.handleAccountPage())
r.Handle("GET /account/{id}", h.handleAccountItemComp())
r.Handle("POST /account/{id}", h.handleUpdateAccount())
r.Handle("DELETE /account/{id}", h.handleDeleteAccount())
}
func (h AccountImpl) handleAccountPage() http.HandlerFunc {
func (h Handler) handleAccountPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -46,20 +38,20 @@ func (h AccountImpl) handleAccountPage() http.HandlerFunc {
accounts, err := h.s.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
comp := t.Account(accounts)
comp := template(accounts)
h.r.RenderLayout(r, w, comp, user)
}
}
func (h AccountImpl) handleAccountItemComp() http.HandlerFunc {
func (h Handler) handleAccountItemComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -67,39 +59,39 @@ func (h AccountImpl) handleAccountItemComp() http.HandlerFunc {
id := r.PathValue("id")
if id == "new" {
comp := t.EditAccount(nil)
comp := editAccount(nil)
h.r.Render(r, w, comp)
return
}
account, err := h.s.Get(r.Context(), user, id)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
var comp templ.Component
if r.URL.Query().Get("edit") == "true" {
comp = t.EditAccount(account)
comp = editAccount(account)
} else {
comp = t.AccountItem(account)
comp = accountItem(account)
}
h.r.Render(r, w, comp)
}
}
func (h AccountImpl) handleUpdateAccount() http.HandlerFunc {
func (h Handler) handleUpdateAccount() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
var (
account *types.Account
account *Account
err error
)
id := r.PathValue("id")
@@ -107,27 +99,27 @@ func (h AccountImpl) handleUpdateAccount() http.HandlerFunc {
if id == "new" {
account, err = h.s.Add(r.Context(), user, name)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
} else {
account, err = h.s.UpdateName(r.Context(), user, id, name)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
}
comp := t.AccountItem(account)
comp := accountItem(account)
h.r.Render(r, w, comp)
}
}
func (h AccountImpl) handleDeleteAccount() http.HandlerFunc {
func (h Handler) handleDeleteAccount() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -137,7 +129,7 @@ func (h AccountImpl) handleDeleteAccount() http.HandlerFunc {
err := h.s.Delete(r.Context(), user, id)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
}

View File

@@ -1,42 +1,42 @@
package service
package account
import (
"context"
"errors"
"fmt"
"log/slog"
"spend-sparrow/internal/db"
"spend-sparrow/internal/types"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"log/slog"
"spend-sparrow/internal/db"
"spend-sparrow/internal/service"
"spend-sparrow/internal/types"
)
type Account interface {
Add(ctx context.Context, user *types.User, name string) (*types.Account, error)
UpdateName(ctx context.Context, user *types.User, id string, name string) (*types.Account, error)
Get(ctx context.Context, user *types.User, id string) (*types.Account, error)
GetAll(ctx context.Context, user *types.User) ([]*types.Account, error)
type Service interface {
Add(ctx context.Context, user *types.User, name string) (*Account, error)
UpdateName(ctx context.Context, user *types.User, id string, name string) (*Account, error)
Get(ctx context.Context, user *types.User, id string) (*Account, error)
GetAll(ctx context.Context, user *types.User) ([]*Account, error)
Delete(ctx context.Context, user *types.User, id string) error
}
type AccountImpl struct {
type ServiceImpl struct {
db *sqlx.DB
clock Clock
random Random
clock service.Clock
random service.Random
}
func NewAccount(db *sqlx.DB, random Random, clock Clock) Account {
return AccountImpl{
func NewServiceImpl(db *sqlx.DB, random service.Random, clock service.Clock) Service {
return ServiceImpl{
db: db,
clock: clock,
random: random,
}
}
func (s AccountImpl) Add(ctx context.Context, user *types.User, name string) (*types.Account, error) {
func (s ServiceImpl) Add(ctx context.Context, user *types.User, name string) (*Account, error) {
if user == nil {
return nil, ErrUnauthorized
return nil, types.ErrUnauthorized
}
newId, err := s.random.UUID(ctx)
@@ -44,12 +44,12 @@ func (s AccountImpl) Add(ctx context.Context, user *types.User, name string) (*t
return nil, types.ErrInternal
}
err = validateString(name, "name")
err = service.ValidateString(name, "name")
if err != nil {
return nil, err
}
account := &types.Account{
account := &Account{
Id: newId,
UserId: user.Id,
@@ -76,18 +76,18 @@ func (s AccountImpl) Add(ctx context.Context, user *types.User, name string) (*t
return account, nil
}
func (s AccountImpl) UpdateName(ctx context.Context, user *types.User, id string, name string) (*types.Account, error) {
func (s ServiceImpl) UpdateName(ctx context.Context, user *types.User, id string, name string) (*Account, error) {
if user == nil {
return nil, ErrUnauthorized
return nil, types.ErrUnauthorized
}
err := validateString(name, "name")
err := service.ValidateString(name, "name")
if err != nil {
return nil, err
}
uuid, err := uuid.Parse(id)
if err != nil {
slog.ErrorContext(ctx, "account update", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
return nil, fmt.Errorf("could not parse Id: %w", service.ErrBadRequest)
}
tx, err := s.db.BeginTxx(ctx, nil)
@@ -99,12 +99,12 @@ func (s AccountImpl) UpdateName(ctx context.Context, user *types.User, id string
_ = tx.Rollback()
}()
var account types.Account
var account Account
err = tx.GetContext(ctx, &account, `SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError(ctx, "account Update", nil, err)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
return nil, fmt.Errorf("account %v not found: %w", id, ErrBadRequest)
return nil, fmt.Errorf("account %v not found: %w", id, service.ErrBadRequest)
}
return nil, types.ErrInternal
}
@@ -136,17 +136,17 @@ func (s AccountImpl) UpdateName(ctx context.Context, user *types.User, id string
return &account, nil
}
func (s AccountImpl) Get(ctx context.Context, user *types.User, id string) (*types.Account, error) {
func (s ServiceImpl) Get(ctx context.Context, user *types.User, id string) (*Account, error) {
if user == nil {
return nil, ErrUnauthorized
return nil, service.ErrUnauthorized
}
uuid, err := uuid.Parse(id)
if err != nil {
slog.ErrorContext(ctx, "account get", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
return nil, fmt.Errorf("could not parse Id: %w", service.ErrBadRequest)
}
var account types.Account
var account Account
err = s.db.GetContext(ctx, &account, `
SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError(ctx, "account Get", nil, err)
@@ -158,12 +158,12 @@ func (s AccountImpl) Get(ctx context.Context, user *types.User, id string) (*typ
return &account, nil
}
func (s AccountImpl) GetAll(ctx context.Context, user *types.User) ([]*types.Account, error) {
func (s ServiceImpl) GetAll(ctx context.Context, user *types.User) ([]*Account, error) {
if user == nil {
return nil, ErrUnauthorized
return nil, service.ErrUnauthorized
}
accounts := make([]*types.Account, 0)
accounts := make([]*Account, 0)
err := s.db.SelectContext(ctx, &accounts, `
SELECT * FROM account WHERE user_id = ? ORDER BY name`, user.Id)
err = db.TransformAndLogDbError(ctx, "account GetAll", nil, err)
@@ -174,14 +174,14 @@ func (s AccountImpl) GetAll(ctx context.Context, user *types.User) ([]*types.Acc
return accounts, nil
}
func (s AccountImpl) Delete(ctx context.Context, user *types.User, id string) error {
func (s ServiceImpl) Delete(ctx context.Context, user *types.User, id string) error {
if user == nil {
return ErrUnauthorized
return service.ErrUnauthorized
}
uuid, err := uuid.Parse(id)
if err != nil {
slog.ErrorContext(ctx, "account delete", "err", err)
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
return fmt.Errorf("could not parse Id: %w", service.ErrBadRequest)
}
tx, err := s.db.BeginTxx(ctx, nil)
@@ -200,7 +200,7 @@ func (s AccountImpl) Delete(ctx context.Context, user *types.User, id string) er
return err
}
if transactionsCount > 0 {
return fmt.Errorf("account has transactions, cannot delete: %w", ErrBadRequest)
return fmt.Errorf("account has transactions, cannot delete: %w", service.ErrBadRequest)
}
res, err := tx.ExecContext(ctx, "DELETE FROM account WHERE id = ? and user_id = ?", uuid, user.Id)

View File

@@ -3,7 +3,7 @@ package account
import "spend-sparrow/internal/template/svg"
import "spend-sparrow/internal/types"
templ Account(accounts []*types.Account) {
templ template(accounts []*Account) {
<div class="max-w-6xl mt-10 mx-auto">
<button
hx-get="/account/new"
@@ -16,28 +16,28 @@ templ Account(accounts []*types.Account) {
</button>
<div id="account-items" class="my-6 flex flex-col items-center">
for _, account := range accounts {
@AccountItem(account)
@accountItem(account)
}
</div>
</div>
}
templ EditAccount(account *types.Account) {
templ editAccount(account *Account) {
{{
var (
name string
id string
cancelUrl string
)
if account == nil {
name = ""
id = "new"
cancelUrl = "/empty"
} else {
name = account.Name
id = account.Id.String()
cancelUrl = "/account/" + id
}
var (
name string
id string
cancelUrl string
)
if account == nil {
name = ""
id = "new"
cancelUrl = "/empty"
} else {
name = account.Name
id = account.Id.String()
cancelUrl = "/account/" + id
}
}}
<div id="account" class="border-1 border-gray-300 w-full my-4 p-4 bg-gray-50 rounded-lg">
<form
@@ -77,7 +77,7 @@ templ EditAccount(account *types.Account) {
</div>
}
templ AccountItem(account *types.Account) {
templ accountItem(account *Account) {
<div id="account" class="border-1 border-gray-300 w-full my-4 p-4 bg-gray-50 rounded-lg">
<div class="text-xl flex justify-end gap-4">
<p class="mr-auto">{ account.Name }</p>

View File

@@ -1,9 +1,8 @@
package types
package account
import (
"time"
"github.com/google/uuid"
"time"
)
// The Account holds money.

View File

@@ -1,4 +1,4 @@
package types
package auth_types
import (
"time"

View File

@@ -1,11 +1,12 @@
package db
package authentication
import (
"context"
"database/sql"
"errors"
"log/slog"
"spend-sparrow/internal/types"
"spend-sparrow/internal/auth_types"
"spend-sparrow/internal/core"
"strings"
"time"
@@ -13,36 +14,36 @@ import (
"github.com/jmoiron/sqlx"
)
type Auth interface {
InsertUser(ctx context.Context, user *types.User) error
UpdateUser(ctx context.Context, user *types.User) error
GetUserByEmail(ctx context.Context, email string) (*types.User, error)
GetUser(ctx context.Context, userId uuid.UUID) (*types.User, error)
type Db interface {
InsertUser(ctx context.Context, user *auth_types.User) error
UpdateUser(ctx context.Context, user *auth_types.User) error
GetUserByEmail(ctx context.Context, email string) (*auth_types.User, error)
GetUser(ctx context.Context, userId uuid.UUID) (*auth_types.User, error)
DeleteUser(ctx context.Context, userId uuid.UUID) error
InsertToken(ctx context.Context, token *types.Token) error
GetToken(ctx context.Context, token string) (*types.Token, error)
GetTokensByUserIdAndType(ctx context.Context, userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error)
GetTokensBySessionIdAndType(ctx context.Context, sessionId string, tokenType types.TokenType) ([]*types.Token, error)
InsertToken(ctx context.Context, token *auth_types.Token) error
GetToken(ctx context.Context, token string) (*auth_types.Token, error)
GetTokensByUserIdAndType(ctx context.Context, userId uuid.UUID, tokenType auth_types.TokenType) ([]*auth_types.Token, error)
GetTokensBySessionIdAndType(ctx context.Context, sessionId string, tokenType auth_types.TokenType) ([]*auth_types.Token, error)
DeleteToken(ctx context.Context, token string) error
InsertSession(ctx context.Context, session *types.Session) error
GetSession(ctx context.Context, sessionId string) (*types.Session, error)
GetSessions(ctx context.Context, userId uuid.UUID) ([]*types.Session, error)
InsertSession(ctx context.Context, session *auth_types.Session) error
GetSession(ctx context.Context, sessionId string) (*auth_types.Session, error)
GetSessions(ctx context.Context, userId uuid.UUID) ([]*auth_types.Session, error)
DeleteSession(ctx context.Context, sessionId string) error
DeleteOldSessions(ctx context.Context) error
DeleteOldTokens(ctx context.Context) error
}
type AuthSqlite struct {
type DbSqlite struct {
db *sqlx.DB
}
func NewAuthSqlite(db *sqlx.DB) *AuthSqlite {
return &AuthSqlite{db: db}
func NewDbSqlite(db *sqlx.DB) *DbSqlite {
return &DbSqlite{db: db}
}
func (db AuthSqlite) InsertUser(ctx context.Context, user *types.User) error {
func (db DbSqlite) InsertUser(ctx context.Context, user *auth_types.User) error {
_, err := db.db.ExecContext(ctx, `
INSERT INTO user (user_id, email, email_verified, email_verified_at, is_admin, password, salt, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -50,17 +51,17 @@ func (db AuthSqlite) InsertUser(ctx context.Context, user *types.User) error {
if err != nil {
if strings.Contains(err.Error(), "email") {
return ErrAlreadyExists
return core.ErrAlreadyExists
}
slog.ErrorContext(ctx, "SQL error InsertUser", "err", err)
return types.ErrInternal
return core.ErrInternal
}
return nil
}
func (db AuthSqlite) UpdateUser(ctx context.Context, user *types.User) error {
func (db DbSqlite) UpdateUser(ctx context.Context, user *auth_types.User) error {
_, err := db.db.ExecContext(ctx, `
UPDATE user
SET email_verified = ?, email_verified_at = ?, password = ?
@@ -69,13 +70,13 @@ func (db AuthSqlite) UpdateUser(ctx context.Context, user *types.User) error {
if err != nil {
slog.ErrorContext(ctx, "SQL error UpdateUser", "err", err)
return types.ErrInternal
return core.ErrInternal
}
return nil
}
func (db AuthSqlite) GetUserByEmail(ctx context.Context, email string) (*types.User, error) {
func (db DbSqlite) GetUserByEmail(ctx context.Context, email string) (*auth_types.User, error) {
var (
userId uuid.UUID
emailVerified bool
@@ -92,17 +93,17 @@ func (db AuthSqlite) GetUserByEmail(ctx context.Context, email string) (*types.U
WHERE email = ?`, email).Scan(&userId, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
return nil, core.ErrNotFound
} else {
slog.ErrorContext(ctx, "SQL error GetUser", "err", err)
return nil, types.ErrInternal
return nil, core.ErrInternal
}
}
return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
return auth_types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
}
func (db AuthSqlite) GetUser(ctx context.Context, userId uuid.UUID) (*types.User, error) {
func (db DbSqlite) GetUser(ctx context.Context, userId uuid.UUID) (*auth_types.User, error) {
var (
email string
emailVerified bool
@@ -119,92 +120,92 @@ func (db AuthSqlite) GetUser(ctx context.Context, userId uuid.UUID) (*types.User
WHERE user_id = ?`, userId).Scan(&email, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
return nil, core.ErrNotFound
} else {
slog.ErrorContext(ctx, "SQL error GetUser", "err", err)
return nil, types.ErrInternal
return nil, core.ErrInternal
}
}
return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
return auth_types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
}
func (db AuthSqlite) DeleteUser(ctx context.Context, userId uuid.UUID) error {
func (db DbSqlite) DeleteUser(ctx context.Context, userId uuid.UUID) error {
tx, err := db.db.BeginTx(ctx, nil)
if err != nil {
slog.ErrorContext(ctx, "Could not start transaction", "err", err)
return types.ErrInternal
return core.ErrInternal
}
_, err = tx.ExecContext(ctx, "DELETE FROM account WHERE user_id = ?", userId)
if err != nil {
_ = tx.Rollback()
slog.ErrorContext(ctx, "Could not delete accounts", "err", err)
return types.ErrInternal
return core.ErrInternal
}
_, err = tx.ExecContext(ctx, "DELETE FROM token WHERE user_id = ?", userId)
if err != nil {
_ = tx.Rollback()
slog.ErrorContext(ctx, "Could not delete user tokens", "err", err)
return types.ErrInternal
return core.ErrInternal
}
_, err = tx.ExecContext(ctx, "DELETE FROM session WHERE user_id = ?", userId)
if err != nil {
_ = tx.Rollback()
slog.ErrorContext(ctx, "Could not delete sessions", "err", err)
return types.ErrInternal
return core.ErrInternal
}
_, err = tx.ExecContext(ctx, "DELETE FROM user WHERE user_id = ?", userId)
if err != nil {
_ = tx.Rollback()
slog.ErrorContext(ctx, "Could not delete user", "err", err)
return types.ErrInternal
return core.ErrInternal
}
_, err = tx.ExecContext(ctx, "DELETE FROM treasure_chest WHERE user_id = ?", userId)
if err != nil {
_ = tx.Rollback()
slog.ErrorContext(ctx, "Could not delete user", "err", err)
return types.ErrInternal
return core.ErrInternal
}
_, err = tx.ExecContext(ctx, "DELETE FROM \"transaction\" WHERE user_id = ?", userId)
if err != nil {
_ = tx.Rollback()
slog.ErrorContext(ctx, "Could not delete user", "err", err)
return types.ErrInternal
return core.ErrInternal
}
err = tx.Commit()
if err != nil {
slog.ErrorContext(ctx, "Could not commit transaction", "err", err)
return types.ErrInternal
return core.ErrInternal
}
return nil
}
func (db AuthSqlite) InsertToken(ctx context.Context, token *types.Token) error {
func (db DbSqlite) InsertToken(ctx context.Context, token *auth_types.Token) error {
_, err := db.db.ExecContext(ctx, `
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 {
slog.ErrorContext(ctx, "Could not insert token", "err", err)
return types.ErrInternal
return core.ErrInternal
}
return nil
}
func (db AuthSqlite) GetToken(ctx context.Context, token string) (*types.Token, error) {
func (db DbSqlite) GetToken(ctx context.Context, token string) (*auth_types.Token, error) {
var (
userId uuid.UUID
sessionId string
tokenType types.TokenType
tokenType auth_types.TokenType
createdAtStr string
expiresAtStr string
createdAt time.Time
@@ -219,29 +220,29 @@ func (db AuthSqlite) GetToken(ctx context.Context, token string) (*types.Token,
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
slog.InfoContext(ctx, "Token not found", "token", token)
return nil, ErrNotFound
return nil, core.ErrNotFound
} else {
slog.ErrorContext(ctx, "Could not get token", "err", err)
return nil, types.ErrInternal
return nil, core.ErrInternal
}
}
createdAt, err = time.Parse(time.RFC3339, createdAtStr)
if err != nil {
slog.ErrorContext(ctx, "Could not parse token.created_at", "err", err)
return nil, types.ErrInternal
return nil, core.ErrInternal
}
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
if err != nil {
slog.ErrorContext(ctx, "Could not parse token.expires_at", "err", err)
return nil, types.ErrInternal
return nil, core.ErrInternal
}
return types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil
return auth_types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil
}
func (db AuthSqlite) GetTokensByUserIdAndType(ctx context.Context, userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error) {
func (db DbSqlite) GetTokensByUserIdAndType(ctx context.Context, userId uuid.UUID, tokenType auth_types.TokenType) ([]*auth_types.Token, error) {
query, err := db.db.QueryContext(ctx, `
SELECT token, created_at, expires_at
FROM token
@@ -250,13 +251,13 @@ func (db AuthSqlite) GetTokensByUserIdAndType(ctx context.Context, userId uuid.U
if err != nil {
slog.ErrorContext(ctx, "Could not get token", "err", err)
return nil, types.ErrInternal
return nil, core.ErrInternal
}
return getTokensFromQuery(ctx, query, userId, "", tokenType)
}
func (db AuthSqlite) GetTokensBySessionIdAndType(ctx context.Context, sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
func (db DbSqlite) GetTokensBySessionIdAndType(ctx context.Context, sessionId string, tokenType auth_types.TokenType) ([]*auth_types.Token, error) {
query, err := db.db.QueryContext(ctx, `
SELECT token, created_at, expires_at
FROM token
@@ -265,14 +266,14 @@ func (db AuthSqlite) GetTokensBySessionIdAndType(ctx context.Context, sessionId
if err != nil {
slog.ErrorContext(ctx, "Could not get token", "err", err)
return nil, types.ErrInternal
return nil, core.ErrInternal
}
return getTokensFromQuery(ctx, query, uuid.Nil, sessionId, tokenType)
}
func getTokensFromQuery(ctx context.Context, query *sql.Rows, userId uuid.UUID, sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
var tokens []*types.Token
func getTokensFromQuery(ctx context.Context, query *sql.Rows, userId uuid.UUID, sessionId string, tokenType auth_types.TokenType) ([]*auth_types.Token, error) {
var tokens []*auth_types.Token
hasRows := false
for query.Next() {
@@ -289,54 +290,54 @@ func getTokensFromQuery(ctx context.Context, query *sql.Rows, userId uuid.UUID,
err := query.Scan(&token, &createdAtStr, &expiresAtStr)
if err != nil {
slog.ErrorContext(ctx, "Could not scan token", "err", err)
return nil, types.ErrInternal
return nil, core.ErrInternal
}
createdAt, err = time.Parse(time.RFC3339, createdAtStr)
if err != nil {
slog.ErrorContext(ctx, "Could not parse token.created_at", "err", err)
return nil, types.ErrInternal
return nil, core.ErrInternal
}
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
if err != nil {
slog.ErrorContext(ctx, "Could not parse token.expires_at", "err", err)
return nil, types.ErrInternal
return nil, core.ErrInternal
}
tokens = append(tokens, types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt))
tokens = append(tokens, auth_types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt))
}
if !hasRows {
return nil, ErrNotFound
return nil, core.ErrNotFound
}
return tokens, nil
}
func (db AuthSqlite) DeleteToken(ctx context.Context, token string) error {
func (db DbSqlite) DeleteToken(ctx context.Context, token string) error {
_, err := db.db.ExecContext(ctx, "DELETE FROM token WHERE token = ?", token)
if err != nil {
slog.ErrorContext(ctx, "Could not delete token", "err", err)
return types.ErrInternal
return core.ErrInternal
}
return nil
}
func (db AuthSqlite) InsertSession(ctx context.Context, session *types.Session) error {
func (db DbSqlite) InsertSession(ctx context.Context, session *auth_types.Session) error {
_, err := db.db.ExecContext(ctx, `
INSERT INTO session (session_id, user_id, created_at, expires_at)
VALUES (?, ?, ?, ?)`, session.Id, session.UserId, session.CreatedAt, session.ExpiresAt)
if err != nil {
slog.ErrorContext(ctx, "Could not insert new session", "err", err)
return types.ErrInternal
return core.ErrInternal
}
return nil
}
func (db AuthSqlite) GetSession(ctx context.Context, sessionId string) (*types.Session, error) {
func (db DbSqlite) GetSession(ctx context.Context, sessionId string) (*auth_types.Session, error) {
var (
userId uuid.UUID
createdAt time.Time
@@ -350,56 +351,56 @@ func (db AuthSqlite) GetSession(ctx context.Context, sessionId string) (*types.S
if err != nil {
slog.WarnContext(ctx, "Session not found", "session-id", sessionId, "err", err)
return nil, ErrNotFound
return nil, core.ErrNotFound
}
return types.NewSession(sessionId, userId, createdAt, expiresAt), nil
return auth_types.NewSession(sessionId, userId, createdAt, expiresAt), nil
}
func (db AuthSqlite) GetSessions(ctx context.Context, userId uuid.UUID) ([]*types.Session, error) {
var sessions []*types.Session
func (db DbSqlite) GetSessions(ctx context.Context, userId uuid.UUID) ([]*auth_types.Session, error) {
var sessions []*auth_types.Session
err := db.db.SelectContext(ctx, &sessions, `
SELECT *
FROM session
WHERE user_id = ?`, userId)
if err != nil {
slog.ErrorContext(ctx, "Could not get sessions", "err", err)
return nil, types.ErrInternal
return nil, core.ErrInternal
}
return sessions, nil
}
func (db AuthSqlite) DeleteSession(ctx context.Context, sessionId string) error {
func (db DbSqlite) DeleteSession(ctx context.Context, sessionId string) error {
if sessionId != "" {
_, err := db.db.ExecContext(ctx, "DELETE FROM session WHERE session_id = ?", sessionId)
if err != nil {
slog.ErrorContext(ctx, "Could not delete session", "err", err)
return types.ErrInternal
return core.ErrInternal
}
}
return nil
}
func (db AuthSqlite) DeleteOldSessions(ctx context.Context) error {
func (db DbSqlite) DeleteOldSessions(ctx context.Context) error {
_, err := db.db.ExecContext(ctx, `
DELETE FROM session
WHERE expires_at < datetime('now')`)
if err != nil {
slog.ErrorContext(ctx, "Could not delete old sessions", "err", err)
return types.ErrInternal
return core.ErrInternal
}
return nil
}
func (db AuthSqlite) DeleteOldTokens(ctx context.Context) error {
func (db DbSqlite) DeleteOldTokens(ctx context.Context) error {
_, err := db.db.ExecContext(ctx, `
DELETE FROM token
WHERE expires_at < datetime('now')`)
if err != nil {
slog.ErrorContext(ctx, "Could not delete old tokens", "err", err)
return types.ErrInternal
return core.ErrInternal
}
return nil
}

View File

@@ -1,35 +1,34 @@
package handler
package authentication
import (
"errors"
"log/slog"
"net/http"
"net/url"
"spend-sparrow/internal/handler/middleware"
"spend-sparrow/internal/service"
"spend-sparrow/internal/template/auth"
"spend-sparrow/internal/authentication/template"
"spend-sparrow/internal/core"
"spend-sparrow/internal/types"
"spend-sparrow/internal/utils"
"time"
)
type Auth interface {
type Handler interface {
Handle(router *http.ServeMux)
}
type AuthImpl struct {
service service.Auth
render *Render
type HandlerImpl struct {
service Service
render *core.Render
}
func NewAuth(service service.Auth, render *Render) Auth {
return AuthImpl{
func NewHandler(service Service, render *core.Render) Handler {
return HandlerImpl{
service: service,
render: render,
}
}
func (handler AuthImpl) Handle(router *http.ServeMux) {
func (handler HandlerImpl) Handle(router *http.ServeMux) {
router.Handle("GET /auth/signin", handler.handleSignInPage())
router.Handle("POST /api/auth/signin", handler.handleSignIn())
@@ -56,11 +55,11 @@ var (
securityWaitDuration = 250 * time.Millisecond
)
func (handler AuthImpl) handleSignInPage() http.HandlerFunc {
func (handler HandlerImpl) handleSignInPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user != nil {
if !user.EmailVerified {
utils.DoRedirect(w, r, "/auth/verify")
@@ -70,18 +69,18 @@ func (handler AuthImpl) handleSignInPage() http.HandlerFunc {
return
}
comp := auth.SignInOrUpComp(true)
comp := template.SignInOrUpComp(true)
handler.render.RenderLayout(r, w, comp, nil)
}
}
func (handler AuthImpl) handleSignIn() http.HandlerFunc {
func (handler HandlerImpl) handleSignIn() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user, err := utils.WaitMinimumTime(securityWaitDuration, func() (*types.User, error) {
session := middleware.GetSession(r)
user, err := utils.WaitMinimumTime(securityWaitDuration, func() (*User, error) {
session := core.GetSession(r)
email := r.FormValue("email")
password := r.FormValue("password")
@@ -113,11 +112,11 @@ func (handler AuthImpl) handleSignIn() http.HandlerFunc {
}
}
func (handler AuthImpl) handleSignUpPage() http.HandlerFunc {
func (handler HandlerImpl) handleSignUpPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user != nil {
if !user.EmailVerified {
@@ -133,11 +132,11 @@ func (handler AuthImpl) handleSignUpPage() http.HandlerFunc {
}
}
func (handler AuthImpl) handleSignUpVerifyPage() http.HandlerFunc {
func (handler HandlerImpl) handleSignUpVerifyPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -153,11 +152,11 @@ func (handler AuthImpl) handleSignUpVerifyPage() http.HandlerFunc {
}
}
func (handler AuthImpl) handleVerifyResendComp() http.HandlerFunc {
func (handler HandlerImpl) handleVerifyResendComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -172,9 +171,9 @@ func (handler AuthImpl) handleVerifyResendComp() http.HandlerFunc {
}
}
func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc {
func (handler HandlerImpl) handleSignUpVerifyResponsePage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
token := r.URL.Query().Get("token")
@@ -194,9 +193,9 @@ func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc {
}
}
func (handler AuthImpl) handleSignUp() http.HandlerFunc {
func (handler HandlerImpl) handleSignUp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
var email = r.FormValue("email")
var password = r.FormValue("password")
@@ -232,11 +231,11 @@ func (handler AuthImpl) handleSignUp() http.HandlerFunc {
}
}
func (handler AuthImpl) handleSignOut() http.HandlerFunc {
func (handler HandlerImpl) handleSignOut() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
session := middleware.GetSession(r)
session := core.GetSession(r)
if session != nil {
err := handler.service.SignOut(r.Context(), session.Id)
@@ -261,11 +260,11 @@ func (handler AuthImpl) handleSignOut() http.HandlerFunc {
}
}
func (handler AuthImpl) handleDeleteAccountPage() http.HandlerFunc {
func (handler HandlerImpl) handleDeleteAccountPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -276,11 +275,11 @@ func (handler AuthImpl) handleDeleteAccountPage() http.HandlerFunc {
}
}
func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc {
func (handler HandlerImpl) handleDeleteAccountComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -302,13 +301,13 @@ func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc {
}
}
func (handler AuthImpl) handleChangePasswordPage() http.HandlerFunc {
func (handler HandlerImpl) handleChangePasswordPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
isPasswordReset := r.URL.Query().Has("token")
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil && !isPasswordReset {
utils.DoRedirect(w, r, "/auth/signin")
@@ -320,12 +319,12 @@ func (handler AuthImpl) handleChangePasswordPage() http.HandlerFunc {
}
}
func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc {
func (handler HandlerImpl) handleChangePasswordComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
session := middleware.GetSession(r)
user := middleware.GetUser(r)
session := core.GetSession(r)
user := core.GetUser(r)
if session == nil || user == nil {
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Unathorized", http.StatusUnauthorized)
return
@@ -344,11 +343,11 @@ func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc {
}
}
func (handler AuthImpl) handleForgotPasswordPage() http.HandlerFunc {
func (handler HandlerImpl) handleForgotPasswordPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user != nil {
utils.DoRedirect(w, r, "/")
return
@@ -359,9 +358,9 @@ func (handler AuthImpl) handleForgotPasswordPage() http.HandlerFunc {
}
}
func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc {
func (handler HandlerImpl) handleForgotPasswordComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
email := r.FormValue("email")
if email == "" {
@@ -382,9 +381,9 @@ func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc {
}
}
func (handler AuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
func (handler HandlerImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
pageUrl, err := url.Parse(r.Header.Get("Hx-Current-Url"))
if err != nil {

View File

@@ -1,4 +1,4 @@
package service
package authentication
import (
"context"
@@ -6,6 +6,8 @@ import (
"errors"
"log/slog"
"net/mail"
"spend-sparrow/internal/auth_types"
"spend-sparrow/internal/core"
"spend-sparrow/internal/db"
mailTemplate "spend-sparrow/internal/template/mail"
"spend-sparrow/internal/types"
@@ -25,39 +27,39 @@ var (
ErrTokenInvalid = errors.New("token is invalid")
)
type Auth interface {
SignUp(ctx context.Context, email string, password string) (*types.User, error)
type Service interface {
SignUp(ctx context.Context, email string, password string) (*auth_types.User, error)
SendVerificationMail(ctx context.Context, userId uuid.UUID, email string)
VerifyUserEmail(ctx context.Context, token string) error
SignIn(ctx context.Context, session *types.Session, email string, password string) (*types.Session, *types.User, error)
SignInSession(ctx context.Context, sessionId string) (*types.Session, *types.User, error)
SignInAnonymous(ctx context.Context) (*types.Session, error)
SignIn(ctx context.Context, session *auth_types.Session, email string, password string) (*auth_types.Session, *auth_types.User, error)
SignInSession(ctx context.Context, sessionId string) (*auth_types.Session, *auth_types.User, error)
SignInAnonymous(ctx context.Context) (*auth_types.Session, error)
SignOut(ctx context.Context, sessionId string) error
DeleteAccount(ctx context.Context, user *types.User, currPass string) error
DeleteAccount(ctx context.Context, user *auth_types.User, currPass string) error
ChangePassword(ctx context.Context, user *types.User, sessionId string, currPass, newPass string) error
ChangePassword(ctx context.Context, user *auth_types.User, sessionId string, currPass, newPass string) error
SendForgotPasswordMail(ctx context.Context, email string) error
ForgotPassword(ctx context.Context, token string, newPass string) error
IsCsrfTokenValid(ctx context.Context, tokenStr string, sessionId string) bool
GetCsrfToken(ctx context.Context, session *types.Session) (string, error)
GetCsrfToken(ctx context.Context, session *auth_types.Session) (string, error)
CleanupSessionsAndTokens(ctx context.Context) error
}
type AuthImpl struct {
db db.Auth
random Random
type ServiceImpl struct {
db Db
random core.Random
clock Clock
mail Mail
serverSettings *types.Settings
}
func NewAuth(db db.Auth, random Random, clock Clock, mail Mail, serverSettings *types.Settings) *AuthImpl {
return &AuthImpl{
func NewService(db db.Auth, random Random, clock Clock, mail Mail, serverSettings *types.Settings) *HandlerImpl {
return &HandlerImpl{
db: db,
random: random,
clock: clock,
@@ -66,7 +68,7 @@ func NewAuth(db db.Auth, random Random, clock Clock, mail Mail, serverSettings *
}
}
func (service AuthImpl) SignIn(ctx context.Context, session *types.Session, email string, password string) (*types.Session, *types.User, error) {
func (service HandlerImpl) SignIn(ctx context.Context, session *auth_types.Session, email string, password string) (*auth_type.Session, *auth_types.User, error) {
user, err := service.db.GetUserByEmail(ctx, email)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
@@ -106,7 +108,7 @@ func (service AuthImpl) SignIn(ctx context.Context, session *types.Session, emai
return newSession, user, nil
}
func (service AuthImpl) SignInSession(ctx context.Context, sessionId string) (*types.Session, *types.User, error) {
func (service HandlerImpl) SignInSession(ctx context.Context, sessionId string) (*auth_types.Session, *auth_types.User, error) {
if sessionId == "" {
return nil, nil, ErrSessionIdInvalid
}
@@ -132,7 +134,7 @@ func (service AuthImpl) SignInSession(ctx context.Context, sessionId string) (*t
return session, user, nil
}
func (service AuthImpl) SignInAnonymous(ctx context.Context) (*types.Session, error) {
func (service HandlerImpl) SignInAnonymous(ctx context.Context) (*auth_types.Session, error) {
session, err := service.createSession(ctx, uuid.Nil)
if err != nil {
return nil, types.ErrInternal
@@ -143,7 +145,7 @@ func (service AuthImpl) SignInAnonymous(ctx context.Context) (*types.Session, er
return session, nil
}
func (service AuthImpl) SignUp(ctx context.Context, email string, password string) (*types.User, error) {
func (service HandlerImpl) SignUp(ctx context.Context, email string, password string) (*auth_types.User, error) {
_, err := mail.ParseAddress(email)
if err != nil {
return nil, ErrInvalidEmail
@@ -179,7 +181,7 @@ func (service AuthImpl) SignUp(ctx context.Context, email string, password strin
return user, nil
}
func (service AuthImpl) SendVerificationMail(ctx context.Context, userId uuid.UUID, email string) {
func (service HandlerImpl) SendVerificationMail(ctx context.Context, userId uuid.UUID, email string) {
tokens, err := service.db.GetTokensByUserIdAndType(ctx, userId, types.TokenTypeEmailVerify)
if err != nil && !errors.Is(err, db.ErrNotFound) {
return
@@ -221,7 +223,7 @@ func (service AuthImpl) SendVerificationMail(ctx context.Context, userId uuid.UU
service.mail.SendMail(ctx, email, "Welcome to spend-sparrow", w.String())
}
func (service AuthImpl) VerifyUserEmail(ctx context.Context, tokenStr string) error {
func (service HandlerImpl) VerifyUserEmail(ctx context.Context, tokenStr string) error {
if tokenStr == "" {
return types.ErrInternal
}
@@ -258,11 +260,11 @@ func (service AuthImpl) VerifyUserEmail(ctx context.Context, tokenStr string) er
return nil
}
func (service AuthImpl) SignOut(ctx context.Context, sessionId string) error {
func (service HandlerImpl) SignOut(ctx context.Context, sessionId string) error {
return service.db.DeleteSession(ctx, sessionId)
}
func (service AuthImpl) DeleteAccount(ctx context.Context, user *types.User, currPass string) error {
func (service HandlerImpl) DeleteAccount(ctx context.Context, user *auth_types.User, currPass string) error {
userDb, err := service.db.GetUser(ctx, user.Id)
if err != nil {
return types.ErrInternal
@@ -283,7 +285,7 @@ func (service AuthImpl) DeleteAccount(ctx context.Context, user *types.User, cur
return nil
}
func (service AuthImpl) ChangePassword(ctx context.Context, user *types.User, sessionId string, currPass, newPass string) error {
func (service HandlerImpl) ChangePassword(ctx context.Context, user *auth_types.User, sessionId string, currPass, newPass string) error {
if !isPasswordValid(newPass) {
return ErrInvalidPassword
}
@@ -322,7 +324,7 @@ func (service AuthImpl) ChangePassword(ctx context.Context, user *types.User, se
return nil
}
func (service AuthImpl) SendForgotPasswordMail(ctx context.Context, email string) error {
func (service HandlerImpl) SendForgotPasswordMail(ctx context.Context, email string) error {
tokenStr, err := service.random.String(ctx, 32)
if err != nil {
return err
@@ -361,7 +363,7 @@ func (service AuthImpl) SendForgotPasswordMail(ctx context.Context, email string
return nil
}
func (service AuthImpl) ForgotPassword(ctx context.Context, tokenStr string, newPass string) error {
func (service HandlerImpl) ForgotPassword(ctx context.Context, tokenStr string, newPass string) error {
if !isPasswordValid(newPass) {
return ErrInvalidPassword
}
@@ -410,7 +412,7 @@ func (service AuthImpl) ForgotPassword(ctx context.Context, tokenStr string, new
return nil
}
func (service AuthImpl) IsCsrfTokenValid(ctx context.Context, tokenStr string, sessionId string) bool {
func (service HandlerImpl) IsCsrfTokenValid(ctx context.Context, tokenStr string, sessionId string) bool {
token, err := service.db.GetToken(ctx, tokenStr)
if err != nil {
return false
@@ -425,7 +427,7 @@ func (service AuthImpl) IsCsrfTokenValid(ctx context.Context, tokenStr string, s
return true
}
func (service AuthImpl) GetCsrfToken(ctx context.Context, session *types.Session) (string, error) {
func (service HandlerImpl) GetCsrfToken(ctx context.Context, session *auth_types.Session) (string, error) {
if session == nil {
return "", types.ErrInternal
}
@@ -458,7 +460,7 @@ func (service AuthImpl) GetCsrfToken(ctx context.Context, session *types.Session
return tokenStr, nil
}
func (service AuthImpl) CleanupSessionsAndTokens(ctx context.Context) error {
func (service HandlerImpl) CleanupSessionsAndTokens(ctx context.Context) error {
err := service.db.DeleteOldSessions(ctx)
if err != nil {
return types.ErrInternal
@@ -472,7 +474,7 @@ func (service AuthImpl) CleanupSessionsAndTokens(ctx context.Context) error {
return nil
}
func (service AuthImpl) createSession(ctx context.Context, userId uuid.UUID) (*types.Session, error) {
func (service HandlerImpl) createSession(ctx context.Context, userId uuid.UUID) (*auth_types.Session, error) {
sessionId, err := service.random.String(ctx, 32)
if err != nil {
return nil, types.ErrInternal

View File

@@ -1,4 +1,4 @@
package auth
package template
templ ChangePasswordComp(isPasswordReset bool) {
<form

View File

@@ -0,0 +1 @@
package template

View File

@@ -1,4 +1,4 @@
package auth
package template
templ DeleteAccountComp() {
<form

View File

@@ -1,4 +1,4 @@
package auth
package template
templ ResetPasswordComp() {
<form

View File

@@ -1,13 +1,13 @@
package auth
package template
templ SignInOrUpComp(isSignIn bool) {
{{
var postUrl string
if isSignIn {
postUrl = "/api/auth/signin"
} else {
postUrl = "/api/auth/signup"
}
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"

View File

@@ -1,4 +1,4 @@
package auth
package template
templ VerifyComp() {
<main class="h-full">

View File

@@ -1,4 +1,4 @@
package auth
package template
templ VerifyResponseComp(isVerified bool) {
<main>

51
internal/core/auth.go Normal file
View File

@@ -0,0 +1,51 @@
package core
import (
"net/http"
"spend-sparrow/internal/auth_types"
)
type ContextKey string
var SessionKey ContextKey = "session"
var UserKey ContextKey = "user"
func GetUser(r *http.Request) *auth_types.User {
obj := r.Context().Value(UserKey)
if obj == nil {
return nil
}
user, ok := obj.(*auth_types.User)
if !ok {
return nil
}
return user
}
func GetSession(r *http.Request) *auth_types.Session {
obj := r.Context().Value(SessionKey)
if obj == nil {
return nil
}
session, ok := obj.(*auth_types.Session)
if !ok {
return nil
}
return session
}
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,10 +1,8 @@
package handler
package core
import (
"errors"
"net/http"
"spend-sparrow/internal/db"
"spend-sparrow/internal/service"
"spend-sparrow/internal/utils"
"strings"
@@ -12,15 +10,15 @@ import (
"go.opentelemetry.io/otel/trace"
)
func handleError(w http.ResponseWriter, r *http.Request, err error) {
func HandleError(w http.ResponseWriter, r *http.Request, err error) {
switch {
case errors.Is(err, service.ErrUnauthorized):
case errors.Is(err, ErrUnauthorized):
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "You are not autorized to perform this operation.", http.StatusUnauthorized)
return
case errors.Is(err, service.ErrBadRequest):
case errors.Is(err, ErrBadRequest):
utils.TriggerToastWithStatus(r.Context(), w, r, "error", extractErrorMessage(err), http.StatusBadRequest)
return
case errors.Is(err, db.ErrNotFound):
case errors.Is(err, ErrNotFound):
utils.TriggerToastWithStatus(r.Context(), w, r, "error", extractErrorMessage(err), http.StatusNotFound)
return
}
@@ -37,7 +35,7 @@ func extractErrorMessage(err error) string {
return strings.SplitN(errMsg, ":", 2)[0]
}
func updateSpan(r *http.Request) {
func UpdateSpan(r *http.Request) {
currentSpan := trace.SpanFromContext(r.Context())
if currentSpan != nil {
currentSpan.SetAttributes(attribute.String("http.pattern", r.Pattern))

13
internal/core/error.go Normal file
View File

@@ -0,0 +1,13 @@
package core
import "errors"
var (
ErrNotFound = errors.New("the value does not exist")
ErrAlreadyExists = errors.New("row already exists")
ErrInternal = errors.New("internal server error")
ErrUnauthorized = errors.New("you are not authorized to perform this action")
ErrBadRequest = errors.New("bad request")
)

View File

@@ -1,4 +1,4 @@
package template
package core
import "spend-sparrow/internal/template/svg"

View File

@@ -1,11 +1,10 @@
package service
package core
import (
"context"
"crypto/rand"
"encoding/base64"
"log/slog"
"spend-sparrow/internal/types"
"github.com/google/uuid"
)
@@ -28,7 +27,7 @@ func (r *RandomImpl) Bytes(ctx context.Context, tsize int) ([]byte, error) {
_, err := rand.Read(b)
if err != nil {
slog.ErrorContext(ctx, "Error generating random bytes", "err", err)
return []byte{}, types.ErrInternal
return []byte{}, ErrInternal
}
return b, nil
@@ -38,7 +37,7 @@ func (r *RandomImpl) String(ctx context.Context, size int) (string, error) {
bytes, err := r.Bytes(ctx, size)
if err != nil {
slog.ErrorContext(ctx, "Error generating random string", "err", err)
return "", types.ErrInternal
return "", ErrInternal
}
return base64.StdEncoding.EncodeToString(bytes), nil
@@ -48,7 +47,7 @@ func (r *RandomImpl) UUID(ctx context.Context) (uuid.UUID, error) {
id, err := uuid.NewRandom()
if err != nil {
slog.ErrorContext(ctx, "Error generating random UUID", "err", err)
return uuid.Nil, types.ErrInternal
return uuid.Nil, ErrInternal
}
return id, nil

View File

@@ -1,11 +1,9 @@
package handler
package core
import (
"log/slog"
"net/http"
"spend-sparrow/internal/template"
"spend-sparrow/internal/template/auth"
"spend-sparrow/internal/types"
"spend-sparrow/internal/auth_types"
"github.com/a-h/templ"
)
@@ -31,21 +29,21 @@ func (render *Render) Render(r *http.Request, w http.ResponseWriter, comp templ.
render.RenderWithStatus(r, w, comp, http.StatusOK)
}
func (render *Render) RenderLayout(r *http.Request, w http.ResponseWriter, slot templ.Component, user *types.User) {
func (render *Render) RenderLayout(r *http.Request, w http.ResponseWriter, slot templ.Component, user *auth_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) {
func (render *Render) RenderLayoutWithStatus(r *http.Request, w http.ResponseWriter, slot templ.Component, user *auth_types.User, status int) {
userComp := render.getUserComp(user)
layout := template.Layout(slot, userComp, user != nil, r.URL.Path)
layout := Layout(slot, userComp, user != nil, r.URL.Path)
render.RenderWithStatus(r, w, layout, status)
}
func (render *Render) getUserComp(user *types.User) templ.Component {
func (render *Render) getUserComp(user *auth_types.User) templ.Component {
if user != nil {
return auth.UserComp(user.Email)
return UserComp(user.Email)
} else {
return auth.UserComp("")
return UserComp("")
}
}

View File

@@ -1,4 +1,4 @@
package auth
package core
templ UserComp(user string) {
<div id="user-info" class="flex items-center gap-2 text-nowrap">

View File

@@ -5,33 +5,28 @@ import (
"database/sql"
"errors"
"log/slog"
"spend-sparrow/internal/types"
)
var (
ErrNotFound = errors.New("the value does not exist")
ErrAlreadyExists = errors.New("row already exists")
"spend-sparrow/internal/core"
)
func TransformAndLogDbError(ctx context.Context, module string, r sql.Result, err error) error {
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
return core.ErrNotFound
}
slog.ErrorContext(ctx, "database sql", "module", module, "err", err)
return types.ErrInternal
return core.ErrInternal
}
if r != nil {
rows, err := r.RowsAffected()
if err != nil {
slog.ErrorContext(ctx, "database rows affected", "module", module, "err", err)
return types.ErrInternal
return core.ErrInternal
}
if rows == 0 {
slog.InfoContext(ctx, "row not found", "module", module)
return ErrNotFound
return core.ErrNotFound
}
}

View File

@@ -4,7 +4,7 @@ import (
"context"
"errors"
"log/slog"
"spend-sparrow/internal/types"
"spend-sparrow/internal/core"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
@@ -25,7 +25,7 @@ func RunMigrations(ctx context.Context, db *sqlx.DB, pathPrefix string) error {
driver, err := sqlite3.WithInstance(db.DB, &sqlite3.Config{})
if err != nil {
slog.ErrorContext(ctx, "Could not create Migration instance", "err", err)
return types.ErrInternal
return core.ErrInternal
}
m, err := migrate.NewWithDatabaseInstance(
@@ -34,14 +34,14 @@ func RunMigrations(ctx context.Context, db *sqlx.DB, pathPrefix string) error {
driver)
if err != nil {
slog.ErrorContext(ctx, "Could not create migrations instance", "err", err)
return types.ErrInternal
return core.ErrInternal
}
m.Log = migrationLogger{}
if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
slog.ErrorContext(ctx, "Could not run migrations", "err", err)
return types.ErrInternal
return core.ErrInternal
}
return nil

View File

@@ -1,19 +1,20 @@
package internal
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os/signal"
"spend-sparrow/internal/account"
"spend-sparrow/internal/core"
"spend-sparrow/internal/db"
"spend-sparrow/internal/handler"
"spend-sparrow/internal/handler/middleware"
"spend-sparrow/internal/log"
"spend-sparrow/internal/service"
"spend-sparrow/internal/types"
"context"
"net/http"
"os/signal"
"sync"
"syscall"
"time"
@@ -113,17 +114,17 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
mailService := service.NewMail(serverSettings)
authService := service.NewAuth(authDb, randomService, clockService, mailService, serverSettings)
accountService := service.NewAccount(d, randomService, clockService)
accountService := account.NewServiceImpl(d, randomService, clockService)
treasureChestService := service.NewTreasureChest(d, randomService, clockService)
transactionService := service.NewTransaction(d, randomService, clockService)
transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService, transactionService)
dashboardService := service.NewDashboard(d)
render := handler.NewRender()
render := core.NewRender()
indexHandler := handler.NewIndex(render, clockService)
dashboardHandler := handler.NewDashboard(render, dashboardService, treasureChestService)
authHandler := handler.NewAuth(authService, render)
accountHandler := handler.NewAccount(accountService, render)
accountHandler := account.NewHandler(accountService, render)
treasureChestHandler := handler.NewTreasureChest(treasureChestService, transactionRecurringService, render)
transactionHandler := handler.NewTransaction(transactionService, accountService, treasureChestService, render)
transactionRecurringHandler := handler.NewTransactionRecurring(transactionRecurringService, render)

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"log/slog"
"net/http"
"spend-sparrow/internal/handler/middleware"
"spend-sparrow/internal/core"
"spend-sparrow/internal/service"
"spend-sparrow/internal/template/dashboard"
"spend-sparrow/internal/utils"
@@ -19,12 +19,12 @@ type Dashboard interface {
}
type DashboardImpl struct {
r *Render
r *core.Render
d *service.Dashboard
treasureChest service.TreasureChest
}
func NewDashboard(r *Render, d *service.Dashboard, treasureChest service.TreasureChest) Dashboard {
func NewDashboard(r *core.Render, d *service.Dashboard, treasureChest service.TreasureChest) Dashboard {
return DashboardImpl{
r: r,
d: d,
@@ -41,9 +41,9 @@ func (handler DashboardImpl) Handle(router *http.ServeMux) {
func (handler DashboardImpl) handleDashboard() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -51,7 +51,7 @@ func (handler DashboardImpl) handleDashboard() http.HandlerFunc {
treasureChests, err := handler.treasureChest.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -62,13 +62,13 @@ func (handler DashboardImpl) handleDashboard() http.HandlerFunc {
func (handler DashboardImpl) handleDashboardMainChart() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
series, err := handler.d.MainChart(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -79,8 +79,8 @@ func (handler DashboardImpl) handleDashboardMainChart() http.HandlerFunc {
savingsBuilder := strings.Builder{}
for _, entry := range series {
accountBuilder.WriteString(fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100))
savingsBuilder.WriteString(fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Savings)/100))
fmt.Fprintf(&accountBuilder, `["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100)
fmt.Fprintf(&savingsBuilder, `["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Savings)/100)
}
account := accountBuilder.String()
@@ -128,13 +128,13 @@ func (handler DashboardImpl) handleDashboardMainChart() http.HandlerFunc {
func (handler DashboardImpl) handleDashboardTreasureChests() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
treeList, err := handler.d.TreasureChests(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -148,15 +148,15 @@ func (handler DashboardImpl) handleDashboardTreasureChests() http.HandlerFunc {
for _, child := range item.Children {
if child.Value < 0 {
childrenBuilder.WriteString(fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, -child.Value))
fmt.Fprintf(&childrenBuilder, `{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, -child.Value)
} else {
childrenBuilder.WriteString(fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, child.Value))
fmt.Fprintf(&childrenBuilder, `{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, child.Value)
}
}
children := childrenBuilder.String()
children = children[:len(children)-1]
dataBuilder.WriteString(fmt.Sprintf(`{"name":"%s","children":[%s]},`, item.Name, children))
fmt.Fprintf(&dataBuilder, `{"name":"%s","children":[%s]},`, item.Name, children)
}
data := dataBuilder.String()
data = data[:len(data)-1]
@@ -183,9 +183,9 @@ func (handler DashboardImpl) handleDashboardTreasureChests() http.HandlerFunc {
func (handler DashboardImpl) handleDashboardTreasureChest() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
var treasureChestId *uuid.UUID
@@ -193,7 +193,7 @@ func (handler DashboardImpl) handleDashboardTreasureChest() http.HandlerFunc {
if treasureChestStr != "" {
id, err := uuid.Parse(treasureChestStr)
if err != nil {
handleError(w, r, fmt.Errorf("could not parse treasure chest: %w", service.ErrBadRequest))
core.HandleError(w, r, fmt.Errorf("could not parse treasure chest: %w", service.ErrBadRequest))
return
}
@@ -202,7 +202,7 @@ func (handler DashboardImpl) handleDashboardTreasureChest() http.HandlerFunc {
series, err := handler.d.TreasureChest(r.Context(), user, treasureChestId)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -212,7 +212,7 @@ func (handler DashboardImpl) handleDashboardTreasureChest() http.HandlerFunc {
valueBuilder := strings.Builder{}
for _, entry := range series {
valueBuilder.WriteString(fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100))
fmt.Fprintf(&valueBuilder, `["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100)
}
value := valueBuilder.String()

View File

@@ -3,17 +3,11 @@ package middleware
import (
"context"
"net/http"
"strings"
"spend-sparrow/internal/core"
"spend-sparrow/internal/service"
"spend-sparrow/internal/types"
"strings"
)
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) {
@@ -41,42 +35,14 @@ func Authenticate(service service.Auth) func(http.Handler) http.Handler {
http.SetCookie(w, &cookie)
}
ctx = context.WithValue(ctx, UserKey, user)
ctx = context.WithValue(ctx, SessionKey, session)
ctx = context.WithValue(ctx, core.UserKey, user)
ctx = context.WithValue(ctx, core.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
}
user, ok := obj.(*types.User)
if !ok {
return nil
}
return user
}
func GetSession(r *http.Request) *types.Session {
obj := r.Context().Value(SessionKey)
if obj == nil {
return nil
}
session, ok := obj.(*types.Session)
if !ok {
return nil
}
return session
}
func getSessionID(r *http.Request) string {
cookie, err := r.Cookie("id")
if err != nil {

View File

@@ -3,6 +3,7 @@ package middleware
import (
"log/slog"
"net/http"
"spend-sparrow/internal/core"
"spend-sparrow/internal/service"
"spend-sparrow/internal/utils"
"strings"
@@ -40,7 +41,7 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler
return
}
session := GetSession(r)
session := core.GetSession(r)
if r.Method == http.MethodPost ||
r.Method == http.MethodPut ||

View File

@@ -1,15 +1 @@
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

@@ -2,7 +2,7 @@ package handler
import (
"net/http"
"spend-sparrow/internal/handler/middleware"
"spend-sparrow/internal/core"
"spend-sparrow/internal/service"
"spend-sparrow/internal/template"
"spend-sparrow/internal/utils"
@@ -15,11 +15,11 @@ type Index interface {
}
type IndexImpl struct {
r *Render
r *core.Render
c service.Clock
}
func NewIndex(r *Render, c service.Clock) Index {
func NewIndex(r *core.Render, c service.Clock) Index {
return IndexImpl{
r: r,
c: c,
@@ -33,9 +33,9 @@ func (handler IndexImpl) Handle(router *http.ServeMux) {
func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
htmx := utils.IsHtmx(r)
@@ -65,7 +65,7 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
func (handler IndexImpl) handleEmpty() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
// Return nothing
}

View File

@@ -4,7 +4,8 @@ import (
"fmt"
"math"
"net/http"
"spend-sparrow/internal/handler/middleware"
"spend-sparrow/internal/account"
"spend-sparrow/internal/core"
"spend-sparrow/internal/service"
t "spend-sparrow/internal/template/transaction"
"spend-sparrow/internal/types"
@@ -22,12 +23,12 @@ type Transaction interface {
type TransactionImpl struct {
s service.Transaction
account service.Account
account account.Service
treasureChest service.TreasureChest
r *Render
r *core.Render
}
func NewTransaction(s service.Transaction, account service.Account, treasureChest service.TreasureChest, r *Render) Transaction {
func NewTransaction(s service.Transaction, account account.Service, treasureChest service.TreasureChest, r *core.Render) Transaction {
return TransactionImpl{
s: s,
account: account,
@@ -46,9 +47,9 @@ func (h TransactionImpl) Handle(r *http.ServeMux) {
func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -63,19 +64,19 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
transactions, err := h.s.GetAll(r.Context(), user, filter)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
accounts, err := h.account.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
treasureChests, err := h.treasureChest.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -93,9 +94,9 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -103,13 +104,13 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
accounts, err := h.account.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
treasureChests, err := h.treasureChest.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -122,7 +123,7 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
transaction, err := h.s.Get(r.Context(), user, id)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -139,9 +140,9 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -156,7 +157,7 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
if idStr != "new" {
id, err = uuid.Parse(idStr)
if err != nil {
handleError(w, r, fmt.Errorf("could not parse Id: %w", service.ErrBadRequest))
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", service.ErrBadRequest))
return
}
}
@@ -166,7 +167,7 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
if accountIdStr != "" {
i, err := uuid.Parse(accountIdStr)
if err != nil {
handleError(w, r, fmt.Errorf("could not parse account id: %w", service.ErrBadRequest))
core.HandleError(w, r, fmt.Errorf("could not parse account id: %w", service.ErrBadRequest))
return
}
accountId = &i
@@ -177,7 +178,7 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
if treasureChestIdStr != "" {
i, err := uuid.Parse(treasureChestIdStr)
if err != nil {
handleError(w, r, fmt.Errorf("could not parse treasure chest id: %w", service.ErrBadRequest))
core.HandleError(w, r, fmt.Errorf("could not parse treasure chest id: %w", service.ErrBadRequest))
return
}
treasureChestId = &i
@@ -185,14 +186,14 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
valueF, err := strconv.ParseFloat(r.FormValue("value"), 64)
if err != nil {
handleError(w, r, fmt.Errorf("could not parse value: %w", service.ErrBadRequest))
core.HandleError(w, r, fmt.Errorf("could not parse value: %w", service.ErrBadRequest))
return
}
value := int64(math.Round(valueF * service.DECIMALS_MULTIPLIER))
timestamp, err := time.Parse("2006-01-02", r.FormValue("timestamp"))
if err != nil {
handleError(w, r, fmt.Errorf("could not parse timestamp: %w", service.ErrBadRequest))
core.HandleError(w, r, fmt.Errorf("could not parse timestamp: %w", service.ErrBadRequest))
return
}
@@ -210,26 +211,26 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
if idStr == "new" {
transaction, err = h.s.Add(r.Context(), nil, user, input)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
} else {
transaction, err = h.s.Update(r.Context(), user, input)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
}
accounts, err := h.account.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
treasureChests, err := h.treasureChest.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -241,9 +242,9 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
func (h TransactionImpl) handleRecalculate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -251,7 +252,7 @@ func (h TransactionImpl) handleRecalculate() http.HandlerFunc {
err := h.s.RecalculateBalances(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -261,9 +262,9 @@ func (h TransactionImpl) handleRecalculate() http.HandlerFunc {
func (h TransactionImpl) handleDeleteTransaction() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -273,13 +274,13 @@ func (h TransactionImpl) handleDeleteTransaction() http.HandlerFunc {
err := h.s.Delete(r.Context(), user, id)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
}
}
func (h TransactionImpl) getTransactionData(accounts []*types.Account, treasureChests []*types.TreasureChest) (map[uuid.UUID]string, map[uuid.UUID]string) {
func (h TransactionImpl) getTransactionData(accounts []*account.Account, treasureChests []*types.TreasureChest) (map[uuid.UUID]string, map[uuid.UUID]string) {
accountMap := make(map[uuid.UUID]string, 0)
for _, account := range accounts {
accountMap[account.Id] = account.Name

View File

@@ -2,7 +2,7 @@ package handler
import (
"net/http"
"spend-sparrow/internal/handler/middleware"
"spend-sparrow/internal/core"
"spend-sparrow/internal/service"
t "spend-sparrow/internal/template/transaction_recurring"
"spend-sparrow/internal/types"
@@ -15,10 +15,10 @@ type TransactionRecurring interface {
type TransactionRecurringImpl struct {
s service.TransactionRecurring
r *Render
r *core.Render
}
func NewTransactionRecurring(s service.TransactionRecurring, r *Render) TransactionRecurring {
func NewTransactionRecurring(s service.TransactionRecurring, r *core.Render) TransactionRecurring {
return TransactionRecurringImpl{
s: s,
r: r,
@@ -33,9 +33,9 @@ func (h TransactionRecurringImpl) Handle(r *http.ServeMux) {
func (h TransactionRecurringImpl) handleTransactionRecurringItemComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -50,9 +50,9 @@ func (h TransactionRecurringImpl) handleTransactionRecurringItemComp() http.Hand
func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -72,13 +72,13 @@ func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.Handle
if input.Id == "new" {
_, err := h.s.Add(r.Context(), user, input)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
} else {
_, err := h.s.Update(r.Context(), user, input)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
}
@@ -89,9 +89,9 @@ func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.Handle
func (h TransactionRecurringImpl) handleDeleteTransactionRecurring() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -103,7 +103,7 @@ func (h TransactionRecurringImpl) handleDeleteTransactionRecurring() http.Handle
err := h.s.Delete(r.Context(), user, id)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -120,13 +120,13 @@ func (h TransactionRecurringImpl) renderItems(w http.ResponseWriter, r *http.Req
if accountId != "" {
transactionsRecurring, err = h.s.GetAllByAccount(r.Context(), user, accountId)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
} else {
transactionsRecurring, err = h.s.GetAllByTreasureChest(r.Context(), user, treasureChestId)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
}

View File

@@ -2,7 +2,7 @@ package handler
import (
"net/http"
"spend-sparrow/internal/handler/middleware"
"spend-sparrow/internal/core"
"spend-sparrow/internal/service"
tr "spend-sparrow/internal/template/transaction_recurring"
t "spend-sparrow/internal/template/treasurechest"
@@ -20,10 +20,10 @@ type TreasureChest interface {
type TreasureChestImpl struct {
s service.TreasureChest
transactionRecurring service.TransactionRecurring
r *Render
r *core.Render
}
func NewTreasureChest(s service.TreasureChest, transactionRecurring service.TransactionRecurring, r *Render) TreasureChest {
func NewTreasureChest(s service.TreasureChest, transactionRecurring service.TransactionRecurring, r *core.Render) TreasureChest {
return TreasureChestImpl{
s: s,
transactionRecurring: transactionRecurring,
@@ -40,9 +40,9 @@ func (h TreasureChestImpl) Handle(r *http.ServeMux) {
func (h TreasureChestImpl) handleTreasureChestPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -50,13 +50,13 @@ func (h TreasureChestImpl) handleTreasureChestPage() http.HandlerFunc {
treasureChests, err := h.s.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
transactionsRecurring, err := h.transactionRecurring.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -69,9 +69,9 @@ func (h TreasureChestImpl) handleTreasureChestPage() http.HandlerFunc {
func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -79,7 +79,7 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
treasureChests, err := h.s.GetAll(r.Context(), user)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -92,13 +92,13 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
treasureChest, err := h.s.Get(r.Context(), user, id)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(r.Context(), user, treasureChest.Id.String())
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
transactionsRec := tr.TransactionRecurringItems(transactionsRecurring, "", "", treasureChest.Id.String())
@@ -116,9 +116,9 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
func (h TreasureChestImpl) handleUpdateTreasureChest() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -134,20 +134,20 @@ func (h TreasureChestImpl) handleUpdateTreasureChest() http.HandlerFunc {
if id == "new" {
treasureChest, err = h.s.Add(r.Context(), user, parentId, name)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
} else {
treasureChest, err = h.s.Update(r.Context(), user, id, parentId, name)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
}
transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(r.Context(), user, treasureChest.Id.String())
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
@@ -161,9 +161,9 @@ func (h TreasureChestImpl) handleUpdateTreasureChest() http.HandlerFunc {
func (h TreasureChestImpl) handleDeleteTreasureChest() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
core.UpdateSpan(r)
user := middleware.GetUser(r)
user := core.GetUser(r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
@@ -173,7 +173,7 @@ func (h TreasureChestImpl) handleDeleteTreasureChest() http.HandlerFunc {
err := h.s.Delete(r.Context(), user, id)
if err != nil {
handleError(w, r, err)
core.HandleError(w, r, err)
return
}
}

View File

@@ -13,7 +13,7 @@ var (
safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9ÄÖÜäöüß,&'". \-\?]+$`)
)
func validateString(value string, fieldName string) error {
func ValidateString(value string, fieldName string) error {
switch {
case value == "":
return fmt.Errorf("field \"%s\" needs to be set: %w", fieldName, ErrBadRequest)

View File

@@ -1,8 +0,0 @@
package service
import "errors"
var (
ErrBadRequest = errors.New("bad request")
ErrUnauthorized = errors.New("unauthorized")
)

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log/slog"
"spend-sparrow/internal/authentication"
"spend-sparrow/internal/db"
"spend-sparrow/internal/types"
"strconv"
@@ -17,13 +18,13 @@ import (
const page_size = 25
type Transaction interface {
Add(ctx context.Context, tx *sqlx.Tx, user *types.User, transaction types.Transaction) (*types.Transaction, error)
Update(ctx context.Context, user *types.User, transaction types.Transaction) (*types.Transaction, error)
Get(ctx context.Context, user *types.User, id string) (*types.Transaction, error)
GetAll(ctx context.Context, user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error)
Delete(ctx context.Context, user *types.User, id string) error
Add(ctx context.Context, tx *sqlx.Tx, user *authentication.User, transaction types.Transaction) (*types.Transaction, error)
Update(ctx context.Context, user *authentication.User, transaction types.Transaction) (*types.Transaction, error)
Get(ctx context.Context, user *authentication.User, id string) (*types.Transaction, error)
GetAll(ctx context.Context, user *authentication.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error)
Delete(ctx context.Context, user *authentication.User, id string) error
RecalculateBalances(ctx context.Context, user *types.User) error
RecalculateBalances(ctx context.Context, user *authentication.User) error
}
type TransactionImpl struct {
@@ -499,13 +500,13 @@ func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *s
}
if input.Party != "" {
err = validateString(input.Party, "party")
err = ValidateString(input.Party, "party")
if err != nil {
return nil, err
}
}
if input.Description != "" {
err = validateString(input.Description, "description")
err = ValidateString(input.Description, "description")
if err != nil {
return nil, err
}

View File

@@ -471,13 +471,13 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
value := int64(math.Round(valueFloat * DECIMALS_MULTIPLIER))
if input.Party != "" {
err = validateString(input.Party, "party")
err = ValidateString(input.Party, "party")
if err != nil {
return nil, err
}
}
if input.Description != "" {
err = validateString(input.Description, "description")
err = ValidateString(input.Description, "description")
if err != nil {
return nil, err
}

View File

@@ -45,7 +45,7 @@ func (s TreasureChestImpl) Add(ctx context.Context, user *types.User, parentId,
return nil, types.ErrInternal
}
err = validateString(name, "name")
err = ValidateString(name, "name")
if err != nil {
return nil, err
}
@@ -92,7 +92,7 @@ func (s TreasureChestImpl) Update(ctx context.Context, user *types.User, idStr,
if user == nil {
return nil, ErrUnauthorized
}
err := validateString(name, "name")
err := ValidateString(name, "name")
if err != nil {
return nil, err
}

View File

@@ -1 +0,0 @@
package account

View File

@@ -1 +0,0 @@
package auth

View File

@@ -4,9 +4,10 @@ import "fmt"
import "time"
import "spend-sparrow/internal/template/svg"
import "spend-sparrow/internal/types"
import "spend-sparrow/internal/account"
import "github.com/google/uuid"
templ Transaction(items templ.Component, filter types.TransactionItemsFilter, accounts []*types.Account, treasureChests []*types.TreasureChest) {
templ Transaction(items templ.Component, filter types.TransactionItemsFilter, accounts []*account.Account, treasureChests []*types.TreasureChest) {
<div class="max-w-6xl mt-10 mx-auto">
<div class="flex items-center gap-4">
<form
@@ -95,7 +96,7 @@ templ TransactionItems(transactions []*types.Transaction, accounts, treasureChes
</div>
}
templ EditTransaction(transaction *types.Transaction, accounts []*types.Account, treasureChests []*types.TreasureChest) {
templ EditTransaction(transaction *types.Transaction, accounts []*account.Account, treasureChests []*types.TreasureChest) {
{{
var (
timestamp time.Time

View File

@@ -1,10 +0,0 @@
package types
import (
"errors"
)
var (
ErrInternal = errors.New("internal server error")
ErrUnauthorized = errors.New("you are not authorized to perform this action")
)

156
package-lock.json generated
View File

@@ -9,10 +9,10 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@tailwindcss/cli": "4.1.16",
"@tailwindcss/cli": "4.1.18",
"echarts": "6.0.0",
"htmx.org": "2.0.8",
"tailwindcss": "4.1.16"
"tailwindcss": "4.1.18"
}
},
"node_modules/@jridgewell/gen-mapping": {
@@ -375,28 +375,28 @@
}
},
"node_modules/@tailwindcss/cli": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.16.tgz",
"integrity": "sha512-dsnANPrh2ZooHyZ/8uJhc9ecpcYtufToc21NY09NS9vF16rxPCjJ8dP7TUAtPqlUJTHSmRkN2hCdoYQIlgh4fw==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.18.tgz",
"integrity": "sha512-sMZ+lZbDyxwjD2E0L7oRUjJ01Ffjtme5OtjvvnC+cV4CEDcbqzbp25TCpxHj6kWLU9+DlqJOiNgSOgctC2aZmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@parcel/watcher": "^2.5.1",
"@tailwindcss/node": "4.1.16",
"@tailwindcss/oxide": "4.1.16",
"@tailwindcss/node": "4.1.18",
"@tailwindcss/oxide": "4.1.18",
"enhanced-resolve": "^5.18.3",
"mri": "^1.2.0",
"picocolors": "^1.1.1",
"tailwindcss": "4.1.16"
"tailwindcss": "4.1.18"
},
"bin": {
"tailwindcss": "dist/index.mjs"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
"integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -404,39 +404,39 @@
"enhanced-resolve": "^5.18.3",
"jiti": "^2.6.1",
"lightningcss": "1.30.2",
"magic-string": "^0.30.19",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.16"
"tailwindcss": "4.1.18"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz",
"integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.16",
"@tailwindcss/oxide-darwin-arm64": "4.1.16",
"@tailwindcss/oxide-darwin-x64": "4.1.16",
"@tailwindcss/oxide-freebsd-x64": "4.1.16",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.16",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.16",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.16",
"@tailwindcss/oxide-linux-x64-musl": "4.1.16",
"@tailwindcss/oxide-wasm32-wasi": "4.1.16",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.16",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.16"
"@tailwindcss/oxide-android-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-x64": "4.1.18",
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz",
"integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
"cpu": [
"arm64"
],
@@ -451,9 +451,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz",
"integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
"cpu": [
"arm64"
],
@@ -468,9 +468,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz",
"integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
"cpu": [
"x64"
],
@@ -485,9 +485,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz",
"integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
"cpu": [
"x64"
],
@@ -502,9 +502,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz",
"integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
"cpu": [
"arm"
],
@@ -519,9 +519,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz",
"integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
"cpu": [
"arm64"
],
@@ -536,9 +536,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz",
"integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
"cpu": [
"arm64"
],
@@ -553,9 +553,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz",
"integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
"cpu": [
"x64"
],
@@ -570,9 +570,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz",
"integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
"cpu": [
"x64"
],
@@ -587,9 +587,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz",
"integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -605,10 +605,10 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.5.0",
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.0.7",
"@napi-rs/wasm-runtime": "^1.1.0",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.4.0"
},
@@ -617,7 +617,7 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.5.0",
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -628,7 +628,7 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.5.0",
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -648,14 +648,14 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.7",
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.5.0",
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
}
},
@@ -677,9 +677,9 @@
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz",
"integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
"cpu": [
"arm64"
],
@@ -694,9 +694,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz",
"integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
"cpu": [
"x64"
],
@@ -1103,9 +1103,9 @@
}
},
"node_modules/magic-string": {
"version": "0.30.19",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1174,9 +1174,9 @@
}
},
"node_modules/tailwindcss": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
"integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"dev": true,
"license": "MIT"
},

View File

@@ -11,9 +11,9 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@tailwindcss/cli": "4.1.16",
"@tailwindcss/cli": "4.1.18",
"htmx.org": "2.0.8",
"tailwindcss": "4.1.16",
"tailwindcss": "4.1.18",
"echarts": "6.0.0"
}
}