From 82cff524b6b97dedcbf02293a10a3fbb28463f7d Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 14:08:14 -0300
Subject: [PATCH 01/91] feature(#6): add dependencies
---
backend/go.mod | 137 ++++++++++++++++-
backend/go.sum | 396 +++++++++++++++++++++++++++++++++++++++++++++++++
flake.nix | 3 +
3 files changed, 535 insertions(+), 1 deletion(-)
diff --git a/backend/go.mod b/backend/go.mod
index 8490843..cef87b0 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -1,3 +1,138 @@
module github.com/taldoflemis/brain.test
-go 1.21.5
+go 1.22.1
+
+require (
+ github.com/gavv/httpexpect/v2 v2.16.0
+ github.com/go-playground/validator/v10 v10.19.0
+ github.com/gofiber/contrib/fiberzap/v2 v2.1.2
+ github.com/gofiber/contrib/jwt v1.0.8
+ github.com/gofiber/fiber/v2 v2.52.2
+ github.com/gofiber/swagger v1.0.0
+ github.com/golang-jwt/jwt/v5 v5.2.1
+ github.com/golang-migrate/migrate/v4 v4.17.0
+ github.com/google/uuid v1.6.0
+ github.com/jackc/pgx/v5 v5.5.5
+ github.com/knadh/koanf/parsers/json v0.1.0
+ github.com/knadh/koanf/providers/file v0.1.0
+ github.com/knadh/koanf/v2 v2.1.0
+ github.com/stretchr/testify v1.9.0
+ github.com/swaggo/swag v1.16.3
+ github.com/testcontainers/testcontainers-go v0.29.1
+ github.com/testcontainers/testcontainers-go/modules/postgres v0.29.1
+ github.com/vgarvardt/pgx-google-uuid/v5 v5.0.0
+ go.uber.org/zap v1.27.0
+ golang.org/x/crypto v0.21.0
+)
+
+require (
+ dario.cat/mergo v1.0.0 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
+ github.com/KyleBanks/depth v1.2.1 // indirect
+ github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
+ github.com/Microsoft/go-winio v0.6.1 // indirect
+ github.com/Microsoft/hcsshim v0.11.4 // indirect
+ github.com/PuerkitoBio/purell v1.1.1 // indirect
+ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
+ github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
+ github.com/ajg/form v1.5.1 // indirect
+ github.com/andybalholm/brotli v1.0.5 // indirect
+ github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+ github.com/containerd/containerd v1.7.12 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/cpuguy83/dockercfg v0.3.1 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/distribution/reference v0.5.0 // indirect
+ github.com/docker/docker v25.0.5+incompatible // indirect
+ github.com/docker/go-connections v0.5.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/fatih/color v1.15.0 // indirect
+ github.com/fatih/structs v1.1.0 // indirect
+ github.com/felixge/httpsnoop v1.0.3 // indirect
+ github.com/fsnotify/fsnotify v1.6.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/go-logr/logr v1.2.4 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/go-openapi/jsonpointer v0.19.5 // indirect
+ github.com/go-openapi/jsonreference v0.19.6 // indirect
+ github.com/go-openapi/spec v0.20.4 // indirect
+ github.com/go-openapi/swag v0.19.15 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
+ github.com/gobwas/glob v0.2.3 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/go-querystring v1.1.0 // indirect
+ github.com/gorilla/websocket v1.4.2 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/imkira/go-interpol v1.1.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+ github.com/jackc/puddle/v2 v2.2.1 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/klauspost/compress v1.17.0 // indirect
+ github.com/knadh/koanf/maps v0.1.1 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/lib/pq v1.10.9 // indirect
+ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
+ github.com/magiconair/properties v1.8.7 // indirect
+ github.com/mailru/easyjson v0.7.6 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/mitchellh/copystructure v1.2.0 // indirect
+ github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+ github.com/mitchellh/reflectwalk v1.0.2 // indirect
+ github.com/moby/patternmatcher v0.6.0 // indirect
+ github.com/moby/sys/sequential v0.5.0 // indirect
+ github.com/moby/sys/user v0.1.0 // indirect
+ github.com/moby/term v0.5.0 // indirect
+ github.com/morikuni/aec v1.0.0 // indirect
+ github.com/nxadm/tail v1.4.11 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+ github.com/rivo/uniseg v0.2.0 // indirect
+ github.com/sanity-io/litter v1.5.5 // indirect
+ github.com/sergi/go-diff v1.0.0 // indirect
+ github.com/shirou/gopsutil/v3 v3.23.12 // indirect
+ github.com/shoenig/go-m1cpu v0.1.6 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/swaggo/files/v2 v2.0.0 // indirect
+ github.com/tklauser/go-sysconf v0.3.12 // indirect
+ github.com/tklauser/numcpus v0.6.1 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasthttp v1.51.0 // indirect
+ github.com/valyala/tcplisten v1.0.0 // indirect
+ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
+ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
+ github.com/xeipuuv/gojsonschema v1.2.0 // indirect
+ github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect
+ github.com/yudai/gojsondiff v1.0.0 // indirect
+ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
+ github.com/yusufpapurcu/wmi v1.2.3 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
+ go.opentelemetry.io/otel v1.19.0 // indirect
+ go.opentelemetry.io/otel/metric v1.19.0 // indirect
+ go.opentelemetry.io/otel/trace v1.19.0 // indirect
+ go.uber.org/atomic v1.7.0 // indirect
+ go.uber.org/multierr v1.10.0 // indirect
+ golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect
+ golang.org/x/mod v0.16.0 // indirect
+ golang.org/x/net v0.21.0 // indirect
+ golang.org/x/sync v0.5.0 // indirect
+ golang.org/x/sys v0.18.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/tools v0.13.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
+ google.golang.org/grpc v1.59.0 // indirect
+ google.golang.org/protobuf v1.31.0 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ moul.io/http2curl/v2 v2.3.0 // indirect
+)
diff --git a/backend/go.sum b/backend/go.sum
index e69de29..5dc3120 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -0,0 +1,396 @@
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
+github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
+github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
+github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
+github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
+github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
+github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
+github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
+github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0=
+github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
+github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/dhui/dktest v0.4.0 h1:z05UmuXZHO/bgj/ds2bGMBu8FI4WA+Ag/m3ghL+om7M=
+github.com/dhui/dktest v0.4.0/go.mod h1:v/Dbz1LgCBOi2Uki2nUqLBGa83hWBGFMu5MrgMDCc78=
+github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
+github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
+github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
+github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
+github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
+github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
+github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gavv/httpexpect/v2 v2.16.0 h1:Ty2favARiTYTOkCRZGX7ojXXjGyNAIohM1lZ3vqaEwI=
+github.com/gavv/httpexpect/v2 v2.16.0/go.mod h1:uJLaO+hQ25ukBJtQi750PsztObHybNllN+t+MbbW8PY=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
+github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
+github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
+github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
+github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
+github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gofiber/contrib/fiberzap/v2 v2.1.2 h1:7Z1BqS1sYK9e9jTwqPcWx9qQt46PI8oeswgAp6YNZC4=
+github.com/gofiber/contrib/fiberzap/v2 v2.1.2/go.mod h1:ulCCQOdDYABGsOQfbndASmCsCN86hsC96iKoOTNYfy8=
+github.com/gofiber/contrib/jwt v1.0.8 h1:/GeOsm/Mr1OGr0GTy+RIVSz5VgNNyP3ZgK4wdqxF/WY=
+github.com/gofiber/contrib/jwt v1.0.8/go.mod h1:gWWBtBiLmKXRN7xy6a96QO0KGvPEyxdh8x496Ujtg84=
+github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo=
+github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
+github.com/gofiber/swagger v1.0.0 h1:BzUzDS9ZT6fDUa692kxmfOjc1DZiloLiPK/W5z1H1tc=
+github.com/gofiber/swagger v1.0.0/go.mod h1:QrYNF1Yrc7ggGK6ATsJ6yfH/8Zi5bu9lA7wB8TmCecg=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU=
+github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
+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/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
+github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
+github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
+github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
+github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
+github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
+github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
+github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU=
+github.com/knadh/koanf/parsers/json v0.1.0/go.mod h1:ll2/MlXcZ2BfXD6YJcjVFzhG9P0TdJ207aIBKQhV2hY=
+github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
+github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
+github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8=
+github.com/knadh/koanf/v2 v2.1.0/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
+github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
+github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
+github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
+github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
+github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
+github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
+github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU=
+github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
+github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
+github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
+github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
+github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
+github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
+github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
+github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
+github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
+github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
+github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
+github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
+github.com/testcontainers/testcontainers-go v0.29.1 h1:z8kxdFlovA2y97RWx98v/TQ+tR+SXZm6p35M+xB92zk=
+github.com/testcontainers/testcontainers-go v0.29.1/go.mod h1:SnKnKQav8UcgtKqjp/AD8bE1MqZm+3TDb/B8crE3XnI=
+github.com/testcontainers/testcontainers-go/modules/postgres v0.29.1 h1:hTn3MzhR9w4btwfzr/NborGCaeNZG0MPBpufeDj10KA=
+github.com/testcontainers/testcontainers-go/modules/postgres v0.29.1/go.mod h1:YsWyy+pHDgvGdi0axGOx6CGXWsE6eqSaApyd1FYYSSc=
+github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
+github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
+github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
+github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
+github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
+github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+github.com/vgarvardt/pgx-google-uuid/v5 v5.0.0 h1:kIIQmW04MYKyRE2ZwREPl1NY4/Uxf5x48ABTQ+yFdFo=
+github.com/vgarvardt/pgx-google-uuid/v5 v5.0.0/go.mod h1:fskJeXpJTJCU9JvsZQRgR4OhKKpciztvx4rdXWil7E0=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
+github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
+github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
+github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
+github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
+github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
+github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q=
+go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
+go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
+go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
+go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
+go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
+go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
+go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
+go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
+go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
+go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
+golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA=
+google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k=
+google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 h1:AB/lmRny7e2pLhFEYIbl5qkDAUt2h0ZRO4wGPhZf+ik=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE=
+google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
+google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
+gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
+moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs=
+moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE=
diff --git a/flake.nix b/flake.nix
index 8600145..d258646 100644
--- a/flake.nix
+++ b/flake.nix
@@ -21,6 +21,9 @@
go
go-task
air
+ govulncheck
+ gotestsum
+ go-swag
];
};
});
From a129e5d4923f50fbeff37530a6e42487631836db Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 14:08:28 -0300
Subject: [PATCH 02/91] improvement(#6): update gitignore to remove
autogenerated code
---
backend/.gitignore | 3 +++
1 file changed, 3 insertions(+)
diff --git a/backend/.gitignore b/backend/.gitignore
index 2fdf604..697ca9a 100644
--- a/backend/.gitignore
+++ b/backend/.gitignore
@@ -29,3 +29,6 @@ tmp/
main
*_gen.go
/cmd/api/gahoot
+coverage.html
+docs/*
+!docs/docs.go
From 21cef13c859cbf73b19fed2ab830ccebdb6df275 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 14:09:07 -0300
Subject: [PATCH 03/91] feature(#6): add test helpers
add container helper
add logger helper
---
backend/test/helpers/containers.go | 46 ++++++++++++++++++++++++++++++
backend/test/helpers/logger.go | 42 +++++++++++++++++++++++++++
2 files changed, 88 insertions(+)
create mode 100644 backend/test/helpers/containers.go
create mode 100644 backend/test/helpers/logger.go
diff --git a/backend/test/helpers/containers.go b/backend/test/helpers/containers.go
new file mode 100644
index 0000000..f20c973
--- /dev/null
+++ b/backend/test/helpers/containers.go
@@ -0,0 +1,46 @@
+package testshelpers
+
+import (
+ "context"
+ "time"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/modules/postgres"
+ "github.com/testcontainers/testcontainers-go/wait"
+)
+
+const (
+ databaseName = "test-db"
+ username = "postgres"
+ password = "postgres"
+)
+
+type PostgresContainer struct {
+ *postgres.PostgresContainer
+ ConnStr string
+}
+
+func CreatePostgresContainer(ctx context.Context) (*PostgresContainer, error) {
+ pgContainer, err := postgres.RunContainer(ctx,
+ testcontainers.WithImage("postgres:16.2-alpine"),
+ postgres.WithDatabase(databaseName),
+ postgres.WithUsername(username),
+ postgres.WithPassword(password),
+ testcontainers.WithWaitStrategy(
+ wait.ForLog("database system is ready to accept connections").
+ WithOccurrence(2).WithStartupTimeout(5*time.Second)),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
+ if err != nil {
+ return nil, err
+ }
+
+ return &PostgresContainer{
+ PostgresContainer: pgContainer,
+ ConnStr: connStr,
+ }, nil
+}
diff --git a/backend/test/helpers/logger.go b/backend/test/helpers/logger.go
new file mode 100644
index 0000000..939bdf9
--- /dev/null
+++ b/backend/test/helpers/logger.go
@@ -0,0 +1,42 @@
+package testshelpers
+
+import (
+ "io"
+ "log"
+)
+
+type DummyLogger struct {
+ w io.Writer
+}
+
+func NewDummyLogger(w io.Writer) *DummyLogger {
+ log.SetOutput(w)
+ return &DummyLogger{
+ w: w,
+ }
+}
+
+func (d *DummyLogger) Info(msg string, keysAndValues ...interface{}) {
+ log.Printf(msg, keysAndValues...)
+}
+func (DummyLogger) Error(msg string, keysAndValues ...interface{}) {
+ log.Printf(msg, keysAndValues...)
+}
+func (DummyLogger) Warn(msg string, keysAndValues ...interface{}) {
+ log.Printf(msg, keysAndValues...)
+}
+func (DummyLogger) Debug(msg string, keysAndValues ...interface{}) {
+ log.Printf(msg, keysAndValues...)
+}
+func (DummyLogger) Warnf(msg string, args ...interface{}) {
+ log.Printf(msg, args...)
+}
+func (DummyLogger) Errorf(msg string, args ...interface{}) {
+ log.Printf(msg, args...)
+}
+func (DummyLogger) Infof(msg string, args ...interface{}) {
+ log.Printf(msg, args...)
+}
+func (DummyLogger) Debugf(msg string, args ...interface{}) {
+ log.Printf(msg, args...)
+}
From efb72963ba1377d97a5eaa8355b07375478ed050 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 14:10:06 -0300
Subject: [PATCH 04/91] feature(#6): add config management
---
backend/.air.toml | 44 +++++++++++++++++++
backend/Taskfile.yaml | 44 ++++++++++++++++++-
backend/config.json | 26 +++++++++++
backend/config/fiber.go | 28 ++++++++++++
backend/config/koanf.go | 24 ++++++++++
backend/config/local_idp.go | 27 ++++++++++++
backend/config/postgres.go | 20 +++++++++
.../internal/adapters/drivers/web/config.go | 10 +++++
8 files changed, 222 insertions(+), 1 deletion(-)
create mode 100644 backend/.air.toml
create mode 100644 backend/config.json
create mode 100644 backend/config/fiber.go
create mode 100644 backend/config/koanf.go
create mode 100644 backend/config/local_idp.go
create mode 100644 backend/config/postgres.go
create mode 100644 backend/internal/adapters/drivers/web/config.go
diff --git a/backend/.air.toml b/backend/.air.toml
new file mode 100644
index 0000000..b6d1d42
--- /dev/null
+++ b/backend/.air.toml
@@ -0,0 +1,44 @@
+root = "."
+testdata_dir = "testdata"
+tmp_dir = "tmp"
+
+[build]
+ args_bin = []
+ full_bin = "go run ./cmd/web/main.go"
+ delay = 1000
+ exclude_dir = ["assets", "tmp", "vendor", "testdata", "test"]
+ exclude_file = []
+ exclude_regex = ["_test.go", "_gen.go", "_templ.go"]
+ exclude_unchanged = false
+ follow_symlink = false
+ include_dir = ["internal", "pkg", "cmd", "config", "docs"]
+ include_ext = ["go", "tpl", "tmpl", "html", "toml"]
+ include_file = ["config.json"]
+ kill_delay = "0s"
+ log = "build-errors.log"
+ poll = false
+ poll_interval = 0
+ post_cmd = []
+ pre_cmd = []
+ rerun = false
+ rerun_delay = 500
+ send_interrupt = true
+ stop_on_error = false
+
+[color]
+ app = ""
+ build = "yellow"
+ main = "magenta"
+ runner = "green"
+ watcher = "cyan"
+
+[log]
+ main_only = false
+ time = false
+
+[misc]
+ clean_on_exit = false
+
+[screen]
+ clear_on_rebuild = false
+ keep_scroll = true
diff --git a/backend/Taskfile.yaml b/backend/Taskfile.yaml
index c9482c7..6511ca7 100644
--- a/backend/Taskfile.yaml
+++ b/backend/Taskfile.yaml
@@ -11,7 +11,49 @@ tasks:
cmds:
- go run ./cmd/web -o ./cmd/web/brain.test
+ format:
+ desc: "Format code"
+ cmds:
+ - go fmt ./...
+
+ check-format:
+ desc: "Check if code is formated"
+ cmds:
+ - test -z "$(gofmt -l .)"
+
test-light:
desc: "Run lighter tests"
cmds:
- - go test -v ./... -short
+ - gotestsum -f pkgname -- -short ./...
+
+ test-all:
+ desc: "Run entire tests"
+ cmds:
+ - gotestsum -f pkgname
+
+ watch-tests:
+ desc: "Watch tests"
+ cmds:
+ - gotestsum --watch -f pkgname
+
+ flag-heavy-tests:
+ desc: "Flag a Heavy test with short so that we can skip if necessary"
+ cmds:
+ - go test -json -short ./... | gotestsum tool slowest --skip-stmt "testing.Short" --threshold 200ms
+
+ coverage:
+ desc: "Test coverage output to coverage.html"
+ cmds:
+ - gotestsum -f pkgname -- -coverprofile=c.out ./...
+ - go tool cover -html=c.out -o coverage.html
+
+ generate-docs:
+ desc: "Generate swagger docs"
+ cmds:
+ - swag fmt
+ - swag init -d ./cmd/web,internal/adapters/drivers/web -g main.go
+
+ check-vuln:
+ desc: "Check for code common vulnerabilities"
+ cmds:
+ - govulncheck ./...
diff --git a/backend/config.json b/backend/config.json
new file mode 100644
index 0000000..a2c8e97
--- /dev/null
+++ b/backend/config.json
@@ -0,0 +1,26 @@
+{
+ "fiber": {
+ "prefix": "",
+ "listen_ip": "0.0.0.0",
+ "port": 42069,
+ "cors": {
+ "allow_origins": "*",
+ "allow_methods": "GET,POST,HEAD,PUT,DELETE,PATCH",
+ "allow_headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization"
+ }
+ },
+ "auth": {
+ "ed25519_seed": "SzceVsT4GdFOlrZn60XMgrFcvMNUMuuJ",
+ "issuer": "brain.test",
+ "audience": "deeznuts",
+ "access_time_in_minutes": 10,
+ "refresh_time_in_hours": 24
+ },
+ "postgres": {
+ "host": "localhost",
+ "port": 5432,
+ "user": "postgres",
+ "password": "password",
+ "database": "postgres"
+ }
+}
diff --git a/backend/config/fiber.go b/backend/config/fiber.go
new file mode 100644
index 0000000..6762bda
--- /dev/null
+++ b/backend/config/fiber.go
@@ -0,0 +1,28 @@
+package config
+
+import "github.com/taldoflemis/brain.test/internal/adapters/drivers/web"
+
+type fiberConfig struct {
+ Prefix string `koanf:"prefix"`
+ ListenIP string `koanf:"listen_ip"`
+ Port int `koanf:"port"`
+ CORSAllowOrigins string `koanf:"cors.allow_origins"`
+ CORSAllowHeaders string `koanf:"cors.allow_headers"`
+ CORSAllowMethods string `koanf:"cors.allow_methods"`
+}
+
+func NewFiberConfig() (*web.Config, error) {
+ var out fiberConfig
+ err := k.Unmarshal("fiber", &out)
+ if err != nil {
+ return nil, err
+ }
+ return &web.Config{
+ Prefix: k.String("fiber.prefix"),
+ ListenIP: k.String("fiber.listen_ip"),
+ Port: k.Int("fiber.port"),
+ CORSAllowOrigins: k.String("fiber.cors.allow_origins"),
+ CORSAllowHeaders: k.String("fiber.cors.allow_headers"),
+ CORSAllowMethods: k.String("fiber.cors.allow_methods"),
+ }, nil
+}
diff --git a/backend/config/koanf.go b/backend/config/koanf.go
new file mode 100644
index 0000000..7a743e6
--- /dev/null
+++ b/backend/config/koanf.go
@@ -0,0 +1,24 @@
+package config
+
+import (
+ "github.com/knadh/koanf/parsers/json"
+ "github.com/knadh/koanf/providers/file"
+ "github.com/knadh/koanf/v2"
+)
+
+var k = koanf.New(".")
+
+type Koanfson struct {
+}
+
+func NewKoanfson() *Koanfson {
+ return &Koanfson{}
+}
+
+func (kson *Koanfson) LoadFromJSON(path string) error {
+ parser := json.Parser()
+ if err := k.Load(file.Provider("config.json"), parser); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/backend/config/local_idp.go b/backend/config/local_idp.go
new file mode 100644
index 0000000..a092756
--- /dev/null
+++ b/backend/config/local_idp.go
@@ -0,0 +1,27 @@
+package config
+
+import "github.com/taldoflemis/brain.test/internal/adapters/driven/auth"
+
+type localIdpConfig struct {
+ Ed25519Seed string `koanf:"ed25519_seed"`
+ Issuer string `koanf:"issuer"`
+ Audience string `koanf:"audience"`
+ AccessTimeInMinutes int `koanf:"access_time_in_minutes"`
+ RefreshtimeInHours int `koanf:"refresh_time_in_hours"`
+}
+
+func NewLocalIDPConfig() (*auth.LocalIdpConfig, error) {
+ var out localIdpConfig
+ err := k.Unmarshal("auth", &out)
+ if err != nil {
+ return nil, err
+ }
+ cfg := auth.NewLocalIdpConfig(
+ out.Ed25519Seed,
+ out.Issuer,
+ out.Audience,
+ out.AccessTimeInMinutes,
+ out.RefreshtimeInHours,
+ )
+ return cfg, nil
+}
diff --git a/backend/config/postgres.go b/backend/config/postgres.go
new file mode 100644
index 0000000..404d9b4
--- /dev/null
+++ b/backend/config/postgres.go
@@ -0,0 +1,20 @@
+package config
+
+import "github.com/taldoflemis/brain.test/internal/adapters/driven/postgres"
+
+type postgresConfig struct {
+ Host string `koanf:"host"`
+ Port int `koanf:"port"`
+ User string `koanf:"user"`
+ Password string `koanf:"password"`
+ Database string `koanf:"database"`
+}
+
+func NewPostgresConfig() (*postgres.Config, error) {
+ var out postgresConfig
+ err := k.Unmarshal("postgres", &out)
+ if err != nil {
+ return nil, err
+ }
+ return postgres.NewConfig(out.User, out.Password, out.Host, out.Database, out.Port), nil
+}
diff --git a/backend/internal/adapters/drivers/web/config.go b/backend/internal/adapters/drivers/web/config.go
new file mode 100644
index 0000000..76f05d7
--- /dev/null
+++ b/backend/internal/adapters/drivers/web/config.go
@@ -0,0 +1,10 @@
+package web
+
+type Config struct {
+ Prefix string
+ ListenIP string
+ Port int
+ CORSAllowOrigins string
+ CORSAllowHeaders string
+ CORSAllowMethods string
+}
From 45b04ce63fb741d31b3eb6dc8b43c7653556e669 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 14:10:25 -0300
Subject: [PATCH 05/91] feature(#6): add auth and logger ports
---
backend/internal/ports/auth.go | 62 ++++++++++++++++++++++++++++++++
backend/internal/ports/logger.go | 13 +++++++
2 files changed, 75 insertions(+)
create mode 100644 backend/internal/ports/auth.go
create mode 100644 backend/internal/ports/logger.go
diff --git a/backend/internal/ports/auth.go b/backend/internal/ports/auth.go
new file mode 100644
index 0000000..b227fc3
--- /dev/null
+++ b/backend/internal/ports/auth.go
@@ -0,0 +1,62 @@
+package ports
+
+import (
+ "context"
+ "errors"
+ "time"
+)
+
+var (
+ ErrFailedToSignToken = errors.New("token sign failed")
+ ErrUserNotFound = errors.New("user not found")
+ ErrInvalidPassword = errors.New("invalid password")
+ ErrExpiredToken = errors.New("token expired")
+ ErrInvalidRefreshToken = errors.New("invalid refresh token")
+ ErrUserAlreadyExists = errors.New("user already exists")
+)
+
+type TokenResponse struct {
+ AccessToken string
+ RefreshToken string
+ ExpiresAt time.Time
+}
+
+type UserIdentityInfo struct {
+ ID string
+ Username string
+ Email string
+}
+
+type AuthenticationManager interface {
+ CreateUser(ctx context.Context, username, email, password string) (*UserIdentityInfo, error)
+ AuthenticateUser(ctx context.Context, username, password string) (*UserIdentityInfo, error)
+ DeleteUser(ctx context.Context, userId string) error
+ UpdateUser(
+ ctx context.Context,
+ userId, username, password, email string,
+ ) (*UserIdentityInfo, error)
+ CreateToken(ctx context.Context, userId string) (*TokenResponse, error)
+ RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error)
+ GetPublicKey() interface{}
+ GetAlgorithm() string
+}
+
+type LocalIDPUserEntity struct {
+ ID string
+ Username string
+ Email string
+ HashedPassword string
+}
+
+type LocalIDPStorer interface {
+ StoreUser(
+ ctx context.Context,
+ username, email, password string,
+ ) (*LocalIDPUserEntity, error)
+ UpdateUser(
+ ctx context.Context,
+ userId, username, password, email string,
+ ) (*LocalIDPUserEntity, error)
+ DeleteUser(ctx context.Context, userId string) error
+ FindUserByUsername(ctx context.Context, username string) (*LocalIDPUserEntity, error)
+}
diff --git a/backend/internal/ports/logger.go b/backend/internal/ports/logger.go
new file mode 100644
index 0000000..798743e
--- /dev/null
+++ b/backend/internal/ports/logger.go
@@ -0,0 +1,13 @@
+package ports
+
+type Logger interface {
+ Info(msg string, keysAndValues ...interface{})
+ Error(msg string, keysAndValues ...interface{})
+ Warn(msg string, keysAndValues ...interface{})
+ Debug(msg string, keysAndValues ...interface{})
+
+ Warnf(msg string, args ...interface{})
+ Errorf(msg string, args ...interface{})
+ Infof(msg string, args ...interface{})
+ Debugf(msg string, args ...interface{})
+}
From d303545d3ec2530dc67a757956570f10f0a7a25a Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 14:10:41 -0300
Subject: [PATCH 06/91] feature(#6): add postgres adapter for auth port
---
.../internal/adapters/driven/postgres/auth.go | 131 +++++++++++
.../adapters/driven/postgres/auth_test.go | 211 ++++++++++++++++++
.../adapters/driven/postgres/connection.go | 72 ++++++
.../adapters/driven/postgres/migrate.go | 34 +++
.../postgres/migrations/01_users.down.sql | 1 +
.../postgres/migrations/01_users.up.sql | 8 +
6 files changed, 457 insertions(+)
create mode 100644 backend/internal/adapters/driven/postgres/auth.go
create mode 100644 backend/internal/adapters/driven/postgres/auth_test.go
create mode 100644 backend/internal/adapters/driven/postgres/connection.go
create mode 100644 backend/internal/adapters/driven/postgres/migrate.go
create mode 100644 backend/internal/adapters/driven/postgres/migrations/01_users.down.sql
create mode 100644 backend/internal/adapters/driven/postgres/migrations/01_users.up.sql
diff --git a/backend/internal/adapters/driven/postgres/auth.go b/backend/internal/adapters/driven/postgres/auth.go
new file mode 100644
index 0000000..e8bab7f
--- /dev/null
+++ b/backend/internal/adapters/driven/postgres/auth.go
@@ -0,0 +1,131 @@
+package postgres
+
+import (
+ "context"
+ "errors"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
+ "github.com/jackc/pgx/v5/pgconn"
+ "github.com/jackc/pgx/v5/pgxpool"
+
+ "github.com/taldoflemis/brain.test/internal/ports"
+)
+
+type LocalIDPPostgresStorer struct {
+ pool *pgxpool.Pool
+}
+
+func NewLocalIDPPostgresStorer(pool *pgxpool.Pool) *LocalIDPPostgresStorer {
+ return &LocalIDPPostgresStorer{
+ pool: pool,
+ }
+}
+
+func (s *LocalIDPPostgresStorer) StoreUser(
+ ctx context.Context,
+ username string,
+ email string,
+ password string,
+) (*ports.LocalIDPUserEntity, error) {
+ id := uuid.New()
+ args := pgx.NamedArgs{
+ "id": id.String(),
+ "username": username,
+ "email": email,
+ "password": password,
+ }
+ insert := `INSERT INTO users (id, username, email, password) VALUES (@id, @username, @email, @password) RETURNING id`
+ _, err := s.pool.Exec(ctx, insert, args)
+ if err != nil {
+ var pgErr *pgconn.PgError
+ if errors.As(err, &pgErr) && pgErr.ConstraintName == "users_username" {
+ return nil, ports.ErrUserAlreadyExists
+ }
+ return nil, err
+ }
+
+ return &ports.LocalIDPUserEntity{
+ ID: id.String(),
+ Username: username,
+ HashedPassword: password,
+ Email: email,
+ }, nil
+}
+
+func (s *LocalIDPPostgresStorer) UpdateUser(
+ ctx context.Context,
+ userId string,
+ username string,
+ password string,
+ email string,
+) (*ports.LocalIDPUserEntity, error) {
+ args := pgx.NamedArgs{
+ "id": userId,
+ "username": username,
+ "email": email,
+ "password": password,
+ }
+ updt := `UPDATE users SET username = @username, email = @email, password = @password WHERE id = @id RETURNING id`
+
+ t, err := s.pool.Exec(ctx, updt, args)
+ if err != nil {
+ var pgErr *pgconn.PgError
+ if errors.As(err, &pgErr) && pgErr.ConstraintName == "users_username" {
+ return nil, ports.ErrUserAlreadyExists
+ }
+ return nil, err
+ }
+
+ if t.RowsAffected() == 0 {
+ return nil, ports.ErrUserNotFound
+ }
+
+ return &ports.LocalIDPUserEntity{
+ ID: userId,
+ Username: username,
+ Email: email,
+ HashedPassword: password,
+ }, nil
+}
+
+func (s *LocalIDPPostgresStorer) DeleteUser(ctx context.Context, userId string) error {
+ args := pgx.NamedArgs{
+ "id": userId,
+ }
+
+ del := `DELETE FROM users WHERE id = @id`
+ tag, err := s.pool.Exec(ctx, del, args)
+ if err != nil {
+ return err
+ }
+
+ if tag.RowsAffected() == 0 {
+ return ports.ErrUserNotFound
+ }
+ return nil
+}
+
+func (s *LocalIDPPostgresStorer) FindUserByUsername(
+ ctx context.Context,
+ username string,
+) (*ports.LocalIDPUserEntity, error) {
+ args := pgx.NamedArgs{
+ "username": username,
+ }
+
+ query := `SELECT id, username, email, password FROM users WHERE username = @username`
+
+ var user ports.LocalIDPUserEntity
+
+ err := s.pool.QueryRow(ctx, query, args).
+ Scan(&user.ID, &user.Username, &user.Email, &user.HashedPassword)
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return nil, ports.ErrUserNotFound
+ }
+ return nil, err
+ }
+
+ return &user, nil
+}
diff --git a/backend/internal/adapters/driven/postgres/auth_test.go b/backend/internal/adapters/driven/postgres/auth_test.go
new file mode 100644
index 0000000..eed655d
--- /dev/null
+++ b/backend/internal/adapters/driven/postgres/auth_test.go
@@ -0,0 +1,211 @@
+package postgres
+
+import (
+ "context"
+ "log"
+ "testing"
+
+ "github.com/jackc/pgx/v5/pgxpool"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/suite"
+
+ "github.com/taldoflemis/brain.test/internal/ports"
+ "github.com/taldoflemis/brain.test/test/helpers"
+)
+
+var (
+ testUsername = "tubias"
+ testEmail = "tubias3@gmail.com"
+ testPassword = "hashedpassword"
+ testUserId = "f7396104-a636-4826-9d9f-b92ae90cea14"
+)
+
+type LocalIDPPostgresStorerTestSuite struct {
+ suite.Suite
+ pgContainer *testshelpers.PostgresContainer
+ ctx context.Context
+ repo *LocalIDPPostgresStorer
+ pool *pgxpool.Pool
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) SetupSuite() {
+ suite.ctx = context.Background()
+ pgContainer, err := testshelpers.CreatePostgresContainer(suite.ctx)
+ if err != nil {
+ log.Fatal(err)
+ }
+ suite.pgContainer = pgContainer
+ pool, err := NewPool(pgContainer.ConnStr)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ Migrate(pgContainer.ConnStr, "./migrations/")
+
+ repository := NewLocalIDPPostgresStorer(pool)
+
+ suite.repo = repository
+ suite.pool = pool
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) SetupTest() {
+ _, err := suite.pool.Exec(
+ suite.ctx,
+ `INSERT INTO users (id, username, email, password) VALUES ($1, $2, $3, $4)`,
+ testUserId, testUsername, testEmail, testPassword,
+ )
+ if err != nil {
+ log.Fatalf("error inserting user: %s", err)
+ }
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TearDownTest() {
+ _, err := suite.pool.Exec(suite.ctx, "TRUNCATE TABLE users")
+ if err != nil {
+ log.Fatalf("error truncating users table: %s", err)
+ }
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TearDownSuite() {
+ if err := suite.pgContainer.Terminate(suite.ctx); err != nil {
+ log.Fatalf("error terminating postgres container: %s", err)
+ }
+}
+
+func TestLocalIDPPostgresStorer(t *testing.T) {
+ if testing.Short() {
+ t.Skip("too slow for testing.Short")
+ }
+
+ suite.Run(t, new(LocalIDPPostgresStorerTestSuite))
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TestFindUserById() {
+ // Arrange
+ t := suite.T()
+
+ // Act
+ user, err := suite.repo.FindUserByUsername(suite.ctx, testUsername)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+ assert.Equal(t, testUsername, user.Username)
+ assert.Equal(t, testEmail, user.Email)
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TestTryToFindUserByIdThatDoesNotExist() {
+ // Arrange
+ t := suite.T()
+ random := "7ebc4755-b7cc-4963-a2b1-636949b035d6"
+
+ // Act
+ user, err := suite.repo.FindUserByUsername(suite.ctx, random)
+
+ // Assert
+ assert.Nil(t, user)
+ assert.ErrorIs(t, err, ports.ErrUserNotFound)
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TestDeleteUser() {
+ // Arrange
+ t := suite.T()
+
+ // Act
+ err := suite.repo.DeleteUser(suite.ctx, testUserId)
+
+ // Assert
+ assert.NoError(t, err)
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TestTryToDeleteUserThatDoesNotExist() {
+ // Arrange
+ t := suite.T()
+ randomId := "d0b8b515-f46b-4179-bb26-f7833ded8f8f"
+
+ // Act
+ err := suite.repo.DeleteUser(suite.ctx, randomId)
+
+ // Assert
+ assert.ErrorIs(t, err, ports.ErrUserNotFound)
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TestCreateUser() {
+ // Arrange
+ t := suite.T()
+ username := "tubias2"
+ email := "tubias2@gmail.com"
+ password := "hashedpassword"
+
+ // Act
+ user, err := suite.repo.StoreUser(suite.ctx, username, email, password)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+ assert.Equal(t, username, user.Username)
+ assert.Equal(t, email, user.Email)
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TestTryToCreateUserWithExistingUsername() {
+ // Arrange
+ t := suite.T()
+ password := "newpassword"
+
+ // Act
+ user, err := suite.repo.StoreUser(suite.ctx, testUsername, testEmail, password)
+
+ // Assert
+ assert.ErrorIs(t, err, ports.ErrUserAlreadyExists)
+ assert.Nil(t, user)
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TestUpdateUser() {
+ // Arrange
+ t := suite.T()
+ username := "newtubias"
+ email := "newtubias3@gmail.com"
+ password := "hashedpassword"
+
+ // Act
+ user, err := suite.repo.UpdateUser(suite.ctx, testUserId, username, password, email)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+ assert.Equal(t, testUserId, user.ID)
+ assert.Equal(t, username, user.Username)
+ assert.Equal(t, email, user.Email)
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TestUpdateUserWithExistingUsername() {
+ // Arrange
+ t := suite.T()
+ newPassword := "newpassword"
+ otherUsername := "otheruser"
+ _, err := suite.repo.StoreUser(suite.ctx, otherUsername, testEmail, testPassword)
+ assert.NoError(t, err)
+
+ // Act
+ user, err := suite.repo.UpdateUser(suite.ctx, testUserId, otherUsername, testEmail, newPassword)
+
+ // Assert
+ assert.ErrorIs(t, err, ports.ErrUserAlreadyExists)
+ assert.Nil(t, user)
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TestTryToUpdateUserThatDoesNotExist() {
+ // Arrange
+ t := suite.T()
+ username := "tubias"
+ email := "tubias@gmail.com"
+ password := "hashedpassword"
+ randomId := "d0b8b515-f46b-4179-bb26-f7833ded8f8f"
+
+ // Act
+ user, err := suite.repo.UpdateUser(suite.ctx, randomId, username, password, email)
+
+ // Assert
+ assert.ErrorIs(t, err, ports.ErrUserNotFound)
+ assert.Nil(t, user)
+}
diff --git a/backend/internal/adapters/driven/postgres/connection.go b/backend/internal/adapters/driven/postgres/connection.go
new file mode 100644
index 0000000..93894f4
--- /dev/null
+++ b/backend/internal/adapters/driven/postgres/connection.go
@@ -0,0 +1,72 @@
+package postgres
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/jackc/pgx/v5"
+ "github.com/jackc/pgx/v5/pgxpool"
+ pgxUUID "github.com/vgarvardt/pgx-google-uuid/v5"
+)
+
+type Config struct {
+ User string
+ Password string
+ Host string
+ Port int
+ Database string
+}
+
+func NewConfig(user, password, domain, database string, port int) *Config {
+ return &Config{
+ User: user,
+ Password: password,
+ Host: domain,
+ Port: port,
+ }
+}
+
+func NewConnection(connStr string) (*pgx.Conn, error) {
+ connConfig, err := pgx.ParseConfig(connStr)
+ if err != nil {
+ return nil, err
+ }
+
+ conn, err := pgx.ConnectConfig(context.Background(), connConfig)
+ if err != nil {
+ return nil, err
+ }
+ pgxUUID.Register(conn.TypeMap())
+ return conn, nil
+}
+
+func NewPool(connStr string) (*pgxpool.Pool, error) {
+ pgxConfig, err := pgxpool.ParseConfig(connStr)
+ if err != nil {
+ return nil, err
+ }
+
+ pgxConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
+ pgxUUID.Register(conn.TypeMap())
+ return nil
+ }
+
+ pool, err := pgxpool.NewWithConfig(context.TODO(), pgxConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return pool, nil
+}
+
+func GenerateConnectionString(cfg *Config) string {
+ str := fmt.Sprintf(
+ "postgres://%s:%s@%s:%d/%s&sslmode=disable",
+ cfg.User,
+ cfg.Password,
+ cfg.Host,
+ cfg.Port,
+ cfg.Database,
+ )
+ return str
+}
diff --git a/backend/internal/adapters/driven/postgres/migrate.go b/backend/internal/adapters/driven/postgres/migrate.go
new file mode 100644
index 0000000..374bb90
--- /dev/null
+++ b/backend/internal/adapters/driven/postgres/migrate.go
@@ -0,0 +1,34 @@
+package postgres
+
+import (
+ "database/sql"
+ "log"
+
+ "github.com/golang-migrate/migrate/v4"
+ "github.com/golang-migrate/migrate/v4/database/postgres"
+ _ "github.com/golang-migrate/migrate/v4/source/file"
+ _ "github.com/jackc/pgx/v5/stdlib"
+)
+
+func Migrate(connStr, path string) {
+ db, err := sql.Open("postgres", connStr)
+ if err != nil {
+ log.Fatal(err)
+ }
+ driver, err := postgres.WithInstance(db, &postgres.Config{})
+
+ if err != nil {
+ log.Fatal(err)
+ }
+ m, err := migrate.NewWithDatabaseInstance(
+ "file://"+path,
+ "postgres", driver)
+ if err != nil {
+ log.Fatal(err)
+ }
+ err = m.Up()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+}
diff --git a/backend/internal/adapters/driven/postgres/migrations/01_users.down.sql b/backend/internal/adapters/driven/postgres/migrations/01_users.down.sql
new file mode 100644
index 0000000..45e5ad1
--- /dev/null
+++ b/backend/internal/adapters/driven/postgres/migrations/01_users.down.sql
@@ -0,0 +1 @@
+DROP table users;
diff --git a/backend/internal/adapters/driven/postgres/migrations/01_users.up.sql b/backend/internal/adapters/driven/postgres/migrations/01_users.up.sql
new file mode 100644
index 0000000..ab4a70e
--- /dev/null
+++ b/backend/internal/adapters/driven/postgres/migrations/01_users.up.sql
@@ -0,0 +1,8 @@
+CREATE TABLE users(
+ id UUID PRIMARY KEY,
+ username TEXT NOT NULL,
+ email TEXT NOT NULL,
+ password TEXT NOT NULL
+);
+
+CREATE UNIQUE INDEX users_username ON users(username);
From 5ba1bf20865c26f6fee61ff060b0c8c1160f119f Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 14:10:51 -0300
Subject: [PATCH 07/91] feature(#6): add zap logger adapter for logger port
---
.../adapters/driven/misc/zap_logger.go | 61 +++++++++++++++++++
1 file changed, 61 insertions(+)
create mode 100644 backend/internal/adapters/driven/misc/zap_logger.go
diff --git a/backend/internal/adapters/driven/misc/zap_logger.go b/backend/internal/adapters/driven/misc/zap_logger.go
new file mode 100644
index 0000000..d31fb11
--- /dev/null
+++ b/backend/internal/adapters/driven/misc/zap_logger.go
@@ -0,0 +1,61 @@
+package misc
+
+import (
+ "go.uber.org/zap"
+)
+
+type ZapLogger struct {
+ logger *zap.SugaredLogger
+}
+
+func NewZapLogger(logger *zap.SugaredLogger) *ZapLogger {
+ return &ZapLogger{logger: logger}
+}
+
+func (z *ZapLogger) Info(msg string, keysAndValues ...interface{}) {
+ if keysAndValues == nil {
+ z.logger.Infow(msg)
+ } else {
+ z.logger.Infow(msg, keysAndValues...)
+ }
+}
+
+func (z *ZapLogger) Error(msg string, keysAndValues ...interface{}) {
+ if keysAndValues == nil {
+ z.logger.Errorf(msg)
+ } else {
+ z.logger.Errorf(msg, keysAndValues...)
+ }
+}
+
+func (z *ZapLogger) Warn(msg string, keysAndValues ...interface{}) {
+ if keysAndValues == nil {
+ z.logger.Warnf(msg)
+ } else {
+ z.logger.Warnf(msg, keysAndValues...)
+ }
+}
+
+func (z *ZapLogger) Debug(msg string, keysAndValues ...interface{}) {
+ if keysAndValues == nil {
+ z.logger.Debugf(msg)
+ } else {
+ z.logger.Debugf(msg, keysAndValues...)
+ }
+}
+
+func (z *ZapLogger) Warnf(msg string, args ...interface{}) {
+ z.logger.Warnf(msg, args...)
+}
+
+func (z *ZapLogger) Errorf(msg string, args ...interface{}) {
+ z.logger.Errorf(msg, args...)
+}
+
+func (z *ZapLogger) Infof(msg string, args ...interface{}) {
+ z.logger.Infof(msg, args...)
+}
+
+func (z *ZapLogger) Debugf(msg string, args ...interface{}) {
+ z.logger.Debugf(msg, args...)
+}
From 25c77284d7ea5c4a8af1a2385208db3f7b8d3d2e Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 14:11:24 -0300
Subject: [PATCH 08/91] feature(#6): add local_idp adapter for auth port
---
.../adapters/driven/auth/local_idp.go | 252 +++++++++++++++++
.../adapters/driven/auth/local_idp_test.go | 263 ++++++++++++++++++
2 files changed, 515 insertions(+)
create mode 100644 backend/internal/adapters/driven/auth/local_idp.go
create mode 100644 backend/internal/adapters/driven/auth/local_idp_test.go
diff --git a/backend/internal/adapters/driven/auth/local_idp.go b/backend/internal/adapters/driven/auth/local_idp.go
new file mode 100644
index 0000000..176801a
--- /dev/null
+++ b/backend/internal/adapters/driven/auth/local_idp.go
@@ -0,0 +1,252 @@
+package auth
+
+import (
+ "context"
+ "crypto"
+ "crypto/ed25519"
+ "errors"
+ "time"
+
+ "github.com/golang-jwt/jwt/v5"
+ "golang.org/x/crypto/bcrypt"
+
+ "github.com/taldoflemis/brain.test/internal/ports"
+)
+
+const (
+ defaultBCryptCost = 12
+)
+
+type LocalIdpConfig struct {
+ privateKey ed25519.PrivateKey
+ publicKey crypto.PublicKey
+ issuer string
+ audience string
+ accessTokenMaxAge time.Duration
+ refreshTokenMaxAge time.Duration
+}
+
+func NewLocalIdpConfig(
+ seed, issuer, audience string,
+ accessTimeInMin, refreshTimeInHours int,
+) *LocalIdpConfig {
+ privateKey := ed25519.NewKeyFromSeed([]byte(seed))
+
+ return &LocalIdpConfig{
+ privateKey: privateKey,
+ publicKey: privateKey.Public(),
+ issuer: issuer,
+ audience: audience,
+ accessTokenMaxAge: time.Duration(accessTimeInMin) * time.Minute,
+ refreshTokenMaxAge: time.Duration(refreshTimeInHours) * time.Hour,
+ }
+}
+
+type localIDP struct {
+ cfg LocalIdpConfig
+ logger ports.Logger
+ repo ports.LocalIDPStorer
+}
+
+func NewLocalIdp(cfg LocalIdpConfig, logger ports.Logger, repo ports.LocalIDPStorer) *localIDP {
+ return &localIDP{
+ cfg: cfg,
+ logger: logger,
+ repo: repo,
+ }
+}
+
+func (i *localIDP) CreateUser(
+ ctx context.Context,
+ username, email, password string,
+) (*ports.UserIdentityInfo, error) {
+ i.logger.Debug("Creating user", "username", username, "email", email)
+
+ hashedPassword, err := i.hashPassword(password)
+ if err != nil {
+ i.logger.Error("Failed to hash password", err)
+ return nil, err
+ }
+
+ user, err := i.repo.StoreUser(ctx, username, email, hashedPassword)
+ if err != nil {
+ i.logger.Error("Failed to store user", err)
+ return nil, err
+ }
+
+ i.logger.Info("User created", "username", username, "email", email)
+
+ return &ports.UserIdentityInfo{
+ ID: user.ID,
+ Email: user.Email,
+ Username: user.Username,
+ }, nil
+}
+
+func (i *localIDP) CreateToken(
+ ctx context.Context,
+ userId string,
+) (*ports.TokenResponse, error) {
+ i.logger.Debug("Creating token", "userId", userId)
+ accessToken, err := i.generateToken(ctx, userId, i.cfg.accessTokenMaxAge)
+ if err != nil {
+ i.logger.Error("Failed to sign access token", err)
+ return nil, ports.ErrFailedToSignToken
+ }
+
+ refreshToken, err := i.generateToken(ctx, userId, i.cfg.refreshTokenMaxAge)
+ if err != nil {
+ i.logger.Error("Failed to sign refresh token", err)
+ return nil, ports.ErrFailedToSignToken
+ }
+
+ i.logger.Info("Token created", "userId", userId)
+ return &ports.TokenResponse{
+ AccessToken: accessToken,
+ RefreshToken: refreshToken,
+ ExpiresAt: time.Now().Add(i.cfg.accessTokenMaxAge),
+ }, nil
+}
+
+func (i *localIDP) RefreshToken(
+ ctx context.Context,
+ refreshToken string,
+) (*ports.TokenResponse, error) {
+ i.logger.Debug("Refreshing token")
+ token, err := jwt.ParseWithClaims(
+ refreshToken,
+ &jwt.RegisteredClaims{},
+ func(token *jwt.Token) (interface{}, error) {
+ return i.cfg.publicKey, nil
+ },
+ jwt.WithAudience(i.cfg.audience),
+ jwt.WithIssuer(i.cfg.issuer),
+ jwt.WithExpirationRequired(),
+ )
+ if err != nil {
+ i.logger.Error("Failed to parse refresh token", err)
+ if errors.Is(err, jwt.ErrTokenExpired) {
+ return nil, ports.ErrExpiredToken
+ }
+ return nil, ports.ErrInvalidRefreshToken
+ }
+ claims := token.Claims.(*jwt.RegisteredClaims)
+
+ accessToken, err := i.generateToken(ctx, claims.Subject, i.cfg.accessTokenMaxAge)
+ if err != nil {
+ return nil, err
+ }
+
+ i.logger.Info("Token refreshed", "userId", claims.Subject)
+
+ return &ports.TokenResponse{
+ AccessToken: accessToken,
+ RefreshToken: refreshToken,
+ ExpiresAt: time.Now().Add(i.cfg.accessTokenMaxAge),
+ }, nil
+}
+
+func (i *localIDP) AuthenticateUser(
+ ctx context.Context,
+ username string,
+ password string,
+) (*ports.UserIdentityInfo, error) {
+ user, err := i.repo.FindUserByUsername(ctx, username)
+ if err != nil {
+ i.logger.Error("Failed to find user", err)
+ return nil, err
+ }
+
+ if i.comparePassword(user.HashedPassword, password) != nil {
+ i.logger.Error("Invalid password")
+ return nil, ports.ErrInvalidPassword
+ }
+
+ return &ports.UserIdentityInfo{
+ ID: user.ID,
+ Email: user.Email,
+ Username: user.Username,
+ }, nil
+}
+
+func (i *localIDP) DeleteUser(ctx context.Context, userId string) error {
+ i.logger.Debug("Deleting user", "userId", userId)
+
+ err := i.repo.DeleteUser(ctx, userId)
+ if err != nil {
+ i.logger.Error("Failed to delete user", err)
+ return err
+ }
+
+ i.logger.Info("User deleted", "userId", userId)
+ return nil
+}
+
+func (i *localIDP) UpdateUser(
+ ctx context.Context,
+ userId,
+ username,
+ password,
+ email string,
+) (*ports.UserIdentityInfo, error) {
+ i.logger.Debug("Updating user", "userId", userId)
+
+ hashedPassword, err := i.hashPassword(password)
+ if err != nil {
+ i.logger.Error("Failed to hash password", err)
+ return nil, err
+ }
+
+ user, err := i.repo.UpdateUser(ctx, userId, username, hashedPassword, email)
+ if err != nil {
+ i.logger.Error("Failed to update user", err)
+ return nil, err
+ }
+
+ i.logger.Info("User updated", "userId", userId)
+ return &ports.UserIdentityInfo{
+ Email: user.Email,
+ Username: user.Username,
+ ID: userId,
+ }, nil
+}
+
+func (i *localIDP) GetPublicKey() interface{} {
+ return i.cfg.publicKey
+}
+
+func (i *localIDP) GetAlgorithm() string {
+ return jwt.SigningMethodEdDSA.Alg()
+}
+
+func (i *localIDP) generateToken(
+ ctx context.Context,
+ userId string,
+ expireDate time.Duration,
+) (string, error) {
+ accessTokenClaims := jwt.RegisteredClaims{
+ Subject: userId,
+ Issuer: i.cfg.issuer,
+ Audience: []string{i.cfg.audience},
+ IssuedAt: jwt.NewNumericDate(time.Now()),
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireDate)),
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, accessTokenClaims)
+ accessToken, err := token.SignedString(i.cfg.privateKey)
+ if err != nil {
+ return "", ports.ErrFailedToSignToken
+ }
+ return accessToken, nil
+}
+
+func (i *localIDP) comparePassword(hashedPassword, password string) error {
+ return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
+}
+
+func (i *localIDP) hashPassword(password string) (string, error) {
+ hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(password), defaultBCryptCost)
+ if err != nil {
+ return "", err
+ }
+ return string(hashedPasswordBytes), nil
+}
diff --git a/backend/internal/adapters/driven/auth/local_idp_test.go b/backend/internal/adapters/driven/auth/local_idp_test.go
new file mode 100644
index 0000000..4209fd0
--- /dev/null
+++ b/backend/internal/adapters/driven/auth/local_idp_test.go
@@ -0,0 +1,263 @@
+package auth
+
+import (
+ "context"
+ "log"
+ "testing"
+ "time"
+
+ "github.com/jackc/pgx/v5/pgxpool"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/suite"
+
+ "github.com/taldoflemis/brain.test/internal/adapters/driven/postgres"
+ "github.com/taldoflemis/brain.test/internal/ports"
+ testshelpers "github.com/taldoflemis/brain.test/test/helpers"
+)
+
+const (
+ seed = "SzceVsT4GdFOlrZn60XMgrFcvMNUMuuJ"
+)
+
+var (
+ testUsername = "gepeto"
+ testEmail = "gepeto@gmail.com"
+ testPassword = "mypassword"
+ testHashedPassword = "$2a$12$TSjLw2cqeD5bcjPUgOWaaew3xP88soPytNTnMi27vxcNMCDaLFkBa"
+ testUserId = ""
+ accessMaxAgeInMin = 15
+ refreshMaxAgeInHours = 24
+)
+
+type LocalIDPTestSuite struct {
+ suite.Suite
+ pgContainer *testshelpers.PostgresContainer
+ ctx context.Context
+ svc *localIDP
+ repo *postgres.LocalIDPPostgresStorer
+ pool *pgxpool.Pool
+}
+
+func (suite *LocalIDPTestSuite) SetupSuite() {
+ suite.ctx = context.Background()
+ pgContainer, err := testshelpers.CreatePostgresContainer(suite.ctx)
+ if err != nil {
+ log.Fatal(err)
+ }
+ suite.pgContainer = pgContainer
+ pool, err := postgres.NewPool(pgContainer.ConnStr)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ postgres.Migrate(pgContainer.ConnStr, "../postgres/migrations/")
+
+ repository := postgres.NewLocalIDPPostgresStorer(pool)
+ logger := testshelpers.NewDummyLogger(log.Writer())
+ cfg := NewLocalIdpConfig(seed, "issuer", "audience", accessMaxAgeInMin, refreshMaxAgeInHours)
+ svc := NewLocalIdp(*cfg, logger, repository)
+
+ suite.svc = svc
+ suite.repo = repository
+ suite.pool = pool
+}
+
+func (suite *LocalIDPTestSuite) SetupTest() {
+ user, err := suite.repo.StoreUser(suite.ctx, testUsername, testEmail, testHashedPassword)
+ if err != nil {
+ log.Fatalf("error inserting user: %s", err)
+ }
+ testUserId = user.ID
+}
+
+func (suite *LocalIDPTestSuite) TearDownTest() {
+ _, err := suite.pool.Exec(suite.ctx, "TRUNCATE TABLE users")
+ if err != nil {
+ log.Fatalf("error truncating users table: %s", err)
+ }
+}
+
+func (suite *LocalIDPTestSuite) TearDownSuite() {
+ if err := suite.pgContainer.Terminate(suite.ctx); err != nil {
+ log.Fatalf("error terminating postgres container: %s", err)
+ }
+}
+
+func TestLocalIDP(t *testing.T) {
+ if testing.Short() {
+ t.Skip("too slow for testing.Short")
+ }
+
+ suite.Run(t, new(LocalIDPTestSuite))
+}
+
+func (suite *LocalIDPTestSuite) TestCreateUser() {
+ // Arrange
+ t := suite.T()
+ username := "tubias"
+ email := "tubias@gmail.com"
+ password := "hashedpass"
+
+ // Act
+ user, err := suite.svc.CreateUser(suite.ctx, username, email, password)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+ assert.Equal(t, username, user.Username)
+ assert.Equal(t, email, user.Email)
+}
+
+func (suite *LocalIDPTestSuite) TestCreateToken() {
+ // Arrange
+ t := suite.T()
+
+ // Act
+ tokenResponse, err := suite.svc.CreateToken(suite.ctx, testUserId)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotEmpty(t, tokenResponse.AccessToken)
+ assert.NotEmpty(t, tokenResponse.RefreshToken)
+ assert.WithinDurationf(
+ t,
+ time.Now().Add((time.Duration(accessMaxAgeInMin) * time.Minute)),
+ tokenResponse.ExpiresAt,
+ 5*time.Second,
+ "expected expiresAt to be within %d minutes",
+ accessMaxAgeInMin,
+ )
+}
+
+func (suite *LocalIDPTestSuite) TestRefreshToken() {
+ // Arrange
+ t := suite.T()
+ validRefreshToken, err := suite.svc.generateToken(suite.ctx, testUserId, time.Hour)
+ assert.NoError(t, err)
+
+ // Act
+ tokenResponse, err := suite.svc.RefreshToken(suite.ctx, validRefreshToken)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotEmpty(t, tokenResponse.AccessToken)
+ assert.NotEmpty(t, tokenResponse.RefreshToken)
+ assert.WithinDurationf(
+ t,
+ time.Now().Add((time.Duration(accessMaxAgeInMin) * time.Minute)),
+ tokenResponse.ExpiresAt,
+ 5*time.Second,
+ "expected expiresAt to be within %d minutes",
+ accessMaxAgeInMin,
+ )
+}
+
+func (suite *LocalIDPTestSuite) TestInvalidRefreshToken() {
+ // Arrange
+ t := suite.T()
+ invalidRefreshToken := "invalid"
+
+ // Act
+ tokenResponse, err := suite.svc.RefreshToken(suite.ctx, invalidRefreshToken)
+
+ // Assert
+ assert.ErrorIs(t, err, ports.ErrInvalidRefreshToken)
+ assert.Nil(t, tokenResponse)
+}
+
+func (suite *LocalIDPTestSuite) TestExpiredRefreshToken() {
+ // Arrange
+ t := suite.T()
+ expiredRefreshToken, err := suite.svc.generateToken(suite.ctx, testUserId, -time.Minute)
+ assert.NoError(t, err)
+
+ // Act
+ tokenResponse, err := suite.svc.RefreshToken(suite.ctx, expiredRefreshToken)
+
+ // Assert
+ assert.ErrorIs(t, err, ports.ErrExpiredToken)
+ assert.Nil(t, tokenResponse)
+}
+
+func (suite *LocalIDPTestSuite) TestAuthenticateUser() {
+ // Arrange
+ t := suite.T()
+
+ // Act
+ user, err := suite.svc.AuthenticateUser(suite.ctx, testUsername, testPassword)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+ assert.Equal(t, testUsername, user.Username)
+ assert.Equal(t, testEmail, user.Email)
+}
+
+func (suite *LocalIDPTestSuite) TestAuthenticateUserWithWrongPassword() {
+ // Arrange
+ t := suite.T()
+ wrongPassword := "wrongpassword"
+
+ // Act
+ user, err := suite.svc.AuthenticateUser(suite.ctx, testUsername, wrongPassword)
+
+ // Assert
+ assert.ErrorIs(t, err, ports.ErrInvalidPassword)
+ assert.Nil(t, user)
+}
+
+func (suite *LocalIDPTestSuite) TestDeleteUser() {
+ // Arrange
+ t := suite.T()
+
+ // Act
+ err := suite.svc.DeleteUser(suite.ctx, testUserId)
+
+ // Assert
+ assert.NoError(t, err)
+}
+
+func (suite *LocalIDPTestSuite) TestDeleteUserThatDoesNotExist() {
+ // Arrange
+ t := suite.T()
+ randomId := "d0b8b515-f46b-4179-bb26-f7833ded8f8f"
+
+ // Act
+ err := suite.svc.DeleteUser(suite.ctx, randomId)
+
+ // Assert
+ assert.ErrorContains(t, err, ports.ErrUserNotFound.Error())
+}
+
+func (suite *LocalIDPTestSuite) TestUpdateUser() {
+ // Arrange
+ t := suite.T()
+ username := "tubias"
+ email := "tubias@gmail.com"
+ password := "hashedpassword"
+
+ // Act
+ user, err := suite.svc.UpdateUser(suite.ctx, testUserId, username, password, email)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+ assert.Equal(t, username, user.Username)
+ assert.Equal(t, email, user.Email)
+}
+
+func (suite *LocalIDPTestSuite) TestUpdateUserThatDoesNotExist() {
+ // Arrange
+ t := suite.T()
+ username := "tubias"
+ email := "tubias@gmail.com"
+ password := "hashedpassword"
+ randomId := "d0b8b515-f46b-4179-bb26-f7833ded8f8f"
+
+ // Act
+ user, err := suite.svc.UpdateUser(suite.ctx, randomId, username, password, email)
+
+ // Assert
+ assert.ErrorContains(t, err, ports.ErrUserNotFound.Error())
+ assert.Nil(t, user)
+}
From 3d28c6eac369816968bc7e75d725dba9ac782820 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 15:19:41 -0300
Subject: [PATCH 09/91] feature(#6): add user domain
---
backend/internal/core/domain/user.go | 13 +++++++++++++
1 file changed, 13 insertions(+)
create mode 100644 backend/internal/core/domain/user.go
diff --git a/backend/internal/core/domain/user.go b/backend/internal/core/domain/user.go
new file mode 100644
index 0000000..1a9145d
--- /dev/null
+++ b/backend/internal/core/domain/user.go
@@ -0,0 +1,13 @@
+package domain
+
+type CreateUserRequest struct {
+ Username string `validate:"required"`
+ Email string `validate:"required,email"`
+ Password string `validate:"required,min=8,max=72"`
+}
+
+type UpdateUserRequest struct {
+ Username string `validate:"required"`
+ Email string `validate:"required,email"`
+ Password string `validate:"required,min=8,max=72"`
+}
From 24dfa6b430fe03833bb47038f8f08bfbae84d513 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 15:07:11 -0300
Subject: [PATCH 10/91] feature(#6): add validation service
---
backend/internal/core/services/validation.go | 56 ++++++++++++++++++++
1 file changed, 56 insertions(+)
create mode 100644 backend/internal/core/services/validation.go
diff --git a/backend/internal/core/services/validation.go b/backend/internal/core/services/validation.go
new file mode 100644
index 0000000..af12ebe
--- /dev/null
+++ b/backend/internal/core/services/validation.go
@@ -0,0 +1,56 @@
+package services
+
+import (
+ "fmt"
+
+ "github.com/go-playground/validator/v10"
+)
+
+type ValidationService struct {
+ validate *validator.Validate
+}
+
+type ErrorMessage struct {
+ Message string
+}
+
+type ValidationError struct {
+ errors []ErrorMessage
+}
+
+func (validationerror *ValidationError) Error() string {
+ return "failed to validate struct"
+}
+
+func (validationerror *ValidationError) GetMessages() []ErrorMessage {
+ return validationerror.errors
+}
+
+func NewValidationService() *ValidationService {
+ return &ValidationService{
+ validate: validator.New(validator.WithRequiredStructEnabled()),
+ }
+}
+
+func (v ValidationService) Validate(i interface{}) error {
+ validationErrors := make([]ErrorMessage, 0)
+
+ errs := v.validate.Struct(i)
+ if errs != nil {
+ for _, err := range errs.(validator.ValidationErrors) {
+ var elem ErrorMessage
+
+ elem.Message = fmt.Sprintf(
+ "[%s]: '%v' | Needs to implement '%s'",
+ err.Field(),
+ err.Value(),
+ err.Tag(),
+ )
+ validationErrors = append(validationErrors, elem)
+ }
+
+ return &ValidationError{errors: validationErrors}
+ }
+
+ return nil
+}
From 8cad6f02247f8366662a512dd8c628042a31b0c5 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 15:19:52 -0300
Subject: [PATCH 11/91] feature(#6): add authentication service
---
.../internal/core/services/authentication.go | 86 ++++
.../core/services/authentication_test.go | 399 ++++++++++++++++++
2 files changed, 485 insertions(+)
create mode 100644 backend/internal/core/services/authentication.go
create mode 100644 backend/internal/core/services/authentication_test.go
diff --git a/backend/internal/core/services/authentication.go b/backend/internal/core/services/authentication.go
new file mode 100644
index 0000000..c252730
--- /dev/null
+++ b/backend/internal/core/services/authentication.go
@@ -0,0 +1,86 @@
+package services
+
+import (
+ "context"
+
+ "github.com/taldoflemis/brain.test/internal/core/domain"
+ "github.com/taldoflemis/brain.test/internal/ports"
+)
+
+type AuthenticationService struct {
+ logger ports.Logger
+ authManager ports.AuthenticationManager
+ validationService *ValidationService
+}
+
+func NewAuthenticationService(
+ logger ports.Logger,
+ authManager ports.AuthenticationManager,
+ validationService *ValidationService,
+) *AuthenticationService {
+ return &AuthenticationService{
+ logger: logger,
+ authManager: authManager,
+ validationService: validationService,
+ }
+}
+
+func (s *AuthenticationService) CreateUser(
+ ctx context.Context,
+ req *domain.CreateUserRequest,
+) (*ports.UserIdentityInfo, error) {
+ err := s.validationService.Validate(req)
+ if err != nil {
+ s.logger.Error("failed to validate struct")
+ return nil, err
+ }
+
+ return s.authManager.CreateUser(ctx, req.Username, req.Email, req.Password)
+}
+
+func (s *AuthenticationService) AuthenticateUser(
+ ctx context.Context,
+ username, password string,
+) (*ports.UserIdentityInfo, error) {
+ return s.authManager.AuthenticateUser(ctx, username, password)
+}
+
+func (s *AuthenticationService) DeleteUser(ctx context.Context, userId string) error {
+ return s.authManager.DeleteUser(ctx, userId)
+}
+
+func (s *AuthenticationService) UpdateUser(
+ ctx context.Context,
+ userId string,
+ req *domain.UpdateUserRequest,
+) (*ports.UserIdentityInfo, error) {
+ err := s.validationService.Validate(req)
+ if err != nil {
+ s.logger.Error("failed to validate struct")
+ return nil, err
+ }
+
+ return s.authManager.UpdateUser(ctx, userId, req.Username, req.Password, req.Email)
+}
+
+func (s *AuthenticationService) CreateToken(
+ ctx context.Context,
+ userId string,
+) (*ports.TokenResponse, error) {
+ return s.authManager.CreateToken(ctx, userId)
+}
+
+func (s *AuthenticationService) RefreshToken(
+ ctx context.Context,
+ refreshToken string,
+) (*ports.TokenResponse, error) {
+ return s.authManager.RefreshToken(ctx, refreshToken)
+}
+
+func (s *AuthenticationService) GetPublicKey() interface{} {
+ return s.authManager.GetPublicKey()
+}
+
+func (s *AuthenticationService) GetAlgorithm() string {
+ return s.authManager.GetAlgorithm()
+}
diff --git a/backend/internal/core/services/authentication_test.go b/backend/internal/core/services/authentication_test.go
new file mode 100644
index 0000000..552900a
--- /dev/null
+++ b/backend/internal/core/services/authentication_test.go
@@ -0,0 +1,399 @@
+package services_test
+
+import (
+ "context"
+ "log"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5/pgxpool"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/suite"
+
+ "github.com/taldoflemis/brain.test/internal/adapters/driven/auth"
+ "github.com/taldoflemis/brain.test/internal/adapters/driven/postgres"
+ "github.com/taldoflemis/brain.test/internal/core/domain"
+ "github.com/taldoflemis/brain.test/internal/core/services"
+ "github.com/taldoflemis/brain.test/internal/ports"
+ testshelpers "github.com/taldoflemis/brain.test/test/helpers"
+)
+
+const (
+ seed = "SzceVsT4GdFOlrZn60XMgrFcvMNUMuuJ"
+)
+
+var (
+ testUsername = "gepeto"
+ testEmail = "gepeto@gmail.com"
+ testPassword = "mypassword"
+ testHashedPassword = "$2a$12$TSjLw2cqeD5bcjPUgOWaaew3xP88soPytNTnMi27vxcNMCDaLFkBa"
+ testUserId = uuid.New().String()
+ accessMaxAgeInMin = 15
+ refreshMaxAgeInHours = 24
+)
+
+type AuthenticationServiceIntegrationTestSuite struct {
+ suite.Suite
+ pgContainer *testshelpers.PostgresContainer
+ ctx context.Context
+ pool *pgxpool.Pool
+ svc *services.AuthenticationService
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) SetupSuite() {
+ suite.ctx = context.Background()
+ pgContainer, err := testshelpers.CreatePostgresContainer(suite.ctx)
+ if err != nil {
+ log.Fatal(err)
+ }
+ suite.pgContainer = pgContainer
+ pool, err := postgres.NewPool(pgContainer.ConnStr)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ postgres.Migrate(pgContainer.ConnStr, "../../adapters/driven/postgres/migrations/")
+
+ repository := postgres.NewLocalIDPPostgresStorer(pool)
+ logger := testshelpers.NewDummyLogger(log.Writer())
+ cfg := auth.NewLocalIdpConfig(
+ seed,
+ "issuer",
+ "audience",
+ accessMaxAgeInMin,
+ refreshMaxAgeInHours,
+ )
+ adapter := auth.NewLocalIdp(*cfg, logger, repository)
+ svc := services.NewAuthenticationService(logger, adapter, services.NewValidationService())
+
+ suite.svc = svc
+ suite.pool = pool
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) SetupTest() {
+ _, err := suite.pool.Exec(
+ suite.ctx,
+ "INSERT INTO users (id, username, email, password) VALUES ($1, $2, $3, $4)",
+ testUserId,
+ testUsername,
+ testEmail,
+ testHashedPassword,
+ )
+ if err != nil {
+ log.Fatalf("error inserting user: %s", err)
+ }
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TearDownTest() {
+ _, err := suite.pool.Exec(suite.ctx, "TRUNCATE TABLE users")
+ if err != nil {
+ log.Fatalf("error truncating users table: %s", err)
+ }
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TearDownSuite() {
+ if err := suite.pgContainer.Terminate(suite.ctx); err != nil {
+ log.Fatalf("error terminating postgres container: %s", err)
+ }
+}
+
+func TestAuthenticationServiceIntegration(t *testing.T) {
+ if testing.Short() {
+ t.Skip("too slow for testing.Short")
+ }
+
+ suite.Run(t, new(AuthenticationServiceIntegrationTestSuite))
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestCreateUser() {
+ // Arrange
+ t := suite.T()
+
+ req := &domain.CreateUserRequest{
+ Email: "newemail@gmail.com",
+ Password: "newpassword",
+ Username: "newusername",
+ }
+
+ // Act
+ user, err := suite.svc.CreateUser(suite.ctx, req)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestCreateUserWithExistingUsername() {
+ // Arrange
+ t := suite.T()
+
+ req := &domain.CreateUserRequest{
+ Username: testUsername,
+ Email: "newmail@gmail.com",
+ Password: "newpassword",
+ }
+
+ // Act
+ user, err := suite.svc.CreateUser(suite.ctx, req)
+
+ // Assert
+ assert.ErrorIs(t, err, ports.ErrUserAlreadyExists)
+ assert.Nil(t, user)
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestCreateUserWithBadInput() {
+ // Arrange
+ t := suite.T()
+
+ req := &domain.CreateUserRequest{
+ Password: "",
+ Email: "",
+ Username: testUsername,
+ }
+
+ table := []struct {
+ badPassword string
+ badEmail string
+ description string
+ }{
+ {
+ badEmail: "bademail",
+ badPassword: testPassword,
+ description: "email: must be a valid email address",
+ },
+ {
+ badEmail: "",
+ badPassword: testPassword,
+ description: "email: cannot be blank",
+ },
+ {
+ badPassword: "123",
+ badEmail: testEmail,
+ description: "password: the length must be greater or equal than 8",
+ },
+ {
+ badPassword: "",
+ badEmail: testEmail,
+ description: "password: cannot be blank",
+ },
+ {
+ badPassword: "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890asd",
+ badEmail: testEmail,
+ description: "password: the length must be less or equal than 72",
+ },
+ }
+
+ validatorError := &services.ValidationError{}
+
+ for _, tt := range table {
+ req.Password = tt.badPassword
+ req.Email = tt.badEmail
+ t.Run(tt.description, func(t *testing.T) {
+ // Act
+ user, err := suite.svc.CreateUser(suite.ctx, req)
+
+ // Assert
+ assert.ErrorContains(t, err, validatorError.Error())
+ assert.Nil(t, user)
+ })
+ }
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestCreateToken() {
+ // Arrange
+ t := suite.T()
+
+ // Act
+ tokenResponse, err := suite.svc.CreateToken(suite.ctx, testUserId)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, tokenResponse)
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestAuthenticateUser() {
+ // Arrange
+ t := suite.T()
+
+ // Act
+ user, err := suite.svc.AuthenticateUser(suite.ctx, testUsername, testPassword)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestAuthenticateUserWithBadInput() {
+ // Arrange
+ t := suite.T()
+
+ table := []struct {
+ username string
+ password string
+ errorString string
+ description string
+ }{
+ {
+ username: "nonexistentuser",
+ password: testPassword,
+ errorString: ports.ErrUserNotFound.Error(),
+ description: "nonexistent user",
+ },
+ {
+ username: testUsername,
+ password: "wrongpassword",
+ errorString: ports.ErrInvalidPassword.Error(),
+ description: "wrong password",
+ },
+ }
+
+ for _, tt := range table {
+ t.Run(tt.description, func(t *testing.T) {
+ // Act
+ user, err := suite.svc.AuthenticateUser(suite.ctx, tt.username, tt.password)
+
+ // Assert
+ assert.ErrorContains(t, err, tt.errorString)
+ assert.Nil(t, user)
+ })
+ }
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestDeleteUser() {
+ // Arrange
+ t := suite.T()
+
+ // Act
+ err := suite.svc.DeleteUser(suite.ctx, testUserId)
+
+ // Assert
+ assert.NoError(t, err)
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestUpdateUser() {
+ // Arrange
+ t := suite.T()
+ newEmail := "newemail@gmail.com"
+
+ req := &domain.UpdateUserRequest{
+ Email: newEmail,
+ Password: testPassword,
+ Username: testUsername,
+ }
+
+ // Act
+ user, err := suite.svc.UpdateUser(suite.ctx, testUserId, req)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+ assert.Equal(t, testUserId, user.ID)
+ assert.Equal(t, newEmail, user.Email)
+ assert.Equal(t, testUsername, user.Username)
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestUpdateUserWithBadInput() {
+ // Arrange
+ t := suite.T()
+
+ req := &domain.UpdateUserRequest{
+ Password: "",
+ Email: "",
+ Username: testUsername,
+ }
+
+ table := []struct {
+ badPassword string
+ badEmail string
+ description string
+ }{
+ {
+ badEmail: "bademail",
+ badPassword: testPassword,
+ description: "email: must be a valid email address",
+ },
+ {
+ badEmail: "",
+ badPassword: testPassword,
+ description: "email: cannot be blank",
+ },
+ {
+ badPassword: "123",
+ badEmail: testEmail,
+ description: "password: the length must be greater or equal than 8",
+ },
+ {
+ badPassword: "",
+ badEmail: testEmail,
+ description: "password: cannot be blank",
+ },
+ {
+ badPassword: "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890asd",
+ badEmail: testEmail,
+ description: "password: the length must be less or equal than 72",
+ },
+ }
+
+ validatorError := &services.ValidationError{}
+
+ for _, tt := range table {
+ req.Password = tt.badPassword
+ req.Email = tt.badEmail
+ t.Run(tt.description, func(t *testing.T) {
+ // Act
+ user, err := suite.svc.UpdateUser(suite.ctx, testUserId, req)
+
+ // Assert
+ assert.ErrorContains(t, err, validatorError.Error())
+ assert.Nil(t, user)
+ })
+ }
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestRefreshToken() {
+ // Arrange
+ t := suite.T()
+ tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ assert.NoError(t, err)
+
+ // Act
+ tok, err = suite.svc.RefreshToken(suite.ctx, tok.RefreshToken)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, tok)
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestRefreshTokenWithBadInput() {
+ // Arrange
+ t := suite.T()
+
+ table := []struct {
+ refreshToken string
+ description string
+ }{
+ {
+ refreshToken: "invalidtoken",
+ description: "invalid token",
+ },
+ {
+ refreshToken: "",
+ description: "empty token",
+ },
+ {
+ refreshToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE3MTE3Mzk2ODEsImV4cCI6MTc0MzI3NTY4MSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSJ9.W07mltoo-kXL4yHNqDhwoyueGI4HWwQeHdxH-m7znnU",
+ description: "token with invalid signature",
+ },
+ }
+
+ for _, tt := range table {
+ t.Run(tt.description, func(t *testing.T) {
+ // Act
+ tok, err := suite.svc.RefreshToken(suite.ctx, tt.refreshToken)
+
+ // Assert
+ assert.ErrorContains(t, err, ports.ErrInvalidRefreshToken.Error())
+ assert.Nil(t, tok)
+ })
+ }
+}
From 0e6bfe177109c7c9948b8e2413358184187cb420 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 14:11:41 -0300
Subject: [PATCH 12/91] feature(#6): add fiber http driver
---
backend/docs/docs.go | 280 +++++++++
backend/internal/adapters/drivers/web/auth.go | 282 +++++++++
.../adapters/drivers/web/auth_test.go | 550 ++++++++++++++++++
.../adapters/drivers/web/middlewares.go | 60 ++
.../internal/adapters/drivers/web/router.go | 87 +++
5 files changed, 1259 insertions(+)
create mode 100644 backend/docs/docs.go
create mode 100644 backend/internal/adapters/drivers/web/auth.go
create mode 100644 backend/internal/adapters/drivers/web/auth_test.go
create mode 100644 backend/internal/adapters/drivers/web/middlewares.go
create mode 100644 backend/internal/adapters/drivers/web/router.go
diff --git a/backend/docs/docs.go b/backend/docs/docs.go
new file mode 100644
index 0000000..1524a66
--- /dev/null
+++ b/backend/docs/docs.go
@@ -0,0 +1,280 @@
+// Package docs Code generated by swaggo/swag. DO NOT EDIT
+package docs
+
+import "github.com/swaggo/swag"
+
+const docTemplate = `{
+ "schemes": {{ marshal .Schemes }},
+ "swagger": "2.0",
+ "info": {
+ "description": "{{escape .Description}}",
+ "title": "{{.Title}}",
+ "contact": {},
+ "version": "{{.Version}}"
+ },
+ "host": "{{.Host}}",
+ "basePath": "{{.BasePath}}",
+ "paths": {
+ "/auth/": {
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Update an Account",
+ "parameters": [
+ {
+ "description": "Update Account Request",
+ "name": "req",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web.UpdateAccountRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ },
+ "409": {
+ "description": "User already exists",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Log In a User",
+ "parameters": [
+ {
+ "description": "login Request",
+ "name": "req",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web.LoginRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/web.TokenResponse"
+ }
+ },
+ "401": {
+ "description": "Authentication Failed",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Delete an Account",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/auth/refresh": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Refresh an Access Token",
+ "parameters": [
+ {
+ "description": "Refresh Token Request",
+ "name": "req",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web.RefreshTokenRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/web.TokenResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Refresh Token",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "401": {
+ "description": "Expired Token",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/auth/register": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Register an User",
+ "parameters": [
+ {
+ "description": "Register User Request",
+ "name": "req",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web.RegisterUserRequest"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "$ref": "#/definitions/web.TokenResponse"
+ }
+ },
+ "409": {
+ "description": "User already exists",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "web.LoginRequest": {
+ "description": "Request of Login",
+ "type": "object",
+ "properties": {
+ "password": {
+ "description": "the password of the user",
+ "type": "string"
+ },
+ "username": {
+ "description": "the username of the user",
+ "type": "string"
+ }
+ }
+ },
+ "web.RefreshTokenRequest": {
+ "description": "Request of Refresh Token",
+ "type": "object",
+ "properties": {
+ "refresh_token": {
+ "description": "refresh token",
+ "type": "string"
+ }
+ }
+ },
+ "web.RegisterUserRequest": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "description": "the email of the user",
+ "type": "string"
+ },
+ "password": {
+ "description": "the password of the user",
+ "type": "string"
+ },
+ "username": {
+ "description": "the username of the user",
+ "type": "string"
+ }
+ }
+ },
+ "web.TokenResponse": {
+ "description": "A Token Response",
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "description": "access token",
+ "type": "string"
+ },
+ "expire_at": {
+ "description": "expired at",
+ "type": "string"
+ },
+ "refresh_token": {
+ "description": "refresh token",
+ "type": "string"
+ }
+ }
+ },
+ "web.UpdateAccountRequest": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "description": "the password of the user",
+ "type": "string"
+ },
+ "password": {
+ "description": "the email of the user",
+ "type": "string"
+ },
+ "username": {
+ "description": "the username of the user",
+ "type": "string"
+ }
+ }
+ }
+ }
+}`
+
+// SwaggerInfo holds exported Swagger Info so clients can modify it
+var SwaggerInfo = &swag.Spec{
+ Version: "0.69420",
+ Host: "localhost:42069",
+ BasePath: "/",
+ Schemes: []string{},
+ Title: "Brain.test API",
+ Description: "",
+ InfoInstanceName: "swagger",
+ SwaggerTemplate: docTemplate,
+ LeftDelim: "{{",
+ RightDelim: "}}",
+}
+
+func init() {
+ swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
+}
diff --git a/backend/internal/adapters/drivers/web/auth.go b/backend/internal/adapters/drivers/web/auth.go
new file mode 100644
index 0000000..8699cba
--- /dev/null
+++ b/backend/internal/adapters/drivers/web/auth.go
@@ -0,0 +1,282 @@
+package web
+
+import (
+ "errors"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/golang-jwt/jwt/v5"
+
+ "github.com/taldoflemis/brain.test/internal/core/domain"
+ "github.com/taldoflemis/brain.test/internal/core/services"
+ "github.com/taldoflemis/brain.test/internal/ports"
+)
+
+// LoginRequest
+//
+// @Description Request of Login
+type LoginRequest struct {
+ // the username of the user
+ Username string `json:"username" validate:"required"`
+ // the password of the user
+ Password string `json:"password" validate:"required"`
+}
+
+// TokenResponse
+//
+// @Description A Token Response
+type TokenResponse struct {
+ // access token
+ AccessToken string `json:"access_token"`
+ // refresh token
+ RefreshToken string `json:"refresh_token"`
+ // expired at
+ ExpireAt string `json:"expire_at"`
+}
+
+// RefreshTokenRequest
+//
+// @Description Request of Refresh Token
+type RefreshTokenRequest struct {
+ // refresh token
+ RefreshToken string `json:"refresh_token" validate:"required"`
+}
+
+// RegisterUserRequest
+type RegisterUserRequest struct {
+ // the username of the user
+ Username string `json:"username" validate:"required"`
+ // the email of the user
+ Email string `json:"email" validate:"required"`
+ // the password of the user
+ Password string `json:"password" validate:"required"`
+}
+
+// UpdateAccountRequest
+type UpdateAccountRequest struct {
+ // the username of the user
+ Username string `json:"username" validate:"required"`
+ // the email of the user
+ Password string `json:"password" validate:"required"`
+ // the password of the user
+ Email string `json:"email" validate:"required"`
+}
+
+type authHandler struct {
+ authService *services.AuthenticationService
+ valService *services.ValidationService
+ jwtMiddleware fiber.Handler
+}
+
+func NewAuthHandler(
+ jwtMiddleware fiber.Handler,
+ authService *services.AuthenticationService,
+ valService *services.ValidationService,
+) *authHandler {
+ return &authHandler{
+ authService: authService,
+ valService: valService,
+ jwtMiddleware: jwtMiddleware,
+ }
+}
+
+func (h *authHandler) RegisterRoutes(router fiber.Router) {
+ authApi := router.Group("/auth")
+
+ authApi.Post("/", h.RegisterUser)
+ authApi.Post("/login", h.Login)
+ authApi.Post("/refresh", h.RefreshToken)
+
+ authApi.Use(h.jwtMiddleware)
+
+ authApi.Put("/", h.UpdateAccount)
+ authApi.Delete("/", h.DeleteAccount)
+}
+
+// Login godoc
+//
+// @Summary Log In a User
+// @Tags Authentication
+// @Accept json
+// @Produce json
+// @Param req body LoginRequest true "login Request"
+// @Success 200 {object} TokenResponse
+// @Failure 401 {string} string "Authentication Failed"
+// @Router /auth/ [post]
+func (h *authHandler) Login(c *fiber.Ctx) error {
+ req := new(LoginRequest)
+
+ err := c.BodyParser(req)
+ if err != nil {
+ return err
+ }
+
+ err = h.valService.Validate(req)
+ if err != nil {
+ return err
+ }
+
+ info, err := h.authService.AuthenticateUser(c.Context(), req.Username, req.Password)
+ if err != nil {
+ if errors.Is(err, ports.ErrUserNotFound) || errors.Is(err, ports.ErrInvalidPassword) {
+ return c.Status(fiber.StatusUnauthorized).SendString("Invalid username or password")
+ }
+ return err
+ }
+
+ token, err := h.authService.CreateToken(c.Context(), info.ID)
+ if err != nil {
+ return err
+ }
+
+ return c.JSON(TokenResponse{
+ AccessToken: token.AccessToken,
+ RefreshToken: token.RefreshToken,
+ ExpireAt: token.ExpiresAt.String(),
+ })
+}
+
+// RefreshToken godoc
+//
+// @Summary Refresh an Access Token
+// @Tags Authentication
+// @Accept json
+// @Produce json
+// @Param req body RefreshTokenRequest true "Refresh Token Request"
+// @Success 200 {object} TokenResponse
+// @Failure 400 {string} string "Bad Refresh Token"
+// @Failure 401 {string} string "Expired Token"
+// @Router /auth/refresh [post]
+func (h *authHandler) RefreshToken(c *fiber.Ctx) error {
+ req := new(RefreshTokenRequest)
+
+ err := c.BodyParser(req)
+ if err != nil {
+ return err
+ }
+
+ err = h.valService.Validate(req)
+ if err != nil {
+ return err
+ }
+
+ token, err := h.authService.RefreshToken(c.Context(), req.RefreshToken)
+ if err != nil {
+ if errors.Is(err, ports.ErrExpiredToken) {
+ return c.Status(fiber.StatusUnauthorized).SendString(ports.ErrExpiredToken.Error())
+ }
+ if errors.Is(err, ports.ErrInvalidRefreshToken) {
+ return c.Status(fiber.StatusBadRequest).
+ SendString(ports.ErrInvalidRefreshToken.Error())
+ }
+
+ return err
+ }
+ return c.JSON(TokenResponse{
+ AccessToken: token.AccessToken,
+ RefreshToken: token.RefreshToken,
+ ExpireAt: token.ExpiresAt.String(),
+ })
+}
+
+// RegisterUser godoc
+//
+// @Summary Register an User
+// @Tags Authentication
+// @Accept json
+// @Produce json
+// @Param req body RegisterUserRequest true "Register User Request"
+// @Success 201 {object} TokenResponse
+// @Failure 409 {string} string "User already exists"
+// @Router /auth/register [post]
+func (h *authHandler) RegisterUser(c *fiber.Ctx) error {
+ req := new(RegisterUserRequest)
+
+ err := c.BodyParser(req)
+ if err != nil {
+ return err
+ }
+
+ err = h.valService.Validate(req)
+ if err != nil {
+ return err
+ }
+
+ user, err := h.authService.CreateUser(
+ c.Context(),
+ &domain.CreateUserRequest{Username: req.Username, Email: req.Email, Password: req.Password},
+ )
+ if err != nil {
+ if errors.Is(err, ports.ErrUserAlreadyExists) {
+ return c.Status(fiber.StatusConflict).SendString("User already exists")
+ }
+ return err
+ }
+
+ token, err := h.authService.CreateToken(c.Context(), user.ID)
+ if err != nil {
+ return err
+ }
+
+ return c.Status(fiber.StatusCreated).JSON(TokenResponse{
+ AccessToken: token.AccessToken,
+ RefreshToken: token.RefreshToken,
+ ExpireAt: token.ExpiresAt.String(),
+ })
+}
+
+// UpdateAccount godoc
+//
+// @Summary Update an Account
+// @Tags Authentication
+// @Accept json
+// @Param req body UpdateAccountRequest true "Update Account Request"
+// @Success 200
+// @Failure 409 {string} string "User already exists"
+// @Router /auth/ [put]
+func (h *authHandler) UpdateAccount(c *fiber.Ctx) error {
+ user := c.Locals("user").(*jwt.Token)
+ id := user.Claims.(jwt.MapClaims)["sub"].(string)
+
+ req := new(UpdateAccountRequest)
+
+ err := c.BodyParser(req)
+ if err != nil {
+ return err
+ }
+
+ err = h.valService.Validate(req)
+ if err != nil {
+ return err
+ }
+
+ _, err = h.authService.UpdateUser(
+ c.Context(),
+ id,
+ &domain.UpdateUserRequest{Username: req.Username, Email: req.Email, Password: req.Password},
+ )
+ if err != nil {
+ if errors.Is(err, ports.ErrUserAlreadyExists) {
+ return c.Status(fiber.StatusConflict).SendString("User already exists")
+ }
+ return err
+ }
+ return c.SendStatus(fiber.StatusNoContent)
+}
+
+// DeleteAccount godoc
+//
+// @Summary Delete an Account
+// @Tags Authentication
+// @Success 200
+// @Router /auth/ [delete]
+func (h *authHandler) DeleteAccount(c *fiber.Ctx) error {
+ user := c.Locals("user").(*jwt.Token)
+ id := user.Claims.(jwt.MapClaims)["sub"].(string)
+
+ err := h.authService.DeleteUser(c.Context(), id)
+ if err != nil {
+ return err
+ }
+
+ return c.SendStatus(fiber.StatusNoContent)
+}
diff --git a/backend/internal/adapters/drivers/web/auth_test.go b/backend/internal/adapters/drivers/web/auth_test.go
new file mode 100644
index 0000000..6acec36
--- /dev/null
+++ b/backend/internal/adapters/drivers/web/auth_test.go
@@ -0,0 +1,550 @@
+package web
+
+import (
+ "context"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gavv/httpexpect/v2"
+ "github.com/gofiber/fiber/v2"
+ "github.com/gofiber/fiber/v2/middleware/adaptor"
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5/pgxpool"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/suite"
+
+ "github.com/taldoflemis/brain.test/internal/adapters/driven/auth"
+ "github.com/taldoflemis/brain.test/internal/adapters/driven/postgres"
+ "github.com/taldoflemis/brain.test/internal/core/services"
+ testshelpers "github.com/taldoflemis/brain.test/test/helpers"
+)
+
+const (
+ seed = "SzceVsT4GdFOlrZn60XMgrFcvMNUMuuJ"
+ authHeaderPrefix = "Bearer "
+)
+
+var (
+ testUsername = "gepeto"
+ testEmail = "gepeto@gmail.com"
+ testPassword = "mypassword"
+ testHashedPassword = "$2a$12$TSjLw2cqeD5bcjPUgOWaaew3xP88soPytNTnMi27vxcNMCDaLFkBa"
+ testUserId = uuid.New().String()
+ accessMaxAgeInMin = 1
+ refreshMaxAgeInHours = 1
+)
+
+type AuthHandlerTestSuite struct {
+ app *fiber.App
+ pgContainer *testshelpers.PostgresContainer
+ suite.Suite
+ ctx context.Context
+ pool *pgxpool.Pool
+ svc *services.AuthenticationService
+}
+
+func (suite *AuthHandlerTestSuite) SetupSuite() {
+ app := fiber.New(fiber.Config{
+ ErrorHandler: ErrorHandlerMiddleware,
+ })
+ suite.ctx = context.Background()
+
+ logger := testshelpers.NewDummyLogger(log.Writer())
+ pgContainer, err := testshelpers.CreatePostgresContainer(suite.ctx)
+ if err != nil {
+ log.Fatal("tubias", err)
+ }
+ pool, err := postgres.NewPool(pgContainer.ConnStr)
+ if err != nil {
+ log.Fatal(err)
+ }
+ postgres.Migrate(pgContainer.ConnStr, "../../driven/postgres/migrations/")
+
+ repository := postgres.NewLocalIDPPostgresStorer(pool)
+ cfg := auth.NewLocalIdpConfig(
+ seed,
+ "issuer",
+ "audience",
+ accessMaxAgeInMin,
+ refreshMaxAgeInHours,
+ )
+ authManager := auth.NewLocalIdp(
+ *cfg,
+ logger,
+ repository,
+ )
+
+ jwtMiddleware := NewJWTMiddleware(authManager)
+ validationService := services.NewValidationService()
+ authService := services.NewAuthenticationService(
+ logger,
+ authManager,
+ validationService,
+ )
+
+ authHandler := NewAuthHandler(jwtMiddleware, authService, validationService)
+
+ authHandler.RegisterRoutes(app)
+
+ suite.app = app
+ suite.pgContainer = pgContainer
+ suite.pool = pool
+ suite.svc = authService
+}
+
+func (suite *AuthHandlerTestSuite) SetupTest() {
+ _, err := suite.pool.Exec(
+ suite.ctx,
+ "INSERT INTO users (id, username, email, password) VALUES ($1, $2, $3, $4)",
+ testUserId,
+ testUsername,
+ testEmail,
+ testHashedPassword,
+ )
+ if err != nil {
+ log.Fatalf("error inserting user: %s", err)
+ }
+}
+
+func (suite *AuthHandlerTestSuite) TearDownTest() {
+ _, err := suite.pool.Exec(suite.ctx, "TRUNCATE TABLE users")
+ if err != nil {
+ log.Fatalf("error truncating users table: %s", err)
+ }
+}
+
+func (suite *AuthHandlerTestSuite) TearDownSuite() {
+ if err := suite.pgContainer.Terminate(suite.ctx); err != nil {
+ log.Fatalf("error terminating postgres container: %s", err)
+ }
+}
+
+func TestLoginTestSuite(t *testing.T) {
+ if testing.Short() {
+ t.Skip("too slow for testing.Short")
+ }
+ suite.Run(t, new(AuthHandlerTestSuite))
+}
+
+func (suite *AuthHandlerTestSuite) TestRegisterUser() {
+ // Arrange
+ t := suite.T()
+ username := "tubias"
+ email := "tubias@gmail.com"
+ password := "hashedpass"
+
+ req := map[string]interface{}{
+ "username": username,
+ "email": email,
+ "password": password,
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.POST("/auth/").WithJSON(req).Expect()
+
+ // Assert
+ resp.Status(http.StatusCreated)
+ obj := resp.JSON().Object()
+ obj.ContainsKey("access_token")
+ obj.ContainsKey("refresh_token")
+ obj.ContainsKey("expire_at")
+}
+
+func (suite *AuthHandlerTestSuite) TestRegisterUserWithBadInput() {
+ // Arrange
+ t := suite.T()
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ table := []struct {
+ req map[string]interface{}
+ description string
+ errorCount int
+ }{
+ {
+ req: map[string]interface{}{},
+ description: "empty request",
+ },
+ {
+ req: map[string]interface{}{
+ "username": "",
+ "email": testEmail,
+ "password": testPassword,
+ },
+ description: "empty username",
+ },
+ {
+ req: map[string]interface{}{
+ "username": "temido",
+ "email": "",
+ "password": testPassword,
+ },
+ description: "empty email",
+ },
+ {
+ req: map[string]interface{}{
+ "username": "gabrigas",
+ "email": testEmail,
+ "password": "",
+ },
+ description: "empty password",
+ },
+ {
+ req: map[string]interface{}{
+ "username": "mx30",
+ "email": "invalidemail",
+ "password": testPassword,
+ },
+ description: "invalid email",
+ },
+ }
+
+ for _, tt := range table {
+ t.Run(tt.description, func(t *testing.T) {
+ // Act
+ resp := e.POST("/auth/").WithJSON(tt.req).Expect()
+
+ // Assert
+ resp.Status(http.StatusUnprocessableEntity)
+ obj := resp.JSON().Object()
+ obj.ContainsKey("errors")
+ obj.Value("errors").Array().NotEmpty()
+ })
+ }
+
+}
+
+func (suite *AuthHandlerTestSuite) TestRegisterUserThatAlreadyExists() {
+ // Arrange
+ t := suite.T()
+ req := map[string]interface{}{
+ "username": testUsername,
+ "email": testEmail,
+ "password": testPassword,
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.POST("/auth/").WithJSON(req).Expect()
+
+ // Assert
+ resp.Status(http.StatusConflict)
+}
+
+func (suite *AuthHandlerTestSuite) TestLogin() {
+ // Arrange
+ t := suite.T()
+ req := map[string]interface{}{
+ "username": testUsername,
+ "password": testPassword,
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.POST("/auth/login").WithJSON(req).Expect()
+
+ // Assert
+ resp.Status(http.StatusOK)
+ obj := resp.JSON().Object()
+ obj.ContainsKey("access_token")
+ obj.ContainsKey("refresh_token")
+ obj.ContainsKey("expire_at")
+}
+
+func (suite *AuthHandlerTestSuite) TestLoginWithUserThatDontExist() {
+ // Arrange
+ t := suite.T()
+ req := map[string]interface{}{
+ "username": "nonexistentuser",
+ "password": testPassword,
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.POST("/auth/login").WithJSON(req).Expect()
+
+ // Assert
+ resp.Status(http.StatusUnauthorized)
+}
+
+func (suite *AuthHandlerTestSuite) TestLoginWithWrongPassword() {
+ // Arrange
+ t := suite.T()
+ req := map[string]interface{}{
+ "username": testUsername,
+ "password": "wrongpassword",
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.POST("/auth/login").WithJSON(req).Expect()
+
+ // Assert
+ resp.Status(http.StatusUnauthorized)
+}
+
+func (suite *AuthHandlerTestSuite) TestRefreshToken() {
+ // Arrange
+ t := suite.T()
+ tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ assert.NoError(t, err)
+ assert.NotNil(t, tok)
+
+ req := map[string]interface{}{
+ "refresh_token": tok.RefreshToken,
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.POST("/auth/refresh").WithJSON(req).Expect()
+
+ // Assert
+ resp.Status(http.StatusOK)
+ obj := resp.JSON().Object()
+ obj.ContainsKey("access_token")
+ obj.ContainsKey("refresh_token")
+ obj.ContainsKey("expire_at")
+}
+
+func (suite *AuthHandlerTestSuite) TestRefreshTokenWithInvalidRefreshToken() {
+ // Arrange
+ t := suite.T()
+ req := map[string]interface{}{
+ "refresh_token": "invalid",
+ }
+
+ table := []struct {
+ req map[string]interface{}
+ description string
+ }{
+ {
+ req: map[string]interface{}{
+ "refresh_token": "invalid",
+ },
+ description: "invalid refresh token",
+ },
+ {
+ req: map[string]interface{}{
+ "refresh_token": "",
+ },
+ description: "empty refresh token",
+ },
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ for _, tt := range table {
+ t.Run(tt.description, func(t *testing.T) {
+ // Act
+ resp := e.POST("/auth/refresh").WithJSON(req).Expect()
+
+ // Assert
+ resp.Status(http.StatusBadRequest)
+ })
+ }
+}
+
+func (suite *AuthHandlerTestSuite) TestUpdateUser() {
+ // Arrange
+ t := suite.T()
+ tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ assert.NoError(t, err)
+
+ req := map[string]interface{}{
+ "username": "newusername",
+ "email": "geponto@gmail.com",
+ "password": "newpassword",
+ }
+
+ headers := map[string]string{
+ "Authorization": authHeaderPrefix + tok.AccessToken,
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.PUT("/auth/").WithHeaders(headers).WithJSON(req).Expect()
+
+ // Assert
+ resp.Status(http.StatusNoContent)
+}
+
+func (suite *AuthHandlerTestSuite) TestUpdateUserWithInvalidInput() {
+ // Arrange
+ t := suite.T()
+ tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ assert.NoError(t, err)
+
+ headers := map[string]string{
+ "Authorization": authHeaderPrefix + tok.AccessToken,
+ }
+
+ table := []struct {
+ req map[string]interface{}
+ description string
+ }{
+ {
+ req: map[string]interface{}{},
+ description: "blank input",
+ },
+ {
+ req: map[string]interface{}{
+ "username": "",
+ "email": testEmail,
+ "password": testPassword,
+ },
+ description: "blank username",
+ },
+ {
+ req: map[string]interface{}{
+ "username": testUsername,
+ "email": "",
+ "password": testPassword,
+ },
+ description: "empty email",
+ },
+ {
+ req: map[string]interface{}{
+ "username": testUsername,
+ "email": testEmail,
+ "password": "",
+ },
+ description: "empty password",
+ },
+ {
+ req: map[string]interface{}{
+ "username": testUsername,
+ "email": "bademail",
+ "password": testPassword,
+ },
+ description: "empty password",
+ },
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ for _, tt := range table {
+ t.Run(tt.description, func(t *testing.T) {
+ // Act
+ resp := e.PUT("/auth/").WithHeaders(headers).WithJSON(tt.req).Expect()
+
+ // Assert
+ resp.Status(http.StatusUnprocessableEntity)
+ resp.JSON().Object().Value("errors").Array().NotEmpty()
+ })
+ }
+}
+
+func (suite *AuthHandlerTestSuite) TestUpdateUserWithoutAuthorization() {
+ // Arrange
+ t := suite.T()
+
+ req := map[string]interface{}{
+ "username": "newusername",
+ "email": "email@gmail.com",
+ "password": "newpassword",
+ }
+ headers := map[string]string{
+ "Authorization": authHeaderPrefix + "invalid_token",
+ }
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.PUT("/auth/").WithHeaders(headers).WithJSON(req).Expect()
+
+ // Assert
+ resp.Status(http.StatusUnauthorized)
+}
+
+func (suite *AuthHandlerTestSuite) TestUpdateUserToUsernameThatAlreadyExists() {
+ // Arrange
+ t := suite.T()
+ otherUsername := "otherusername"
+ _, err := suite.pool.Exec(
+ suite.ctx,
+ "INSERT INTO users (id, username, email, password) VALUES ($1, $2, $3, $4)",
+ uuid.New().String(),
+ otherUsername,
+ testEmail,
+ testHashedPassword,
+ )
+ assert.NoError(t, err)
+
+ tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ assert.NoError(t, err)
+
+ req := map[string]interface{}{
+ "username": otherUsername,
+ "email": testEmail,
+ "password": testPassword,
+ }
+ headers := map[string]string{
+ "Authorization": authHeaderPrefix + tok.AccessToken,
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.PUT("/auth/").WithHeaders(headers).WithJSON(req).Expect()
+
+ // Assert
+ resp.Status(http.StatusConflict)
+}
+
+func (suite *AuthHandlerTestSuite) TestDeleteAccount() {
+ // Arrange
+ t := suite.T()
+ tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ assert.NoError(t, err)
+
+ headers := map[string]string{
+ "Authorization": authHeaderPrefix + tok.AccessToken,
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.DELETE("/auth/").WithHeaders(headers).Expect()
+
+ // Assert
+ resp.Status(http.StatusNoContent)
+}
+
+func (suite *AuthHandlerTestSuite) TestDeleteAccountWithoutAuthorization() {
+ // Arrange
+ t := suite.T()
+
+ headers := map[string]string{
+ "Authorization": authHeaderPrefix + "invalid_token",
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.DELETE("/auth/").WithHeaders(headers).Expect()
+
+ // Assert
+ resp.Status(http.StatusUnauthorized)
+}
diff --git a/backend/internal/adapters/drivers/web/middlewares.go b/backend/internal/adapters/drivers/web/middlewares.go
new file mode 100644
index 0000000..fbb8c0d
--- /dev/null
+++ b/backend/internal/adapters/drivers/web/middlewares.go
@@ -0,0 +1,60 @@
+package web
+
+import (
+ "fmt"
+
+ jwtware "github.com/gofiber/contrib/jwt"
+ "github.com/gofiber/fiber/v2"
+ "github.com/golang-jwt/jwt/v5"
+
+ "github.com/taldoflemis/brain.test/internal/core/services"
+ "github.com/taldoflemis/brain.test/internal/ports"
+)
+
+func NewJWTMiddleware(authManager ports.AuthenticationManager) fiber.Handler {
+ return jwtware.New(jwtware.Config{
+ KeyFunc: customKeyFunc(authManager),
+ })
+}
+
+func customKeyFunc(authManager ports.AuthenticationManager) jwt.Keyfunc {
+ return func(t *jwt.Token) (interface{}, error) {
+ if t.Method.Alg() != authManager.GetAlgorithm() {
+ return nil, fmt.Errorf("Unexpected jwt signing method=%v", t.Header["alg"])
+ }
+
+ return authManager.GetPublicKey(), nil
+ }
+}
+
+type ValidationErrorResponse struct {
+ Errors []string `json:"errors"`
+}
+
+func ErrorHandlerMiddleware(c *fiber.Ctx, err error) error {
+ if err != nil {
+ validationErrors, ok := err.(*services.ValidationError)
+ if ok {
+ resp := convertValidationErrorsToResponse(validationErrors)
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(resp)
+ }
+ return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
+ }
+
+ return nil
+}
+
+func convertValidationErrorsToResponse(
+ validationErrors *services.ValidationError,
+) ValidationErrorResponse {
+ messages := validationErrors.GetMessages()
+ error := ValidationErrorResponse{
+ Errors: make([]string, len(messages)),
+ }
+
+ for i, message := range messages {
+ error.Errors[i] = message.Message
+ }
+
+ return error
+}
diff --git a/backend/internal/adapters/drivers/web/router.go b/backend/internal/adapters/drivers/web/router.go
new file mode 100644
index 0000000..d3c098d
--- /dev/null
+++ b/backend/internal/adapters/drivers/web/router.go
@@ -0,0 +1,87 @@
+package web
+
+import (
+ "fmt"
+
+ "github.com/gofiber/contrib/fiberzap/v2"
+ "github.com/gofiber/fiber/v2"
+ "github.com/gofiber/fiber/v2/middleware/cors"
+ "github.com/gofiber/fiber/v2/middleware/recover"
+ "github.com/gofiber/swagger"
+ "go.uber.org/zap"
+
+ _ "github.com/taldoflemis/brain.test/docs"
+)
+
+type Router struct {
+ config Config
+ zapLogger *zap.Logger
+ handlers []Handler
+}
+
+type Handler interface {
+ RegisterRoutes(router fiber.Router)
+}
+
+func NewRouter(
+ config Config,
+ zapLogger *zap.Logger,
+ handlers []Handler,
+) *Router {
+ return &Router{
+ config: config,
+ zapLogger: zapLogger,
+ handlers: handlers,
+ }
+}
+
+func (r *Router) Serve() error {
+ app := fiber.New(
+ fiber.Config{
+ ServerHeader: "brain.test",
+ AppName: "brain.test v0.69420",
+ DisableStartupMessage: true,
+ ErrorHandler: ErrorHandlerMiddleware,
+ },
+ )
+ app.Use(recover.New())
+
+ app.Use(fiberzap.New(fiberzap.Config{
+ Logger: r.zapLogger,
+ }))
+
+ app.Use(cors.New(cors.Config{
+ AllowOrigins: r.config.CORSAllowOrigins,
+ AllowMethods: r.config.CORSAllowMethods,
+ AllowHeaders: r.config.CORSAllowHeaders,
+ }))
+
+ api := app.Group(r.config.Prefix)
+
+ // Health check
+ api.Get("/health", func(c *fiber.Ctx) error {
+ return c.SendString("OK")
+ })
+
+ api.Get("/swagger/*", swagger.HandlerDefault)
+
+ // Register routes
+ for _, handler := range r.handlers {
+ handler.RegisterRoutes(api)
+ }
+
+ r.zapLogger.Info(
+ "Starting server",
+ zap.String("listen_ip", r.config.ListenIP),
+ zap.Int("port", r.config.Port),
+ )
+
+ err := app.Listen(fmt.Sprintf("%s:%d", r.config.ListenIP, r.config.Port))
+
+ if err != nil {
+ r.zapLogger.Error("Failed to start server", zap.Error(err))
+ return err
+ }
+
+ return nil
+}
From 55cc174130934ea86a8755f511d448c841de5d60 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 14:12:02 -0300
Subject: [PATCH 13/91] improvement(#6): update main.go to mount and inject
services
---
backend/cmd/web/main.go | 74 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 73 insertions(+), 1 deletion(-)
diff --git a/backend/cmd/web/main.go b/backend/cmd/web/main.go
index 0e4299e..8550bbe 100644
--- a/backend/cmd/web/main.go
+++ b/backend/cmd/web/main.go
@@ -2,8 +2,80 @@ package main
import (
"log"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "go.uber.org/zap"
+
+ "github.com/taldoflemis/brain.test/config"
+ "github.com/taldoflemis/brain.test/internal/adapters/driven/auth"
+ "github.com/taldoflemis/brain.test/internal/adapters/driven/misc"
+ "github.com/taldoflemis/brain.test/internal/adapters/driven/postgres"
+ "github.com/taldoflemis/brain.test/internal/adapters/drivers/web"
+ "github.com/taldoflemis/brain.test/internal/core/services"
)
+// @title Brain.test API
+// @version 0.69420
+// @BasePath /
+// @host localhost:42069
func main() {
- log.Println("Starting server on :4000")
+ // Init configs
+ koanf := config.NewKoanfson()
+ err := koanf.LoadFromJSON("config.json")
+ if err != nil {
+ log.Fatal(err)
+ }
+ fiberCfg, err := config.NewFiberConfig()
+ if err != nil {
+ log.Fatal(err)
+ }
+ pgCfg, err := config.NewPostgresConfig()
+ if err != nil {
+ log.Fatal(err)
+ }
+ localIDPCfg, err := config.NewLocalIDPConfig()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // Init Drivens
+ logger, err := zap.NewProduction()
+ if err != nil {
+ log.Fatal(err)
+ }
+ sugar := logger.Sugar()
+ zapLoggerAdapter := misc.NewZapLogger(sugar)
+
+ pool, err := postgres.NewPool(postgres.GenerateConnectionString(pgCfg))
+ if err != nil {
+ log.Fatal(err)
+ }
+ localIDPStorer := postgres.NewLocalIDPPostgresStorer(pool)
+
+ localIDP := auth.NewLocalIdp(*localIDPCfg, zapLoggerAdapter, localIDPStorer)
+
+ // Init Services
+ validationService := services.NewValidationService()
+ authService := services.NewAuthenticationService(zapLoggerAdapter, localIDP, validationService)
+
+ // Init Drivers
+ handlers := make([]web.Handler, 0)
+
+ jwtMiddleware := web.NewJWTMiddleware(localIDP)
+ authHandler := web.NewAuthHandler(jwtMiddleware, authService, validationService)
+ handlers = append(handlers, authHandler)
+
+ router := web.NewRouter(*fiberCfg, logger, handlers)
+ err = router.Serve()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // // Graceful shutdown
+ quit := make(chan os.Signal, 1)
+ signal.Notify(quit, os.Interrupt, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
+
+ <-quit
}
From b6793d680a9f1c9b4975032271ede4550fd82f55 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 14:33:42 -0300
Subject: [PATCH 14/91] improvement(#6): update golang ci
---
.github/workflows/go-ci.yaml | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/go-ci.yaml b/.github/workflows/go-ci.yaml
index 4d85cf2..a4f4031 100644
--- a/.github/workflows/go-ci.yaml
+++ b/.github/workflows/go-ci.yaml
@@ -24,7 +24,12 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
- go-version: '1.21.x'
+ go-version: '1.22.x'
+
+ - name: Install dependencies
+ run: |
+ go install golang.org/x/vuln/cmd/govulncheck@latest
+ go install gotest.tools/gotestsum@latest
- name: Install Task
uses: arduino/setup-task@v1
@@ -33,7 +38,7 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Format Check
- run: test -z "$(gofmt -l .)"
+ run: task check-format
- name: Lint
uses: golangci/golangci-lint-action@v4
@@ -45,4 +50,7 @@ jobs:
run: task build-web
- name: Test with the Go CLI
- run: task test-light
+ run: task test-all
+
+ - name: Check for vulnerabilities
+ run: task check-vuln
From f851abe10a2e495e33a3e5c62fcf171ec31f1cd2 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Fri, 29 Mar 2024 18:14:35 -0300
Subject: [PATCH 15/91] fix(#6): build-web running web
---
backend/Taskfile.yaml | 7 ++++++-
flake.nix | 1 +
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/backend/Taskfile.yaml b/backend/Taskfile.yaml
index 6511ca7..d970d90 100644
--- a/backend/Taskfile.yaml
+++ b/backend/Taskfile.yaml
@@ -9,7 +9,12 @@ tasks:
build-web:
desc: "Build the web handler"
cmds:
- - go run ./cmd/web -o ./cmd/web/brain.test
+ - go build -o brain.test ./cmd/web
+
+ lint:
+ desc: "Lint the backend"
+ cmds:
+ - golangci-lint run
format:
desc: "Format code"
diff --git a/flake.nix b/flake.nix
index d258646..f562c26 100644
--- a/flake.nix
+++ b/flake.nix
@@ -24,6 +24,7 @@
govulncheck
gotestsum
go-swag
+ golangci-lint
];
};
});
From a7864e2d63d985f22667372948ef215d34b8257d Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Sat, 30 Mar 2024 09:09:51 -0300
Subject: [PATCH 16/91] improvement(#6): add unprocessable entity in swagger
docs
---
backend/docs/docs.go | 53 +++++++++++++++++++
backend/internal/adapters/drivers/web/auth.go | 4 ++
.../adapters/drivers/web/middlewares.go | 2 +
3 files changed, 59 insertions(+)
diff --git a/backend/docs/docs.go b/backend/docs/docs.go
index 1524a66..11de9f5 100644
--- a/backend/docs/docs.go
+++ b/backend/docs/docs.go
@@ -44,6 +44,12 @@ const docTemplate = `{
"schema": {
"type": "string"
}
+ },
+ "422": {
+ "description": "Unprocessable Entity",
+ "schema": {
+ "$ref": "#/definitions/web.ValidationErrorResponse"
+ }
}
}
},
@@ -81,6 +87,12 @@ const docTemplate = `{
"schema": {
"type": "string"
}
+ },
+ "422": {
+ "description": "Unprocessable Entity",
+ "schema": {
+ "$ref": "#/definitions/web.ValidationErrorResponse"
+ }
}
}
},
@@ -137,6 +149,12 @@ const docTemplate = `{
"schema": {
"type": "string"
}
+ },
+ "422": {
+ "description": "Unprocessable Entity",
+ "schema": {
+ "$ref": "#/definitions/web.ValidationErrorResponse"
+ }
}
}
}
@@ -176,6 +194,12 @@ const docTemplate = `{
"schema": {
"type": "string"
}
+ },
+ "422": {
+ "description": "Unprocessable Entity",
+ "schema": {
+ "$ref": "#/definitions/web.ValidationErrorResponse"
+ }
}
}
}
@@ -185,6 +209,10 @@ const docTemplate = `{
"web.LoginRequest": {
"description": "Request of Login",
"type": "object",
+ "required": [
+ "password",
+ "username"
+ ],
"properties": {
"password": {
"description": "the password of the user",
@@ -199,6 +227,9 @@ const docTemplate = `{
"web.RefreshTokenRequest": {
"description": "Request of Refresh Token",
"type": "object",
+ "required": [
+ "refresh_token"
+ ],
"properties": {
"refresh_token": {
"description": "refresh token",
@@ -208,6 +239,11 @@ const docTemplate = `{
},
"web.RegisterUserRequest": {
"type": "object",
+ "required": [
+ "email",
+ "password",
+ "username"
+ ],
"properties": {
"email": {
"description": "the email of the user",
@@ -243,6 +279,11 @@ const docTemplate = `{
},
"web.UpdateAccountRequest": {
"type": "object",
+ "required": [
+ "email",
+ "password",
+ "username"
+ ],
"properties": {
"email": {
"description": "the password of the user",
@@ -257,6 +298,18 @@ const docTemplate = `{
"type": "string"
}
}
+ },
+ "web.ValidationErrorResponse": {
+ "type": "object",
+ "properties": {
+ "errors": {
+ "description": "errors in the request",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
}
}
}`
diff --git a/backend/internal/adapters/drivers/web/auth.go b/backend/internal/adapters/drivers/web/auth.go
index 8699cba..60a36a4 100644
--- a/backend/internal/adapters/drivers/web/auth.go
+++ b/backend/internal/adapters/drivers/web/auth.go
@@ -101,6 +101,7 @@ func (h *authHandler) RegisterRoutes(router fiber.Router) {
// @Param req body LoginRequest true "login Request"
// @Success 200 {object} TokenResponse
// @Failure 401 {string} string "Authentication Failed"
+// @Failure 422 {object} ValidationErrorResponse
// @Router /auth/ [post]
func (h *authHandler) Login(c *fiber.Ctx) error {
req := new(LoginRequest)
@@ -145,6 +146,7 @@ func (h *authHandler) Login(c *fiber.Ctx) error {
// @Success 200 {object} TokenResponse
// @Failure 400 {string} string "Bad Refresh Token"
// @Failure 401 {string} string "Expired Token"
+// @Failure 422 {object} ValidationErrorResponse
// @Router /auth/refresh [post]
func (h *authHandler) RefreshToken(c *fiber.Ctx) error {
req := new(RefreshTokenRequest)
@@ -187,6 +189,7 @@ func (h *authHandler) RefreshToken(c *fiber.Ctx) error {
// @Param req body RegisterUserRequest true "Register User Request"
// @Success 201 {object} TokenResponse
// @Failure 409 {string} string "User already exists"
+// @Failure 422 {object} ValidationErrorResponse
// @Router /auth/register [post]
func (h *authHandler) RegisterUser(c *fiber.Ctx) error {
req := new(RegisterUserRequest)
@@ -232,6 +235,7 @@ func (h *authHandler) RegisterUser(c *fiber.Ctx) error {
// @Param req body UpdateAccountRequest true "Update Account Request"
// @Success 200
// @Failure 409 {string} string "User already exists"
+// @Failure 422 {object} ValidationErrorResponse
// @Router /auth/ [put]
func (h *authHandler) UpdateAccount(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
diff --git a/backend/internal/adapters/drivers/web/middlewares.go b/backend/internal/adapters/drivers/web/middlewares.go
index fbb8c0d..4e09f00 100644
--- a/backend/internal/adapters/drivers/web/middlewares.go
+++ b/backend/internal/adapters/drivers/web/middlewares.go
@@ -27,7 +27,9 @@ func customKeyFunc(authManager ports.AuthenticationManager) jwt.Keyfunc {
}
}
+// ValidationErrorResponse
type ValidationErrorResponse struct {
+ // errors in the request
Errors []string `json:"errors"`
}
From b039b746f2ad0efbb167d39acc5988bc504b3a75 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Sun, 31 Mar 2024 08:19:59 -0300
Subject: [PATCH 17/91] improvement(#6): move user dtos to service instead of
domain
---
backend/internal/adapters/drivers/web/auth.go | 20 +++++++++++--------
.../adapters/drivers/web/middlewares.go | 6 ++++++
backend/internal/core/domain/user.go | 13 ------------
.../internal/core/services/authentication.go | 17 +++++++++++++---
.../core/services/authentication_test.go | 11 +++++-----
5 files changed, 37 insertions(+), 30 deletions(-)
delete mode 100644 backend/internal/core/domain/user.go
diff --git a/backend/internal/adapters/drivers/web/auth.go b/backend/internal/adapters/drivers/web/auth.go
index 60a36a4..05689a4 100644
--- a/backend/internal/adapters/drivers/web/auth.go
+++ b/backend/internal/adapters/drivers/web/auth.go
@@ -4,9 +4,7 @@ import (
"errors"
"github.com/gofiber/fiber/v2"
- "github.com/golang-jwt/jwt/v5"
- "github.com/taldoflemis/brain.test/internal/core/domain"
"github.com/taldoflemis/brain.test/internal/core/services"
"github.com/taldoflemis/brain.test/internal/ports"
)
@@ -206,7 +204,11 @@ func (h *authHandler) RegisterUser(c *fiber.Ctx) error {
user, err := h.authService.CreateUser(
c.Context(),
- &domain.CreateUserRequest{Username: req.Username, Email: req.Email, Password: req.Password},
+ &services.CreateUserRequest{
+ Username: req.Username,
+ Email: req.Email,
+ Password: req.Password,
+ },
)
if err != nil {
if errors.Is(err, ports.ErrUserAlreadyExists) {
@@ -238,8 +240,7 @@ func (h *authHandler) RegisterUser(c *fiber.Ctx) error {
// @Failure 422 {object} ValidationErrorResponse
// @Router /auth/ [put]
func (h *authHandler) UpdateAccount(c *fiber.Ctx) error {
- user := c.Locals("user").(*jwt.Token)
- id := user.Claims.(jwt.MapClaims)["sub"].(string)
+ id := extractTokenFromContext(c)
req := new(UpdateAccountRequest)
@@ -256,7 +257,11 @@ func (h *authHandler) UpdateAccount(c *fiber.Ctx) error {
_, err = h.authService.UpdateUser(
c.Context(),
id,
- &domain.UpdateUserRequest{Username: req.Username, Email: req.Email, Password: req.Password},
+ &services.UpdateUserRequest{
+ Username: req.Username,
+ Email: req.Email,
+ Password: req.Password,
+ },
)
if err != nil {
if errors.Is(err, ports.ErrUserAlreadyExists) {
@@ -274,8 +279,7 @@ func (h *authHandler) UpdateAccount(c *fiber.Ctx) error {
// @Success 200
// @Router /auth/ [delete]
func (h *authHandler) DeleteAccount(c *fiber.Ctx) error {
- user := c.Locals("user").(*jwt.Token)
- id := user.Claims.(jwt.MapClaims)["sub"].(string)
+ id := extractTokenFromContext(c)
err := h.authService.DeleteUser(c.Context(), id)
if err != nil {
diff --git a/backend/internal/adapters/drivers/web/middlewares.go b/backend/internal/adapters/drivers/web/middlewares.go
index 4e09f00..9fac1d7 100644
--- a/backend/internal/adapters/drivers/web/middlewares.go
+++ b/backend/internal/adapters/drivers/web/middlewares.go
@@ -60,3 +60,9 @@ func convertValidationErrorsToResponse(
return error
}
+
+func extractTokenFromContext(c *fiber.Ctx) string {
+ user := c.Locals("user").(*jwt.Token)
+ id := user.Claims.(jwt.MapClaims)["sub"].(string)
+ return id
+}
diff --git a/backend/internal/core/domain/user.go b/backend/internal/core/domain/user.go
deleted file mode 100644
index 1a9145d..0000000
--- a/backend/internal/core/domain/user.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package domain
-
-type CreateUserRequest struct {
- Username string `validate:"required"`
- Email string `validate:"required,email"`
- Password string `validate:"required,min=8,max=72"`
-}
-
-type UpdateUserRequest struct {
- Username string `validate:"required"`
- Email string `validate:"required,email"`
- Password string `validate:"required,min=8,max=72"`
-}
diff --git a/backend/internal/core/services/authentication.go b/backend/internal/core/services/authentication.go
index c252730..357179c 100644
--- a/backend/internal/core/services/authentication.go
+++ b/backend/internal/core/services/authentication.go
@@ -3,10 +3,21 @@ package services
import (
"context"
- "github.com/taldoflemis/brain.test/internal/core/domain"
"github.com/taldoflemis/brain.test/internal/ports"
)
+type CreateUserRequest struct {
+ Username string `validate:"required"`
+ Email string `validate:"required,email"`
+ Password string `validate:"required,min=8,max=72"`
+}
+
+type UpdateUserRequest struct {
+ Username string `validate:"required"`
+ Email string `validate:"required,email"`
+ Password string `validate:"required,min=8,max=72"`
+}
+
type AuthenticationService struct {
logger ports.Logger
authManager ports.AuthenticationManager
@@ -27,7 +38,7 @@ func NewAuthenticationService(
func (s *AuthenticationService) CreateUser(
ctx context.Context,
- req *domain.CreateUserRequest,
+ req *CreateUserRequest,
) (*ports.UserIdentityInfo, error) {
err := s.validationService.Validate(req)
if err != nil {
@@ -52,7 +63,7 @@ func (s *AuthenticationService) DeleteUser(ctx context.Context, userId string) e
func (s *AuthenticationService) UpdateUser(
ctx context.Context,
userId string,
- req *domain.UpdateUserRequest,
+ req *UpdateUserRequest,
) (*ports.UserIdentityInfo, error) {
err := s.validationService.Validate(req)
if err != nil {
diff --git a/backend/internal/core/services/authentication_test.go b/backend/internal/core/services/authentication_test.go
index 552900a..13bb908 100644
--- a/backend/internal/core/services/authentication_test.go
+++ b/backend/internal/core/services/authentication_test.go
@@ -12,7 +12,6 @@ import (
"github.com/taldoflemis/brain.test/internal/adapters/driven/auth"
"github.com/taldoflemis/brain.test/internal/adapters/driven/postgres"
- "github.com/taldoflemis/brain.test/internal/core/domain"
"github.com/taldoflemis/brain.test/internal/core/services"
"github.com/taldoflemis/brain.test/internal/ports"
testshelpers "github.com/taldoflemis/brain.test/test/helpers"
@@ -109,7 +108,7 @@ func (suite *AuthenticationServiceIntegrationTestSuite) TestCreateUser() {
// Arrange
t := suite.T()
- req := &domain.CreateUserRequest{
+ req := &services.CreateUserRequest{
Email: "newemail@gmail.com",
Password: "newpassword",
Username: "newusername",
@@ -127,7 +126,7 @@ func (suite *AuthenticationServiceIntegrationTestSuite) TestCreateUserWithExisti
// Arrange
t := suite.T()
- req := &domain.CreateUserRequest{
+ req := &services.CreateUserRequest{
Username: testUsername,
Email: "newmail@gmail.com",
Password: "newpassword",
@@ -145,7 +144,7 @@ func (suite *AuthenticationServiceIntegrationTestSuite) TestCreateUserWithBadInp
// Arrange
t := suite.T()
- req := &domain.CreateUserRequest{
+ req := &services.CreateUserRequest{
Password: "",
Email: "",
Username: testUsername,
@@ -275,7 +274,7 @@ func (suite *AuthenticationServiceIntegrationTestSuite) TestUpdateUser() {
t := suite.T()
newEmail := "newemail@gmail.com"
- req := &domain.UpdateUserRequest{
+ req := &services.UpdateUserRequest{
Email: newEmail,
Password: testPassword,
Username: testUsername,
@@ -296,7 +295,7 @@ func (suite *AuthenticationServiceIntegrationTestSuite) TestUpdateUserWithBadInp
// Arrange
t := suite.T()
- req := &domain.UpdateUserRequest{
+ req := &services.UpdateUserRequest{
Password: "",
Email: "",
Username: testUsername,
From b99f2e3d7d96a9c8531169fb379d3e0f59c0a5be Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Sun, 31 Mar 2024 09:26:00 -0300
Subject: [PATCH 18/91] improvement(#6): add get user info endpoint
---
backend/docs/docs.go | 49 ++++++++++++
.../adapters/driven/auth/local_idp.go | 51 ++++++++++---
.../adapters/driven/auth/local_idp_test.go | 35 +++++++++
.../internal/adapters/driven/postgres/auth.go | 24 ++++++
.../adapters/driven/postgres/auth_test.go | 31 +++++++-
backend/internal/adapters/drivers/web/auth.go | 45 +++++++++++
.../adapters/drivers/web/auth_test.go | 74 +++++++++++++++++++
.../internal/core/services/authentication.go | 7 ++
.../core/services/authentication_test.go | 35 +++++++++
backend/internal/ports/auth.go | 2 +
10 files changed, 341 insertions(+), 12 deletions(-)
diff --git a/backend/docs/docs.go b/backend/docs/docs.go
index 11de9f5..b8b0624 100644
--- a/backend/docs/docs.go
+++ b/backend/docs/docs.go
@@ -39,6 +39,18 @@ const docTemplate = `{
"200": {
"description": "OK"
},
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "schema": {
+ "type": "string"
+ }
+ },
"409": {
"description": "User already exists",
"schema": {
@@ -104,6 +116,18 @@ const docTemplate = `{
"responses": {
"200": {
"description": "OK"
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "schema": {
+ "type": "string"
+ }
}
}
}
@@ -203,6 +227,31 @@ const docTemplate = `{
}
}
}
+ },
+ "/auth/userinfo": {
+ "get": {
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Get User Info",
+ "responses": {
+ "200": {
+ "description": "OK"
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
}
},
"definitions": {
diff --git a/backend/internal/adapters/driven/auth/local_idp.go b/backend/internal/adapters/driven/auth/local_idp.go
index 176801a..d7e81b0 100644
--- a/backend/internal/adapters/driven/auth/local_idp.go
+++ b/backend/internal/adapters/driven/auth/local_idp.go
@@ -113,16 +113,7 @@ func (i *localIDP) RefreshToken(
refreshToken string,
) (*ports.TokenResponse, error) {
i.logger.Debug("Refreshing token")
- token, err := jwt.ParseWithClaims(
- refreshToken,
- &jwt.RegisteredClaims{},
- func(token *jwt.Token) (interface{}, error) {
- return i.cfg.publicKey, nil
- },
- jwt.WithAudience(i.cfg.audience),
- jwt.WithIssuer(i.cfg.issuer),
- jwt.WithExpirationRequired(),
- )
+ token, err := i.parseToken(refreshToken)
if err != nil {
i.logger.Error("Failed to parse refresh token", err)
if errors.Is(err, jwt.ErrTokenExpired) {
@@ -219,6 +210,33 @@ func (i *localIDP) GetAlgorithm() string {
return jwt.SigningMethodEdDSA.Alg()
}
+func (i *localIDP) GetUserInfo(ctx context.Context, tok string) (*ports.UserIdentityInfo, error) {
+ i.logger.Info("Getting user info")
+ token, err := i.parseToken(tok)
+ if err != nil {
+ i.logger.Error("Failed to parse token", err)
+ if errors.Is(err, jwt.ErrTokenExpired) {
+ return nil, ports.ErrExpiredToken
+ }
+ return nil, ports.ErrInvalidRefreshToken
+ }
+ claims := token.Claims.(*jwt.RegisteredClaims)
+
+ id := claims.Subject
+
+ info, err := i.repo.FindUserById(ctx, id)
+ if err != nil {
+ i.logger.Error("Failed to find user", "userId", id)
+ return nil, err
+ }
+
+ return &ports.UserIdentityInfo{
+ ID: info.ID,
+ Email: info.Email,
+ Username: info.Username,
+ }, nil
+}
+
func (i *localIDP) generateToken(
ctx context.Context,
userId string,
@@ -250,3 +268,16 @@ func (i *localIDP) hashPassword(password string) (string, error) {
}
return string(hashedPasswordBytes), nil
}
+
+func (i *localIDP) parseToken(token string) (*jwt.Token, error) {
+ return jwt.ParseWithClaims(
+ token,
+ &jwt.RegisteredClaims{},
+ func(token *jwt.Token) (interface{}, error) {
+ return i.cfg.publicKey, nil
+ },
+ jwt.WithAudience(i.cfg.audience),
+ jwt.WithIssuer(i.cfg.issuer),
+ jwt.WithExpirationRequired(),
+ )
+}
diff --git a/backend/internal/adapters/driven/auth/local_idp_test.go b/backend/internal/adapters/driven/auth/local_idp_test.go
index 4209fd0..7df45d7 100644
--- a/backend/internal/adapters/driven/auth/local_idp_test.go
+++ b/backend/internal/adapters/driven/auth/local_idp_test.go
@@ -261,3 +261,38 @@ func (suite *LocalIDPTestSuite) TestUpdateUserThatDoesNotExist() {
assert.ErrorContains(t, err, ports.ErrUserNotFound.Error())
assert.Nil(t, user)
}
+
+func (suite *LocalIDPTestSuite) TestGetUserInfo() {
+ // Arrange
+ t := suite.T()
+ validToken, err := suite.svc.generateToken(suite.ctx, testUserId, time.Hour)
+ assert.NoError(t, err)
+ expected := &ports.UserIdentityInfo{
+ ID: testUserId,
+ Email: testEmail,
+ Username: testUsername,
+ }
+
+ // Act
+ user, err := suite.svc.GetUserInfo(suite.ctx, validToken)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+ assert.Equal(t, expected, user)
+}
+
+func (suite *LocalIDPTestSuite) TestGetUserInfoThatDoesNotExist() {
+ // Arrange
+ t := suite.T()
+ randomId := "d0b8b515-f46b-4179-bb26-f7833ded8f8f"
+ invalidToken, err := suite.svc.generateToken(suite.ctx, randomId, time.Hour)
+ assert.NoError(t, err)
+
+ // Act
+ user, err := suite.svc.GetUserInfo(suite.ctx, invalidToken)
+
+ // Assert
+ assert.ErrorContains(t, err, ports.ErrUserNotFound.Error())
+ assert.Nil(t, user)
+}
diff --git a/backend/internal/adapters/driven/postgres/auth.go b/backend/internal/adapters/driven/postgres/auth.go
index e8bab7f..bd79c64 100644
--- a/backend/internal/adapters/driven/postgres/auth.go
+++ b/backend/internal/adapters/driven/postgres/auth.go
@@ -129,3 +129,27 @@ func (s *LocalIDPPostgresStorer) FindUserByUsername(
return &user, nil
}
+
+func (s *LocalIDPPostgresStorer) FindUserById(
+ ctx context.Context,
+ userId string,
+) (*ports.LocalIDPUserEntity, error) {
+ args := pgx.NamedArgs{
+ "id": userId,
+ }
+
+ query := `SELECT id, username, email, password FROM users WHERE id = @id`
+
+ var user ports.LocalIDPUserEntity
+
+ err := s.pool.QueryRow(ctx, query, args).
+ Scan(&user.ID, &user.Username, &user.Email, &user.HashedPassword)
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return nil, ports.ErrUserNotFound
+ }
+ return nil, err
+ }
+
+ return &user, nil
+}
diff --git a/backend/internal/adapters/driven/postgres/auth_test.go b/backend/internal/adapters/driven/postgres/auth_test.go
index eed655d..6839acc 100644
--- a/backend/internal/adapters/driven/postgres/auth_test.go
+++ b/backend/internal/adapters/driven/postgres/auth_test.go
@@ -80,7 +80,7 @@ func TestLocalIDPPostgresStorer(t *testing.T) {
suite.Run(t, new(LocalIDPPostgresStorerTestSuite))
}
-func (suite *LocalIDPPostgresStorerTestSuite) TestFindUserById() {
+func (suite *LocalIDPPostgresStorerTestSuite) TestFindUserByUsername() {
// Arrange
t := suite.T()
@@ -94,7 +94,7 @@ func (suite *LocalIDPPostgresStorerTestSuite) TestFindUserById() {
assert.Equal(t, testEmail, user.Email)
}
-func (suite *LocalIDPPostgresStorerTestSuite) TestTryToFindUserByIdThatDoesNotExist() {
+func (suite *LocalIDPPostgresStorerTestSuite) TestTryToFindUserByUsernameThatDoesNotExist() {
// Arrange
t := suite.T()
random := "7ebc4755-b7cc-4963-a2b1-636949b035d6"
@@ -107,6 +107,33 @@ func (suite *LocalIDPPostgresStorerTestSuite) TestTryToFindUserByIdThatDoesNotEx
assert.ErrorIs(t, err, ports.ErrUserNotFound)
}
+func (suite *LocalIDPPostgresStorerTestSuite) TestFindUserById() {
+ // Arrange
+ t := suite.T()
+
+ // Act
+ user, err := suite.repo.FindUserById(suite.ctx, testUserId)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.NotNil(t, user)
+ assert.Equal(t, testUsername, user.Username)
+ assert.Equal(t, testEmail, user.Email)
+ assert.Equal(t, testUserId, user.ID)
+}
+
+func (suite *LocalIDPPostgresStorerTestSuite) TestTryToFindUserByIdThatDoesNotExist() {
+ // Arrange
+ t := suite.T()
+
+ // Act
+ user, err := suite.repo.FindUserById(suite.ctx, "7ebc4755-b7cc-4963-a2b1-636949b035d6")
+
+ // Assert
+ assert.Nil(t, user)
+ assert.ErrorIs(t, err, ports.ErrUserNotFound)
+}
+
func (suite *LocalIDPPostgresStorerTestSuite) TestDeleteUser() {
// Arrange
t := suite.T()
diff --git a/backend/internal/adapters/drivers/web/auth.go b/backend/internal/adapters/drivers/web/auth.go
index 05689a4..cedee12 100644
--- a/backend/internal/adapters/drivers/web/auth.go
+++ b/backend/internal/adapters/drivers/web/auth.go
@@ -2,6 +2,7 @@ package web
import (
"errors"
+ "strings"
"github.com/gofiber/fiber/v2"
@@ -59,6 +60,16 @@ type UpdateAccountRequest struct {
Email string `json:"email" validate:"required"`
}
+// UserInfoResponse
+type UserInfoResponse struct {
+ // the user id
+ ID string `json:"id"`
+ // the username
+ Username string `json:"username"`
+ // the user email
+ Email string `json:"email"`
+}
+
type authHandler struct {
authService *services.AuthenticationService
valService *services.ValidationService
@@ -86,6 +97,7 @@ func (h *authHandler) RegisterRoutes(router fiber.Router) {
authApi.Use(h.jwtMiddleware)
+ authApi.Get("/userinfo", h.UserInfo)
authApi.Put("/", h.UpdateAccount)
authApi.Delete("/", h.DeleteAccount)
}
@@ -236,6 +248,8 @@ func (h *authHandler) RegisterUser(c *fiber.Ctx) error {
// @Accept json
// @Param req body UpdateAccountRequest true "Update Account Request"
// @Success 200
+// @Failure 400 {string} string
+// @Failure 401 {string} string
// @Failure 409 {string} string "User already exists"
// @Failure 422 {object} ValidationErrorResponse
// @Router /auth/ [put]
@@ -277,6 +291,8 @@ func (h *authHandler) UpdateAccount(c *fiber.Ctx) error {
// @Summary Delete an Account
// @Tags Authentication
// @Success 200
+// @Failure 400 {string} string
+// @Failure 401 {string} string
// @Router /auth/ [delete]
func (h *authHandler) DeleteAccount(c *fiber.Ctx) error {
id := extractTokenFromContext(c)
@@ -288,3 +304,32 @@ func (h *authHandler) DeleteAccount(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
}
+
+// UserInfo godoc
+//
+// @Summary Get User Info
+// @Tags Authentication
+// @Success 200
+// @Failure 400 {string} string
+// @Failure 401 {string} string
+// @Router /auth/userinfo [get]
+func (h *authHandler) UserInfo(c *fiber.Ctx) error {
+ header := c.GetReqHeaders()["Authorization"]
+ token := strings.Split(header[0], " ")[1]
+
+ info, err := h.authService.GetUserInfo(c.Context(), token)
+ if err != nil {
+ if errors.Is(err, ports.ErrUserNotFound) {
+ return c.Status(fiber.StatusUnauthorized).SendString("Unauthorized")
+ }
+ return err
+ }
+
+ resp := &UserInfoResponse{
+ ID: info.ID,
+ Username: info.Username,
+ Email: info.Email,
+ }
+
+ return c.JSON(resp)
+}
diff --git a/backend/internal/adapters/drivers/web/auth_test.go b/backend/internal/adapters/drivers/web/auth_test.go
index 6acec36..7990db8 100644
--- a/backend/internal/adapters/drivers/web/auth_test.go
+++ b/backend/internal/adapters/drivers/web/auth_test.go
@@ -548,3 +548,77 @@ func (suite *AuthHandlerTestSuite) TestDeleteAccountWithoutAuthorization() {
// Assert
resp.Status(http.StatusUnauthorized)
}
+
+func (suite *AuthHandlerTestSuite) TestUserInfo() {
+ // Arrange
+ t := suite.T()
+ tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ assert.NoError(t, err)
+
+ headers := map[string]string{
+ "Authorization": authHeaderPrefix + tok.AccessToken,
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ // Act
+ resp := e.GET("/auth/userinfo").WithHeaders(headers).Expect()
+
+ // Assert
+ resp.Status(http.StatusOK)
+ json := resp.JSON().Object()
+ json.IsEqual(map[string]interface{}{
+ "username": testUsername,
+ "email": testEmail,
+ "id": testUserId,
+ })
+}
+
+func (suite *AuthHandlerTestSuite) TestUserInfoWithBadInput() {
+ // Arrange
+ t := suite.T()
+ invalidToken, err := suite.svc.CreateToken(suite.ctx, uuid.New().String())
+ assert.NoError(t, err)
+
+ table := []struct {
+ headers map[string]string
+ status int
+ description string
+ }{
+ {
+ headers: make(map[string]string),
+ status: fiber.StatusUnauthorized,
+ description: "empty token",
+ },
+ {
+ headers: map[string]string{
+ "Authorization": authHeaderPrefix + "invalid_token",
+ },
+ status: fiber.StatusUnauthorized,
+ description: "invalid token",
+ },
+ {
+ headers: map[string]string{
+ "Authorization": authHeaderPrefix + invalidToken.AccessToken,
+ },
+ status: fiber.StatusUnauthorized,
+ description: "invalid user id",
+ },
+ }
+
+ server := httptest.NewServer(adaptor.FiberApp(suite.app))
+ e := httpexpect.Default(t, server.URL)
+
+ for _, tt := range table {
+ t.Run(tt.description, func(t *testing.T) {
+ // Act
+ resp := e.GET("/auth/userinfo").
+ WithHeaders(tt.headers).
+ Expect()
+
+ // Assert
+ resp.Status(tt.status)
+ })
+ }
+}
diff --git a/backend/internal/core/services/authentication.go b/backend/internal/core/services/authentication.go
index 357179c..0ba3222 100644
--- a/backend/internal/core/services/authentication.go
+++ b/backend/internal/core/services/authentication.go
@@ -95,3 +95,10 @@ func (s *AuthenticationService) GetPublicKey() interface{} {
func (s *AuthenticationService) GetAlgorithm() string {
return s.authManager.GetAlgorithm()
}
+
+func (s *AuthenticationService) GetUserInfo(
+ ctx context.Context,
+ token string,
+) (*ports.UserIdentityInfo, error) {
+ return s.authManager.GetUserInfo(ctx, token)
+}
diff --git a/backend/internal/core/services/authentication_test.go b/backend/internal/core/services/authentication_test.go
index 13bb908..96a2ab1 100644
--- a/backend/internal/core/services/authentication_test.go
+++ b/backend/internal/core/services/authentication_test.go
@@ -396,3 +396,38 @@ func (suite *AuthenticationServiceIntegrationTestSuite) TestRefreshTokenWithBadI
})
}
}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestGetUserInfo() {
+ // Arrange
+ t := suite.T()
+ toks, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ assert.NoError(t, err)
+ expectedUser := &ports.UserIdentityInfo{
+ ID: testUserId,
+ Email: testEmail,
+ Username: testUsername,
+ }
+
+ // Act
+ info, err := suite.svc.GetUserInfo(suite.ctx, toks.AccessToken)
+
+ // Assert
+ assert.NoError(t, err)
+ assert.Equal(t, expectedUser, info)
+}
+
+func (suite *AuthenticationServiceIntegrationTestSuite) TestGetUserInfoWithUnknownUser() {
+ // Arrange
+ t := suite.T()
+ unexistentUserId := uuid.New().String()
+ toks, err := suite.svc.CreateToken(suite.ctx, unexistentUserId)
+ assert.NoError(t, err)
+
+
+ // Act
+ info, err := suite.svc.GetUserInfo(suite.ctx, toks.AccessToken)
+
+ // Assert
+ assert.ErrorIs(t, err, ports.ErrUserNotFound)
+ assert.Nil(t, info)
+}
diff --git a/backend/internal/ports/auth.go b/backend/internal/ports/auth.go
index b227fc3..db60cc3 100644
--- a/backend/internal/ports/auth.go
+++ b/backend/internal/ports/auth.go
@@ -39,6 +39,7 @@ type AuthenticationManager interface {
RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error)
GetPublicKey() interface{}
GetAlgorithm() string
+ GetUserInfo(ctx context.Context, tok string) (*UserIdentityInfo, error)
}
type LocalIDPUserEntity struct {
@@ -59,4 +60,5 @@ type LocalIDPStorer interface {
) (*LocalIDPUserEntity, error)
DeleteUser(ctx context.Context, userId string) error
FindUserByUsername(ctx context.Context, username string) (*LocalIDPUserEntity, error)
+ FindUserById(ctx context.Context, userId string) (*LocalIDPUserEntity, error)
}
From 66dbe64f992df5e028fb34e9f6293b08af7cc93a Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Sun, 31 Mar 2024 09:34:20 -0300
Subject: [PATCH 19/91] improvement(#6): move create token to service
---
backend/internal/adapters/drivers/web/auth.go | 18 +++----------
.../adapters/drivers/web/auth_test.go | 17 +++++++------
.../internal/core/services/authentication.go | 25 +++++++++++--------
.../core/services/authentication_test.go | 11 ++++----
4 files changed, 34 insertions(+), 37 deletions(-)
diff --git a/backend/internal/adapters/drivers/web/auth.go b/backend/internal/adapters/drivers/web/auth.go
index cedee12..5fc6798 100644
--- a/backend/internal/adapters/drivers/web/auth.go
+++ b/backend/internal/adapters/drivers/web/auth.go
@@ -126,7 +126,7 @@ func (h *authHandler) Login(c *fiber.Ctx) error {
return err
}
- info, err := h.authService.AuthenticateUser(c.Context(), req.Username, req.Password)
+ token, err := h.authService.AuthenticateUser(c.Context(), req.Username, req.Password)
if err != nil {
if errors.Is(err, ports.ErrUserNotFound) || errors.Is(err, ports.ErrInvalidPassword) {
return c.Status(fiber.StatusUnauthorized).SendString("Invalid username or password")
@@ -134,11 +134,6 @@ func (h *authHandler) Login(c *fiber.Ctx) error {
return err
}
- token, err := h.authService.CreateToken(c.Context(), info.ID)
- if err != nil {
- return err
- }
-
return c.JSON(TokenResponse{
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
@@ -214,7 +209,7 @@ func (h *authHandler) RegisterUser(c *fiber.Ctx) error {
return err
}
- user, err := h.authService.CreateUser(
+ token, err := h.authService.CreateUser(
c.Context(),
&services.CreateUserRequest{
Username: req.Username,
@@ -229,11 +224,6 @@ func (h *authHandler) RegisterUser(c *fiber.Ctx) error {
return err
}
- token, err := h.authService.CreateToken(c.Context(), user.ID)
- if err != nil {
- return err
- }
-
return c.Status(fiber.StatusCreated).JSON(TokenResponse{
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
@@ -326,9 +316,9 @@ func (h *authHandler) UserInfo(c *fiber.Ctx) error {
}
resp := &UserInfoResponse{
- ID: info.ID,
+ ID: info.ID,
Username: info.Username,
- Email: info.Email,
+ Email: info.Email,
}
return c.JSON(resp)
diff --git a/backend/internal/adapters/drivers/web/auth_test.go b/backend/internal/adapters/drivers/web/auth_test.go
index 7990db8..bf7fc31 100644
--- a/backend/internal/adapters/drivers/web/auth_test.go
+++ b/backend/internal/adapters/drivers/web/auth_test.go
@@ -18,6 +18,7 @@ import (
"github.com/taldoflemis/brain.test/internal/adapters/driven/auth"
"github.com/taldoflemis/brain.test/internal/adapters/driven/postgres"
"github.com/taldoflemis/brain.test/internal/core/services"
+ "github.com/taldoflemis/brain.test/internal/ports"
testshelpers "github.com/taldoflemis/brain.test/test/helpers"
)
@@ -43,6 +44,7 @@ type AuthHandlerTestSuite struct {
ctx context.Context
pool *pgxpool.Pool
svc *services.AuthenticationService
+ idp ports.AuthenticationManager
}
func (suite *AuthHandlerTestSuite) SetupSuite() {
@@ -92,6 +94,7 @@ func (suite *AuthHandlerTestSuite) SetupSuite() {
suite.pgContainer = pgContainer
suite.pool = pool
suite.svc = authService
+ suite.idp = authManager
}
func (suite *AuthHandlerTestSuite) SetupTest() {
@@ -299,7 +302,7 @@ func (suite *AuthHandlerTestSuite) TestLoginWithWrongPassword() {
func (suite *AuthHandlerTestSuite) TestRefreshToken() {
// Arrange
t := suite.T()
- tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ tok, err := suite.idp.CreateToken(suite.ctx, testUserId)
assert.NoError(t, err)
assert.NotNil(t, tok)
@@ -363,7 +366,7 @@ func (suite *AuthHandlerTestSuite) TestRefreshTokenWithInvalidRefreshToken() {
func (suite *AuthHandlerTestSuite) TestUpdateUser() {
// Arrange
t := suite.T()
- tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ tok, err := suite.idp.CreateToken(suite.ctx, testUserId)
assert.NoError(t, err)
req := map[string]interface{}{
@@ -389,7 +392,7 @@ func (suite *AuthHandlerTestSuite) TestUpdateUser() {
func (suite *AuthHandlerTestSuite) TestUpdateUserWithInvalidInput() {
// Arrange
t := suite.T()
- tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ tok, err := suite.idp.CreateToken(suite.ctx, testUserId)
assert.NoError(t, err)
headers := map[string]string{
@@ -489,7 +492,7 @@ func (suite *AuthHandlerTestSuite) TestUpdateUserToUsernameThatAlreadyExists() {
)
assert.NoError(t, err)
- tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ tok, err := suite.idp.CreateToken(suite.ctx, testUserId)
assert.NoError(t, err)
req := map[string]interface{}{
@@ -514,7 +517,7 @@ func (suite *AuthHandlerTestSuite) TestUpdateUserToUsernameThatAlreadyExists() {
func (suite *AuthHandlerTestSuite) TestDeleteAccount() {
// Arrange
t := suite.T()
- tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ tok, err := suite.idp.CreateToken(suite.ctx, testUserId)
assert.NoError(t, err)
headers := map[string]string{
@@ -552,7 +555,7 @@ func (suite *AuthHandlerTestSuite) TestDeleteAccountWithoutAuthorization() {
func (suite *AuthHandlerTestSuite) TestUserInfo() {
// Arrange
t := suite.T()
- tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ tok, err := suite.idp.CreateToken(suite.ctx, testUserId)
assert.NoError(t, err)
headers := map[string]string{
@@ -578,7 +581,7 @@ func (suite *AuthHandlerTestSuite) TestUserInfo() {
func (suite *AuthHandlerTestSuite) TestUserInfoWithBadInput() {
// Arrange
t := suite.T()
- invalidToken, err := suite.svc.CreateToken(suite.ctx, uuid.New().String())
+ invalidToken, err := suite.idp.CreateToken(suite.ctx, uuid.New().String())
assert.NoError(t, err)
table := []struct {
diff --git a/backend/internal/core/services/authentication.go b/backend/internal/core/services/authentication.go
index 0ba3222..da21f2d 100644
--- a/backend/internal/core/services/authentication.go
+++ b/backend/internal/core/services/authentication.go
@@ -39,21 +39,31 @@ func NewAuthenticationService(
func (s *AuthenticationService) CreateUser(
ctx context.Context,
req *CreateUserRequest,
-) (*ports.UserIdentityInfo, error) {
+) (*ports.TokenResponse, error) {
err := s.validationService.Validate(req)
if err != nil {
s.logger.Error("failed to validate struct")
return nil, err
}
- return s.authManager.CreateUser(ctx, req.Username, req.Email, req.Password)
+ info, err := s.authManager.CreateUser(ctx, req.Username, req.Email, req.Password)
+ if err != nil {
+ return nil, err
+ }
+
+ return s.authManager.CreateToken(ctx, info.ID)
}
func (s *AuthenticationService) AuthenticateUser(
ctx context.Context,
username, password string,
-) (*ports.UserIdentityInfo, error) {
- return s.authManager.AuthenticateUser(ctx, username, password)
+) (*ports.TokenResponse, error) {
+ info, err := s.authManager.AuthenticateUser(ctx, username, password)
+ if err != nil {
+ return nil, err
+ }
+
+ return s.authManager.CreateToken(ctx, info.ID)
}
func (s *AuthenticationService) DeleteUser(ctx context.Context, userId string) error {
@@ -74,13 +84,6 @@ func (s *AuthenticationService) UpdateUser(
return s.authManager.UpdateUser(ctx, userId, req.Username, req.Password, req.Email)
}
-func (s *AuthenticationService) CreateToken(
- ctx context.Context,
- userId string,
-) (*ports.TokenResponse, error) {
- return s.authManager.CreateToken(ctx, userId)
-}
-
func (s *AuthenticationService) RefreshToken(
ctx context.Context,
refreshToken string,
diff --git a/backend/internal/core/services/authentication_test.go b/backend/internal/core/services/authentication_test.go
index 96a2ab1..101edce 100644
--- a/backend/internal/core/services/authentication_test.go
+++ b/backend/internal/core/services/authentication_test.go
@@ -37,6 +37,7 @@ type AuthenticationServiceIntegrationTestSuite struct {
ctx context.Context
pool *pgxpool.Pool
svc *services.AuthenticationService
+ idp ports.AuthenticationManager
}
func (suite *AuthenticationServiceIntegrationTestSuite) SetupSuite() {
@@ -67,6 +68,7 @@ func (suite *AuthenticationServiceIntegrationTestSuite) SetupSuite() {
suite.svc = svc
suite.pool = pool
+ suite.idp = adapter
}
func (suite *AuthenticationServiceIntegrationTestSuite) SetupTest() {
@@ -203,7 +205,7 @@ func (suite *AuthenticationServiceIntegrationTestSuite) TestCreateToken() {
t := suite.T()
// Act
- tokenResponse, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ tokenResponse, err := suite.idp.CreateToken(suite.ctx, testUserId)
// Assert
assert.NoError(t, err)
@@ -352,7 +354,7 @@ func (suite *AuthenticationServiceIntegrationTestSuite) TestUpdateUserWithBadInp
func (suite *AuthenticationServiceIntegrationTestSuite) TestRefreshToken() {
// Arrange
t := suite.T()
- tok, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ tok, err := suite.idp.CreateToken(suite.ctx, testUserId)
assert.NoError(t, err)
// Act
@@ -400,7 +402,7 @@ func (suite *AuthenticationServiceIntegrationTestSuite) TestRefreshTokenWithBadI
func (suite *AuthenticationServiceIntegrationTestSuite) TestGetUserInfo() {
// Arrange
t := suite.T()
- toks, err := suite.svc.CreateToken(suite.ctx, testUserId)
+ toks, err := suite.idp.CreateToken(suite.ctx, testUserId)
assert.NoError(t, err)
expectedUser := &ports.UserIdentityInfo{
ID: testUserId,
@@ -420,10 +422,9 @@ func (suite *AuthenticationServiceIntegrationTestSuite) TestGetUserInfoWithUnkno
// Arrange
t := suite.T()
unexistentUserId := uuid.New().String()
- toks, err := suite.svc.CreateToken(suite.ctx, unexistentUserId)
+ toks, err := suite.idp.CreateToken(suite.ctx, unexistentUserId)
assert.NoError(t, err)
-
// Act
info, err := suite.svc.GetUserInfo(suite.ctx, toks.AccessToken)
From fffd3b74d43d3839d1502ea4aa742347b94623b5 Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Tue, 16 Apr 2024 23:23:34 -0300
Subject: [PATCH 20/91] improvement(#20): setup shadcn
---
frontend/components.json | 17 ++
frontend/package-lock.json | 304 ++++++++++++++------------
frontend/package.json | 15 +-
frontend/src/app/globals.css | 33 ---
frontend/src/app/layout.tsx | 4 +-
frontend/src/app/page.tsx | 108 +--------
frontend/src/components/.gitKeep | 0
frontend/src/components/ui/button.tsx | 56 +++++
frontend/src/components/ui/card.tsx | 79 +++++++
frontend/src/components/ui/input.tsx | 25 +++
frontend/src/components/ui/label.tsx | 26 +++
frontend/src/lib/utils.ts | 6 +
frontend/src/styles/globals.css | 77 +++++++
frontend/tailwind.config.ts | 86 ++++++--
14 files changed, 540 insertions(+), 296 deletions(-)
create mode 100644 frontend/components.json
delete mode 100644 frontend/src/app/globals.css
delete mode 100644 frontend/src/components/.gitKeep
create mode 100644 frontend/src/components/ui/button.tsx
create mode 100644 frontend/src/components/ui/card.tsx
create mode 100644 frontend/src/components/ui/input.tsx
create mode 100644 frontend/src/components/ui/label.tsx
create mode 100644 frontend/src/lib/utils.ts
create mode 100644 frontend/src/styles/globals.css
diff --git a/frontend/components.json b/frontend/components.json
new file mode 100644
index 0000000..8c574b7
--- /dev/null
+++ b/frontend/components.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "src/app/globals.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils"
+ }
+}
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index af20d1d..3fd8a92 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,9 +8,16 @@
"name": "brain.test",
"version": "0.1.0",
"dependencies": {
+ "@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-slot": "^1.0.2",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.0",
+ "lucide-react": "^0.368.0",
"next": "14.1.3",
"react": "^18",
- "react-dom": "^18"
+ "react-dom": "^18",
+ "tailwind-merge": "^2.2.2",
+ "tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20",
@@ -37,7 +44,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
- "dev": true,
"engines": {
"node": ">=10"
},
@@ -49,7 +55,6 @@
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
"integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
- "dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -150,7 +155,6 @@
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
@@ -167,7 +171,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -179,7 +182,6 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -194,7 +196,6 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
- "dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@@ -208,7 +209,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -217,7 +217,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -225,14 +224,12 @@
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
- "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
- "dev": true
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
- "dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -391,7 +388,6 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
@@ -404,7 +400,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
"engines": {
"node": ">= 8"
}
@@ -413,7 +408,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
@@ -426,12 +420,92 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
+ "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz",
+ "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
+ "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-slot": "1.0.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
+ "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-compose-refs": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rushstack/eslint-patch": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz",
@@ -465,13 +539,13 @@
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
- "dev": true
+ "devOptional": true
},
"node_modules/@types/react": {
"version": "18.2.66",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.66.tgz",
"integrity": "sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -482,7 +556,7 @@
"version": "18.2.22",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz",
"integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"@types/react": "*"
}
@@ -491,7 +565,7 @@
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
- "dev": true
+ "devOptional": true
},
"node_modules/@typescript-eslint/parser": {
"version": "6.21.0",
@@ -667,7 +741,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -676,7 +749,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -690,14 +762,12 @@
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
- "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
- "dev": true
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -709,8 +779,7 @@
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
- "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
- "dev": true
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
},
"node_modules/argparse": {
"version": "2.0.1",
@@ -999,14 +1068,12 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
"engines": {
"node": ">=8"
},
@@ -1028,7 +1095,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
- "dev": true,
"dependencies": {
"fill-range": "^7.0.1"
},
@@ -1111,7 +1177,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
- "dev": true,
"engines": {
"node": ">= 6"
}
@@ -1155,7 +1220,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -1179,7 +1243,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -1187,16 +1250,42 @@
"node": ">= 6"
}
},
+ "node_modules/class-variance-authority": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz",
+ "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==",
+ "dependencies": {
+ "clsx": "2.0.0"
+ },
+ "funding": {
+ "url": "https://joebell.co.uk"
+ }
+ },
+ "node_modules/class-variance-authority/node_modules/clsx": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
+ "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
+ "node_modules/clsx": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
+ "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -1207,14 +1296,12 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
- "dev": true,
"engines": {
"node": ">= 6"
}
@@ -1229,7 +1316,6 @@
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
- "dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -1243,7 +1329,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true,
"bin": {
"cssesc": "bin/cssesc"
},
@@ -1255,7 +1340,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true
+ "devOptional": true
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -1332,8 +1417,7 @@
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
- "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
- "dev": true
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
},
"node_modules/dir-glob": {
"version": "3.0.1",
@@ -1350,8 +1434,7 @@
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
- "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
- "dev": true
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
},
"node_modules/doctrine": {
"version": "3.0.0",
@@ -1368,8 +1451,7 @@
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.707",
@@ -1380,8 +1462,7 @@
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/enhanced-resolve": {
"version": "5.16.0",
@@ -1987,7 +2068,6 @@
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
- "dev": true,
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@@ -2003,7 +2083,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -2027,7 +2106,6 @@
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
- "dev": true,
"dependencies": {
"reusify": "^1.0.4"
}
@@ -2048,7 +2126,6 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
- "dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -2105,7 +2182,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
- "dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
@@ -2140,7 +2216,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -2154,7 +2229,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -2238,7 +2312,6 @@
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
- "dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.5",
@@ -2260,7 +2333,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -2272,7 +2344,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -2281,7 +2352,6 @@
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
- "dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -2438,7 +2508,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -2557,7 +2626,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@@ -2597,7 +2665,6 @@
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
- "dev": true,
"dependencies": {
"hasown": "^2.0.0"
},
@@ -2624,7 +2691,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2645,7 +2711,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -2669,7 +2734,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
@@ -2705,7 +2769,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
"engines": {
"node": ">=0.12.0"
}
@@ -2871,8 +2934,7 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/iterator.prototype": {
"version": "1.1.2",
@@ -2891,7 +2953,6 @@
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
- "dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
@@ -2909,7 +2970,6 @@
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
"integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
- "dev": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -3020,7 +3080,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
- "dev": true,
"engines": {
"node": ">=10"
}
@@ -3028,8 +3087,7 @@
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/locate-path": {
"version": "6.0.0",
@@ -3067,16 +3125,22 @@
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
"integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
- "dev": true,
"engines": {
"node": "14 || >=16.14"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.368.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.368.0.tgz",
+ "integrity": "sha512-soryVrCjheZs8rbXKdINw9B8iPi5OajBJZMJ1HORig89ljcOcEokKKAgGbg3QWxSXel7JwHOfDFUdDHAKyUAMw==",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
"engines": {
"node": ">= 8"
}
@@ -3085,7 +3149,6 @@
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
- "dev": true,
"dependencies": {
"braces": "^3.0.2",
"picomatch": "^2.3.1"
@@ -3119,7 +3182,6 @@
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
- "dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
@@ -3134,7 +3196,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
- "dev": true,
"dependencies": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
@@ -3246,7 +3307,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3264,7 +3324,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3273,7 +3332,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
- "dev": true,
"engines": {
"node": ">= 6"
}
@@ -3478,7 +3536,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -3486,14 +3543,12 @@
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-scurry": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
- "dev": true,
"dependencies": {
"lru-cache": "^9.1.1 || ^10.0.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
@@ -3523,7 +3578,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
"engines": {
"node": ">=8.6"
},
@@ -3535,7 +3589,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3544,7 +3597,6 @@
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
- "dev": true,
"engines": {
"node": ">= 6"
}
@@ -3562,7 +3614,6 @@
"version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
"integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -3590,7 +3641,6 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
- "dev": true,
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
@@ -3607,7 +3657,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
- "dev": true,
"dependencies": {
"camelcase-css": "^2.0.1"
},
@@ -3626,7 +3675,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -3661,7 +3709,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
"integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
- "dev": true,
"engines": {
"node": ">=14"
},
@@ -3673,7 +3720,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
"integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
- "dev": true,
"dependencies": {
"postcss-selector-parser": "^6.0.11"
},
@@ -3692,7 +3738,6 @@
"version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
"integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
- "dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -3704,8 +3749,7 @@
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
- "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "dev": true
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/prelude-ls": {
"version": "1.2.1",
@@ -3740,7 +3784,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -3789,7 +3832,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
- "dev": true,
"dependencies": {
"pify": "^2.3.0"
}
@@ -3798,7 +3840,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@@ -3830,8 +3871,7 @@
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "dev": true
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.2",
@@ -3855,7 +3895,6 @@
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
- "dev": true,
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
@@ -3890,7 +3929,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
- "dev": true,
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
@@ -3935,7 +3973,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -4060,7 +4097,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -4072,7 +4108,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -4099,7 +4134,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
"engines": {
"node": ">=14"
},
@@ -4136,7 +4170,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
@@ -4154,7 +4187,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -4167,14 +4199,12 @@
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/string-width/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -4186,7 +4216,6 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -4266,7 +4295,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -4279,7 +4307,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -4334,7 +4361,6 @@
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
- "dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
@@ -4368,7 +4394,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true,
"engines": {
"node": ">= 0.4"
},
@@ -4376,11 +4401,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tailwind-merge": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.2.tgz",
+ "integrity": "sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==",
+ "dependencies": {
+ "@babel/runtime": "^7.24.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
"integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
- "dev": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -4413,6 +4449,14 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -4432,7 +4476,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
- "dev": true,
"dependencies": {
"any-promise": "^1.0.0"
}
@@ -4441,7 +4484,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
- "dev": true,
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
@@ -4453,7 +4495,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
@@ -4476,8 +4517,7 @@
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
- "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
- "dev": true
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
@@ -4669,14 +4709,12 @@
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
@@ -4770,7 +4808,6 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
@@ -4788,7 +4825,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -4804,14 +4840,12 @@
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -4825,7 +4859,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -4837,7 +4870,6 @@
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -4849,7 +4881,6 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -4876,7 +4907,6 @@
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
"integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==",
- "dev": true,
"bin": {
"yaml": "bin.mjs"
},
diff --git a/frontend/package.json b/frontend/package.json
index 0a4d8fd..a4ad58f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,19 +10,26 @@
"type-check": "tsc --pretty --noEmit"
},
"dependencies": {
+ "@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-slot": "^1.0.2",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.0",
+ "lucide-react": "^0.368.0",
+ "next": "14.1.3",
"react": "^18",
"react-dom": "^18",
- "next": "14.1.3"
+ "tailwind-merge": "^2.2.2",
+ "tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
- "typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
+ "eslint": "^8",
+ "eslint-config-next": "14.1.3",
"postcss": "^8",
"tailwindcss": "^3.3.0",
- "eslint": "^8",
- "eslint-config-next": "14.1.3"
+ "typescript": "^5"
}
}
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
deleted file mode 100644
index 875c01e..0000000
--- a/frontend/src/app/globals.css
+++ /dev/null
@@ -1,33 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-:root {
- --foreground-rgb: 0, 0, 0;
- --background-start-rgb: 214, 219, 220;
- --background-end-rgb: 255, 255, 255;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --foreground-rgb: 255, 255, 255;
- --background-start-rgb: 0, 0, 0;
- --background-end-rgb: 0, 0, 0;
- }
-}
-
-body {
- color: rgb(var(--foreground-rgb));
- background: linear-gradient(
- to bottom,
- transparent,
- rgb(var(--background-end-rgb))
- )
- rgb(var(--background-start-rgb));
-}
-
-@layer utilities {
- .text-balance {
- text-wrap: balance;
- }
-}
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 3314e47..ec576c8 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -1,11 +1,11 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
-import "./globals.css";
+import "../styles/globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
- title: "Create Next App",
+ title: "Brain Test",
description: "Generated by create next app",
};
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index b81507d..1d0f29d 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -1,113 +1,7 @@
-import Image from "next/image";
export default function Home() {
return (
-
-
-
- Get started by editing
- src/app/page.tsx
-
-
-
-
-
-
-
-
-
+
);
}
diff --git a/frontend/src/components/.gitKeep b/frontend/src/components/.gitKeep
deleted file mode 100644
index e69de29..0000000
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
new file mode 100644
index 0000000..0ba4277
--- /dev/null
+++ b/frontend/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx
new file mode 100644
index 0000000..afa13ec
--- /dev/null
+++ b/frontend/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx
new file mode 100644
index 0000000..677d05f
--- /dev/null
+++ b/frontend/src/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx
new file mode 100644
index 0000000..5341821
--- /dev/null
+++ b/frontend/src/components/ui/label.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
new file mode 100644
index 0000000..d084cca
--- /dev/null
+++ b/frontend/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css
new file mode 100644
index 0000000..bc724a1
--- /dev/null
+++ b/frontend/src/styles/globals.css
@@ -0,0 +1,77 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
index e9a0944..84287e8 100644
--- a/frontend/tailwind.config.ts
+++ b/frontend/tailwind.config.ts
@@ -1,20 +1,80 @@
-import type { Config } from "tailwindcss";
+import type { Config } from "tailwindcss"
-const config: Config = {
+const config = {
+ darkMode: ["class"],
content: [
- "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
- "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
- "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
- ],
+ './pages/**/*.{ts,tsx}',
+ './components/**/*.{ts,tsx}',
+ './app/**/*.{ts,tsx}',
+ './src/**/*.{ts,tsx}',
+ ],
+ prefix: "",
theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
extend: {
- backgroundImage: {
- "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
- "gradient-conic":
- "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "accordion-down": {
+ from: { height: "0" },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: "0" },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
},
},
},
- plugins: [],
-};
-export default config;
+ plugins: [require("tailwindcss-animate")],
+} satisfies Config
+
+export default config
\ No newline at end of file
From f44022b137bd647cd9080faf2c2d0b2cd0fc6c80 Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Wed, 17 Apr 2024 01:42:24 -0300
Subject: [PATCH 21/91] improvement(#20): add shadcn form component
---
frontend/package-lock.json | 36 +++++-
frontend/package.json | 5 +-
frontend/src/components/ui/form.tsx | 176 ++++++++++++++++++++++++++++
3 files changed, 215 insertions(+), 2 deletions(-)
create mode 100644 frontend/src/components/ui/form.tsx
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 3fd8a92..a31ad68 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,6 +8,7 @@
"name": "brain.test",
"version": "0.1.0",
"dependencies": {
+ "@hookform/resolvers": "^3.3.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
@@ -16,8 +17,10 @@
"next": "14.1.3",
"react": "^18",
"react-dom": "^18",
+ "react-hook-form": "^7.51.3",
"tailwind-merge": "^2.2.2",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20",
@@ -118,6 +121,14 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz",
+ "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -3822,6 +3833,21 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.51.3",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz",
+ "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==",
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -4925,6 +4951,14 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zod": {
+ "version": "3.22.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
+ "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index a4ad58f..b59fbba 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,7 @@
"type-check": "tsc --pretty --noEmit"
},
"dependencies": {
+ "@hookform/resolvers": "^3.3.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
@@ -18,8 +19,10 @@
"next": "14.1.3",
"react": "^18",
"react-dom": "^18",
+ "react-hook-form": "^7.51.3",
"tailwind-merge": "^2.2.2",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20",
diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx
new file mode 100644
index 0000000..4603f8b
--- /dev/null
+++ b/frontend/src/components/ui/form.tsx
@@ -0,0 +1,176 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState, formState } = useFormContext()
+
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+})
+FormItem.displayName = "FormItem"
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+})
+FormDescription.displayName = "FormDescription"
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+})
+FormMessage.displayName = "FormMessage"
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
From 7ad9361caba26b041e94b0cb6ef0fe25ed70a5b8 Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Wed, 17 Apr 2024 01:43:23 -0300
Subject: [PATCH 22/91] improvement(#20): add shadcn button variant
---
frontend/src/components/ui/button.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
index 0ba4277..209db02 100644
--- a/frontend/src/components/ui/button.tsx
+++ b/frontend/src/components/ui/button.tsx
@@ -23,6 +23,7 @@ const buttonVariants = cva(
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
+ full: "h-10 px-4 py-2 w-full",
icon: "h-10 w-10",
},
},
From 5c67e68a5a061237113880fec691a2d9622299b7 Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Wed, 17 Apr 2024 01:44:00 -0300
Subject: [PATCH 23/91] feature(#20): add login form component
---
.../components/pages/signin/login-form.tsx | 90 +++++++++++++++++++
1 file changed, 90 insertions(+)
create mode 100644 frontend/src/components/pages/signin/login-form.tsx
diff --git a/frontend/src/components/pages/signin/login-form.tsx b/frontend/src/components/pages/signin/login-form.tsx
new file mode 100644
index 0000000..5fe9cfd
--- /dev/null
+++ b/frontend/src/components/pages/signin/login-form.tsx
@@ -0,0 +1,90 @@
+"use client"
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import Link from "next/link";
+
+const formSchema = z.object({
+ email: z.string().email({ message: "Insira um email valido" }),
+ password: z.string(),
+});
+
+type LoginFormSchema = z.infer;
+
+function LoginForm() {
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: "",
+ password: "",
+ },
+ });
+
+ function onSubmit(values: LoginFormSchema) {
+ console.log(values);
+ }
+
+ return (
+
+
+ );
+}
+
+export default LoginForm;
From 7d86f55615bfd68dc32d439afa59ae952b40cc81 Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Wed, 17 Apr 2024 01:44:34 -0300
Subject: [PATCH 24/91] feature(#20): add login section component
---
frontend/public/brain-logo-black.svg | 19 ++++
frontend/public/brain-logo-white.svg | 19 ++++
frontend/public/brain-surface.svg | 92 +++++++++++++++++++
.../components/pages/signin/login-section.tsx | 36 ++++++++
4 files changed, 166 insertions(+)
create mode 100644 frontend/public/brain-logo-black.svg
create mode 100644 frontend/public/brain-logo-white.svg
create mode 100644 frontend/public/brain-surface.svg
create mode 100644 frontend/src/components/pages/signin/login-section.tsx
diff --git a/frontend/public/brain-logo-black.svg b/frontend/public/brain-logo-black.svg
new file mode 100644
index 0000000..373135b
--- /dev/null
+++ b/frontend/public/brain-logo-black.svg
@@ -0,0 +1,19 @@
+
diff --git a/frontend/public/brain-logo-white.svg b/frontend/public/brain-logo-white.svg
new file mode 100644
index 0000000..96b1ab8
--- /dev/null
+++ b/frontend/public/brain-logo-white.svg
@@ -0,0 +1,19 @@
+
diff --git a/frontend/public/brain-surface.svg b/frontend/public/brain-surface.svg
new file mode 100644
index 0000000..b866ac7
--- /dev/null
+++ b/frontend/public/brain-surface.svg
@@ -0,0 +1,92 @@
+
diff --git a/frontend/src/components/pages/signin/login-section.tsx b/frontend/src/components/pages/signin/login-section.tsx
new file mode 100644
index 0000000..78775f6
--- /dev/null
+++ b/frontend/src/components/pages/signin/login-section.tsx
@@ -0,0 +1,36 @@
+import Image from "next/image";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+import LoginForm from "./login-form";
+
+function LoginSection() {
+ return (
+
+
+
+
+ Logue em uma conta
+
+
+ Digite suas credenciais
+
+
+
+
+ Nao tem uma conta ainda?
+
+
+
+ );
+}
+
+export default LoginSection;
From 3c3489b9989f00c38a0d7bb50298afc7aca68c7d Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Wed, 17 Apr 2024 01:44:57 -0300
Subject: [PATCH 25/91] feature(#20): add login page
---
frontend/src/app/(auth)/signin/page.tsx | 17 +++++++++++++++++
frontend/src/components/common/.gitKeep | 0
frontend/src/components/layout/.gitKeep | 0
frontend/src/components/pages/.gitKeep | 0
4 files changed, 17 insertions(+)
create mode 100644 frontend/src/app/(auth)/signin/page.tsx
create mode 100644 frontend/src/components/common/.gitKeep
create mode 100644 frontend/src/components/layout/.gitKeep
create mode 100644 frontend/src/components/pages/.gitKeep
diff --git a/frontend/src/app/(auth)/signin/page.tsx b/frontend/src/app/(auth)/signin/page.tsx
new file mode 100644
index 0000000..3dcada4
--- /dev/null
+++ b/frontend/src/app/(auth)/signin/page.tsx
@@ -0,0 +1,17 @@
+import type { Metadata } from "next";
+
+import LoginSection from "@/components/pages/signin/login-section";
+
+export const metadata: Metadata = {
+ title: "Brain Test • Sign in",
+ description: "Generated by create next app",
+};
+
+export default function SignIn() {
+ return (
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/common/.gitKeep b/frontend/src/components/common/.gitKeep
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/components/layout/.gitKeep b/frontend/src/components/layout/.gitKeep
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/components/pages/.gitKeep b/frontend/src/components/pages/.gitKeep
new file mode 100644
index 0000000..e69de29
From b0456e5fc2c9aac61e31a59e80526ef1b21e5b00 Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Wed, 17 Apr 2024 06:41:59 -0300
Subject: [PATCH 26/91] fix(#20): broken generate connection str
---
backend/internal/adapters/driven/postgres/connection.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/backend/internal/adapters/driven/postgres/connection.go b/backend/internal/adapters/driven/postgres/connection.go
index 93894f4..c38aa7d 100644
--- a/backend/internal/adapters/driven/postgres/connection.go
+++ b/backend/internal/adapters/driven/postgres/connection.go
@@ -23,6 +23,7 @@ func NewConfig(user, password, domain, database string, port int) *Config {
Password: password,
Host: domain,
Port: port,
+ Database: database,
}
}
@@ -61,7 +62,7 @@ func NewPool(connStr string) (*pgxpool.Pool, error) {
func GenerateConnectionString(cfg *Config) string {
str := fmt.Sprintf(
- "postgres://%s:%s@%s:%d/%s&sslmode=disable",
+ "postgres://%s:%s@%s:%d/%s?sslmode=disable",
cfg.User,
cfg.Password,
cfg.Host,
From 2d90679556ff1411fe9fde44e8783f4468cc49dc Mon Sep 17 00:00:00 2001
From: Said Rodrigues
Date: Wed, 17 Apr 2024 06:42:11 -0300
Subject: [PATCH 27/91] feature(#20): add migration script
---
backend/Taskfile.yaml | 5 +++++
backend/cmd/migrate/main.go | 25 +++++++++++++++++++++++++
backend/config.json | 4 ++--
3 files changed, 32 insertions(+), 2 deletions(-)
create mode 100644 backend/cmd/migrate/main.go
diff --git a/backend/Taskfile.yaml b/backend/Taskfile.yaml
index d970d90..dd78b1d 100644
--- a/backend/Taskfile.yaml
+++ b/backend/Taskfile.yaml
@@ -62,3 +62,8 @@ tasks:
desc: "Check for code common vulnerabilities"
cmds:
- govulncheck ./...
+
+ migrate:
+ desc: "Migrate postgres"
+ cmds:
+ - go run ./cmd/migrate/
diff --git a/backend/cmd/migrate/main.go b/backend/cmd/migrate/main.go
new file mode 100644
index 0000000..55876e6
--- /dev/null
+++ b/backend/cmd/migrate/main.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+ "log"
+
+ "github.com/taldoflemis/brain.test/config"
+ "github.com/taldoflemis/brain.test/internal/adapters/driven/postgres"
+)
+
+func main() {
+ koanf := config.NewKoanfson()
+ err := koanf.LoadFromJSON("config.json")
+ if err != nil {
+ log.Fatal(err)
+ }
+ pgCfg, err := config.NewPostgresConfig()
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Print(pgCfg.Database)
+
+ connStr := postgres.GenerateConnectionString(pgCfg)
+ log.Print(connStr)
+ postgres.Migrate(connStr, "internal/adapters/driven/postgres/migrations/")
+}
diff --git a/backend/config.json b/backend/config.json
index a2c8e97..03ac5ae 100644
--- a/backend/config.json
+++ b/backend/config.json
@@ -19,8 +19,8 @@
"postgres": {
"host": "localhost",
"port": 5432,
- "user": "postgres",
+ "user": "adminson",
"password": "password",
- "database": "postgres"
+ "database": "brain"
}
}
From 85e5aa9456070c3e84b486963acb649abd20f4e1 Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Wed, 17 Apr 2024 16:11:05 -0300
Subject: [PATCH 28/91] improvement(#20): add prettier
---
frontend/.prettierignore | 4 ++++
frontend/package-lock.json | 16 ++++++++++++++++
frontend/package.json | 1 +
frontend/prettierrc.json | 6 ++++++
4 files changed, 27 insertions(+)
create mode 100644 frontend/.prettierignore
create mode 100644 frontend/prettierrc.json
diff --git a/frontend/.prettierignore b/frontend/.prettierignore
new file mode 100644
index 0000000..cfa89a0
--- /dev/null
+++ b/frontend/.prettierignore
@@ -0,0 +1,4 @@
+node_modules
+build
+coverage
+.next
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index a31ad68..2b2bdf1 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -30,6 +30,7 @@
"eslint": "^8",
"eslint-config-next": "14.1.3",
"postcss": "^8",
+ "prettier": "3.2.5",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
@@ -3771,6 +3772,21 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
+ "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index b59fbba..8a26bce 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -32,6 +32,7 @@
"eslint": "^8",
"eslint-config-next": "14.1.3",
"postcss": "^8",
+ "prettier": "3.2.5",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
diff --git a/frontend/prettierrc.json b/frontend/prettierrc.json
new file mode 100644
index 0000000..f0eb61e
--- /dev/null
+++ b/frontend/prettierrc.json
@@ -0,0 +1,6 @@
+{
+ "trailingComma": "es5",
+ "tabWidth": 2,
+ "semi": true,
+ "singleQuote": false
+}
From b7680ff35c5f0e14cdaada6f9412aa3db0971787 Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Wed, 17 Apr 2024 16:11:20 -0300
Subject: [PATCH 29/91] improvement(#20): add prettier tasks
---
frontend/Taskfile.yaml | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/frontend/Taskfile.yaml b/frontend/Taskfile.yaml
index 80c0b6e..440e9b2 100644
--- a/frontend/Taskfile.yaml
+++ b/frontend/Taskfile.yaml
@@ -6,6 +6,16 @@ tasks:
cmds:
- npm run lint
+ format:
+ desc: "Format"
+ cmds:
+ - npx prettier . --write
+
+ check-format:
+ desc: "Check format"
+ cmds:
+ - npx prettier . --check
+
type-check:
desc: "Type-check"
cmds:
From e37aea08f7d50d3249b2e10d7ea0274b51dc9efd Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Wed, 17 Apr 2024 16:12:13 -0300
Subject: [PATCH 30/91] cleanup(#20): format code
---
frontend/components.json | 2 +-
frontend/next.config.mjs | 2 +-
frontend/src/app/page.tsx | 6 +-
.../components/pages/signin/login-form.tsx | 2 +-
frontend/src/components/ui/button.tsx | 26 ++---
frontend/src/components/ui/card.tsx | 41 ++++---
frontend/src/components/ui/form.tsx | 101 +++++++++---------
frontend/src/components/ui/input.tsx | 16 +--
frontend/src/components/ui/label.tsx | 20 ++--
frontend/src/lib/utils.ts | 6 +-
frontend/tailwind.config.ts | 16 +--
11 files changed, 121 insertions(+), 117 deletions(-)
diff --git a/frontend/components.json b/frontend/components.json
index 8c574b7..7559f63 100644
--- a/frontend/components.json
+++ b/frontend/components.json
@@ -14,4 +14,4 @@
"components": "@/components",
"utils": "@/lib/utils"
}
-}
\ No newline at end of file
+}
diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs
index eab71bb..8c8bab6 100644
--- a/frontend/next.config.mjs
+++ b/frontend/next.config.mjs
@@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
- output: "standalone",
+ output: "standalone",
};
export default nextConfig;
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index 1d0f29d..75c1b1d 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -1,7 +1,3 @@
-
export default function Home() {
- return (
-
-
- );
+ return ;
}
diff --git a/frontend/src/components/pages/signin/login-form.tsx b/frontend/src/components/pages/signin/login-form.tsx
index 5fe9cfd..94e1ea5 100644
--- a/frontend/src/components/pages/signin/login-form.tsx
+++ b/frontend/src/components/pages/signin/login-form.tsx
@@ -1,4 +1,4 @@
-"use client"
+"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
index 209db02..e546d15 100644
--- a/frontend/src/components/ui/button.tsx
+++ b/frontend/src/components/ui/button.tsx
@@ -1,8 +1,8 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
@@ -31,27 +31,27 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
- }
-)
+ },
+);
export interface ButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
- asChild?: boolean
+ asChild?: boolean;
}
const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button"
+ const Comp = asChild ? Slot : "button";
return (
- )
- }
-)
-Button.displayName = "Button"
+ );
+ },
+);
+Button.displayName = "Button";
-export { Button, buttonVariants }
+export { Button, buttonVariants };
diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx
index afa13ec..dc3b01d 100644
--- a/frontend/src/components/ui/card.tsx
+++ b/frontend/src/components/ui/card.tsx
@@ -1,6 +1,6 @@
-import * as React from "react"
+import * as React from "react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
- className
+ className,
)}
{...props}
/>
-))
-Card.displayName = "Card"
+));
+Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
-))
-CardHeader.displayName = "CardHeader"
+));
+CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
@@ -37,12 +37,12 @@ const CardTitle = React.forwardRef<
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
- className
+ className,
)}
{...props}
/>
-))
-CardTitle.displayName = "CardTitle"
+));
+CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
@@ -53,16 +53,16 @@ const CardDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
-))
-CardDescription.displayName = "CardDescription"
+));
+CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
-))
-CardContent.displayName = "CardContent"
+));
+CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
@@ -73,7 +73,14 @@ const CardFooter = React.forwardRef<
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
-))
-CardFooter.displayName = "CardFooter"
+));
+CardFooter.displayName = "CardFooter";
-export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
+};
diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx
index 4603f8b..497718a 100644
--- a/frontend/src/components/ui/form.tsx
+++ b/frontend/src/components/ui/form.tsx
@@ -1,6 +1,6 @@
-import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
-import { Slot } from "@radix-ui/react-slot"
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { Slot } from "@radix-ui/react-slot";
import {
Controller,
ControllerProps,
@@ -8,27 +8,27 @@ import {
FieldValues,
FormProvider,
useFormContext,
-} from "react-hook-form"
+} from "react-hook-form";
-import { cn } from "@/lib/utils"
-import { Label } from "@/components/ui/label"
+import { cn } from "@/lib/utils";
+import { Label } from "@/components/ui/label";
-const Form = FormProvider
+const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath
+ TName extends FieldPath = FieldPath,
> = {
- name: TName
-}
+ name: TName;
+};
const FormFieldContext = React.createContext(
- {} as FormFieldContextValue
-)
+ {} as FormFieldContextValue,
+);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath
+ TName extends FieldPath = FieldPath,
>({
...props
}: ControllerProps) => {
@@ -36,21 +36,21 @@ const FormField = <
- )
-}
+ );
+};
const useFormField = () => {
- const fieldContext = React.useContext(FormFieldContext)
- const itemContext = React.useContext(FormItemContext)
- const { getFieldState, formState } = useFormContext()
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
- const fieldState = getFieldState(fieldContext.name, formState)
+ const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
- throw new Error("useFormField should be used within ")
+ throw new Error("useFormField should be used within ");
}
- const { id } = itemContext
+ const { id } = itemContext;
return {
id,
@@ -59,36 +59,36 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
- }
-}
+ };
+};
type FormItemContextValue = {
- id: string
-}
+ id: string;
+};
const FormItemContext = React.createContext(
- {} as FormItemContextValue
-)
+ {} as FormItemContextValue,
+);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
- const id = React.useId()
+ const id = React.useId();
return (
- )
-})
-FormItem.displayName = "FormItem"
+ );
+});
+FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => {
- const { error, formItemId } = useFormField()
+ const { error, formItemId } = useFormField();
return (
- )
-})
-FormLabel.displayName = "FormLabel"
+ );
+});
+FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ ...props }, ref) => {
- const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+ const { error, formItemId, formDescriptionId, formMessageId } =
+ useFormField();
return (
- )
-})
-FormControl.displayName = "FormControl"
+ );
+});
+FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
- const { formDescriptionId } = useFormField()
+ const { formDescriptionId } = useFormField();
return (
- )
-})
-FormDescription.displayName = "FormDescription"
+ );
+});
+FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, children, ...props }, ref) => {
- const { error, formMessageId } = useFormField()
- const body = error ? String(error?.message) : children
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
if (!body) {
- return null
+ return null;
}
return (
@@ -160,9 +161,9 @@ const FormMessage = React.forwardRef<
>
{body}
- )
-})
-FormMessage.displayName = "FormMessage"
+ );
+});
+FormMessage.displayName = "FormMessage";
export {
useFormField,
@@ -173,4 +174,4 @@ export {
FormDescription,
FormMessage,
FormField,
-}
+};
diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx
index 677d05f..9d631e7 100644
--- a/frontend/src/components/ui/input.tsx
+++ b/frontend/src/components/ui/input.tsx
@@ -1,6 +1,6 @@
-import * as React from "react"
+import * as React from "react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes {}
@@ -12,14 +12,14 @@ const Input = React.forwardRef(
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
- className
+ className,
)}
ref={ref}
{...props}
/>
- )
- }
-)
-Input.displayName = "Input"
+ );
+ },
+);
+Input.displayName = "Input";
-export { Input }
+export { Input };
diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx
index 5341821..84f8b0c 100644
--- a/frontend/src/components/ui/label.tsx
+++ b/frontend/src/components/ui/label.tsx
@@ -1,14 +1,14 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const labelVariants = cva(
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
-)
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+);
const Label = React.forwardRef<
React.ElementRef,
@@ -20,7 +20,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)}
{...props}
/>
-))
-Label.displayName = LabelPrimitive.Root.displayName
+));
+Label.displayName = LabelPrimitive.Root.displayName;
-export { Label }
+export { Label };
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
index d084cca..365058c 100644
--- a/frontend/src/lib/utils.ts
+++ b/frontend/src/lib/utils.ts
@@ -1,6 +1,6 @@
-import { type ClassValue, clsx } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
}
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
index 84287e8..41668a3 100644
--- a/frontend/tailwind.config.ts
+++ b/frontend/tailwind.config.ts
@@ -1,13 +1,13 @@
-import type { Config } from "tailwindcss"
+import type { Config } from "tailwindcss";
const config = {
darkMode: ["class"],
content: [
- './pages/**/*.{ts,tsx}',
- './components/**/*.{ts,tsx}',
- './app/**/*.{ts,tsx}',
- './src/**/*.{ts,tsx}',
- ],
+ "./pages/**/*.{ts,tsx}",
+ "./components/**/*.{ts,tsx}",
+ "./app/**/*.{ts,tsx}",
+ "./src/**/*.{ts,tsx}",
+ ],
prefix: "",
theme: {
container: {
@@ -75,6 +75,6 @@ const config = {
},
},
plugins: [require("tailwindcss-animate")],
-} satisfies Config
+} satisfies Config;
-export default config
\ No newline at end of file
+export default config;
From fa72391a39edb45f077a991b90638a84a76b99db Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Wed, 17 Apr 2024 16:28:01 -0300
Subject: [PATCH 31/91] improvement(#20): add formating tast on CI
---
.github/workflows/node-ci.yaml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/node-ci.yaml b/.github/workflows/node-ci.yaml
index 2a8dbc7..843c5c4 100644
--- a/.github/workflows/node-ci.yaml
+++ b/.github/workflows/node-ci.yaml
@@ -38,6 +38,9 @@ jobs:
- name: Check lint
run: task lint
+ - name: Check format
+ run: task check-format
+
- name: Check types
run: task type-check
From cac0b35215ba6ab5fa4dc4aef34853c5d7f9b657 Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Thu, 18 Apr 2024 11:46:29 -0300
Subject: [PATCH 32/91] improvement(#20): Add new color scheme
---
...ain-logo-black.svg => brain-logo-blue.svg} | 20 ++----------
frontend/src/styles/globals.css | 32 +++++++++----------
2 files changed, 19 insertions(+), 33 deletions(-)
rename frontend/public/{brain-logo-black.svg => brain-logo-blue.svg} (93%)
diff --git a/frontend/public/brain-logo-black.svg b/frontend/public/brain-logo-blue.svg
similarity index 93%
rename from frontend/public/brain-logo-black.svg
rename to frontend/public/brain-logo-blue.svg
index 373135b..48252b1 100644
--- a/frontend/public/brain-logo-black.svg
+++ b/frontend/public/brain-logo-blue.svg
@@ -1,19 +1,5 @@
diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css
index bc724a1..40f2b5b 100644
--- a/frontend/src/styles/globals.css
+++ b/frontend/src/styles/globals.css
@@ -5,7 +5,7 @@
@layer base {
:root {
--background: 0 0% 100%;
- --foreground: 222.2 84% 4.9%;
+ --foreground: 229 19% 23%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
@@ -13,7 +13,7 @@
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
- --primary: 222.2 47.4% 11.2%;
+ --primary: 222 74% 60%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
@@ -25,7 +25,7 @@
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
- --destructive: 0 84.2% 60.2%;
+ --destructive: 359 68% 60%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
@@ -36,17 +36,17 @@
}
.dark {
- --background: 222.2 84% 4.9%;
- --foreground: 210 40% 98%;
+ --background: 229 19% 15%;
+ --foreground: 227 70% 87%;
- --card: 222.2 84% 4.9%;
- --card-foreground: 210 40% 98%;
+ --card: 229 19% 15%;
+ --card-foreground: 227 70% 87%;
- --popover: 222.2 84% 4.9%;
- --popover-foreground: 210 40% 98%;
+ --popover: 229 19% 15%;
+ --popover-foreground: 227 70% 87%;
- --primary: 210 40% 98%;
- --primary-foreground: 222.2 47.4% 11.2%;
+ --primary: 222 74% 74%;
+ --primary-foreground: 227 70% 95%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
@@ -57,12 +57,12 @@
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
- --destructive: 0 62.8% 30.6%;
+ --destructive: 359 68% 60%;
--destructive-foreground: 210 40% 98%;
-
- --border: 217.2 32.6% 17.5%;
- --input: 217.2 32.6% 17.5%;
- --ring: 212.7 26.8% 83.9%;
+
+ --border: 227 17% 58%;
+ --input: 227 17% 58%;
+ --ring: 227 17% 58%;
}
}
From df6f178765014b55dd1be614dd35e55baca85e38 Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Thu, 18 Apr 2024 11:49:26 -0300
Subject: [PATCH 33/91] cleanup(#20): format code
---
frontend/src/app/(auth)/{signin => sign-in}/page.tsx | 0
frontend/src/styles/globals.css | 2 +-
2 files changed, 1 insertion(+), 1 deletion(-)
rename frontend/src/app/(auth)/{signin => sign-in}/page.tsx (100%)
diff --git a/frontend/src/app/(auth)/signin/page.tsx b/frontend/src/app/(auth)/sign-in/page.tsx
similarity index 100%
rename from frontend/src/app/(auth)/signin/page.tsx
rename to frontend/src/app/(auth)/sign-in/page.tsx
diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css
index 40f2b5b..630ca9f 100644
--- a/frontend/src/styles/globals.css
+++ b/frontend/src/styles/globals.css
@@ -59,7 +59,7 @@
--destructive: 359 68% 60%;
--destructive-foreground: 210 40% 98%;
-
+
--border: 227 17% 58%;
--input: 227 17% 58%;
--ring: 227 17% 58%;
From b511364ac492345144b624490614b4f41f94e283 Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Thu, 18 Apr 2024 12:52:57 -0300
Subject: [PATCH 34/91] feature(#22): add register form, register section
---
.../components/pages/signup/register-form.tsx | 141 ++++++++++++++++++
.../pages/signup/register-section.tsx | 36 +++++
2 files changed, 177 insertions(+)
create mode 100644 frontend/src/components/pages/signup/register-form.tsx
create mode 100644 frontend/src/components/pages/signup/register-section.tsx
diff --git a/frontend/src/components/pages/signup/register-form.tsx b/frontend/src/components/pages/signup/register-form.tsx
new file mode 100644
index 0000000..bc392f9
--- /dev/null
+++ b/frontend/src/components/pages/signup/register-form.tsx
@@ -0,0 +1,141 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+
+const formSchema = z.object({
+ username: z
+ .string()
+ .min(5, { message: "Nome deve ter conter 5 ou mais caracteres" })
+ .max(50, { message: "Nome deve ter no maximo 50 caracteres" }),
+ email: z.string().email({ message: "Insira um email valido" }),
+ password: z
+ .string()
+ .min(5, { message: "Senha deve conter 5 ou mais caracteres" }),
+ confirm: z
+ .string()
+ .min(5, { message: "Senha deve conter 5 ou mais caracteres" }),
+});
+
+type RegisterFormSchema = z.infer;
+
+function RegisterForm() {
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ username: "",
+ email: "",
+ password: "",
+ confirm: "",
+ },
+ });
+
+ function onSubmit(values: RegisterFormSchema) {
+ if (values.password !== values.confirm) {
+ form.setError("confirm", {
+ type: "validate",
+ message: "Senhas distintas",
+ });
+ return;
+ }
+
+ console.log(values);
+ }
+
+ return (
+
+
+ );
+}
+
+export default RegisterForm;
diff --git a/frontend/src/components/pages/signup/register-section.tsx b/frontend/src/components/pages/signup/register-section.tsx
new file mode 100644
index 0000000..16b6bf6
--- /dev/null
+++ b/frontend/src/components/pages/signup/register-section.tsx
@@ -0,0 +1,36 @@
+import Image from "next/image";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+import RegisterForm from "./register-form";
+
+function RegisterSection() {
+ return (
+
+
+
+
+ Registrar uma conta
+
+
+ Digite suas credenciais
+
+
+
+
+ Ja possui uma conta?
+
+
+
+ );
+}
+
+export default RegisterSection;
From 2e74a42312ae527f1326fb97a3b4af2e3498e8fa Mon Sep 17 00:00:00 2001
From: Lu1z-Gust4v0
Date: Thu, 18 Apr 2024 12:54:02 -0300
Subject: [PATCH 35/91] improvement(#22): Update login section, add register
page
---
frontend/public/brain-logo-white.svg | 21 +++----------------
frontend/src/app/(auth)/sign-in/page.tsx | 1 -
frontend/src/app/(auth)/sign-up/page.tsx | 16 ++++++++++++++
.../components/pages/signin/login-section.tsx | 6 +++---
4 files changed, 22 insertions(+), 22 deletions(-)
create mode 100644 frontend/src/app/(auth)/sign-up/page.tsx
diff --git a/frontend/public/brain-logo-white.svg b/frontend/public/brain-logo-white.svg
index 96b1ab8..4b92e17 100644
--- a/frontend/public/brain-logo-white.svg
+++ b/frontend/public/brain-logo-white.svg
@@ -1,19 +1,4 @@
-