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 -

- -
- -
- Next.js Logo -
- - +
); } 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 ( +
- +

Ja possui uma conta? +

+ ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 56cec8a..11a898a 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,20 +1,7 @@ -"use client"; - -import useSessionWithRefresh from "@/hooks/useSessionWithRefresh"; -import { SIGN_OUT_CALLBACK_URL } from "@/utils/constants"; -import { signOut } from "next-auth/react"; - export default function Home() { - const session = useSessionWithRefresh(); - return (
-

- Bem vindo {session.data?.user.username} -

- +

Root page

); } diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index 2c6c770..400b845 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -1,4 +1,4 @@ export { default } from "next-auth/middleware"; // Define the routes you want to protect here -export const config = { matcher: ["/"] }; +export const config = { matcher: ["/dashboard/:path"] }; diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 10932cb..9b5794f 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -1,4 +1,4 @@ export const BACKEND_API_URL = process.env.BACKEND_API_URL || "http://localhost:42069"; export const SIGN_OUT_CALLBACK_URL = "/sign-in"; -export const SIGN_IN_CALLBACK_URL = "/"; +export const SIGN_IN_CALLBACK_URL = "/dashboard"; From 4fc7ca4be9d49ea270a777c98993c533447c2f52 Mon Sep 17 00:00:00 2001 From: Lu1z-Gust4v0 Date: Tue, 23 Apr 2024 21:59:27 -0300 Subject: [PATCH 78/91] improvement(#52): Standarize submit functions, update tests --- .../pages/signin/login-form.test.tsx | 50 +++++++++---------- .../components/pages/signin/login-form.tsx | 14 ++---- .../components/pages/signin/login-section.tsx | 7 ++- .../pages/signup/register-form.test.tsx | 31 ++---------- 4 files changed, 40 insertions(+), 62 deletions(-) diff --git a/frontend/src/components/pages/signin/login-form.test.tsx b/frontend/src/components/pages/signin/login-form.test.tsx index f88a5df..99e4a80 100644 --- a/frontend/src/components/pages/signin/login-form.test.tsx +++ b/frontend/src/components/pages/signin/login-form.test.tsx @@ -2,17 +2,6 @@ import { act, render, screen, waitFor } from "@/utils/vitest/utilities"; import LoginForm from "./login-form"; import { Mock } from "vitest"; import { axe } from "jest-axe"; -import userEvent from "@testing-library/user-event"; - -// mock useRouter -vi.mock("next/navigation", async () => ({ - useRouter() { - return { - push: () => null, - prefetch: () => null, - }; - }, -})); describe("Login Form Tests", () => { const renderLoginForm = (fn: Mock) => { @@ -31,12 +20,6 @@ describe("Login Form Tests", () => { // Arrange const fn = vi.fn(async () => { await sleep(0); - // mock clear function - const userNameInput = screen.getByLabelText("Username"); - const passwordInput = screen.getByLabelText("Senha"); - await userEvent.clear(userNameInput); - await userEvent.clear(passwordInput); - return true; }); const { user, userNameInput, passwordInput, submitBtn } = @@ -48,10 +31,8 @@ describe("Login Form Tests", () => { await user.click(submitBtn); //Assert - await waitFor(() => { - expect(userNameInput).toHaveValue(""); - expect(passwordInput).toHaveValue(""); - }); + expect(userNameInput).toHaveValue("marcelo jr") + expect(passwordInput).toHaveValue("melikofornite123") expect(screen.getByText(/esqueci minha senha/i)); expect(fn).toHaveBeenCalledOnce(); }); @@ -60,7 +41,6 @@ describe("Login Form Tests", () => { // Arrange const fn = vi.fn(async () => { await sleep(0); - return true; }); const { user, userNameInput, passwordInput, submitBtn } = renderLoginForm(fn); @@ -79,11 +59,33 @@ describe("Login Form Tests", () => { expect(fn).not.toHaveBeenCalledOnce(); }); + it("Should not submit with invalid password", async () => { + // Arrange + const fn = vi.fn(async () => { + await sleep(0); + }); + const { user, userNameInput, passwordInput, submitBtn } = + renderLoginForm(fn); + const badPassword = "12345" + // Act + await user.type(userNameInput, "marcelo jr"); + await user.type(passwordInput, badPassword); + await user.click(submitBtn); + + // Assert + expect(userNameInput).toHaveValue("marcelo jr") + expect(passwordInput).toHaveValue(badPassword) + expect(passwordInput).toHaveAccessibleErrorMessage( + /Senha deve conter 8 ou mais caracteres/i, + ); + expect(fn).not.toHaveBeenCalledOnce(); + }) + it("Should not reset form if fails", async () => { // Arrange const fn = vi.fn(async () => { await sleep(0); - return false; + return; }); const { user, userNameInput, passwordInput, submitBtn } = renderLoginForm(fn); @@ -105,7 +107,6 @@ describe("Login Form Tests", () => { it("Should disable btn until promises resolves", async () => { const fn = vi.fn(async () => { await sleep(100); - return true; }); const { user, userNameInput, passwordInput, submitBtn } = renderLoginForm(fn); @@ -130,7 +131,6 @@ describe("Login Form Tests", () => { it("Should be accessible", async () => { const fn = vi.fn(async () => { await sleep(100); - return true; }); const { container } = render(); diff --git a/frontend/src/components/pages/signin/login-form.tsx b/frontend/src/components/pages/signin/login-form.tsx index 00784b9..7266d82 100644 --- a/frontend/src/components/pages/signin/login-form.tsx +++ b/frontend/src/components/pages/signin/login-form.tsx @@ -15,8 +15,6 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { SIGN_IN_CALLBACK_URL } from "@/utils/constants"; const formSchema = z.object({ username: z @@ -31,7 +29,7 @@ export type LoginFormSchema = z.infer; export type UseForm = UseFormReturn; export type LoginFormProps = { - submitForm: (values: LoginFormSchema, form: UseForm) => Promise; + submitForm: (values: LoginFormSchema, form: UseForm) => Promise; }; function LoginForm({ submitForm }: LoginFormProps) { @@ -43,12 +41,8 @@ function LoginForm({ submitForm }: LoginFormProps) { }, }); - const router = useRouter(); - async function onSubmit(values: LoginFormSchema) { - const valid = await submitForm(values, form); - if (!valid) return; - router.push(SIGN_IN_CALLBACK_URL); + await submitForm(values, form); } return ( @@ -91,10 +85,12 @@ function LoginForm({ submitForm }: LoginFormProps) { type="password" id="password-input" placeholder="*****" + aria-invalid={!!form.formState.errors.password} + aria-errormessage="password-error" {...field} /> - + )} /> diff --git a/frontend/src/components/pages/signin/login-section.tsx b/frontend/src/components/pages/signin/login-section.tsx index 6b4359f..0854487 100644 --- a/frontend/src/components/pages/signin/login-section.tsx +++ b/frontend/src/components/pages/signin/login-section.tsx @@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button"; import Link from "next/link"; import LoginForm, { LoginFormSchema, UseForm } from "./login-form"; import { signIn } from "next-auth/react"; +import { useRouter } from "next/navigation"; import { SIGN_IN_CALLBACK_URL } from "@/utils/constants"; function LoginSection() { + const router = useRouter(); + const submitForm = async (values: LoginFormSchema, form: UseForm) => { const response = await signIn("credentials", { username: values.username, @@ -21,11 +24,11 @@ function LoginSection() { type: "validate", message: "Usuario ou senhas incorretas", }); - return false; + return } form.reset(); - return true; + router.push(SIGN_IN_CALLBACK_URL); }; return ( diff --git a/frontend/src/components/pages/signup/register-form.test.tsx b/frontend/src/components/pages/signup/register-form.test.tsx index 9ef32c6..b94a9e6 100644 --- a/frontend/src/components/pages/signup/register-form.test.tsx +++ b/frontend/src/components/pages/signup/register-form.test.tsx @@ -2,21 +2,6 @@ import { act, render, screen, waitFor } from "@/utils/vitest/utilities"; import { Mock } from "vitest"; import RegisterForm from "./register-form"; import { axe } from "jest-axe"; -import userEvent from "@testing-library/user-event"; - -const getRegisterFormInputs = () => { - const usernameInput = screen.getByLabelText(/username/i); - const emailInput = screen.getByLabelText(/email/i); - const passwordInput = screen.getByLabelText(/^senha/i); - const confirmPasswordInput = screen.getByLabelText(/confirmar senha/i); - - return { - usernameInput, - emailInput, - passwordInput, - confirmPasswordInput, - }; -}; const renderRegisterForm = (fn: Mock) => { const { user } = render(); @@ -40,11 +25,7 @@ const generateSubmitFn = (ms: number = 0, value: boolean = true) => { return vi.fn(async () => { await new Promise((resolve) => setTimeout(resolve, ms)); - if (!value) return; - - Object.values(getRegisterFormInputs()).forEach((input) => { - userEvent.clear(input); - }); + return value }); }; @@ -87,12 +68,10 @@ describe("Register Form tests", () => { }); // Assert - await waitFor(() => { - expect(usernameInput).toHaveValue(""); - expect(emailInput).toHaveValue(""); - expect(passwordInput).toHaveValue(""); - expect(confirmPasswordInput).toHaveValue(""); - }); + expect(usernameInput).toHaveValue(validUsername); + expect(emailInput).toHaveValue(validEmail); + expect(passwordInput).toHaveValue(validPass); + expect(confirmPasswordInput).toHaveValue(validPass); expect(fn).toHaveBeenCalledOnce(); }); From 3d1e252a84d120ec24e83b63eba55a8f185c5bc9 Mon Sep 17 00:00:00 2001 From: Lu1z-Gust4v0 Date: Tue, 23 Apr 2024 22:01:51 -0300 Subject: [PATCH 79/91] improvement(#52): add auth secret to constants, and provide default value --- .../pages/signin/login-form.test.tsx | 18 +++++++++--------- .../components/pages/signin/login-section.tsx | 2 +- .../pages/signup/register-form.test.tsx | 2 +- frontend/src/providers/auth.ts | 3 ++- frontend/src/utils/constants.ts | 1 + 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/pages/signin/login-form.test.tsx b/frontend/src/components/pages/signin/login-form.test.tsx index 99e4a80..782e4ed 100644 --- a/frontend/src/components/pages/signin/login-form.test.tsx +++ b/frontend/src/components/pages/signin/login-form.test.tsx @@ -31,8 +31,8 @@ describe("Login Form Tests", () => { await user.click(submitBtn); //Assert - expect(userNameInput).toHaveValue("marcelo jr") - expect(passwordInput).toHaveValue("melikofornite123") + expect(userNameInput).toHaveValue("marcelo jr"); + expect(passwordInput).toHaveValue("melikofornite123"); expect(screen.getByText(/esqueci minha senha/i)); expect(fn).toHaveBeenCalledOnce(); }); @@ -60,26 +60,26 @@ describe("Login Form Tests", () => { }); it("Should not submit with invalid password", async () => { - // Arrange + // Arrange const fn = vi.fn(async () => { await sleep(0); }); const { user, userNameInput, passwordInput, submitBtn } = renderLoginForm(fn); - const badPassword = "12345" - // Act + const badPassword = "12345"; + // Act await user.type(userNameInput, "marcelo jr"); await user.type(passwordInput, badPassword); await user.click(submitBtn); - + // Assert - expect(userNameInput).toHaveValue("marcelo jr") - expect(passwordInput).toHaveValue(badPassword) + expect(userNameInput).toHaveValue("marcelo jr"); + expect(passwordInput).toHaveValue(badPassword); expect(passwordInput).toHaveAccessibleErrorMessage( /Senha deve conter 8 ou mais caracteres/i, ); expect(fn).not.toHaveBeenCalledOnce(); - }) + }); it("Should not reset form if fails", async () => { // Arrange diff --git a/frontend/src/components/pages/signin/login-section.tsx b/frontend/src/components/pages/signin/login-section.tsx index 0854487..9eebf67 100644 --- a/frontend/src/components/pages/signin/login-section.tsx +++ b/frontend/src/components/pages/signin/login-section.tsx @@ -24,7 +24,7 @@ function LoginSection() { type: "validate", message: "Usuario ou senhas incorretas", }); - return + return; } form.reset(); diff --git a/frontend/src/components/pages/signup/register-form.test.tsx b/frontend/src/components/pages/signup/register-form.test.tsx index b94a9e6..ec53855 100644 --- a/frontend/src/components/pages/signup/register-form.test.tsx +++ b/frontend/src/components/pages/signup/register-form.test.tsx @@ -25,7 +25,7 @@ const generateSubmitFn = (ms: number = 0, value: boolean = true) => { return vi.fn(async () => { await new Promise((resolve) => setTimeout(resolve, ms)); - return value + return value; }); }; diff --git a/frontend/src/providers/auth.ts b/frontend/src/providers/auth.ts index 5901631..9eeecc3 100644 --- a/frontend/src/providers/auth.ts +++ b/frontend/src/providers/auth.ts @@ -7,6 +7,7 @@ import { isTokenExpired, } from "@/utils/token"; import { isAxiosError } from "axios"; +import { NEXT_AUTH_SECRET } from "@/utils/constants"; export const authOptions: NextAuthOptions = { providers: [ @@ -40,7 +41,7 @@ export const authOptions: NextAuthOptions = { signOut: "/sign-in", newUser: "/sign-up", }, - secret: process.env.NEXTAUTH_SECRET, + secret: NEXT_AUTH_SECRET, callbacks: { async jwt({ token, user }) { // User just signed in diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 9b5794f..06d1509 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -2,3 +2,4 @@ export const BACKEND_API_URL = process.env.BACKEND_API_URL || "http://localhost:42069"; export const SIGN_OUT_CALLBACK_URL = "/sign-in"; export const SIGN_IN_CALLBACK_URL = "/dashboard"; +export const NEXT_AUTH_SECRET = process.env.NEXTAUTH_SECRET || "top-secret"; From 9f355bb7dc6575624cace8bc7f878f40b84b6b08 Mon Sep 17 00:00:00 2001 From: Lu1z-Gust4v0 Date: Tue, 23 Apr 2024 22:10:30 -0300 Subject: [PATCH 80/91] fix(#52): type error on mock function --- frontend/src/components/pages/signup/register-form.test.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/pages/signup/register-form.test.tsx b/frontend/src/components/pages/signup/register-form.test.tsx index ec53855..4700167 100644 --- a/frontend/src/components/pages/signup/register-form.test.tsx +++ b/frontend/src/components/pages/signup/register-form.test.tsx @@ -21,11 +21,9 @@ const renderRegisterForm = (fn: Mock) => { }; }; -const generateSubmitFn = (ms: number = 0, value: boolean = true) => { +const generateSubmitFn = (ms: number = 0) => { return vi.fn(async () => { await new Promise((resolve) => setTimeout(resolve, ms)); - - return value; }); }; @@ -198,7 +196,7 @@ describe("Register Form tests", () => { }); it("Should not reset form if promise fails", async () => { - const fn = generateSubmitFn(0, false); + const fn = generateSubmitFn(0); const { user, From b46fcebc899e491616fb14ce21c455d51c0dab02 Mon Sep 17 00:00:00 2001 From: Lu1z-Gust4v0 Date: Tue, 23 Apr 2024 22:11:56 -0300 Subject: [PATCH 81/91] fix(#52): package lock --- frontend/package-lock.json | 105 +++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a51b5ac..09b6dd5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -885,6 +885,66 @@ "glob": "10.3.10" } }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.3.tgz", + "integrity": "sha512-LALu0yIBPRiG9ANrD5ncB3pjpO0Gli9ZLhxdOu6ZUNf3x1r3ea1rd9Q+4xxUkGrUXLqKVK9/lDkpYIJaCJ6AHQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.3.tgz", + "integrity": "sha512-E/9WQeXxkqw2dfcn5UcjApFgUq73jqNKaE5bysDm58hEUdUGedVrnRhblhJM7HbCZNhtVl0j+6TXsK0PuzXTCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.3.tgz", + "integrity": "sha512-USArX9B+3rZSXYLFvgy0NVWQgqh6LHWDmMt38O4lmiJNQcwazeI6xRvSsliDLKt+78KChVacNiwvOMbl6g6BBw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.3.tgz", + "integrity": "sha512-esk1RkRBLSIEp1qaQXv1+s6ZdYzuVCnDAZySpa62iFTMGTisCyNQmqyCTL9P+cLJ4N9FKCI3ojtSfsyPHJDQNw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-linux-x64-gnu": { "version": "14.1.3", "cpu": [ @@ -913,6 +973,51 @@ "node": ">= 10" } }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.3.tgz", + "integrity": "sha512-HjssFsCdsD4GHstXSQxsi2l70F/5FsRTRQp8xNgmQs15SxUfUJRvSI9qKny/jLkY3gLgiCR3+6A7wzzK0DBlfA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.3.tgz", + "integrity": "sha512-DRuxD5axfDM1/Ue4VahwSxl1O5rn61hX8/sF0HY8y0iCbpqdxw3rB3QasdHn/LJ6Wb2y5DoWzXcz3L1Cr+Thrw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.3.tgz", + "integrity": "sha512-uC2DaDoWH7h1P/aJ4Fok3Xiw6P0Lo4ez7NbowW2VGNXw/Xv6tOuLUcxhBYZxsSUJtpeknCi8/fvnSpyCFp4Rcg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", From 8c5b660c46798fbcf516836610203e5c0529e3fd Mon Sep 17 00:00:00 2001 From: Lu1z-Gust4v0 Date: Tue, 23 Apr 2024 23:21:49 -0300 Subject: [PATCH 82/91] improvement(#52): add husky and lint-staged to format commits --- frontend/.husky/pre-commit | 2 + frontend/package-lock.json | 538 +++++++++++++++++++++++++++++++++++++ frontend/package.json | 8 +- 3 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 frontend/.husky/pre-commit diff --git a/frontend/.husky/pre-commit b/frontend/.husky/pre-commit new file mode 100644 index 0000000..4f6d1d7 --- /dev/null +++ b/frontend/.husky/pre-commit @@ -0,0 +1,2 @@ +cd frontend +npx lint-staged diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 09b6dd5..a23c5e1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -41,9 +41,11 @@ "eslint": "^8", "eslint-config-next": "14.1.3", "eslint-plugin-jsx-a11y": "^6.8.0", + "husky": "^8.0.0", "jest-axe": "^8.0.0", "jest-extended": "^4.0.2", "jsdom": "^24.0.0", + "lint-staged": "^15.2.2", "msw": "^2.2.14", "postcss": "^8", "prettier": "3.2.5", @@ -2823,6 +2825,21 @@ "node": ">=6" } }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-spinners": { "version": "2.9.2", "dev": true, @@ -2834,6 +2851,72 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/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" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "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" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cli-width": { "version": "4.1.0", "dev": true, @@ -2914,6 +2997,12 @@ "version": "1.1.4", "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "license": "MIT", @@ -3891,6 +3980,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/execa": { "version": "8.0.1", "dev": true, @@ -4163,6 +4258,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "dev": true, @@ -4490,6 +4597,21 @@ "node": ">=16.17.0" } }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "dev": true, @@ -5468,6 +5590,168 @@ "version": "1.2.4", "license": "MIT" }, + "node_modules/lint-staged": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.2.tgz", + "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==", + "dev": true, + "dependencies": { + "chalk": "5.3.0", + "commander": "11.1.0", + "debug": "4.3.4", + "execa": "8.0.1", + "lilconfig": "3.0.0", + "listr2": "8.0.1", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/lint-staged/node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/listr2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz", + "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.0.0", + "rfdc": "^1.3.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/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" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "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" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "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" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/local-pkg": { "version": "0.5.0", "dev": true, @@ -5537,6 +5821,147 @@ "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "dev": true }, + "node_modules/log-update": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", + "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^6.2.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^7.0.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/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" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "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" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "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" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -6359,6 +6784,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "license": "MIT", @@ -6940,6 +7377,52 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -6957,6 +7440,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, "node_modules/rimraf": { "version": "3.0.2", "dev": true, @@ -7251,6 +7740,46 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "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" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "license": "BSD-3-Clause", @@ -7372,6 +7901,15 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "5.1.2", "license": "MIT", diff --git a/frontend/package.json b/frontend/package.json index a4aea80..c9af6ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,11 @@ "test:ci": "vitest --run", "test:coverage": "vitest --run --coverage", "e2e-test:ui": "playwright test --ui", - "e2e-test:ci": "playwright test" + "e2e-test:ci": "playwright test", + "prepare": "cd .. && npx husky frontend/.husky" + }, + "lint-staged": { + "*.{ts,tsx,css,md}": "prettier --ignore-unknown --write" }, "dependencies": { "@hookform/resolvers": "^3.3.4", @@ -48,9 +52,11 @@ "eslint": "^8", "eslint-config-next": "14.1.3", "eslint-plugin-jsx-a11y": "^6.8.0", + "husky": "^8.0.0", "jest-axe": "^8.0.0", "jest-extended": "^4.0.2", "jsdom": "^24.0.0", + "lint-staged": "^15.2.2", "msw": "^2.2.14", "postcss": "^8", "prettier": "3.2.5", From 9a9ec75ed8c5f8ead38976b9ea23de306e6b39cd Mon Sep 17 00:00:00 2001 From: Lu1z-Gust4v0 Date: Wed, 24 Apr 2024 14:58:17 -0300 Subject: [PATCH 83/91] feature(#52): Add forgot password page --- frontend/src/app/(auth)/forgot-password/page.tsx | 7 +++++++ frontend/src/components/pages/signin/login-form.tsx | 2 +- frontend/src/components/pages/signin/login-section.tsx | 1 - 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/(auth)/forgot-password/page.tsx diff --git a/frontend/src/app/(auth)/forgot-password/page.tsx b/frontend/src/app/(auth)/forgot-password/page.tsx new file mode 100644 index 0000000..de3546a --- /dev/null +++ b/frontend/src/app/(auth)/forgot-password/page.tsx @@ -0,0 +1,7 @@ +export default function ForgotPassword() { + return ( +
+

Forgot password

+
+ ); +} diff --git a/frontend/src/components/pages/signin/login-form.tsx b/frontend/src/components/pages/signin/login-form.tsx index 7266d82..d46ef04 100644 --- a/frontend/src/components/pages/signin/login-form.tsx +++ b/frontend/src/components/pages/signin/login-form.tsx @@ -96,7 +96,7 @@ function LoginForm({ submitForm }: LoginFormProps) { />