From 917eb7281f7db933a586daf751c28d4f83637223 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 01:58:11 +0200 Subject: [PATCH 01/28] fix posible integer overflow conversion uint64 -> int64 --- internal/clients/substrate/interface.go | 4 +--- internal/clients/substrate/substrate.go | 11 +++++------ internal/services/tokens.go | 3 +-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/internal/clients/substrate/interface.go b/internal/clients/substrate/interface.go index 92da7d3..7d991cc 100644 --- a/internal/clients/substrate/interface.go +++ b/internal/clients/substrate/interface.go @@ -1,7 +1,5 @@ package substrate -import "math/big" - type SubstrateConfig interface { GetWsProviderURL() string } @@ -9,5 +7,5 @@ type SubstrateConfig interface { type SubstrateClient interface { GetChainName() (string, error) GetAddressByTwinID(twinID string) (string, error) - GetAccountBalance(address string) (*big.Int, error) + GetAccountBalance(address string) (uint64, error) } diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index 9c7e9ca..d497951 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -2,7 +2,6 @@ package substrate import ( "fmt" - "math/big" "strconv" "github.com/threefoldtech/tf-kyc-verifier/internal/logger" @@ -31,21 +30,21 @@ func New(config SubstrateConfig, logger logger.Logger) (*Substrate, error) { return c, nil } -func (c *Substrate) GetAccountBalance(address string) (*big.Int, error) { +func (c *Substrate) GetAccountBalance(address string) (uint64, error) { pubkeyBytes, err := tfchain.FromAddress(address) if err != nil { - return nil, fmt.Errorf("failed to decode ss58 address: %w", err) + return 0, fmt.Errorf("failed to decode ss58 address: %w", err) } accountID := tfchain.AccountID(pubkeyBytes) balance, err := c.api.GetBalance(accountID) if err != nil { if err.Error() == "account not found" { - return big.NewInt(0), nil + return 0, nil } - return nil, fmt.Errorf("failed to get balance: %w", err) + return 0, fmt.Errorf("failed to get balance: %w", err) } - return balance.Free.Int, nil + return balance.Free.Uint64(), nil } func (c *Substrate) GetAddressByTwinID(twinID string) (string, error) { diff --git a/internal/services/tokens.go b/internal/services/tokens.go index f9dc13c..144bc41 100644 --- a/internal/services/tokens.go +++ b/internal/services/tokens.go @@ -3,7 +3,6 @@ package services import ( "context" "fmt" - "math/big" "time" "github.com/threefoldtech/tf-kyc-verifier/internal/errors" @@ -82,5 +81,5 @@ func (s *kycService) AccountHasRequiredBalance(ctx context.Context, address stri s.logger.Error("Error getting account balance", logger.Fields{"address": address, "error": err}) return false, errors.NewExternalError("error getting account balance", err) } - return balance.Cmp(big.NewInt(int64(s.config.MinBalanceToVerifyAccount))) >= 0, nil + return balance >= s.config.MinBalanceToVerifyAccount, nil } From e92ee59cb4114fe4c94bbdc7cc6cae0a08d6257f Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 01:58:53 +0200 Subject: [PATCH 02/28] update swag docs --- cmd/api/main.go | 9 --------- internal/handlers/handlers.go | 9 +++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index aef9266..db5158e 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -9,15 +9,6 @@ import ( "github.com/threefoldtech/tf-kyc-verifier/internal/server" ) -// @title TFGrid KYC API -// @version 0.2.0 -// @description This is a KYC service for TFGrid. -// @termsOfService http://swagger.io/terms/ - -// @contact.name Codescalers Egypt -// @contact.url https://codescalers-egypt.com -// @contact.email info@codescalers.com -// @BasePath / func main() { config, err := configs.LoadConfig() if err != nil { diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index a003699..3fad50d 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -25,6 +25,15 @@ type Handler struct { logger logger.Logger } +// @title TFGrid KYC API +// @version 0.2.0 +// @description This is a KYC service for TFGrid. +// @termsOfService http://swagger.io/terms/ + +// @contact.name Codescalers Egypt +// @contact.url https://codescalers-egypt.com +// @contact.email info@codescalers.com +// @BasePath / func NewHandler(kycService services.KYCService, config *configs.Config, logger logger.Logger) *Handler { return &Handler{kycService: kycService, config: config, logger: logger} } From 2d0916d4b17b91f061b514db2b7fad7d2173a64f Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 01:59:44 +0200 Subject: [PATCH 03/28] minor context timeout refactor --- internal/repository/token_repository.go | 21 ++++++++++++------- .../repository/verification_repository.go | 17 +++++++++++++-- internal/server/server.go | 10 ++++----- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index f3999f9..fe2f1ec 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -17,19 +17,17 @@ type MongoTokenRepository struct { logger logger.Logger } -func NewMongoTokenRepository(db *mongo.Database, logger logger.Logger) TokenRepository { +func NewMongoTokenRepository(ctx context.Context, db *mongo.Database, logger logger.Logger) TokenRepository { repo := &MongoTokenRepository{ collection: db.Collection("tokens"), logger: logger, } - repo.createTTLIndex() + repo.createTTLIndex(ctx) + repo.createClientIdIndex(ctx) return repo } -func (r *MongoTokenRepository) createTTLIndex() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - +func (r *MongoTokenRepository) createTTLIndex(ctx context.Context) { _, err := r.collection.Indexes().CreateOne( ctx, mongo.IndexModel{ @@ -37,12 +35,21 @@ func (r *MongoTokenRepository) createTTLIndex() { Options: options.Index().SetExpireAfterSeconds(0), }, ) - if err != nil { r.logger.Error("Error creating TTL index", logger.Fields{"error": err}) } } +func (r *MongoTokenRepository) createClientIdIndex(ctx context.Context) { + _, err := r.collection.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "clientId", Value: 1}}, + Options: options.Index().SetUnique(true), + }) + if err != nil { + r.logger.Error("Error creating clientId index", logger.Fields{"error": err}) + } +} + func (r *MongoTokenRepository) SaveToken(ctx context.Context, token *models.Token) error { token.CreatedAt = time.Now() token.ExpiresAt = token.CreatedAt.Add(time.Duration(token.ExpiryTime) * time.Second) diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go index e563818..8873012 100644 --- a/internal/repository/verification_repository.go +++ b/internal/repository/verification_repository.go @@ -16,11 +16,24 @@ type MongoVerificationRepository struct { logger logger.Logger } -func NewMongoVerificationRepository(db *mongo.Database, logger logger.Logger) VerificationRepository { - return &MongoVerificationRepository{ +func NewMongoVerificationRepository(ctx context.Context, db *mongo.Database, logger logger.Logger) VerificationRepository { + // create index for clientId + repo := &MongoVerificationRepository{ collection: db.Collection("verifications"), logger: logger, } + repo.createClientIdIndex(ctx) + return repo +} + +func (r *MongoVerificationRepository) createClientIdIndex(ctx context.Context) { + _, err := r.collection.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "clientId", Value: 1}}, + Options: options.Index().SetUnique(true), + }) + if err != nil { + r.logger.Error("Error creating clientId index", logger.Fields{"error": err}) + } } func (r *MongoVerificationRepository) SaveVerification(ctx context.Context, verification *models.Verification) error { diff --git a/internal/server/server.go b/internal/server/server.go index 54edbe5..695da3a 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -40,7 +40,7 @@ type Server struct { // New creates a new server instance with the given configuration and options func New(config *configs.Config, srvLogger logger.Logger) (*Server, error) { // Create base context for initialization - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Initialize server with base configuration @@ -79,7 +79,7 @@ func (s *Server) initializeCore(ctx context.Context) error { } // Setup repositories - repos, err := s.setupRepositories(db) + repos, err := s.setupRepositories(ctx, db) if err != nil { return fmt.Errorf("failed to setup repositories: %w", err) } @@ -174,12 +174,12 @@ type repositories struct { verification repository.VerificationRepository } -func (s *Server) setupRepositories(db *mongo.Database) (*repositories, error) { +func (s *Server) setupRepositories(ctx context.Context, db *mongo.Database) (*repositories, error) { s.logger.Debug("Setting up repositories", nil) return &repositories{ - token: repository.NewMongoTokenRepository(db, s.logger), - verification: repository.NewMongoVerificationRepository(db, s.logger), + token: repository.NewMongoTokenRepository(ctx, db, s.logger), + verification: repository.NewMongoVerificationRepository(ctx, db, s.logger), }, nil } From 57ece66d8460e0affbd76776a0f43e7bbfb5ac38 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 02:00:04 +0200 Subject: [PATCH 04/28] update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2907874..2f5558a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.dll *.so *.dylib +bin/ # Test binary, built with `go test -c` *.test From a2d2eef404d2ebfbbb0c76e08e22be2a5e5a14af Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 02:00:33 +0200 Subject: [PATCH 05/28] update Dockerfile --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index cfb0799..8f0e7f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,14 +9,14 @@ COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN VERSION=`git describe --tags` && \ - CGO_ENABLED=0 GOOS=linux go build -o tfgrid-kyc -ldflags "-X github.com/threefoldtech/tf-kyc-verifier/internal/build.Version=$VERSION" cmd/api/main.go +RUN VERSION=$(git describe --tags --always) && \ + CGO_ENABLED=0 GOOS=linux go build -o tfkycv -ldflags "-X github.com/threefoldtech/tf-kyc-verifier/internal/build.Version=$VERSION" cmd/api/main.go FROM alpine:3.19 -COPY --from=builder /app/tfgrid-kyc . +COPY --from=builder /app/tfkycv . RUN apk --no-cache add curl -ENTRYPOINT ["/tfgrid-kyc"] +ENTRYPOINT ["/tfkycv"] EXPOSE 8080 From e8706cdf35354fa48bce01bd61d5f37ad7d5961f Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 02:00:59 +0200 Subject: [PATCH 06/28] add Makefile --- Makefile | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dfcd867 --- /dev/null +++ b/Makefile @@ -0,0 +1,131 @@ +# Variables +APP_NAME := tfkycv +IMAGE_NAME := ghcr.io/threefoldtech/tf-kyc-verifier +MAIN_PATH := cmd/api/main.go +SWAGGER_GENERAL_API_INFO_PATH := internal/handlers/handlers.go +DOCKER_COMPOSE := docker compose + +# Go related variables +GOBASE := $(shell pwd) +GOBIN := $(GOBASE)/bin +GOFILES := $(wildcard *.go) + +# Git related variables +GIT_COMMIT := $(shell git rev-parse --short HEAD) +VERSION := $(shell git describe --tags --always) + +# Build flags +LDFLAGS := -X github.com/threefoldtech/tf-kyc-verifier/internal/build.Version=$(VERSION) + +.PHONY: all build clean test coverage lint swagger run docker-build docker-up docker-down help + +# Default target +all: clean build + +# Build the application +build: + @echo "Building $(APP_NAME)..." + @go build -ldflags "$(LDFLAGS)" -o $(GOBIN)/$(APP_NAME) $(MAIN_PATH) + +# Clean build artifacts +clean: + @echo "Cleaning..." + @rm -rf $(GOBIN) + @go clean + +# Run tests +test: + @echo "Running tests..." + @go test -v ./... + +# Run tests with coverage +coverage: + @echo "Running tests with coverage..." + @go test -coverprofile=coverage.out ./... + @go tool cover -html=coverage.out + @rm coverage.out + +# Run linter +lint: + @echo "Running linter..." + @golangci-lint run + +# Generate swagger documentation +swagger: + @echo "Generating Swagger documentation..." + @export PATH=$PATH:$(go env GOPATH)/bin + @swag init -g $(SWAGGER_GENERAL_API_INFO_PATH) --output api/docs + +# Run the application locally +run: swagger build + @echo "Running $(APP_NAME)..." + @set -o allexport; . ./.app.env; set +o allexport; $(GOBIN)/$(APP_NAME) + +# Build docker image +docker-build: + @echo "Building Docker image..." + @docker build -t $(IMAGE_NAME):$(VERSION) . + +# Start docker compose services +docker-up: + @echo "Starting Docker services..." + @$(DOCKER_COMPOSE) up --build -d + +# Stop docker compose services +docker-down: + @echo "Stopping Docker services..." + @$(DOCKER_COMPOSE) down + +# Start development environment +dev: swagger docker-up + @echo "Starting development environment..." + @$(DOCKER_COMPOSE) logs -f api + +# Update dependencies +deps-update: + @echo "Updating dependencies..." + @go get -u ./... + @go mod tidy + +# Verify dependencies +deps-verify: + @echo "Verifying dependencies..." + @go mod verify + +# Check for security vulnerabilities +security-check: + @echo "Checking for security vulnerabilities..." + @gosec ./... + +# Format code +fmt: + @echo "Formatting code..." + @go fmt ./... + +# Show help +help: + @echo "Available targets:" + @echo " make : Build the application after cleaning" + @echo " make build : Build the application" + @echo " make clean : Clean build artifacts" + @echo " make test : Run tests" + @echo " make coverage : Run tests with coverage report" + @echo " make lint : Run linter" + @echo " make swagger : Generate Swagger documentation" + @echo " make run : Run the application locally" + @echo " make docker-build : Build Docker image" + @echo " make docker-up : Start Docker services" + @echo " make docker-down : Stop Docker services" + @echo " make dev : Start development environment" + @echo " make deps-update : Update dependencies" + @echo " make deps-verify : Verify dependencies" + @echo " make security-check: Check for security vulnerabilities" + @echo " make fmt : Format code" + +# Install development tools +.PHONY: install-tools +install-tools: + @echo "Installing development tools..." + @go install github.com/swaggo/swag/cmd/swag@latest + @go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + @go install github.com/securego/gosec/v2/cmd/gosec@latest From 067bbed0479f4dbf4b53ed31155db4eccb0e3255 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 11:32:46 +0200 Subject: [PATCH 07/28] add package doc --- Makefile | 1 + internal/build/build.go | 5 +++++ internal/clients/idenfy/idenfy.go | 6 ++++++ internal/clients/substrate/substrate.go | 4 ++++ internal/configs/config.go | 4 ++++ internal/errors/errors.go | 4 ++++ internal/handlers/handlers.go | 8 ++++++++ internal/logger/logger.go | 5 +++++ internal/server/server.go | 9 +++++++++ internal/services/{kyc_service.go => services.go} | 4 ++++ 10 files changed, 50 insertions(+) rename internal/services/{kyc_service.go => services.go} (93%) diff --git a/Makefile b/Makefile index dfcd867..69e0706 100644 --- a/Makefile +++ b/Makefile @@ -121,6 +121,7 @@ help: @echo " make deps-verify : Verify dependencies" @echo " make security-check: Check for security vulnerabilities" @echo " make fmt : Format code" + @echo " make install-tools: Install development tools" # Install development tools .PHONY: install-tools diff --git a/internal/build/build.go b/internal/build/build.go index 4f365c1..65a4b98 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -1,3 +1,8 @@ +/* +Package build contains the build information for the application. +This package is responsible for providing the build information to the application. +This information is injected at build time using ldflags. +*/ package build var Version string = "unknown" diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index a8ecd1d..001c113 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -1,3 +1,9 @@ +/* +Package idenfy contains the iDenfy client for the application. +This layer is responsible for interacting with the iDenfy API. the main operations are: +- creating a verification session +- verifying the callback signature +*/ package idenfy import ( diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index d497951..2386dd0 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -1,3 +1,7 @@ +/* +Package substrate contains the Substrate client for the application. +This layer is responsible for interacting with the Substrate API. It wraps the tfchain go client and provide basic operations. +*/ package substrate import ( diff --git a/internal/configs/config.go b/internal/configs/config.go index 3210e2a..17fd24a 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -1,3 +1,7 @@ +/* +Package configs contains the configuration for the application. +This layer is responsible for loading the configuration from the environment variables and validating it. +*/ package configs import ( diff --git a/internal/errors/errors.go b/internal/errors/errors.go index fc0ea57..bccfd23 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -1,3 +1,7 @@ +/* +Package errors contains custom error types and constructors for the application. +This layer is responsible for defining the error types and constructors for the application. +*/ package errors import "fmt" diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 3fad50d..88beabf 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -1,3 +1,11 @@ +/* +Package handlers contains the handlers for the API. +This layer is responsible for handling the requests and responses, in more details: +- validating the requests +- formatting the responses +- handling the errors +- delegating the requests to the services +*/ package handlers import ( diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 33e056e..369860d 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -1,3 +1,8 @@ +/* +Package logger contains a Logger Wrapper to enable support for multiple logging libraries. +This is a layer between the application code and the underlying logging library. +It provides a simplified API that abstracts away the complexity of different logging libraries, making it easier to switch between them or add new ones. +*/ package logger import ( diff --git a/internal/server/server.go b/internal/server/server.go index 695da3a..2d70a11 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,3 +1,12 @@ +/* +Package server contains the HTTP server for the application. +This layer is responsible for initializing the server and its dependencies. in more details: +- setting up the middleware +- setting up the database +- setting up the repositories +- setting up the services +- setting up the routes +*/ package server import ( diff --git a/internal/services/kyc_service.go b/internal/services/services.go similarity index 93% rename from internal/services/kyc_service.go rename to internal/services/services.go index 7974c96..3d41a06 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/services.go @@ -1,3 +1,7 @@ +/* +Package services contains the services for the application. +This layer is responsible for handling the business logic. +*/ package services import ( From 7a914e8f66d3c482716388516493c7cc24b4b5bf Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 11:37:55 +0200 Subject: [PATCH 08/28] refactor: rename configs package to config --- cmd/api/main.go | 4 ++-- internal/clients/idenfy/idenfy_test.go | 6 +++--- internal/{configs => config}/config.go | 4 ++-- internal/handlers/handlers.go | 6 +++--- internal/logger/logger.go | 4 ++-- internal/middleware/middleware.go | 4 ++-- internal/server/server.go | 6 +++--- internal/services/services.go | 8 ++++---- scripts/dev/balance/check-account-balance.go | 2 +- scripts/dev/chain/chain_name.go | 2 +- scripts/dev/twin/get-address-by-twin-id.go | 2 +- 11 files changed, 24 insertions(+), 24 deletions(-) rename internal/{configs => config}/config.go (98%) diff --git a/cmd/api/main.go b/cmd/api/main.go index db5158e..fc48525 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -4,13 +4,13 @@ import ( "log" _ "github.com/threefoldtech/tf-kyc-verifier/api/docs" - "github.com/threefoldtech/tf-kyc-verifier/internal/configs" + "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/server" ) func main() { - config, err := configs.LoadConfig() + config, err := config.LoadConfig() if err != nil { log.Fatal("Failed to load configuration:", err) } diff --git a/internal/clients/idenfy/idenfy_test.go b/internal/clients/idenfy/idenfy_test.go index e494a74..277a0c7 100644 --- a/internal/clients/idenfy/idenfy_test.go +++ b/internal/clients/idenfy/idenfy_test.go @@ -8,16 +8,16 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/threefoldtech/tf-kyc-verifier/internal/configs" + "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/models" ) func TestClient_DecodeReaderIdentityCallback(t *testing.T) { expectedSig := "249d9a838e9b981935324b02367ca72552aa430fc766f45f77fab7a81f9f3b9d" - logger.Init(configs.Log{}) + logger.Init(config.Log{}) log := logger.GetLogger() - client := New(&configs.Idenfy{ + client := New(&config.Idenfy{ CallbackSignKey: "TestingKey", }, log) diff --git a/internal/configs/config.go b/internal/config/config.go similarity index 98% rename from internal/configs/config.go rename to internal/config/config.go index 17fd24a..b9f5e6b 100644 --- a/internal/configs/config.go +++ b/internal/config/config.go @@ -1,8 +1,8 @@ /* -Package configs contains the configuration for the application. +Package config contains the configuration for the application. This layer is responsible for loading the configuration from the environment variables and validating it. */ -package configs +package config import ( "errors" diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 88beabf..7f05928 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -19,7 +19,7 @@ import ( "go.mongodb.org/mongo-driver/mongo/readpref" "github.com/threefoldtech/tf-kyc-verifier/internal/build" - "github.com/threefoldtech/tf-kyc-verifier/internal/configs" + "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/errors" "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/models" @@ -29,7 +29,7 @@ import ( type Handler struct { kycService services.KYCService - config *configs.Config + config *config.Config logger logger.Logger } @@ -42,7 +42,7 @@ type Handler struct { // @contact.url https://codescalers-egypt.com // @contact.email info@codescalers.com // @BasePath / -func NewHandler(kycService services.KYCService, config *configs.Config, logger logger.Logger) *Handler { +func NewHandler(kycService services.KYCService, config *config.Config, logger logger.Logger) *Handler { return &Handler{kycService: kycService, config: config, logger: logger} } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 369860d..d2cf397 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -8,7 +8,7 @@ package logger import ( "context" - "github.com/threefoldtech/tf-kyc-verifier/internal/configs" + "github.com/threefoldtech/tf-kyc-verifier/internal/config" ) type LoggerW struct { @@ -19,7 +19,7 @@ type Fields map[string]interface{} var log *LoggerW -func Init(config configs.Log) { +func Init(config config.Log) { zapLogger, err := NewZapLogger(config.Debug, context.Background()) if err != nil { panic(err) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index c51a13e..ba1cd9a 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -7,7 +7,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/threefoldtech/tf-kyc-verifier/internal/configs" + "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/errors" "github.com/threefoldtech/tf-kyc-verifier/internal/handlers" "github.com/threefoldtech/tf-kyc-verifier/internal/logger" @@ -22,7 +22,7 @@ func CORS() fiber.Handler { } // AuthMiddleware is a middleware that validates the authentication credentials -func AuthMiddleware(config configs.Challenge) fiber.Handler { +func AuthMiddleware(config config.Challenge) fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") signature := c.Get("X-Signature") diff --git a/internal/server/server.go b/internal/server/server.go index 2d70a11..baf5ee4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -30,7 +30,7 @@ import ( _ "github.com/threefoldtech/tf-kyc-verifier/api/docs" "github.com/threefoldtech/tf-kyc-verifier/internal/clients/idenfy" "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" - "github.com/threefoldtech/tf-kyc-verifier/internal/configs" + "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/handlers" "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/middleware" @@ -42,12 +42,12 @@ import ( // Server represents the HTTP server and its dependencies type Server struct { app *fiber.App - config *configs.Config + config *config.Config logger logger.Logger } // New creates a new server instance with the given configuration and options -func New(config *configs.Config, srvLogger logger.Logger) (*Server, error) { +func New(config *config.Config, srvLogger logger.Logger) (*Server, error) { // Create base context for initialization ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() diff --git a/internal/services/services.go b/internal/services/services.go index 3d41a06..9f4358e 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -9,7 +9,7 @@ import ( "github.com/threefoldtech/tf-kyc-verifier/internal/clients/idenfy" "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" - "github.com/threefoldtech/tf-kyc-verifier/internal/configs" + "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/errors" "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/repository" @@ -22,17 +22,17 @@ type kycService struct { tokenRepo repository.TokenRepository idenfy idenfy.IdenfyClient substrate substrate.SubstrateClient - config *configs.Verification + config *config.Verification logger logger.Logger IdenfySuffix string } -func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy idenfy.IdenfyClient, substrateClient substrate.SubstrateClient, config *configs.Config, logger logger.Logger) KYCService { +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy idenfy.IdenfyClient, substrateClient substrate.SubstrateClient, config *config.Config, logger logger.Logger) KYCService { idenfySuffix := GetIdenfySuffix(substrateClient, config) return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: &config.Verification, logger: logger, IdenfySuffix: idenfySuffix} } -func GetIdenfySuffix(substrateClient substrate.SubstrateClient, config *configs.Config) string { +func GetIdenfySuffix(substrateClient substrate.SubstrateClient, config *config.Config) string { idenfySuffix := GetChainNetworkName(substrateClient) if config.Idenfy.Namespace != "" { idenfySuffix = config.Idenfy.Namespace + ":" + idenfySuffix diff --git a/scripts/dev/balance/check-account-balance.go b/scripts/dev/balance/check-account-balance.go index ff50348..caee9d1 100644 --- a/scripts/dev/balance/check-account-balance.go +++ b/scripts/dev/balance/check-account-balance.go @@ -55,7 +55,7 @@ type TFChainConfig struct { WsProviderURL string } -// implement SubstrateConfig for configs.TFChain +// implement SubstrateConfig for config.TFChain func (c *TFChainConfig) GetWsProviderURL() string { return c.WsProviderURL } diff --git a/scripts/dev/chain/chain_name.go b/scripts/dev/chain/chain_name.go index 90b6d95..a6cedeb 100644 --- a/scripts/dev/chain/chain_name.go +++ b/scripts/dev/chain/chain_name.go @@ -56,7 +56,7 @@ type TFChainConfig struct { WsProviderURL string } -// implement SubstrateConfig for configs.TFChain +// implement SubstrateConfig for config.TFChain func (c *TFChainConfig) GetWsProviderURL() string { return c.WsProviderURL } diff --git a/scripts/dev/twin/get-address-by-twin-id.go b/scripts/dev/twin/get-address-by-twin-id.go index 174bb68..339df5d 100644 --- a/scripts/dev/twin/get-address-by-twin-id.go +++ b/scripts/dev/twin/get-address-by-twin-id.go @@ -56,7 +56,7 @@ type TFChainConfig struct { WsProviderURL string } -// implement SubstrateConfig for configs.TFChain +// implement SubstrateConfig for config.TFChain func (c *TFChainConfig) GetWsProviderURL() string { return c.WsProviderURL } From 25b99b6d8085e518cade57f6256c75e867ad0473 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 11:53:35 +0200 Subject: [PATCH 09/28] refactor: use uint for MaxTokenRequests and TokenExpiration --- internal/config/config.go | 8 ++++---- internal/server/server.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index b9f5e6b..378d25a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -85,12 +85,12 @@ type Verification struct { AlwaysVerifiedIDs []string `env:"VERIFICATION_ALWAYS_VERIFIED_IDS" env-separator:","` } type IPLimiter struct { - MaxTokenRequests int `env:"IP_LIMITER_MAX_TOKEN_REQUESTS" env-default:"4"` - TokenExpiration int `env:"IP_LIMITER_TOKEN_EXPIRATION" env-default:"1440"` + MaxTokenRequests uint `env:"IP_LIMITER_MAX_TOKEN_REQUESTS" env-default:"4"` + TokenExpiration uint `env:"IP_LIMITER_TOKEN_EXPIRATION" env-default:"1440"` } type IDLimiter struct { - MaxTokenRequests int `env:"ID_LIMITER_MAX_TOKEN_REQUESTS" env-default:"4"` - TokenExpiration int `env:"ID_LIMITER_TOKEN_EXPIRATION" env-default:"1440"` + MaxTokenRequests uint `env:"ID_LIMITER_MAX_TOKEN_REQUESTS" env-default:"4"` + TokenExpiration uint `env:"ID_LIMITER_TOKEN_EXPIRATION" env-default:"1440"` } type Log struct { Debug bool `env:"DEBUG" env-default:"false"` diff --git a/internal/server/server.go b/internal/server/server.go index baf5ee4..5b31810 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -127,7 +127,7 @@ func (s *Server) setupMiddleware() error { // Configure rate limiters ipLimiterConfig := limiter.Config{ - Max: s.config.IPLimiter.MaxTokenRequests, + Max: int(s.config.IPLimiter.MaxTokenRequests), Expiration: time.Duration(s.config.IPLimiter.TokenExpiration) * time.Minute, Storage: ipLimiterStore, KeyGenerator: func(c *fiber.Ctx) string { @@ -140,7 +140,7 @@ func (s *Server) setupMiddleware() error { } idLimiterConfig := limiter.Config{ - Max: s.config.IDLimiter.MaxTokenRequests, + Max: int(s.config.IDLimiter.MaxTokenRequests), Expiration: time.Duration(s.config.IDLimiter.TokenExpiration) * time.Minute, Storage: idLimiterStore, KeyGenerator: func(c *fiber.Ctx) string { From c1efabea71b77c9a2513bc47a8815ba365e9a028 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 12:23:42 +0200 Subject: [PATCH 10/28] refactor: rename LoadConfig to LoadConfigFromEnv --- cmd/api/main.go | 2 +- internal/config/config.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index fc48525..6e42fd6 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -10,7 +10,7 @@ import ( ) func main() { - config, err := config.LoadConfig() + config, err := config.LoadConfigFromEnv() if err != nil { log.Fatal("Failed to load configuration:", err) } diff --git a/internal/config/config.go b/internal/config/config.go index 378d25a..f4a7ba3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -100,7 +100,7 @@ type Challenge struct { Domain string `env:"CHALLENGE_DOMAIN" env-required:"true"` } -func LoadConfig() (*Config, error) { +func LoadConfigFromEnv() (*Config, error) { cfg := &Config{} err := cleanenv.ReadEnv(cfg) if err != nil { From 2b04efcd05a2e418bda6d31a081c1dc3f794481e Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 13:09:05 +0200 Subject: [PATCH 11/28] refactor: rename server.Start() to server.Run() --- cmd/api/main.go | 2 +- internal/server/server.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 6e42fd6..ca87fb1 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -32,5 +32,5 @@ func main() { srvLogger.Info("Starting server on port:", logger.Fields{ "port": config.Server.Port, }) - server.Start() + server.Run() } diff --git a/internal/server/server.go b/internal/server/server.go index 5b31810..200e96d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -268,7 +268,7 @@ func extractIPFromRequest(c *fiber.Ctx) string { return "127.0.0.1" } -func (s *Server) Start() { +func (s *Server) Run() { go func() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) From 3e9e58f297542d9368b677dc65ca179cd16d883f Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 13:36:03 +0200 Subject: [PATCH 12/28] doc: change conatct info in swagger --- api/docs/docs.go | 6 +++--- api/docs/swagger.json | 6 +++--- api/docs/swagger.yaml | 6 +++--- internal/handlers/handlers.go | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index cfdcdf4..90c6b30 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -11,9 +11,9 @@ const docTemplate = `{ "title": "{{.Title}}", "termsOfService": "http://swagger.io/terms/", "contact": { - "name": "Codescalers Egypt", - "url": "https://codescalers-egypt.com", - "email": "info@codescalers.com" + "name": "threefold.io", + "url": "https://threefold.io", + "email": "info@threefold.io" }, "version": "{{.Version}}" }, diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 42ec22a..5009c08 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -5,9 +5,9 @@ "title": "TFGrid KYC API", "termsOfService": "http://swagger.io/terms/", "contact": { - "name": "Codescalers Egypt", - "url": "https://codescalers-egypt.com", - "email": "info@codescalers.com" + "name": "threefold.io", + "url": "https://threefold.io", + "email": "info@threefold.io" }, "version": "0.2.0" }, diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 1e97399..2dd0d55 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -145,9 +145,9 @@ definitions: type: object info: contact: - email: info@codescalers.com - name: Codescalers Egypt - url: https://codescalers-egypt.com + email: info@threefold.io + name: threefold.io + url: https://threefold.io description: This is a KYC service for TFGrid. termsOfService: http://swagger.io/terms/ title: TFGrid KYC API diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 7f05928..89ba67f 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -38,9 +38,9 @@ type Handler struct { // @description This is a KYC service for TFGrid. // @termsOfService http://swagger.io/terms/ -// @contact.name Codescalers Egypt -// @contact.url https://codescalers-egypt.com -// @contact.email info@codescalers.com +// @contact.name threefold.io +// @contact.url https://threefold.io +// @contact.email info@threefold.io // @BasePath / func NewHandler(kycService services.KYCService, config *config.Config, logger logger.Logger) *Handler { return &Handler{kycService: kycService, config: config, logger: logger} From 987a561724b8de1c5231fa2f8c17b681e3dea7ac Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 13:46:52 +0200 Subject: [PATCH 13/28] refactor: rename substrate config interface to WsProviderURLGetter --- internal/clients/substrate/interface.go | 11 ----------- internal/clients/substrate/substrate.go | 12 +++++++++++- 2 files changed, 11 insertions(+), 12 deletions(-) delete mode 100644 internal/clients/substrate/interface.go diff --git a/internal/clients/substrate/interface.go b/internal/clients/substrate/interface.go deleted file mode 100644 index 7d991cc..0000000 --- a/internal/clients/substrate/interface.go +++ /dev/null @@ -1,11 +0,0 @@ -package substrate - -type SubstrateConfig interface { - GetWsProviderURL() string -} - -type SubstrateClient interface { - GetChainName() (string, error) - GetAddressByTwinID(twinID string) (string, error) - GetAccountBalance(address string) (uint64, error) -} diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index 2386dd0..6216381 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -15,12 +15,22 @@ import ( tfchain "github.com/threefoldtech/tfchain/clients/tfchain-client-go" ) +type WsProviderURLGetter interface { + GetWsProviderURL() string +} + +type SubstrateClient interface { + GetChainName() (string, error) + GetAddressByTwinID(twinID string) (string, error) + GetAccountBalance(address string) (uint64, error) +} + type Substrate struct { api *tfchain.Substrate logger logger.Logger } -func New(config SubstrateConfig, logger logger.Logger) (*Substrate, error) { +func New(config WsProviderURLGetter, logger logger.Logger) (*Substrate, error) { mgr := tfchain.NewManager(config.GetWsProviderURL()) api, err := mgr.Substrate() if err != nil { From 4c518839899c35346accf86d816188f1e4a0dceb Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 14:26:27 +0200 Subject: [PATCH 14/28] refactor error messages --- internal/clients/idenfy/idenfy.go | 11 ++++------- internal/clients/substrate/substrate.go | 17 +++++++---------- internal/config/config.go | 10 +++++----- internal/logger/zap_logger.go | 3 ++- internal/middleware/middleware.go | 4 ++-- internal/repository/mongo.go | 5 +++-- internal/server/server.go | 16 ++++++++-------- internal/services/tokens.go | 12 ++++++------ internal/services/verification.go | 12 ++++++------ 9 files changed, 43 insertions(+), 47 deletions(-) diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index 001c113..f45d69b 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -59,7 +59,7 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) jsonBody, err := json.Marshal(RequestBody) if err != nil { - return models.Token{}, fmt.Errorf("error marshaling request body: %w", err) + return models.Token{}, fmt.Errorf("marshaling request body: %w", err) } req.SetBody(jsonBody) // Set deadline from context @@ -75,7 +75,7 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) }) err = c.client.Do(req, resp) if err != nil { - return models.Token{}, fmt.Errorf("error sending request: %w", err) + return models.Token{}, fmt.Errorf("sending token request to iDenfy: %w", err) } if resp.StatusCode() < 200 || resp.StatusCode() >= 300 { @@ -83,7 +83,7 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) "status": resp.StatusCode(), "error": string(resp.Body()), }) - return models.Token{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) + return models.Token{}, fmt.Errorf("unexpected status code from iDenfy: %d", resp.StatusCode()) } c.logger.Debug("Received response from iDenfy", logger.Fields{ "response": string(resp.Body()), @@ -91,7 +91,7 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) var result models.Token if err := json.Unmarshal(resp.Body(), &result); err != nil { - return models.Token{}, fmt.Errorf("error decoding response: %w", err) + return models.Token{}, fmt.Errorf("decoding token response from iDenfy: %w", err) } return result, nil @@ -99,9 +99,6 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) // verify signature of the callback func (c *Idenfy) VerifyCallbackSignature(ctx context.Context, body []byte, sigHeader string) error { - if len(c.config.GetCallbackSignKey()) < 1 { - return errors.New("callback was received but no signature key was provided") - } sig, err := hex.DecodeString(sigHeader) if err != nil { return err diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index 6216381..ec5475e 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -9,9 +9,6 @@ import ( "strconv" "github.com/threefoldtech/tf-kyc-verifier/internal/logger" - - // use tfchain go client - tfchain "github.com/threefoldtech/tfchain/clients/tfchain-client-go" ) @@ -34,7 +31,7 @@ func New(config WsProviderURLGetter, logger logger.Logger) (*Substrate, error) { mgr := tfchain.NewManager(config.GetWsProviderURL()) api, err := mgr.Substrate() if err != nil { - return nil, fmt.Errorf("substrate connection error: failed to initialize Substrate client: %w", err) + return nil, fmt.Errorf("initializing Substrate client: %w", err) } c := &Substrate{ @@ -47,7 +44,7 @@ func New(config WsProviderURLGetter, logger logger.Logger) (*Substrate, error) { func (c *Substrate) GetAccountBalance(address string) (uint64, error) { pubkeyBytes, err := tfchain.FromAddress(address) if err != nil { - return 0, fmt.Errorf("failed to decode ss58 address: %w", err) + return 0, fmt.Errorf("decoding ss58 address: %w", err) } accountID := tfchain.AccountID(pubkeyBytes) balance, err := c.api.GetBalance(accountID) @@ -55,7 +52,7 @@ func (c *Substrate) GetAccountBalance(address string) (uint64, error) { if err.Error() == "account not found" { return 0, nil } - return 0, fmt.Errorf("failed to get balance: %w", err) + return 0, fmt.Errorf("getting account balance: %w", err) } return balance.Free.Uint64(), nil @@ -64,11 +61,11 @@ func (c *Substrate) GetAccountBalance(address string) (uint64, error) { func (c *Substrate) GetAddressByTwinID(twinID string) (string, error) { twinIDUint32, err := strconv.ParseUint(twinID, 10, 32) if err != nil { - return "", fmt.Errorf("failed to parse twin ID: %w", err) + return "", fmt.Errorf("parsing twin ID: %w", err) } twin, err := c.api.GetTwin(uint32(twinIDUint32)) if err != nil { - return "", fmt.Errorf("failed to get twin: %w", err) + return "", fmt.Errorf("getting twin from tfchain: %w", err) } return twin.Account.String(), nil } @@ -77,11 +74,11 @@ func (c *Substrate) GetAddressByTwinID(twinID string) (string, error) { func (c *Substrate) GetChainName() (string, error) { api, _, err := c.api.GetClient() if err != nil { - return "", fmt.Errorf("failed to get substrate client: %w", err) + return "", fmt.Errorf("getting substrate inner client: %w", err) } chain, err := api.RPC.System.Chain() if err != nil { - return "", fmt.Errorf("failed to get chain: %w", err) + return "", fmt.Errorf("getting chain name: %w", err) } return string(chain), nil } diff --git a/internal/config/config.go b/internal/config/config.go index f4a7ba3..8a95880 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -104,7 +104,7 @@ func LoadConfigFromEnv() (*Config, error) { cfg := &Config{} err := cleanenv.ReadEnv(cfg) if err != nil { - return nil, errors.Join(errors.New("error loading config"), err) + return nil, errors.Join(errors.New("loading config"), err) } // cfg.Validate() return cfg, nil @@ -124,7 +124,7 @@ func (c Config) GetPublicConfig() Config { func (c *Config) Validate() error { // iDenfy base URL should be https://ivs.idenfy.com. This is the only supported base URL for now. if c.Idenfy.BaseURL != "https://ivs.idenfy.com" { - return errors.New("invalid iDenfy base URL. It should be https://ivs.idenfy.com") + return errors.New("invalid iDenfy base URL. it should be https://ivs.idenfy.com") } // CallbackUrl should be valid URL parsedCallbackUrl, err := url.ParseRequestURI(c.Idenfy.CallbackUrl) @@ -133,7 +133,7 @@ func (c *Config) Validate() error { } // CallbackSignKey should not be empty if len(c.Idenfy.CallbackSignKey) < 16 { - return errors.New("CallbackSignKey should be at least 16 characters long") + return errors.New("invalid callbackSignKey. it should be at least 16 characters long") } // WsProviderURL should be valid URL and start with wss:// if u, err := url.ParseRequestURI(c.TFChain.WsProviderURL); err != nil || u.Scheme != "wss" { @@ -149,11 +149,11 @@ func (c *Config) Validate() error { } // SuspiciousVerificationOutcome should be either APPROVED or REJECTED if !slices.Contains([]string{"APPROVED", "REJECTED"}, c.Verification.SuspiciousVerificationOutcome) { - return errors.New("invalid SuspiciousVerificationOutcome") + return errors.New("invalid SuspiciousVerificationOutcome. should be either APPROVED or REJECTED") } // ExpiredDocumentOutcome should be either APPROVED or REJECTED if !slices.Contains([]string{"APPROVED", "REJECTED"}, c.Verification.ExpiredDocumentOutcome) { - return errors.New("invalid ExpiredDocumentOutcome") + return errors.New("invalid ExpiredDocumentOutcome. should be either APPROVED or REJECTED") } // MinBalanceToVerifyAccount if c.Verification.MinBalanceToVerifyAccount < 20000000 { diff --git a/internal/logger/zap_logger.go b/internal/logger/zap_logger.go index 2f53803..085998b 100644 --- a/internal/logger/zap_logger.go +++ b/internal/logger/zap_logger.go @@ -2,6 +2,7 @@ package logger import ( "context" + "errors" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -21,7 +22,7 @@ func NewZapLogger(debug bool, ctx context.Context) (*ZapLogger, error) { zapConfig.DisableCaller = true zapLog, err := zapConfig.Build() if err != nil { - return nil, err + return nil, errors.Join(errors.New("building zap logger from the config"), err) } return &ZapLogger{logger: zapLog, ctx: ctx}, nil diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index ba1cd9a..2a3409e 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -85,14 +85,14 @@ func VerifySubstrateSignature(address, signature, challenge string) error { // Create a new ed25519 public key pubkeyEd25519, err := ed25519.Scheme{}.FromPublicKey(pubkeyBytes) if err != nil { - return errors.NewValidationError("error: can't create ed25519 public key", err) + return errors.NewValidationError("creating ed25519 public key", err) } if !pubkeyEd25519.Verify(challengeBytes, sig) { // Create a new sr25519 public key pubkeySr25519, err := sr25519.Scheme{}.FromPublicKey(pubkeyBytes) if err != nil { - return errors.NewValidationError("error: can't create sr25519 public key", err) + return errors.NewValidationError("creating sr25519 public key", err) } if !pubkeySr25519.Verify(challengeBytes, sig) { return errors.NewAuthorizationError("bad signature: signature does not match", nil) diff --git a/internal/repository/mongo.go b/internal/repository/mongo.go index 6f89485..ba27c4c 100644 --- a/internal/repository/mongo.go +++ b/internal/repository/mongo.go @@ -2,6 +2,7 @@ package repository import ( "context" + "errors" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -10,12 +11,12 @@ import ( func ConnectToMongoDB(ctx context.Context, mongoURI string) (*mongo.Client, error) { client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) if err != nil { - return nil, err + return nil, errors.Join(errors.New("connecting to MongoDB"), err) } err = client.Ping(ctx, nil) if err != nil { - return nil, err + return nil, errors.Join(errors.New("pinging MongoDB"), err) } return client, nil diff --git a/internal/server/server.go b/internal/server/server.go index 200e96d..f24218d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -68,7 +68,7 @@ func New(config *config.Config, srvLogger logger.Logger) (*Server, error) { // Initialize core components if err := server.initializeCore(ctx); err != nil { - return nil, fmt.Errorf("failed to initialize core components: %w", err) + return nil, fmt.Errorf("initializing core components: %w", err) } return server, nil @@ -78,30 +78,30 @@ func New(config *config.Config, srvLogger logger.Logger) (*Server, error) { func (s *Server) initializeCore(ctx context.Context) error { // Setup middleware if err := s.setupMiddleware(); err != nil { - return fmt.Errorf("failed to setup middleware: %w", err) + return fmt.Errorf("setting up middleware: %w", err) } // Setup database dbClient, db, err := s.setupDatabase(ctx) if err != nil { - return fmt.Errorf("failed to setup database: %w", err) + return fmt.Errorf("setting up database: %w", err) } // Setup repositories repos, err := s.setupRepositories(ctx, db) if err != nil { - return fmt.Errorf("failed to setup repositories: %w", err) + return fmt.Errorf("setting up repositories: %w", err) } // Setup services service, err := s.setupServices(repos) if err != nil { - return fmt.Errorf("failed to setup services: %w", err) + return fmt.Errorf("setting up services: %w", err) } // Setup routes if err := s.setupRoutes(service, dbClient); err != nil { - return fmt.Errorf("failed to setup routes: %w", err) + return fmt.Errorf("setting up routes: %w", err) } return nil @@ -172,7 +172,7 @@ func (s *Server) setupDatabase(ctx context.Context) (*mongo.Client, *mongo.Datab client, err := repository.ConnectToMongoDB(ctx, s.config.MongoDB.URI) if err != nil { - return nil, nil, errors.Join(fmt.Errorf("failed to connect to MongoDB: %w", err)) + return nil, nil, errors.Join(fmt.Errorf("setting up database: %w", err)) } return client, client.Database(s.config.MongoDB.DatabaseName), nil @@ -199,7 +199,7 @@ func (s *Server) setupServices(repos *repositories) (services.KYCService, error) substrateClient, err := substrate.New(&s.config.TFChain, s.logger) if err != nil { - return nil, fmt.Errorf("failed to initialize substrate client: %w", err) + return nil, fmt.Errorf("initializing substrate client: %w", err) } return services.NewKYCService( diff --git a/internal/services/tokens.go b/internal/services/tokens.go index 144bc41..4847932 100644 --- a/internal/services/tokens.go +++ b/internal/services/tokens.go @@ -14,7 +14,7 @@ func (s *kycService) GetorCreateVerificationToken(ctx context.Context, clientID isVerified, err := s.IsUserVerified(ctx, clientID) if err != nil { s.logger.Error("Error checking if user is verified", logger.Fields{"clientID": clientID, "error": err}) - return nil, false, errors.NewInternalError("error getting verification status from database", err) // db error + return nil, false, errors.NewInternalError("getting verification status from database", err) // db error } if isVerified { return nil, false, errors.NewConflictError("user already verified", nil) // TODO: implement a custom error that can be converted in the handler to a 4xx such 409 status code @@ -22,7 +22,7 @@ func (s *kycService) GetorCreateVerificationToken(ctx context.Context, clientID token, err_ := s.tokenRepo.GetToken(ctx, clientID) if err_ != nil { s.logger.Error("Error getting token from database", logger.Fields{"clientID": clientID, "error": err_}) - return nil, false, errors.NewInternalError("error getting token from database", err_) // db error + return nil, false, errors.NewInternalError("getting token from database", err_) // db error } // check if token is found and not expired if token != nil { @@ -38,7 +38,7 @@ func (s *kycService) GetorCreateVerificationToken(ctx context.Context, clientID hasRequiredBalance, err_ := s.AccountHasRequiredBalance(ctx, clientID) if err_ != nil { s.logger.Error("Error checking if user account has required balance", logger.Fields{"clientID": clientID, "error": err_}) - return nil, false, errors.NewExternalError("error checking if user account has required balance", err_) + return nil, false, errors.NewExternalError("checking if user account has required balance", err_) } if !hasRequiredBalance { requiredBalance := s.config.MinBalanceToVerifyAccount / TFT_CONVERSION_FACTOR @@ -49,7 +49,7 @@ func (s *kycService) GetorCreateVerificationToken(ctx context.Context, clientID newToken, err_ := s.idenfy.CreateVerificationSession(ctx, uniqueClientID) if err_ != nil { s.logger.Error("Error creating iDenfy verification session", logger.Fields{"clientID": clientID, "uniqueClientID": uniqueClientID, "error": err_}) - return nil, false, errors.NewExternalError("error creating iDenfy verification session", err_) + return nil, false, errors.NewExternalError("creating iDenfy verification session", err_) } // save the token with the original clientID newToken.ClientID = clientID @@ -66,7 +66,7 @@ func (s *kycService) DeleteToken(ctx context.Context, clientID string, scanRef s err := s.tokenRepo.DeleteToken(ctx, clientID, scanRef) if err != nil { s.logger.Error("Error deleting verification token from database", logger.Fields{"clientID": clientID, "scanRef": scanRef, "error": err}) - return errors.NewInternalError("error deleting verification token from database", err) + return errors.NewInternalError("deleting verification token from database", err) } return nil } @@ -79,7 +79,7 @@ func (s *kycService) AccountHasRequiredBalance(ctx context.Context, address stri balance, err := s.substrate.GetAccountBalance(address) if err != nil { s.logger.Error("Error getting account balance", logger.Fields{"address": address, "error": err}) - return false, errors.NewExternalError("error getting account balance", err) + return false, errors.NewExternalError("getting account balance", err) } return balance >= s.config.MinBalanceToVerifyAccount, nil } diff --git a/internal/services/verification.go b/internal/services/verification.go index 920ec85..d3e4b0a 100644 --- a/internal/services/verification.go +++ b/internal/services/verification.go @@ -14,7 +14,7 @@ func (s *kycService) GetVerificationData(ctx context.Context, clientID string) ( verification, err := s.verificationRepo.GetVerification(ctx, clientID) if err != nil { s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) - return nil, errors.NewInternalError("error getting verification from database", err) + return nil, errors.NewInternalError("getting verification from database", err) } return verification, nil } @@ -34,7 +34,7 @@ func (s *kycService) GetVerificationStatus(ctx context.Context, clientID string) verification, err := s.verificationRepo.GetVerification(ctx, clientID) if err != nil { s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) - return nil, errors.NewInternalError("error getting verification from database", err) + return nil, errors.NewInternalError("getting verification from database", err) } var outcome models.Outcome if verification != nil { @@ -59,7 +59,7 @@ func (s *kycService) GetVerificationStatusByTwinID(ctx context.Context, twinID s address, err := s.substrate.GetAddressByTwinID(twinID) if err != nil { s.logger.Error("Error getting address from twinID", logger.Fields{"twinID": twinID, "error": err}) - return nil, errors.NewExternalError("error looking up twinID address from TFChain", err) + return nil, errors.NewExternalError("looking up twinID address from TFChain", err) } return s.GetVerificationStatus(ctx, address) } @@ -68,7 +68,7 @@ func (s *kycService) ProcessVerificationResult(ctx context.Context, body []byte, err := s.idenfy.VerifyCallbackSignature(ctx, body, sigHeader) if err != nil { s.logger.Error("Error verifying callback signature", logger.Fields{"sigHeader": sigHeader, "error": err}) - return errors.NewAuthorizationError("error verifying callback signature", err) + return errors.NewAuthorizationError("verifying callback signature", err) } clientIDParts := strings.Split(result.ClientID, ":") if len(clientIDParts) < 2 { @@ -93,7 +93,7 @@ func (s *kycService) ProcessVerificationResult(ctx context.Context, body []byte, err = s.verificationRepo.SaveVerification(ctx, &result) if err != nil { s.logger.Error("Error saving verification to database", logger.Fields{"clientID": result.ClientID, "scanRef": result.IdenfyRef, "error": err}) - return errors.NewInternalError("error saving verification to database", err) + return errors.NewInternalError("saving verification to database", err) } } s.logger.Debug("Verification result processed successfully", logger.Fields{"result": result}) @@ -108,7 +108,7 @@ func (s *kycService) IsUserVerified(ctx context.Context, clientID string) (bool, verification, err := s.verificationRepo.GetVerification(ctx, clientID) if err != nil { s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) - return false, errors.NewInternalError("error getting verification from database", err) + return false, errors.NewInternalError("getting verification from database", err) } if verification == nil { return false, nil From 6ca048aeb9484b4bb709c7705f04da2b239d1125 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 14:35:17 +0200 Subject: [PATCH 15/28] refactor: update GetAddressByTwinID to accept uint32 instaed of string --- internal/clients/substrate/substrate.go | 16 +++++----------- internal/services/verification.go | 8 +++++++- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index ec5475e..d16595c 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -6,7 +6,6 @@ package substrate import ( "fmt" - "strconv" "github.com/threefoldtech/tf-kyc-verifier/internal/logger" tfchain "github.com/threefoldtech/tfchain/clients/tfchain-client-go" @@ -18,7 +17,7 @@ type WsProviderURLGetter interface { type SubstrateClient interface { GetChainName() (string, error) - GetAddressByTwinID(twinID string) (string, error) + GetAddressByTwinID(twinID uint32) (string, error) GetAccountBalance(address string) (uint64, error) } @@ -34,11 +33,10 @@ func New(config WsProviderURLGetter, logger logger.Logger) (*Substrate, error) { return nil, fmt.Errorf("initializing Substrate client: %w", err) } - c := &Substrate{ + return &Substrate{ api: api, logger: logger, - } - return c, nil + }, nil } func (c *Substrate) GetAccountBalance(address string) (uint64, error) { @@ -58,12 +56,8 @@ func (c *Substrate) GetAccountBalance(address string) (uint64, error) { return balance.Free.Uint64(), nil } -func (c *Substrate) GetAddressByTwinID(twinID string) (string, error) { - twinIDUint32, err := strconv.ParseUint(twinID, 10, 32) - if err != nil { - return "", fmt.Errorf("parsing twin ID: %w", err) - } - twin, err := c.api.GetTwin(uint32(twinIDUint32)) +func (c *Substrate) GetAddressByTwinID(twinID uint32) (string, error) { + twin, err := c.api.GetTwin(twinID) if err != nil { return "", fmt.Errorf("getting twin from tfchain: %w", err) } diff --git a/internal/services/verification.go b/internal/services/verification.go index d3e4b0a..6ee02e1 100644 --- a/internal/services/verification.go +++ b/internal/services/verification.go @@ -3,6 +3,7 @@ package services import ( "context" "slices" + "strconv" "strings" "github.com/threefoldtech/tf-kyc-verifier/internal/errors" @@ -56,7 +57,12 @@ func (s *kycService) GetVerificationStatus(ctx context.Context, clientID string) func (s *kycService) GetVerificationStatusByTwinID(ctx context.Context, twinID string) (*models.VerificationOutcome, error) { // get the address from the twinID - address, err := s.substrate.GetAddressByTwinID(twinID) + twinIDUint64, err := strconv.ParseUint(twinID, 10, 32) + if err != nil { + s.logger.Error("Error parsing twinID", logger.Fields{"twinID": twinID, "error": err}) + return nil, errors.NewInternalError("parsing twinID", err) + } + address, err := s.substrate.GetAddressByTwinID(uint32(twinIDUint64)) if err != nil { s.logger.Error("Error getting address from twinID", logger.Fields{"twinID": twinID, "error": err}) return nil, errors.NewExternalError("looking up twinID address from TFChain", err) From 75b791c5d24919917f973830e08497b0258055dd Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 14:38:56 +0200 Subject: [PATCH 16/28] fix dev script --- scripts/dev/twin/get-address-by-twin-id.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dev/twin/get-address-by-twin-id.go b/scripts/dev/twin/get-address-by-twin-id.go index 339df5d..84a882c 100644 --- a/scripts/dev/twin/get-address-by-twin-id.go +++ b/scripts/dev/twin/get-address-by-twin-id.go @@ -19,7 +19,7 @@ func main() { panic(err) } - address, err := substrateClient.GetAddressByTwinID("41") + address, err := substrateClient.GetAddressByTwinID(41) if err != nil { panic(err) } From 25be6c4b2631ddca57ace567d320deb0b5ddd9d7 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 14:39:31 +0200 Subject: [PATCH 17/28] refactor: use a shorter name msg in error package --- internal/errors/errors.go | 66 +++++++++++++++++------------------ internal/handlers/handlers.go | 2 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/internal/errors/errors.go b/internal/errors/errors.go index bccfd23..eb5b5c9 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -22,71 +22,71 @@ const ( // ServiceError represents a service-level error type ServiceError struct { - Type ErrorType - Message string - Err error + Type ErrorType + Msg string + Err error } func (e *ServiceError) Error() string { if e.Err != nil { - return fmt.Sprintf("%s: %s (%v)", e.Type, e.Message, e.Err) + return fmt.Sprintf("%s: %s (%v)", e.Type, e.Msg, e.Err) } - return fmt.Sprintf("%s: %s", e.Type, e.Message) + return fmt.Sprintf("%s: %s", e.Type, e.Msg) } // Error constructors -func NewValidationError(message string, err error) *ServiceError { +func NewValidationError(msg string, err error) *ServiceError { return &ServiceError{ - Type: ErrorTypeValidation, - Message: message, - Err: err, + Type: ErrorTypeValidation, + Msg: msg, + Err: err, } } -func NewAuthorizationError(message string, err error) *ServiceError { +func NewAuthorizationError(msg string, err error) *ServiceError { return &ServiceError{ - Type: ErrorTypeAuthorization, - Message: message, - Err: err, + Type: ErrorTypeAuthorization, + Msg: msg, + Err: err, } } -func NewNotFoundError(message string, err error) *ServiceError { +func NewNotFoundError(msg string, err error) *ServiceError { return &ServiceError{ - Type: ErrorTypeNotFound, - Message: message, - Err: err, + Type: ErrorTypeNotFound, + Msg: msg, + Err: err, } } -func NewConflictError(message string, err error) *ServiceError { +func NewConflictError(msg string, err error) *ServiceError { return &ServiceError{ - Type: ErrorTypeConflict, - Message: message, - Err: err, + Type: ErrorTypeConflict, + Msg: msg, + Err: err, } } -func NewInternalError(message string, err error) *ServiceError { +func NewInternalError(msg string, err error) *ServiceError { return &ServiceError{ - Type: ErrorTypeInternal, - Message: message, - Err: err, + Type: ErrorTypeInternal, + Msg: msg, + Err: err, } } -func NewExternalError(message string, err error) *ServiceError { +func NewExternalError(msg string, err error) *ServiceError { return &ServiceError{ - Type: ErrorTypeExternal, - Message: message, - Err: err, + Type: ErrorTypeExternal, + Msg: msg, + Err: err, } } -func NewNotSufficientBalanceError(message string, err error) *ServiceError { +func NewNotSufficientBalanceError(msg string, err error) *ServiceError { return &ServiceError{ - Type: ErrorTypeNotSufficientBalance, - Message: message, - Err: err, + Type: ErrorTypeNotSufficientBalance, + Msg: msg, + Err: err, } } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 89ba67f..5bf3643 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -277,7 +277,7 @@ func HandleError(c *fiber.Ctx, err error) error { func HandleServiceError(c *fiber.Ctx, err *errors.ServiceError) error { statusCode := getStatusCode(err.Type) return c.Status(statusCode).JSON(fiber.Map{ - "error": err.Message, + "error": err.Msg, }) } From d6d0f297adff4537b5c09e24f34a64b159c17e1d Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 14:43:49 +0200 Subject: [PATCH 18/28] fix Pascal Case in GetorCreateVerificationToken --- internal/handlers/handlers.go | 4 ++-- internal/server/server.go | 2 +- internal/services/interface.go | 2 +- internal/services/tokens.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 5bf3643..e5310ae 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -62,12 +62,12 @@ func NewHandler(kycService services.KYCService, config *config.Config, logger lo // @Failure 409 {object} responses.ErrorResponse // @Failure 500 {object} responses.ErrorResponse // @Router /api/v1/token [post] -func (h *Handler) GetorCreateVerificationToken() fiber.Handler { +func (h *Handler) GetOrCreateVerificationToken() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) defer cancel() - token, isNewToken, err := h.kycService.GetorCreateVerificationToken(ctx, clientID) + token, isNewToken, err := h.kycService.GetOrCreateVerificationToken(ctx, clientID) if err != nil { return HandleError(c, err) } diff --git a/internal/server/server.go b/internal/server/server.go index f24218d..792b2c7 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -219,7 +219,7 @@ func (s *Server) setupRoutes(kycService services.KYCService, mongoCl *mongo.Clie // API routes v1 := s.app.Group("/api/v1") - v1.Post("/token", middleware.AuthMiddleware(s.config.Challenge), handler.GetorCreateVerificationToken()) + v1.Post("/token", middleware.AuthMiddleware(s.config.Challenge), handler.GetOrCreateVerificationToken()) v1.Get("/data", middleware.AuthMiddleware(s.config.Challenge), handler.GetVerificationData()) v1.Get("/status", handler.GetVerificationStatus()) v1.Get("/health", handler.HealthCheck(mongoCl)) diff --git a/internal/services/interface.go b/internal/services/interface.go index ad0b264..f85a6ce 100644 --- a/internal/services/interface.go +++ b/internal/services/interface.go @@ -7,7 +7,7 @@ import ( ) type KYCService interface { - GetorCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) + GetOrCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) DeleteToken(ctx context.Context, clientID string, scanRef string) error AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) GetVerificationData(ctx context.Context, clientID string) (*models.Verification, error) diff --git a/internal/services/tokens.go b/internal/services/tokens.go index 4847932..eb77db1 100644 --- a/internal/services/tokens.go +++ b/internal/services/tokens.go @@ -10,7 +10,7 @@ import ( "github.com/threefoldtech/tf-kyc-verifier/internal/models" ) -func (s *kycService) GetorCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) { +func (s *kycService) GetOrCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) { isVerified, err := s.IsUserVerified(ctx, clientID) if err != nil { s.logger.Error("Error checking if user is verified", logger.Fields{"clientID": clientID, "error": err}) From 100ba32fa487180c155e2f28e5a40486ba2aaa4f Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 15:14:49 +0200 Subject: [PATCH 19/28] makes mongo-express optional and development-only --- README.md | 14 ++++++++++---- docker-compose.dev.yml | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 docker-compose.dev.yml diff --git a/README.md b/README.md index af477d4..2977b47 100644 --- a/README.md +++ b/README.md @@ -113,16 +113,22 @@ First make sure to create and set the environment variables in the `.app.env`, ` Examples can be found in `.app.env.example`, `.db.env.example`. In beta releases, we include the mongo-express container, but you can opt to disable it. -To start only the server and MongoDB using Docker Compose: +To start only the core services (API and MongoDB) using Docker Compose: ```bash -docker compose up -d db api +docker compose up -d ``` -For a full setup with mongo-express, make sure to create and set the environment variables in the `.express.env` file as well, then run: +To include mongo-express for development, make sure to create and set the environment variables in the `.express.env` file as well, then run: ```bash -docker compose up -d +docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d +``` + +To start only mongo-express if core services are already running, run: + +```bash +docker compose -f docker-compose.dev.yml up -d mongo-express ``` ### Running Locally diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..fbc2afc --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,20 @@ +services: + mongo-express: + image: mongo-express:latest + container_name: mongo_express + environment: + - ME_CONFIG_MONGODB_SERVER=db + - ME_CONFIG_MONGODB_PORT=27017 + depends_on: + - db + ports: + - "8888:8081" + env_file: + - .express.env + networks: + - default + +networks: + default: + external: true + name: tf_kyc_network \ No newline at end of file From fbed0b4af46a3093bb99ff7a5812237752c63976 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 15:28:25 +0200 Subject: [PATCH 20/28] replace errors.Join with fmt.Errorf --- internal/config/config.go | 3 ++- internal/logger/zap_logger.go | 4 ++-- internal/repository/mongo.go | 6 +++--- internal/server/server.go | 8 ++++---- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 8a95880..7c961cf 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ package config import ( "errors" + "fmt" "log" "net/url" "slices" @@ -104,7 +105,7 @@ func LoadConfigFromEnv() (*Config, error) { cfg := &Config{} err := cleanenv.ReadEnv(cfg) if err != nil { - return nil, errors.Join(errors.New("loading config"), err) + return nil, fmt.Errorf("loading config: %w", err) } // cfg.Validate() return cfg, nil diff --git a/internal/logger/zap_logger.go b/internal/logger/zap_logger.go index 085998b..c1bb74d 100644 --- a/internal/logger/zap_logger.go +++ b/internal/logger/zap_logger.go @@ -2,7 +2,7 @@ package logger import ( "context" - "errors" + "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -22,7 +22,7 @@ func NewZapLogger(debug bool, ctx context.Context) (*ZapLogger, error) { zapConfig.DisableCaller = true zapLog, err := zapConfig.Build() if err != nil { - return nil, errors.Join(errors.New("building zap logger from the config"), err) + return nil, fmt.Errorf("building zap logger from the config: %w", err) } return &ZapLogger{logger: zapLog, ctx: ctx}, nil diff --git a/internal/repository/mongo.go b/internal/repository/mongo.go index ba27c4c..6d5d0fc 100644 --- a/internal/repository/mongo.go +++ b/internal/repository/mongo.go @@ -2,7 +2,7 @@ package repository import ( "context" - "errors" + "fmt" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -11,12 +11,12 @@ import ( func ConnectToMongoDB(ctx context.Context, mongoURI string) (*mongo.Client, error) { client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) if err != nil { - return nil, errors.Join(errors.New("connecting to MongoDB"), err) + return nil, fmt.Errorf("connecting to MongoDB: %w", err) } err = client.Ping(ctx, nil) if err != nil { - return nil, errors.Join(errors.New("pinging MongoDB"), err) + return nil, fmt.Errorf("pinging MongoDB: %w", err) } return client, nil diff --git a/internal/server/server.go b/internal/server/server.go index 792b2c7..d349e70 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -11,7 +11,6 @@ package server import ( "context" - "errors" "fmt" "net" "net/http" @@ -172,7 +171,7 @@ func (s *Server) setupDatabase(ctx context.Context) (*mongo.Client, *mongo.Datab client, err := repository.ConnectToMongoDB(ctx, s.config.MongoDB.URI) if err != nil { - return nil, nil, errors.Join(fmt.Errorf("setting up database: %w", err)) + return nil, nil, fmt.Errorf("setting up database: %w", err) } return client, client.Database(s.config.MongoDB.DatabaseName), nil @@ -268,7 +267,7 @@ func extractIPFromRequest(c *fiber.Ctx) string { return "127.0.0.1" } -func (s *Server) Run() { +func (s *Server) Run() error { go func() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) @@ -284,6 +283,7 @@ func (s *Server) Run() { // Start server if err := s.app.Listen(":" + s.config.Server.Port); err != nil && err != http.ErrServerClosed { - s.logger.Fatal("Server startup failed", logger.Fields{"error": err}) + return fmt.Errorf("starting server: %w", err) } + return nil } From ec97b898aeac5285c8d57598b9428b7b4ca8fa93 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 15:46:18 +0200 Subject: [PATCH 21/28] refactor: keep a single exit point for the service --- cmd/api/main.go | 17 ++++++++++++++--- internal/clients/idenfy/idenfy_test.go | 5 ++++- internal/logger/logger.go | 12 +++++++----- internal/server/server.go | 9 ++++++--- internal/services/services.go | 26 ++++++++++++++++---------- 5 files changed, 47 insertions(+), 22 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index ca87fb1..58b5136 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -15,8 +15,14 @@ func main() { log.Fatal("Failed to load configuration:", err) } - logger.Init(config.Log) - srvLogger := logger.GetLogger() + err = logger.Init(config.Log) + if err != nil { + log.Fatal("Failed to initialize logger:", err) + } + srvLogger, err := logger.GetLogger() + if err != nil { + log.Fatal("Failed to get logger:", err) + } srvLogger.Debug("Configuration loaded successfully", logger.Fields{ "config": config.GetPublicConfig(), @@ -32,5 +38,10 @@ func main() { srvLogger.Info("Starting server on port:", logger.Fields{ "port": config.Server.Port, }) - server.Run() + err = server.Run() + if err != nil { + srvLogger.Fatal("Failed to start server", logger.Fields{ + "error": err, + }) + } } diff --git a/internal/clients/idenfy/idenfy_test.go b/internal/clients/idenfy/idenfy_test.go index 277a0c7..0b40322 100644 --- a/internal/clients/idenfy/idenfy_test.go +++ b/internal/clients/idenfy/idenfy_test.go @@ -16,7 +16,10 @@ import ( func TestClient_DecodeReaderIdentityCallback(t *testing.T) { expectedSig := "249d9a838e9b981935324b02367ca72552aa430fc766f45f77fab7a81f9f3b9d" logger.Init(config.Log{}) - log := logger.GetLogger() + log, err := logger.GetLogger() + if err != nil { + t.Fatalf("getting logger: %v", err) + } client := New(&config.Idenfy{ CallbackSignKey: "TestingKey", }, log) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index d2cf397..2d11fab 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -7,6 +7,7 @@ package logger import ( "context" + "fmt" "github.com/threefoldtech/tf-kyc-verifier/internal/config" ) @@ -19,20 +20,21 @@ type Fields map[string]interface{} var log *LoggerW -func Init(config config.Log) { +func Init(config config.Log) error { zapLogger, err := NewZapLogger(config.Debug, context.Background()) if err != nil { - panic(err) + return fmt.Errorf("initializing zap logger: %w", err) } log = &LoggerW{logger: zapLogger} + return nil } -func GetLogger() *LoggerW { +func GetLogger() (*LoggerW, error) { if log == nil { - panic("logger not initialized") + return nil, fmt.Errorf("logger not initialized") } - return log + return log, nil } func (lw *LoggerW) Debug(msg string, fields Fields) { diff --git a/internal/server/server.go b/internal/server/server.go index d349e70..2043269 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -200,15 +200,18 @@ func (s *Server) setupServices(repos *repositories) (services.KYCService, error) if err != nil { return nil, fmt.Errorf("initializing substrate client: %w", err) } - - return services.NewKYCService( + kycService, err := services.NewKYCService( repos.verification, repos.token, idenfyClient, substrateClient, s.config, s.logger, - ), nil + ) + if err != nil { + return nil, err + } + return kycService, nil } func (s *Server) setupRoutes(kycService services.KYCService, mongoCl *mongo.Client) error { diff --git a/internal/services/services.go b/internal/services/services.go index 9f4358e..bb25f54 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -5,12 +5,12 @@ This layer is responsible for handling the business logic. package services import ( + "fmt" "strings" "github.com/threefoldtech/tf-kyc-verifier/internal/clients/idenfy" "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" "github.com/threefoldtech/tf-kyc-verifier/internal/config" - "github.com/threefoldtech/tf-kyc-verifier/internal/errors" "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/repository" ) @@ -27,25 +27,31 @@ type kycService struct { IdenfySuffix string } -func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy idenfy.IdenfyClient, substrateClient substrate.SubstrateClient, config *config.Config, logger logger.Logger) KYCService { - idenfySuffix := GetIdenfySuffix(substrateClient, config) - return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: &config.Verification, logger: logger, IdenfySuffix: idenfySuffix} +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy idenfy.IdenfyClient, substrateClient substrate.SubstrateClient, config *config.Config, logger logger.Logger) (KYCService, error) { + idenfySuffix, err := GetIdenfySuffix(substrateClient, config) + if err != nil { + return nil, fmt.Errorf("getting idenfy suffix: %w", err) + } + return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: &config.Verification, logger: logger, IdenfySuffix: idenfySuffix}, nil } -func GetIdenfySuffix(substrateClient substrate.SubstrateClient, config *config.Config) string { - idenfySuffix := GetChainNetworkName(substrateClient) +func GetIdenfySuffix(substrateClient substrate.SubstrateClient, config *config.Config) (string, error) { + idenfySuffix, err := GetChainNetworkName(substrateClient) + if err != nil { + return "", fmt.Errorf("getting chain network name: %w", err) + } if config.Idenfy.Namespace != "" { idenfySuffix = config.Idenfy.Namespace + ":" + idenfySuffix } - return idenfySuffix + return idenfySuffix, nil } -func GetChainNetworkName(substrateClient substrate.SubstrateClient) string { +func GetChainNetworkName(substrateClient substrate.SubstrateClient) (string, error) { chainName, err := substrateClient.GetChainName() if err != nil { - panic(errors.NewInternalError("error getting chain name", err)) + return "", err } chainNameParts := strings.Split(chainName, " ") chainNetworkName := strings.ToLower(chainNameParts[len(chainNameParts)-1]) - return chainNetworkName + return chainNetworkName, nil } From fd0bef08faa0664944c99ec7b451fa3acad635d5 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 16:31:41 +0200 Subject: [PATCH 22/28] refactor: consolidate files in services package and remove the interface based on code review --- internal/handlers/handlers.go | 4 +- internal/server/server.go | 4 +- internal/services/interface.go | 19 --- internal/services/services.go | 203 +++++++++++++++++++++++++++++- internal/services/tokens.go | 85 ------------- internal/services/verification.go | 123 ------------------ 6 files changed, 204 insertions(+), 234 deletions(-) delete mode 100644 internal/services/interface.go delete mode 100644 internal/services/tokens.go delete mode 100644 internal/services/verification.go diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index e5310ae..32b58c9 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -28,7 +28,7 @@ import ( ) type Handler struct { - kycService services.KYCService + kycService *services.KYCService config *config.Config logger logger.Logger } @@ -42,7 +42,7 @@ type Handler struct { // @contact.url https://threefold.io // @contact.email info@threefold.io // @BasePath / -func NewHandler(kycService services.KYCService, config *config.Config, logger logger.Logger) *Handler { +func NewHandler(kycService *services.KYCService, config *config.Config, logger logger.Logger) *Handler { return &Handler{kycService: kycService, config: config, logger: logger} } diff --git a/internal/server/server.go b/internal/server/server.go index 2043269..433ae0b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -191,7 +191,7 @@ func (s *Server) setupRepositories(ctx context.Context, db *mongo.Database) (*re }, nil } -func (s *Server) setupServices(repos *repositories) (services.KYCService, error) { +func (s *Server) setupServices(repos *repositories) (*services.KYCService, error) { s.logger.Debug("Setting up services", nil) idenfyClient := idenfy.New(&s.config.Idenfy, s.logger) @@ -214,7 +214,7 @@ func (s *Server) setupServices(repos *repositories) (services.KYCService, error) return kycService, nil } -func (s *Server) setupRoutes(kycService services.KYCService, mongoCl *mongo.Client) error { +func (s *Server) setupRoutes(kycService *services.KYCService, mongoCl *mongo.Client) error { s.logger.Debug("Setting up routes", nil) handler := handlers.NewHandler(kycService, s.config, s.logger) diff --git a/internal/services/interface.go b/internal/services/interface.go deleted file mode 100644 index f85a6ce..0000000 --- a/internal/services/interface.go +++ /dev/null @@ -1,19 +0,0 @@ -package services - -import ( - "context" - - "github.com/threefoldtech/tf-kyc-verifier/internal/models" -) - -type KYCService interface { - GetOrCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) - DeleteToken(ctx context.Context, clientID string, scanRef string) error - AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) - GetVerificationData(ctx context.Context, clientID string) (*models.Verification, error) - GetVerificationStatus(ctx context.Context, clientID string) (*models.VerificationOutcome, error) - GetVerificationStatusByTwinID(ctx context.Context, twinID string) (*models.VerificationOutcome, error) - ProcessVerificationResult(ctx context.Context, body []byte, sigHeader string, result models.Verification) error - ProcessDocExpirationNotification(ctx context.Context, clientID string) error - IsUserVerified(ctx context.Context, clientID string) (bool, error) -} diff --git a/internal/services/services.go b/internal/services/services.go index bb25f54..a6d4a53 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -5,19 +5,25 @@ This layer is responsible for handling the business logic. package services import ( + "context" "fmt" + "slices" + "strconv" "strings" + "time" "github.com/threefoldtech/tf-kyc-verifier/internal/clients/idenfy" "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" "github.com/threefoldtech/tf-kyc-verifier/internal/config" + "github.com/threefoldtech/tf-kyc-verifier/internal/errors" "github.com/threefoldtech/tf-kyc-verifier/internal/logger" + "github.com/threefoldtech/tf-kyc-verifier/internal/models" "github.com/threefoldtech/tf-kyc-verifier/internal/repository" ) const TFT_CONVERSION_FACTOR = 10000000 -type kycService struct { +type KYCService struct { verificationRepo repository.VerificationRepository tokenRepo repository.TokenRepository idenfy idenfy.IdenfyClient @@ -27,12 +33,12 @@ type kycService struct { IdenfySuffix string } -func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy idenfy.IdenfyClient, substrateClient substrate.SubstrateClient, config *config.Config, logger logger.Logger) (KYCService, error) { +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy idenfy.IdenfyClient, substrateClient substrate.SubstrateClient, config *config.Config, logger logger.Logger) (*KYCService, error) { idenfySuffix, err := GetIdenfySuffix(substrateClient, config) if err != nil { return nil, fmt.Errorf("getting idenfy suffix: %w", err) } - return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: &config.Verification, logger: logger, IdenfySuffix: idenfySuffix}, nil + return &KYCService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: &config.Verification, logger: logger, IdenfySuffix: idenfySuffix}, nil } func GetIdenfySuffix(substrateClient substrate.SubstrateClient, config *config.Config) (string, error) { @@ -55,3 +61,194 @@ func GetChainNetworkName(substrateClient substrate.SubstrateClient) (string, err chainNetworkName := strings.ToLower(chainNameParts[len(chainNameParts)-1]) return chainNetworkName, nil } + +// ----------------------------- +// Token related methods +// ----------------------------- +func (s *KYCService) GetOrCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) { + isVerified, err := s.IsUserVerified(ctx, clientID) + if err != nil { + s.logger.Error("Error checking if user is verified", logger.Fields{"clientID": clientID, "error": err}) + return nil, false, errors.NewInternalError("getting verification status from database", err) // db error + } + if isVerified { + return nil, false, errors.NewConflictError("user already verified", nil) // TODO: implement a custom error that can be converted in the handler to a 4xx such 409 status code + } + token, err_ := s.tokenRepo.GetToken(ctx, clientID) + if err_ != nil { + s.logger.Error("Error getting token from database", logger.Fields{"clientID": clientID, "error": err_}) + return nil, false, errors.NewInternalError("getting token from database", err_) // db error + } + // check if token is found and not expired + if token != nil { + duration := time.Since(token.CreatedAt) + if duration < time.Duration(token.ExpiryTime)*time.Second { + remainingTime := time.Duration(token.ExpiryTime)*time.Second - duration + token.ExpiryTime = int(remainingTime.Seconds()) + return token, false, nil + } + } + + // check if user account balance satisfies the minimum required balance, return an error if not + hasRequiredBalance, err_ := s.AccountHasRequiredBalance(ctx, clientID) + if err_ != nil { + s.logger.Error("Error checking if user account has required balance", logger.Fields{"clientID": clientID, "error": err_}) + return nil, false, errors.NewExternalError("checking if user account has required balance", err_) + } + if !hasRequiredBalance { + requiredBalance := s.config.MinBalanceToVerifyAccount / TFT_CONVERSION_FACTOR + return nil, false, errors.NewNotSufficientBalanceError(fmt.Sprintf("account does not have the minimum required balance to verify (%d) TFT", requiredBalance), nil) + } + // prefix clientID with tfchain network prefix + uniqueClientID := clientID + ":" + s.IdenfySuffix + newToken, err_ := s.idenfy.CreateVerificationSession(ctx, uniqueClientID) + if err_ != nil { + s.logger.Error("Error creating iDenfy verification session", logger.Fields{"clientID": clientID, "uniqueClientID": uniqueClientID, "error": err_}) + return nil, false, errors.NewExternalError("creating iDenfy verification session", err_) + } + // save the token with the original clientID + newToken.ClientID = clientID + err_ = s.tokenRepo.SaveToken(ctx, &newToken) + if err_ != nil { + s.logger.Error("Error saving verification token to database", logger.Fields{"clientID": clientID, "error": err_}) + } + + return &newToken, true, nil +} + +func (s *KYCService) DeleteToken(ctx context.Context, clientID string, scanRef string) error { + + err := s.tokenRepo.DeleteToken(ctx, clientID, scanRef) + if err != nil { + s.logger.Error("Error deleting verification token from database", logger.Fields{"clientID": clientID, "scanRef": scanRef, "error": err}) + return errors.NewInternalError("deleting verification token from database", err) + } + return nil +} + +func (s *KYCService) AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) { + if s.config.MinBalanceToVerifyAccount == 0 { + s.logger.Warn("Minimum balance to verify account is 0 which is not recommended", logger.Fields{"address": address}) + return true, nil + } + balance, err := s.substrate.GetAccountBalance(address) + if err != nil { + s.logger.Error("Error getting account balance", logger.Fields{"address": address, "error": err}) + return false, errors.NewExternalError("getting account balance", err) + } + return balance >= s.config.MinBalanceToVerifyAccount, nil +} + +// ----------------------------- +// Verifications related methods +// ----------------------------- +func (s *KYCService) GetVerificationData(ctx context.Context, clientID string) (*models.Verification, error) { + verification, err := s.verificationRepo.GetVerification(ctx, clientID) + if err != nil { + s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) + return nil, errors.NewInternalError("getting verification from database", err) + } + return verification, nil +} + +func (s *KYCService) GetVerificationStatus(ctx context.Context, clientID string) (*models.VerificationOutcome, error) { + // check first if the clientID is in alwaysVerifiedAddresses + if s.config.AlwaysVerifiedIDs != nil && slices.Contains(s.config.AlwaysVerifiedIDs, clientID) { + final := true + s.logger.Info("ClientID is in always verified addresses. skipping verification", logger.Fields{"clientID": clientID}) + return &models.VerificationOutcome{ + Final: &final, + ClientID: clientID, + IdenfyRef: "", + Outcome: models.OutcomeApproved, + }, nil + } + verification, err := s.verificationRepo.GetVerification(ctx, clientID) + if err != nil { + s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) + return nil, errors.NewInternalError("getting verification from database", err) + } + var outcome models.Outcome + if verification != nil { + if verification.Status.Overall != nil && *verification.Status.Overall == models.OverallApproved || (s.config.SuspiciousVerificationOutcome == "APPROVED" && *verification.Status.Overall == models.OverallSuspected) { + outcome = models.OutcomeApproved + } else { + outcome = models.OutcomeRejected + } + } else { + return nil, nil + } + return &models.VerificationOutcome{ + Final: verification.Final, + ClientID: clientID, + IdenfyRef: verification.IdenfyRef, + Outcome: outcome, + }, nil +} + +func (s *KYCService) GetVerificationStatusByTwinID(ctx context.Context, twinID string) (*models.VerificationOutcome, error) { + // get the address from the twinID + twinIDUint64, err := strconv.ParseUint(twinID, 10, 32) + if err != nil { + s.logger.Error("Error parsing twinID", logger.Fields{"twinID": twinID, "error": err}) + return nil, errors.NewInternalError("parsing twinID", err) + } + address, err := s.substrate.GetAddressByTwinID(uint32(twinIDUint64)) + if err != nil { + s.logger.Error("Error getting address from twinID", logger.Fields{"twinID": twinID, "error": err}) + return nil, errors.NewExternalError("looking up twinID address from TFChain", err) + } + return s.GetVerificationStatus(ctx, address) +} + +func (s *KYCService) ProcessVerificationResult(ctx context.Context, body []byte, sigHeader string, result models.Verification) error { + err := s.idenfy.VerifyCallbackSignature(ctx, body, sigHeader) + if err != nil { + s.logger.Error("Error verifying callback signature", logger.Fields{"sigHeader": sigHeader, "error": err}) + return errors.NewAuthorizationError("verifying callback signature", err) + } + clientIDParts := strings.Split(result.ClientID, ":") + if len(clientIDParts) < 2 { + s.logger.Error("clientID have no network suffix", logger.Fields{"clientID": result.ClientID}) + return errors.NewInternalError("invalid clientID", nil) + } + networkSuffix := clientIDParts[len(clientIDParts)-1] + if networkSuffix != s.IdenfySuffix { + s.logger.Error("clientID has different network suffix", logger.Fields{"clientID": result.ClientID, "expectedSuffix": s.IdenfySuffix, "actualSuffix": networkSuffix}) + return errors.NewInternalError("invalid clientID", nil) + } + // delete the token with the same clientID and same scanRef + result.ClientID = clientIDParts[0] + + err = s.tokenRepo.DeleteToken(ctx, result.ClientID, result.IdenfyRef) + if err != nil { + s.logger.Warn("Error deleting verification token from database", logger.Fields{"clientID": result.ClientID, "scanRef": result.IdenfyRef, "error": err}) + } + // if the verification status is EXPIRED, we don't need to save it + if result.Status.Overall != nil && *result.Status.Overall != models.Overall("EXPIRED") { + // remove idenfy suffix from clientID + err = s.verificationRepo.SaveVerification(ctx, &result) + if err != nil { + s.logger.Error("Error saving verification to database", logger.Fields{"clientID": result.ClientID, "scanRef": result.IdenfyRef, "error": err}) + return errors.NewInternalError("saving verification to database", err) + } + } + s.logger.Debug("Verification result processed successfully", logger.Fields{"result": result}) + return nil +} + +func (s *KYCService) ProcessDocExpirationNotification(ctx context.Context, clientID string) error { + return nil +} + +func (s *KYCService) IsUserVerified(ctx context.Context, clientID string) (bool, error) { + verification, err := s.verificationRepo.GetVerification(ctx, clientID) + if err != nil { + s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) + return false, errors.NewInternalError("getting verification from database", err) + } + if verification == nil { + return false, nil + } + return verification.Status.Overall != nil && (*verification.Status.Overall == models.OverallApproved || (s.config.SuspiciousVerificationOutcome == "APPROVED" && *verification.Status.Overall == models.OverallSuspected)), nil +} diff --git a/internal/services/tokens.go b/internal/services/tokens.go deleted file mode 100644 index eb77db1..0000000 --- a/internal/services/tokens.go +++ /dev/null @@ -1,85 +0,0 @@ -package services - -import ( - "context" - "fmt" - "time" - - "github.com/threefoldtech/tf-kyc-verifier/internal/errors" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" - "github.com/threefoldtech/tf-kyc-verifier/internal/models" -) - -func (s *kycService) GetOrCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) { - isVerified, err := s.IsUserVerified(ctx, clientID) - if err != nil { - s.logger.Error("Error checking if user is verified", logger.Fields{"clientID": clientID, "error": err}) - return nil, false, errors.NewInternalError("getting verification status from database", err) // db error - } - if isVerified { - return nil, false, errors.NewConflictError("user already verified", nil) // TODO: implement a custom error that can be converted in the handler to a 4xx such 409 status code - } - token, err_ := s.tokenRepo.GetToken(ctx, clientID) - if err_ != nil { - s.logger.Error("Error getting token from database", logger.Fields{"clientID": clientID, "error": err_}) - return nil, false, errors.NewInternalError("getting token from database", err_) // db error - } - // check if token is found and not expired - if token != nil { - duration := time.Since(token.CreatedAt) - if duration < time.Duration(token.ExpiryTime)*time.Second { - remainingTime := time.Duration(token.ExpiryTime)*time.Second - duration - token.ExpiryTime = int(remainingTime.Seconds()) - return token, false, nil - } - } - - // check if user account balance satisfies the minimum required balance, return an error if not - hasRequiredBalance, err_ := s.AccountHasRequiredBalance(ctx, clientID) - if err_ != nil { - s.logger.Error("Error checking if user account has required balance", logger.Fields{"clientID": clientID, "error": err_}) - return nil, false, errors.NewExternalError("checking if user account has required balance", err_) - } - if !hasRequiredBalance { - requiredBalance := s.config.MinBalanceToVerifyAccount / TFT_CONVERSION_FACTOR - return nil, false, errors.NewNotSufficientBalanceError(fmt.Sprintf("account does not have the minimum required balance to verify (%d) TFT", requiredBalance), nil) - } - // prefix clientID with tfchain network prefix - uniqueClientID := clientID + ":" + s.IdenfySuffix - newToken, err_ := s.idenfy.CreateVerificationSession(ctx, uniqueClientID) - if err_ != nil { - s.logger.Error("Error creating iDenfy verification session", logger.Fields{"clientID": clientID, "uniqueClientID": uniqueClientID, "error": err_}) - return nil, false, errors.NewExternalError("creating iDenfy verification session", err_) - } - // save the token with the original clientID - newToken.ClientID = clientID - err_ = s.tokenRepo.SaveToken(ctx, &newToken) - if err_ != nil { - s.logger.Error("Error saving verification token to database", logger.Fields{"clientID": clientID, "error": err_}) - } - - return &newToken, true, nil -} - -func (s *kycService) DeleteToken(ctx context.Context, clientID string, scanRef string) error { - - err := s.tokenRepo.DeleteToken(ctx, clientID, scanRef) - if err != nil { - s.logger.Error("Error deleting verification token from database", logger.Fields{"clientID": clientID, "scanRef": scanRef, "error": err}) - return errors.NewInternalError("deleting verification token from database", err) - } - return nil -} - -func (s *kycService) AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) { - if s.config.MinBalanceToVerifyAccount == 0 { - s.logger.Warn("Minimum balance to verify account is 0 which is not recommended", logger.Fields{"address": address}) - return true, nil - } - balance, err := s.substrate.GetAccountBalance(address) - if err != nil { - s.logger.Error("Error getting account balance", logger.Fields{"address": address, "error": err}) - return false, errors.NewExternalError("getting account balance", err) - } - return balance >= s.config.MinBalanceToVerifyAccount, nil -} diff --git a/internal/services/verification.go b/internal/services/verification.go deleted file mode 100644 index 6ee02e1..0000000 --- a/internal/services/verification.go +++ /dev/null @@ -1,123 +0,0 @@ -package services - -import ( - "context" - "slices" - "strconv" - "strings" - - "github.com/threefoldtech/tf-kyc-verifier/internal/errors" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" - "github.com/threefoldtech/tf-kyc-verifier/internal/models" -) - -func (s *kycService) GetVerificationData(ctx context.Context, clientID string) (*models.Verification, error) { - verification, err := s.verificationRepo.GetVerification(ctx, clientID) - if err != nil { - s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) - return nil, errors.NewInternalError("getting verification from database", err) - } - return verification, nil -} - -func (s *kycService) GetVerificationStatus(ctx context.Context, clientID string) (*models.VerificationOutcome, error) { - // check first if the clientID is in alwaysVerifiedAddresses - if s.config.AlwaysVerifiedIDs != nil && slices.Contains(s.config.AlwaysVerifiedIDs, clientID) { - final := true - s.logger.Info("ClientID is in always verified addresses. skipping verification", logger.Fields{"clientID": clientID}) - return &models.VerificationOutcome{ - Final: &final, - ClientID: clientID, - IdenfyRef: "", - Outcome: models.OutcomeApproved, - }, nil - } - verification, err := s.verificationRepo.GetVerification(ctx, clientID) - if err != nil { - s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) - return nil, errors.NewInternalError("getting verification from database", err) - } - var outcome models.Outcome - if verification != nil { - if verification.Status.Overall != nil && *verification.Status.Overall == models.OverallApproved || (s.config.SuspiciousVerificationOutcome == "APPROVED" && *verification.Status.Overall == models.OverallSuspected) { - outcome = models.OutcomeApproved - } else { - outcome = models.OutcomeRejected - } - } else { - return nil, nil - } - return &models.VerificationOutcome{ - Final: verification.Final, - ClientID: clientID, - IdenfyRef: verification.IdenfyRef, - Outcome: outcome, - }, nil -} - -func (s *kycService) GetVerificationStatusByTwinID(ctx context.Context, twinID string) (*models.VerificationOutcome, error) { - // get the address from the twinID - twinIDUint64, err := strconv.ParseUint(twinID, 10, 32) - if err != nil { - s.logger.Error("Error parsing twinID", logger.Fields{"twinID": twinID, "error": err}) - return nil, errors.NewInternalError("parsing twinID", err) - } - address, err := s.substrate.GetAddressByTwinID(uint32(twinIDUint64)) - if err != nil { - s.logger.Error("Error getting address from twinID", logger.Fields{"twinID": twinID, "error": err}) - return nil, errors.NewExternalError("looking up twinID address from TFChain", err) - } - return s.GetVerificationStatus(ctx, address) -} - -func (s *kycService) ProcessVerificationResult(ctx context.Context, body []byte, sigHeader string, result models.Verification) error { - err := s.idenfy.VerifyCallbackSignature(ctx, body, sigHeader) - if err != nil { - s.logger.Error("Error verifying callback signature", logger.Fields{"sigHeader": sigHeader, "error": err}) - return errors.NewAuthorizationError("verifying callback signature", err) - } - clientIDParts := strings.Split(result.ClientID, ":") - if len(clientIDParts) < 2 { - s.logger.Error("clientID have no network suffix", logger.Fields{"clientID": result.ClientID}) - return errors.NewInternalError("invalid clientID", nil) - } - networkSuffix := clientIDParts[len(clientIDParts)-1] - if networkSuffix != s.IdenfySuffix { - s.logger.Error("clientID has different network suffix", logger.Fields{"clientID": result.ClientID, "expectedSuffix": s.IdenfySuffix, "actualSuffix": networkSuffix}) - return errors.NewInternalError("invalid clientID", nil) - } - // delete the token with the same clientID and same scanRef - result.ClientID = clientIDParts[0] - - err = s.tokenRepo.DeleteToken(ctx, result.ClientID, result.IdenfyRef) - if err != nil { - s.logger.Warn("Error deleting verification token from database", logger.Fields{"clientID": result.ClientID, "scanRef": result.IdenfyRef, "error": err}) - } - // if the verification status is EXPIRED, we don't need to save it - if result.Status.Overall != nil && *result.Status.Overall != models.Overall("EXPIRED") { - // remove idenfy suffix from clientID - err = s.verificationRepo.SaveVerification(ctx, &result) - if err != nil { - s.logger.Error("Error saving verification to database", logger.Fields{"clientID": result.ClientID, "scanRef": result.IdenfyRef, "error": err}) - return errors.NewInternalError("saving verification to database", err) - } - } - s.logger.Debug("Verification result processed successfully", logger.Fields{"result": result}) - return nil -} - -func (s *kycService) ProcessDocExpirationNotification(ctx context.Context, clientID string) error { - return nil -} - -func (s *kycService) IsUserVerified(ctx context.Context, clientID string) (bool, error) { - verification, err := s.verificationRepo.GetVerification(ctx, clientID) - if err != nil { - s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) - return false, errors.NewInternalError("getting verification from database", err) - } - if verification == nil { - return false, nil - } - return verification.Status.Overall != nil && (*verification.Status.Overall == models.OverallApproved || (s.config.SuspiciousVerificationOutcome == "APPROVED" && *verification.Status.Overall == models.OverallSuspected)), nil -} From eed65f749c6e3feb1924b5cf751f81cbb995d28e Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 16:42:17 +0200 Subject: [PATCH 23/28] use const for loopback and server timeout --- internal/server/server.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 433ae0b..ac39c26 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -38,6 +38,15 @@ import ( "go.mongodb.org/mongo-driver/mongo" ) +const ( + SERVER_STARTUP_TIMEOUT = 10 * time.Second + REQUEST_READ_TIMEOUT = 15 * time.Second + RESPONSE_WRITE_TIMEOUT = 15 * time.Second + CONNECTION_IDLE_TIMEOUT = 20 * time.Second + REQUETS_BODY_LIMIT = 512 * 1024 // 512KB + LOOPBACK = "127.0.0.1" +) + // Server represents the HTTP server and its dependencies type Server struct { app *fiber.App @@ -48,7 +57,7 @@ type Server struct { // New creates a new server instance with the given configuration and options func New(config *config.Config, srvLogger logger.Logger) (*Server, error) { // Create base context for initialization - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), SERVER_STARTUP_TIMEOUT) defer cancel() // Initialize server with base configuration @@ -59,10 +68,10 @@ func New(config *config.Config, srvLogger logger.Logger) (*Server, error) { // Initialize Fiber app with base configuration server.app = fiber.New(fiber.Config{ - ReadTimeout: 15 * time.Second, - WriteTimeout: 15 * time.Second, - IdleTimeout: 20 * time.Second, - BodyLimit: 512 * 1024, // 512KB + ReadTimeout: REQUEST_READ_TIMEOUT, + WriteTimeout: RESPONSE_WRITE_TIMEOUT, + IdleTimeout: CONNECTION_IDLE_TIMEOUT, + BodyLimit: REQUETS_BODY_LIMIT, }) // Initialize core components @@ -133,7 +142,7 @@ func (s *Server) setupMiddleware() error { return extractIPFromRequest(c) }, Next: func(c *fiber.Ctx) bool { - return extractIPFromRequest(c) == "127.0.0.1" + return extractIPFromRequest(c) == LOOPBACK }, SkipFailedRequests: true, } @@ -267,7 +276,7 @@ func extractIPFromRequest(c *fiber.Ctx) string { } } // If we still have a private IP, return a default value that will be skipped by the limiter - return "127.0.0.1" + return LOOPBACK } func (s *Server) Run() error { From beb431993ac803acff464c43ab1a4ce4810f2bd7 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 16:44:04 +0200 Subject: [PATCH 24/28] remove redundant check --- internal/server/server.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index ac39c26..39b9136 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -252,13 +252,10 @@ func extractIPFromRequest(c *fiber.Ctx) string { // Check for X-Forwarded-For header if ip := c.Get("X-Forwarded-For"); ip != "" { ips := strings.Split(ip, ",") - if len(ips) > 0 { - - for _, ip := range ips { - // return the first non-private ip in the list - if net.ParseIP(strings.TrimSpace(ip)) != nil && !net.ParseIP(strings.TrimSpace(ip)).IsPrivate() { - return strings.TrimSpace(ip) - } + for _, ip := range ips { + // return the first non-private ip in the list + if net.ParseIP(strings.TrimSpace(ip)) != nil && !net.ParseIP(strings.TrimSpace(ip)).IsPrivate() { + return strings.TrimSpace(ip) } } } From ca9baba39980783c7fe7be237ffdbf4b38830294 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 16:51:52 +0200 Subject: [PATCH 25/28] refactor: renaming functions and consolidating files --- internal/middleware/middleware.go | 12 ++++++------ internal/repository/interface.go | 18 ------------------ internal/repository/mongo.go | 14 +++++++++++++- internal/server/server.go | 2 +- 4 files changed, 20 insertions(+), 26 deletions(-) delete mode 100644 internal/repository/interface.go diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 2a3409e..72b7745 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -67,13 +67,13 @@ func fromHex(hex string) ([]byte, bool) { } func VerifySubstrateSignature(address, signature, challenge string) error { - challengeBytes, success := fromHex(challenge) - if !success { + challengeBytes, ok := fromHex(challenge) + if !ok { return errors.NewValidationError("malformed challenge: failed to decode hex-encoded challenge", nil) } // hex to string - sig, success := fromHex(signature) - if !success { + sig, ok := fromHex(signature) + if !ok { return errors.NewValidationError("malformed signature: failed to decode hex-encoded signature", nil) } // Convert address to public key @@ -104,8 +104,8 @@ func VerifySubstrateSignature(address, signature, challenge string) error { func ValidateChallenge(address, signature, challenge, expectedDomain string, challengeWindow int64) error { // Parse and validate the challenge - challengeBytes, success := fromHex(challenge) - if !success { + challengeBytes, ok := fromHex(challenge) + if !ok { return errors.NewValidationError("malformed challenge: failed to decode hex-encoded challenge", nil) } parts := strings.Split(string(challengeBytes), ":") diff --git a/internal/repository/interface.go b/internal/repository/interface.go deleted file mode 100644 index 1a4ee36..0000000 --- a/internal/repository/interface.go +++ /dev/null @@ -1,18 +0,0 @@ -package repository - -import ( - "context" - - "github.com/threefoldtech/tf-kyc-verifier/internal/models" -) - -type TokenRepository interface { - SaveToken(ctx context.Context, token *models.Token) error - GetToken(ctx context.Context, clientID string) (*models.Token, error) - DeleteToken(ctx context.Context, clientID string, scanRef string) error -} - -type VerificationRepository interface { - SaveVerification(ctx context.Context, verification *models.Verification) error - GetVerification(ctx context.Context, clientID string) (*models.Verification, error) -} diff --git a/internal/repository/mongo.go b/internal/repository/mongo.go index 6d5d0fc..1206271 100644 --- a/internal/repository/mongo.go +++ b/internal/repository/mongo.go @@ -4,11 +4,23 @@ import ( "context" "fmt" + "github.com/threefoldtech/tf-kyc-verifier/internal/models" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) -func ConnectToMongoDB(ctx context.Context, mongoURI string) (*mongo.Client, error) { +type TokenRepository interface { + SaveToken(ctx context.Context, token *models.Token) error + GetToken(ctx context.Context, clientID string) (*models.Token, error) + DeleteToken(ctx context.Context, clientID string, scanRef string) error +} + +type VerificationRepository interface { + SaveVerification(ctx context.Context, verification *models.Verification) error + GetVerification(ctx context.Context, clientID string) (*models.Verification, error) +} + +func NewMongoClient(ctx context.Context, mongoURI string) (*mongo.Client, error) { client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) if err != nil { return nil, fmt.Errorf("connecting to MongoDB: %w", err) diff --git a/internal/server/server.go b/internal/server/server.go index 39b9136..f97fba9 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -178,7 +178,7 @@ func (s *Server) setupMiddleware() error { func (s *Server) setupDatabase(ctx context.Context) (*mongo.Client, *mongo.Database, error) { s.logger.Debug("Connecting to database", nil) - client, err := repository.ConnectToMongoDB(ctx, s.config.MongoDB.URI) + client, err := repository.NewMongoClient(ctx, s.config.MongoDB.URI) if err != nil { return nil, nil, fmt.Errorf("setting up database: %w", err) } From 74a09c03048111b7b3df88b81354568381e0080c Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 16:59:24 +0200 Subject: [PATCH 26/28] remove Cors from middelware package --- internal/middleware/middleware.go | 6 ------ internal/server/server.go | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 72b7745..a5e31ff 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -6,7 +6,6 @@ import ( "time" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cors" "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/errors" "github.com/threefoldtech/tf-kyc-verifier/internal/handlers" @@ -16,11 +15,6 @@ import ( "github.com/vedhavyas/go-subkey/v2/sr25519" ) -// CORS returns a CORS middleware -func CORS() fiber.Handler { - return cors.New() -} - // AuthMiddleware is a middleware that validates the authentication credentials func AuthMiddleware(config config.Challenge) fiber.Handler { return func(c *fiber.Ctx) error { diff --git a/internal/server/server.go b/internal/server/server.go index f97fba9..08aea09 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -21,6 +21,7 @@ import ( "time" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/helmet" "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/fiber/v2/middleware/recover" @@ -159,7 +160,7 @@ func (s *Server) setupMiddleware() error { // Apply middleware s.app.Use(middleware.NewLoggingMiddleware(s.logger)) - s.app.Use(middleware.CORS()) + s.app.Use(cors.New()) s.app.Use(recover.New(recover.Config{ EnableStackTrace: true, })) From 4800a7272a5654464eaa138d5764dfbeee65ce89 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Tue, 5 Nov 2024 12:29:37 +0200 Subject: [PATCH 27/28] refactor: remove logger package, add response wrapper and helper functions --- api/docs/docs.go | 313 ++++++++++++++++-- api/docs/swagger.json | 313 ++++++++++++++++-- api/docs/swagger.yaml | 212 ++++++++++-- cmd/api/main.go | 40 +-- go.mod | 2 - go.sum | 6 - internal/clients/idenfy/idenfy.go | 19 +- internal/clients/idenfy/idenfy_test.go | 14 +- internal/clients/substrate/substrate.go | 6 +- internal/config/config.go | 8 +- internal/errors/errors.go | 2 +- internal/handlers/handlers.go | 114 +++---- internal/logger/interface.go | 8 - internal/logger/logger.go | 58 ---- internal/logger/zap_logger.go | 69 ---- internal/middleware/middleware.go | 44 +-- internal/repository/token_repository.go | 14 +- .../repository/verification_repository.go | 8 +- internal/responses/responses.go | 29 +- internal/server/server.go | 20 +- internal/services/services.go | 46 +-- scripts/dev/balance/check-account-balance.go | 30 +- scripts/dev/chain/chain_name.go | 30 +- scripts/dev/twin/get-address-by-twin-id.go | 30 +- 24 files changed, 949 insertions(+), 486 deletions(-) delete mode 100644 internal/logger/logger.go delete mode 100644 internal/logger/zap_logger.go diff --git a/api/docs/docs.go b/api/docs/docs.go index 90c6b30..73194ed 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -30,7 +30,14 @@ const docTemplate = `{ "responses": { "200": { "description": "OK", - "schema": {} + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.AppConfigsResponse" + } + } + } } } } @@ -79,31 +86,56 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.VerificationDataResponse" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.VerificationDataResponse" + } + } } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } } } @@ -120,7 +152,12 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.HealthResponse" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.HealthResponse" + } + } } } } @@ -160,25 +197,56 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.VerificationStatusResponse" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.VerificationStatusResponse" + } + } } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } } } @@ -228,43 +296,89 @@ const docTemplate = `{ "200": { "description": "Existing token retrieved", "schema": { - "$ref": "#/definitions/responses.TokenResponse" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.TokenResponse" + } + } } }, "201": { "description": "New token created", "schema": { - "$ref": "#/definitions/responses.TokenResponse" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.TokenResponse" + } + } } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "402": { "description": "Payment Required", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } } } @@ -281,7 +395,12 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "string" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.AppVersionResponse" + } + } } } } @@ -329,10 +448,162 @@ const docTemplate = `{ } }, "definitions": { - "responses.ErrorResponse": { + "config.Challenge": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "window": { + "type": "integer" + } + } + }, + "config.IDLimiter": { + "type": "object", + "properties": { + "maxTokenRequests": { + "type": "integer" + }, + "tokenExpiration": { + "type": "integer" + } + } + }, + "config.IPLimiter": { + "type": "object", + "properties": { + "maxTokenRequests": { + "type": "integer" + }, + "tokenExpiration": { + "type": "integer" + } + } + }, + "config.Idenfy": { + "type": "object", + "properties": { + "apikey": { + "type": "string" + }, + "apisecret": { + "type": "string" + }, + "baseURL": { + "type": "string" + }, + "callbackSignKey": { + "type": "string" + }, + "callbackUrl": { + "type": "string" + }, + "devMode": { + "type": "boolean" + }, + "namespace": { + "type": "string" + }, + "whitelistedIPs": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "config.Log": { + "type": "object", + "properties": { + "debug": { + "type": "boolean" + } + } + }, + "config.MongoDB": { + "type": "object", + "properties": { + "databaseName": { + "type": "string" + }, + "uri": { + "type": "string" + } + } + }, + "config.Server": { + "type": "object", + "properties": { + "port": { + "type": "string" + } + } + }, + "config.TFChain": { + "type": "object", + "properties": { + "wsProviderURL": { + "type": "string" + } + } + }, + "config.Verification": { + "type": "object", + "properties": { + "alwaysVerifiedIDs": { + "type": "array", + "items": { + "type": "string" + } + }, + "expiredDocumentOutcome": { + "type": "string" + }, + "minBalanceToVerifyAccount": { + "type": "integer" + }, + "suspiciousVerificationOutcome": { + "type": "string" + } + } + }, + "responses.AppConfigsResponse": { + "type": "object", + "properties": { + "challenge": { + "$ref": "#/definitions/config.Challenge" + }, + "idenfy": { + "$ref": "#/definitions/config.Idenfy" + }, + "idlimiter": { + "$ref": "#/definitions/config.IDLimiter" + }, + "iplimiter": { + "$ref": "#/definitions/config.IPLimiter" + }, + "log": { + "$ref": "#/definitions/config.Log" + }, + "mongoDB": { + "$ref": "#/definitions/config.MongoDB" + }, + "server": { + "$ref": "#/definitions/config.Server" + }, + "tfchain": { + "$ref": "#/definitions/config.TFChain" + }, + "verification": { + "$ref": "#/definitions/config.Verification" + } + } + }, + "responses.AppVersionResponse": { "type": "object", "properties": { - "error": { + "version": { "type": "string" } } diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 5009c08..b37ce8d 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -23,7 +23,14 @@ "responses": { "200": { "description": "OK", - "schema": {} + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.AppConfigsResponse" + } + } + } } } } @@ -72,31 +79,56 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.VerificationDataResponse" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.VerificationDataResponse" + } + } } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } } } @@ -113,7 +145,12 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.HealthResponse" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.HealthResponse" + } + } } } } @@ -153,25 +190,56 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.VerificationStatusResponse" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.VerificationStatusResponse" + } + } } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } } } @@ -221,43 +289,89 @@ "200": { "description": "Existing token retrieved", "schema": { - "$ref": "#/definitions/responses.TokenResponse" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.TokenResponse" + } + } } }, "201": { "description": "New token created", "schema": { - "$ref": "#/definitions/responses.TokenResponse" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.TokenResponse" + } + } } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "402": { "description": "Payment Required", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/responses.ErrorResponse" + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } } } } @@ -274,7 +388,12 @@ "200": { "description": "OK", "schema": { - "type": "string" + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/responses.AppVersionResponse" + } + } } } } @@ -322,10 +441,162 @@ } }, "definitions": { - "responses.ErrorResponse": { + "config.Challenge": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "window": { + "type": "integer" + } + } + }, + "config.IDLimiter": { + "type": "object", + "properties": { + "maxTokenRequests": { + "type": "integer" + }, + "tokenExpiration": { + "type": "integer" + } + } + }, + "config.IPLimiter": { + "type": "object", + "properties": { + "maxTokenRequests": { + "type": "integer" + }, + "tokenExpiration": { + "type": "integer" + } + } + }, + "config.Idenfy": { + "type": "object", + "properties": { + "apikey": { + "type": "string" + }, + "apisecret": { + "type": "string" + }, + "baseURL": { + "type": "string" + }, + "callbackSignKey": { + "type": "string" + }, + "callbackUrl": { + "type": "string" + }, + "devMode": { + "type": "boolean" + }, + "namespace": { + "type": "string" + }, + "whitelistedIPs": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "config.Log": { + "type": "object", + "properties": { + "debug": { + "type": "boolean" + } + } + }, + "config.MongoDB": { + "type": "object", + "properties": { + "databaseName": { + "type": "string" + }, + "uri": { + "type": "string" + } + } + }, + "config.Server": { + "type": "object", + "properties": { + "port": { + "type": "string" + } + } + }, + "config.TFChain": { + "type": "object", + "properties": { + "wsProviderURL": { + "type": "string" + } + } + }, + "config.Verification": { + "type": "object", + "properties": { + "alwaysVerifiedIDs": { + "type": "array", + "items": { + "type": "string" + } + }, + "expiredDocumentOutcome": { + "type": "string" + }, + "minBalanceToVerifyAccount": { + "type": "integer" + }, + "suspiciousVerificationOutcome": { + "type": "string" + } + } + }, + "responses.AppConfigsResponse": { + "type": "object", + "properties": { + "challenge": { + "$ref": "#/definitions/config.Challenge" + }, + "idenfy": { + "$ref": "#/definitions/config.Idenfy" + }, + "idlimiter": { + "$ref": "#/definitions/config.IDLimiter" + }, + "iplimiter": { + "$ref": "#/definitions/config.IPLimiter" + }, + "log": { + "$ref": "#/definitions/config.Log" + }, + "mongoDB": { + "$ref": "#/definitions/config.MongoDB" + }, + "server": { + "$ref": "#/definitions/config.Server" + }, + "tfchain": { + "$ref": "#/definitions/config.TFChain" + }, + "verification": { + "$ref": "#/definitions/config.Verification" + } + } + }, + "responses.AppVersionResponse": { "type": "object", "properties": { - "error": { + "version": { "type": "string" } } diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 2dd0d55..465ce7b 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -1,8 +1,106 @@ basePath: / definitions: - responses.ErrorResponse: + config.Challenge: properties: - error: + domain: + type: string + window: + type: integer + type: object + config.IDLimiter: + properties: + maxTokenRequests: + type: integer + tokenExpiration: + type: integer + type: object + config.IPLimiter: + properties: + maxTokenRequests: + type: integer + tokenExpiration: + type: integer + type: object + config.Idenfy: + properties: + apikey: + type: string + apisecret: + type: string + baseURL: + type: string + callbackSignKey: + type: string + callbackUrl: + type: string + devMode: + type: boolean + namespace: + type: string + whitelistedIPs: + items: + type: string + type: array + type: object + config.Log: + properties: + debug: + type: boolean + type: object + config.MongoDB: + properties: + databaseName: + type: string + uri: + type: string + type: object + config.Server: + properties: + port: + type: string + type: object + config.TFChain: + properties: + wsProviderURL: + type: string + type: object + config.Verification: + properties: + alwaysVerifiedIDs: + items: + type: string + type: array + expiredDocumentOutcome: + type: string + minBalanceToVerifyAccount: + type: integer + suspiciousVerificationOutcome: + type: string + type: object + responses.AppConfigsResponse: + properties: + challenge: + $ref: '#/definitions/config.Challenge' + idenfy: + $ref: '#/definitions/config.Idenfy' + idlimiter: + $ref: '#/definitions/config.IDLimiter' + iplimiter: + $ref: '#/definitions/config.IPLimiter' + log: + $ref: '#/definitions/config.Log' + mongoDB: + $ref: '#/definitions/config.MongoDB' + server: + $ref: '#/definitions/config.Server' + tfchain: + $ref: '#/definitions/config.TFChain' + verification: + $ref: '#/definitions/config.Verification' + type: object + responses.AppVersionResponse: + properties: + version: type: string type: object responses.HealthResponse: @@ -159,7 +257,11 @@ paths: responses: "200": description: OK - schema: {} + schema: + properties: + result: + $ref: '#/definitions/responses.AppConfigsResponse' + type: object summary: Get Service Configs tags: - Misc @@ -194,23 +296,38 @@ paths: "200": description: OK schema: - $ref: '#/definitions/responses.VerificationDataResponse' + properties: + result: + $ref: '#/definitions/responses.VerificationDataResponse' + type: object "400": description: Bad Request schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object "401": description: Unauthorized schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object "404": description: Not Found schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object summary: Get Verification Data tags: - Verification @@ -221,7 +338,10 @@ paths: "200": description: OK schema: - $ref: '#/definitions/responses.HealthResponse' + properties: + result: + $ref: '#/definitions/responses.HealthResponse' + type: object summary: Health Check tags: - Health @@ -248,19 +368,38 @@ paths: "200": description: OK schema: - $ref: '#/definitions/responses.VerificationStatusResponse' + properties: + result: + $ref: '#/definitions/responses.VerificationStatusResponse' + type: object "400": description: Bad Request schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object "404": description: Not Found schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object + "503": + description: Service Unavailable + schema: + properties: + error: + type: string + type: object summary: Get Verification Status tags: - Verification @@ -295,31 +434,59 @@ paths: "200": description: Existing token retrieved schema: - $ref: '#/definitions/responses.TokenResponse' + properties: + result: + $ref: '#/definitions/responses.TokenResponse' + type: object "201": description: New token created schema: - $ref: '#/definitions/responses.TokenResponse' + properties: + result: + $ref: '#/definitions/responses.TokenResponse' + type: object "400": description: Bad Request schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object "401": description: Unauthorized schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object "402": description: Payment Required schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object "409": description: Conflict schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.ErrorResponse' + properties: + error: + type: string + type: object + "503": + description: Service Unavailable + schema: + properties: + error: + type: string + type: object summary: Get or Generate iDenfy Verification Token tags: - Token @@ -330,7 +497,10 @@ paths: "200": description: OK schema: - type: string + properties: + result: + $ref: '#/definitions/responses.AppVersionResponse' + type: object summary: Get Service Version tags: - Misc diff --git a/cmd/api/main.go b/cmd/api/main.go index 58b5136..8a87440 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,47 +1,37 @@ package main import ( - "log" + "log/slog" + "os" _ "github.com/threefoldtech/tf-kyc-verifier/api/docs" "github.com/threefoldtech/tf-kyc-verifier/internal/config" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/server" ) func main() { config, err := config.LoadConfigFromEnv() if err != nil { - log.Fatal("Failed to load configuration:", err) + slog.Error("Failed to load configuration:", "error", err) + os.Exit(1) } - - err = logger.Init(config.Log) - if err != nil { - log.Fatal("Failed to initialize logger:", err) + logLevel := slog.LevelInfo + if config.Log.Debug { + logLevel = slog.LevelDebug } - srvLogger, err := logger.GetLogger() - if err != nil { - log.Fatal("Failed to get logger:", err) - } - - srvLogger.Debug("Configuration loaded successfully", logger.Fields{ - "config": config.GetPublicConfig(), - }) + logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel})) + logger.Debug("Configuration loaded successfully", "config", config.GetPublicConfig()) - server, err := server.New(config, srvLogger) + server, err := server.New(config, logger) if err != nil { - srvLogger.Error("Failed to create server:", logger.Fields{ - "error": err, - }) + logger.Error("Failed to create server:", "error", err) + os.Exit(1) } - srvLogger.Info("Starting server on port:", logger.Fields{ - "port": config.Server.Port, - }) + logger.Info("Starting server on port", "port", config.Server.Port) err = server.Run() if err != nil { - srvLogger.Fatal("Failed to start server", logger.Fields{ - "error": err, - }) + logger.Error("Server exited with error", "error", err) + os.Exit(1) } } diff --git a/go.mod b/go.mod index 6fa8884..308aab7 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/valyala/fasthttp v1.51.0 github.com/vedhavyas/go-subkey/v2 v2.0.0 go.mongodb.org/mongo-driver v1.17.1 - go.uber.org/zap v1.27.0 ) require ( @@ -68,7 +67,6 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.8.0 // indirect diff --git a/go.sum b/go.sum index 7eba6b4..d831879 100644 --- a/go.sum +++ b/go.sum @@ -170,12 +170,6 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= -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-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index f45d69b..91d26e3 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -15,9 +15,9 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "time" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/models" "github.com/valyala/fasthttp" ) @@ -25,14 +25,14 @@ import ( type Idenfy struct { client *fasthttp.Client // TODO: Interface config IdenfyConfig // TODO: Interface - logger logger.Logger + logger *slog.Logger } const ( VerificationSessionEndpoint = "/api/v2/token" ) -func New(config IdenfyConfig, logger logger.Logger) *Idenfy { +func New(config IdenfyConfig, logger *slog.Logger) *Idenfy { return &Idenfy{ client: &fasthttp.Client{}, config: config, @@ -70,24 +70,17 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) - c.logger.Debug("Preparing iDenfy verification session request", logger.Fields{ - "request": jsonBody, - }) + c.logger.Debug("Preparing iDenfy verification session request", "request", jsonBody) err = c.client.Do(req, resp) if err != nil { return models.Token{}, fmt.Errorf("sending token request to iDenfy: %w", err) } if resp.StatusCode() < 200 || resp.StatusCode() >= 300 { - c.logger.Debug("Received unexpected status code from iDenfy", logger.Fields{ - "status": resp.StatusCode(), - "error": string(resp.Body()), - }) + c.logger.Debug("Received unexpected status code from iDenfy", "status", resp.StatusCode(), "error", string(resp.Body())) return models.Token{}, fmt.Errorf("unexpected status code from iDenfy: %d", resp.StatusCode()) } - c.logger.Debug("Received response from iDenfy", logger.Fields{ - "response": string(resp.Body()), - }) + c.logger.Debug("Received response from iDenfy", "response", string(resp.Body())) var result models.Token if err := json.Unmarshal(resp.Body(), &result); err != nil { diff --git a/internal/clients/idenfy/idenfy_test.go b/internal/clients/idenfy/idenfy_test.go index 0b40322..9d642b0 100644 --- a/internal/clients/idenfy/idenfy_test.go +++ b/internal/clients/idenfy/idenfy_test.go @@ -4,25 +4,21 @@ import ( "bytes" "context" "encoding/json" + "log/slog" "os" "testing" "github.com/stretchr/testify/assert" "github.com/threefoldtech/tf-kyc-verifier/internal/config" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/models" ) func TestClient_DecodeReaderIdentityCallback(t *testing.T) { expectedSig := "249d9a838e9b981935324b02367ca72552aa430fc766f45f77fab7a81f9f3b9d" - logger.Init(config.Log{}) - log, err := logger.GetLogger() - if err != nil { - t.Fatalf("getting logger: %v", err) - } + logger := slog.Default() client := New(&config.Idenfy{ CallbackSignKey: "TestingKey", - }, log) + }, logger) assert.NotNil(t, client, "Client is nil") webhook1, err := os.ReadFile("testdata/webhook.1.json") @@ -34,9 +30,7 @@ func TestClient_DecodeReaderIdentityCallback(t *testing.T) { err = decoder.Decode(&resp) assert.NoError(t, err) // Basic verification info - log.Info("resp", logger.Fields{ - "resp": resp, - }) + logger.Info("resp", "resp", resp) assert.Equal(t, "123", resp.ClientID) assert.Equal(t, "scan-ref", resp.IdenfyRef) assert.Equal(t, "external-ref", resp.ExternalRef) diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index d16595c..868202c 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -6,8 +6,8 @@ package substrate import ( "fmt" + "log/slog" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" tfchain "github.com/threefoldtech/tfchain/clients/tfchain-client-go" ) @@ -23,10 +23,10 @@ type SubstrateClient interface { type Substrate struct { api *tfchain.Substrate - logger logger.Logger + logger *slog.Logger } -func New(config WsProviderURLGetter, logger logger.Logger) (*Substrate, error) { +func New(config WsProviderURLGetter, logger *slog.Logger) (*Substrate, error) { mgr := tfchain.NewManager(config.GetWsProviderURL()) api, err := mgr.Substrate() if err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index 7c961cf..7e05a7a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,7 +7,7 @@ package config import ( "errors" "fmt" - "log" + "log/slog" "net/url" "slices" @@ -158,15 +158,15 @@ func (c *Config) Validate() error { } // MinBalanceToVerifyAccount if c.Verification.MinBalanceToVerifyAccount < 20000000 { - log.Println("Warn: Verification MinBalanceToVerifyAccount is less than 20000000. This is not recommended and can lead to security issues. If you are sure about this, you can ignore this message.") + slog.Warn("Verification MinBalanceToVerifyAccount is less than 20000000. This is not recommended and can lead to security issues. If you are sure about this, you can ignore this message.") } // DevMode if c.Idenfy.DevMode { - log.Println("Warn: iDenfy DevMode is enabled. This is not intended for environments other than development. If you are sure about this, you can ignore this message.") + slog.Warn("iDenfy DevMode is enabled. This is not intended for environments other than development. If you are sure about this, you can ignore this message.") } // Namespace if c.Idenfy.Namespace != "" { - log.Println("Warn: iDenfy Namespace is set. This ideally should be empty. If you are sure about this, you can ignore this message.") + slog.Warn("iDenfy Namespace is set. This ideally should be empty. If you are sure about this, you can ignore this message.") } return nil } diff --git a/internal/errors/errors.go b/internal/errors/errors.go index eb5b5c9..d91aded 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -29,7 +29,7 @@ type ServiceError struct { func (e *ServiceError) Error() string { if e.Err != nil { - return fmt.Sprintf("%s: %s (%v)", e.Type, e.Msg, e.Err) + return fmt.Sprintf("%s: %s: %v", e.Type, e.Msg, e.Err) } return fmt.Sprintf("%s: %s", e.Type, e.Msg) } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 32b58c9..30aa72d 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -12,6 +12,8 @@ import ( "bytes" "context" "encoding/json" + "fmt" + "log/slog" "time" "github.com/gofiber/fiber/v2" @@ -21,7 +23,6 @@ import ( "github.com/threefoldtech/tf-kyc-verifier/internal/build" "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/errors" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/models" "github.com/threefoldtech/tf-kyc-verifier/internal/responses" "github.com/threefoldtech/tf-kyc-verifier/internal/services" @@ -30,7 +31,7 @@ import ( type Handler struct { kycService *services.KYCService config *config.Config - logger logger.Logger + logger *slog.Logger } // @title TFGrid KYC API @@ -42,7 +43,7 @@ type Handler struct { // @contact.url https://threefold.io // @contact.email info@threefold.io // @BasePath / -func NewHandler(kycService *services.KYCService, config *config.Config, logger logger.Logger) *Handler { +func NewHandler(kycService *services.KYCService, config *config.Config, logger *slog.Logger) *Handler { return &Handler{kycService: kycService, config: config, logger: logger} } @@ -54,13 +55,14 @@ func NewHandler(kycService *services.KYCService, config *config.Config, logger l // @Param X-Client-ID header string true "TFChain SS58Address" minlength(48) maxlength(48) // @Param X-Challenge header string true "hex-encoded message `{api-domain}:{timestamp}`" // @Param X-Signature header string true "hex-encoded sr25519|ed25519 signature" minlength(128) maxlength(128) -// @Success 200 {object} responses.TokenResponse "Existing token retrieved" -// @Success 201 {object} responses.TokenResponse "New token created" -// @Failure 400 {object} responses.ErrorResponse -// @Failure 401 {object} responses.ErrorResponse -// @Failure 402 {object} responses.ErrorResponse -// @Failure 409 {object} responses.ErrorResponse -// @Failure 500 {object} responses.ErrorResponse +// @Success 200 {object} object{result=responses.TokenResponse} "Existing token retrieved" +// @Success 201 {object} object{result=responses.TokenResponse} "New token created" +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 402 {object} object{error=string} +// @Failure 409 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Failure 503 {object} object{error=string} // @Router /api/v1/token [post] func (h *Handler) GetOrCreateVerificationToken() fiber.Handler { return func(c *fiber.Ctx) error { @@ -73,9 +75,9 @@ func (h *Handler) GetOrCreateVerificationToken() fiber.Handler { } response := responses.NewTokenResponseWithStatus(token, isNewToken) if isNewToken { - return c.Status(fiber.StatusCreated).JSON(fiber.Map{"result": response}) + return responses.RespondWithData(c, fiber.StatusCreated, response) } - return c.Status(fiber.StatusOK).JSON(fiber.Map{"result": response}) + return responses.RespondWithData(c, fiber.StatusOK, response) } } @@ -87,11 +89,11 @@ func (h *Handler) GetOrCreateVerificationToken() fiber.Handler { // @Param X-Client-ID header string true "TFChain SS58Address" minlength(48) maxlength(48) // @Param X-Challenge header string true "hex-encoded message `{api-domain}:{timestamp}`" // @Param X-Signature header string true "hex-encoded sr25519|ed25519 signature" minlength(128) maxlength(128) -// @Success 200 {object} responses.VerificationDataResponse -// @Failure 400 {object} responses.ErrorResponse -// @Failure 401 {object} responses.ErrorResponse -// @Failure 404 {object} responses.ErrorResponse -// @Failure 500 {object} responses.ErrorResponse +// @Success 200 {object} object{result=responses.VerificationDataResponse} +// @Failure 400 {object} object{error=string} +// @Failure 401 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Failure 500 {object} object{error=string} // @Router /api/v1/data [get] func (h *Handler) GetVerificationData() fiber.Handler { return func(c *fiber.Ctx) error { @@ -103,10 +105,10 @@ func (h *Handler) GetVerificationData() fiber.Handler { return HandleError(c, err) } if verification == nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Verification not found"}) + return responses.RespondWithError(c, fiber.StatusNotFound, fmt.Errorf("verification not found for client")) } response := responses.NewVerificationDataResponse(verification) - return c.JSON(fiber.Map{"result": response}) + return responses.RespondWithData(c, fiber.StatusOK, response) } } @@ -117,10 +119,11 @@ func (h *Handler) GetVerificationData() fiber.Handler { // @Produce json // @Param client_id query string false "TFChain SS58Address" minlength(48) maxlength(48) // @Param twin_id query string false "Twin ID" minlength(1) -// @Success 200 {object} responses.VerificationStatusResponse -// @Failure 400 {object} responses.ErrorResponse -// @Failure 404 {object} responses.ErrorResponse -// @Failure 500 {object} responses.ErrorResponse +// @Success 200 {object} object{result=responses.VerificationStatusResponse} +// @Failure 400 {object} object{error=string} +// @Failure 404 {object} object{error=string} +// @Failure 500 {object} object{error=string} +// @Failure 503 {object} object{error=string} // @Router /api/v1/status [get] func (h *Handler) GetVerificationStatus() fiber.Handler { return func(c *fiber.Ctx) error { @@ -128,8 +131,8 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { twinID := c.Query("twin_id") if clientID == "" && twinID == "" { - h.logger.Warn("Bad request: missing client_id and twin_id", nil) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Either client_id or twin_id must be provided"}) + h.logger.Warn("Bad request: missing client_id and twin_id") + return responses.RespondWithError(c, fiber.StatusBadRequest, fmt.Errorf("either client_id or twin_id must be provided")) } var verification *models.VerificationOutcome var err error @@ -141,22 +144,15 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { verification, err = h.kycService.GetVerificationStatusByTwinID(ctx, twinID) } if err != nil { - h.logger.Error("Failed to get verification status", logger.Fields{ - "clientID": clientID, - "twinID": twinID, - "error": err, - }) + h.logger.Error("Failed to get verification status", "clientID", clientID, "twinID", twinID, "error", err) return HandleError(c, err) } if verification == nil { - h.logger.Info("Verification not found", logger.Fields{ - "clientID": clientID, - "twinID": twinID, - }) - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Verification not found"}) + h.logger.Info("Verification not found", "clientID", clientID, "twinID", twinID) + return responses.RespondWithError(c, fiber.StatusNotFound, fmt.Errorf("verification not found")) } response := responses.NewVerificationStatusResponse(verification) - return c.JSON(fiber.Map{"result": response}) + return responses.RespondWithData(c, fiber.StatusOK, response) } } @@ -169,34 +165,30 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { // @Router /webhooks/idenfy/verification-update [post] func (h *Handler) ProcessVerificationResult() fiber.Handler { return func(c *fiber.Ctx) error { - h.logger.Debug("Received verification update", logger.Fields{ - "body": string(c.Body()), - "headers": &c.Request().Header, - }) + h.logger.Debug("Received verification update", + "body", string(c.Body()), + "headers", &c.Request().Header, + ) sigHeader := c.Get("Idenfy-Signature") if len(sigHeader) < 1 { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "No signature provided"}) + return responses.RespondWithError(c, fiber.StatusBadRequest, fmt.Errorf("no signature provided")) } body := c.Body() var result models.Verification decoder := json.NewDecoder(bytes.NewReader(body)) err := decoder.Decode(&result) if err != nil { - h.logger.Error("Error decoding verification update", logger.Fields{ - "error": err, - }) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + h.logger.Error("Error decoding verification update", "error", err) + return responses.RespondWithError(c, fiber.StatusBadRequest, err) } - h.logger.Debug("Verification update after decoding", logger.Fields{ - "result": result, - }) + h.logger.Debug("Verification update after decoding", "result", result) ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) defer cancel() err = h.kycService.ProcessVerificationResult(ctx, body, sigHeader, result) if err != nil { return HandleError(c, err) } - return c.SendStatus(fiber.StatusOK) + return responses.RespondWithData(c, fiber.StatusOK, nil) } } @@ -210,7 +202,7 @@ func (h *Handler) ProcessVerificationResult() fiber.Handler { func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { return func(c *fiber.Ctx) error { // TODO: implement - h.logger.Error("Received ID expiration notification but not implemented", nil) + h.logger.Error("Received ID expiration notification but not implemented") return c.SendStatus(fiber.StatusNotImplemented) } } @@ -218,7 +210,7 @@ func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { // @Summary Health Check // @Description Returns the health status of the service // @Tags Health -// @Success 200 {object} responses.HealthResponse +// @Success 200 {object} object{result=responses.HealthResponse} // @Router /api/v1/health [get] func (h *Handler) HealthCheck(dbClient *mongo.Client) fiber.Handler { return func(c *fiber.Ctx) error { @@ -232,7 +224,7 @@ func (h *Handler) HealthCheck(dbClient *mongo.Client) fiber.Handler { Timestamp: time.Now().UTC().Format(time.RFC3339), Errors: []string{err.Error()}, } - return c.JSON(health) + return responses.RespondWithData(c, fiber.StatusOK, health) } health := responses.HealthResponse{ Status: responses.HealthStatusHealthy, @@ -240,30 +232,30 @@ func (h *Handler) HealthCheck(dbClient *mongo.Client) fiber.Handler { Errors: []string{}, } - return c.JSON(fiber.Map{"result": health}) + return responses.RespondWithData(c, fiber.StatusOK, health) } } // @Summary Get Service Configs // @Description Returns the service configs // @Tags Misc -// @Success 200 {object} responses.AppConfigsResponse +// @Success 200 {object} object{result=responses.AppConfigsResponse} // @Router /api/v1/configs [get] func (h *Handler) GetServiceConfigs() fiber.Handler { return func(c *fiber.Ctx) error { - return c.JSON(fiber.Map{"result": h.config.GetPublicConfig()}) + return responses.RespondWithData(c, fiber.StatusOK, h.config.GetPublicConfig()) } } // @Summary Get Service Version // @Description Returns the service version // @Tags Misc -// @Success 200 {object} string +// @Success 200 {object} object{result=responses.AppVersionResponse} // @Router /api/v1/version [get] func (h *Handler) GetServiceVersion() fiber.Handler { return func(c *fiber.Ctx) error { response := responses.AppVersionResponse{Version: build.Version} - return c.JSON(fiber.Map{"result": response}) + return responses.RespondWithData(c, fiber.StatusOK, response) } } @@ -271,14 +263,12 @@ func HandleError(c *fiber.Ctx, err error) error { if serviceErr, ok := err.(*errors.ServiceError); ok { return HandleServiceError(c, serviceErr) } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return responses.RespondWithError(c, fiber.StatusInternalServerError, err) } func HandleServiceError(c *fiber.Ctx, err *errors.ServiceError) error { statusCode := getStatusCode(err.Type) - return c.Status(statusCode).JSON(fiber.Map{ - "error": err.Msg, - }) + return responses.RespondWithError(c, statusCode, err) } func getStatusCode(errorType errors.ErrorType) int { @@ -292,7 +282,7 @@ func getStatusCode(errorType errors.ErrorType) int { case errors.ErrorTypeConflict: return fiber.StatusConflict case errors.ErrorTypeExternal: - return fiber.StatusInternalServerError + return fiber.StatusServiceUnavailable case errors.ErrorTypeNotSufficientBalance: return fiber.StatusPaymentRequired default: diff --git a/internal/logger/interface.go b/internal/logger/interface.go index 2c36ee8..90c66f6 100644 --- a/internal/logger/interface.go +++ b/internal/logger/interface.go @@ -1,9 +1 @@ package logger - -type Logger interface { - Debug(msg string, fields Fields) - Info(msg string, fields Fields) - Warn(msg string, fields Fields) - Error(msg string, fields Fields) - Fatal(msg string, fields Fields) -} diff --git a/internal/logger/logger.go b/internal/logger/logger.go deleted file mode 100644 index 2d11fab..0000000 --- a/internal/logger/logger.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Package logger contains a Logger Wrapper to enable support for multiple logging libraries. -This is a layer between the application code and the underlying logging library. -It provides a simplified API that abstracts away the complexity of different logging libraries, making it easier to switch between them or add new ones. -*/ -package logger - -import ( - "context" - "fmt" - - "github.com/threefoldtech/tf-kyc-verifier/internal/config" -) - -type LoggerW struct { - logger Logger -} - -type Fields map[string]interface{} - -var log *LoggerW - -func Init(config config.Log) error { - zapLogger, err := NewZapLogger(config.Debug, context.Background()) - if err != nil { - return fmt.Errorf("initializing zap logger: %w", err) - } - - log = &LoggerW{logger: zapLogger} - return nil -} - -func GetLogger() (*LoggerW, error) { - if log == nil { - return nil, fmt.Errorf("logger not initialized") - } - return log, nil -} - -func (lw *LoggerW) Debug(msg string, fields Fields) { - lw.logger.Debug(msg, fields) -} - -func (lw *LoggerW) Info(msg string, fields Fields) { - lw.logger.Info(msg, fields) -} - -func (lw *LoggerW) Warn(msg string, fields Fields) { - lw.logger.Warn(msg, fields) -} - -func (lw *LoggerW) Error(msg string, fields Fields) { - lw.logger.Error(msg, fields) -} - -func (lw *LoggerW) Fatal(msg string, fields Fields) { - lw.logger.Fatal(msg, fields) -} diff --git a/internal/logger/zap_logger.go b/internal/logger/zap_logger.go deleted file mode 100644 index c1bb74d..0000000 --- a/internal/logger/zap_logger.go +++ /dev/null @@ -1,69 +0,0 @@ -package logger - -import ( - "context" - "fmt" - - "go.uber.org/zap" - "go.uber.org/zap/zapcore" -) - -type ZapLogger struct { - logger *zap.Logger - ctx context.Context -} - -func NewZapLogger(debug bool, ctx context.Context) (*ZapLogger, error) { - zapConfig := zap.NewProductionConfig() - if debug { - zapConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel) - } - zapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder - zapConfig.DisableCaller = true - zapLog, err := zapConfig.Build() - if err != nil { - return nil, fmt.Errorf("building zap logger from the config: %w", err) - } - - return &ZapLogger{logger: zapLog, ctx: ctx}, nil -} - -func (l *ZapLogger) Debug(msg string, fields Fields) { - l.addContextCommonFields(fields) - - l.logger.Debug(msg, zap.Any("args", fields)) -} - -func (l *ZapLogger) Info(msg string, fields Fields) { - l.addContextCommonFields(fields) - - l.logger.Info(msg, zap.Any("args", fields)) -} - -func (l *ZapLogger) Warn(msg string, fields Fields) { - l.addContextCommonFields(fields) - - l.logger.Warn(msg, zap.Any("args", fields)) -} - -func (l *ZapLogger) Error(msg string, fields Fields) { - l.addContextCommonFields(fields) - - l.logger.Error(msg, zap.Any("args", fields)) -} - -func (l *ZapLogger) Fatal(msg string, fields Fields) { - l.addContextCommonFields(fields) - - l.logger.Fatal(msg, zap.Any("args", fields)) -} - -func (l *ZapLogger) addContextCommonFields(fields Fields) { - if l.ctx != nil && l.ctx.Value("commonFields") != nil && fields != nil { - for k, v := range l.ctx.Value("commonFields").(map[string]interface{}) { - if _, ok := fields[k]; !ok { - fields[k] = v - } - } - } -} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index a5e31ff..f70107d 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -1,6 +1,8 @@ package middleware import ( + "fmt" + "log/slog" "strconv" "strings" "time" @@ -9,7 +11,7 @@ import ( "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/errors" "github.com/threefoldtech/tf-kyc-verifier/internal/handlers" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" + "github.com/threefoldtech/tf-kyc-verifier/internal/responses" "github.com/vedhavyas/go-subkey/v2" "github.com/vedhavyas/go-subkey/v2/ed25519" "github.com/vedhavyas/go-subkey/v2/sr25519" @@ -23,9 +25,7 @@ func AuthMiddleware(config config.Challenge) fiber.Handler { challenge := c.Get("X-Challenge") if clientID == "" || signature == "" || challenge == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Missing authentication credentials", - }) + return responses.RespondWithError(c, fiber.StatusBadRequest, fmt.Errorf("missing authentication credentials")) } // Verify the clientID and signature here @@ -36,9 +36,7 @@ func AuthMiddleware(config config.Challenge) fiber.Handler { if ok { return handlers.HandleServiceError(c, serviceError) } - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": err.Error(), - }) + return responses.RespondWithError(c, fiber.StatusBadRequest, err) } // Verify the signature err = VerifySubstrateSignature(clientID, signature, challenge) @@ -47,9 +45,7 @@ func AuthMiddleware(config config.Challenge) fiber.Handler { if ok { return handlers.HandleServiceError(c, serviceError) } - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": err.Error(), - }) + return responses.RespondWithError(c, fiber.StatusUnauthorized, err) } return c.Next() @@ -125,7 +121,7 @@ func ValidateChallenge(address, signature, challenge, expectedDomain string, cha return nil } -func NewLoggingMiddleware(log logger.Logger) fiber.Handler { +func NewLoggingMiddleware(logger *slog.Logger) fiber.Handler { return func(c *fiber.Ctx) error { start := time.Now() path := c.Path() @@ -133,14 +129,7 @@ func NewLoggingMiddleware(log logger.Logger) fiber.Handler { ip := c.IP() // Log request - log.Info("Incoming request", logger.Fields{ - "method": method, - "path": path, - "queries": c.Queries(), - "ip": ip, - "user_agent": string(c.Request().Header.UserAgent()), - "headers": c.GetReqHeaders(), - }) + logger.Info("Incoming request", slog.Any("method", method), slog.Any("path", path), slog.Any("queries", c.Queries()), slog.Any("ip", ip), slog.Any("user_agent", string(c.Request().Header.UserAgent())), slog.Any("headers", c.GetReqHeaders())) // Handle request err := c.Next() @@ -153,25 +142,18 @@ func NewLoggingMiddleware(log logger.Logger) fiber.Handler { responseSize := len(c.Response().Body()) // Log the response - logFields := logger.Fields{ - "method": method, - "path": path, - "ip": ip, - "status": status, - "duration": duration, - "response_size": responseSize, - } + logger := logger.With(slog.Any("method", method), slog.Any("path", path), slog.Any("ip", ip), slog.Any("status", status), slog.Any("duration", duration), slog.Any("response_size", responseSize)) // Add error if present if err != nil { - logFields["error"] = err + logger = logger.With(slog.Any("error", err)) if status >= 500 { - log.Error("Request failed", logFields) + logger.Error("Request failed") } else { - log.Info("Request failed", logFields) + logger.Info("Request failed") } } else { - log.Info("Request completed", logFields) + logger.Info("Request completed") } return err diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index fe2f1ec..9d16d5f 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -4,20 +4,20 @@ import ( "context" "time" + "log/slog" + + "github.com/threefoldtech/tf-kyc-verifier/internal/models" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" - "github.com/threefoldtech/tf-kyc-verifier/internal/models" ) type MongoTokenRepository struct { collection *mongo.Collection - logger logger.Logger + logger *slog.Logger } -func NewMongoTokenRepository(ctx context.Context, db *mongo.Database, logger logger.Logger) TokenRepository { +func NewMongoTokenRepository(ctx context.Context, db *mongo.Database, logger *slog.Logger) TokenRepository { repo := &MongoTokenRepository{ collection: db.Collection("tokens"), logger: logger, @@ -36,7 +36,7 @@ func (r *MongoTokenRepository) createTTLIndex(ctx context.Context) { }, ) if err != nil { - r.logger.Error("Error creating TTL index", logger.Fields{"error": err}) + r.logger.Error("Error creating TTL index", "error", err) } } @@ -46,7 +46,7 @@ func (r *MongoTokenRepository) createClientIdIndex(ctx context.Context) { Options: options.Index().SetUnique(true), }) if err != nil { - r.logger.Error("Error creating clientId index", logger.Fields{"error": err}) + r.logger.Error("Error creating clientId index", "error", err) } } diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go index 8873012..a2e9a7c 100644 --- a/internal/repository/verification_repository.go +++ b/internal/repository/verification_repository.go @@ -2,9 +2,9 @@ package repository import ( "context" + "log/slog" "time" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/models" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" @@ -13,10 +13,10 @@ import ( type MongoVerificationRepository struct { collection *mongo.Collection - logger logger.Logger + logger *slog.Logger } -func NewMongoVerificationRepository(ctx context.Context, db *mongo.Database, logger logger.Logger) VerificationRepository { +func NewMongoVerificationRepository(ctx context.Context, db *mongo.Database, logger *slog.Logger) VerificationRepository { // create index for clientId repo := &MongoVerificationRepository{ collection: db.Collection("verifications"), @@ -32,7 +32,7 @@ func (r *MongoVerificationRepository) createClientIdIndex(ctx context.Context) { Options: options.Index().SetUnique(true), }) if err != nil { - r.logger.Error("Error creating clientId index", logger.Fields{"error": err}) + r.logger.Error("Error creating clientId index", "error", err) } } diff --git a/internal/responses/responses.go b/internal/responses/responses.go index e6d6a56..e8ed6db 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -1,11 +1,34 @@ package responses import ( + "github.com/gofiber/fiber/v2" + "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/models" ) -type ErrorResponse struct { - Error string `json:"error"` +type APIResponse struct { + Result any `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +func Success(data any) *APIResponse { + return &APIResponse{ + Result: data, + } +} + +func Error(err string) *APIResponse { + return &APIResponse{ + Error: err, + } +} + +func RespondWithError(c *fiber.Ctx, status int, err error) error { + return c.Status(status).JSON(Error(err.Error())) +} + +func RespondWithData(c *fiber.Ctx, status int, data any) error { + return c.Status(status).JSON(Success(data)) } type HealthStatus string @@ -176,7 +199,7 @@ func NewVerificationDataResponse(verification *models.Verification) *Verificatio } // appConfigsResponse -type AppConfigsResponse interface{} +type AppConfigsResponse = config.Config // appVersionResponse type AppVersionResponse struct { diff --git a/internal/server/server.go b/internal/server/server.go index 08aea09..1b79c7e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -12,6 +12,7 @@ package server import ( "context" "fmt" + "log/slog" "net" "net/http" "os" @@ -32,7 +33,6 @@ import ( "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/handlers" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/middleware" "github.com/threefoldtech/tf-kyc-verifier/internal/repository" "github.com/threefoldtech/tf-kyc-verifier/internal/services" @@ -52,11 +52,11 @@ const ( type Server struct { app *fiber.App config *config.Config - logger logger.Logger + logger *slog.Logger } // New creates a new server instance with the given configuration and options -func New(config *config.Config, srvLogger logger.Logger) (*Server, error) { +func New(config *config.Config, srvLogger *slog.Logger) (*Server, error) { // Create base context for initialization ctx, cancel := context.WithTimeout(context.Background(), SERVER_STARTUP_TIMEOUT) defer cancel() @@ -117,7 +117,7 @@ func (s *Server) initializeCore(ctx context.Context) error { } func (s *Server) setupMiddleware() error { - s.logger.Debug("Setting up middleware", nil) + s.logger.Debug("Setting up middleware") // Setup rate limiter stores ipLimiterStore := mongodb.New(mongodb.Config{ @@ -177,7 +177,7 @@ func (s *Server) setupMiddleware() error { } func (s *Server) setupDatabase(ctx context.Context) (*mongo.Client, *mongo.Database, error) { - s.logger.Debug("Connecting to database", nil) + s.logger.Debug("Connecting to database") client, err := repository.NewMongoClient(ctx, s.config.MongoDB.URI) if err != nil { @@ -193,7 +193,7 @@ type repositories struct { } func (s *Server) setupRepositories(ctx context.Context, db *mongo.Database) (*repositories, error) { - s.logger.Debug("Setting up repositories", nil) + s.logger.Debug("Setting up repositories") return &repositories{ token: repository.NewMongoTokenRepository(ctx, db, s.logger), @@ -202,7 +202,7 @@ func (s *Server) setupRepositories(ctx context.Context, db *mongo.Database) (*re } func (s *Server) setupServices(repos *repositories) (*services.KYCService, error) { - s.logger.Debug("Setting up services", nil) + s.logger.Debug("Setting up services") idenfyClient := idenfy.New(&s.config.Idenfy, s.logger) @@ -225,7 +225,7 @@ func (s *Server) setupServices(repos *repositories) (*services.KYCService, error } func (s *Server) setupRoutes(kycService *services.KYCService, mongoCl *mongo.Client) error { - s.logger.Debug("Setting up routes", nil) + s.logger.Debug("Setting up routes") handler := handlers.NewHandler(kycService, s.config, s.logger) @@ -283,11 +283,11 @@ func (s *Server) Run() error { signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) <-sigChan // Graceful shutdown - s.logger.Info("Shutting down server...", nil) + s.logger.Info("Shutting down server...") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := s.app.ShutdownWithContext(ctx); err != nil { - s.logger.Error("Server forced to shutdown:", logger.Fields{"error": err}) + s.logger.Error("Server forced to shutdown:", slog.String("error", err.Error())) } }() diff --git a/internal/services/services.go b/internal/services/services.go index a6d4a53..24c07ca 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -7,6 +7,7 @@ package services import ( "context" "fmt" + "log/slog" "slices" "strconv" "strings" @@ -16,7 +17,6 @@ import ( "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" "github.com/threefoldtech/tf-kyc-verifier/internal/config" "github.com/threefoldtech/tf-kyc-verifier/internal/errors" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/threefoldtech/tf-kyc-verifier/internal/models" "github.com/threefoldtech/tf-kyc-verifier/internal/repository" ) @@ -29,11 +29,11 @@ type KYCService struct { idenfy idenfy.IdenfyClient substrate substrate.SubstrateClient config *config.Verification - logger logger.Logger + logger *slog.Logger IdenfySuffix string } -func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy idenfy.IdenfyClient, substrateClient substrate.SubstrateClient, config *config.Config, logger logger.Logger) (*KYCService, error) { +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy idenfy.IdenfyClient, substrateClient substrate.SubstrateClient, config *config.Config, logger *slog.Logger) (*KYCService, error) { idenfySuffix, err := GetIdenfySuffix(substrateClient, config) if err != nil { return nil, fmt.Errorf("getting idenfy suffix: %w", err) @@ -68,7 +68,7 @@ func GetChainNetworkName(substrateClient substrate.SubstrateClient) (string, err func (s *KYCService) GetOrCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) { isVerified, err := s.IsUserVerified(ctx, clientID) if err != nil { - s.logger.Error("Error checking if user is verified", logger.Fields{"clientID": clientID, "error": err}) + s.logger.Error("Error checking if user is verified", "clientID", clientID, "error", err) return nil, false, errors.NewInternalError("getting verification status from database", err) // db error } if isVerified { @@ -76,7 +76,7 @@ func (s *KYCService) GetOrCreateVerificationToken(ctx context.Context, clientID } token, err_ := s.tokenRepo.GetToken(ctx, clientID) if err_ != nil { - s.logger.Error("Error getting token from database", logger.Fields{"clientID": clientID, "error": err_}) + s.logger.Error("Error getting token from database", "clientID", clientID, "error", err_) return nil, false, errors.NewInternalError("getting token from database", err_) // db error } // check if token is found and not expired @@ -92,7 +92,7 @@ func (s *KYCService) GetOrCreateVerificationToken(ctx context.Context, clientID // check if user account balance satisfies the minimum required balance, return an error if not hasRequiredBalance, err_ := s.AccountHasRequiredBalance(ctx, clientID) if err_ != nil { - s.logger.Error("Error checking if user account has required balance", logger.Fields{"clientID": clientID, "error": err_}) + s.logger.Error("Error checking if user account has required balance", "clientID", clientID, "error", err_) return nil, false, errors.NewExternalError("checking if user account has required balance", err_) } if !hasRequiredBalance { @@ -103,14 +103,14 @@ func (s *KYCService) GetOrCreateVerificationToken(ctx context.Context, clientID uniqueClientID := clientID + ":" + s.IdenfySuffix newToken, err_ := s.idenfy.CreateVerificationSession(ctx, uniqueClientID) if err_ != nil { - s.logger.Error("Error creating iDenfy verification session", logger.Fields{"clientID": clientID, "uniqueClientID": uniqueClientID, "error": err_}) + s.logger.Error("Error creating iDenfy verification session", "clientID", clientID, "uniqueClientID", uniqueClientID, "error", err_) return nil, false, errors.NewExternalError("creating iDenfy verification session", err_) } // save the token with the original clientID newToken.ClientID = clientID err_ = s.tokenRepo.SaveToken(ctx, &newToken) if err_ != nil { - s.logger.Error("Error saving verification token to database", logger.Fields{"clientID": clientID, "error": err_}) + s.logger.Error("Error saving verification token to database", "clientID", clientID, "error", err_) } return &newToken, true, nil @@ -120,7 +120,7 @@ func (s *KYCService) DeleteToken(ctx context.Context, clientID string, scanRef s err := s.tokenRepo.DeleteToken(ctx, clientID, scanRef) if err != nil { - s.logger.Error("Error deleting verification token from database", logger.Fields{"clientID": clientID, "scanRef": scanRef, "error": err}) + s.logger.Error("Error deleting verification token from database", "clientID", clientID, "scanRef", scanRef, "error", err) return errors.NewInternalError("deleting verification token from database", err) } return nil @@ -128,12 +128,12 @@ func (s *KYCService) DeleteToken(ctx context.Context, clientID string, scanRef s func (s *KYCService) AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) { if s.config.MinBalanceToVerifyAccount == 0 { - s.logger.Warn("Minimum balance to verify account is 0 which is not recommended", logger.Fields{"address": address}) + s.logger.Warn("Minimum balance to verify account is 0 which is not recommended", "address", address) return true, nil } balance, err := s.substrate.GetAccountBalance(address) if err != nil { - s.logger.Error("Error getting account balance", logger.Fields{"address": address, "error": err}) + s.logger.Error("Error getting account balance", "address", address, "error", err) return false, errors.NewExternalError("getting account balance", err) } return balance >= s.config.MinBalanceToVerifyAccount, nil @@ -145,7 +145,7 @@ func (s *KYCService) AccountHasRequiredBalance(ctx context.Context, address stri func (s *KYCService) GetVerificationData(ctx context.Context, clientID string) (*models.Verification, error) { verification, err := s.verificationRepo.GetVerification(ctx, clientID) if err != nil { - s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) + s.logger.Error("Error getting verification from database", "clientID", clientID, "error", err) return nil, errors.NewInternalError("getting verification from database", err) } return verification, nil @@ -155,7 +155,7 @@ func (s *KYCService) GetVerificationStatus(ctx context.Context, clientID string) // check first if the clientID is in alwaysVerifiedAddresses if s.config.AlwaysVerifiedIDs != nil && slices.Contains(s.config.AlwaysVerifiedIDs, clientID) { final := true - s.logger.Info("ClientID is in always verified addresses. skipping verification", logger.Fields{"clientID": clientID}) + s.logger.Info("ClientID is in always verified addresses. skipping verification", "clientID", clientID) return &models.VerificationOutcome{ Final: &final, ClientID: clientID, @@ -165,7 +165,7 @@ func (s *KYCService) GetVerificationStatus(ctx context.Context, clientID string) } verification, err := s.verificationRepo.GetVerification(ctx, clientID) if err != nil { - s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) + s.logger.Error("Error getting verification from database", "clientID", clientID, "error", err) return nil, errors.NewInternalError("getting verification from database", err) } var outcome models.Outcome @@ -190,12 +190,12 @@ func (s *KYCService) GetVerificationStatusByTwinID(ctx context.Context, twinID s // get the address from the twinID twinIDUint64, err := strconv.ParseUint(twinID, 10, 32) if err != nil { - s.logger.Error("Error parsing twinID", logger.Fields{"twinID": twinID, "error": err}) + s.logger.Error("Error parsing twinID", "twinID", twinID, "error", err) return nil, errors.NewInternalError("parsing twinID", err) } address, err := s.substrate.GetAddressByTwinID(uint32(twinIDUint64)) if err != nil { - s.logger.Error("Error getting address from twinID", logger.Fields{"twinID": twinID, "error": err}) + s.logger.Error("Error getting address from twinID", "twinID", twinID, "error", err) return nil, errors.NewExternalError("looking up twinID address from TFChain", err) } return s.GetVerificationStatus(ctx, address) @@ -204,17 +204,17 @@ func (s *KYCService) GetVerificationStatusByTwinID(ctx context.Context, twinID s func (s *KYCService) ProcessVerificationResult(ctx context.Context, body []byte, sigHeader string, result models.Verification) error { err := s.idenfy.VerifyCallbackSignature(ctx, body, sigHeader) if err != nil { - s.logger.Error("Error verifying callback signature", logger.Fields{"sigHeader": sigHeader, "error": err}) + s.logger.Error("Error verifying callback signature", "sigHeader", sigHeader, "error", err) return errors.NewAuthorizationError("verifying callback signature", err) } clientIDParts := strings.Split(result.ClientID, ":") if len(clientIDParts) < 2 { - s.logger.Error("clientID have no network suffix", logger.Fields{"clientID": result.ClientID}) + s.logger.Error("clientID have no network suffix", "clientID", result.ClientID) return errors.NewInternalError("invalid clientID", nil) } networkSuffix := clientIDParts[len(clientIDParts)-1] if networkSuffix != s.IdenfySuffix { - s.logger.Error("clientID has different network suffix", logger.Fields{"clientID": result.ClientID, "expectedSuffix": s.IdenfySuffix, "actualSuffix": networkSuffix}) + s.logger.Error("clientID has different network suffix", "clientID", result.ClientID, "expectedSuffix", s.IdenfySuffix, "actualSuffix", networkSuffix) return errors.NewInternalError("invalid clientID", nil) } // delete the token with the same clientID and same scanRef @@ -222,18 +222,18 @@ func (s *KYCService) ProcessVerificationResult(ctx context.Context, body []byte, err = s.tokenRepo.DeleteToken(ctx, result.ClientID, result.IdenfyRef) if err != nil { - s.logger.Warn("Error deleting verification token from database", logger.Fields{"clientID": result.ClientID, "scanRef": result.IdenfyRef, "error": err}) + s.logger.Warn("Error deleting verification token from database", "clientID", result.ClientID, "scanRef", result.IdenfyRef, "error", err) } // if the verification status is EXPIRED, we don't need to save it if result.Status.Overall != nil && *result.Status.Overall != models.Overall("EXPIRED") { // remove idenfy suffix from clientID err = s.verificationRepo.SaveVerification(ctx, &result) if err != nil { - s.logger.Error("Error saving verification to database", logger.Fields{"clientID": result.ClientID, "scanRef": result.IdenfyRef, "error": err}) + s.logger.Error("Error saving verification to database", "clientID", result.ClientID, "scanRef", result.IdenfyRef, "error", err) return errors.NewInternalError("saving verification to database", err) } } - s.logger.Debug("Verification result processed successfully", logger.Fields{"result": result}) + s.logger.Debug("Verification result processed successfully", "result", result) return nil } @@ -244,7 +244,7 @@ func (s *KYCService) ProcessDocExpirationNotification(ctx context.Context, clien func (s *KYCService) IsUserVerified(ctx context.Context, clientID string) (bool, error) { verification, err := s.verificationRepo.GetVerification(ctx, clientID) if err != nil { - s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) + s.logger.Error("Error getting verification from database", "clientID", clientID, "error", err) return false, errors.NewInternalError("getting verification from database", err) } if verification == nil { diff --git a/scripts/dev/balance/check-account-balance.go b/scripts/dev/balance/check-account-balance.go index caee9d1..881f30a 100644 --- a/scripts/dev/balance/check-account-balance.go +++ b/scripts/dev/balance/check-account-balance.go @@ -3,10 +3,9 @@ package main import ( "fmt" - "log" + "log/slog" "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" ) func main() { @@ -14,7 +13,7 @@ func main() { WsProviderURL: "wss://tfchain.dev.grid.tf", } - logger := &LoggerW{log.Default()} + logger := slog.Default() substrateClient, err := substrate.New(config, logger) if err != nil { panic(err) @@ -26,31 +25,6 @@ func main() { fmt.Println(free_balance) } -// implement logger.LoggerW for log.Logger -type LoggerW struct { - *log.Logger -} - -func (l *LoggerW) Debug(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Info(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Warn(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Error(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Fatal(msg string, fields logger.Fields) { - l.Println(msg) -} - type TFChainConfig struct { WsProviderURL string } diff --git a/scripts/dev/chain/chain_name.go b/scripts/dev/chain/chain_name.go index a6cedeb..08d6d2e 100644 --- a/scripts/dev/chain/chain_name.go +++ b/scripts/dev/chain/chain_name.go @@ -2,10 +2,9 @@ package main import ( "fmt" - "log" + "log/slog" "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" ) func main() { @@ -13,7 +12,7 @@ func main() { WsProviderURL: "wss://tfchain.dev.grid.tf", } - logger := &LoggerW{log.Default()} + logger := slog.Default() substrateClient, err := substrate.New(config, logger) if err != nil { panic(err) @@ -27,31 +26,6 @@ func main() { } -// implement logger.LoggerW for log.Logger -type LoggerW struct { - *log.Logger -} - -func (l *LoggerW) Debug(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Info(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Warn(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Error(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Fatal(msg string, fields logger.Fields) { - l.Println(msg) -} - type TFChainConfig struct { WsProviderURL string } diff --git a/scripts/dev/twin/get-address-by-twin-id.go b/scripts/dev/twin/get-address-by-twin-id.go index 84a882c..c48e1e0 100644 --- a/scripts/dev/twin/get-address-by-twin-id.go +++ b/scripts/dev/twin/get-address-by-twin-id.go @@ -2,10 +2,9 @@ package main import ( "fmt" - "log" + "log/slog" "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" - "github.com/threefoldtech/tf-kyc-verifier/internal/logger" ) func main() { @@ -13,7 +12,7 @@ func main() { WsProviderURL: "wss://tfchain.dev.grid.tf", } - logger := &LoggerW{log.Default()} + logger := slog.Default() substrateClient, err := substrate.New(config, logger) if err != nil { panic(err) @@ -27,31 +26,6 @@ func main() { } -// implement logger.LoggerW for log.Logger -type LoggerW struct { - *log.Logger -} - -func (l *LoggerW) Debug(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Info(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Warn(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Error(msg string, fields logger.Fields) { - l.Println(msg) -} - -func (l *LoggerW) Fatal(msg string, fields logger.Fields) { - l.Println(msg) -} - type TFChainConfig struct { WsProviderURL string } From 65f7a9bf25367b788afca97536bf2b291a5b1b69 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Tue, 5 Nov 2024 14:50:16 +0200 Subject: [PATCH 28/28] Adding AuthMiddleware tests --- internal/middleware/middleware_test.go | 256 ++++++++++++++++++ internal/repository/token_repository.go | 22 +- .../repository/verification_repository.go | 11 +- 3 files changed, 276 insertions(+), 13 deletions(-) create mode 100644 internal/middleware/middleware_test.go diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go new file mode 100644 index 0000000..2449c46 --- /dev/null +++ b/internal/middleware/middleware_test.go @@ -0,0 +1,256 @@ +package middleware + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/stretchr/testify/assert" + "github.com/threefoldtech/tf-kyc-verifier/internal/config" + "github.com/vedhavyas/go-subkey/v2" + "github.com/vedhavyas/go-subkey/v2/ed25519" + "github.com/vedhavyas/go-subkey/v2/sr25519" +) + +func TestAuthMiddleware(t *testing.T) { + // Setup + app := fiber.New() + cfg := config.Challenge{ + Window: 8, + Domain: "test.grid.tf", + } + + // Mock handler that should be called after middleware + successHandler := func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + } + + // Apply middleware + app.Use(AuthMiddleware(cfg)) + app.Get("/test", successHandler) + + // Generate keys + krSr25519, err := generateTestSr25519Keys() + if err != nil { + t.Fatal(err) + } + krEd25519, err := generateTestEd25519Keys() + if err != nil { + t.Fatal(err) + } + clientIDSr := krSr25519.SS58Address(42) + clientIDEd := krEd25519.SS58Address(42) + invalidChallenge := createInvalidSignMessageInvalidFormat(cfg.Domain) + expiredChallenge := createInvalidSignMessageExpired(cfg.Domain) + wrongDomainChallenge := createInvalidSignMessageWrongDomain() + validChallenge := createValidSignMessage(cfg.Domain) + sigSr, err := krSr25519.Sign([]byte(validChallenge)) + if err != nil { + t.Fatal(err) + } + sigEd, err := krEd25519.Sign([]byte(validChallenge)) + if err != nil { + t.Fatal(err) + } + sigSrHex := hex.EncodeToString(sigSr) + sigEdHex := hex.EncodeToString(sigEd) + tests := []struct { + name string + clientID string + signature string + challenge string + expectedStatus int + expectedError string + }{ + { + name: "Missing all credentials", + clientID: "", + signature: "", + challenge: "", + expectedStatus: fiber.StatusBadRequest, + expectedError: "missing authentication credentials", + }, + { + name: "Missing client ID", + clientID: "", + signature: sigSrHex, + challenge: toHex(validChallenge), + expectedStatus: fiber.StatusBadRequest, + expectedError: "missing authentication credentials", + }, + { + name: "Missing signature", + clientID: clientIDSr, + signature: "", + challenge: toHex(validChallenge), + expectedStatus: fiber.StatusBadRequest, + expectedError: "missing authentication credentials", + }, + { + name: "Missing challenge", + clientID: clientIDSr, + signature: sigSrHex, + challenge: "", + expectedStatus: fiber.StatusBadRequest, + expectedError: "missing authentication credentials", + }, + { + name: "Invalid client ID format", + clientID: toHex("invalid_client_id"), + signature: sigSrHex, + challenge: toHex(validChallenge), + expectedStatus: fiber.StatusBadRequest, + expectedError: "malformed address", + }, + { + name: "Invalid challenge format", + clientID: clientIDSr, + signature: sigSrHex, + challenge: toHex(invalidChallenge), + expectedStatus: fiber.StatusBadRequest, + expectedError: "invalid challenge format", + }, + { + name: "Expired challenge", + clientID: clientIDSr, + signature: sigSrHex, + challenge: toHex(expiredChallenge), + expectedStatus: fiber.StatusBadRequest, + expectedError: "challenge expired", + }, + { + name: "Invalid domain in challenge", + clientID: clientIDSr, + signature: sigSrHex, + challenge: toHex(wrongDomainChallenge), + expectedStatus: fiber.StatusBadRequest, + expectedError: "unexpected domain", + }, + { + name: "invalid signature format", + clientID: clientIDSr, + signature: "invalid_signature", + challenge: toHex(validChallenge), + expectedStatus: fiber.StatusBadRequest, + expectedError: "malformed signature", + }, + { + name: "bad signature", + clientID: clientIDSr, + signature: sigEdHex, + challenge: toHex(validChallenge), + expectedStatus: fiber.StatusUnauthorized, + expectedError: "signature does not match", + }, + { + name: "valid credentials SR25519", + clientID: clientIDSr, + signature: sigSrHex, + challenge: toHex(validChallenge), + expectedStatus: fiber.StatusOK, + }, + { + name: "valid credentials ED25519", + clientID: clientIDEd, + signature: sigEdHex, + challenge: toHex(validChallenge), + expectedStatus: fiber.StatusOK, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create request + req := createTestRequest(tt.clientID, tt.signature, tt.challenge) + resp, err := app.Test(req) + + // Assert response + assert.NoError(t, err) + assert.Equal(t, tt.expectedStatus, resp.StatusCode) + + // Check error message if expected + if tt.expectedError != "" { + var errorResp struct { + Error string `json:"error"` + } + err = parseResponse(resp, &errorResp) + assert.NoError(t, err) + assert.Contains(t, errorResp.Error, tt.expectedError) + } + }) + } +} + +// Helper function to create test requests +func createTestRequest(clientID, signature, challenge string) *http.Request { + req := httptest.NewRequest(fiber.MethodGet, "/test", nil) + if clientID != "" { + req.Header.Set("X-Client-ID", clientID) + } + if signature != "" { + req.Header.Set("X-Signature", signature) + } + if challenge != "" { + req.Header.Set("X-Challenge", challenge) + } + return req +} + +// Helper function to parse response body +func parseResponse(resp *http.Response, v interface{}) error { + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + return json.Unmarshal(body, v) +} + +func toHex(message string) string { + return hex.EncodeToString([]byte(message)) +} + +func createValidSignMessage(domain string) string { + // return a message with the domain and the current timestamp in hex + message := fmt.Sprintf("%s:%d", domain, time.Now().Unix()) + return message +} + +func createInvalidSignMessageWrongDomain() string { + // return a message with the domain and the current timestamp in hex + message := fmt.Sprintf("%s:%d", "wrong.domain", time.Now().Unix()) + return message +} + +func createInvalidSignMessageExpired(domain string) string { + // return a message with the domain and the current timestamp in hex + message := fmt.Sprintf("%s:%d", domain, time.Now().Add(-10*time.Minute).Unix()) + return message +} + +func createInvalidSignMessageInvalidFormat(domain string) string { + // return a message with the domain and the current timestamp in hex + message := fmt.Sprintf("%s%d", domain, time.Now().Unix()) + return message +} + +func generateTestSr25519Keys() (subkey.KeyPair, error) { + krSr25519, err := sr25519.Scheme{}.Generate() + if err != nil { + return nil, err + } + return krSr25519, nil +} + +func generateTestEd25519Keys() (subkey.KeyPair, error) { + krEd25519, err := ed25519.Scheme{}.Generate() + if err != nil { + return nil, err + } + return krEd25519, nil +} diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index 9d16d5f..44499b7 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -23,7 +23,7 @@ func NewMongoTokenRepository(ctx context.Context, db *mongo.Database, logger *sl logger: logger, } repo.createTTLIndex(ctx) - repo.createClientIdIndex(ctx) + repo.createCollectionIndexes(ctx) return repo } @@ -40,13 +40,19 @@ func (r *MongoTokenRepository) createTTLIndex(ctx context.Context) { } } -func (r *MongoTokenRepository) createClientIdIndex(ctx context.Context) { - _, err := r.collection.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "clientId", Value: 1}}, - Options: options.Index().SetUnique(true), - }) - if err != nil { - r.logger.Error("Error creating clientId index", "error", err) +func (r *MongoTokenRepository) createCollectionIndexes(ctx context.Context) { + keys := []bson.D{ + {{Key: "clientId", Value: 1}}, + {{Key: "scanRef", Value: 1}}, + } + for _, key := range keys { + _, err := r.collection.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: key, + Options: options.Index().SetUnique(true), + }) + if err != nil { + r.logger.Error("Error creating index", "key", key, "error", err) + } } } diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go index a2e9a7c..3b1d203 100644 --- a/internal/repository/verification_repository.go +++ b/internal/repository/verification_repository.go @@ -22,17 +22,18 @@ func NewMongoVerificationRepository(ctx context.Context, db *mongo.Database, log collection: db.Collection("verifications"), logger: logger, } - repo.createClientIdIndex(ctx) + repo.createCollectionIndexes(ctx) return repo } -func (r *MongoVerificationRepository) createClientIdIndex(ctx context.Context) { +func (r *MongoVerificationRepository) createCollectionIndexes(ctx context.Context) { + key := bson.D{{Key: "clientId", Value: 1}} _, err := r.collection.Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "clientId", Value: 1}}, - Options: options.Index().SetUnique(true), + Keys: key, + Options: options.Index().SetUnique(false), }) if err != nil { - r.logger.Error("Error creating clientId index", "error", err) + r.logger.Error("Error creating index", "key", key, "error", err) } }