diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 48baed4..7ff95c5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,6 +12,8 @@ jobs: runs-on: ubuntu-latest env: TESTCONTAINER_DOCKER_NETWORK: nestled-testcontainers + TESTCONTAINERS_RYUK_DISABLED: "true" + DOCKER_HOST: unix:///var/run/docker.sock steps: - name: Checkout @@ -20,10 +22,10 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.26' + go-version: '1.23' - name: Setup Docker - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v3 - name: Tidy run: go mod tidy @@ -39,5 +41,5 @@ jobs: - name: Build docker image run: | - docker build -t nestled-api . - docker build -t nestled-migrations -f migrations/Dockerfile . \ No newline at end of file + docker build -t nestled-api . + docker build -t nestled-migrations -f migrations/Dockerfile . \ No newline at end of file diff --git a/.gitignore b/.gitignore index aaadf73..1b17134 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ go.work.sum # Editor/IDE # .idea/ # .vscode/ + +# compiled binaries output +/bin/ diff --git a/bin/nestled b/bin/nestled deleted file mode 100755 index 7c352fa..0000000 Binary files a/bin/nestled and /dev/null differ diff --git a/cmd/main/main.go b/cmd/main/main.go index 2c2506b..74ac27d 100644 --- a/cmd/main/main.go +++ b/cmd/main/main.go @@ -1,6 +1,7 @@ package main import ( + "github.com/albertoadami/nestled/internal/auth" "github.com/albertoadami/nestled/internal/config" "github.com/albertoadami/nestled/internal/database" "github.com/albertoadami/nestled/internal/handlers" @@ -30,19 +31,21 @@ func main() { } defer database.Close() + tokenManager := auth.NewTokenManager(configuration.JWT) + // repositories userRepository := repositories.NewUserRepository(database) // services userService := services.NewUserService(userRepository) - authService := services.NewAuthService(userRepository, configuration.JWT) + authService := services.NewAuthService(userRepository, tokenManager) // Initialize handlers userHandler := handlers.NewUserHandler(userService, logger) healthHandler := handlers.NewHealthHandler(database) authHandler := handlers.NewAuthHandler(authService) - routes.SetupRoutes(r, userHandler, healthHandler, authHandler) + routes.SetupRoutes(r, userHandler, healthHandler, authHandler, tokenManager) // Start the server if err := r.Run(); err != nil { diff --git a/go.mod b/go.mod index a43ecdf..d862617 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,18 @@ module github.com/albertoadami/nestled go 1.26.0 require ( + github.com/docker/docker v28.5.1+incompatible github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/golang-migrate/migrate/v4 v4.19.1 + github.com/google/uuid v1.6.0 + github.com/jmoiron/sqlx v1.4.0 + github.com/lib/pq v1.11.2 + github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.40.0 + go.uber.org/zap v1.27.1 + golang.org/x/crypto v0.47.0 ) require ( @@ -15,6 +25,7 @@ require ( github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -23,7 +34,6 @@ require ( github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect @@ -40,27 +50,18 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.1 // indirect - github.com/golang-migrate/migrate v3.5.4+incompatible // indirect - github.com/golang-migrate/migrate/v4 v4.19.1 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.8.0 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.11.2 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.1.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect @@ -76,7 +77,6 @@ require ( github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/shirou/gopsutil/v4 v4.25.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -84,29 +84,28 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/testcontainers/testcontainers-go v0.40.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect - golang.org/x/crypto v0.47.0 // indirect golang.org/x/net v0.49.0 // indirect - golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.12.0 // indirect + google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3ef0f66..6ab8f44 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,9 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -13,6 +16,8 @@ github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiD github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -25,12 +30,14 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/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 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= +github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= @@ -43,6 +50,8 @@ github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0o github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= @@ -66,6 +75,7 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= @@ -75,8 +85,6 @@ github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= -github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -85,14 +93,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -107,7 +109,6 @@ 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/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= @@ -117,6 +118,7 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= @@ -124,6 +126,8 @@ github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= 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/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= @@ -147,7 +151,6 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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 v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -178,6 +181,7 @@ github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjb 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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -201,16 +205,28 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= +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.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +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/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -225,8 +241,6 @@ golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -236,9 +250,20 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -247,3 +272,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 2dc7537..0ea9f93 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/albertoadami/nestled/internal/config" "github.com/albertoadami/nestled/internal/errors" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" @@ -19,8 +20,16 @@ type TokenInfo struct { ExpirationTime time.Time } -func GenerateToken(userId uuid.UUID, secretKey string, expireHours int) (*Token, error) { - expirationTime := time.Now().Add(time.Hour * time.Duration(expireHours)).Local().UTC() +type TokenManager struct { + jwtConfig config.JWTConfig +} + +func NewTokenManager(jwtConfig config.JWTConfig) *TokenManager { + return &TokenManager{jwtConfig: jwtConfig} +} + +func (tm *TokenManager) GenerateToken(userId uuid.UUID) (*Token, error) { + expirationTime := time.Now().Add(time.Hour * time.Duration(tm.jwtConfig.Expiration)).Local().UTC() claims := jwt.MapClaims{ "userId": userId.String(), @@ -28,7 +37,7 @@ func GenerateToken(userId uuid.UUID, secretKey string, expireHours int) (*Token, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signed, err := token.SignedString([]byte(secretKey)) + signed, err := token.SignedString([]byte(tm.jwtConfig.Secret)) if err != nil { return nil, errors.ErrGeneratingToken @@ -40,12 +49,12 @@ func GenerateToken(userId uuid.UUID, secretKey string, expireHours int) (*Token, }, nil } -func ParseToken(tokenValue string, secretKey string) (*TokenInfo, error) { +func (tm *TokenManager) ParseToken(tokenValue string) (*TokenInfo, error) { token, err := jwt.Parse(tokenValue, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method") } - return []byte(secretKey), nil + return []byte(tm.jwtConfig.Secret), nil }) if err != nil || !token.Valid { diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index d9173a6..ce9ee9b 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -4,17 +4,30 @@ import ( "testing" "time" + "github.com/albertoadami/nestled/internal/config" "github.com/albertoadami/nestled/internal/errors" "github.com/google/uuid" "github.com/stretchr/testify/assert" ) +var tokenManager = NewTokenManager( + config.JWTConfig{ + Secret: "test_secret_key", + Expiration: 6, + }, +) + +var expiredTokenManager = NewTokenManager( + config.JWTConfig{ + Secret: "test_secret_key", + Expiration: -1, // token already expired + }, +) + func TestGeneratingToken(t *testing.T) { userId := uuid.New() - secretKey := "test_secret_key" - expireHours := 6 - token, err := GenerateToken(userId, secretKey, expireHours) + token, err := tokenManager.GenerateToken(userId) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -25,7 +38,7 @@ func TestGeneratingToken(t *testing.T) { func TestParsingInvalidToken(t *testing.T) { token := "invalid.token.value" - _, err := ParseToken(token, "test_secret_key") + _, err := tokenManager.ParseToken(token) assert.ErrorIs(t, err, errors.ErrInvalidToken) @@ -33,12 +46,11 @@ func TestParsingInvalidToken(t *testing.T) { func TestParsingValidTokenAndExtractInfo(t *testing.T) { userId := uuid.New() - secretKey := "test_secret_key" - token, err := GenerateToken(userId, secretKey, 6) + token, err := tokenManager.GenerateToken(userId) assert.NoError(t, err) - tokenInfo, err := ParseToken(token.Value, secretKey) + tokenInfo, err := tokenManager.ParseToken(token.Value) assert.NoError(t, err) assert.Equal(t, userId.String(), tokenInfo.UserId) diff --git a/internal/dto/user_response.go b/internal/dto/user_response.go new file mode 100644 index 0000000..5a8c020 --- /dev/null +++ b/internal/dto/user_response.go @@ -0,0 +1,9 @@ +package dto + +type UserResponse struct { + Id string `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Username string `json:"username"` + Email string `json:"email"` +} diff --git a/internal/handlers/helpers.go b/internal/handlers/helpers.go new file mode 100644 index 0000000..25e511a --- /dev/null +++ b/internal/handlers/helpers.go @@ -0,0 +1,22 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func getUserIdFromContext(c *gin.Context) (uuid.UUID, bool) { + userId, exists := c.Get("userId") + if !exists { + c.Status(http.StatusInternalServerError) + return uuid.Nil, false + } + userIdUUID, err := uuid.Parse(userId.(string)) + if err != nil { + c.Status(http.StatusInternalServerError) + return uuid.Nil, false + } + return userIdUUID, true +} diff --git a/internal/handlers/user_handler.go b/internal/handlers/user_handler.go index a33e5d3..3360793 100644 --- a/internal/handlers/user_handler.go +++ b/internal/handlers/user_handler.go @@ -57,3 +57,24 @@ func (u *UserHandler) RegisterUser(c *gin.Context) { c.Status(http.StatusCreated) } + +func (u *UserHandler) GetCurrentUser(c *gin.Context) { + userId, ok := getUserIdFromContext(c) + if !ok { + return + } + + user, err := u.userService.GetUserById(userId) + if err != nil || user == nil { + c.Status(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, &dto.UserResponse{ + Id: user.Id.String(), + Username: user.Username, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + }) +} diff --git a/internal/handlers/user_handler_test.go b/internal/handlers/user_handler_test.go index cc9ce0f..01844e4 100644 --- a/internal/handlers/user_handler_test.go +++ b/internal/handlers/user_handler_test.go @@ -6,8 +6,13 @@ import ( "strings" "testing" + "encoding/json" + "github.com/albertoadami/nestled/internal/dto" "github.com/albertoadami/nestled/internal/errors" + "github.com/albertoadami/nestled/internal/model" + "github.com/albertoadami/nestled/internal/testhelpers" + "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -16,12 +21,17 @@ import ( type mockUserService struct { createUserFn func(req *dto.CreateUserRequest) (uuid.UUID, error) + getByIdFn func(id uuid.UUID) (*model.User, error) } func (m *mockUserService) CreateUser(req *dto.CreateUserRequest) (uuid.UUID, error) { return m.createUserFn(req) } +func (m *mockUserService) GetUserById(id uuid.UUID) (*model.User, error) { + return m.getByIdFn(id) +} + func setupUserRouter(mockService *mockUserService) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() @@ -30,6 +40,15 @@ func setupUserRouter(mockService *mockUserService) *gin.Engine { return router } +func setUpUserProfileRouter(mockService *mockUserService, userId uuid.UUID) *gin.Engine { + gin.SetMode(gin.TestMode) + router := gin.New() + handler := NewUserHandler(mockService, zap.NewNop()) + // apply mock authentication as middleware before the handler + router.GET("/api/v1/users/me", testhelpers.MockAuthentication(userId), handler.GetCurrentUser) + return router +} + func createUserRequest() *http.Request { body := `{"username":"test","email":"test@github.com","password":"secret123", "first_name":"Test","last_name":"User"}` req, _ := http.NewRequest("POST", "/api/v1/users/register", strings.NewReader(body)) @@ -96,3 +115,43 @@ func TestRegisterEmailAlreadyExists(t *testing.T) { assert.Equal(t, http.StatusConflict, w.Code) } + +func TestUserProfileSuccessfully(t *testing.T) { + + userId := uuid.New() + + mockService := &mockUserService{ + getByIdFn: func(id uuid.UUID) (*model.User, error) { + if id == userId { + return &model.User{ + Id: userId, + Username: "test", + Email: "test@test.it", + FirstName: "Test", + LastName: "User", + }, nil + } + return nil, nil + }, + } + + router := setUpUserProfileRouter(mockService, userId) + + req, _ := http.NewRequest("GET", "/api/v1/users/me", nil) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + expected := dto.UserResponse{ + Id: userId.String(), + Username: "test", + Email: "test@test.it", + FirstName: "Test", + LastName: "User", + } + expectedJSON, err := json.Marshal(expected) + assert.NoError(t, err) + assert.JSONEq(t, string(expectedJSON), w.Body.String()) +} diff --git a/internal/middleware/authentication.go b/internal/middleware/authentication.go index 2229f0a..b2a653e 100644 --- a/internal/middleware/authentication.go +++ b/internal/middleware/authentication.go @@ -9,7 +9,7 @@ import ( "github.com/gin-gonic/gin" ) -func BearerAuthentication(secretKey string) gin.HandlerFunc { +func BearerAuthentication(tokenManager *auth.TokenManager) gin.HandlerFunc { return func(c *gin.Context) { @@ -20,7 +20,7 @@ func BearerAuthentication(secretKey string) gin.HandlerFunc { } token := strings.TrimPrefix(authHeader, "Bearer ") - tokenInfo, err := auth.ParseToken(token, secretKey) + tokenInfo, err := tokenManager.ParseToken(token) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, &dto.ErrorResponse{Message: "invalid token"}) return diff --git a/internal/middleware/authentication_test.go b/internal/middleware/authentication_test.go index 9acb1bb..690d22a 100644 --- a/internal/middleware/authentication_test.go +++ b/internal/middleware/authentication_test.go @@ -6,15 +6,19 @@ import ( "testing" "github.com/albertoadami/nestled/internal/auth" + "github.com/albertoadami/nestled/internal/config" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" ) -func setupMiddlewareRouter(secretKey string) *gin.Engine { +var tokenManager = auth.NewTokenManager(config.JWTConfig{Secret: "secret", Expiration: 1}) +var expiredTokenManager = auth.NewTokenManager(config.JWTConfig{Secret: "secret", Expiration: -1}) + +func setupMiddlewareRouter(tokenManager *auth.TokenManager) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() - router.Use(BearerAuthentication(secretKey)) + router.Use(BearerAuthentication(tokenManager)) router.GET("/test", func(c *gin.Context) { userId, _ := c.Get("userId") c.JSON(http.StatusOK, gin.H{"userId": userId}) @@ -23,7 +27,7 @@ func setupMiddlewareRouter(secretKey string) *gin.Engine { } func TestBearerAuthentication_MissingHeader(t *testing.T) { - router := setupMiddlewareRouter("secret") + router := setupMiddlewareRouter(tokenManager) req, _ := http.NewRequest("GET", "/test", nil) w := httptest.NewRecorder() @@ -33,7 +37,7 @@ func TestBearerAuthentication_MissingHeader(t *testing.T) { } func TestBearerAuthentication_InvalidToken(t *testing.T) { - router := setupMiddlewareRouter("secret") + router := setupMiddlewareRouter(tokenManager) req, _ := http.NewRequest("GET", "/test", nil) req.Header.Set("Authorization", "Bearer invalid.token.here") @@ -44,12 +48,11 @@ func TestBearerAuthentication_InvalidToken(t *testing.T) { } func TestBearerAuthentication_ValidToken(t *testing.T) { - secretKey := "secret" userId := uuid.New() - token, _ := auth.GenerateToken(userId, secretKey, 6) + token, _ := tokenManager.GenerateToken(userId) - router := setupMiddlewareRouter(secretKey) + router := setupMiddlewareRouter(tokenManager) req, _ := http.NewRequest("GET", "/test", nil) req.Header.Set("Authorization", "Bearer "+token.Value) @@ -60,13 +63,12 @@ func TestBearerAuthentication_ValidToken(t *testing.T) { } func TestBearerAuthentication_ExpiredToken(t *testing.T) { - secretKey := "secret" userId := uuid.New() // generate token already expired (-1 hour) - token, _ := auth.GenerateToken(userId, secretKey, -1) + token, _ := expiredTokenManager.GenerateToken(userId) - router := setupMiddlewareRouter(secretKey) + router := setupMiddlewareRouter(expiredTokenManager) req, _ := http.NewRequest("GET", "/test", nil) req.Header.Set("Authorization", "Bearer "+token.Value) diff --git a/internal/repositories/user_repository.go b/internal/repositories/user_repository.go index 9cdaaf6..eea0f24 100644 --- a/internal/repositories/user_repository.go +++ b/internal/repositories/user_repository.go @@ -1,7 +1,10 @@ package repositories import ( - "github.com/albertoadami/nestled/internal/errors" + "database/sql" + "errors" + + customErrors "github.com/albertoadami/nestled/internal/errors" "github.com/albertoadami/nestled/internal/model" "github.com/google/uuid" "github.com/jmoiron/sqlx" @@ -11,6 +14,7 @@ import ( type UserRepository interface { CreateUser(user *model.User) (uuid.UUID, error) GetUserByUsername(username string) (*model.User, error) + GetUserById(id uuid.UUID) (*model.User, error) } type userRepository struct { @@ -41,9 +45,9 @@ func (r *userRepository) CreateUser(user *model.User) (uuid.UUID, error) { if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" { switch pqErr.Constraint { case "users_username_key": - return uuid.Nil, errors.ErrUsernameAlreadyExists + return uuid.Nil, customErrors.ErrUsernameAlreadyExists case "users_email_key": - return uuid.Nil, errors.ErrEmailAlreadyExists + return uuid.Nil, customErrors.ErrEmailAlreadyExists } } return uuid.Nil, err @@ -59,6 +63,24 @@ func (r *userRepository) GetUserByUsername(username string) (*model.User, error) var user model.User err := r.db.Get(&user, query, username) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + return &user, nil +} + +func (r *userRepository) GetUserById(id uuid.UUID) (*model.User, error) { + query := `SELECT id, username, first_name, last_name, email, password_hash, status + FROM users + WHERE id = $1 AND status != 'BLOCKED'::user_status` + var user model.User + err := r.db.Get(&user, query, id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } return nil, err } return &user, nil diff --git a/internal/repositories/user_repository_test.go b/internal/repositories/user_repository_test.go index a8f1436..fa3fb9d 100644 --- a/internal/repositories/user_repository_test.go +++ b/internal/repositories/user_repository_test.go @@ -139,9 +139,43 @@ func TestGetUserByUsernameFailedDueToNonExistingUser(t *testing.T) { userRepo := NewUserRepository(db) result, err := userRepo.GetUserByUsername("non-existing-username") - if err == nil { - t.Fatal("expected error, got nil") + assert.Nil(t, err, "expected err to be nil") + assert.Nil(t, result, "expected result to be nil") +} + +func TestGetUserByIdSucessfully(t *testing.T) { + + db, terminate := testhelpers.SetupPostgres(t) + defer terminate() + truncateUsers(t, db) + + userRepo := NewUserRepository(db) + user := createTestUser() + _, err := userRepo.CreateUser(user) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + retrievedUser, err := userRepo.GetUserById(user.Id) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if retrievedUser.Username != user.Username { + t.Fatalf("expected username %v, got %v", user.Username, retrievedUser.Username) } +} + +func TestGetUserByIdFailedDueToNonExistingUser(t *testing.T) { + + db, terminate := testhelpers.SetupPostgres(t) + defer terminate() + truncateUsers(t, db) + + userRepo := NewUserRepository(db) + + result, err := userRepo.GetUserById(uuid.New()) + assert.Nil(t, err, "expected err to be nil") assert.Nil(t, result, "expected result to be nil") } diff --git a/internal/routes/routes.go b/internal/routes/routes.go index d7fa28c..29bfb29 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -1,16 +1,22 @@ package routes import ( + "github.com/albertoadami/nestled/internal/auth" "github.com/albertoadami/nestled/internal/handlers" + "github.com/albertoadami/nestled/internal/middleware" "github.com/gin-gonic/gin" ) const ApiPrefix = "/api/v1" -func SetupRoutes(r *gin.Engine, userHandler *handlers.UserHandler, healthHandler *handlers.HealthHandler, authHandler *handlers.AuthHandler) { +func SetupRoutes(r *gin.Engine, userHandler *handlers.UserHandler, healthHandler *handlers.HealthHandler, authHandler *handlers.AuthHandler, tokenManager *auth.TokenManager) { + r.GET("/health", healthHandler.Health) apiGroup := r.Group(ApiPrefix) + protected := r.Group(ApiPrefix).Use(middleware.BearerAuthentication(tokenManager)) + apiGroup.POST("/register", userHandler.RegisterUser) apiGroup.POST("/auth/token", authHandler.GenerateToken) + protected.GET("/users/me", userHandler.GetCurrentUser) } diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index a6e9786..ec27fc1 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -2,7 +2,6 @@ package services import ( "github.com/albertoadami/nestled/internal/auth" - "github.com/albertoadami/nestled/internal/config" "github.com/albertoadami/nestled/internal/crypto" "github.com/albertoadami/nestled/internal/errors" "github.com/albertoadami/nestled/internal/repositories" @@ -14,13 +13,13 @@ type AuthService interface { type authService struct { userRepository repositories.UserRepository - JWtConfig config.JWTConfig + authManager *auth.TokenManager } -func NewAuthService(userRepository repositories.UserRepository, jwtConfig config.JWTConfig) AuthService { +func NewAuthService(userRepository repositories.UserRepository, manager *auth.TokenManager) AuthService { return &authService{ userRepository: userRepository, - JWtConfig: jwtConfig, + authManager: manager, } } @@ -36,7 +35,7 @@ func (s *authService) GenerateToken(username string, password string) (*auth.Tok valid := crypto.CheckPassword(password, user.PasswordHash) if valid { - jwtToken, err := auth.GenerateToken(user.Id, s.JWtConfig.Secret, s.JWtConfig.Expiration) + jwtToken, err := s.authManager.GenerateToken(user.Id) if err != nil { return nil, err } diff --git a/internal/services/auth_service_test.go b/internal/services/auth_service_test.go new file mode 100644 index 0000000..cf8a56a --- /dev/null +++ b/internal/services/auth_service_test.go @@ -0,0 +1,83 @@ +package services + +import ( + "testing" + + "github.com/albertoadami/nestled/internal/auth" + "github.com/albertoadami/nestled/internal/config" + "github.com/albertoadami/nestled/internal/crypto" + "github.com/albertoadami/nestled/internal/errors" + "github.com/albertoadami/nestled/internal/model" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +// mockUserRepo allows us to simulate the repository behavior. +type mockUserRepo struct { + getFn func(username string) (*model.User, error) + getFnById func(id uuid.UUID) (*model.User, error) +} + +func (m *mockUserRepo) CreateUser(user *model.User) (uuid.UUID, error) { + return uuid.Nil, nil +} + +func (m *mockUserRepo) GetUserById(id uuid.UUID) (*model.User, error) { + return m.getFnById(id) +} + +func (m *mockUserRepo) GetUserByUsername(username string) (*model.User, error) { + return m.getFn(username) +} + +var tokenManager = auth.NewTokenManager(config.JWTConfig{Secret: "secret", Expiration: 1}) + +func TestGenerateToken_UserNotFound(t *testing.T) { + mockRepo := &mockUserRepo{ + getFn: func(username string) (*model.User, error) { + return nil, nil + }, + getFnById: func(userId uuid.UUID) (*model.User, error) { + return nil, nil + }, + } + service := NewAuthService(mockRepo, tokenManager) + + token, err := service.GenerateToken("noexist", "pwd") + assert.Nil(t, token) + assert.ErrorIs(t, err, errors.CredentialsInvalid) +} + +func TestGenerateToken_InvalidPassword(t *testing.T) { + // create a user with a known hashed password + hash, _ := crypto.HashPassword("correct") + user := &model.User{Id: uuid.New(), PasswordHash: hash} + + mockRepo := &mockUserRepo{ + getFn: func(username string) (*model.User, error) { + return user, nil + }, + } + service := NewAuthService(mockRepo, tokenManager) + + token, err := service.GenerateToken("someuser", "wrong") + assert.Nil(t, token) + assert.ErrorIs(t, err, errors.CredentialsInvalid) +} + +func TestGenerateToken_Success(t *testing.T) { + hash, _ := crypto.HashPassword("correct") + user := &model.User{Id: uuid.New(), PasswordHash: hash} + + mockRepo := &mockUserRepo{ + getFn: func(username string) (*model.User, error) { + return user, nil + }, + } + service := NewAuthService(mockRepo, tokenManager) + + token, err := service.GenerateToken("someuser", "correct") + assert.NoError(t, err) + assert.NotNil(t, token) + assert.NotEmpty(t, token.Value) +} diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 404a225..4d08619 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -10,6 +10,7 @@ import ( type UserService interface { CreateUser(request *dto.CreateUserRequest) (uuid.UUID, error) + GetUserById(id uuid.UUID) (*model.User, error) } type userService struct { @@ -42,3 +43,7 @@ func (s *userService) CreateUser(request *dto.CreateUserRequest) (uuid.UUID, err return s.userRepository.CreateUser(user) } + +func (s *userService) GetUserById(id uuid.UUID) (*model.User, error) { + return s.userRepository.GetUserById(id) +} diff --git a/internal/testhelpers/mocked_context.go b/internal/testhelpers/mocked_context.go new file mode 100644 index 0000000..4106211 --- /dev/null +++ b/internal/testhelpers/mocked_context.go @@ -0,0 +1,16 @@ +package testhelpers + +import ( + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func MockAuthentication(userId uuid.UUID) gin.HandlerFunc { + + return func(c *gin.Context) { + + c.Set("userId", userId.String()) + c.Next() + + } +} diff --git a/internal/testhelpers/postgres.go b/internal/testhelpers/postgres.go index 5d81ae1..0904c95 100644 --- a/internal/testhelpers/postgres.go +++ b/internal/testhelpers/postgres.go @@ -26,7 +26,7 @@ func SetupPostgres(t *testing.T) (*sqlx.DB, func()) { "POSTGRES_PASSWORD": "testpass", "POSTGRES_DB": "testdb", }, - WaitingFor: wait.ForListeningPort("5432/tcp"), + WaitingFor: wait.ForLog("database system is ready to accept connections").WithOccurrence(2), } postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{