From c1fd2d93843f6f76b96002d04b3d49cb2d6b9246 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Tue, 15 Oct 2024 03:17:09 +0300 Subject: [PATCH 001/105] init project --- .app.env | 14 + .app.env.example | 14 + .db.env | 2 + .db.env.example | 2 + Dockerfile | 19 + README.md | 176 ++++++++ api/docs/docs.go | 407 ++++++++++++++++++ api/docs/swagger.json | 382 ++++++++++++++++ api/docs/swagger.yaml | 259 +++++++++++ cmd/api/main.go | 29 ++ docker-compose.yml | 41 ++ go.mod | 84 ++++ go.sum | 252 +++++++++++ internal/clients/idenfy/idenfy.go | 38 ++ internal/clients/idenfy/types.go | 1 + internal/clients/substrate/substrate.go | 66 +++ internal/configs/config.go | 67 +++ internal/handlers/handlers.go | 113 +++++ internal/middleware/middleware.go | 128 ++++++ internal/models/models.go | 212 +++++++++ internal/repository/mongo.go | 26 ++ internal/repository/repository.go | 19 + internal/repository/token_repository.go | 44 ++ .../repository/verification_repository.go | 43 ++ internal/responses/responses.go | 81 ++++ internal/server/server.go | 98 +++++ internal/services/service.go | 19 + internal/services/token_service.go | 44 ++ internal/services/verification_service.go | 27 ++ internal/utils/utils.go | 1 + scripts/dev/auth/generate-test-auth-data.go | 64 +++ scripts/dev/balance/check-account-balance.go | 25 ++ 32 files changed, 2797 insertions(+) create mode 100644 .app.env create mode 100644 .app.env.example create mode 100644 .db.env create mode 100644 .db.env.example create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 api/docs/docs.go create mode 100644 api/docs/swagger.json create mode 100644 api/docs/swagger.yaml create mode 100644 cmd/api/main.go create mode 100644 docker-compose.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/clients/idenfy/idenfy.go create mode 100644 internal/clients/idenfy/types.go create mode 100644 internal/clients/substrate/substrate.go create mode 100644 internal/configs/config.go create mode 100644 internal/handlers/handlers.go create mode 100644 internal/middleware/middleware.go create mode 100644 internal/models/models.go create mode 100644 internal/repository/mongo.go create mode 100644 internal/repository/repository.go create mode 100644 internal/repository/token_repository.go create mode 100644 internal/repository/verification_repository.go create mode 100644 internal/responses/responses.go create mode 100644 internal/server/server.go create mode 100644 internal/services/service.go create mode 100644 internal/services/token_service.go create mode 100644 internal/services/verification_service.go create mode 100644 internal/utils/utils.go create mode 100644 scripts/dev/auth/generate-test-auth-data.go create mode 100644 scripts/dev/balance/check-account-balance.go diff --git a/.app.env b/.app.env new file mode 100644 index 0000000..123fa92 --- /dev/null +++ b/.app.env @@ -0,0 +1,14 @@ +MONGO_URI=mongodb://root:password@mongodb:27017 +DATABASE_NAME=tfgrid-kyc-db +PORT=8080 +MAX_TOKEN_REQUESTS_PER_MINUTE=2 +SUSPICIOUS_VERIFICATION_OUTCOME=verified +EXPIRED_DOCUMENT_OUTCOME=verified +CHALLENGE_WINDOW=120 +IDENFY_BASE_URL=https://ivs.idenfy.com/api/v2 +IDENFY_API_KEY= +IDENFY_SECRET= +IDENFY_CALLBACK_SIGN_KEY= +IDENFY_WHITELISTED_IPS= +TFCHAIN_WS_PROVIDER_URL=wss://tfchain.grid.tf +TFCHAIN_MIN_BALANCE_TO_VERIFY_ACCOUNT=1000000 \ No newline at end of file diff --git a/.app.env.example b/.app.env.example new file mode 100644 index 0000000..123fa92 --- /dev/null +++ b/.app.env.example @@ -0,0 +1,14 @@ +MONGO_URI=mongodb://root:password@mongodb:27017 +DATABASE_NAME=tfgrid-kyc-db +PORT=8080 +MAX_TOKEN_REQUESTS_PER_MINUTE=2 +SUSPICIOUS_VERIFICATION_OUTCOME=verified +EXPIRED_DOCUMENT_OUTCOME=verified +CHALLENGE_WINDOW=120 +IDENFY_BASE_URL=https://ivs.idenfy.com/api/v2 +IDENFY_API_KEY= +IDENFY_SECRET= +IDENFY_CALLBACK_SIGN_KEY= +IDENFY_WHITELISTED_IPS= +TFCHAIN_WS_PROVIDER_URL=wss://tfchain.grid.tf +TFCHAIN_MIN_BALANCE_TO_VERIFY_ACCOUNT=1000000 \ No newline at end of file diff --git a/.db.env b/.db.env new file mode 100644 index 0000000..077b4a8 --- /dev/null +++ b/.db.env @@ -0,0 +1,2 @@ +MONGO_INITDB_ROOT_USERNAME=root +MONGO_INITDB_ROOT_PASSWORD=password \ No newline at end of file diff --git a/.db.env.example b/.db.env.example new file mode 100644 index 0000000..077b4a8 --- /dev/null +++ b/.db.env.example @@ -0,0 +1,2 @@ +MONGO_INITDB_ROOT_USERNAME=root +MONGO_INITDB_ROOT_PASSWORD=password \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9fbb7fc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.22-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ + +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -o tfgrid-kyc cmd/api/main.go + +FROM alpine:3.19 + +COPY --from=builder /app/tfgrid-kyc . + +ENTRYPOINT ["/tfgrid-kyc"] + +EXPOSE 8080 diff --git a/README.md b/README.md new file mode 100644 index 0000000..2314014 --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# TFGrid KYC Service + +## Overview + +TFGrid KYC Service is a Go-based service that provides Know Your Customer (KYC) functionality for the TFGrid. It integrates with iDenfy for identity verification. + +## Features + +- Identity verification using iDenfy +- Blockchain integration with TFChain (Substrate-based) +- MongoDB for data persistence +- RESTful API endpoints for KYC operations +- Swagger documentation +- Containerized deployment + +## Prerequisites + +- Go 1.22+ +- MongoDB 4.4+ +- Docker and Docker Compose (for containerized deployment) +- iDenfy API credentials + +## Installation + +1. Clone the repository: + + ```bash + git clone https://github.com/yourusername/tfgrid-kyc-verifier.git + cd tfgrid-kyc-verifier + ``` + +2. Set up your environment variables: + + ```bash + cp .app.env.example .app.env + cp .db.env.example .db.env + ``` + +Edit `.app.env` and `.db.env` with your specific configuration details. + +## Configuration + +The application uses environment variables for configuration. Here's a list of all available configuration options: + +### Database Configuration + +- `MONGO_URI`: MongoDB connection URI (default: "mongodb://localhost:27017") +- `DATABASE_NAME`: Name of the MongoDB database (default: "tfgrid-kyc-db") + +### Server Configuration + +- `PORT`: Port on which the server will run (default: "8080") + +### iDenfy Configuration + +- `IDENFY_API_KEY`: API key for iDenfy service +- `IDENFY_API_SECRET`: API secret for iDenfy service +- `IDENFY_BASE_URL`: Base URL for iDenfy API (default: "") +- `IDENFY_CALLBACK_SIGN_KEY`: Callback signing key for iDenfy webhooks +- `IDENFY_WHITELISTED_IPS`: Comma-separated list of whitelisted IPs for iDenfy callbacks + +### TFChain Configuration + +- `TFCHAIN_WS_PROVIDER_URL`: WebSocket provider URL for TFChain (default: "wss://tfchain.grid.tf") + +### Rate Limiting + +- `MAX_TOKEN_REQUESTS_PER_MINUTE`: Maximum number of token requests allowed per minute for same IP address (default: 4) + +### Verification Settings + +- `SUSPICIOUS_VERIFICATION_OUTCOME`: Outcome for suspicious verifications (default: "verified") +- `EXPIRED_DOCUMENT_OUTCOME`: Outcome for expired documents (default: "unverified") +- `CHALLENGE_WINDOW`: Time window (in seconds) for challenge validation (default: 120) +- `MIN_BALANCE_TO_VERIFY_ACCOUNT`: Minimum balance (in units TFT) required to verify an account (default: 10000000) + +To configure these options, you can either set them as environment variables or include them in your `.env` file. + +Refer to `internal/configs/config.go` for a full list of configuration options. + +## Running the Application + +### Using Docker Compose + +To start the server and MongoDB using Docker Compose: + +```bash +docker-compose up -d --build +``` + +### Running Locally + +To run the application locally: + +1. Ensure MongoDB is running and accessible. +2. export the environment variables: + + ```bash + set -a + source .app.env + set +a + ``` + +3. Run the application: + + ```bash + go run cmd/api/main.go + ``` + +## API Endpoints + +### Client endpoints + +- `POST /api/v1/token`: Get or create a verification token +- `GET /api/v1/data`: Get verification data +- `GET /api/v1/status`: Get verification status + +### Webhook endpoints + +- `POST /webhooks/idenfy/verification-update`: Process verification update (webhook) +- `POST /webhooks/idenfy/id-expiration`: Process document expiration notification (webhook) + +Refer to the Swagger documentation for detailed information on request/response formats. + +## Swagger Documentation + +Swagger documentation is available. To view it, run the application and navigate to the `/docs` endpoint in your browser. + +## Project Structure + +- `cmd/`: Application entrypoints +- `internal/`: Internal packages + - `configs/`: Configuration handling + - `handlers/`: HTTP request handlers + - `models/`: Data models + - `responses/`: API response structures + - `services/`: Business logic + - `repositories/`: Data repositories + - `middlewares/`: Middlewares + - `clients/`: External clients + - `server/`: Server and router setup +- `api/docs/`: Swagger documentation +- `scripts/`: Development and utility scripts +- `docs/`: Documentation + +## Development + +### Running Tests + +To run the test suite: + +TODO: Add tests + +### Building the Docker Image + +To build the Docker image: + +```bash +docker build -t tfgrid-kyc-service . +``` + +### Running the Docker Container + +To run the Docker container and use .env variables: + +```bash +docker run -d -p 8080:8080 --env-file .app.env tfgrid-kyc-service +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the Apache 2.0 License. See the `LICENSE` file for more details. diff --git a/api/docs/docs.go b/api/docs/docs.go new file mode 100644 index 0000000..83f3cac --- /dev/null +++ b/api/docs/docs.go @@ -0,0 +1,407 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Codescalers Egypt", + "url": "https://codescalers-egypt.com", + "email": "info@codescalers.com" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/data": { + "get": { + "description": "Returns the verification data for a client", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Get Verification Data", + "parameters": [ + { + "maxLength": 48, + "minLength": 48, + "type": "string", + "description": "TFChain SS58Address", + "name": "X-Client-ID", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "hex-encoded message ` + "`" + `{api-domain}:{timestamp}` + "`" + `", + "name": "X-Challenge", + "in": "header", + "required": true + }, + { + "maxLength": 128, + "minLength": 128, + "type": "string", + "description": "hex-encoded sr25519|ed25519 signature", + "name": "X-Signature", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.VerificationDataResponse" + } + } + } + } + }, + "/api/v1/status": { + "get": { + "description": "Returns the verification status for a client", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Get Verification Status", + "parameters": [ + { + "maxLength": 48, + "minLength": 48, + "type": "string", + "description": "TFChain SS58Address", + "name": "X-Client-ID", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "hex-encoded message ` + "`" + `{api-domain}:{timestamp}` + "`" + `", + "name": "X-Challenge", + "in": "header", + "required": true + }, + { + "maxLength": 128, + "minLength": 128, + "type": "string", + "description": "hex-encoded sr25519|ed25519 signature", + "name": "X-Signature", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.VerificationStatusResponse" + } + } + } + } + }, + "/api/v1/token": { + "post": { + "description": "Returns a token for a client", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Get or Create Token", + "parameters": [ + { + "maxLength": 48, + "minLength": 48, + "type": "string", + "description": "TFChain SS58Address", + "name": "X-Client-ID", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "hex-encoded message ` + "`" + `{api-domain}:{timestamp}` + "`" + `", + "name": "X-Challenge", + "in": "header", + "required": true + }, + { + "maxLength": 128, + "minLength": 128, + "type": "string", + "description": "hex-encoded sr25519|ed25519 signature", + "name": "X-Signature", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.TokenResponse" + } + } + } + } + }, + "/webhooks/idenfy/id-expiration": { + "post": { + "description": "Processes the doc expiration notification for a client", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Process Doc Expiration Notification", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/webhooks/idenfy/verification-update": { + "post": { + "description": "Processes the verification update for a client", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Process Verification Update", + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "definitions": { + "responses.TokenResponse": { + "type": "object", + "properties": { + "authToken": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "digitString": { + "type": "string" + }, + "expiryTime": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "scanRef": { + "type": "string" + }, + "sessionLength": { + "type": "integer" + }, + "tokenType": { + "type": "string" + } + } + }, + "responses.VerificationDataResponse": { + "type": "object", + "properties": { + "additionalData": {}, + "address": { + "type": "string" + }, + "addressVerification": {}, + "ageEstimate": { + "type": "string" + }, + "authority": { + "type": "string" + }, + "birthPlace": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "clientIpProxyRiskLevel": { + "type": "string" + }, + "docBirthName": { + "type": "string" + }, + "docDateOfIssue": { + "type": "string" + }, + "docDob": { + "type": "string" + }, + "docExpiry": { + "type": "string" + }, + "docFirstName": { + "type": "string" + }, + "docIssuingCountry": { + "type": "string" + }, + "docLastName": { + "type": "string" + }, + "docNationality": { + "type": "string" + }, + "docNumber": { + "type": "string" + }, + "docPersonalCode": { + "type": "string" + }, + "docSex": { + "type": "string" + }, + "docTemporaryAddress": { + "type": "string" + }, + "docType": { + "type": "string" + }, + "driverLicenseCategory": { + "type": "string" + }, + "duplicateDocFaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "duplicateFaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "fullName": { + "type": "string" + }, + "manuallyDataChanged": { + "type": "boolean" + }, + "mothersMaidenName": { + "type": "string" + }, + "orgAddress": { + "type": "string" + }, + "orgAuthority": { + "type": "string" + }, + "orgBirthName": { + "type": "string" + }, + "orgBirthPlace": { + "type": "string" + }, + "orgFirstName": { + "type": "string" + }, + "orgLastName": { + "type": "string" + }, + "orgMothersMaidenName": { + "type": "string" + }, + "orgNationality": { + "type": "string" + }, + "orgTemporaryAddress": { + "type": "string" + }, + "scanRef": { + "type": "string" + }, + "selectedCountry": { + "type": "string" + } + } + }, + "responses.VerificationStatusResponse": { + "type": "object", + "properties": { + "autoDocument": { + "type": "string" + }, + "autoFace": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "fraudTags": { + "type": "array", + "items": { + "type": "string" + } + }, + "manualDocument": { + "type": "string" + }, + "manualFace": { + "type": "string" + }, + "mismatchTags": { + "type": "array", + "items": { + "type": "string" + } + }, + "scanRef": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "0.1.0", + Host: "", + BasePath: "/", + Schemes: []string{}, + Title: "TFGrid KYC API", + Description: "This is a KYC service for TFGrid.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/api/docs/swagger.json b/api/docs/swagger.json new file mode 100644 index 0000000..7c9c0d2 --- /dev/null +++ b/api/docs/swagger.json @@ -0,0 +1,382 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a KYC service for TFGrid.", + "title": "TFGrid KYC API", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Codescalers Egypt", + "url": "https://codescalers-egypt.com", + "email": "info@codescalers.com" + }, + "version": "0.1.0" + }, + "basePath": "/", + "paths": { + "/api/v1/data": { + "get": { + "description": "Returns the verification data for a client", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Get Verification Data", + "parameters": [ + { + "maxLength": 48, + "minLength": 48, + "type": "string", + "description": "TFChain SS58Address", + "name": "X-Client-ID", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "hex-encoded message `{api-domain}:{timestamp}`", + "name": "X-Challenge", + "in": "header", + "required": true + }, + { + "maxLength": 128, + "minLength": 128, + "type": "string", + "description": "hex-encoded sr25519|ed25519 signature", + "name": "X-Signature", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.VerificationDataResponse" + } + } + } + } + }, + "/api/v1/status": { + "get": { + "description": "Returns the verification status for a client", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Get Verification Status", + "parameters": [ + { + "maxLength": 48, + "minLength": 48, + "type": "string", + "description": "TFChain SS58Address", + "name": "X-Client-ID", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "hex-encoded message `{api-domain}:{timestamp}`", + "name": "X-Challenge", + "in": "header", + "required": true + }, + { + "maxLength": 128, + "minLength": 128, + "type": "string", + "description": "hex-encoded sr25519|ed25519 signature", + "name": "X-Signature", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.VerificationStatusResponse" + } + } + } + } + }, + "/api/v1/token": { + "post": { + "description": "Returns a token for a client", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Get or Create Token", + "parameters": [ + { + "maxLength": 48, + "minLength": 48, + "type": "string", + "description": "TFChain SS58Address", + "name": "X-Client-ID", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "hex-encoded message `{api-domain}:{timestamp}`", + "name": "X-Challenge", + "in": "header", + "required": true + }, + { + "maxLength": 128, + "minLength": 128, + "type": "string", + "description": "hex-encoded sr25519|ed25519 signature", + "name": "X-Signature", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.TokenResponse" + } + } + } + } + }, + "/webhooks/idenfy/id-expiration": { + "post": { + "description": "Processes the doc expiration notification for a client", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Process Doc Expiration Notification", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/webhooks/idenfy/verification-update": { + "post": { + "description": "Processes the verification update for a client", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Process Verification Update", + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "definitions": { + "responses.TokenResponse": { + "type": "object", + "properties": { + "authToken": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "digitString": { + "type": "string" + }, + "expiryTime": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "scanRef": { + "type": "string" + }, + "sessionLength": { + "type": "integer" + }, + "tokenType": { + "type": "string" + } + } + }, + "responses.VerificationDataResponse": { + "type": "object", + "properties": { + "additionalData": {}, + "address": { + "type": "string" + }, + "addressVerification": {}, + "ageEstimate": { + "type": "string" + }, + "authority": { + "type": "string" + }, + "birthPlace": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "clientIpProxyRiskLevel": { + "type": "string" + }, + "docBirthName": { + "type": "string" + }, + "docDateOfIssue": { + "type": "string" + }, + "docDob": { + "type": "string" + }, + "docExpiry": { + "type": "string" + }, + "docFirstName": { + "type": "string" + }, + "docIssuingCountry": { + "type": "string" + }, + "docLastName": { + "type": "string" + }, + "docNationality": { + "type": "string" + }, + "docNumber": { + "type": "string" + }, + "docPersonalCode": { + "type": "string" + }, + "docSex": { + "type": "string" + }, + "docTemporaryAddress": { + "type": "string" + }, + "docType": { + "type": "string" + }, + "driverLicenseCategory": { + "type": "string" + }, + "duplicateDocFaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "duplicateFaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "fullName": { + "type": "string" + }, + "manuallyDataChanged": { + "type": "boolean" + }, + "mothersMaidenName": { + "type": "string" + }, + "orgAddress": { + "type": "string" + }, + "orgAuthority": { + "type": "string" + }, + "orgBirthName": { + "type": "string" + }, + "orgBirthPlace": { + "type": "string" + }, + "orgFirstName": { + "type": "string" + }, + "orgLastName": { + "type": "string" + }, + "orgMothersMaidenName": { + "type": "string" + }, + "orgNationality": { + "type": "string" + }, + "orgTemporaryAddress": { + "type": "string" + }, + "scanRef": { + "type": "string" + }, + "selectedCountry": { + "type": "string" + } + } + }, + "responses.VerificationStatusResponse": { + "type": "object", + "properties": { + "autoDocument": { + "type": "string" + }, + "autoFace": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "fraudTags": { + "type": "array", + "items": { + "type": "string" + } + }, + "manualDocument": { + "type": "string" + }, + "manualFace": { + "type": "string" + }, + "mismatchTags": { + "type": "array", + "items": { + "type": "string" + } + }, + "scanRef": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml new file mode 100644 index 0000000..aaef222 --- /dev/null +++ b/api/docs/swagger.yaml @@ -0,0 +1,259 @@ +basePath: / +definitions: + responses.TokenResponse: + properties: + authToken: + type: string + clientId: + type: string + digitString: + type: string + expiryTime: + type: integer + message: + type: string + scanRef: + type: string + sessionLength: + type: integer + tokenType: + type: string + type: object + responses.VerificationDataResponse: + properties: + additionalData: {} + address: + type: string + addressVerification: {} + ageEstimate: + type: string + authority: + type: string + birthPlace: + type: string + clientId: + type: string + clientIpProxyRiskLevel: + type: string + docBirthName: + type: string + docDateOfIssue: + type: string + docDob: + type: string + docExpiry: + type: string + docFirstName: + type: string + docIssuingCountry: + type: string + docLastName: + type: string + docNationality: + type: string + docNumber: + type: string + docPersonalCode: + type: string + docSex: + type: string + docTemporaryAddress: + type: string + docType: + type: string + driverLicenseCategory: + type: string + duplicateDocFaces: + items: + type: string + type: array + duplicateFaces: + items: + type: string + type: array + fullName: + type: string + manuallyDataChanged: + type: boolean + mothersMaidenName: + type: string + orgAddress: + type: string + orgAuthority: + type: string + orgBirthName: + type: string + orgBirthPlace: + type: string + orgFirstName: + type: string + orgLastName: + type: string + orgMothersMaidenName: + type: string + orgNationality: + type: string + orgTemporaryAddress: + type: string + scanRef: + type: string + selectedCountry: + type: string + type: object + responses.VerificationStatusResponse: + properties: + autoDocument: + type: string + autoFace: + type: string + clientId: + type: string + fraudTags: + items: + type: string + type: array + manualDocument: + type: string + manualFace: + type: string + mismatchTags: + items: + type: string + type: array + scanRef: + type: string + status: + type: string + type: object +info: + contact: + email: info@codescalers.com + name: Codescalers Egypt + url: https://codescalers-egypt.com + description: This is a KYC service for TFGrid. + termsOfService: http://swagger.io/terms/ + title: TFGrid KYC API + version: 0.1.0 +paths: + /api/v1/data: + get: + consumes: + - application/json + description: Returns the verification data for a client + parameters: + - description: TFChain SS58Address + in: header + maxLength: 48 + minLength: 48 + name: X-Client-ID + required: true + type: string + - description: hex-encoded message `{api-domain}:{timestamp}` + in: header + name: X-Challenge + required: true + type: string + - description: hex-encoded sr25519|ed25519 signature + in: header + maxLength: 128 + minLength: 128 + name: X-Signature + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/responses.VerificationDataResponse' + summary: Get Verification Data + /api/v1/status: + get: + consumes: + - application/json + description: Returns the verification status for a client + parameters: + - description: TFChain SS58Address + in: header + maxLength: 48 + minLength: 48 + name: X-Client-ID + required: true + type: string + - description: hex-encoded message `{api-domain}:{timestamp}` + in: header + name: X-Challenge + required: true + type: string + - description: hex-encoded sr25519|ed25519 signature + in: header + maxLength: 128 + minLength: 128 + name: X-Signature + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/responses.VerificationStatusResponse' + summary: Get Verification Status + /api/v1/token: + post: + consumes: + - application/json + description: Returns a token for a client + parameters: + - description: TFChain SS58Address + in: header + maxLength: 48 + minLength: 48 + name: X-Client-ID + required: true + type: string + - description: hex-encoded message `{api-domain}:{timestamp}` + in: header + name: X-Challenge + required: true + type: string + - description: hex-encoded sr25519|ed25519 signature + in: header + maxLength: 128 + minLength: 128 + name: X-Signature + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/responses.TokenResponse' + summary: Get or Create Token + /webhooks/idenfy/id-expiration: + post: + consumes: + - application/json + description: Processes the doc expiration notification for a client + produces: + - application/json + responses: + "200": + description: OK + summary: Process Doc Expiration Notification + /webhooks/idenfy/verification-update: + post: + consumes: + - application/json + description: Processes the verification update for a client + produces: + - application/json + responses: + "200": + description: OK + summary: Process Verification Update +swagger: "2.0" diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..46d6122 --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "log" + + _ "example.com/tfgrid-kyc-service/api/docs" + "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/server" +) + +// @title TFGrid KYC API +// @version 0.1.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 { + log.Fatal("Failed to load configuration:", err) + } + + server := server.New(config) + + server.Start() +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..725b4a1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +version: '3.8' + +services: + tfgrid_kyc_api: + build: + context: . + dockerfile: Dockerfile + container_name: tfgrid_kyc_api + restart: unless-stopped + ports: + - "8080:8080" + depends_on: + mongodb: + condition: service_healthy + env_file: + - .app.env + + + mongodb: + image: mongo:latest + container_name: mongodb_dev + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + env_file: + - .db.env + healthcheck: + test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet + interval: 10s + timeout: 10s + retries: 5 + start_period: 40s + +volumes: + mongodb_data: + +networks: + default: + name: tfgrid_kyc_network + driver: bridge \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f058e17 --- /dev/null +++ b/go.mod @@ -0,0 +1,84 @@ +module example.com/tfgrid-kyc-service + +go 1.22.1 + +require ( + github.com/centrifuge/go-substrate-rpc-client/v4 v4.2.1 + github.com/gofiber/fiber/v2 v2.52.5 + github.com/gofiber/swagger v1.1.0 + github.com/spf13/viper v1.19.0 + github.com/swaggo/swag v1.16.3 + github.com/vedhavyas/go-subkey/v2 v2.0.0 + go.mongodb.org/mongo-driver v1.17.1 +) + +require ( + github.com/ChainSafe/go-schnorrkel v1.1.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/cosmos/go-bip39 v1.0.0 // indirect + github.com/deckarep/golang-set v1.8.0 // indirect + github.com/decred/base58 v1.0.4 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/ethereum/go-ethereum v1.10.20 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/gtank/merlin v0.1.1 // indirect + github.com/gtank/ristretto255 v0.1.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.2 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/pierrec/xxHash v0.1.5 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/rs/cors v1.8.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect + github.com/tinylib/msgp v1.1.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + 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/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace example.com/tfgrid-kyc-service => . diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dac608d --- /dev/null +++ b/go.sum @@ -0,0 +1,252 @@ +github.com/ChainSafe/go-schnorrkel v1.1.0 h1:rZ6EU+CZFCjB4sHUE1jIu8VDoB/wRKZxoe1tkcO71Wk= +github.com/ChainSafe/go-schnorrkel v1.1.0/go.mod h1:ABkENxiP+cvjFiByMIZ9LYbRoNNLeBLiakC1XeTFxfE= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= +github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/centrifuge/go-substrate-rpc-client/v4 v4.2.1 h1:io49TJ8IOIlzipioJc9pJlrjgdJvqktpUWYxVY5AUjE= +github.com/centrifuge/go-substrate-rpc-client/v4 v4.2.1/go.mod h1:k61SBXqYmnZO4frAJyH3iuqjolYrYsq79r8EstmklDY= +github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= +github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/decred/base58 v1.0.4 h1:QJC6B0E0rXOPA8U/kw2rP+qiRJsUaE2Er+pYb3siUeA= +github.com/decred/base58 v1.0.4/go.mod h1:jJswKPEdvpFpvf7dsDvFZyLT22xZ9lWqEByX38oGd9E= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/ethereum/go-ethereum v1.10.20 h1:75IW830ClSS40yrQC1ZCMZCt5I+zU16oqId2SiQwdQ4= +github.com/ethereum/go-ethereum v1.10.20/go.mod h1:LWUN82TCHGpxB3En5HVmLLzPD7YSrEUFmFfN1nKkVN0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= +github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gofiber/swagger v1.1.0 h1:ff3rg1fB+Rp5JN/N8jfxTiZtMKe/9tB9QDc79fPiJKQ= +github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa h1:Q75Upo5UN4JbPFURXZ8nLKYUvF85dyFRop/vQ0Rv+64= +github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= +github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b h1:QrHweqAtyJ9EwCaGHBu1fghwxIPiopAHV06JlXrMHjk= +github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b/go.mod h1:xxLb2ip6sSUts3g1irPVHyk/DGslwQsNOo9I7smJfNU= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/pierrec/xxHash v0.1.5 h1:n/jBpwTHiER4xYvK3/CdPVnLDPchj8eTJFFLUb4QHBo= +github.com/pierrec/xxHash v0.1.5/go.mod h1:w2waW5Zoa/Wc4Yqe0wgrIYAGKqRMf7czn2HNKXmuL+I= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vedhavyas/go-subkey/v2 v2.0.0 h1:LemDIsrVtRSOkp0FA8HxP6ynfKjeOj3BY2U9UNfeDMA= +github.com/vedhavyas/go-subkey/v2 v2.0.0/go.mod h1:95aZ+XDCWAUUynjlmi7BtPExjXgXxByE0WfBwbmIRH4= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +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/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +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= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go new file mode 100644 index 0000000..5358493 --- /dev/null +++ b/internal/clients/idenfy/idenfy.go @@ -0,0 +1,38 @@ +package idenfy + +import ( + "context" + "net/http" + + "example.com/tfgrid-kyc-service/internal/configs" +) + +type Idenfy struct { + client *http.Client + accessKey string + secretKey string + baseURL string + callbackSignKey []byte +} + +func New(config configs.IdenfyConfig) *Idenfy { + return &Idenfy{ + baseURL: config.BaseURL, + client: &http.Client{}, + accessKey: config.APIKey, + secretKey: config.APISecret, + callbackSignKey: []byte(config.CallbackSignKey), + } +} + +func (c *Idenfy) CreateVerificationSession(ctx context.Context) (interface{}, error) { + var data interface{} + + return data, nil +} + +func (c *Idenfy) ProcessVerificationResult(ctx context.Context, sessionID string) (interface{}, error) { + var data interface{} + + return data, nil +} diff --git a/internal/clients/idenfy/types.go b/internal/clients/idenfy/types.go new file mode 100644 index 0000000..23f36e1 --- /dev/null +++ b/internal/clients/idenfy/types.go @@ -0,0 +1 @@ +package idenfy diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go new file mode 100644 index 0000000..a010bb3 --- /dev/null +++ b/internal/clients/substrate/substrate.go @@ -0,0 +1,66 @@ +package substrate + +import ( + "fmt" + "sync" + + "example.com/tfgrid-kyc-service/internal/configs" + gsrpc "github.com/centrifuge/go-substrate-rpc-client/v4" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + "github.com/vedhavyas/go-subkey/v2" +) + +type Substrate struct { + api *gsrpc.SubstrateAPI + mu sync.Mutex // TODO: Check if SubstrateAPI is thread safe +} + +func New(config configs.TFChainConfig) (*Substrate, error) { + api, err := gsrpc.NewSubstrateAPI(config.WsProviderURL) + if err != nil { + return nil, fmt.Errorf("substrate connection error: failed to initialize Substrate client: %w", err) + } + + chain, _ := api.RPC.System.Chain() + nodeName, _ := api.RPC.System.Name() + nodeVersion, _ := api.RPC.System.Version() + fmt.Println("conected to chain:", chain, "| nodeName:", nodeName, "| nodeVersion:", nodeVersion) + + c := &Substrate{ + api: api, + mu: sync.Mutex{}, + } + return c, nil +} + +func (c *Substrate) GetAccountBalance(address string) (uint64, error) { + _, pubkeyBytes, err := subkey.SS58Decode(address) + if err != nil { + return 0, fmt.Errorf("failed to decode ss58 address: %w", err) + } + account, err := types.NewAddressFromAccountID(pubkeyBytes) + if err != nil { + return 0, fmt.Errorf("failed to create AccountID: %w", err) + } + meta, err := c.api.RPC.State.GetMetadataLatest() + if err != nil { + return 0, fmt.Errorf("failed to get metadata: %w", err) + } + // Create a storage key for the account's balance + key, err := types.CreateStorageKey(meta, "System", "Account", account.AsAccountID.ToBytes()) + if err != nil { + return 0, fmt.Errorf("failed to create storage key: %w", err) + } + + // Query the storage + var accountInfo types.AccountInfo + ok, err := c.api.RPC.State.GetStorageLatest(key, &accountInfo) + if err != nil { + return 0, fmt.Errorf("failed to get storage: %w", err) + } + if !ok { + return 0, nil // account not found + } + + return accountInfo.Data.Free.Uint64(), nil +} diff --git a/internal/configs/config.go b/internal/configs/config.go new file mode 100644 index 0000000..0a1bdb0 --- /dev/null +++ b/internal/configs/config.go @@ -0,0 +1,67 @@ +package configs + +import ( + "fmt" + "strings" + + "github.com/spf13/viper" +) + +type Config struct { + // DB + MongoURI string `mapstructure:"mongo_uri"` + DatabaseName string `mapstructure:"database_name"` + // Server + Port string `mapstructure:"port"` + // Idenfy + Idenfy IdenfyConfig `mapstructure:"idenfy"` + // TFChain + TFChain TFChainConfig `mapstructure:"tfchain"` + // IP limiter + MaxTokenRequestsPerMinute int `mapstructure:"max_token_requests_per_minute"` + // Other + SuspiciousVerificationOutcome string `mapstructure:"suspicious_verification_outcome"` + ExpiredDocumentOutcome string `mapstructure:"expired_document_outcome"` + ChallengeWindow int64 `mapstructure:"challenge_window"` + MinBalanceToVerifyAccount uint64 `mapstructure:"min_balance_to_verify_account"` +} + +type IdenfyConfig struct { + APIKey string `mapstructure:"api_key"` + APISecret string `mapstructure:"api_secret"` + BaseURL string `mapstructure:"base_url"` + CallbackSignKey string `mapstructure:"callback_sign_key"` + WhitelistedIPs []string `mapstructure:"whitelisted_ips"` +} + +type TFChainConfig struct { + WsProviderURL string `mapstructure:"ws_provider_url"` +} + +func LoadConfig() (*Config, error) { + // Replace dots with underscores for nested keys + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + // Make Viper read environment variables + viper.AutomaticEnv() + + // Set default values + viper.SetDefault("port", "8080") + viper.SetDefault("max_token_requests_per_minute", 4) + viper.SetDefault("suspicious_verification_outcome", "verified") + viper.SetDefault("expired_document_outcome", "unverified") + viper.SetDefault("mongo_uri", "mongodb://localhost:27017") + viper.SetDefault("database_name", "tfgrid-kyc-db") + viper.SetDefault("idenfy.base_url", "https://ivs.idenfy.com/api/v2") + viper.SetDefault("tfchain.ws_provider_url", "wss://tfchain.grid.tf") + viper.SetDefault("min_balance_to_verify_account", 10000000) + viper.SetDefault("challenge_window", 120) + + config := &Config{} + err := viper.Unmarshal(config) + if err != nil { + return nil, err + } + fmt.Printf("%+v\n", config) + return config, nil +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go new file mode 100644 index 0000000..4fd6f92 --- /dev/null +++ b/internal/handlers/handlers.go @@ -0,0 +1,113 @@ +package handlers + +import ( + "github.com/gofiber/fiber/v2" + + "example.com/tfgrid-kyc-service/internal/services" +) + +type Handler struct { + tokenService services.TokenService + verificationService services.VerificationService +} + +func NewHandler(tokenService services.TokenService, verificationService services.VerificationService) *Handler { + return &Handler{tokenService: tokenService, verificationService: verificationService} +} + +// @Summary Get or Create Token +// @Description Returns a token for a client +// @Accept json +// @Produce json +// @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 +// @Router /api/v1/token [post] +func (h *Handler) GetorCreateVerificationToken() fiber.Handler { + return func(c *fiber.Ctx) error { + clientID := c.Get("X-Client-ID") + hasRequiredBalance, err := h.tokenService.AccountHasRequiredBalance(c.Context(), clientID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + if !hasRequiredBalance { + return c.Status(fiber.StatusPaymentRequired).JSON(fiber.Map{"error": "Account does not have the required balance"}) + } + result, err := h.tokenService.GetToken(c.Context(), clientID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"result": result}) + } +} + +// @Summary Get Verification Data +// @Description Returns the verification data for a client +// @Accept json +// @Produce json +// @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 +// @Router /api/v1/data [get] +func (h *Handler) GetVerificationData() fiber.Handler { + return func(c *fiber.Ctx) error { + clientID := c.Query("clientID") + result, err := h.verificationService.GetVerificationData(c.Context(), clientID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + if result == nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Verification not found"}) + } + return c.JSON(fiber.Map{"result": result}) + } +} + +// @Summary Get Verification Status +// @Description Returns the verification status for a client +// @Accept json +// @Produce json +// @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.VerificationStatusResponse +// @Router /api/v1/status [get] +func (h *Handler) GetVerificationStatus() fiber.Handler { + return func(c *fiber.Ctx) error { + clientID := c.Query("clientID") + result, err := h.verificationService.GetVerificationStatus(c.Context(), clientID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + if result == nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Verification not found"}) + } + return c.JSON(fiber.Map{"result": result}) + } +} + +// @Summary Process Verification Update +// @Description Processes the verification update for a client +// @Accept json +// @Produce json +// @Success 200 +// @Router /webhooks/idenfy/verification-update [post] +func (h *Handler) ProcessVerificationResult() fiber.Handler { + return func(c *fiber.Ctx) error { + return nil + } +} + +// @Summary Process Doc Expiration Notification +// @Description Processes the doc expiration notification for a client +// @Accept json +// @Produce json +// @Success 200 +// @Router /webhooks/idenfy/id-expiration [post] +func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { + return func(c *fiber.Ctx) error { + return nil + } +} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go new file mode 100644 index 0000000..575886e --- /dev/null +++ b/internal/middleware/middleware.go @@ -0,0 +1,128 @@ +package middleware + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/vedhavyas/go-subkey/v2" + "github.com/vedhavyas/go-subkey/v2/ed25519" + "github.com/vedhavyas/go-subkey/v2/sr25519" +) + +// Logger returns a logger middleware +func Logger() fiber.Handler { + return logger.New() +} + +// CORS returns a CORS middleware +func CORS() fiber.Handler { + return cors.New() +} + +// AuthMiddleware is a middleware that validates the authentication credentials +func AuthMiddleware(challengeWindow int64) fiber.Handler { + return func(c *fiber.Ctx) error { + clientID := c.Get("X-Client-ID") + signature := c.Get("X-Signature") + challenge := c.Get("X-Challenge") + + if clientID == "" || signature == "" || challenge == "" { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Missing authentication credentials", + }) + } + + // Verify the clientID and signature here + err := ValidateChallenge(clientID, signature, challenge, "kyc1.gent01.dev.grid.tf", challengeWindow) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": err.Error(), + }) + } + fmt.Println("✔️ challenge is valid") + // Verify the signature + err = VerifySubstrateSignature(clientID, signature, challenge) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + fmt.Println("✔️ signature is valid") + return c.Next() + } +} + +func fromHex(hex string) ([]byte, bool) { + return subkey.DecodeHex(hex) +} + +func VerifySubstrateSignature(address, signature, challenge string) error { + challengeBytes, success := fromHex(challenge) + if !success { + return fmt.Errorf("malformed challenge: failed to decode hex-encoded challenge") + } + // hex to string + sig, success := fromHex(signature) + if !success { + return fmt.Errorf("malformed signature: failed to decode hex-encoded signature") + } + // Convert address to public key + _, pubkeyBytes, err := subkey.SS58Decode(address) + if err != nil { + return fmt.Errorf("malformed address:failed to decode ss58 address: %w", err) + } + + // Create a new ed25519 public key + pubkeyEd25519, err := ed25519.Scheme{}.FromPublicKey(pubkeyBytes) + if err != nil { + return fmt.Errorf("error: can't create ed25519 public key: %w", err) + } + + if !pubkeyEd25519.Verify(challengeBytes, sig) { + // Create a new sr25519 public key + pubkeySr25519, err := sr25519.Scheme{}.FromPublicKey(pubkeyBytes) + if err != nil { + return fmt.Errorf("error: can't create sr25519 public key: %w", err) + } + if !pubkeySr25519.Verify(challengeBytes, sig) { + return fmt.Errorf("bad signature: signature does not match") + } + } + + return nil +} + +func ValidateChallenge(address, signature, challenge, expectedDomain string, challengeWindow int64) error { + // Parse and validate the challenge + challengeBytes, success := fromHex(challenge) + if !success { + return fmt.Errorf("malformed challenge: failed to decode hex-encoded challenge") + } + parts := strings.Split(string(challengeBytes), ":") + if len(parts) != 2 { + return fmt.Errorf("malformed challenge: invalid challenge format") + } + + // Check the domain + if parts[0] != expectedDomain { + return fmt.Errorf("bad challenge: unexpected domain") + } + + // Check the timestamp + timestamp, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return fmt.Errorf("bad challenge: invalid timestamp") + } + + // Check if the timestamp is within an acceptable range (e.g., last 1 minutes) + if time.Now().Unix()-timestamp > challengeWindow { + return fmt.Errorf("bad challenge: challenge expired") + } + return nil +} diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..82467c2 --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,212 @@ +package models + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Token struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + AuthToken string `bson:"authToken"` + ScanRef string `bson:"scanRef"` + ClientID string `bson:"clientId"` + FirstName string `bson:"firstName"` + LastName string `bson:"lastName"` + SuccessURL string `bson:"successUrl"` + ErrorURL string `bson:"errorUrl"` + UnverifiedURL string `bson:"unverifiedUrl"` + CallbackURL string `bson:"callbackUrl"` + Locale string `bson:"locale"` + ShowInstructions bool `bson:"showInstructions"` + Country string `bson:"country"` + ExpiryTime int `bson:"expiryTime"` + SessionLength int `bson:"sessionLength"` + Documents []string `bson:"documents"` + AllowedDocuments map[string][]string `bson:"allowedDocuments"` + DateOfBirth string `bson:"dateOfBirth"` + DateOfExpiry string `bson:"dateOfExpiry"` + DateOfIssue string `bson:"dateOfIssue"` + Nationality string `bson:"nationality"` + PersonalNumber string `bson:"personalNumber"` + DocumentNumber string `bson:"documentNumber"` + Sex string `bson:"sex"` + DigitString string `bson:"digitString"` + Address string `bson:"address"` + TokenType string `bson:"tokenType"` + ExternalRef string `bson:"externalRef"` + Questionnaire interface{} `bson:"questionnaire"` + UtilityBill bool `bson:"utilityBill"` + AdditionalSteps interface{} `bson:"additionalSteps"` + AdditionalData interface{} `bson:"additionalData"` + CreatedAt time.Time `bson:"createdAt"` +} + +type Verification struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Final *bool `bson:"final"` + Platform string `bson:"platform"` + Status Status `bson:"status"` + Data PersonData `bson:"data"` + FileUrls map[string]string `bson:"fileUrls"` + AdditionalStepPdfUrls map[string]string `bson:"additionalStepPdfUrls"` + AML []AMLCheck `bson:"AML"` + LID interface{} `bson:"LID"` + ScanRef string `bson:"scanRef"` + ExternalRef string `bson:"externalRef"` + ClientID string `bson:"clientId"` + StartTime int64 `bson:"startTime"` + FinishTime int64 `bson:"finishTime"` + ClientIP string `bson:"clientIp"` + ClientIPCountry string `bson:"clientIpCountry"` + ClientLocation string `bson:"clientLocation"` + ManualAddress interface{} `bson:"manualAddress"` + ManualAddressMatch bool `bson:"manualAddressMatch"` + RegistryCenterCheck interface{} `bson:"registryCenterCheck"` + AddressVerification interface{} `bson:"addressVerification"` + QuestionnaireAnswers interface{} `bson:"questionnaireAnswers"` + CompanyID interface{} `bson:"companyId"` + BeneficiaryID interface{} `bson:"beneficiaryId"` + AdditionalSteps map[string]string `bson:"additionalSteps"` + CreatedAt time.Time `bson:"createdAt"` +} + +type Overall string + +const ( + OverallApproved Overall = "APPROVED" + OverallDenied Overall = "DENIED" + OverallSuspected Overall = "SUSPECTED" + OverallExpired Overall = "EXPIRED" +) + +type Status struct { + Overall Overall `bson:"overall"` + SuspicionReasons []string `bson:"suspicionReasons"` + DenyReasons []string `bson:"denyReasons"` + FraudTags []string `bson:"fraudTags"` + MismatchTags []string `bson:"mismatchTags"` + AutoFace string `bson:"autoFace"` + ManualFace string `bson:"manualFace"` + AutoDocument string `bson:"autoDocument"` + ManualDocument string `bson:"manualDocument"` + AdditionalSteps string `bson:"additionalSteps"` + AMLResultClass string `bson:"amlResultClass"` + PEPSStatus string `bson:"pepsStatus"` + SanctionsStatus string `bson:"sanctionsStatus"` + AdverseMediaStatus string `bson:"adverseMediaStatus"` +} + +type DocumentType string + +const ( + ID_CARD DocumentType = "ID_CARD" + PASSPORT DocumentType = "PASSPORT" + RESIDENCE_PERMIT DocumentType = "RESIDENCE_PERMIT" + DRIVER_LICENSE DocumentType = "DRIVER_LICENSE" + PAN_CARD DocumentType = "PAN_CARD" + AADHAAR DocumentType = "AADHAAR" + OTHER DocumentType = "OTHER" + VISA DocumentType = "VISA" + BORDER_CROSSING DocumentType = "BORDER_CROSSING" + ASYLUM DocumentType = "ASYLUM" + NATIONAL_PASSPORT DocumentType = "NATIONAL_PASSPORT" + PROVISIONAL_DRIVER_LICENSE DocumentType = "PROVISIONAL_DRIVER_LICENSE" + VOTER_CARD DocumentType = "VOTER_CARD" + OLD_ID_CARD DocumentType = "OLD_ID_CARD" + TRAVEL_CARD DocumentType = "TRAVEL_CARD" + PHOTO_CARD DocumentType = "PHOTO_CARD" + MILITARY_CARD DocumentType = "MILITARY_CARD" + PROOF_OF_AGE_CARD DocumentType = "PROOF_OF_AGE_CARD" + DIPLOMATIC_ID DocumentType = "DIPLOMATIC_ID" +) + +type Sex string + +const ( + MALE Sex = "MALE" + FEMALE Sex = "FEMALE" + UNDEFINED Sex = "UNDEFINED" +) + +type AgeEstimate string + +const ( + UNDER_13 AgeEstimate = "UNDER_13" + OVER_13 AgeEstimate = "OVER_13" + OVER_18 AgeEstimate = "OVER_18" + OVER_22 AgeEstimate = "OVER_22" + OVER_25 AgeEstimate = "OVER_25" + OVER_30 AgeEstimate = "OVER_30" +) + +type PersonData struct { + DocFirstName string `bson:"docFirstName"` + DocLastName string `bson:"docLastName"` + DocNumber string `bson:"docNumber"` + DocPersonalCode string `bson:"docPersonalCode"` + DocExpiry string `bson:"docExpiry"` + DocDOB string `bson:"docDob"` + DocDateOfIssue string `bson:"docDateOfIssue"` + DocType DocumentType `bson:"docType"` + DocSex Sex `bson:"docSex"` + DocNationality string `bson:"docNationality"` + DocIssuingCountry string `bson:"docIssuingCountry"` + BirthPlace string `bson:"birthPlace"` + Authority string `bson:"authority"` + Address string `bson:"address"` + DocTemporaryAddress string `bson:"docTemporaryAddress"` + MothersMaidenName string `bson:"mothersMaidenName"` + DocBirthName string `bson:"docBirthName"` + DriverLicenseCategory string `bson:"driverLicenseCategory"` + ManuallyDataChanged bool `bson:"manuallyDataChanged"` + FullName string `bson:"fullName"` + SelectedCountry string `bson:"selectedCountry"` + OrgFirstName string `bson:"orgFirstName"` + OrgLastName string `bson:"orgLastName"` + OrgNationality string `bson:"orgNationality"` + OrgBirthPlace string `bson:"orgBirthPlace"` + OrgAuthority string `bson:"orgAuthority"` + OrgAddress string `bson:"orgAddress"` + OrgTemporaryAddress string `bson:"orgTemporaryAddress"` + OrgMothersMaidenName string `bson:"orgMothersMaidenName"` + OrgBirthName string `bson:"orgBirthName"` + AgeEstimate AgeEstimate `bson:"ageEstimate"` + ClientIPProxyRiskLevel string `bson:"clientIpProxyRiskLevel"` + DuplicateFaces []string `bson:"duplicateFaces"` + DuplicateDocFaces []string `bson:"duplicateDocFaces"` + AdditionalData interface{} `bson:"additionalData"` +} + +type AMLCheck struct { + Status AMLStatus `bson:"status"` + Data []AMLData `bson:"data"` + ServiceName string `bson:"serviceName"` + ServiceGroupType string `bson:"serviceGroupType"` + UID string `bson:"uid"` + ErrorMessage string `bson:"errorMessage"` +} + +type AMLStatus struct { + ServiceSuspected bool `bson:"serviceSuspected"` + ServiceUsed bool `bson:"serviceUsed"` + ServiceFound bool `bson:"serviceFound"` + CheckSuccessful bool `bson:"checkSuccessful"` + OverallStatus string `bson:"overallStatus"` +} + +type AMLData struct { + Name string `bson:"name"` + Surname string `bson:"surname"` + Nationality string `bson:"nationality"` + DOB string `bson:"dob"` + Suspicion string `bson:"suspicion"` + Reason string `bson:"reason"` + ListNumber string `bson:"listNumber"` + ListName string `bson:"listName"` + Score *float64 `bson:"score"` + LastUpdate *string `bson:"lastUpdate"` + IsPerson *bool `bson:"isPerson"` + IsActive *bool `bson:"isActive"` + CheckDate string `bson:"checkDate"` +} diff --git a/internal/repository/mongo.go b/internal/repository/mongo.go new file mode 100644 index 0000000..2374355 --- /dev/null +++ b/internal/repository/mongo.go @@ -0,0 +1,26 @@ +package repository + +import ( + "context" + "time" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func ConnectToMongoDB(mongoURI string) (*mongo.Client, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) + if err != nil { + return nil, err + } + + err = client.Ping(ctx, nil) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..91652b1 --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,19 @@ +package repository + +import ( + "context" + + "example.com/tfgrid-kyc-service/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) error +} + +type VerificationRepository interface { + SaveVerification(ctx context.Context, verification *models.Verification) error + GetVerification(ctx context.Context, clientID string) (*models.Verification, error) + DeleteVerification(ctx context.Context, clientID string) error +} diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go new file mode 100644 index 0000000..559f5bc --- /dev/null +++ b/internal/repository/token_repository.go @@ -0,0 +1,44 @@ +package repository + +import ( + "context" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + + "example.com/tfgrid-kyc-service/internal/models" +) + +type MongoTokenRepository struct { + collection *mongo.Collection +} + +func NewMongoTokenRepository(db *mongo.Database) TokenRepository { + return &MongoTokenRepository{ + collection: db.Collection("tokens"), + } +} + +func (r *MongoTokenRepository) SaveToken(ctx context.Context, token *models.Token) error { + token.CreatedAt = time.Now() + _, err := r.collection.InsertOne(ctx, token) + return err +} + +func (r *MongoTokenRepository) GetToken(ctx context.Context, authToken string) (*models.Token, error) { + var token models.Token + err := r.collection.FindOne(ctx, bson.M{"authToken": authToken}).Decode(&token) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + return &token, nil +} + +func (r *MongoTokenRepository) DeleteToken(ctx context.Context, authToken string) error { + _, err := r.collection.DeleteOne(ctx, bson.M{"authToken": authToken}) + return err +} diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go new file mode 100644 index 0000000..b08dedb --- /dev/null +++ b/internal/repository/verification_repository.go @@ -0,0 +1,43 @@ +package repository + +import ( + "context" + "time" + + "example.com/tfgrid-kyc-service/internal/models" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +type MongoVerificationRepository struct { + collection *mongo.Collection +} + +func NewMongoVerificationRepository(db *mongo.Database) VerificationRepository { + return &MongoVerificationRepository{ + collection: db.Collection("verifications"), + } +} + +func (r *MongoVerificationRepository) SaveVerification(ctx context.Context, verification *models.Verification) error { + verification.CreatedAt = time.Now() + _, err := r.collection.InsertOne(ctx, verification) + return err +} + +func (r *MongoVerificationRepository) GetVerification(ctx context.Context, clientID string) (*models.Verification, error) { + var verification models.Verification + err := r.collection.FindOne(ctx, bson.M{"clientID": clientID}).Decode(&verification) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + return &verification, nil +} + +func (r *MongoVerificationRepository) DeleteVerification(ctx context.Context, clientID string) error { + _, err := r.collection.DeleteOne(ctx, bson.M{"clientID": clientID}) + return err +} diff --git a/internal/responses/responses.go b/internal/responses/responses.go new file mode 100644 index 0000000..c649567 --- /dev/null +++ b/internal/responses/responses.go @@ -0,0 +1,81 @@ +package responses + +import "github.com/gofiber/fiber/v2" + +type Response struct { + Success bool `json:"success"` + Data interface{} `json:"data,omitempty"` + Message string `json:"message,omitempty"` +} + +func SuccessResponse(c *fiber.Ctx, statusCode int, data interface{}, message string) error { + return c.Status(statusCode).JSON(Response{ + Success: true, + Data: data, + Message: message, + }) +} + +type TokenResponse struct { + Message string `json:"message"` + AuthToken string `json:"authToken"` + ScanRef string `json:"scanRef"` + ClientID string `json:"clientId"` + ExpiryTime int `json:"expiryTime"` + SessionLength int `json:"sessionLength"` + DigitString string `json:"digitString"` + TokenType string `json:"tokenType"` +} + +type VerificationStatusResponse struct { + FraudTags []string `json:"fraudTags"` + MismatchTags []string `json:"mismatchTags"` + AutoDocument string `json:"autoDocument"` + AutoFace string `json:"autoFace"` + ManualDocument string `json:"manualDocument"` + ManualFace string `json:"manualFace"` + ScanRef string `json:"scanRef"` + ClientID string `json:"clientId"` + Status string `json:"status"` +} + +type VerificationDataResponse struct { + DocFirstName string `json:"docFirstName"` + DocLastName string `json:"docLastName"` + DocNumber string `json:"docNumber"` + DocPersonalCode string `json:"docPersonalCode"` + DocExpiry string `json:"docExpiry"` + DocDob string `json:"docDob"` + DocDateOfIssue string `json:"docDateOfIssue"` + DocType string `json:"docType"` + DocSex string `json:"docSex"` + DocNationality string `json:"docNationality"` + DocIssuingCountry string `json:"docIssuingCountry"` + DocTemporaryAddress string `json:"docTemporaryAddress"` + DocBirthName string `json:"docBirthName"` + BirthPlace string `json:"birthPlace"` + Authority string `json:"authority"` + Address string `json:"address"` + MotherMaidenName string `json:"mothersMaidenName"` + DriverLicenseCategory string `json:"driverLicenseCategory"` + ManuallyDataChanged bool `json:"manuallyDataChanged"` + FullName string `json:"fullName"` + OrgFirstName string `json:"orgFirstName"` + OrgLastName string `json:"orgLastName"` + OrgNationality string `json:"orgNationality"` + OrgBirthPlace string `json:"orgBirthPlace"` + OrgAuthority string `json:"orgAuthority"` + OrgAddress string `json:"orgAddress"` + OrgTemporaryAddress string `json:"orgTemporaryAddress"` + OrgMothersMaidenName string `json:"orgMothersMaidenName"` + OrgBirthName string `json:"orgBirthName"` + SelectedCountry string `json:"selectedCountry"` + AgeEstimate string `json:"ageEstimate"` + ClientIpProxyRiskLevel string `json:"clientIpProxyRiskLevel"` + DuplicateFaces []string `json:"duplicateFaces"` + DuplicateDocFaces []string `json:"duplicateDocFaces"` + AddressVerification interface{} `json:"addressVerification"` + AdditionalData interface{} `json:"additionalData"` + ScanRef string `json:"scanRef"` + ClientID string `json:"clientId"` +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..09c40d2 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,98 @@ +package server + +import ( + "log" + "os" + "os/signal" + "syscall" + + _ "example.com/tfgrid-kyc-service/api/docs" + "example.com/tfgrid-kyc-service/internal/clients/idenfy" + "example.com/tfgrid-kyc-service/internal/clients/substrate" + "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/handlers" + "example.com/tfgrid-kyc-service/internal/middleware" + "example.com/tfgrid-kyc-service/internal/repository" + "example.com/tfgrid-kyc-service/internal/services" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/helmet" + "github.com/gofiber/fiber/v2/middleware/limiter" + "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/gofiber/swagger" +) + +// implement server struct that have fiber app and config +type Server struct { + app *fiber.App + config *configs.Config +} + +func New(config *configs.Config) *Server { + + app := fiber.New() + + // Global middlewares + app.Use(middleware.Logger()) + app.Use(middleware.CORS()) + app.Use(recover.New()) + app.Use(helmet.New()) + + // Database connection + db, err := repository.ConnectToMongoDB(config.MongoURI) + if err != nil { + log.Fatalf("Failed to connect to MongoDB: %v", err) + } + database := db.Database(config.DatabaseName) + + // Initialize repositories + tokenRepo := repository.NewMongoTokenRepository(database) + verificationRepo := repository.NewMongoVerificationRepository(database) + + // Initialize services + idenfyClient := idenfy.New(config.Idenfy) + substrateClient, err := substrate.New(config.TFChain) + if err != nil { + log.Fatalf("Failed to initialize substrate client: %v", err) + } + tokenService := services.NewTokenService(tokenRepo, idenfyClient, substrateClient, config.MinBalanceToVerifyAccount) + verificationService := services.NewVerificationService(verificationRepo) + + // Initialize handler + handler := handlers.NewHandler(tokenService, verificationService) + + // Routes + app.Get("/docs/*", swagger.HandlerDefault) + + v1 := app.Group("/api/v1", limiter.New(), middleware.AuthMiddleware(config.ChallengeWindow)) + v1.Post("/token", handler.GetorCreateVerificationToken()) + v1.Get("/data", handler.GetVerificationData()) + v1.Get("/status", handler.GetVerificationStatus()) + + // Webhook routes + webhooks := app.Group("/webhooks/idenfy") // TODO: middleware to verify hmac signature of the webhook, only accept from whitelisted ip addresses + webhooks.Post("/verification-update", handler.ProcessVerificationResult()) + webhooks.Post("/id-expiration", handler.ProcessDocExpirationNotification()) + + return &Server{app: app, config: config} +} + +func (s *Server) Start() { + // Start server + go func() { + if err := s.app.Listen(":" + s.config.Port); err != nil { + log.Fatalf("Failed to start server: %v", err) + } + }() + + // Graceful shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt, syscall.SIGTERM) + <-quit + log.Println("Shutting down server...") + + if err := s.app.Shutdown(); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + + log.Println("Server exiting") +} diff --git a/internal/services/service.go b/internal/services/service.go new file mode 100644 index 0000000..ef93111 --- /dev/null +++ b/internal/services/service.go @@ -0,0 +1,19 @@ +package services + +import ( + "context" + + "example.com/tfgrid-kyc-service/internal/responses" +) + +type TokenService interface { + CreateToken(ctx context.Context, clientID string) (*responses.TokenResponse, error) + GetToken(ctx context.Context, clientID string) (*responses.TokenResponse, error) + DeleteToken(ctx context.Context, clientID string) error + AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) +} + +type VerificationService interface { + GetVerificationData(ctx context.Context, clientID string) (*responses.VerificationDataResponse, error) + GetVerificationStatus(ctx context.Context, clientID string) (*responses.VerificationStatusResponse, error) +} diff --git a/internal/services/token_service.go b/internal/services/token_service.go new file mode 100644 index 0000000..a25d71a --- /dev/null +++ b/internal/services/token_service.go @@ -0,0 +1,44 @@ +package services + +import ( + "context" + + "example.com/tfgrid-kyc-service/internal/clients/idenfy" + "example.com/tfgrid-kyc-service/internal/clients/substrate" + "example.com/tfgrid-kyc-service/internal/repository" + "example.com/tfgrid-kyc-service/internal/responses" +) + +type tokenService struct { + repo repository.TokenRepository + idenfy *idenfy.Idenfy + substrate *substrate.Substrate + requiredBalance uint64 +} + +func NewTokenService(repo repository.TokenRepository, idenfy *idenfy.Idenfy, substrate *substrate.Substrate, requiredBalance uint64) TokenService { + return &tokenService{repo: repo, idenfy: idenfy, substrate: substrate, requiredBalance: requiredBalance} +} + +func (s *tokenService) CreateToken(ctx context.Context, clientID string) (*responses.TokenResponse, error) { + token := &responses.TokenResponse{} + + return token, nil +} + +func (s *tokenService) GetToken(ctx context.Context, clientID string) (*responses.TokenResponse, error) { + token := &responses.TokenResponse{} + return token, nil +} + +func (s *tokenService) DeleteToken(ctx context.Context, clientID string) error { + return s.repo.DeleteToken(ctx, clientID) +} + +func (s *tokenService) AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) { + balance, err := s.substrate.GetAccountBalance(address) + if err != nil { + return false, err + } + return balance >= s.requiredBalance, nil +} diff --git a/internal/services/verification_service.go b/internal/services/verification_service.go new file mode 100644 index 0000000..b8aaed0 --- /dev/null +++ b/internal/services/verification_service.go @@ -0,0 +1,27 @@ +package services + +import ( + "context" + + "example.com/tfgrid-kyc-service/internal/repository" + "example.com/tfgrid-kyc-service/internal/responses" +) + +type verificationService struct { + repo repository.VerificationRepository +} + +func NewVerificationService(repo repository.VerificationRepository) VerificationService { + return &verificationService{repo: repo} +} + +func (s *verificationService) GetVerificationData(ctx context.Context, clientID string) (*responses.VerificationDataResponse, error) { + // build responses.VerificationDataResponse from models.Verification + verificationData := &responses.VerificationDataResponse{} + return verificationData, nil +} + +func (s *verificationService) GetVerificationStatus(ctx context.Context, clientID string) (*responses.VerificationStatusResponse, error) { + verificationStatus := &responses.VerificationStatusResponse{} + return verificationStatus, nil +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..d4b585b --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1 @@ +package utils diff --git a/scripts/dev/auth/generate-test-auth-data.go b/scripts/dev/auth/generate-test-auth-data.go new file mode 100644 index 0000000..dd1a111 --- /dev/null +++ b/scripts/dev/auth/generate-test-auth-data.go @@ -0,0 +1,64 @@ +package main + +import ( + "encoding/hex" + "fmt" + "time" + + "github.com/vedhavyas/go-subkey/v2/ed25519" + "github.com/vedhavyas/go-subkey/v2/sr25519" +) + +const ( + domain = "kyc1.gent01.dev.grid.tf" +) + +// Generate test auth data for development use +func main() { + message := createSignMessage() + krSr25519, err := sr25519.Scheme{}.Generate() + if err != nil { + panic(err) + } + krEd25519, err := ed25519.Scheme{}.Generate() + if err != nil { + panic(err) + } + msg := []byte(message) + sigSr25519, err := krSr25519.Sign(msg) + if err != nil { + panic(err) + } + sigEd25519, err := krEd25519.Sign(msg) + if err != nil { + panic(err) + } + messageString := hex.EncodeToString([]byte(message)) + fmt.Println("______________________") + fmt.Println("Auth Data") + fmt.Println("______________________") + fmt.Println("** SR25519 **") + //fmt.Println("Public key sr25519: ", hex.EncodeToString(krSr25519.Public())) + fmt.Println("SS58Address sr25519: ", krSr25519.SS58Address(42)) + fmt.Println("Challenge hex: ", hex.EncodeToString([]byte(message))) + fmt.Println("Signature sr25519: ", hex.EncodeToString(sigSr25519)) + fmt.Println("______________________") + fmt.Println("** ED25519 **") + // fmt.Println("Public key ed25519: ", hex.EncodeToString(krEd25519.Public())) + fmt.Println("SS58Address ed25519: ", krEd25519.SS58Address(42)) + fmt.Println("Challenge hex: ", hex.EncodeToString([]byte(message))) + fmt.Println("Signature ed25519: ", hex.EncodeToString(sigEd25519)) + fmt.Println("______________________") + bytes, err := hex.DecodeString(messageString) + if err != nil { + panic(err) + } + fmt.Println("challenge string (plain text): ", string(bytes)) +} + +func createSignMessage() string { + // return a message with the domain and the current timestamp in hex + message := fmt.Sprintf("%s:%d", domain, time.Now().Unix()) + fmt.Println("message: ", message) + return message +} diff --git a/scripts/dev/balance/check-account-balance.go b/scripts/dev/balance/check-account-balance.go new file mode 100644 index 0000000..4a0ea66 --- /dev/null +++ b/scripts/dev/balance/check-account-balance.go @@ -0,0 +1,25 @@ +// Use substarte client to get account free balance for development use +package main + +import ( + "fmt" + + "example.com/tfgrid-kyc-service/internal/clients/substrate" + "example.com/tfgrid-kyc-service/internal/configs" +) + +func main() { + config, err := configs.LoadConfig() + if err != nil { + panic(err) + } + substrateClient, err := substrate.New(config.TFChain) + if err != nil { + panic(err) + } + free_balance, err := substrateClient.GetAccountBalance("5DFkH2fcqYecVHjfgAEfxgsJyoEg5Kd93JFihfpHDaNoWagJ") + if err != nil { + panic(err) + } + fmt.Println("balance: ", free_balance) +} From 6ec75baa9a1d6287ea2c72be96c282d8538d36c7 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Tue, 15 Oct 2024 14:11:06 +0300 Subject: [PATCH 002/105] update .gitignore --- .app.env | 14 -------------- .db.env | 2 -- .gitignore | 2 ++ 3 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 .app.env delete mode 100644 .db.env diff --git a/.app.env b/.app.env deleted file mode 100644 index 123fa92..0000000 --- a/.app.env +++ /dev/null @@ -1,14 +0,0 @@ -MONGO_URI=mongodb://root:password@mongodb:27017 -DATABASE_NAME=tfgrid-kyc-db -PORT=8080 -MAX_TOKEN_REQUESTS_PER_MINUTE=2 -SUSPICIOUS_VERIFICATION_OUTCOME=verified -EXPIRED_DOCUMENT_OUTCOME=verified -CHALLENGE_WINDOW=120 -IDENFY_BASE_URL=https://ivs.idenfy.com/api/v2 -IDENFY_API_KEY= -IDENFY_SECRET= -IDENFY_CALLBACK_SIGN_KEY= -IDENFY_WHITELISTED_IPS= -TFCHAIN_WS_PROVIDER_URL=wss://tfchain.grid.tf -TFCHAIN_MIN_BALANCE_TO_VERIFY_ACCOUNT=1000000 \ No newline at end of file diff --git a/.db.env b/.db.env deleted file mode 100644 index 077b4a8..0000000 --- a/.db.env +++ /dev/null @@ -1,2 +0,0 @@ -MONGO_INITDB_ROOT_USERNAME=root -MONGO_INITDB_ROOT_PASSWORD=password \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6f72f89..f2f23db 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ go.work.sum # env file .env +.app.env +.db.env From a124296ab6083367bf8378ad5f21b36915f1e7c5 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Tue, 15 Oct 2024 15:04:12 +0300 Subject: [PATCH 003/105] chore: grouping gndpoints in Swagger UI --- api/docs/docs.go | 17 ++++++++++++++++- api/docs/swagger.json | 17 ++++++++++++++++- api/docs/swagger.yaml | 12 +++++++++++- internal/handlers/handlers.go | 7 ++++++- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 83f3cac..78d8582 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -29,6 +29,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "Verification" + ], "summary": "Get Verification Data", "parameters": [ { @@ -76,6 +79,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "Verification" + ], "summary": "Get Verification Status", "parameters": [ { @@ -123,7 +129,10 @@ const docTemplate = `{ "produces": [ "application/json" ], - "summary": "Get or Create Token", + "tags": [ + "Token" + ], + "summary": "Get or Generate iDenfy Verification Token", "parameters": [ { "maxLength": 48, @@ -170,6 +179,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "Webhooks" + ], "summary": "Process Doc Expiration Notification", "responses": { "200": { @@ -187,6 +199,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "Webhooks" + ], "summary": "Process Verification Update", "responses": { "200": { diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 7c9c0d2..2ed8cda 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -22,6 +22,9 @@ "produces": [ "application/json" ], + "tags": [ + "Verification" + ], "summary": "Get Verification Data", "parameters": [ { @@ -69,6 +72,9 @@ "produces": [ "application/json" ], + "tags": [ + "Verification" + ], "summary": "Get Verification Status", "parameters": [ { @@ -116,7 +122,10 @@ "produces": [ "application/json" ], - "summary": "Get or Create Token", + "tags": [ + "Token" + ], + "summary": "Get or Generate iDenfy Verification Token", "parameters": [ { "maxLength": 48, @@ -163,6 +172,9 @@ "produces": [ "application/json" ], + "tags": [ + "Webhooks" + ], "summary": "Process Doc Expiration Notification", "responses": { "200": { @@ -180,6 +192,9 @@ "produces": [ "application/json" ], + "tags": [ + "Webhooks" + ], "summary": "Process Verification Update", "responses": { "200": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index aaef222..255a04b 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -168,6 +168,8 @@ paths: schema: $ref: '#/definitions/responses.VerificationDataResponse' summary: Get Verification Data + tags: + - Verification /api/v1/status: get: consumes: @@ -201,6 +203,8 @@ paths: schema: $ref: '#/definitions/responses.VerificationStatusResponse' summary: Get Verification Status + tags: + - Verification /api/v1/token: post: consumes: @@ -233,7 +237,9 @@ paths: description: OK schema: $ref: '#/definitions/responses.TokenResponse' - summary: Get or Create Token + summary: Get or Generate iDenfy Verification Token + tags: + - Token /webhooks/idenfy/id-expiration: post: consumes: @@ -245,6 +251,8 @@ paths: "200": description: OK summary: Process Doc Expiration Notification + tags: + - Webhooks /webhooks/idenfy/verification-update: post: consumes: @@ -256,4 +264,6 @@ paths: "200": description: OK summary: Process Verification Update + tags: + - Webhooks swagger: "2.0" diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 4fd6f92..145e3d5 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -15,8 +15,9 @@ func NewHandler(tokenService services.TokenService, verificationService services return &Handler{tokenService: tokenService, verificationService: verificationService} } -// @Summary Get or Create Token +// @Summary Get or Generate iDenfy Verification Token // @Description Returns a token for a client +// @Tags Token // @Accept json // @Produce json // @Param X-Client-ID header string true "TFChain SS58Address" minlength(48) maxlength(48) @@ -44,6 +45,7 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { // @Summary Get Verification Data // @Description Returns the verification data for a client +// @Tags Verification // @Accept json // @Produce json // @Param X-Client-ID header string true "TFChain SS58Address" minlength(48) maxlength(48) @@ -67,6 +69,7 @@ func (h *Handler) GetVerificationData() fiber.Handler { // @Summary Get Verification Status // @Description Returns the verification status for a client +// @Tags Verification // @Accept json // @Produce json // @Param X-Client-ID header string true "TFChain SS58Address" minlength(48) maxlength(48) @@ -90,6 +93,7 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { // @Summary Process Verification Update // @Description Processes the verification update for a client +// @Tags Webhooks // @Accept json // @Produce json // @Success 200 @@ -102,6 +106,7 @@ func (h *Handler) ProcessVerificationResult() fiber.Handler { // @Summary Process Doc Expiration Notification // @Description Processes the doc expiration notification for a client +// @Tags Webhooks // @Accept json // @Produce json // @Success 200 From eeefa12d334bac4c2399ceb82c1d44c1574b439c Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Wed, 16 Oct 2024 17:46:00 +0300 Subject: [PATCH 004/105] Implement token generation flow --- .app.env.example | 4 +- .express.env.example | 4 + .gitignore | 3 +- api/docs/docs.go | 19 +++- api/docs/swagger.json | 19 +++- api/docs/swagger.yaml | 13 ++- docker-compose.yml | 17 +++- go.mod | 1 + go.sum | 2 + internal/clients/idenfy/idenfy.go | 63 ++++++++++-- internal/configs/config.go | 113 +++++++++++++++++----- internal/handlers/handlers.go | 21 +++- internal/models/models.go | 1 + internal/repository/token_repository.go | 43 +++++++- internal/responses/responses.go | 7 +- internal/server/server.go | 9 +- internal/services/service.go | 6 +- internal/services/token_service.go | 63 +++++++++--- internal/services/verification_service.go | 49 +++++++++- 19 files changed, 384 insertions(+), 73 deletions(-) create mode 100644 .express.env.example diff --git a/.app.env.example b/.app.env.example index 123fa92..1b266af 100644 --- a/.app.env.example +++ b/.app.env.example @@ -7,8 +7,8 @@ EXPIRED_DOCUMENT_OUTCOME=verified CHALLENGE_WINDOW=120 IDENFY_BASE_URL=https://ivs.idenfy.com/api/v2 IDENFY_API_KEY= -IDENFY_SECRET= +IDENFY_API_SECRET= IDENFY_CALLBACK_SIGN_KEY= IDENFY_WHITELISTED_IPS= TFCHAIN_WS_PROVIDER_URL=wss://tfchain.grid.tf -TFCHAIN_MIN_BALANCE_TO_VERIFY_ACCOUNT=1000000 \ No newline at end of file +MIN_BALANCE_TO_VERIFY_ACCOUNT=1000000 \ No newline at end of file diff --git a/.express.env.example b/.express.env.example new file mode 100644 index 0000000..bf876a6 --- /dev/null +++ b/.express.env.example @@ -0,0 +1,4 @@ +ME_CONFIG_MONGODB_AUTH_USERNAME=root +ME_CONFIG_MONGODB_AUTH_PASSWORD=password +ME_CONFIG_BASICAUTH_USERNAME=admin +ME_CONFIG_BASICAUTH_PASSWORD=password \ No newline at end of file diff --git a/.gitignore b/.gitignore index f2f23db..70a08e7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,4 @@ go.work.sum # env file .env -.app.env -.db.env +.*.env diff --git a/api/docs/docs.go b/api/docs/docs.go index 78d8582..3fe3aa8 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -164,7 +164,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.TokenResponse" + "$ref": "#/definitions/responses.TokenResponseWithStatus" } } } @@ -227,9 +227,6 @@ const docTemplate = `{ "expiryTime": { "type": "integer" }, - "message": { - "type": "string" - }, "scanRef": { "type": "string" }, @@ -241,6 +238,20 @@ const docTemplate = `{ } } }, + "responses.TokenResponseWithStatus": { + "type": "object", + "properties": { + "is_new_token": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "token": { + "$ref": "#/definitions/responses.TokenResponse" + } + } + }, "responses.VerificationDataResponse": { "type": "object", "properties": { diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 2ed8cda..12847b0 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -157,7 +157,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.TokenResponse" + "$ref": "#/definitions/responses.TokenResponseWithStatus" } } } @@ -220,9 +220,6 @@ "expiryTime": { "type": "integer" }, - "message": { - "type": "string" - }, "scanRef": { "type": "string" }, @@ -234,6 +231,20 @@ } } }, + "responses.TokenResponseWithStatus": { + "type": "object", + "properties": { + "is_new_token": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "token": { + "$ref": "#/definitions/responses.TokenResponse" + } + } + }, "responses.VerificationDataResponse": { "type": "object", "properties": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 255a04b..f267d98 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -10,8 +10,6 @@ definitions: type: string expiryTime: type: integer - message: - type: string scanRef: type: string sessionLength: @@ -19,6 +17,15 @@ definitions: tokenType: type: string type: object + responses.TokenResponseWithStatus: + properties: + is_new_token: + type: boolean + message: + type: string + token: + $ref: '#/definitions/responses.TokenResponse' + type: object responses.VerificationDataResponse: properties: additionalData: {} @@ -236,7 +243,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/responses.TokenResponse' + $ref: '#/definitions/responses.TokenResponseWithStatus' summary: Get or Generate iDenfy Verification Token tags: - Token diff --git a/docker-compose.yml b/docker-compose.yml index 725b4a1..7e140cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: build: context: . dockerfile: Dockerfile - container_name: tfgrid_kyc_api + container_name: tf_kyc_api restart: unless-stopped ports: - "8080:8080" @@ -18,7 +18,7 @@ services: mongodb: image: mongo:latest - container_name: mongodb_dev + container_name: tf_kyc_db ports: - "27017:27017" volumes: @@ -32,6 +32,19 @@ services: retries: 5 start_period: 40s + mongo-express: + image: mongo-express:latest + container_name: mongo_express + environment: + - ME_CONFIG_MONGODB_SERVER=mongodb + - ME_CONFIG_MONGODB_PORT=27017 + depends_on: + - mongodb + ports: + - "8888:8081" + env_file: + - .express.env + volumes: mongodb_data: diff --git a/go.mod b/go.mod index f058e17..3ae4c4b 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.1 require ( github.com/centrifuge/go-substrate-rpc-client/v4 v4.2.1 + github.com/gboddin/go-idenfy v1.0.1 github.com/gofiber/fiber/v2 v2.52.5 github.com/gofiber/swagger v1.1.0 github.com/spf13/viper v1.19.0 diff --git a/go.sum b/go.sum index dac608d..39e3d5f 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gboddin/go-idenfy v1.0.1 h1:7ggqkSaOY6XaZX/lBkdoHUmNnqQiPyqAFabmGqPANAM= +github.com/gboddin/go-idenfy v1.0.1/go.mod h1:vtPk7bX+AADEohnyygs65JwmTYWpApOUbhTWw9nw8zc= github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index 5358493..9271729 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -2,33 +2,84 @@ package idenfy import ( "context" - "net/http" + "encoding/base64" + "encoding/json" + "fmt" "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/models" + "github.com/valyala/fasthttp" ) type Idenfy struct { - client *http.Client + client *fasthttp.Client accessKey string secretKey string baseURL string callbackSignKey []byte } +const ( + VerificationSessionEndpoint = "/api/v2/token" +) + func New(config configs.IdenfyConfig) *Idenfy { return &Idenfy{ baseURL: config.BaseURL, - client: &http.Client{}, + client: &fasthttp.Client{}, accessKey: config.APIKey, secretKey: config.APISecret, callbackSignKey: []byte(config.CallbackSignKey), } } -func (c *Idenfy) CreateVerificationSession(ctx context.Context) (interface{}, error) { - var data interface{} +func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) (models.Token, error) { + url := c.baseURL + VerificationSessionEndpoint - return data, nil + req := fasthttp.AcquireRequest() + defer fasthttp.ReleaseRequest(req) + + req.SetRequestURI(url) + req.Header.SetMethod(fasthttp.MethodPost) + req.Header.Set("Content-Type", "application/json") + + // Set basic auth + authStr := c.accessKey + ":" + c.secretKey + auth := base64.StdEncoding.EncodeToString([]byte(authStr)) + req.Header.Set("Authorization", "Basic "+auth) + + jsonBody, err := json.Marshal(map[string]interface{}{ + "clientId": clientID, + "generateDigitString": true, + "expiryTime": 30, + "dummyStatus": "APPROVED", // TODO: remove this after testing + }) + if err != nil { + return models.Token{}, fmt.Errorf("error marshaling request body: %w", err) + } + req.SetBody(jsonBody) + + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseResponse(resp) + fmt.Println("request", req) + err = c.client.Do(req, resp) + if err != nil { + return models.Token{}, fmt.Errorf("error sending request: %w", err) + } + + if resp.StatusCode() < 200 || resp.StatusCode() >= 300 { + fmt.Println("response", resp) + return models.Token{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) + } + fmt.Println(string(resp.Body())) + + var result models.Token + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return models.Token{}, fmt.Errorf("error decoding response: %w", err) + } + + fmt.Println(result) + return result, nil } func (c *Idenfy) ProcessVerificationResult(ctx context.Context, sessionID string) (interface{}, error) { diff --git a/internal/configs/config.go b/internal/configs/config.go index 0a1bdb0..19de4c0 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -19,11 +19,11 @@ type Config struct { TFChain TFChainConfig `mapstructure:"tfchain"` // IP limiter MaxTokenRequestsPerMinute int `mapstructure:"max_token_requests_per_minute"` + // Verification + Verification VerificationConfig `mapstructure:"verification"` // Other - SuspiciousVerificationOutcome string `mapstructure:"suspicious_verification_outcome"` - ExpiredDocumentOutcome string `mapstructure:"expired_document_outcome"` - ChallengeWindow int64 `mapstructure:"challenge_window"` - MinBalanceToVerifyAccount uint64 `mapstructure:"min_balance_to_verify_account"` + ChallengeWindow int64 `mapstructure:"challenge_window"` + MinBalanceToVerifyAccount uint64 `mapstructure:"min_balance_to_verify_account"` } type IdenfyConfig struct { @@ -31,37 +31,106 @@ type IdenfyConfig struct { APISecret string `mapstructure:"api_secret"` BaseURL string `mapstructure:"base_url"` CallbackSignKey string `mapstructure:"callback_sign_key"` - WhitelistedIPs []string `mapstructure:"whitelisted_ips"` + WhitelistedIPs []string `mapstructure:"whitelisted_ips,omitempty"` } type TFChainConfig struct { WsProviderURL string `mapstructure:"ws_provider_url"` } +type VerificationConfig struct { + SuspiciousVerificationOutcome string `mapstructure:"suspicious_verification_outcome"` + ExpiredDocumentOutcome string `mapstructure:"expired_document_outcome"` +} + func LoadConfig() (*Config, error) { - // Replace dots with underscores for nested keys - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + // replacer - // Make Viper read environment variables + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() + err := viper.BindEnv("mongo_uri") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("database_name") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("port") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("idenfy.api_key") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("idenfy.api_secret") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("idenfy.base_url") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("idenfy.callback_sign_key") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("idenfy.whitelisted_ips") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("tfchain.ws_provider_url") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("max_token_requests_per_minute") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("suspicious_verification_outcome") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("expired_document_outcome") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("challenge_window") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("min_balance_to_verify_account") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("verification.suspicious_verification_outcome") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("verification.expired_document_outcome") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } // Set default values - viper.SetDefault("port", "8080") - viper.SetDefault("max_token_requests_per_minute", 4) - viper.SetDefault("suspicious_verification_outcome", "verified") - viper.SetDefault("expired_document_outcome", "unverified") - viper.SetDefault("mongo_uri", "mongodb://localhost:27017") - viper.SetDefault("database_name", "tfgrid-kyc-db") - viper.SetDefault("idenfy.base_url", "https://ivs.idenfy.com/api/v2") - viper.SetDefault("tfchain.ws_provider_url", "wss://tfchain.grid.tf") - viper.SetDefault("min_balance_to_verify_account", 10000000) - viper.SetDefault("challenge_window", 120) + // viper.SetDefault("port", "8080") + // viper.SetDefault("max_token_requests_per_minute", 4) + // viper.SetDefault("suspicious_verification_outcome", "verified") + // viper.SetDefault("expired_document_outcome", "unverified") + // viper.SetDefault("mongo_uri", "mongodb://localhost:27017") + // viper.SetDefault("database_name", "tfgrid-kyc-db") + // viper.SetDefault("idenfy.base_url", "https://ivs.idenfy.com") + // viper.SetDefault("tfchain.ws_provider_url", "wss://tfchain.grid.tf") + // viper.SetDefault("min_balance_to_verify_account", 10000000) + // viper.SetDefault("challenge_window", 120) - config := &Config{} - err := viper.Unmarshal(config) + var config Config + err = viper.Unmarshal(&config) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to decode into struct: %w", err) } + fmt.Printf("%+v\n", config) - return config, nil + return &config, nil } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 145e3d5..35c776a 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -1,6 +1,8 @@ package handlers import ( + "fmt" + "github.com/gofiber/fiber/v2" "example.com/tfgrid-kyc-service/internal/services" @@ -23,11 +25,12 @@ func NewHandler(tokenService services.TokenService, verificationService services // @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 +// @Success 200 {object} responses.TokenResponseWithStatus // @Router /api/v1/token [post] func (h *Handler) GetorCreateVerificationToken() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") + // check if user account balance satisfies the minimum required balance, return an error if not hasRequiredBalance, err := h.tokenService.AccountHasRequiredBalance(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) @@ -35,11 +38,23 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { if !hasRequiredBalance { return c.Status(fiber.StatusPaymentRequired).JSON(fiber.Map{"error": "Account does not have the required balance"}) } - result, err := h.tokenService.GetToken(c.Context(), clientID) + // check if user is unverified, return an error if not + // this should be client responsibility to check if they are verified before requesting a new verification + isVerified, err := h.verificationService.IsUserVerified(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } - return c.JSON(fiber.Map{"result": result}) + if isVerified { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User already verified"}) + } + + fmt.Println("creating new token") + token, err := h.tokenService.GetorCreateVerificationToken(c.Context(), clientID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + fmt.Println("token from handler", token) + return c.JSON(fiber.Map{"result": token}) } } diff --git a/internal/models/models.go b/internal/models/models.go index 82467c2..2d291ec 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -40,6 +40,7 @@ type Token struct { AdditionalSteps interface{} `bson:"additionalSteps"` AdditionalData interface{} `bson:"additionalData"` CreatedAt time.Time `bson:"createdAt"` + ExpiresAt time.Time `bson:"expiresAt"` } type Verification struct { diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index 559f5bc..598648f 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -2,10 +2,12 @@ package repository import ( "context" + "fmt" "time" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" "example.com/tfgrid-kyc-service/internal/models" ) @@ -15,30 +17,61 @@ type MongoTokenRepository struct { } func NewMongoTokenRepository(db *mongo.Database) TokenRepository { - return &MongoTokenRepository{ + repo := &MongoTokenRepository{ collection: db.Collection("tokens"), } + repo.createTTLIndex() + return repo +} + +func (r *MongoTokenRepository) createTTLIndex() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + _, err := r.collection.Indexes().CreateOne( + ctx, + mongo.IndexModel{ + Keys: bson.D{{"expiresAt", 1}}, + Options: options.Index().SetExpireAfterSeconds(0), + }, + ) + + if err != nil { + fmt.Printf("Error creating TTL index: %v\n", 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) _, err := r.collection.InsertOne(ctx, token) + fmt.Println("token saved to db", err) return err } -func (r *MongoTokenRepository) GetToken(ctx context.Context, authToken string) (*models.Token, error) { +func (r *MongoTokenRepository) GetToken(ctx context.Context, clientID string) (*models.Token, error) { var token models.Token - err := r.collection.FindOne(ctx, bson.M{"authToken": authToken}).Decode(&token) + fmt.Println("clientID from repo", clientID) + err := r.collection.FindOne(ctx, bson.M{"clientId": clientID}).Decode(&token) if err != nil { if err == mongo.ErrNoDocuments { + fmt.Println("no document found") return nil, nil } return nil, err } + // calculate duration between createdAt and now then updae expiry time with remaining time + duration := time.Since(token.CreatedAt) + // protect against overflow + if duration >= time.Duration(token.ExpiryTime)*time.Second { + return nil, nil + } + remainingTime := time.Duration(token.ExpiryTime)*time.Second - duration + token.ExpiryTime = int(remainingTime.Seconds()) return &token, nil } -func (r *MongoTokenRepository) DeleteToken(ctx context.Context, authToken string) error { - _, err := r.collection.DeleteOne(ctx, bson.M{"authToken": authToken}) +func (r *MongoTokenRepository) DeleteToken(ctx context.Context, clientID string) error { + _, err := r.collection.DeleteOne(ctx, bson.M{"clientId": clientID}) return err } diff --git a/internal/responses/responses.go b/internal/responses/responses.go index c649567..c79706a 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -16,8 +16,13 @@ func SuccessResponse(c *fiber.Ctx, statusCode int, data interface{}, message str }) } +type TokenResponseWithStatus struct { + Token *TokenResponse `json:"token"` + IsNewToken bool `json:"is_new_token"` + Message string `json:"message"` +} + type TokenResponse struct { - Message string `json:"message"` AuthToken string `json:"authToken"` ScanRef string `json:"scanRef"` ClientID string `json:"clientId"` diff --git a/internal/server/server.go b/internal/server/server.go index 09c40d2..dfeef7c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -28,7 +28,7 @@ type Server struct { } func New(config *configs.Config) *Server { - + // debug log app := fiber.New() // Global middlewares @@ -50,12 +50,17 @@ func New(config *configs.Config) *Server { // Initialize services idenfyClient := idenfy.New(config.Idenfy) + + if err != nil { + log.Fatalf("Failed to initialize idenfy client: %v", err) + } + substrateClient, err := substrate.New(config.TFChain) if err != nil { log.Fatalf("Failed to initialize substrate client: %v", err) } tokenService := services.NewTokenService(tokenRepo, idenfyClient, substrateClient, config.MinBalanceToVerifyAccount) - verificationService := services.NewVerificationService(verificationRepo) + verificationService := services.NewVerificationService(verificationRepo, idenfyClient, &config.Verification) // Initialize handler handler := handlers.NewHandler(tokenService, verificationService) diff --git a/internal/services/service.go b/internal/services/service.go index ef93111..a98ddd1 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -7,8 +7,7 @@ import ( ) type TokenService interface { - CreateToken(ctx context.Context, clientID string) (*responses.TokenResponse, error) - GetToken(ctx context.Context, clientID string) (*responses.TokenResponse, error) + GetorCreateVerificationToken(ctx context.Context, clientID string) (*responses.TokenResponseWithStatus, error) DeleteToken(ctx context.Context, clientID string) error AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) } @@ -16,4 +15,7 @@ type TokenService interface { type VerificationService interface { GetVerificationData(ctx context.Context, clientID string) (*responses.VerificationDataResponse, error) GetVerificationStatus(ctx context.Context, clientID string) (*responses.VerificationStatusResponse, error) + ProcessVerificationResult(ctx context.Context, clientID string) error + ProcessDocExpirationNotification(ctx context.Context, clientID string) error + IsUserVerified(ctx context.Context, clientID string) (bool, error) } diff --git a/internal/services/token_service.go b/internal/services/token_service.go index a25d71a..7feed49 100644 --- a/internal/services/token_service.go +++ b/internal/services/token_service.go @@ -2,6 +2,7 @@ package services import ( "context" + "fmt" "example.com/tfgrid-kyc-service/internal/clients/idenfy" "example.com/tfgrid-kyc-service/internal/clients/substrate" @@ -16,19 +17,59 @@ type tokenService struct { requiredBalance uint64 } -func NewTokenService(repo repository.TokenRepository, idenfy *idenfy.Idenfy, substrate *substrate.Substrate, requiredBalance uint64) TokenService { - return &tokenService{repo: repo, idenfy: idenfy, substrate: substrate, requiredBalance: requiredBalance} +func NewTokenService(repo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, requiredBalance uint64) TokenService { + return &tokenService{repo: repo, idenfy: idenfy, substrate: substrateClient, requiredBalance: requiredBalance} } -func (s *tokenService) CreateToken(ctx context.Context, clientID string) (*responses.TokenResponse, error) { - token := &responses.TokenResponse{} - - return token, nil -} - -func (s *tokenService) GetToken(ctx context.Context, clientID string) (*responses.TokenResponse, error) { - token := &responses.TokenResponse{} - return token, nil +func (s *tokenService) GetorCreateVerificationToken(ctx context.Context, clientID string) (*responses.TokenResponseWithStatus, error) { + token, err := s.repo.GetToken(ctx, clientID) + if err != nil { + return nil, err + } + // check if token is not nil and not expired or near expiry (2 min) + if token != nil { //&& time.Since(token.CreatedAt)+2*time.Minute < time.Duration(token.ExpiryTime)*time.Second { + tokenResponse := &responses.TokenResponse{ + AuthToken: token.AuthToken, + ClientID: token.ClientID, + ScanRef: token.ScanRef, + ExpiryTime: token.ExpiryTime, + SessionLength: token.SessionLength, + DigitString: token.DigitString, + TokenType: token.TokenType, + } + tokenResponseWithStatus := &responses.TokenResponseWithStatus{ + Token: tokenResponse, + IsNewToken: false, + Message: "Existing valid token retrieved.", + } + fmt.Println("token from db", token) + return tokenResponseWithStatus, nil + } + fmt.Println("token is nil or expired") + newToken, err := s.idenfy.CreateVerificationSession(ctx, clientID) + if err != nil { + return nil, err + } + fmt.Println("new token", newToken) + err = s.repo.SaveToken(ctx, &newToken) + if err != nil { + fmt.Println("warning: was not able to save verification token to db", err) + } + tokenResponse := &responses.TokenResponse{ + AuthToken: newToken.AuthToken, + ClientID: newToken.ClientID, + ScanRef: newToken.ScanRef, + ExpiryTime: newToken.ExpiryTime, + SessionLength: newToken.SessionLength, + DigitString: newToken.DigitString, + TokenType: newToken.TokenType, + } + tokenResponseWithStatus := &responses.TokenResponseWithStatus{ + Token: tokenResponse, + IsNewToken: true, + Message: "New token created", + } + return tokenResponseWithStatus, nil } func (s *tokenService) DeleteToken(ctx context.Context, clientID string) error { diff --git a/internal/services/verification_service.go b/internal/services/verification_service.go index b8aaed0..f9c007e 100644 --- a/internal/services/verification_service.go +++ b/internal/services/verification_service.go @@ -3,16 +3,20 @@ package services import ( "context" + "example.com/tfgrid-kyc-service/internal/clients/idenfy" + "example.com/tfgrid-kyc-service/internal/configs" "example.com/tfgrid-kyc-service/internal/repository" "example.com/tfgrid-kyc-service/internal/responses" ) type verificationService struct { - repo repository.VerificationRepository + repo repository.VerificationRepository + idenfy *idenfy.Idenfy + config *configs.VerificationConfig } -func NewVerificationService(repo repository.VerificationRepository) VerificationService { - return &verificationService{repo: repo} +func NewVerificationService(repo repository.VerificationRepository, idenfyClient *idenfy.Idenfy, config *configs.VerificationConfig) VerificationService { + return &verificationService{repo: repo, idenfy: idenfyClient, config: config} } func (s *verificationService) GetVerificationData(ctx context.Context, clientID string) (*responses.VerificationDataResponse, error) { @@ -22,6 +26,43 @@ func (s *verificationService) GetVerificationData(ctx context.Context, clientID } func (s *verificationService) GetVerificationStatus(ctx context.Context, clientID string) (*responses.VerificationStatusResponse, error) { - verificationStatus := &responses.VerificationStatusResponse{} + verification, err := s.repo.GetVerification(ctx, clientID) + if err != nil { + return nil, err + } + if verification == nil { + return nil, nil + } + verificationStatus := &responses.VerificationStatusResponse{ + FraudTags: verification.Status.FraudTags, + MismatchTags: verification.Status.MismatchTags, + AutoDocument: verification.Status.AutoDocument, + ManualDocument: verification.Status.ManualDocument, + AutoFace: verification.Status.AutoFace, + ManualFace: verification.Status.ManualFace, + ScanRef: verification.ScanRef, + ClientID: verification.ClientID, + Status: string(verification.Status.Overall), + } + return verificationStatus, nil } + +func (s *verificationService) ProcessVerificationResult(ctx context.Context, clientID string) error { + return nil +} + +func (s *verificationService) ProcessDocExpirationNotification(ctx context.Context, clientID string) error { + return nil +} + +func (s *verificationService) IsUserVerified(ctx context.Context, clientID string) (bool, error) { + verification, err := s.repo.GetVerification(ctx, clientID) + if err != nil { + return false, err + } + if verification == nil { + return false, nil + } + return verification.Status.Overall == "APPROVED" || (s.config.SuspiciousVerificationOutcome == "APPROVED" && verification.Status.Overall == "SUSPECTED"), nil +} From 5400baaa533faecf5b21ec96560a1403092d184d Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 17 Oct 2024 14:48:37 +0300 Subject: [PATCH 005/105] adjust limiter config and on-disk persistence store --- go.mod | 4 +-- go.sum | 6 ++-- internal/server/server.go | 45 ++++++++++++++++++++++++++++-- internal/services/token_service.go | 3 ++ 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 3ae4c4b..a085d63 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.22.1 require ( github.com/centrifuge/go-substrate-rpc-client/v4 v4.2.1 - github.com/gboddin/go-idenfy v1.0.1 github.com/gofiber/fiber/v2 v2.52.5 + github.com/gofiber/storage/mongodb v1.3.9 github.com/gofiber/swagger v1.1.0 github.com/spf13/viper v1.19.0 github.com/swaggo/swag v1.16.3 + github.com/valyala/fasthttp v1.51.0 github.com/vedhavyas/go-subkey/v2 v2.0.0 go.mongodb.org/mongo-driver v1.17.1 ) @@ -61,7 +62,6 @@ require ( github.com/swaggo/files/v2 v2.0.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect diff --git a/go.sum b/go.sum index 39e3d5f..8b50f25 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gboddin/go-idenfy v1.0.1 h1:7ggqkSaOY6XaZX/lBkdoHUmNnqQiPyqAFabmGqPANAM= -github.com/gboddin/go-idenfy v1.0.1/go.mod h1:vtPk7bX+AADEohnyygs65JwmTYWpApOUbhTWw9nw8zc= github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -55,8 +53,12 @@ github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gofiber/storage/mongodb v1.3.9 h1:uoFHBuGLWjlNsYFsMZkGxfzpryIq64mfGAUox8WRLkc= +github.com/gofiber/storage/mongodb v1.3.9/go.mod h1:HMl4mg6iUWovowiY8SexUnsyeJwZtihB1rlNjLlpIKA= github.com/gofiber/swagger v1.1.0 h1:ff3rg1fB+Rp5JN/N8jfxTiZtMKe/9tB9QDc79fPiJKQ= github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8= +github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= +github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= diff --git a/internal/server/server.go b/internal/server/server.go index dfeef7c..82f7440 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -5,6 +5,7 @@ import ( "os" "os/signal" "syscall" + "time" _ "example.com/tfgrid-kyc-service/api/docs" "example.com/tfgrid-kyc-service/internal/clients/idenfy" @@ -18,6 +19,7 @@ import ( "github.com/gofiber/fiber/v2/middleware/helmet" "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/gofiber/storage/mongodb" "github.com/gofiber/swagger" ) @@ -31,6 +33,43 @@ func New(config *configs.Config) *Server { // debug log app := fiber.New() + // Setup Limter Config and store + ipLimiterstore := mongodb.New(mongodb.Config{ + ConnectionURI: config.MongoURI, + Database: config.DatabaseName, + Collection: "ip_limit", + Reset: false, + }) + ipLimiterConfig := limiter.Config{ // TODO: use configurable parameters + Max: 3, + Expiration: 24 * time.Hour, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + Store: ipLimiterstore, + // skip the limiter for localhost + Next: func(c *fiber.Ctx) bool { + return c.IP() == "127.0.0.1" + }, + } + clientLimiterStore := mongodb.New(mongodb.Config{ + ConnectionURI: config.MongoURI, + Database: config.DatabaseName, + Collection: "client_limit", + Reset: false, + }) + + clientLimiterConfig := limiter.Config{ // TODO: use configurable parameters + Max: 10, + Expiration: 24 * time.Hour, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + Store: clientLimiterStore, + // Use client id as key to limit the number of requests per client + KeyGenerator: func(c *fiber.Ctx) string { + return c.Get("X-Client-ID") + }, + } + // Global middlewares app.Use(middleware.Logger()) app.Use(middleware.CORS()) @@ -68,13 +107,13 @@ func New(config *configs.Config) *Server { // Routes app.Get("/docs/*", swagger.HandlerDefault) - v1 := app.Group("/api/v1", limiter.New(), middleware.AuthMiddleware(config.ChallengeWindow)) - v1.Post("/token", handler.GetorCreateVerificationToken()) + v1 := app.Group("/api/v1", limiter.New(ipLimiterConfig), middleware.AuthMiddleware(config.ChallengeWindow)) + v1.Post("/token", limiter.New(clientLimiterConfig), handler.GetorCreateVerificationToken()) v1.Get("/data", handler.GetVerificationData()) v1.Get("/status", handler.GetVerificationStatus()) // Webhook routes - webhooks := app.Group("/webhooks/idenfy") // TODO: middleware to verify hmac signature of the webhook, only accept from whitelisted ip addresses + webhooks := app.Group("/webhooks/idenfy") webhooks.Post("/verification-update", handler.ProcessVerificationResult()) webhooks.Post("/id-expiration", handler.ProcessDocExpirationNotification()) diff --git a/internal/services/token_service.go b/internal/services/token_service.go index 7feed49..c6ed8b9 100644 --- a/internal/services/token_service.go +++ b/internal/services/token_service.go @@ -77,6 +77,9 @@ func (s *tokenService) DeleteToken(ctx context.Context, clientID string) error { } func (s *tokenService) AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) { + if s.requiredBalance == 0 { + return true, nil + } balance, err := s.substrate.GetAccountBalance(address) if err != nil { return false, err From 9cd973a77ee838fc257c87830c0aca3bbe065f97 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 17 Oct 2024 18:14:21 +0300 Subject: [PATCH 006/105] clean handler layer --- internal/handlers/handlers.go | 44 ++++------ internal/models/token.go | 44 ++++++++++ .../models/{models.go => verification.go} | 37 --------- internal/responses/responses.go | 82 ++++++++++++++++++- internal/server/server.go | 3 +- internal/services/coordinator_service.go | 34 ++++++++ internal/services/service.go | 21 ----- internal/services/services.go | 27 ++++++ internal/services/token_service.go | 51 ++++-------- internal/services/verification_service.go | 29 +------ 10 files changed, 224 insertions(+), 148 deletions(-) create mode 100644 internal/models/token.go rename internal/models/{models.go => verification.go} (78%) create mode 100644 internal/services/coordinator_service.go delete mode 100644 internal/services/service.go create mode 100644 internal/services/services.go diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 35c776a..8ae320c 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -5,16 +5,18 @@ import ( "github.com/gofiber/fiber/v2" + "example.com/tfgrid-kyc-service/internal/responses" "example.com/tfgrid-kyc-service/internal/services" ) type Handler struct { tokenService services.TokenService verificationService services.VerificationService + coordinatorService services.CoordinatorService } -func NewHandler(tokenService services.TokenService, verificationService services.VerificationService) *Handler { - return &Handler{tokenService: tokenService, verificationService: verificationService} +func NewHandler(tokenService services.TokenService, verificationService services.VerificationService, coordinatorService services.CoordinatorService) *Handler { + return &Handler{tokenService: tokenService, verificationService: verificationService, coordinatorService: coordinatorService} } // @Summary Get or Generate iDenfy Verification Token @@ -30,31 +32,15 @@ func NewHandler(tokenService services.TokenService, verificationService services func (h *Handler) GetorCreateVerificationToken() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") - // check if user account balance satisfies the minimum required balance, return an error if not - hasRequiredBalance, err := h.tokenService.AccountHasRequiredBalance(c.Context(), clientID) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) - } - if !hasRequiredBalance { - return c.Status(fiber.StatusPaymentRequired).JSON(fiber.Map{"error": "Account does not have the required balance"}) - } - // check if user is unverified, return an error if not - // this should be client responsibility to check if they are verified before requesting a new verification - isVerified, err := h.verificationService.IsUserVerified(c.Context(), clientID) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) - } - if isVerified { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User already verified"}) - } - fmt.Println("creating new token") - token, err := h.tokenService.GetorCreateVerificationToken(c.Context(), clientID) + token, isNewToken, err := h.tokenService.GetorCreateVerificationToken(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } + response := responses.NewTokenResponseWithStatus(token, isNewToken) + fmt.Println("token from handler", token) - return c.JSON(fiber.Map{"result": token}) + return c.JSON(fiber.Map{"result": response}) } } @@ -71,14 +57,15 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { func (h *Handler) GetVerificationData() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Query("clientID") - result, err := h.verificationService.GetVerificationData(c.Context(), clientID) + verification, err := h.verificationService.GetVerification(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } - if result == nil { + if verification == nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Verification not found"}) } - return c.JSON(fiber.Map{"result": result}) + response := responses.NewVerificationDataResponse(verification) + return c.JSON(fiber.Map{"result": response}) } } @@ -95,14 +82,15 @@ func (h *Handler) GetVerificationData() fiber.Handler { func (h *Handler) GetVerificationStatus() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Query("clientID") - result, err := h.verificationService.GetVerificationStatus(c.Context(), clientID) + verification, err := h.verificationService.GetVerification(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } - if result == nil { + if verification == nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Verification not found"}) } - return c.JSON(fiber.Map{"result": result}) + response := responses.NewVerificationStatusResponse(verification) + return c.JSON(fiber.Map{"result": response}) } } diff --git a/internal/models/token.go b/internal/models/token.go new file mode 100644 index 0000000..7a2b393 --- /dev/null +++ b/internal/models/token.go @@ -0,0 +1,44 @@ +package models + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Token struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + AuthToken string `bson:"authToken"` + ScanRef string `bson:"scanRef"` + ClientID string `bson:"clientId"` + FirstName string `bson:"firstName"` + LastName string `bson:"lastName"` + SuccessURL string `bson:"successUrl"` + ErrorURL string `bson:"errorUrl"` + UnverifiedURL string `bson:"unverifiedUrl"` + CallbackURL string `bson:"callbackUrl"` + Locale string `bson:"locale"` + ShowInstructions bool `bson:"showInstructions"` + Country string `bson:"country"` + ExpiryTime int `bson:"expiryTime"` + SessionLength int `bson:"sessionLength"` + Documents []string `bson:"documents"` + AllowedDocuments map[string][]string `bson:"allowedDocuments"` + DateOfBirth string `bson:"dateOfBirth"` + DateOfExpiry string `bson:"dateOfExpiry"` + DateOfIssue string `bson:"dateOfIssue"` + Nationality string `bson:"nationality"` + PersonalNumber string `bson:"personalNumber"` + DocumentNumber string `bson:"documentNumber"` + Sex string `bson:"sex"` + DigitString string `bson:"digitString"` + Address string `bson:"address"` + TokenType string `bson:"tokenType"` + ExternalRef string `bson:"externalRef"` + Questionnaire interface{} `bson:"questionnaire"` + UtilityBill bool `bson:"utilityBill"` + AdditionalSteps interface{} `bson:"additionalSteps"` + AdditionalData interface{} `bson:"additionalData"` + CreatedAt time.Time `bson:"createdAt"` + ExpiresAt time.Time `bson:"expiresAt"` +} diff --git a/internal/models/models.go b/internal/models/verification.go similarity index 78% rename from internal/models/models.go rename to internal/models/verification.go index 2d291ec..a3cb586 100644 --- a/internal/models/models.go +++ b/internal/models/verification.go @@ -6,43 +6,6 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) -type Token struct { - ID primitive.ObjectID `bson:"_id,omitempty"` - AuthToken string `bson:"authToken"` - ScanRef string `bson:"scanRef"` - ClientID string `bson:"clientId"` - FirstName string `bson:"firstName"` - LastName string `bson:"lastName"` - SuccessURL string `bson:"successUrl"` - ErrorURL string `bson:"errorUrl"` - UnverifiedURL string `bson:"unverifiedUrl"` - CallbackURL string `bson:"callbackUrl"` - Locale string `bson:"locale"` - ShowInstructions bool `bson:"showInstructions"` - Country string `bson:"country"` - ExpiryTime int `bson:"expiryTime"` - SessionLength int `bson:"sessionLength"` - Documents []string `bson:"documents"` - AllowedDocuments map[string][]string `bson:"allowedDocuments"` - DateOfBirth string `bson:"dateOfBirth"` - DateOfExpiry string `bson:"dateOfExpiry"` - DateOfIssue string `bson:"dateOfIssue"` - Nationality string `bson:"nationality"` - PersonalNumber string `bson:"personalNumber"` - DocumentNumber string `bson:"documentNumber"` - Sex string `bson:"sex"` - DigitString string `bson:"digitString"` - Address string `bson:"address"` - TokenType string `bson:"tokenType"` - ExternalRef string `bson:"externalRef"` - Questionnaire interface{} `bson:"questionnaire"` - UtilityBill bool `bson:"utilityBill"` - AdditionalSteps interface{} `bson:"additionalSteps"` - AdditionalData interface{} `bson:"additionalData"` - CreatedAt time.Time `bson:"createdAt"` - ExpiresAt time.Time `bson:"expiresAt"` -} - type Verification struct { ID primitive.ObjectID `bson:"_id,omitempty"` Final *bool `bson:"final"` diff --git a/internal/responses/responses.go b/internal/responses/responses.go index c79706a..8f8fce9 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -1,6 +1,9 @@ package responses -import "github.com/gofiber/fiber/v2" +import ( + "example.com/tfgrid-kyc-service/internal/models" + "github.com/gofiber/fiber/v2" +) type Response struct { Success bool `json:"success"` @@ -84,3 +87,80 @@ type VerificationDataResponse struct { ScanRef string `json:"scanRef"` ClientID string `json:"clientId"` } + +// implement from() method for TokenResponseWithStatus +func NewTokenResponseWithStatus(token *models.Token, isNewToken bool) *TokenResponseWithStatus { + message := "Existing valid token retrieved." + if isNewToken { + message = "New token created." + } + return &TokenResponseWithStatus{ + Token: &TokenResponse{ + AuthToken: token.AuthToken, + ScanRef: token.ScanRef, + ClientID: token.ClientID, + ExpiryTime: token.ExpiryTime, + SessionLength: token.SessionLength, + DigitString: token.DigitString, + TokenType: token.TokenType, + }, + IsNewToken: isNewToken, + Message: message, + } +} + +func NewVerificationStatusResponse(verification *models.Verification) *VerificationStatusResponse { + return &VerificationStatusResponse{ + FraudTags: verification.Status.FraudTags, + MismatchTags: verification.Status.MismatchTags, + AutoDocument: verification.Status.AutoDocument, + ManualDocument: verification.Status.ManualDocument, + AutoFace: verification.Status.AutoFace, + ManualFace: verification.Status.ManualFace, + ScanRef: verification.ScanRef, + ClientID: verification.ClientID, + Status: string(verification.Status.Overall), + } +} + +func NewVerificationDataResponse(verification *models.Verification) *VerificationDataResponse { + return &VerificationDataResponse{ + DocFirstName: verification.Data.DocFirstName, + DocLastName: verification.Data.DocLastName, + DocNumber: verification.Data.DocNumber, + DocPersonalCode: verification.Data.DocPersonalCode, + DocExpiry: verification.Data.DocExpiry, + DocDob: verification.Data.DocDOB, + DocDateOfIssue: verification.Data.DocDateOfIssue, + DocType: string(verification.Data.DocType), + DocSex: string(verification.Data.DocSex), + DocNationality: verification.Data.DocNationality, + DocIssuingCountry: verification.Data.DocIssuingCountry, + DocTemporaryAddress: verification.Data.DocTemporaryAddress, + DocBirthName: verification.Data.DocBirthName, + BirthPlace: verification.Data.BirthPlace, + Authority: verification.Data.Authority, + MotherMaidenName: verification.Data.MothersMaidenName, + DriverLicenseCategory: verification.Data.DriverLicenseCategory, + ManuallyDataChanged: verification.Data.ManuallyDataChanged, + FullName: verification.Data.FullName, + OrgFirstName: verification.Data.OrgFirstName, + OrgLastName: verification.Data.OrgLastName, + OrgNationality: verification.Data.OrgNationality, + OrgBirthPlace: verification.Data.OrgBirthPlace, + OrgAuthority: verification.Data.OrgAuthority, + OrgAddress: verification.Data.OrgAddress, + OrgTemporaryAddress: verification.Data.OrgTemporaryAddress, + OrgMothersMaidenName: verification.Data.OrgMothersMaidenName, + OrgBirthName: verification.Data.OrgBirthName, + SelectedCountry: verification.Data.SelectedCountry, + AgeEstimate: string(verification.Data.AgeEstimate), + ClientIpProxyRiskLevel: verification.Data.ClientIPProxyRiskLevel, + DuplicateFaces: verification.Data.DuplicateFaces, + DuplicateDocFaces: verification.Data.DuplicateDocFaces, + AddressVerification: verification.AddressVerification, + AdditionalData: verification.Data.AdditionalData, + ScanRef: verification.ScanRef, + ClientID: verification.ClientID, + } +} diff --git a/internal/server/server.go b/internal/server/server.go index 82f7440..128e7d0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -100,9 +100,10 @@ func New(config *configs.Config) *Server { } tokenService := services.NewTokenService(tokenRepo, idenfyClient, substrateClient, config.MinBalanceToVerifyAccount) verificationService := services.NewVerificationService(verificationRepo, idenfyClient, &config.Verification) + coordinatorService := services.NewCoordinatorService(tokenService, verificationService) // Initialize handler - handler := handlers.NewHandler(tokenService, verificationService) + handler := handlers.NewHandler(tokenService, verificationService, coordinatorService) // Routes app.Get("/docs/*", swagger.HandlerDefault) diff --git a/internal/services/coordinator_service.go b/internal/services/coordinator_service.go new file mode 100644 index 0000000..ab13947 --- /dev/null +++ b/internal/services/coordinator_service.go @@ -0,0 +1,34 @@ +package services + +import ( + "context" + "errors" + + "example.com/tfgrid-kyc-service/internal/models" +) + +type coordinatorService struct { + tokenService TokenService + verificationService VerificationService +} + +func NewCoordinatorService(tokenService TokenService, verificationService VerificationService) CoordinatorService { + return &coordinatorService{tokenService: tokenService, verificationService: verificationService} +} + +func (s *coordinatorService) GetorCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) { + // check if user is unverified, return an error if not + // this should be client responsibility to check if they are verified before requesting a new verification + isVerified, err := s.verificationService.IsUserVerified(ctx, clientID) + if err != nil { + return nil, false, err + } + if isVerified { + return nil, false, errors.New("user already verified") // TODO: implement a custom error that can be converted in the handler to a 400 status code + } + token, isNew, err := s.tokenService.GetorCreateVerificationToken(ctx, clientID) + if err != nil { + return nil, false, err + } + return token, isNew, nil +} diff --git a/internal/services/service.go b/internal/services/service.go deleted file mode 100644 index a98ddd1..0000000 --- a/internal/services/service.go +++ /dev/null @@ -1,21 +0,0 @@ -package services - -import ( - "context" - - "example.com/tfgrid-kyc-service/internal/responses" -) - -type TokenService interface { - GetorCreateVerificationToken(ctx context.Context, clientID string) (*responses.TokenResponseWithStatus, error) - DeleteToken(ctx context.Context, clientID string) error - AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) -} - -type VerificationService interface { - GetVerificationData(ctx context.Context, clientID string) (*responses.VerificationDataResponse, error) - GetVerificationStatus(ctx context.Context, clientID string) (*responses.VerificationStatusResponse, error) - ProcessVerificationResult(ctx context.Context, clientID string) 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 new file mode 100644 index 0000000..59517e3 --- /dev/null +++ b/internal/services/services.go @@ -0,0 +1,27 @@ +package services + +import ( + "context" + + "example.com/tfgrid-kyc-service/internal/models" +) + +type TokenService interface { + GetorCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) + DeleteToken(ctx context.Context, clientID string) error + AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) +} + +type VerificationService interface { + GetVerification(ctx context.Context, clientID string) (*models.Verification, error) + ProcessVerificationResult(ctx context.Context, clientID string) error + ProcessDocExpirationNotification(ctx context.Context, clientID string) error + IsUserVerified(ctx context.Context, clientID string) (bool, error) +} + +// The existing services (TokenService and VerificationService) already encapsulate most of the logic. +// This coordinator service would orchestrate operations between these services, +// encapsulating the logic that spans over them while keeping them focused on their specific domains. +type CoordinatorService interface { + GetorCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) +} diff --git a/internal/services/token_service.go b/internal/services/token_service.go index c6ed8b9..967f8fc 100644 --- a/internal/services/token_service.go +++ b/internal/services/token_service.go @@ -2,12 +2,13 @@ package services import ( "context" + "errors" "fmt" "example.com/tfgrid-kyc-service/internal/clients/idenfy" "example.com/tfgrid-kyc-service/internal/clients/substrate" + "example.com/tfgrid-kyc-service/internal/models" "example.com/tfgrid-kyc-service/internal/repository" - "example.com/tfgrid-kyc-service/internal/responses" ) type tokenService struct { @@ -21,55 +22,35 @@ func NewTokenService(repo repository.TokenRepository, idenfy *idenfy.Idenfy, sub return &tokenService{repo: repo, idenfy: idenfy, substrate: substrateClient, requiredBalance: requiredBalance} } -func (s *tokenService) GetorCreateVerificationToken(ctx context.Context, clientID string) (*responses.TokenResponseWithStatus, error) { +func (s *tokenService) GetorCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) { token, err := s.repo.GetToken(ctx, clientID) if err != nil { - return nil, err + return nil, false, err } // check if token is not nil and not expired or near expiry (2 min) if token != nil { //&& time.Since(token.CreatedAt)+2*time.Minute < time.Duration(token.ExpiryTime)*time.Second { - tokenResponse := &responses.TokenResponse{ - AuthToken: token.AuthToken, - ClientID: token.ClientID, - ScanRef: token.ScanRef, - ExpiryTime: token.ExpiryTime, - SessionLength: token.SessionLength, - DigitString: token.DigitString, - TokenType: token.TokenType, - } - tokenResponseWithStatus := &responses.TokenResponseWithStatus{ - Token: tokenResponse, - IsNewToken: false, - Message: "Existing valid token retrieved.", - } - fmt.Println("token from db", token) - return tokenResponseWithStatus, nil + return token, false, nil } fmt.Println("token is nil or expired") + // check if user account balance satisfies the minimum required balance, return an error if not + hasRequiredBalance, err := s.AccountHasRequiredBalance(ctx, clientID) + if err != nil { + return nil, false, err // todo: implement a custom error that can be converted in the handler to a 500 status code + } + if !hasRequiredBalance { + return nil, false, errors.New("account does not have the required balance") // todo: implement a custom error that can be converted in the handler to a 402 status code + } newToken, err := s.idenfy.CreateVerificationSession(ctx, clientID) if err != nil { - return nil, err + return nil, false, err } fmt.Println("new token", newToken) err = s.repo.SaveToken(ctx, &newToken) if err != nil { fmt.Println("warning: was not able to save verification token to db", err) } - tokenResponse := &responses.TokenResponse{ - AuthToken: newToken.AuthToken, - ClientID: newToken.ClientID, - ScanRef: newToken.ScanRef, - ExpiryTime: newToken.ExpiryTime, - SessionLength: newToken.SessionLength, - DigitString: newToken.DigitString, - TokenType: newToken.TokenType, - } - tokenResponseWithStatus := &responses.TokenResponseWithStatus{ - Token: tokenResponse, - IsNewToken: true, - Message: "New token created", - } - return tokenResponseWithStatus, nil + + return &newToken, true, nil } func (s *tokenService) DeleteToken(ctx context.Context, clientID string) error { diff --git a/internal/services/verification_service.go b/internal/services/verification_service.go index f9c007e..ea31c52 100644 --- a/internal/services/verification_service.go +++ b/internal/services/verification_service.go @@ -5,8 +5,8 @@ import ( "example.com/tfgrid-kyc-service/internal/clients/idenfy" "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/models" "example.com/tfgrid-kyc-service/internal/repository" - "example.com/tfgrid-kyc-service/internal/responses" ) type verificationService struct { @@ -19,33 +19,12 @@ func NewVerificationService(repo repository.VerificationRepository, idenfyClient return &verificationService{repo: repo, idenfy: idenfyClient, config: config} } -func (s *verificationService) GetVerificationData(ctx context.Context, clientID string) (*responses.VerificationDataResponse, error) { - // build responses.VerificationDataResponse from models.Verification - verificationData := &responses.VerificationDataResponse{} - return verificationData, nil -} - -func (s *verificationService) GetVerificationStatus(ctx context.Context, clientID string) (*responses.VerificationStatusResponse, error) { +func (s *verificationService) GetVerification(ctx context.Context, clientID string) (*models.Verification, error) { verification, err := s.repo.GetVerification(ctx, clientID) if err != nil { return nil, err } - if verification == nil { - return nil, nil - } - verificationStatus := &responses.VerificationStatusResponse{ - FraudTags: verification.Status.FraudTags, - MismatchTags: verification.Status.MismatchTags, - AutoDocument: verification.Status.AutoDocument, - ManualDocument: verification.Status.ManualDocument, - AutoFace: verification.Status.AutoFace, - ManualFace: verification.Status.ManualFace, - ScanRef: verification.ScanRef, - ClientID: verification.ClientID, - Status: string(verification.Status.Overall), - } - - return verificationStatus, nil + return verification, nil } func (s *verificationService) ProcessVerificationResult(ctx context.Context, clientID string) error { @@ -57,7 +36,7 @@ func (s *verificationService) ProcessDocExpirationNotification(ctx context.Conte } func (s *verificationService) IsUserVerified(ctx context.Context, clientID string) (bool, error) { - verification, err := s.repo.GetVerification(ctx, clientID) + verification, err := s.GetVerification(ctx, clientID) if err != nil { return false, err } From 84d10773e416d77e6a9b6196480c548afa2efd7b Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 17 Oct 2024 19:49:31 +0300 Subject: [PATCH 007/105] remove isNew bool field in the token response --- api/docs/docs.go | 27 +++++++++++---------------- api/docs/swagger.json | 27 +++++++++++---------------- api/docs/swagger.yaml | 19 ++++++++----------- docker-compose.yml | 10 +++++----- internal/clients/idenfy/idenfy.go | 2 +- internal/handlers/handlers.go | 12 ++++++------ internal/responses/responses.go | 30 +++++++++++------------------- 7 files changed, 53 insertions(+), 74 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 3fe3aa8..90413f9 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -162,9 +162,15 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Existing token retrieved", "schema": { - "$ref": "#/definitions/responses.TokenResponseWithStatus" + "$ref": "#/definitions/responses.TokenResponse" + } + }, + "201": { + "description": "New token created", + "schema": { + "$ref": "#/definitions/responses.TokenResponse" } } } @@ -227,6 +233,9 @@ const docTemplate = `{ "expiryTime": { "type": "integer" }, + "message": { + "type": "string" + }, "scanRef": { "type": "string" }, @@ -238,20 +247,6 @@ const docTemplate = `{ } } }, - "responses.TokenResponseWithStatus": { - "type": "object", - "properties": { - "is_new_token": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "token": { - "$ref": "#/definitions/responses.TokenResponse" - } - } - }, "responses.VerificationDataResponse": { "type": "object", "properties": { diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 12847b0..6b39eb0 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -155,9 +155,15 @@ ], "responses": { "200": { - "description": "OK", + "description": "Existing token retrieved", "schema": { - "$ref": "#/definitions/responses.TokenResponseWithStatus" + "$ref": "#/definitions/responses.TokenResponse" + } + }, + "201": { + "description": "New token created", + "schema": { + "$ref": "#/definitions/responses.TokenResponse" } } } @@ -220,6 +226,9 @@ "expiryTime": { "type": "integer" }, + "message": { + "type": "string" + }, "scanRef": { "type": "string" }, @@ -231,20 +240,6 @@ } } }, - "responses.TokenResponseWithStatus": { - "type": "object", - "properties": { - "is_new_token": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "token": { - "$ref": "#/definitions/responses.TokenResponse" - } - } - }, "responses.VerificationDataResponse": { "type": "object", "properties": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index f267d98..1cdf35e 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -10,6 +10,8 @@ definitions: type: string expiryTime: type: integer + message: + type: string scanRef: type: string sessionLength: @@ -17,15 +19,6 @@ definitions: tokenType: type: string type: object - responses.TokenResponseWithStatus: - properties: - is_new_token: - type: boolean - message: - type: string - token: - $ref: '#/definitions/responses.TokenResponse' - type: object responses.VerificationDataResponse: properties: additionalData: {} @@ -241,9 +234,13 @@ paths: - application/json responses: "200": - description: OK + description: Existing token retrieved + schema: + $ref: '#/definitions/responses.TokenResponse' + "201": + description: New token created schema: - $ref: '#/definitions/responses.TokenResponseWithStatus' + $ref: '#/definitions/responses.TokenResponse' summary: Get or Generate iDenfy Verification Token tags: - Token diff --git a/docker-compose.yml b/docker-compose.yml index 7e140cc..bba1b52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,13 +10,13 @@ services: ports: - "8080:8080" depends_on: - mongodb: + mongo: condition: service_healthy env_file: - .app.env - mongodb: + mongo: image: mongo:latest container_name: tf_kyc_db ports: @@ -30,16 +30,16 @@ services: interval: 10s timeout: 10s retries: 5 - start_period: 40s + start_period: 10s mongo-express: image: mongo-express:latest container_name: mongo_express environment: - - ME_CONFIG_MONGODB_SERVER=mongodb + - ME_CONFIG_MONGODB_SERVER=mongo - ME_CONFIG_MONGODB_PORT=27017 depends_on: - - mongodb + - mongo ports: - "8888:8081" env_file: diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index 9271729..722bf56 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -33,7 +33,7 @@ func New(config configs.IdenfyConfig) *Idenfy { } } -func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) (models.Token, error) { +func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) (models.Token, error) { // TODO: Refactor url := c.baseURL + VerificationSessionEndpoint req := fasthttp.AcquireRequest() diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 8ae320c..d5a7ac2 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -1,8 +1,6 @@ package handlers import ( - "fmt" - "github.com/gofiber/fiber/v2" "example.com/tfgrid-kyc-service/internal/responses" @@ -27,7 +25,8 @@ func NewHandler(tokenService services.TokenService, verificationService services // @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.TokenResponseWithStatus +// @Success 200 {object} responses.TokenResponse "Existing token retrieved" +// @Success 201 {object} responses.TokenResponse "New token created" // @Router /api/v1/token [post] func (h *Handler) GetorCreateVerificationToken() fiber.Handler { return func(c *fiber.Ctx) error { @@ -38,9 +37,10 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } response := responses.NewTokenResponseWithStatus(token, isNewToken) - - fmt.Println("token from handler", token) - return c.JSON(fiber.Map{"result": response}) + if isNewToken { + return c.Status(fiber.StatusCreated).JSON(fiber.Map{"result": response}) + } + return c.Status(fiber.StatusOK).JSON(fiber.Map{"result": response}) } } diff --git a/internal/responses/responses.go b/internal/responses/responses.go index 8f8fce9..f8226e7 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -19,13 +19,8 @@ func SuccessResponse(c *fiber.Ctx, statusCode int, data interface{}, message str }) } -type TokenResponseWithStatus struct { - Token *TokenResponse `json:"token"` - IsNewToken bool `json:"is_new_token"` - Message string `json:"message"` -} - type TokenResponse struct { + Message string `json:"message"` AuthToken string `json:"authToken"` ScanRef string `json:"scanRef"` ClientID string `json:"clientId"` @@ -89,23 +84,20 @@ type VerificationDataResponse struct { } // implement from() method for TokenResponseWithStatus -func NewTokenResponseWithStatus(token *models.Token, isNewToken bool) *TokenResponseWithStatus { +func NewTokenResponseWithStatus(token *models.Token, isNewToken bool) *TokenResponse { message := "Existing valid token retrieved." if isNewToken { message = "New token created." } - return &TokenResponseWithStatus{ - Token: &TokenResponse{ - AuthToken: token.AuthToken, - ScanRef: token.ScanRef, - ClientID: token.ClientID, - ExpiryTime: token.ExpiryTime, - SessionLength: token.SessionLength, - DigitString: token.DigitString, - TokenType: token.TokenType, - }, - IsNewToken: isNewToken, - Message: message, + return &TokenResponse{ + AuthToken: token.AuthToken, + ScanRef: token.ScanRef, + ClientID: token.ClientID, + ExpiryTime: token.ExpiryTime, + SessionLength: token.SessionLength, + DigitString: token.DigitString, + TokenType: token.TokenType, + Message: message, } } From da7c48e2248f3a636d48b63060caef96e1731644 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Sun, 20 Oct 2024 18:01:43 +0300 Subject: [PATCH 008/105] implement idenfy callback verifier --- internal/clients/idenfy/idenfy.go | 26 ++- internal/handlers/handlers.go | 96 ++++++++++- internal/models/new_verification_model.go | 163 ++++++++++++++++++ .../repository/verification_repository.go | 8 +- internal/server/server.go | 12 +- internal/services/services.go | 2 +- internal/services/verification_service.go | 14 +- internal/utils/utils.go | 1 - scripts/dev/auth/generate-test-auth-data.go | 33 +++- 9 files changed, 332 insertions(+), 23 deletions(-) create mode 100644 internal/models/new_verification_model.go delete mode 100644 internal/utils/utils.go diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index 722bf56..7f459a5 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -2,8 +2,12 @@ package idenfy import ( "context" + "crypto/hmac" + "crypto/sha256" "encoding/base64" + "encoding/hex" "encoding/json" + "errors" "fmt" "example.com/tfgrid-kyc-service/internal/configs" @@ -82,8 +86,24 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) return result, nil } -func (c *Idenfy) ProcessVerificationResult(ctx context.Context, sessionID string) (interface{}, error) { - var data interface{} +// verify signature of the callback +func (c *Idenfy) VerifyCallbackSignature(ctx context.Context, body []byte, sigHeader string) error { + fmt.Println("start verifying callback signature") + if len(c.callbackSignKey) < 1 { + return errors.New("callback was received but no signature key was provided") + } + sig, err := hex.DecodeString(sigHeader) + if err != nil { + return err + } + mac := hmac.New(sha256.New, c.callbackSignKey) + + mac.Write(body) + + if !hmac.Equal(sig, mac.Sum(nil)) { + return errors.New("signature verification failed") + } + fmt.Println("signature verified") - return data, nil + return nil } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index d5a7ac2..c048c0e 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -1,8 +1,17 @@ package handlers import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "strings" + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" + "example.com/tfgrid-kyc-service/internal/models" "example.com/tfgrid-kyc-service/internal/responses" "example.com/tfgrid-kyc-service/internal/services" ) @@ -32,7 +41,7 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") - token, isNewToken, err := h.tokenService.GetorCreateVerificationToken(c.Context(), clientID) + token, isNewToken, err := h.coordinatorService.GetorCreateVerificationToken(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } @@ -56,7 +65,7 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { // @Router /api/v1/data [get] func (h *Handler) GetVerificationData() fiber.Handler { return func(c *fiber.Ctx) error { - clientID := c.Query("clientID") + clientID := c.Get("X-Client-ID") verification, err := h.verificationService.GetVerification(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) @@ -81,7 +90,7 @@ func (h *Handler) GetVerificationData() fiber.Handler { // @Router /api/v1/status [get] func (h *Handler) GetVerificationStatus() fiber.Handler { return func(c *fiber.Ctx) error { - clientID := c.Query("clientID") + clientID := c.Get("X-Client-ID") verification, err := h.verificationService.GetVerification(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) @@ -103,7 +112,28 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { // @Router /webhooks/idenfy/verification-update [post] func (h *Handler) ProcessVerificationResult() fiber.Handler { return func(c *fiber.Ctx) error { - return nil + // print request body and headers and return 200 + fmt.Printf("%+v", c.Body()) + fmt.Printf("%+v", &c.Request().Header) + sigHeader := c.Get("Idenfy-Signature") + if len(sigHeader) < 1 { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "No signature provided"}) + } + body := c.Body() + // encode the body to json and save it to the database + var result models.Verification + decoder := json.NewDecoder(bytes.NewReader(body)) + err := decoder.Decode(&result) + if err != nil { + fmt.Println(err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + fmt.Printf("after decoding: %+v", result) + err = h.verificationService.ProcessVerificationResult(c.Context(), body, sigHeader, result) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.SendStatus(fiber.StatusOK) } } @@ -119,3 +149,61 @@ func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { return nil } } + +func decodeJSONBody(r *fasthttp.Request, dst interface{}) error { + fmt.Println("start decoding json body") + // check if request type contains application/json + contentType := string(r.Header.ContentType()) + if !strings.Contains(contentType, "application/json") { + return errors.New("Content-Type header is not application/json") + } + + dec := json.NewDecoder(r.BodyStream()) + dec.DisallowUnknownFields() + fmt.Println("decoding json body") + err := dec.Decode(&dst) + if err != nil { + var syntaxError *json.SyntaxError + var unmarshalTypeError *json.UnmarshalTypeError + + switch { + case errors.As(err, &syntaxError): + fmt.Println("syntax error") + msg := fmt.Sprintf("request body contains badly-formed JSON (at position %d)", syntaxError.Offset) + return errors.New(msg) + + case errors.Is(err, io.ErrUnexpectedEOF): + fmt.Println("unexpected EOF") + msg := "request body contains badly-formed JSON" + return errors.New(msg) + + case errors.As(err, &unmarshalTypeError): + fmt.Println("unmarshal type error") + msg := fmt.Sprintf("request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset) + return errors.New(msg) + + case strings.HasPrefix(err.Error(), "json: unknown field "): + fmt.Println("unknown field error") + fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") + msg := fmt.Sprintf("request body contains unknown field %s", fieldName) + return errors.New(msg) + + case errors.Is(err, io.EOF): + fmt.Println("EOF error") + msg := "request body must not be empty" + return errors.New(msg) + + case err.Error() == "http: request body too large": + fmt.Println("request body too large") + msg := "request body must not be larger than 1MB" + return errors.New(msg) + + default: + fmt.Println("default error") + return err + } + } + + fmt.Println("end decoding json body") + return nil +} diff --git a/internal/models/new_verification_model.go b/internal/models/new_verification_model.go new file mode 100644 index 0000000..e2bf7a9 --- /dev/null +++ b/internal/models/new_verification_model.go @@ -0,0 +1,163 @@ +package models + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// TODO: switch to this model +type Platform string + +const ( + PlatformPC Platform = "PC" + PlatformMobile Platform = "MOBILE" + PlatformTablet Platform = "TABLET" + PlatformMobileApp Platform = "MOBILE_APP" + PlatformMobileSDK Platform = "MOBILE_SDK" + PlatformOther Platform = "OTHER" +) + +type OverallStatus string + +const ( + StatusApproved OverallStatus = "APPROVED" + StatusDenied OverallStatus = "DENIED" + StatusSuspected OverallStatus = "SUSPECTED" + StatusReviewing OverallStatus = "REVIEWING" + StatusExpired OverallStatus = "EXPIRED" + StatusActive OverallStatus = "ACTIVE" + StatusDeleted OverallStatus = "DELETED" + StatusArchived OverallStatus = "ARCHIVED" +) + +type SuspicionReason string + +// Define constants for SuspicionReason + +type FraudTag string + +// Define constants for FraudTag + +type MismatchTag string + +// Define constants for MismatchTag + +type FaceStatus string + +const ( + FaceMatch FaceStatus = "FACE_MATCH" + FaceNotChecked FaceStatus = "FACE_NOT_CHECKED" + FaceMismatch FaceStatus = "FACE_MISMATCH" + NoFaceFound FaceStatus = "NO_FACE_FOUND" + TooManyFaces FaceStatus = "TOO_MANY_FACES" + FaceTooBlurry FaceStatus = "FACE_TOO_BLURRY" + FaceGlared FaceStatus = "FACE_GLARED" + FaceUncertain FaceStatus = "FACE_UNCERTAIN" + FaceNotAnalysed FaceStatus = "FACE_NOT_ANALYSED" + FaceError FaceStatus = "FACE_ERROR" + AutoUnverifiable FaceStatus = "AUTO_UNVERIFIABLE" + FakeFace FaceStatus = "FAKE_FACE" +) + +type DocumentStatus string + +// Define constants for DocumentStatus + +type VerificationStatus string + +const ( + StatusVerified VerificationStatus = "VERIFIED" + StatusPartiallyVerified VerificationStatus = "PARTIALLY_VERIFIED" + StatusUnverified VerificationStatus = "UNVERIFIED" +) + +type Quality string + +const ( + QualityExcellent Quality = "EXCELLENT" + QualityGood Quality = "GOOD" + QualityAverage Quality = "AVERAGE" + QualityPoor Quality = "POOR" + QualityBad Quality = "BAD" +) + +type QuestionType string + +const ( + QuestionTypeCheckbox QuestionType = "CHECKBOX" + QuestionTypeColor QuestionType = "COLOR" + QuestionTypeCountry QuestionType = "COUNTRY" + QuestionTypeDate QuestionType = "DATE" + QuestionTypeDateTime QuestionType = "DATETIME" + QuestionTypeEmail QuestionType = "EMAIL" + QuestionTypeFile QuestionType = "FILE" + QuestionTypeFloat QuestionType = "FLOAT" + QuestionTypeList QuestionType = "LIST" + QuestionTypeInteger QuestionType = "INTEGER" + QuestionTypePassword QuestionType = "PASSWORD" + QuestionTypeRadio QuestionType = "RADIO" + QuestionTypeSelect QuestionType = "SELECT" + QuestionTypeSelectMulti QuestionType = "SELECT_MULTI" + QuestionTypeTel QuestionType = "TEL" + QuestionTypeText QuestionType = "TEXT" + QuestionTypeTextArea QuestionType = "TEXT_AREA" + QuestionTypeTime QuestionType = "TIME" + QuestionTypeURL QuestionType = "URL" +) + +type Verification_ struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Final bool `bson:"final"` + Platform Platform `bson:"platform"` + Status Status_ `bson:"status"` + Data PersonData `bson:"data"` + FileUrls map[string]string `bson:"fileUrls"` + ScanRef string `bson:"scanRef"` + ClientID string `bson:"clientId"` + CompanyID string `bson:"companyId"` + BeneficiaryID string `bson:"beneficiaryId"` + StartTime int64 `bson:"startTime"` + FinishTime int64 `bson:"finishTime"` + ClientIP string `bson:"clientIp"` + ClientIPCountry string `bson:"clientIpCountry"` + ClientLocation string `bson:"clientLocation"` + QuestionnaireAnswers *QuestionnaireAnswers `bson:"questionnaireAnswers,omitempty"` + AdditionalSteps map[string]interface{} `bson:"additionalSteps,omitempty"` + UtilityData []string `bson:"utilityData,omitempty"` +} + +type Status_ struct { + Overall OverallStatus `bson:"overall"` + SuspicionReasons []SuspicionReason `bson:"suspicionReasons"` + DenyReasons []string `bson:"denyReasons"` + FraudTags []FraudTag `bson:"fraudTags"` + MismatchTags []MismatchTag `bson:"mismatchTags"` + AutoFace FaceStatus `bson:"autoFace"` + ManualFace FaceStatus `bson:"manualFace"` + AutoDocument DocumentStatus `bson:"autoDocument"` + ManualDocument DocumentStatus `bson:"manualDocument"` +} + +type PersonData_ struct { + // Add fields based on the schema + Address string `bson:"address"` + Status VerificationStatus `bson:"status"` + Accuracy *int `bson:"accuracy,omitempty"` + Quality *Quality `bson:"quality,omitempty"` +} + +type QuestionnaireAnswers struct { + Title string `bson:"title"` + Sections []Section `bson:"sections"` +} + +type Section struct { + Title string `bson:"title"` + Questions []Question `bson:"questions"` +} + +type Question struct { + Key string `bson:"key"` + Title string `bson:"title"` + Type QuestionType `bson:"type"` + Value string `bson:"value"` +} diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go index b08dedb..52d952a 100644 --- a/internal/repository/verification_repository.go +++ b/internal/repository/verification_repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" "time" "example.com/tfgrid-kyc-service/internal/models" @@ -20,14 +21,17 @@ func NewMongoVerificationRepository(db *mongo.Database) VerificationRepository { } func (r *MongoVerificationRepository) SaveVerification(ctx context.Context, verification *models.Verification) error { + fmt.Println("start saving verification to the database") verification.CreatedAt = time.Now() _, err := r.collection.InsertOne(ctx, verification) + fmt.Println(err) + fmt.Println("end saving verification to the database") return err } func (r *MongoVerificationRepository) GetVerification(ctx context.Context, clientID string) (*models.Verification, error) { var verification models.Verification - err := r.collection.FindOne(ctx, bson.M{"clientID": clientID}).Decode(&verification) + err := r.collection.FindOne(ctx, bson.M{"clientId": clientID}).Decode(&verification) if err != nil { if err == mongo.ErrNoDocuments { return nil, nil @@ -38,6 +42,6 @@ func (r *MongoVerificationRepository) GetVerification(ctx context.Context, clien } func (r *MongoVerificationRepository) DeleteVerification(ctx context.Context, clientID string) error { - _, err := r.collection.DeleteOne(ctx, bson.M{"clientID": clientID}) + _, err := r.collection.DeleteOne(ctx, bson.M{"clientId": clientID}) return err } diff --git a/internal/server/server.go b/internal/server/server.go index 128e7d0..350c2d2 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -34,14 +34,14 @@ func New(config *configs.Config) *Server { app := fiber.New() // Setup Limter Config and store - ipLimiterstore := mongodb.New(mongodb.Config{ + /* ipLimiterstore := mongodb.New(mongodb.Config{ ConnectionURI: config.MongoURI, Database: config.DatabaseName, Collection: "ip_limit", Reset: false, - }) - ipLimiterConfig := limiter.Config{ // TODO: use configurable parameters - Max: 3, + }) */ + /* ipLimiterConfig := limiter.Config{ // TODO: use configurable parameters, also check if it works well after passing the request through an SSL gateway + Max: 5, Expiration: 24 * time.Hour, SkipFailedRequests: false, SkipSuccessfulRequests: false, @@ -50,7 +50,7 @@ func New(config *configs.Config) *Server { Next: func(c *fiber.Ctx) bool { return c.IP() == "127.0.0.1" }, - } + } */ clientLimiterStore := mongodb.New(mongodb.Config{ ConnectionURI: config.MongoURI, Database: config.DatabaseName, @@ -108,7 +108,7 @@ func New(config *configs.Config) *Server { // Routes app.Get("/docs/*", swagger.HandlerDefault) - v1 := app.Group("/api/v1", limiter.New(ipLimiterConfig), middleware.AuthMiddleware(config.ChallengeWindow)) + v1 := app.Group("/api/v1", middleware.AuthMiddleware(config.ChallengeWindow)) // TODO: add limiter.New(ipLimiterConfig) v1.Post("/token", limiter.New(clientLimiterConfig), handler.GetorCreateVerificationToken()) v1.Get("/data", handler.GetVerificationData()) v1.Get("/status", handler.GetVerificationStatus()) diff --git a/internal/services/services.go b/internal/services/services.go index 59517e3..0129b17 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -14,7 +14,7 @@ type TokenService interface { type VerificationService interface { GetVerification(ctx context.Context, clientID string) (*models.Verification, error) - ProcessVerificationResult(ctx context.Context, clientID string) 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/verification_service.go b/internal/services/verification_service.go index ea31c52..8e10537 100644 --- a/internal/services/verification_service.go +++ b/internal/services/verification_service.go @@ -2,6 +2,7 @@ package services import ( "context" + "fmt" "example.com/tfgrid-kyc-service/internal/clients/idenfy" "example.com/tfgrid-kyc-service/internal/configs" @@ -27,7 +28,18 @@ func (s *verificationService) GetVerification(ctx context.Context, clientID stri return verification, nil } -func (s *verificationService) ProcessVerificationResult(ctx context.Context, clientID string) error { +func (s *verificationService) ProcessVerificationResult(ctx context.Context, body []byte, sigHeader string, result models.Verification) error { + err := s.idenfy.VerifyCallbackSignature(ctx, body, sigHeader) + if err != nil { + return err + } + err = s.repo.SaveVerification(ctx, &result) + if err != nil { + fmt.Printf("error saving verification to the database: %v", err) + return err + } + // fmt the result + fmt.Println(result) return nil } diff --git a/internal/utils/utils.go b/internal/utils/utils.go deleted file mode 100644 index d4b585b..0000000 --- a/internal/utils/utils.go +++ /dev/null @@ -1 +0,0 @@ -package utils diff --git a/scripts/dev/auth/generate-test-auth-data.go b/scripts/dev/auth/generate-test-auth-data.go index dd1a111..932dd9e 100644 --- a/scripts/dev/auth/generate-test-auth-data.go +++ b/scripts/dev/auth/generate-test-auth-data.go @@ -3,8 +3,10 @@ package main import ( "encoding/hex" "fmt" + "os" "time" + "github.com/vedhavyas/go-subkey/v2" "github.com/vedhavyas/go-subkey/v2/ed25519" "github.com/vedhavyas/go-subkey/v2/sr25519" ) @@ -16,11 +18,8 @@ const ( // Generate test auth data for development use func main() { message := createSignMessage() - krSr25519, err := sr25519.Scheme{}.Generate() - if err != nil { - panic(err) - } - krEd25519, err := ed25519.Scheme{}.Generate() + // if no arg provided, generate random keys + krSr25519, krEd25519, err := loadKeys() if err != nil { panic(err) } @@ -62,3 +61,27 @@ func createSignMessage() string { fmt.Println("message: ", message) return message } + +func loadKeys() (subkey.KeyPair, subkey.KeyPair, error) { + if len(os.Args) < 2 { + krSr25519, err := sr25519.Scheme{}.Generate() + if err != nil { + return nil, nil, err + } + krEd25519, err := ed25519.Scheme{}.Generate() + if err != nil { + return nil, nil, err + } + return krSr25519, krEd25519, nil + } else { + krSr25519, err := sr25519.Scheme{}.FromPhrase(os.Args[1], "") + if err != nil { + return nil, nil, err + } + krEd25519, err := ed25519.Scheme{}.FromPhrase(os.Args[1], "") + if err != nil { + return nil, nil, err + } + return krSr25519, krEd25519, nil + } +} From 712f374b00ea0a24d22d2a41cb454c1aa6bd09c1 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Sun, 20 Oct 2024 18:28:13 +0300 Subject: [PATCH 009/105] delete unused decodeJSONBody function --- internal/handlers/handlers.go | 62 ----------------------------------- 1 file changed, 62 deletions(-) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index c048c0e..7288c99 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -3,13 +3,9 @@ package handlers import ( "bytes" "encoding/json" - "errors" "fmt" - "io" - "strings" "github.com/gofiber/fiber/v2" - "github.com/valyala/fasthttp" "example.com/tfgrid-kyc-service/internal/models" "example.com/tfgrid-kyc-service/internal/responses" @@ -149,61 +145,3 @@ func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { return nil } } - -func decodeJSONBody(r *fasthttp.Request, dst interface{}) error { - fmt.Println("start decoding json body") - // check if request type contains application/json - contentType := string(r.Header.ContentType()) - if !strings.Contains(contentType, "application/json") { - return errors.New("Content-Type header is not application/json") - } - - dec := json.NewDecoder(r.BodyStream()) - dec.DisallowUnknownFields() - fmt.Println("decoding json body") - err := dec.Decode(&dst) - if err != nil { - var syntaxError *json.SyntaxError - var unmarshalTypeError *json.UnmarshalTypeError - - switch { - case errors.As(err, &syntaxError): - fmt.Println("syntax error") - msg := fmt.Sprintf("request body contains badly-formed JSON (at position %d)", syntaxError.Offset) - return errors.New(msg) - - case errors.Is(err, io.ErrUnexpectedEOF): - fmt.Println("unexpected EOF") - msg := "request body contains badly-formed JSON" - return errors.New(msg) - - case errors.As(err, &unmarshalTypeError): - fmt.Println("unmarshal type error") - msg := fmt.Sprintf("request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset) - return errors.New(msg) - - case strings.HasPrefix(err.Error(), "json: unknown field "): - fmt.Println("unknown field error") - fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") - msg := fmt.Sprintf("request body contains unknown field %s", fieldName) - return errors.New(msg) - - case errors.Is(err, io.EOF): - fmt.Println("EOF error") - msg := "request body must not be empty" - return errors.New(msg) - - case err.Error() == "http: request body too large": - fmt.Println("request body too large") - msg := "request body must not be larger than 1MB" - return errors.New(msg) - - default: - fmt.Println("default error") - return err - } - } - - fmt.Println("end decoding json body") - return nil -} From e265b55e1c2c74a8524414bdbca9ff0d46f17b27 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 21 Oct 2024 13:11:28 +0300 Subject: [PATCH 010/105] update status endpoint response --- api/docs/docs.go | 27 ++------------- api/docs/swagger.json | 27 ++------------- api/docs/swagger.yaml | 20 ++--------- internal/handlers/handlers.go | 4 +-- internal/models/verification.go | 9 ++++- internal/repository/repository.go | 1 - .../repository/verification_repository.go | 10 +++--- internal/responses/responses.go | 34 ++++++++----------- internal/services/services.go | 1 + internal/services/verification_service.go | 25 +++++++++++++- 10 files changed, 62 insertions(+), 96 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 90413f9..63cc0cd 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -371,34 +371,13 @@ const docTemplate = `{ "responses.VerificationStatusResponse": { "type": "object", "properties": { - "autoDocument": { - "type": "string" - }, - "autoFace": { - "type": "string" - }, "clientId": { "type": "string" }, - "fraudTags": { - "type": "array", - "items": { - "type": "string" - } - }, - "manualDocument": { - "type": "string" - }, - "manualFace": { - "type": "string" - }, - "mismatchTags": { - "type": "array", - "items": { - "type": "string" - } + "final": { + "type": "boolean" }, - "scanRef": { + "idenfyRef": { "type": "string" }, "status": { diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 6b39eb0..2764709 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -364,34 +364,13 @@ "responses.VerificationStatusResponse": { "type": "object", "properties": { - "autoDocument": { - "type": "string" - }, - "autoFace": { - "type": "string" - }, "clientId": { "type": "string" }, - "fraudTags": { - "type": "array", - "items": { - "type": "string" - } - }, - "manualDocument": { - "type": "string" - }, - "manualFace": { - "type": "string" - }, - "mismatchTags": { - "type": "array", - "items": { - "type": "string" - } + "final": { + "type": "boolean" }, - "scanRef": { + "idenfyRef": { "type": "string" }, "status": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 1cdf35e..aa5f2ff 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -102,25 +102,11 @@ definitions: type: object responses.VerificationStatusResponse: properties: - autoDocument: - type: string - autoFace: - type: string clientId: type: string - fraudTags: - items: - type: string - type: array - manualDocument: - type: string - manualFace: - type: string - mismatchTags: - items: - type: string - type: array - scanRef: + final: + type: boolean + idenfyRef: type: string status: type: string diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 7288c99..4bbd41c 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -87,7 +87,7 @@ func (h *Handler) GetVerificationData() fiber.Handler { func (h *Handler) GetVerificationStatus() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") - verification, err := h.verificationService.GetVerification(c.Context(), clientID) + verification, err := h.verificationService.GetVerificationStatus(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } @@ -108,7 +108,6 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { // @Router /webhooks/idenfy/verification-update [post] func (h *Handler) ProcessVerificationResult() fiber.Handler { return func(c *fiber.Ctx) error { - // print request body and headers and return 200 fmt.Printf("%+v", c.Body()) fmt.Printf("%+v", &c.Request().Header) sigHeader := c.Get("Idenfy-Signature") @@ -116,7 +115,6 @@ func (h *Handler) ProcessVerificationResult() fiber.Handler { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "No signature provided"}) } body := c.Body() - // encode the body to json and save it to the database var result models.Verification decoder := json.NewDecoder(bytes.NewReader(body)) err := decoder.Decode(&result) diff --git a/internal/models/verification.go b/internal/models/verification.go index a3cb586..537c611 100644 --- a/internal/models/verification.go +++ b/internal/models/verification.go @@ -8,7 +8,7 @@ import ( type Verification struct { ID primitive.ObjectID `bson:"_id,omitempty"` - Final *bool `bson:"final"` + Final bool `bson:"final"` Platform string `bson:"platform"` Status Status `bson:"status"` Data PersonData `bson:"data"` @@ -174,3 +174,10 @@ type AMLData struct { IsActive *bool `bson:"isActive"` CheckDate string `bson:"checkDate"` } + +type VerificationOutcome struct { + Final bool `bson:"final"` + ClientID string `bson:"clientId"` + IdenfyRef string `bson:"idenfyRef"` + Outcome string `bson:"outcome"` +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 91652b1..ed22a8c 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -15,5 +15,4 @@ type TokenRepository interface { type VerificationRepository interface { SaveVerification(ctx context.Context, verification *models.Verification) error GetVerification(ctx context.Context, clientID string) (*models.Verification, error) - DeleteVerification(ctx context.Context, clientID string) error } diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go index 52d952a..3ff37ac 100644 --- a/internal/repository/verification_repository.go +++ b/internal/repository/verification_repository.go @@ -8,6 +8,7 @@ import ( "example.com/tfgrid-kyc-service/internal/models" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) type MongoVerificationRepository struct { @@ -31,7 +32,9 @@ func (r *MongoVerificationRepository) SaveVerification(ctx context.Context, veri func (r *MongoVerificationRepository) GetVerification(ctx context.Context, clientID string) (*models.Verification, error) { var verification models.Verification - err := r.collection.FindOne(ctx, bson.M{"clientId": clientID}).Decode(&verification) + // return the latest verification + opts := options.FindOne().SetSort(bson.D{{"createdAt", -1}}) + err := r.collection.FindOne(ctx, bson.M{"clientId": clientID}, opts).Decode(&verification) if err != nil { if err == mongo.ErrNoDocuments { return nil, nil @@ -40,8 +43,3 @@ func (r *MongoVerificationRepository) GetVerification(ctx context.Context, clien } return &verification, nil } - -func (r *MongoVerificationRepository) DeleteVerification(ctx context.Context, clientID string) error { - _, err := r.collection.DeleteOne(ctx, bson.M{"clientId": clientID}) - return err -} diff --git a/internal/responses/responses.go b/internal/responses/responses.go index f8226e7..dc90da5 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -31,15 +31,10 @@ type TokenResponse struct { } type VerificationStatusResponse struct { - FraudTags []string `json:"fraudTags"` - MismatchTags []string `json:"mismatchTags"` - AutoDocument string `json:"autoDocument"` - AutoFace string `json:"autoFace"` - ManualDocument string `json:"manualDocument"` - ManualFace string `json:"manualFace"` - ScanRef string `json:"scanRef"` - ClientID string `json:"clientId"` - Status string `json:"status"` + Final bool `json:"final"` + IdenfyRef string `json:"idenfyRef"` + ClientID string `json:"clientId"` + Status string `json:"status"` } type VerificationDataResponse struct { @@ -101,17 +96,18 @@ func NewTokenResponseWithStatus(token *models.Token, isNewToken bool) *TokenResp } } -func NewVerificationStatusResponse(verification *models.Verification) *VerificationStatusResponse { +func NewVerificationStatusResponse(verificationOutcome *models.VerificationOutcome) *VerificationStatusResponse { return &VerificationStatusResponse{ - FraudTags: verification.Status.FraudTags, - MismatchTags: verification.Status.MismatchTags, - AutoDocument: verification.Status.AutoDocument, - ManualDocument: verification.Status.ManualDocument, - AutoFace: verification.Status.AutoFace, - ManualFace: verification.Status.ManualFace, - ScanRef: verification.ScanRef, - ClientID: verification.ClientID, - Status: string(verification.Status.Overall), + /* FraudTags: verification.Status.FraudTags, + MismatchTags: verification.Status.MismatchTags, + AutoDocument: verification.Status.AutoDocument, + ManualDocument: verification.Status.ManualDocument, + AutoFace: verification.Status.AutoFace, + ManualFace: verification.Status.ManualFace, */ + Final: verificationOutcome.Final, + IdenfyRef: verificationOutcome.IdenfyRef, + ClientID: verificationOutcome.ClientID, + Status: verificationOutcome.Outcome, } } diff --git a/internal/services/services.go b/internal/services/services.go index 0129b17..3862dd2 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -14,6 +14,7 @@ type TokenService interface { type VerificationService interface { GetVerification(ctx context.Context, clientID string) (*models.Verification, error) + GetVerificationStatus(ctx context.Context, clientID 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/verification_service.go b/internal/services/verification_service.go index 8e10537..469748c 100644 --- a/internal/services/verification_service.go +++ b/internal/services/verification_service.go @@ -28,6 +28,29 @@ func (s *verificationService) GetVerification(ctx context.Context, clientID stri return verification, nil } +func (s *verificationService) GetVerificationStatus(ctx context.Context, clientID string) (*models.VerificationOutcome, error) { + verification, err := s.GetVerification(ctx, clientID) + if err != nil { + return nil, err + } + var outcome string + if verification != nil { + if verification.Status.Overall == "APPROVED" || (s.config.SuspiciousVerificationOutcome == "APPROVED" && verification.Status.Overall == "SUSPECTED") { + outcome = "APPROVED" + } else { + outcome = "REJECTED" + } + } else { + return nil, nil + } + return &models.VerificationOutcome{ + Final: verification.Final, + ClientID: clientID, + IdenfyRef: verification.ScanRef, + Outcome: outcome, + }, nil +} + func (s *verificationService) ProcessVerificationResult(ctx context.Context, body []byte, sigHeader string, result models.Verification) error { err := s.idenfy.VerifyCallbackSignature(ctx, body, sigHeader) if err != nil { @@ -48,7 +71,7 @@ func (s *verificationService) ProcessDocExpirationNotification(ctx context.Conte } func (s *verificationService) IsUserVerified(ctx context.Context, clientID string) (bool, error) { - verification, err := s.GetVerification(ctx, clientID) + verification, err := s.repo.GetVerification(ctx, clientID) if err != nil { return false, err } From ffef4178dc0c98f543d7e63125426246007ecc3d Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 21 Oct 2024 14:00:13 +0300 Subject: [PATCH 011/105] merge token and verification services --- internal/handlers/handlers.go | 16 +-- internal/repository/repository.go | 2 +- internal/repository/token_repository.go | 7 +- internal/server/server.go | 6 +- internal/services/coordinator_service.go | 34 ----- internal/services/kyc_service.go | 156 ++++++++++++++++++++++ internal/services/services.go | 14 +- internal/services/token_service.go | 69 ---------- internal/services/verification_service.go | 82 ------------ 9 files changed, 173 insertions(+), 213 deletions(-) delete mode 100644 internal/services/coordinator_service.go create mode 100644 internal/services/kyc_service.go delete mode 100644 internal/services/token_service.go delete mode 100644 internal/services/verification_service.go diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 4bbd41c..30f451b 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -13,13 +13,11 @@ import ( ) type Handler struct { - tokenService services.TokenService - verificationService services.VerificationService - coordinatorService services.CoordinatorService + kycService services.KYCService } -func NewHandler(tokenService services.TokenService, verificationService services.VerificationService, coordinatorService services.CoordinatorService) *Handler { - return &Handler{tokenService: tokenService, verificationService: verificationService, coordinatorService: coordinatorService} +func NewHandler(kycService services.KYCService) *Handler { + return &Handler{kycService: kycService} } // @Summary Get or Generate iDenfy Verification Token @@ -37,7 +35,7 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") - token, isNewToken, err := h.coordinatorService.GetorCreateVerificationToken(c.Context(), clientID) + token, isNewToken, err := h.kycService.GetorCreateVerificationToken(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } @@ -62,7 +60,7 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { func (h *Handler) GetVerificationData() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") - verification, err := h.verificationService.GetVerification(c.Context(), clientID) + verification, err := h.kycService.GetVerification(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } @@ -87,7 +85,7 @@ func (h *Handler) GetVerificationData() fiber.Handler { func (h *Handler) GetVerificationStatus() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") - verification, err := h.verificationService.GetVerificationStatus(c.Context(), clientID) + verification, err := h.kycService.GetVerificationStatus(c.Context(), clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } @@ -123,7 +121,7 @@ func (h *Handler) ProcessVerificationResult() fiber.Handler { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } fmt.Printf("after decoding: %+v", result) - err = h.verificationService.ProcessVerificationResult(c.Context(), body, sigHeader, result) + err = h.kycService.ProcessVerificationResult(c.Context(), body, sigHeader, result) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } diff --git a/internal/repository/repository.go b/internal/repository/repository.go index ed22a8c..8138a8e 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -9,7 +9,7 @@ import ( 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) error + DeleteToken(ctx context.Context, clientID string, scanRef string) error } type VerificationRepository interface { diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index 598648f..c31ac2a 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -71,7 +71,10 @@ func (r *MongoTokenRepository) GetToken(ctx context.Context, clientID string) (* return &token, nil } -func (r *MongoTokenRepository) DeleteToken(ctx context.Context, clientID string) error { - _, err := r.collection.DeleteOne(ctx, bson.M{"clientId": clientID}) +func (r *MongoTokenRepository) DeleteToken(ctx context.Context, clientID string, scanRef string) error { + res, err := r.collection.DeleteOne(ctx, bson.M{"clientId": clientID, "scanRef": scanRef}) + if err == nil { + fmt.Println("token deletion succeeded. deleted count: ", res.DeletedCount) + } return err } diff --git a/internal/server/server.go b/internal/server/server.go index 350c2d2..6ba9d72 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -98,12 +98,10 @@ func New(config *configs.Config) *Server { if err != nil { log.Fatalf("Failed to initialize substrate client: %v", err) } - tokenService := services.NewTokenService(tokenRepo, idenfyClient, substrateClient, config.MinBalanceToVerifyAccount) - verificationService := services.NewVerificationService(verificationRepo, idenfyClient, &config.Verification) - coordinatorService := services.NewCoordinatorService(tokenService, verificationService) + kycService := services.NewKYCService(verificationRepo, tokenRepo, idenfyClient, substrateClient, config.MinBalanceToVerifyAccount, &config.Verification) // Initialize handler - handler := handlers.NewHandler(tokenService, verificationService, coordinatorService) + handler := handlers.NewHandler(kycService) // Routes app.Get("/docs/*", swagger.HandlerDefault) diff --git a/internal/services/coordinator_service.go b/internal/services/coordinator_service.go deleted file mode 100644 index ab13947..0000000 --- a/internal/services/coordinator_service.go +++ /dev/null @@ -1,34 +0,0 @@ -package services - -import ( - "context" - "errors" - - "example.com/tfgrid-kyc-service/internal/models" -) - -type coordinatorService struct { - tokenService TokenService - verificationService VerificationService -} - -func NewCoordinatorService(tokenService TokenService, verificationService VerificationService) CoordinatorService { - return &coordinatorService{tokenService: tokenService, verificationService: verificationService} -} - -func (s *coordinatorService) GetorCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) { - // check if user is unverified, return an error if not - // this should be client responsibility to check if they are verified before requesting a new verification - isVerified, err := s.verificationService.IsUserVerified(ctx, clientID) - if err != nil { - return nil, false, err - } - if isVerified { - return nil, false, errors.New("user already verified") // TODO: implement a custom error that can be converted in the handler to a 400 status code - } - token, isNew, err := s.tokenService.GetorCreateVerificationToken(ctx, clientID) - if err != nil { - return nil, false, err - } - return token, isNew, nil -} diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go new file mode 100644 index 0000000..c79dfe8 --- /dev/null +++ b/internal/services/kyc_service.go @@ -0,0 +1,156 @@ +package services + +import ( + "context" + "errors" + "fmt" + + "example.com/tfgrid-kyc-service/internal/clients/idenfy" + "example.com/tfgrid-kyc-service/internal/clients/substrate" + "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/models" + "example.com/tfgrid-kyc-service/internal/repository" +) + +type kycService struct { + verificationRepo repository.VerificationRepository + tokenRepo repository.TokenRepository + idenfy *idenfy.Idenfy + substrate *substrate.Substrate + requiredBalance uint64 + config *configs.VerificationConfig +} + +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, requiredBalance uint64, config *configs.VerificationConfig) KYCService { + return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, requiredBalance: requiredBalance, config: config} +} + +// --------------------------------------------------------------------------------------------------------------------- +// 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 { + return nil, false, err + } + if isVerified { + return nil, false, errors.New("user already verified") // TODO: implement a custom error that can be converted in the handler to a 400 status code + } + token, err := s.tokenRepo.GetToken(ctx, clientID) + if err != nil { + return nil, false, err + } + // check if token is not nil and not expired or near expiry (2 min) + if token != nil { //&& time.Since(token.CreatedAt)+2*time.Minute < time.Duration(token.ExpiryTime)*time.Second { + return token, false, nil + } + fmt.Println("token is nil or expired") + // check if user account balance satisfies the minimum required balance, return an error if not + hasRequiredBalance, err := s.AccountHasRequiredBalance(ctx, clientID) + if err != nil { + return nil, false, err // todo: implement a custom error that can be converted in the handler to a 500 status code + } + if !hasRequiredBalance { + return nil, false, errors.New("account does not have the required balance") // todo: implement a custom error that can be converted in the handler to a 402 status code + } + newToken, err := s.idenfy.CreateVerificationSession(ctx, clientID) + if err != nil { + return nil, false, err + } + fmt.Println("new token", newToken) + err = s.tokenRepo.SaveToken(ctx, &newToken) + if err != nil { + fmt.Println("warning: was not able to save verification token to db", err) + } + + return &newToken, true, nil +} + +func (s *kycService) DeleteToken(ctx context.Context, clientID string, scanRef string) error { + return s.tokenRepo.DeleteToken(ctx, clientID, scanRef) +} + +func (s *kycService) AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) { + if s.requiredBalance == 0 { + return true, nil + } + balance, err := s.substrate.GetAccountBalance(address) + if err != nil { + return false, err + } + return balance >= s.requiredBalance, nil +} + +// --------------------------------------------------------------------------------------------------------------------- +// verification related methods +// --------------------------------------------------------------------------------------------------------------------- + +func (s *kycService) GetVerification(ctx context.Context, clientID string) (*models.Verification, error) { + verification, err := s.verificationRepo.GetVerification(ctx, clientID) + if err != nil { + return nil, err + } + return verification, nil +} + +func (s *kycService) GetVerificationStatus(ctx context.Context, clientID string) (*models.VerificationOutcome, error) { + verification, err := s.GetVerification(ctx, clientID) + if err != nil { + return nil, err + } + var outcome string + if verification != nil { + if verification.Status.Overall == "APPROVED" || (s.config.SuspiciousVerificationOutcome == "APPROVED" && verification.Status.Overall == "SUSPECTED") { + outcome = "APPROVED" + } else { + outcome = "REJECTED" + } + } else { + return nil, nil + } + return &models.VerificationOutcome{ + Final: verification.Final, + ClientID: clientID, + IdenfyRef: verification.ScanRef, + Outcome: outcome, + }, nil +} + +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 { + return err + } + // delete the token with the same clientID and same scanRef + err = s.tokenRepo.DeleteToken(ctx, result.ClientID, result.ScanRef) + if err != nil { + fmt.Printf("error deleting token: %v", err) + } + // if the verification status is EXPIRED, we don't need to save it + if result.Status.Overall != "EXPIRED" { + err = s.verificationRepo.SaveVerification(ctx, &result) + if err != nil { + fmt.Printf("error saving verification to the database: %v", err) + return err + } + } + // fmt the result + fmt.Println(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 { + return false, err + } + if verification == nil { + return false, nil + } + return verification.Status.Overall == "APPROVED" || (s.config.SuspiciousVerificationOutcome == "APPROVED" && verification.Status.Overall == "SUSPECTED"), nil +} diff --git a/internal/services/services.go b/internal/services/services.go index 3862dd2..6290315 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -6,23 +6,13 @@ import ( "example.com/tfgrid-kyc-service/internal/models" ) -type TokenService interface { +type KYCService interface { GetorCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) - DeleteToken(ctx context.Context, clientID string) error + DeleteToken(ctx context.Context, clientID string, scanRef string) error AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) -} - -type VerificationService interface { GetVerification(ctx context.Context, clientID string) (*models.Verification, error) GetVerificationStatus(ctx context.Context, clientID 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) } - -// The existing services (TokenService and VerificationService) already encapsulate most of the logic. -// This coordinator service would orchestrate operations between these services, -// encapsulating the logic that spans over them while keeping them focused on their specific domains. -type CoordinatorService interface { - GetorCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) -} diff --git a/internal/services/token_service.go b/internal/services/token_service.go deleted file mode 100644 index 967f8fc..0000000 --- a/internal/services/token_service.go +++ /dev/null @@ -1,69 +0,0 @@ -package services - -import ( - "context" - "errors" - "fmt" - - "example.com/tfgrid-kyc-service/internal/clients/idenfy" - "example.com/tfgrid-kyc-service/internal/clients/substrate" - "example.com/tfgrid-kyc-service/internal/models" - "example.com/tfgrid-kyc-service/internal/repository" -) - -type tokenService struct { - repo repository.TokenRepository - idenfy *idenfy.Idenfy - substrate *substrate.Substrate - requiredBalance uint64 -} - -func NewTokenService(repo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, requiredBalance uint64) TokenService { - return &tokenService{repo: repo, idenfy: idenfy, substrate: substrateClient, requiredBalance: requiredBalance} -} - -func (s *tokenService) GetorCreateVerificationToken(ctx context.Context, clientID string) (*models.Token, bool, error) { - token, err := s.repo.GetToken(ctx, clientID) - if err != nil { - return nil, false, err - } - // check if token is not nil and not expired or near expiry (2 min) - if token != nil { //&& time.Since(token.CreatedAt)+2*time.Minute < time.Duration(token.ExpiryTime)*time.Second { - return token, false, nil - } - fmt.Println("token is nil or expired") - // check if user account balance satisfies the minimum required balance, return an error if not - hasRequiredBalance, err := s.AccountHasRequiredBalance(ctx, clientID) - if err != nil { - return nil, false, err // todo: implement a custom error that can be converted in the handler to a 500 status code - } - if !hasRequiredBalance { - return nil, false, errors.New("account does not have the required balance") // todo: implement a custom error that can be converted in the handler to a 402 status code - } - newToken, err := s.idenfy.CreateVerificationSession(ctx, clientID) - if err != nil { - return nil, false, err - } - fmt.Println("new token", newToken) - err = s.repo.SaveToken(ctx, &newToken) - if err != nil { - fmt.Println("warning: was not able to save verification token to db", err) - } - - return &newToken, true, nil -} - -func (s *tokenService) DeleteToken(ctx context.Context, clientID string) error { - return s.repo.DeleteToken(ctx, clientID) -} - -func (s *tokenService) AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) { - if s.requiredBalance == 0 { - return true, nil - } - balance, err := s.substrate.GetAccountBalance(address) - if err != nil { - return false, err - } - return balance >= s.requiredBalance, nil -} diff --git a/internal/services/verification_service.go b/internal/services/verification_service.go deleted file mode 100644 index 469748c..0000000 --- a/internal/services/verification_service.go +++ /dev/null @@ -1,82 +0,0 @@ -package services - -import ( - "context" - "fmt" - - "example.com/tfgrid-kyc-service/internal/clients/idenfy" - "example.com/tfgrid-kyc-service/internal/configs" - "example.com/tfgrid-kyc-service/internal/models" - "example.com/tfgrid-kyc-service/internal/repository" -) - -type verificationService struct { - repo repository.VerificationRepository - idenfy *idenfy.Idenfy - config *configs.VerificationConfig -} - -func NewVerificationService(repo repository.VerificationRepository, idenfyClient *idenfy.Idenfy, config *configs.VerificationConfig) VerificationService { - return &verificationService{repo: repo, idenfy: idenfyClient, config: config} -} - -func (s *verificationService) GetVerification(ctx context.Context, clientID string) (*models.Verification, error) { - verification, err := s.repo.GetVerification(ctx, clientID) - if err != nil { - return nil, err - } - return verification, nil -} - -func (s *verificationService) GetVerificationStatus(ctx context.Context, clientID string) (*models.VerificationOutcome, error) { - verification, err := s.GetVerification(ctx, clientID) - if err != nil { - return nil, err - } - var outcome string - if verification != nil { - if verification.Status.Overall == "APPROVED" || (s.config.SuspiciousVerificationOutcome == "APPROVED" && verification.Status.Overall == "SUSPECTED") { - outcome = "APPROVED" - } else { - outcome = "REJECTED" - } - } else { - return nil, nil - } - return &models.VerificationOutcome{ - Final: verification.Final, - ClientID: clientID, - IdenfyRef: verification.ScanRef, - Outcome: outcome, - }, nil -} - -func (s *verificationService) ProcessVerificationResult(ctx context.Context, body []byte, sigHeader string, result models.Verification) error { - err := s.idenfy.VerifyCallbackSignature(ctx, body, sigHeader) - if err != nil { - return err - } - err = s.repo.SaveVerification(ctx, &result) - if err != nil { - fmt.Printf("error saving verification to the database: %v", err) - return err - } - // fmt the result - fmt.Println(result) - return nil -} - -func (s *verificationService) ProcessDocExpirationNotification(ctx context.Context, clientID string) error { - return nil -} - -func (s *verificationService) IsUserVerified(ctx context.Context, clientID string) (bool, error) { - verification, err := s.repo.GetVerification(ctx, clientID) - if err != nil { - return false, err - } - if verification == nil { - return false, nil - } - return verification.Status.Overall == "APPROVED" || (s.config.SuspiciousVerificationOutcome == "APPROVED" && verification.Status.Overall == "SUSPECTED"), nil -} From 081689a8f1b1d68d0afdc16f6e39517fa3592684 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 21 Oct 2024 14:05:43 +0300 Subject: [PATCH 012/105] move MinBalanceToVerifyAccount config to verification struct --- internal/configs/config.go | 6 +++--- internal/server/server.go | 2 +- internal/services/kyc_service.go | 9 ++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/internal/configs/config.go b/internal/configs/config.go index 19de4c0..2b48ffe 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -22,8 +22,7 @@ type Config struct { // Verification Verification VerificationConfig `mapstructure:"verification"` // Other - ChallengeWindow int64 `mapstructure:"challenge_window"` - MinBalanceToVerifyAccount uint64 `mapstructure:"min_balance_to_verify_account"` + ChallengeWindow int64 `mapstructure:"challenge_window"` } type IdenfyConfig struct { @@ -41,6 +40,7 @@ type TFChainConfig struct { type VerificationConfig struct { SuspiciousVerificationOutcome string `mapstructure:"suspicious_verification_outcome"` ExpiredDocumentOutcome string `mapstructure:"expired_document_outcome"` + MinBalanceToVerifyAccount uint64 `mapstructure:"min_balance_to_verify_account"` } func LoadConfig() (*Config, error) { @@ -100,7 +100,7 @@ func LoadConfig() (*Config, error) { if err != nil { return nil, fmt.Errorf("error binding env variable: %w", err) } - err = viper.BindEnv("min_balance_to_verify_account") + err = viper.BindEnv("verification.min_balance_to_verify_account") if err != nil { return nil, fmt.Errorf("error binding env variable: %w", err) } diff --git a/internal/server/server.go b/internal/server/server.go index 6ba9d72..2754732 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -98,7 +98,7 @@ func New(config *configs.Config) *Server { if err != nil { log.Fatalf("Failed to initialize substrate client: %v", err) } - kycService := services.NewKYCService(verificationRepo, tokenRepo, idenfyClient, substrateClient, config.MinBalanceToVerifyAccount, &config.Verification) + kycService := services.NewKYCService(verificationRepo, tokenRepo, idenfyClient, substrateClient, &config.Verification) // Initialize handler handler := handlers.NewHandler(kycService) diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index c79dfe8..8ec4a1e 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -17,12 +17,11 @@ type kycService struct { tokenRepo repository.TokenRepository idenfy *idenfy.Idenfy substrate *substrate.Substrate - requiredBalance uint64 config *configs.VerificationConfig } -func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, requiredBalance uint64, config *configs.VerificationConfig) KYCService { - return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, requiredBalance: requiredBalance, config: config} +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.VerificationConfig) KYCService { + return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: config} } // --------------------------------------------------------------------------------------------------------------------- @@ -72,14 +71,14 @@ func (s *kycService) DeleteToken(ctx context.Context, clientID string, scanRef s } func (s *kycService) AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) { - if s.requiredBalance == 0 { + if s.config.MinBalanceToVerifyAccount == 0 { return true, nil } balance, err := s.substrate.GetAccountBalance(address) if err != nil { return false, err } - return balance >= s.requiredBalance, nil + return balance >= s.config.MinBalanceToVerifyAccount, nil } // --------------------------------------------------------------------------------------------------------------------- From 88fe7530eb1ccdd4867939f1296f86a25d90baef Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 21 Oct 2024 14:37:01 +0300 Subject: [PATCH 013/105] update config struct --- .app.env.example | 11 +++++++---- internal/configs/config.go | 29 ++++++++++++++++++++++++----- internal/server/server.go | 30 +++++++++++++++++------------- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/.app.env.example b/.app.env.example index 1b266af..3d81337 100644 --- a/.app.env.example +++ b/.app.env.example @@ -1,14 +1,17 @@ MONGO_URI=mongodb://root:password@mongodb:27017 DATABASE_NAME=tfgrid-kyc-db PORT=8080 -MAX_TOKEN_REQUESTS_PER_MINUTE=2 -SUSPICIOUS_VERIFICATION_OUTCOME=verified -EXPIRED_DOCUMENT_OUTCOME=verified CHALLENGE_WINDOW=120 +VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME=verified +VERIFICATION_EXPIRED_DOCUMENT_OUTCOME=verified +VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT=1000000 IDENFY_BASE_URL=https://ivs.idenfy.com/api/v2 IDENFY_API_KEY= IDENFY_API_SECRET= IDENFY_CALLBACK_SIGN_KEY= IDENFY_WHITELISTED_IPS= TFCHAIN_WS_PROVIDER_URL=wss://tfchain.grid.tf -MIN_BALANCE_TO_VERIFY_ACCOUNT=1000000 \ No newline at end of file +IP_LIMITER_MAX_TOKEN_REQUESTS=5 +IP_LIMITER_TOKEN_EXPIRATION=24 +ID_LIMITER_MAX_TOKEN_REQUESTS=5 +ID_LIMITER_TOKEN_EXPIRATION=24 \ No newline at end of file diff --git a/internal/configs/config.go b/internal/configs/config.go index 2b48ffe..bb7f7f8 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -18,7 +18,9 @@ type Config struct { // TFChain TFChain TFChainConfig `mapstructure:"tfchain"` // IP limiter - MaxTokenRequestsPerMinute int `mapstructure:"max_token_requests_per_minute"` + IPLimiter LimiterConfig `mapstructure:"ip_limiter"` + // Client limiter + IDLimiter LimiterConfig `mapstructure:"id_limiter"` // Verification Verification VerificationConfig `mapstructure:"verification"` // Other @@ -43,6 +45,11 @@ type VerificationConfig struct { MinBalanceToVerifyAccount uint64 `mapstructure:"min_balance_to_verify_account"` } +type LimiterConfig struct { + MaxTokenRequests int `mapstructure:"max_token_requests"` + TokenExpiration int `mapstructure:"token_expiration"` +} + func LoadConfig() (*Config, error) { // replacer @@ -84,10 +91,6 @@ func LoadConfig() (*Config, error) { if err != nil { return nil, fmt.Errorf("error binding env variable: %w", err) } - err = viper.BindEnv("max_token_requests_per_minute") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } err = viper.BindEnv("suspicious_verification_outcome") if err != nil { return nil, fmt.Errorf("error binding env variable: %w", err) @@ -112,6 +115,22 @@ func LoadConfig() (*Config, error) { if err != nil { return nil, fmt.Errorf("error binding env variable: %w", err) } + err = viper.BindEnv("ip_limiter.max_token_requests") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("ip_limiter.token_expiration") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("id_limiter.max_token_requests") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } + err = viper.BindEnv("id_limiter.token_expiration") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } // Set default values // viper.SetDefault("port", "8080") diff --git a/internal/server/server.go b/internal/server/server.go index 2754732..ad9cb2b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "log" "os" "os/signal" @@ -34,15 +35,15 @@ func New(config *configs.Config) *Server { app := fiber.New() // Setup Limter Config and store - /* ipLimiterstore := mongodb.New(mongodb.Config{ + ipLimiterstore := mongodb.New(mongodb.Config{ ConnectionURI: config.MongoURI, Database: config.DatabaseName, Collection: "ip_limit", Reset: false, - }) */ - /* ipLimiterConfig := limiter.Config{ // TODO: use configurable parameters, also check if it works well after passing the request through an SSL gateway - Max: 5, - Expiration: 24 * time.Hour, + }) + ipLimiterConfig := limiter.Config{ // TODO: use configurable parameters, also check if it works well after passing the request through an SSL gateway + Max: config.IPLimiter.MaxTokenRequests, + Expiration: time.Duration(config.IPLimiter.TokenExpiration) * time.Hour, SkipFailedRequests: false, SkipSuccessfulRequests: false, Store: ipLimiterstore, @@ -50,25 +51,28 @@ func New(config *configs.Config) *Server { Next: func(c *fiber.Ctx) bool { return c.IP() == "127.0.0.1" }, - } */ - clientLimiterStore := mongodb.New(mongodb.Config{ + } + idLimiterStore := mongodb.New(mongodb.Config{ ConnectionURI: config.MongoURI, Database: config.DatabaseName, Collection: "client_limit", Reset: false, }) - clientLimiterConfig := limiter.Config{ // TODO: use configurable parameters - Max: 10, - Expiration: 24 * time.Hour, + idLimiterConfig := limiter.Config{ // TODO: use configurable parameters + Max: config.IDLimiter.MaxTokenRequests, + Expiration: time.Duration(config.IDLimiter.TokenExpiration) * time.Hour, SkipFailedRequests: false, SkipSuccessfulRequests: false, - Store: clientLimiterStore, + Store: idLimiterStore, // Use client id as key to limit the number of requests per client KeyGenerator: func(c *fiber.Ctx) string { return c.Get("X-Client-ID") }, } + // print limtters config + fmt.Printf("IP Limiter Config: %+v\n", ipLimiterConfig) + fmt.Printf("ID Limiter Config: %+v\n", idLimiterConfig) // Global middlewares app.Use(middleware.Logger()) @@ -106,8 +110,8 @@ func New(config *configs.Config) *Server { // Routes app.Get("/docs/*", swagger.HandlerDefault) - v1 := app.Group("/api/v1", middleware.AuthMiddleware(config.ChallengeWindow)) // TODO: add limiter.New(ipLimiterConfig) - v1.Post("/token", limiter.New(clientLimiterConfig), handler.GetorCreateVerificationToken()) + v1 := app.Group("/api/v1", middleware.AuthMiddleware(config.ChallengeWindow)) + v1.Post("/token", limiter.New(idLimiterConfig), limiter.New(ipLimiterConfig), handler.GetorCreateVerificationToken()) v1.Get("/data", handler.GetVerificationData()) v1.Get("/status", handler.GetVerificationStatus()) From f5c44f38af7f8cf7d6d2a1e69aab6d171fe3cc7c Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 21 Oct 2024 15:00:33 +0300 Subject: [PATCH 014/105] make idenfy dev mode configurbale --- .app.env.example | 3 ++- internal/clients/idenfy/idenfy.go | 24 ++++++++++++++++++------ internal/configs/config.go | 5 +++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.app.env.example b/.app.env.example index 3d81337..705b715 100644 --- a/.app.env.example +++ b/.app.env.example @@ -10,8 +10,9 @@ IDENFY_API_KEY= IDENFY_API_SECRET= IDENFY_CALLBACK_SIGN_KEY= IDENFY_WHITELISTED_IPS= +IDENFY_DEV_MODE=false TFCHAIN_WS_PROVIDER_URL=wss://tfchain.grid.tf IP_LIMITER_MAX_TOKEN_REQUESTS=5 IP_LIMITER_TOKEN_EXPIRATION=24 ID_LIMITER_MAX_TOKEN_REQUESTS=5 -ID_LIMITER_TOKEN_EXPIRATION=24 \ No newline at end of file +ID_LIMITER_TOKEN_EXPIRATION=24 diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index 7f459a5..ea0c8ab 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -21,6 +21,7 @@ type Idenfy struct { secretKey string baseURL string callbackSignKey []byte + devMode bool } const ( @@ -34,6 +35,7 @@ func New(config configs.IdenfyConfig) *Idenfy { accessKey: config.APIKey, secretKey: config.APISecret, callbackSignKey: []byte(config.CallbackSignKey), + devMode: config.DevMode, } } @@ -52,12 +54,9 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) auth := base64.StdEncoding.EncodeToString([]byte(authStr)) req.Header.Set("Authorization", "Basic "+auth) - jsonBody, err := json.Marshal(map[string]interface{}{ - "clientId": clientID, - "generateDigitString": true, - "expiryTime": 30, - "dummyStatus": "APPROVED", // TODO: remove this after testing - }) + RequestBody := c.createVerificationSessionRequestBody(clientID, c.devMode) + + jsonBody, err := json.Marshal(RequestBody) if err != nil { return models.Token{}, fmt.Errorf("error marshaling request body: %w", err) } @@ -107,3 +106,16 @@ func (c *Idenfy) VerifyCallbackSignature(ctx context.Context, body []byte, sigHe return nil } + +// function to create a request body for the verification session +func (c *Idenfy) createVerificationSessionRequestBody(clientID string, devMode bool) map[string]interface{} { + RequestBody := map[string]interface{}{ + "clientId": clientID, + "generateDigitString": true, + } + if devMode { + RequestBody["expiryTime"] = 30 + RequestBody["dummyStatus"] = "APPROVED" + } + return RequestBody +} diff --git a/internal/configs/config.go b/internal/configs/config.go index bb7f7f8..c80a5c1 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -33,6 +33,7 @@ type IdenfyConfig struct { BaseURL string `mapstructure:"base_url"` CallbackSignKey string `mapstructure:"callback_sign_key"` WhitelistedIPs []string `mapstructure:"whitelisted_ips,omitempty"` + DevMode bool `mapstructure:"dev_mode"` } type TFChainConfig struct { @@ -131,6 +132,10 @@ func LoadConfig() (*Config, error) { if err != nil { return nil, fmt.Errorf("error binding env variable: %w", err) } + err = viper.BindEnv("idenfy.dev_mode") + if err != nil { + return nil, fmt.Errorf("error binding env variable: %w", err) + } // Set default values // viper.SetDefault("port", "8080") From 1b229dc4ac29e4ed2d83c60c2705a50faf692d0f Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 21 Oct 2024 16:05:34 +0300 Subject: [PATCH 015/105] implement a custom IP extractor to handles forwarded headers --- internal/server/server.go | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/internal/server/server.go b/internal/server/server.go index ad9cb2b..af44d98 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,8 +3,10 @@ package server import ( "fmt" "log" + "net" "os" "os/signal" + "strings" "syscall" "time" @@ -51,11 +53,43 @@ func New(config *configs.Config) *Server { Next: func(c *fiber.Ctx) bool { return c.IP() == "127.0.0.1" }, + KeyGenerator: func(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 { + // return the first non-private ip in the list + for _, ip := range ips { + if net.ParseIP(strings.TrimSpace(ip)) != nil && !net.ParseIP(strings.TrimSpace(ip)).IsPrivate() { + return strings.TrimSpace(ip) + } + } + } + } + + // Check for X-Real-IP header if not a private IP + if ip := c.Get("X-Real-IP"); ip != "" { + if net.ParseIP(strings.TrimSpace(ip)) != nil && !net.ParseIP(strings.TrimSpace(ip)).IsPrivate() { + return strings.TrimSpace(ip) + } + } + + // Fall back to RemoteIP() if no proxy headers are present + ip := c.IP() + if parsedIP := net.ParseIP(ip); parsedIP != nil { + if !parsedIP.IsPrivate() { + return ip + } + } + + // If we still have a private IP, return a default value that will be skipped by the limiter + return "127.0.0.1" + }, } idLimiterStore := mongodb.New(mongodb.Config{ ConnectionURI: config.MongoURI, Database: config.DatabaseName, - Collection: "client_limit", + Collection: "id_limit", Reset: false, }) From 81e7d18af7cdc0b6a84fe1066dedfdbe38a90767 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 21 Oct 2024 16:51:34 +0300 Subject: [PATCH 016/105] update VerificationStatusResponse to use enum for status field --- api/docs/docs.go | 13 ++++++++++++- api/docs/swagger.json | 13 ++++++++++++- api/docs/swagger.yaml | 10 +++++++++- internal/responses/responses.go | 22 +++++++++++++++++----- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 63cc0cd..dbf6de8 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -218,6 +218,17 @@ const docTemplate = `{ } }, "definitions": { + "responses.Outcome": { + "type": "string", + "enum": [ + "VERIFIED", + "REJECTED" + ], + "x-enum-varnames": [ + "OutcomeVerified", + "OutcomeRejected" + ] + }, "responses.TokenResponse": { "type": "object", "properties": { @@ -381,7 +392,7 @@ const docTemplate = `{ "type": "string" }, "status": { - "type": "string" + "$ref": "#/definitions/responses.Outcome" } } } diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 2764709..3afe5c0 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -211,6 +211,17 @@ } }, "definitions": { + "responses.Outcome": { + "type": "string", + "enum": [ + "VERIFIED", + "REJECTED" + ], + "x-enum-varnames": [ + "OutcomeVerified", + "OutcomeRejected" + ] + }, "responses.TokenResponse": { "type": "object", "properties": { @@ -374,7 +385,7 @@ "type": "string" }, "status": { - "type": "string" + "$ref": "#/definitions/responses.Outcome" } } } diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index aa5f2ff..bdc7ca4 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -1,5 +1,13 @@ basePath: / definitions: + responses.Outcome: + enum: + - VERIFIED + - REJECTED + type: string + x-enum-varnames: + - OutcomeVerified + - OutcomeRejected responses.TokenResponse: properties: authToken: @@ -109,7 +117,7 @@ definitions: idenfyRef: type: string status: - type: string + $ref: '#/definitions/responses.Outcome' type: object info: contact: diff --git a/internal/responses/responses.go b/internal/responses/responses.go index dc90da5..458aa31 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -31,10 +31,10 @@ type TokenResponse struct { } type VerificationStatusResponse struct { - Final bool `json:"final"` - IdenfyRef string `json:"idenfyRef"` - ClientID string `json:"clientId"` - Status string `json:"status"` + Final bool `json:"final"` + IdenfyRef string `json:"idenfyRef"` + ClientID string `json:"clientId"` + Status Outcome `json:"status"` } type VerificationDataResponse struct { @@ -96,7 +96,19 @@ func NewTokenResponseWithStatus(token *models.Token, isNewToken bool) *TokenResp } } +// Outcome enum can be VERIFIED or REJECTED +type Outcome string + +const ( + OutcomeVerified Outcome = "VERIFIED" + OutcomeRejected Outcome = "REJECTED" +) + func NewVerificationStatusResponse(verificationOutcome *models.VerificationOutcome) *VerificationStatusResponse { + outcome := OutcomeVerified + if verificationOutcome.Outcome == "REJECTED" { + outcome = OutcomeRejected + } return &VerificationStatusResponse{ /* FraudTags: verification.Status.FraudTags, MismatchTags: verification.Status.MismatchTags, @@ -107,7 +119,7 @@ func NewVerificationStatusResponse(verificationOutcome *models.VerificationOutco Final: verificationOutcome.Final, IdenfyRef: verificationOutcome.IdenfyRef, ClientID: verificationOutcome.ClientID, - Status: verificationOutcome.Outcome, + Status: outcome, } } From 14ad7f3642ee7e1171224a8d1b1c54e7148f52aa Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 21 Oct 2024 16:53:42 +0300 Subject: [PATCH 017/105] Allow configure the limiter exp in minutes instaed of hours --- internal/server/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index af44d98..6b5ccb1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -45,7 +45,7 @@ func New(config *configs.Config) *Server { }) ipLimiterConfig := limiter.Config{ // TODO: use configurable parameters, also check if it works well after passing the request through an SSL gateway Max: config.IPLimiter.MaxTokenRequests, - Expiration: time.Duration(config.IPLimiter.TokenExpiration) * time.Hour, + Expiration: time.Duration(config.IPLimiter.TokenExpiration) * time.Minute, SkipFailedRequests: false, SkipSuccessfulRequests: false, Store: ipLimiterstore, @@ -95,7 +95,7 @@ func New(config *configs.Config) *Server { idLimiterConfig := limiter.Config{ // TODO: use configurable parameters Max: config.IDLimiter.MaxTokenRequests, - Expiration: time.Duration(config.IDLimiter.TokenExpiration) * time.Hour, + Expiration: time.Duration(config.IDLimiter.TokenExpiration) * time.Minute, SkipFailedRequests: false, SkipSuccessfulRequests: false, Store: idLimiterStore, From aa038ac9076b2c7d47d770b5d0887c02d2d4b3b7 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 24 Oct 2024 17:32:00 +0300 Subject: [PATCH 018/105] Update status endpoint --- api/docs/docs.go | 22 +++----- api/docs/swagger.json | 22 +++----- api/docs/swagger.yaml | 20 +++---- go.mod | 10 +++- go.sum | 38 +++++++++---- internal/clients/substrate/substrate.go | 57 +++++++++----------- internal/handlers/handlers.go | 21 ++++++-- internal/server/server.go | 7 +-- internal/services/kyc_service.go | 12 ++++- internal/services/services.go | 1 + scripts/dev/balance/check-account-balance.go | 2 +- scripts/dev/twin/get-address-by-twin-id.go | 26 +++++++++ 12 files changed, 138 insertions(+), 100 deletions(-) create mode 100644 scripts/dev/twin/get-address-by-twin-id.go diff --git a/api/docs/docs.go b/api/docs/docs.go index dbf6de8..a123206 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -89,25 +89,15 @@ const docTemplate = `{ "minLength": 48, "type": "string", "description": "TFChain SS58Address", - "name": "X-Client-ID", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "hex-encoded message ` + "`" + `{api-domain}:{timestamp}` + "`" + `", - "name": "X-Challenge", - "in": "header", - "required": true + "name": "client_id", + "in": "query" }, { - "maxLength": 128, - "minLength": 128, + "minLength": 1, "type": "string", - "description": "hex-encoded sr25519|ed25519 signature", - "name": "X-Signature", - "in": "header", - "required": true + "description": "Twin ID", + "name": "twin_id", + "in": "query" } ], "responses": { diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 3afe5c0..601a76c 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -82,25 +82,15 @@ "minLength": 48, "type": "string", "description": "TFChain SS58Address", - "name": "X-Client-ID", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "hex-encoded message `{api-domain}:{timestamp}`", - "name": "X-Challenge", - "in": "header", - "required": true + "name": "client_id", + "in": "query" }, { - "maxLength": 128, - "minLength": 128, + "minLength": 1, "type": "string", - "description": "hex-encoded sr25519|ed25519 signature", - "name": "X-Signature", - "in": "header", - "required": true + "description": "Twin ID", + "name": "twin_id", + "in": "query" } ], "responses": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index bdc7ca4..8f5c774 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -171,23 +171,15 @@ paths: description: Returns the verification status for a client parameters: - description: TFChain SS58Address - in: header + in: query maxLength: 48 minLength: 48 - name: X-Client-ID - required: true - type: string - - description: hex-encoded message `{api-domain}:{timestamp}` - in: header - name: X-Challenge - required: true + name: client_id type: string - - description: hex-encoded sr25519|ed25519 signature - in: header - maxLength: 128 - minLength: 128 - name: X-Signature - required: true + - description: Twin ID + in: query + minLength: 1 + name: twin_id type: string produces: - application/json diff --git a/go.mod b/go.mod index a085d63..d7ab9ee 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module example.com/tfgrid-kyc-service go 1.22.1 require ( - github.com/centrifuge/go-substrate-rpc-client/v4 v4.2.1 github.com/gofiber/fiber/v2 v2.52.5 github.com/gofiber/storage/mongodb v1.3.9 github.com/gofiber/swagger v1.1.0 github.com/spf13/viper v1.19.0 github.com/swaggo/swag v1.16.3 + github.com/threefoldtech/tfchain/clients/tfchain-client-go v0.0.0-20241007205731-5e76664a3cc4 github.com/valyala/fasthttp v1.51.0 github.com/vedhavyas/go-subkey/v2 v2.0.0 go.mongodb.org/mongo-driver v1.17.1 @@ -20,6 +20,8 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/andybalholm/brotli v1.0.5 // indirect + github.com/cenkalti/backoff v2.2.1+incompatible // indirect + github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.12 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/deckarep/golang-set v1.8.0 // indirect github.com/decred/base58 v1.0.4 // indirect @@ -37,6 +39,7 @@ require ( github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jbenet/go-base58 v0.0.0-20150317085156-6237cf65f3a6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.2 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -50,8 +53,10 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pierrec/xxHash v0.1.5 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/rs/cors v1.8.2 // indirect + github.com/rs/zerolog v1.33.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -63,6 +68,7 @@ require ( github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + github.com/vedhavyas/go-subkey v1.0.3 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect @@ -82,4 +88,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace example.com/tfgrid-kyc-service => . +replace example.com/tfgrid-kyc-service => ./ diff --git a/go.sum b/go.sum index 8b50f25..3a343a6 100644 --- a/go.sum +++ b/go.sum @@ -6,16 +6,20 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= -github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/btcsuite/btcd v0.22.0-beta h1:LTDpDKUM5EeOFBPM8IXpinEcmZ6FWfNZbE3lfrfdnWo= github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= -github.com/centrifuge/go-substrate-rpc-client/v4 v4.2.1 h1:io49TJ8IOIlzipioJc9pJlrjgdJvqktpUWYxVY5AUjE= -github.com/centrifuge/go-substrate-rpc-client/v4 v4.2.1/go.mod h1:k61SBXqYmnZO4frAJyH3iuqjolYrYsq79r8EstmklDY= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.12 h1:DCYWIBOalB0mKKfUg2HhtGgIkBbMA1fnlnkZp7fHB18= +github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.12/go.mod h1:5g1oM4Zu3BOaLpsKQ+O8PAv2kNuq+kPcA1VzFbsSqxE= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -37,8 +41,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= -github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -51,6 +55,7 @@ github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyr github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofiber/storage/mongodb v1.3.9 h1:uoFHBuGLWjlNsYFsMZkGxfzpryIq64mfGAUox8WRLkc= @@ -75,6 +80,8 @@ github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uM github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/jbenet/go-base58 v0.0.0-20150317085156-6237cf65f3a6 h1:4zOlv2my+vf98jT1nQt4bT/yKWUImevYPJ2H344CloE= +github.com/jbenet/go-base58 v0.0.0-20150317085156-6237cf65f3a6/go.mod h1:r/8JmuR0qjuCiEhAolkfvdZgmPiHTnJaG0UXCSeR1Zo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= @@ -95,6 +102,7 @@ github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= @@ -106,7 +114,6 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= @@ -114,6 +121,8 @@ github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pierrec/xxHash v0.1.5 h1:n/jBpwTHiER4xYvK3/CdPVnLDPchj8eTJFFLUb4QHBo= github.com/pierrec/xxHash v0.1.5/go.mod h1:w2waW5Zoa/Wc4Yqe0wgrIYAGKqRMf7czn2HNKXmuL+I= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -123,6 +132,9 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -156,10 +168,12 @@ github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/threefoldtech/tfchain/clients/tfchain-client-go v0.0.0-20241007205731-5e76664a3cc4 h1:XIXVdFrum50Wnxv62sS+cEgqHtvdInWB2Co8AJVJ8xs= +github.com/threefoldtech/tfchain/clients/tfchain-client-go v0.0.0-20241007205731-5e76664a3cc4/go.mod h1:cOL5YgHUmDG5SAXrsZxFjUECRQQuAqOoqvXhZG5sEUw= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= -github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= -github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo= +github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -168,6 +182,8 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vedhavyas/go-subkey v1.0.3 h1:iKR33BB/akKmcR2PMlXPBeeODjWLM90EL98OrOGs8CA= +github.com/vedhavyas/go-subkey v1.0.3/go.mod h1:CloUaFQSSTdWnINfBRFjVMkWXZANW+nd8+TI5jYcl6Y= github.com/vedhavyas/go-subkey/v2 v2.0.0 h1:LemDIsrVtRSOkp0FA8HxP6ynfKjeOj3BY2U9UNfeDMA= github.com/vedhavyas/go-subkey/v2 v2.0.0/go.mod h1:95aZ+XDCWAUUynjlmi7BtPExjXgXxByE0WfBwbmIRH4= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= @@ -219,6 +235,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -241,8 +258,9 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index a010bb3..b1c86ff 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -2,30 +2,29 @@ package substrate import ( "fmt" + "math/big" + "strconv" "sync" "example.com/tfgrid-kyc-service/internal/configs" - gsrpc "github.com/centrifuge/go-substrate-rpc-client/v4" - "github.com/centrifuge/go-substrate-rpc-client/v4/types" - "github.com/vedhavyas/go-subkey/v2" + + // use tfchain go client + + tfchain "github.com/threefoldtech/tfchain/clients/tfchain-client-go" ) type Substrate struct { - api *gsrpc.SubstrateAPI + api *tfchain.Substrate mu sync.Mutex // TODO: Check if SubstrateAPI is thread safe } func New(config configs.TFChainConfig) (*Substrate, error) { - api, err := gsrpc.NewSubstrateAPI(config.WsProviderURL) + mgr := tfchain.NewManager(config.WsProviderURL) + api, err := mgr.Substrate() if err != nil { return nil, fmt.Errorf("substrate connection error: failed to initialize Substrate client: %w", err) } - chain, _ := api.RPC.System.Chain() - nodeName, _ := api.RPC.System.Name() - nodeVersion, _ := api.RPC.System.Version() - fmt.Println("conected to chain:", chain, "| nodeName:", nodeName, "| nodeVersion:", nodeVersion) - c := &Substrate{ api: api, mu: sync.Mutex{}, @@ -33,34 +32,28 @@ func New(config configs.TFChainConfig) (*Substrate, error) { return c, nil } -func (c *Substrate) GetAccountBalance(address string) (uint64, error) { - _, pubkeyBytes, err := subkey.SS58Decode(address) +func (c *Substrate) GetAccountBalance(address string) (*big.Int, error) { + pubkeyBytes, err := tfchain.FromAddress(address) if err != nil { - return 0, fmt.Errorf("failed to decode ss58 address: %w", err) + return nil, fmt.Errorf("failed to decode ss58 address: %w", err) } - account, err := types.NewAddressFromAccountID(pubkeyBytes) + accountID := tfchain.AccountID(pubkeyBytes) + balance, err := c.api.GetBalance(accountID) if err != nil { - return 0, fmt.Errorf("failed to create AccountID: %w", err) - } - meta, err := c.api.RPC.State.GetMetadataLatest() - if err != nil { - return 0, fmt.Errorf("failed to get metadata: %w", err) - } - // Create a storage key for the account's balance - key, err := types.CreateStorageKey(meta, "System", "Account", account.AsAccountID.ToBytes()) - if err != nil { - return 0, fmt.Errorf("failed to create storage key: %w", err) + return nil, fmt.Errorf("failed to get balance: %w", err) } - // Query the storage - var accountInfo types.AccountInfo - ok, err := c.api.RPC.State.GetStorageLatest(key, &accountInfo) + return balance.Free.Int, nil +} + +func (c *Substrate) GetAddressByTwinID(twinID string) (string, error) { + twinIDUint32, err := strconv.ParseUint(twinID, 10, 32) if err != nil { - return 0, fmt.Errorf("failed to get storage: %w", err) + return "", fmt.Errorf("failed to parse twin ID: %w", err) } - if !ok { - return 0, nil // account not found + twin, err := c.api.GetTwin(uint32(twinIDUint32)) + if err != nil { + return "", fmt.Errorf("failed to get twin: %w", err) } - - return accountInfo.Data.Free.Uint64(), nil + return twin.Account.String(), nil } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 30f451b..815b6e2 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -77,15 +77,26 @@ func (h *Handler) GetVerificationData() fiber.Handler { // @Tags Verification // @Accept json // @Produce json -// @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) +// @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 // @Router /api/v1/status [get] func (h *Handler) GetVerificationStatus() fiber.Handler { return func(c *fiber.Ctx) error { - clientID := c.Get("X-Client-ID") - verification, err := h.kycService.GetVerificationStatus(c.Context(), clientID) + clientID := c.Query("client_id") + twinID := c.Query("twin_id") + + if clientID == "" && twinID == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Either client_id or twin_id must be provided"}) + } + var verification *models.VerificationOutcome + var err error + + if clientID != "" { + verification, err = h.kycService.GetVerificationStatus(c.Context(), clientID) + } else { + verification, err = h.kycService.GetVerificationStatusByTwinID(c.Context(), twinID) + } if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } diff --git a/internal/server/server.go b/internal/server/server.go index 6b5ccb1..6e1823f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -144,9 +144,10 @@ func New(config *configs.Config) *Server { // Routes app.Get("/docs/*", swagger.HandlerDefault) - v1 := app.Group("/api/v1", middleware.AuthMiddleware(config.ChallengeWindow)) - v1.Post("/token", limiter.New(idLimiterConfig), limiter.New(ipLimiterConfig), handler.GetorCreateVerificationToken()) - v1.Get("/data", handler.GetVerificationData()) + v1 := app.Group("/api/v1") + v1.Post("/token", middleware.AuthMiddleware(config.ChallengeWindow), limiter.New(idLimiterConfig), limiter.New(ipLimiterConfig), handler.GetorCreateVerificationToken()) + v1.Get("/data", middleware.AuthMiddleware(config.ChallengeWindow), handler.GetVerificationData()) + // status route accepts either client_id or twin_id as query parameters v1.Get("/status", handler.GetVerificationStatus()) // Webhook routes diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index 8ec4a1e..1b497d1 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math/big" "example.com/tfgrid-kyc-service/internal/clients/idenfy" "example.com/tfgrid-kyc-service/internal/clients/substrate" @@ -78,7 +79,7 @@ func (s *kycService) AccountHasRequiredBalance(ctx context.Context, address stri if err != nil { return false, err } - return balance >= s.config.MinBalanceToVerifyAccount, nil + return balance.Cmp(big.NewInt(int64(s.config.MinBalanceToVerifyAccount))) >= 0, nil } // --------------------------------------------------------------------------------------------------------------------- @@ -116,6 +117,15 @@ func (s *kycService) GetVerificationStatus(ctx context.Context, clientID string) }, nil } +func (s *kycService) GetVerificationStatusByTwinID(ctx context.Context, twinID string) (*models.VerificationOutcome, error) { + // get the address from the twinID + address, err := s.substrate.GetAddressByTwinID(twinID) + if err != nil { + return nil, 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 { diff --git a/internal/services/services.go b/internal/services/services.go index 6290315..09990a9 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -12,6 +12,7 @@ type KYCService interface { AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) GetVerification(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/scripts/dev/balance/check-account-balance.go b/scripts/dev/balance/check-account-balance.go index 4a0ea66..63d23b7 100644 --- a/scripts/dev/balance/check-account-balance.go +++ b/scripts/dev/balance/check-account-balance.go @@ -21,5 +21,5 @@ func main() { if err != nil { panic(err) } - fmt.Println("balance: ", free_balance) + fmt.Println(free_balance) } diff --git a/scripts/dev/twin/get-address-by-twin-id.go b/scripts/dev/twin/get-address-by-twin-id.go new file mode 100644 index 0000000..c5f2a7c --- /dev/null +++ b/scripts/dev/twin/get-address-by-twin-id.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + + "example.com/tfgrid-kyc-service/internal/clients/substrate" + "example.com/tfgrid-kyc-service/internal/configs" +) + +func main() { + config, err := configs.LoadConfig() + if err != nil { + panic(err) + } + substrateClient, err := substrate.New(config.TFChain) + if err != nil { + panic(err) + } + + address, err := substrateClient.GetAddressByTwinID("41") + if err != nil { + panic(err) + } + fmt.Println(address) + +} From d59048d51f14b556dc017cc56942a55b25fb5483 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Sun, 27 Oct 2024 14:43:11 +0300 Subject: [PATCH 019/105] refactor config --- go.mod | 25 ++-- go.sum | 50 ++----- internal/clients/idenfy/idenfy.go | 2 +- internal/clients/substrate/substrate.go | 2 +- internal/configs/config.go | 182 ++++++------------------ internal/server/server.go | 14 +- internal/services/kyc_service.go | 4 +- 7 files changed, 68 insertions(+), 211 deletions(-) diff --git a/go.mod b/go.mod index d7ab9ee..016d41c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/gofiber/fiber/v2 v2.52.5 github.com/gofiber/storage/mongodb v1.3.9 github.com/gofiber/swagger v1.1.0 - github.com/spf13/viper v1.19.0 + github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/swaggo/swag v1.16.3 github.com/threefoldtech/tfchain/clients/tfchain-client-go v0.0.0-20241007205731-5e76664a3cc4 github.com/valyala/fasthttp v1.51.0 @@ -15,6 +15,7 @@ require ( ) require ( + github.com/BurntSushi/toml v1.2.1 // indirect github.com/ChainSafe/go-schnorrkel v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect @@ -23,11 +24,11 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.12 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set v1.8.0 // indirect github.com/decred/base58 v1.0.4 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/ethereum/go-ethereum v1.10.20 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/spec v0.20.4 // indirect @@ -38,32 +39,25 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/jbenet/go-base58 v0.0.0-20150317085156-6237cf65f3a6 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.2 // indirect - github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/montanaflynn/stats v0.7.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pierrec/xxHash v0.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/rs/cors v1.8.2 // indirect github.com/rs/zerolog v1.33.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.6.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect @@ -73,19 +67,16 @@ 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/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.26.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) replace example.com/tfgrid-kyc-service => ./ diff --git a/go.sum b/go.sum index 3a343a6..9eebf8b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/ChainSafe/go-schnorrkel v1.1.0 h1:rZ6EU+CZFCjB4sHUE1jIu8VDoB/wRKZxoe1tkcO71Wk= github.com/ChainSafe/go-schnorrkel v1.1.0/go.mod h1:ABkENxiP+cvjFiByMIZ9LYbRoNNLeBLiakC1XeTFxfE= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -37,10 +39,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/ethereum/go-ethereum v1.10.20 h1:75IW830ClSS40yrQC1ZCMZCt5I+zU16oqId2SiQwdQ4= github.com/ethereum/go-ethereum v1.10.20/go.mod h1:LWUN82TCHGpxB3En5HVmLLzPD7YSrEUFmFfN1nKkVN0= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -78,10 +76,12 @@ github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= +github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= github.com/jbenet/go-base58 v0.0.0-20150317085156-6237cf65f3a6 h1:4zOlv2my+vf98jT1nQt4bT/yKWUImevYPJ2H344CloE= github.com/jbenet/go-base58 v0.0.0-20150317085156-6237cf65f3a6/go.mod h1:r/8JmuR0qjuCiEhAolkfvdZgmPiHTnJaG0UXCSeR1Zo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= @@ -93,8 +93,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= @@ -110,13 +108,9 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b h1:QrHweqAtyJ9EwCaGHBu1fghwxIPiopAHV06JlXrMHjk= github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b/go.mod h1:xxLb2ip6sSUts3g1irPVHyk/DGslwQsNOo9I7smJfNU= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pierrec/xxHash v0.1.5 h1:n/jBpwTHiER4xYvK3/CdPVnLDPchj8eTJFFLUb4QHBo= @@ -135,35 +129,13 @@ github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= @@ -197,17 +169,11 @@ 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/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 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= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= @@ -261,8 +227,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -272,3 +236,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index ea0c8ab..d576c51 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -28,7 +28,7 @@ const ( VerificationSessionEndpoint = "/api/v2/token" ) -func New(config configs.IdenfyConfig) *Idenfy { +func New(config configs.Idenfy) *Idenfy { return &Idenfy{ baseURL: config.BaseURL, client: &fasthttp.Client{}, diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index b1c86ff..5064661 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -18,7 +18,7 @@ type Substrate struct { mu sync.Mutex // TODO: Check if SubstrateAPI is thread safe } -func New(config configs.TFChainConfig) (*Substrate, error) { +func New(config configs.TFChain) (*Substrate, error) { mgr := tfchain.NewManager(config.WsProviderURL) api, err := mgr.Substrate() if err != nil { diff --git a/internal/configs/config.go b/internal/configs/config.go index c80a5c1..8563f27 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -2,159 +2,59 @@ package configs import ( "fmt" - "strings" - "github.com/spf13/viper" + "github.com/ilyakaznacheev/cleanenv" ) type Config struct { - // DB - MongoURI string `mapstructure:"mongo_uri"` - DatabaseName string `mapstructure:"database_name"` - // Server - Port string `mapstructure:"port"` - // Idenfy - Idenfy IdenfyConfig `mapstructure:"idenfy"` - // TFChain - TFChain TFChainConfig `mapstructure:"tfchain"` - // IP limiter - IPLimiter LimiterConfig `mapstructure:"ip_limiter"` - // Client limiter - IDLimiter LimiterConfig `mapstructure:"id_limiter"` - // Verification - Verification VerificationConfig `mapstructure:"verification"` - // Other - ChallengeWindow int64 `mapstructure:"challenge_window"` + MongoDB MongoDB + Server Server + Idenfy Idenfy + TFChain TFChain + Verification Verification + IPLimiter IPLimiter + IDLimiter IDLimiter + ChallengeWindow int64 `env:"CHALLENGE_WINDOW" env-default:"8"` } -type IdenfyConfig struct { - APIKey string `mapstructure:"api_key"` - APISecret string `mapstructure:"api_secret"` - BaseURL string `mapstructure:"base_url"` - CallbackSignKey string `mapstructure:"callback_sign_key"` - WhitelistedIPs []string `mapstructure:"whitelisted_ips,omitempty"` - DevMode bool `mapstructure:"dev_mode"` +type MongoDB struct { + URI string `env:"MONGO_URI" env-default:"mongodb://localhost:27017"` + DatabaseName string `env:"DATABASE_NAME" env-default:"tfgrid-kyc-db"` } - -type TFChainConfig struct { - WsProviderURL string `mapstructure:"ws_provider_url"` +type Server struct { + Port string `env:"PORT" env-default:"8080"` } - -type VerificationConfig struct { - SuspiciousVerificationOutcome string `mapstructure:"suspicious_verification_outcome"` - ExpiredDocumentOutcome string `mapstructure:"expired_document_outcome"` - MinBalanceToVerifyAccount uint64 `mapstructure:"min_balance_to_verify_account"` +type Idenfy struct { + APIKey string `env:"IDENFY_API_KEY" env-required:"true"` + APISecret string `env:"IDENFY_API_SECRET" env-required:"true"` + BaseURL string `env:"IDENFY_BASE_URL" env-default:"https://ivs.idenfy.com"` + CallbackSignKey string `env:"IDENFY_CALLBACK_SIGN_KEY" env-required:"true"` + WhitelistedIPs []string `env:"IDENFY_WHITELISTED_IPS" env-separator:","` + DevMode bool `env:"IDENFY_DEV_MODE" env-default:"false"` } - -type LimiterConfig struct { - MaxTokenRequests int `mapstructure:"max_token_requests"` - TokenExpiration int `mapstructure:"token_expiration"` +type TFChain struct { + WsProviderURL string `env:"TFCHAIN_WS_PROVIDER_URL" env-default:"wss://tfchain.grid.tf"` +} +type Verification struct { + SuspiciousVerificationOutcome string `env:"SUSPICIOUS_VERIFICATION_OUTCOME" env-default:"verified"` + ExpiredDocumentOutcome string `env:"EXPIRED_DOCUMENT_OUTCOME" env-default:"unverified"` + MinBalanceToVerifyAccount uint64 `env:"MIN_BALANCE_TO_VERIFY_ACCOUNT" env-default:"10000000"` +} +type IPLimiter struct { + MaxTokenRequests int `env:"IP_LIMITER_MAX_TOKEN_REQUESTS" env-default:"4"` + TokenExpiration int `env:"IP_LIMITER_TOKEN_EXPIRATION" env-default:"24"` +} +type IDLimiter struct { + MaxTokenRequests int `env:"ID_LIMITER_MAX_TOKEN_REQUESTS" env-default:"4"` + TokenExpiration int `env:"ID_LIMITER_TOKEN_EXPIRATION" env-default:"24"` } func LoadConfig() (*Config, error) { - // replacer - - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - viper.AutomaticEnv() - err := viper.BindEnv("mongo_uri") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("database_name") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("port") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("idenfy.api_key") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("idenfy.api_secret") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("idenfy.base_url") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("idenfy.callback_sign_key") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("idenfy.whitelisted_ips") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("tfchain.ws_provider_url") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("suspicious_verification_outcome") + cfg := &Config{} + err := cleanenv.ReadEnv(cfg) if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) + return nil, fmt.Errorf("error loading config: %w", err) } - err = viper.BindEnv("expired_document_outcome") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("challenge_window") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("verification.min_balance_to_verify_account") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("verification.suspicious_verification_outcome") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("verification.expired_document_outcome") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("ip_limiter.max_token_requests") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("ip_limiter.token_expiration") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("id_limiter.max_token_requests") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("id_limiter.token_expiration") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - err = viper.BindEnv("idenfy.dev_mode") - if err != nil { - return nil, fmt.Errorf("error binding env variable: %w", err) - } - - // Set default values - // viper.SetDefault("port", "8080") - // viper.SetDefault("max_token_requests_per_minute", 4) - // viper.SetDefault("suspicious_verification_outcome", "verified") - // viper.SetDefault("expired_document_outcome", "unverified") - // viper.SetDefault("mongo_uri", "mongodb://localhost:27017") - // viper.SetDefault("database_name", "tfgrid-kyc-db") - // viper.SetDefault("idenfy.base_url", "https://ivs.idenfy.com") - // viper.SetDefault("tfchain.ws_provider_url", "wss://tfchain.grid.tf") - // viper.SetDefault("min_balance_to_verify_account", 10000000) - // viper.SetDefault("challenge_window", 120) - - var config Config - err = viper.Unmarshal(&config) - if err != nil { - return nil, fmt.Errorf("unable to decode into struct: %w", err) - } - - fmt.Printf("%+v\n", config) - return &config, nil + fmt.Printf("%+v\n", cfg) + return cfg, nil } diff --git a/internal/server/server.go b/internal/server/server.go index 6e1823f..0de5420 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -38,8 +38,8 @@ func New(config *configs.Config) *Server { // Setup Limter Config and store ipLimiterstore := mongodb.New(mongodb.Config{ - ConnectionURI: config.MongoURI, - Database: config.DatabaseName, + ConnectionURI: config.MongoDB.URI, + Database: config.MongoDB.DatabaseName, Collection: "ip_limit", Reset: false, }) @@ -87,8 +87,8 @@ func New(config *configs.Config) *Server { }, } idLimiterStore := mongodb.New(mongodb.Config{ - ConnectionURI: config.MongoURI, - Database: config.DatabaseName, + ConnectionURI: config.MongoDB.URI, + Database: config.MongoDB.DatabaseName, Collection: "id_limit", Reset: false, }) @@ -115,11 +115,11 @@ func New(config *configs.Config) *Server { app.Use(helmet.New()) // Database connection - db, err := repository.ConnectToMongoDB(config.MongoURI) + db, err := repository.ConnectToMongoDB(config.MongoDB.URI) if err != nil { log.Fatalf("Failed to connect to MongoDB: %v", err) } - database := db.Database(config.DatabaseName) + database := db.Database(config.MongoDB.DatabaseName) // Initialize repositories tokenRepo := repository.NewMongoTokenRepository(database) @@ -161,7 +161,7 @@ func New(config *configs.Config) *Server { func (s *Server) Start() { // Start server go func() { - if err := s.app.Listen(":" + s.config.Port); err != nil { + if err := s.app.Listen(":" + s.config.Server.Port); err != nil { log.Fatalf("Failed to start server: %v", err) } }() diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index 1b497d1..2e7dc9a 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -18,10 +18,10 @@ type kycService struct { tokenRepo repository.TokenRepository idenfy *idenfy.Idenfy substrate *substrate.Substrate - config *configs.VerificationConfig + config *configs.Verification } -func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.VerificationConfig) KYCService { +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.Verification) KYCService { return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: config} } From 9adf3c0d4a9bed4d34833a6b022b31b658e7cc68 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Sun, 27 Oct 2024 17:39:04 +0300 Subject: [PATCH 020/105] Implement logger package --- .app.env.example | 1 + cmd/api/main.go | 10 +++- go.mod | 2 + go.sum | 6 +++ internal/clients/idenfy/idenfy.go | 15 +++--- internal/clients/substrate/substrate.go | 13 +++-- internal/configs/config.go | 5 +- internal/handlers/handlers.go | 34 ++++++++++--- internal/logger/logger.go | 36 ++++++++++++++ internal/middleware/middleware.go | 2 - internal/repository/token_repository.go | 17 +++---- .../repository/verification_repository.go | 9 ++-- internal/server/server.go | 49 +++++++++---------- internal/services/kyc_service.go | 35 ++++++++----- scripts/dev/balance/check-account-balance.go | 5 +- scripts/dev/twin/get-address-by-twin-id.go | 5 +- 16 files changed, 165 insertions(+), 79 deletions(-) create mode 100644 internal/logger/logger.go diff --git a/.app.env.example b/.app.env.example index 705b715..71e3981 100644 --- a/.app.env.example +++ b/.app.env.example @@ -16,3 +16,4 @@ IP_LIMITER_MAX_TOKEN_REQUESTS=5 IP_LIMITER_TOKEN_EXPIRATION=24 ID_LIMITER_MAX_TOKEN_REQUESTS=5 ID_LIMITER_TOKEN_EXPIRATION=24 +DEBUG=false \ No newline at end of file diff --git a/cmd/api/main.go b/cmd/api/main.go index 46d6122..c10e270 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -5,7 +5,9 @@ import ( _ "example.com/tfgrid-kyc-service/api/docs" "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/server" + "go.uber.org/zap" ) // @title TFGrid KYC API @@ -23,7 +25,13 @@ func main() { log.Fatal("Failed to load configuration:", err) } - server := server.New(config) + logger.Init(config.Log) + logger := logger.GetLogger() + defer logger.Sync() + logger.Debug("Configuration loaded successfully", zap.Any("config", config)) + + server := server.New(config, logger) + logger.Info("Starting server on port:", zap.String("port", config.Server.Port)) server.Start() } diff --git a/go.mod b/go.mod index 016d41c..10b3447 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ 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 ( @@ -67,6 +68,7 @@ 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 9eebf8b..f65c65f 100644 --- a/go.sum +++ b/go.sum @@ -169,6 +169,12 @@ 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 d576c51..9bf6045 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -11,8 +11,10 @@ import ( "fmt" "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" "github.com/valyala/fasthttp" + "go.uber.org/zap" ) type Idenfy struct { @@ -22,13 +24,14 @@ type Idenfy struct { baseURL string callbackSignKey []byte devMode bool + logger *logger.Logger } const ( VerificationSessionEndpoint = "/api/v2/token" ) -func New(config configs.Idenfy) *Idenfy { +func New(config configs.Idenfy, logger *logger.Logger) *Idenfy { return &Idenfy{ baseURL: config.BaseURL, client: &fasthttp.Client{}, @@ -36,6 +39,7 @@ func New(config configs.Idenfy) *Idenfy { secretKey: config.APISecret, callbackSignKey: []byte(config.CallbackSignKey), devMode: config.DevMode, + logger: logger, } } @@ -64,30 +68,27 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) - fmt.Println("request", req) + c.logger.Debug("Preparing iDenfy verification session request", zap.Any("request", req)) err = c.client.Do(req, resp) if err != nil { return models.Token{}, fmt.Errorf("error sending request: %w", err) } if resp.StatusCode() < 200 || resp.StatusCode() >= 300 { - fmt.Println("response", resp) return models.Token{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) } - fmt.Println(string(resp.Body())) + c.logger.Debug("Received response from iDenfy", zap.Any("response", resp)) var result models.Token if err := json.Unmarshal(resp.Body(), &result); err != nil { return models.Token{}, fmt.Errorf("error decoding response: %w", err) } - fmt.Println(result) return result, nil } // verify signature of the callback func (c *Idenfy) VerifyCallbackSignature(ctx context.Context, body []byte, sigHeader string) error { - fmt.Println("start verifying callback signature") if len(c.callbackSignKey) < 1 { return errors.New("callback was received but no signature key was provided") } @@ -102,8 +103,6 @@ func (c *Idenfy) VerifyCallbackSignature(ctx context.Context, body []byte, sigHe if !hmac.Equal(sig, mac.Sum(nil)) { return errors.New("signature verification failed") } - fmt.Println("signature verified") - return nil } diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index 5064661..1c6e35e 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -7,6 +7,7 @@ import ( "sync" "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/logger" // use tfchain go client @@ -14,11 +15,12 @@ import ( ) type Substrate struct { - api *tfchain.Substrate - mu sync.Mutex // TODO: Check if SubstrateAPI is thread safe + api *tfchain.Substrate + mu sync.Mutex // TODO: Check if SubstrateAPI is thread safe + logger *logger.Logger } -func New(config configs.TFChain) (*Substrate, error) { +func New(config configs.TFChain, logger *logger.Logger) (*Substrate, error) { mgr := tfchain.NewManager(config.WsProviderURL) api, err := mgr.Substrate() if err != nil { @@ -26,8 +28,9 @@ func New(config configs.TFChain) (*Substrate, error) { } c := &Substrate{ - api: api, - mu: sync.Mutex{}, + api: api, + mu: sync.Mutex{}, + logger: logger, } return c, nil } diff --git a/internal/configs/config.go b/internal/configs/config.go index 8563f27..418c94a 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -15,6 +15,7 @@ type Config struct { IPLimiter IPLimiter IDLimiter IDLimiter ChallengeWindow int64 `env:"CHALLENGE_WINDOW" env-default:"8"` + Log Log } type MongoDB struct { @@ -48,6 +49,9 @@ type IDLimiter struct { MaxTokenRequests int `env:"ID_LIMITER_MAX_TOKEN_REQUESTS" env-default:"4"` TokenExpiration int `env:"ID_LIMITER_TOKEN_EXPIRATION" env-default:"24"` } +type Log struct { + Debug bool `env:"DEBUG" env-default:"false"` +} func LoadConfig() (*Config, error) { cfg := &Config{} @@ -55,6 +59,5 @@ func LoadConfig() (*Config, error) { if err != nil { return nil, fmt.Errorf("error loading config: %w", err) } - fmt.Printf("%+v\n", cfg) return cfg, nil } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 815b6e2..397edc3 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -3,10 +3,11 @@ package handlers import ( "bytes" "encoding/json" - "fmt" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" + "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" "example.com/tfgrid-kyc-service/internal/responses" "example.com/tfgrid-kyc-service/internal/services" @@ -14,10 +15,11 @@ import ( type Handler struct { kycService services.KYCService + logger *logger.Logger } -func NewHandler(kycService services.KYCService) *Handler { - return &Handler{kycService: kycService} +func NewHandler(kycService services.KYCService, logger *logger.Logger) *Handler { + return &Handler{kycService: kycService, logger: logger} } // @Summary Get or Generate iDenfy Verification Token @@ -83,10 +85,12 @@ func (h *Handler) GetVerificationData() fiber.Handler { // @Router /api/v1/status [get] func (h *Handler) GetVerificationStatus() fiber.Handler { return func(c *fiber.Ctx) error { + h.logger.Debug("GetVerificationStatus request received", zap.Any("query", c.Queries())) clientID := c.Query("client_id") twinID := c.Query("twin_id") if clientID == "" && twinID == "" { + h.logger.Warn("Bad request: missing client_id and twin_id") return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Either client_id or twin_id must be provided"}) } var verification *models.VerificationOutcome @@ -98,11 +102,24 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { verification, err = h.kycService.GetVerificationStatusByTwinID(c.Context(), twinID) } if err != nil { + h.logger.Error("Failed to get verification status", + zap.String("clientID", clientID), + zap.String("twinID", twinID), + zap.Error(err), + ) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } if verification == nil { + h.logger.Info("Verification not found", + zap.String("clientID", clientID), + zap.String("twinID", twinID), + ) return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Verification not found"}) } + h.logger.Info("Verification status retrieved successfully", + zap.String("clientID", clientID), + zap.String("twinID", twinID), + ) response := responses.NewVerificationStatusResponse(verification) return c.JSON(fiber.Map{"result": response}) } @@ -117,8 +134,7 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { // @Router /webhooks/idenfy/verification-update [post] func (h *Handler) ProcessVerificationResult() fiber.Handler { return func(c *fiber.Ctx) error { - fmt.Printf("%+v", c.Body()) - fmt.Printf("%+v", &c.Request().Header) + h.logger.Debug("Received verification update", zap.Any("body", c.Body()), zap.Any("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"}) @@ -128,10 +144,10 @@ func (h *Handler) ProcessVerificationResult() fiber.Handler { decoder := json.NewDecoder(bytes.NewReader(body)) err := decoder.Decode(&result) if err != nil { - fmt.Println(err) + h.logger.Error("Error decoding verification update", zap.Error(err)) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } - fmt.Printf("after decoding: %+v", result) + h.logger.Debug("Verification update after decoding", zap.Any("result", result)) err = h.kycService.ProcessVerificationResult(c.Context(), body, sigHeader, result) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) @@ -149,6 +165,8 @@ func (h *Handler) ProcessVerificationResult() fiber.Handler { // @Router /webhooks/idenfy/id-expiration [post] func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { return func(c *fiber.Ctx) error { - return nil + // TODO: implement + h.logger.Error("Received ID expiration notification but not implemented") + return c.SendStatus(fiber.StatusNotImplemented) } } diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..0b1d7a8 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,36 @@ +package logger + +import ( + "example.com/tfgrid-kyc-service/internal/configs" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Logger struct { + *zap.Logger +} + +var log *Logger + +func Init(config configs.Log) { + debug := config.Debug + zapConfig := zap.NewProductionConfig() + if debug { + zapConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel) + } + zapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + var err error + + zapLog, err := zapConfig.Build() + if err != nil { + panic(err) + } + log = &Logger{zapLog} +} + +func GetLogger() *Logger { + if log == nil { + panic("logger not initialized") + } + return log +} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 575886e..31f226f 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -44,7 +44,6 @@ func AuthMiddleware(challengeWindow int64) fiber.Handler { "error": err.Error(), }) } - fmt.Println("✔️ challenge is valid") // Verify the signature err = VerifySubstrateSignature(clientID, signature, challenge) if err != nil { @@ -53,7 +52,6 @@ func AuthMiddleware(challengeWindow int64) fiber.Handler { }) } - fmt.Println("✔️ signature is valid") return c.Next() } } diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index c31ac2a..ebc854e 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -2,23 +2,26 @@ package repository import ( "context" - "fmt" "time" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" + "go.uber.org/zap" + "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" ) type MongoTokenRepository struct { collection *mongo.Collection + logger *logger.Logger } -func NewMongoTokenRepository(db *mongo.Database) TokenRepository { +func NewMongoTokenRepository(db *mongo.Database, logger *logger.Logger) TokenRepository { repo := &MongoTokenRepository{ collection: db.Collection("tokens"), + logger: logger, } repo.createTTLIndex() return repo @@ -37,7 +40,7 @@ func (r *MongoTokenRepository) createTTLIndex() { ) if err != nil { - fmt.Printf("Error creating TTL index: %v\n", err) + r.logger.Error("Error creating TTL index", zap.Error(err)) } } @@ -45,17 +48,14 @@ func (r *MongoTokenRepository) SaveToken(ctx context.Context, token *models.Toke token.CreatedAt = time.Now() token.ExpiresAt = token.CreatedAt.Add(time.Duration(token.ExpiryTime) * time.Second) _, err := r.collection.InsertOne(ctx, token) - fmt.Println("token saved to db", err) return err } func (r *MongoTokenRepository) GetToken(ctx context.Context, clientID string) (*models.Token, error) { var token models.Token - fmt.Println("clientID from repo", clientID) err := r.collection.FindOne(ctx, bson.M{"clientId": clientID}).Decode(&token) if err != nil { if err == mongo.ErrNoDocuments { - fmt.Println("no document found") return nil, nil } return nil, err @@ -72,9 +72,6 @@ func (r *MongoTokenRepository) GetToken(ctx context.Context, clientID string) (* } func (r *MongoTokenRepository) DeleteToken(ctx context.Context, clientID string, scanRef string) error { - res, err := r.collection.DeleteOne(ctx, bson.M{"clientId": clientID, "scanRef": scanRef}) - if err == nil { - fmt.Println("token deletion succeeded. deleted count: ", res.DeletedCount) - } + _, err := r.collection.DeleteOne(ctx, bson.M{"clientId": clientID, "scanRef": scanRef}) return err } diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go index 3ff37ac..ac7658f 100644 --- a/internal/repository/verification_repository.go +++ b/internal/repository/verification_repository.go @@ -2,9 +2,9 @@ package repository import ( "context" - "fmt" "time" + "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" @@ -13,20 +13,19 @@ import ( type MongoVerificationRepository struct { collection *mongo.Collection + logger *logger.Logger } -func NewMongoVerificationRepository(db *mongo.Database) VerificationRepository { +func NewMongoVerificationRepository(db *mongo.Database, logger *logger.Logger) VerificationRepository { return &MongoVerificationRepository{ collection: db.Collection("verifications"), + logger: logger, } } func (r *MongoVerificationRepository) SaveVerification(ctx context.Context, verification *models.Verification) error { - fmt.Println("start saving verification to the database") verification.CreatedAt = time.Now() _, err := r.collection.InsertOne(ctx, verification) - fmt.Println(err) - fmt.Println("end saving verification to the database") return err } diff --git a/internal/server/server.go b/internal/server/server.go index 0de5420..4e78ee3 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,8 +1,6 @@ package server import ( - "fmt" - "log" "net" "os" "os/signal" @@ -15,6 +13,7 @@ import ( "example.com/tfgrid-kyc-service/internal/clients/substrate" "example.com/tfgrid-kyc-service/internal/configs" "example.com/tfgrid-kyc-service/internal/handlers" + "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/middleware" "example.com/tfgrid-kyc-service/internal/repository" "example.com/tfgrid-kyc-service/internal/services" @@ -24,18 +23,19 @@ import ( "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/storage/mongodb" "github.com/gofiber/swagger" + "go.uber.org/zap" ) // implement server struct that have fiber app and config type Server struct { app *fiber.App config *configs.Config + logger *logger.Logger } -func New(config *configs.Config) *Server { +func New(config *configs.Config, logger *logger.Logger) *Server { // debug log app := fiber.New() - // Setup Limter Config and store ipLimiterstore := mongodb.New(mongodb.Config{ ConnectionURI: config.MongoDB.URI, @@ -48,7 +48,7 @@ func New(config *configs.Config) *Server { Expiration: time.Duration(config.IPLimiter.TokenExpiration) * time.Minute, SkipFailedRequests: false, SkipSuccessfulRequests: false, - Store: ipLimiterstore, + Storage: ipLimiterstore, // skip the limiter for localhost Next: func(c *fiber.Ctx) bool { return c.IP() == "127.0.0.1" @@ -98,15 +98,17 @@ func New(config *configs.Config) *Server { Expiration: time.Duration(config.IDLimiter.TokenExpiration) * time.Minute, SkipFailedRequests: false, SkipSuccessfulRequests: false, - Store: idLimiterStore, + Storage: idLimiterStore, // Use client id as key to limit the number of requests per client KeyGenerator: func(c *fiber.Ctx) string { return c.Get("X-Client-ID") }, } - // print limtters config - fmt.Printf("IP Limiter Config: %+v\n", ipLimiterConfig) - fmt.Printf("ID Limiter Config: %+v\n", idLimiterConfig) + + logger.Info("Limiter configurations", + zap.Any("ipLimiter", ipLimiterConfig), + zap.Any("idLimiter", idLimiterConfig), + ) // Global middlewares app.Use(middleware.Logger()) @@ -117,29 +119,25 @@ func New(config *configs.Config) *Server { // Database connection db, err := repository.ConnectToMongoDB(config.MongoDB.URI) if err != nil { - log.Fatalf("Failed to connect to MongoDB: %v", err) + logger.Fatal("Failed to connect to MongoDB", zap.Error(err)) } database := db.Database(config.MongoDB.DatabaseName) // Initialize repositories - tokenRepo := repository.NewMongoTokenRepository(database) - verificationRepo := repository.NewMongoVerificationRepository(database) + tokenRepo := repository.NewMongoTokenRepository(database, logger) + verificationRepo := repository.NewMongoVerificationRepository(database, logger) // Initialize services - idenfyClient := idenfy.New(config.Idenfy) - - if err != nil { - log.Fatalf("Failed to initialize idenfy client: %v", err) - } + idenfyClient := idenfy.New(config.Idenfy, logger) - substrateClient, err := substrate.New(config.TFChain) + substrateClient, err := substrate.New(config.TFChain, logger) if err != nil { - log.Fatalf("Failed to initialize substrate client: %v", err) + logger.Fatal("Failed to initialize substrate client", zap.Error(err)) } - kycService := services.NewKYCService(verificationRepo, tokenRepo, idenfyClient, substrateClient, &config.Verification) + kycService := services.NewKYCService(verificationRepo, tokenRepo, idenfyClient, substrateClient, &config.Verification, logger) // Initialize handler - handler := handlers.NewHandler(kycService) + handler := handlers.NewHandler(kycService, logger) // Routes app.Get("/docs/*", swagger.HandlerDefault) @@ -162,19 +160,18 @@ func (s *Server) Start() { // Start server go func() { if err := s.app.Listen(":" + s.config.Server.Port); err != nil { - log.Fatalf("Failed to start server: %v", err) + s.logger.Fatal("Failed to start server", zap.Error(err)) } }() - // Graceful shutdown quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) <-quit - log.Println("Shutting down server...") + s.logger.Info("Shutting down server...") if err := s.app.Shutdown(); err != nil { - log.Fatalf("Server forced to shutdown: %v", err) + s.logger.Fatal("Server forced to shutdown", zap.Error(err)) } - log.Println("Server exiting") + s.logger.Info("Server exiting") } diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index 2e7dc9a..f1a4345 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -3,14 +3,15 @@ package services import ( "context" "errors" - "fmt" "math/big" "example.com/tfgrid-kyc-service/internal/clients/idenfy" "example.com/tfgrid-kyc-service/internal/clients/substrate" "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" "example.com/tfgrid-kyc-service/internal/repository" + "go.uber.org/zap" ) type kycService struct { @@ -19,10 +20,11 @@ type kycService struct { idenfy *idenfy.Idenfy substrate *substrate.Substrate config *configs.Verification + logger *logger.Logger } -func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.Verification) KYCService { - return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: config} +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.Verification, logger *logger.Logger) KYCService { + return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: config, logger: logger} } // --------------------------------------------------------------------------------------------------------------------- @@ -32,6 +34,7 @@ func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo 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", zap.String("clientID", clientID), zap.Error(err)) return nil, false, err } if isVerified { @@ -39,16 +42,17 @@ 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", zap.String("clientID", clientID), zap.Error(err)) return nil, false, err } // check if token is not nil and not expired or near expiry (2 min) if token != nil { //&& time.Since(token.CreatedAt)+2*time.Minute < time.Duration(token.ExpiryTime)*time.Second { return token, false, nil } - fmt.Println("token is nil or expired") // 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", zap.String("clientID", clientID), zap.Error(err)) return nil, false, err // todo: implement a custom error that can be converted in the handler to a 500 status code } if !hasRequiredBalance { @@ -56,27 +60,34 @@ func (s *kycService) GetorCreateVerificationToken(ctx context.Context, clientID } newToken, err := s.idenfy.CreateVerificationSession(ctx, clientID) if err != nil { + s.logger.Error("Error creating iDenfy verification session", zap.String("clientID", clientID), zap.Error(err)) return nil, false, err } - fmt.Println("new token", newToken) err = s.tokenRepo.SaveToken(ctx, &newToken) if err != nil { - fmt.Println("warning: was not able to save verification token to db", err) + s.logger.Error("Error saving verification token to database", zap.String("clientID", clientID), zap.Error(err)) } return &newToken, true, nil } func (s *kycService) DeleteToken(ctx context.Context, clientID string, scanRef string) error { - return s.tokenRepo.DeleteToken(ctx, clientID, scanRef) + + err := s.tokenRepo.DeleteToken(ctx, clientID, scanRef) + if err != nil { + s.logger.Error("Error deleting verification token from database", zap.String("clientID", clientID), zap.String("scanRef", scanRef), zap.Error(err)) + } + return err } 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", zap.String("address", address)) return true, nil } balance, err := s.substrate.GetAccountBalance(address) if err != nil { + s.logger.Error("Error getting account balance", zap.String("address", address), zap.Error(err)) return false, err } return balance.Cmp(big.NewInt(int64(s.config.MinBalanceToVerifyAccount))) >= 0, nil @@ -97,6 +108,7 @@ func (s *kycService) GetVerification(ctx context.Context, clientID string) (*mod func (s *kycService) GetVerificationStatus(ctx context.Context, clientID string) (*models.VerificationOutcome, error) { verification, err := s.GetVerification(ctx, clientID) if err != nil { + s.logger.Error("Error getting verification from database", zap.String("clientID", clientID), zap.Error(err)) return nil, err } var outcome string @@ -121,6 +133,7 @@ func (s *kycService) GetVerificationStatusByTwinID(ctx context.Context, twinID s // get the address from the twinID address, err := s.substrate.GetAddressByTwinID(twinID) if err != nil { + s.logger.Error("Error getting address from twinID", zap.String("twinID", twinID), zap.Error(err)) return nil, err } return s.GetVerificationStatus(ctx, address) @@ -129,23 +142,23 @@ 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", zap.String("sigHeader", sigHeader), zap.Error(err)) return err } // delete the token with the same clientID and same scanRef err = s.tokenRepo.DeleteToken(ctx, result.ClientID, result.ScanRef) if err != nil { - fmt.Printf("error deleting token: %v", err) + s.logger.Warn("Error deleting verification token from database", zap.String("clientID", result.ClientID), zap.String("scanRef", result.ScanRef), zap.Error(err)) } // if the verification status is EXPIRED, we don't need to save it if result.Status.Overall != "EXPIRED" { err = s.verificationRepo.SaveVerification(ctx, &result) if err != nil { - fmt.Printf("error saving verification to the database: %v", err) + s.logger.Error("Error saving verification to database", zap.String("clientID", result.ClientID), zap.String("scanRef", result.ScanRef), zap.Error(err)) return err } } - // fmt the result - fmt.Println(result) + s.logger.Debug("Verification result processed successfully", zap.Any("result", result)) return nil } diff --git a/scripts/dev/balance/check-account-balance.go b/scripts/dev/balance/check-account-balance.go index 63d23b7..49fb879 100644 --- a/scripts/dev/balance/check-account-balance.go +++ b/scripts/dev/balance/check-account-balance.go @@ -6,6 +6,7 @@ import ( "example.com/tfgrid-kyc-service/internal/clients/substrate" "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/logger" ) func main() { @@ -13,7 +14,9 @@ func main() { if err != nil { panic(err) } - substrateClient, err := substrate.New(config.TFChain) + logger.Init(config.Log) + logger := logger.GetLogger() + substrateClient, err := substrate.New(config.TFChain, logger) if err != nil { panic(err) } diff --git a/scripts/dev/twin/get-address-by-twin-id.go b/scripts/dev/twin/get-address-by-twin-id.go index c5f2a7c..ccf5ac0 100644 --- a/scripts/dev/twin/get-address-by-twin-id.go +++ b/scripts/dev/twin/get-address-by-twin-id.go @@ -5,6 +5,7 @@ import ( "example.com/tfgrid-kyc-service/internal/clients/substrate" "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/logger" ) func main() { @@ -12,7 +13,9 @@ func main() { if err != nil { panic(err) } - substrateClient, err := substrate.New(config.TFChain) + logger.Init(config.Log) + logger := logger.GetLogger() + substrateClient, err := substrate.New(config.TFChain, logger) if err != nil { panic(err) } From b2df7007d499b95aba17b338e3a71967143641c6 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 01:33:10 +0300 Subject: [PATCH 021/105] Adjust models --- go.mod | 2 +- internal/clients/idenfy/idenfy.go | 1 + internal/clients/idenfy/types.go | 1 - internal/models/new_verification_model.go | 163 -------------- internal/models/verification.go | 254 ++++++++++++++-------- internal/repository/token_repository.go | 9 +- internal/responses/responses.go | 54 +++-- internal/services/kyc_service.go | 33 +-- 8 files changed, 215 insertions(+), 302 deletions(-) delete mode 100644 internal/clients/idenfy/types.go delete mode 100644 internal/models/new_verification_model.go diff --git a/go.mod b/go.mod index 10b3447..42d6b4e 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gofiber/storage/mongodb v1.3.9 github.com/gofiber/swagger v1.1.0 github.com/ilyakaznacheev/cleanenv v1.5.0 + github.com/stretchr/testify v1.9.0 github.com/swaggo/swag v1.16.3 github.com/threefoldtech/tfchain/clients/tfchain-client-go v0.0.0-20241007205731-5e76664a3cc4 github.com/valyala/fasthttp v1.51.0 @@ -58,7 +59,6 @@ require ( github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/rs/cors v1.8.2 // indirect github.com/rs/zerolog v1.33.0 // indirect - github.com/stretchr/testify v1.9.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index 9bf6045..416bcea 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -101,6 +101,7 @@ func (c *Idenfy) VerifyCallbackSignature(ctx context.Context, body []byte, sigHe mac.Write(body) if !hmac.Equal(sig, mac.Sum(nil)) { + c.logger.Error("Signature verification failed", zap.String("sigHeader", sigHeader), zap.String("key", string(c.callbackSignKey)), zap.String("mac", hex.EncodeToString(mac.Sum(nil)))) return errors.New("signature verification failed") } return nil diff --git a/internal/clients/idenfy/types.go b/internal/clients/idenfy/types.go deleted file mode 100644 index 23f36e1..0000000 --- a/internal/clients/idenfy/types.go +++ /dev/null @@ -1 +0,0 @@ -package idenfy diff --git a/internal/models/new_verification_model.go b/internal/models/new_verification_model.go deleted file mode 100644 index e2bf7a9..0000000 --- a/internal/models/new_verification_model.go +++ /dev/null @@ -1,163 +0,0 @@ -package models - -import ( - "go.mongodb.org/mongo-driver/bson/primitive" -) - -// TODO: switch to this model -type Platform string - -const ( - PlatformPC Platform = "PC" - PlatformMobile Platform = "MOBILE" - PlatformTablet Platform = "TABLET" - PlatformMobileApp Platform = "MOBILE_APP" - PlatformMobileSDK Platform = "MOBILE_SDK" - PlatformOther Platform = "OTHER" -) - -type OverallStatus string - -const ( - StatusApproved OverallStatus = "APPROVED" - StatusDenied OverallStatus = "DENIED" - StatusSuspected OverallStatus = "SUSPECTED" - StatusReviewing OverallStatus = "REVIEWING" - StatusExpired OverallStatus = "EXPIRED" - StatusActive OverallStatus = "ACTIVE" - StatusDeleted OverallStatus = "DELETED" - StatusArchived OverallStatus = "ARCHIVED" -) - -type SuspicionReason string - -// Define constants for SuspicionReason - -type FraudTag string - -// Define constants for FraudTag - -type MismatchTag string - -// Define constants for MismatchTag - -type FaceStatus string - -const ( - FaceMatch FaceStatus = "FACE_MATCH" - FaceNotChecked FaceStatus = "FACE_NOT_CHECKED" - FaceMismatch FaceStatus = "FACE_MISMATCH" - NoFaceFound FaceStatus = "NO_FACE_FOUND" - TooManyFaces FaceStatus = "TOO_MANY_FACES" - FaceTooBlurry FaceStatus = "FACE_TOO_BLURRY" - FaceGlared FaceStatus = "FACE_GLARED" - FaceUncertain FaceStatus = "FACE_UNCERTAIN" - FaceNotAnalysed FaceStatus = "FACE_NOT_ANALYSED" - FaceError FaceStatus = "FACE_ERROR" - AutoUnverifiable FaceStatus = "AUTO_UNVERIFIABLE" - FakeFace FaceStatus = "FAKE_FACE" -) - -type DocumentStatus string - -// Define constants for DocumentStatus - -type VerificationStatus string - -const ( - StatusVerified VerificationStatus = "VERIFIED" - StatusPartiallyVerified VerificationStatus = "PARTIALLY_VERIFIED" - StatusUnverified VerificationStatus = "UNVERIFIED" -) - -type Quality string - -const ( - QualityExcellent Quality = "EXCELLENT" - QualityGood Quality = "GOOD" - QualityAverage Quality = "AVERAGE" - QualityPoor Quality = "POOR" - QualityBad Quality = "BAD" -) - -type QuestionType string - -const ( - QuestionTypeCheckbox QuestionType = "CHECKBOX" - QuestionTypeColor QuestionType = "COLOR" - QuestionTypeCountry QuestionType = "COUNTRY" - QuestionTypeDate QuestionType = "DATE" - QuestionTypeDateTime QuestionType = "DATETIME" - QuestionTypeEmail QuestionType = "EMAIL" - QuestionTypeFile QuestionType = "FILE" - QuestionTypeFloat QuestionType = "FLOAT" - QuestionTypeList QuestionType = "LIST" - QuestionTypeInteger QuestionType = "INTEGER" - QuestionTypePassword QuestionType = "PASSWORD" - QuestionTypeRadio QuestionType = "RADIO" - QuestionTypeSelect QuestionType = "SELECT" - QuestionTypeSelectMulti QuestionType = "SELECT_MULTI" - QuestionTypeTel QuestionType = "TEL" - QuestionTypeText QuestionType = "TEXT" - QuestionTypeTextArea QuestionType = "TEXT_AREA" - QuestionTypeTime QuestionType = "TIME" - QuestionTypeURL QuestionType = "URL" -) - -type Verification_ struct { - ID primitive.ObjectID `bson:"_id,omitempty"` - Final bool `bson:"final"` - Platform Platform `bson:"platform"` - Status Status_ `bson:"status"` - Data PersonData `bson:"data"` - FileUrls map[string]string `bson:"fileUrls"` - ScanRef string `bson:"scanRef"` - ClientID string `bson:"clientId"` - CompanyID string `bson:"companyId"` - BeneficiaryID string `bson:"beneficiaryId"` - StartTime int64 `bson:"startTime"` - FinishTime int64 `bson:"finishTime"` - ClientIP string `bson:"clientIp"` - ClientIPCountry string `bson:"clientIpCountry"` - ClientLocation string `bson:"clientLocation"` - QuestionnaireAnswers *QuestionnaireAnswers `bson:"questionnaireAnswers,omitempty"` - AdditionalSteps map[string]interface{} `bson:"additionalSteps,omitempty"` - UtilityData []string `bson:"utilityData,omitempty"` -} - -type Status_ struct { - Overall OverallStatus `bson:"overall"` - SuspicionReasons []SuspicionReason `bson:"suspicionReasons"` - DenyReasons []string `bson:"denyReasons"` - FraudTags []FraudTag `bson:"fraudTags"` - MismatchTags []MismatchTag `bson:"mismatchTags"` - AutoFace FaceStatus `bson:"autoFace"` - ManualFace FaceStatus `bson:"manualFace"` - AutoDocument DocumentStatus `bson:"autoDocument"` - ManualDocument DocumentStatus `bson:"manualDocument"` -} - -type PersonData_ struct { - // Add fields based on the schema - Address string `bson:"address"` - Status VerificationStatus `bson:"status"` - Accuracy *int `bson:"accuracy,omitempty"` - Quality *Quality `bson:"quality,omitempty"` -} - -type QuestionnaireAnswers struct { - Title string `bson:"title"` - Sections []Section `bson:"sections"` -} - -type Section struct { - Title string `bson:"title"` - Questions []Question `bson:"questions"` -} - -type Question struct { - Key string `bson:"key"` - Title string `bson:"title"` - Type QuestionType `bson:"type"` - Value string `bson:"value"` -} diff --git a/internal/models/verification.go b/internal/models/verification.go index 537c611..e9ed685 100644 --- a/internal/models/verification.go +++ b/internal/models/verification.go @@ -7,60 +7,104 @@ import ( ) type Verification struct { - ID primitive.ObjectID `bson:"_id,omitempty"` - Final bool `bson:"final"` - Platform string `bson:"platform"` - Status Status `bson:"status"` - Data PersonData `bson:"data"` - FileUrls map[string]string `bson:"fileUrls"` - AdditionalStepPdfUrls map[string]string `bson:"additionalStepPdfUrls"` - AML []AMLCheck `bson:"AML"` - LID interface{} `bson:"LID"` - ScanRef string `bson:"scanRef"` - ExternalRef string `bson:"externalRef"` - ClientID string `bson:"clientId"` - StartTime int64 `bson:"startTime"` - FinishTime int64 `bson:"finishTime"` - ClientIP string `bson:"clientIp"` - ClientIPCountry string `bson:"clientIpCountry"` - ClientLocation string `bson:"clientLocation"` - ManualAddress interface{} `bson:"manualAddress"` - ManualAddressMatch bool `bson:"manualAddressMatch"` - RegistryCenterCheck interface{} `bson:"registryCenterCheck"` - AddressVerification interface{} `bson:"addressVerification"` - QuestionnaireAnswers interface{} `bson:"questionnaireAnswers"` - CompanyID interface{} `bson:"companyId"` - BeneficiaryID interface{} `bson:"beneficiaryId"` - AdditionalSteps map[string]string `bson:"additionalSteps"` - CreatedAt time.Time `bson:"createdAt"` + ID primitive.ObjectID `bson:"_id,omitempty" json:"-"` + CreatedAt time.Time `bson:"createdAt" json:"-"` + Final *bool `bson:"final" json:"final"` // required + Platform Platform `bson:"platform" json:"platform"` // required + Status Status `bson:"status" json:"status"` // required + Data PersonData `bson:"data" json:"data"` // required + FileUrls map[string]string `bson:"fileUrls" json:"fileUrls"` // required + IdenfyRef string `bson:"idenfyRef" json:"scanRef"` // required + ClientID string `bson:"clientId" json:"clientId"` // required + StartTime int64 `bson:"startTime" json:"startTime"` // required + FinishTime int64 `bson:"finishTime" json:"finishTime"` // required + ClientIP string `bson:"clientIp" json:"clientIp"` // required + ClientIPCountry string `bson:"clientIpCountry" json:"clientIpCountry"` // required + ClientLocation string `bson:"clientLocation" json:"clientLocation"` // required + CompanyID string `bson:"companyId" json:"companyId"` // required + BeneficiaryID string `bson:"beneficiaryId" json:"beneficiaryId"` // required + RegistryCenterCheck interface{} `json:"registryCenterCheck,omitempty"` + AddressVerification interface{} `json:"addressVerification,omitempty"` + QuestionnaireAnswers interface{} `json:"questionnaireAnswers,omitempty"` + AdditionalSteps map[string]string `json:"additionalSteps,omitempty"` + UtilityData []string `json:"utilityData,omitempty"` + AdditionalStepPdfUrls map[string]string `json:"additionalStepPdfUrls,omitempty"` + AML []AMLCheck `bson:"AML" json:"AML,omitempty"` + LID []LID `bson:"LID" json:"LID,omitempty"` + ExternalRef string `bson:"externalRef" json:"externalRef,omitempty"` + ManualAddress string `bson:"manualAddress" json:"manualAddress,omitempty"` + ManualAddressMatch *bool `bson:"manualAddressMatch" json:"manualAddressMatch,omitempty"` } +type Platform string + +const ( + PlatformPC Platform = "PC" + PlatformMobile Platform = "MOBILE" + PlatformTablet Platform = "TABLET" + PlatformMobileApp Platform = "MOBILE_APP" + PlatformMobileSDK Platform = "MOBILE_SDK" + PlatformOther Platform = "OTHER" +) + type Overall string const ( OverallApproved Overall = "APPROVED" OverallDenied Overall = "DENIED" OverallSuspected Overall = "SUSPECTED" + OverallReviewing Overall = "REVIEWING" OverallExpired Overall = "EXPIRED" + OverallActive Overall = "ACTIVE" + OverallDeleted Overall = "DELETED" + OverallArchived Overall = "ARCHIVED" ) type Status struct { - Overall Overall `bson:"overall"` - SuspicionReasons []string `bson:"suspicionReasons"` - DenyReasons []string `bson:"denyReasons"` - FraudTags []string `bson:"fraudTags"` - MismatchTags []string `bson:"mismatchTags"` - AutoFace string `bson:"autoFace"` - ManualFace string `bson:"manualFace"` - AutoDocument string `bson:"autoDocument"` - ManualDocument string `bson:"manualDocument"` - AdditionalSteps string `bson:"additionalSteps"` - AMLResultClass string `bson:"amlResultClass"` - PEPSStatus string `bson:"pepsStatus"` - SanctionsStatus string `bson:"sanctionsStatus"` - AdverseMediaStatus string `bson:"adverseMediaStatus"` + Overall *Overall `bson:"overall" json:"overall"` + SuspicionReasons []SuspicionReason `bson:"suspicionReasons" json:"suspicionReasons"` + DenyReasons []string `bson:"denyReasons" json:"denyReasons"` + FraudTags []string `bson:"fraudTags" json:"fraudTags"` + MismatchTags []string `bson:"mismatchTags" json:"mismatchTags"` + AutoFace string `bson:"autoFace" json:"autoFace,omitempty"` + ManualFace string `bson:"manualFace" json:"manualFace,omitempty"` + AutoDocument string `bson:"autoDocument" json:"autoDocument,omitempty"` + ManualDocument string `bson:"manualDocument" json:"manualDocument,omitempty"` + AdditionalSteps *AdditionalStep `bson:"additionalSteps" json:"additionalSteps,omitempty"` + AMLResultClass string `bson:"amlResultClass" json:"amlResultClass,omitempty"` + PEPSStatus string `bson:"pepsStatus" json:"pepsStatus,omitempty"` + SanctionsStatus string `bson:"sanctionsStatus" json:"sanctionsStatus,omitempty"` + AdverseMediaStatus string `bson:"adverseMediaStatus" json:"adverseMediaStatus,omitempty"` } +type SuspicionReason string + +const ( + SuspicionFaceSuspected SuspicionReason = "FACE_SUSPECTED" + SuspicionFaceBlacklisted SuspicionReason = "FACE_BLACKLISTED" + SuspicionDocFaceBlacklisted SuspicionReason = "DOC_FACE_BLACKLISTED" + SuspicionDocMobilePhoto SuspicionReason = "DOC_MOBILE_PHOTO" + SuspicionDevToolsOpened SuspicionReason = "DEV_TOOLS_OPENED" + SuspicionDocPrintSpoofed SuspicionReason = "DOC_PRINT_SPOOFED" + SuspicionFakePhoto SuspicionReason = "FAKE_PHOTO" + SuspicionAMLSuspection SuspicionReason = "AML_SUSPECTION" + SuspicionAMLFailed SuspicionReason = "AML_FAILED" + SuspicionLIDSuspection SuspicionReason = "LID_SUSPECTION" + SuspicionLIDFailed SuspicionReason = "LID_FAILED" + SuspicionSanctionsSuspection SuspicionReason = "SANCTIONS_SUSPECTION" + SuspicionSanctionsFailed SuspicionReason = "SANCTIONS_FAILED" + SuspicionRCFailed SuspicionReason = "RC_FAILED" + SuspicionAutoUnverifiable SuspicionReason = "AUTO_UNVERIFIABLE" +) + +type AdditionalStep string + +const ( + AdditionalStepValid AdditionalStep = "VALID" + AdditionalStepInvalid AdditionalStep = "INVALID" + AdditionalStepNotFound AdditionalStep = "NOT_FOUND" +) + type DocumentType string const ( @@ -105,58 +149,50 @@ const ( ) type PersonData struct { - DocFirstName string `bson:"docFirstName"` - DocLastName string `bson:"docLastName"` - DocNumber string `bson:"docNumber"` - DocPersonalCode string `bson:"docPersonalCode"` - DocExpiry string `bson:"docExpiry"` - DocDOB string `bson:"docDob"` - DocDateOfIssue string `bson:"docDateOfIssue"` - DocType DocumentType `bson:"docType"` - DocSex Sex `bson:"docSex"` - DocNationality string `bson:"docNationality"` - DocIssuingCountry string `bson:"docIssuingCountry"` - BirthPlace string `bson:"birthPlace"` - Authority string `bson:"authority"` - Address string `bson:"address"` - DocTemporaryAddress string `bson:"docTemporaryAddress"` - MothersMaidenName string `bson:"mothersMaidenName"` - DocBirthName string `bson:"docBirthName"` - DriverLicenseCategory string `bson:"driverLicenseCategory"` - ManuallyDataChanged bool `bson:"manuallyDataChanged"` - FullName string `bson:"fullName"` - SelectedCountry string `bson:"selectedCountry"` - OrgFirstName string `bson:"orgFirstName"` - OrgLastName string `bson:"orgLastName"` - OrgNationality string `bson:"orgNationality"` - OrgBirthPlace string `bson:"orgBirthPlace"` - OrgAuthority string `bson:"orgAuthority"` - OrgAddress string `bson:"orgAddress"` - OrgTemporaryAddress string `bson:"orgTemporaryAddress"` - OrgMothersMaidenName string `bson:"orgMothersMaidenName"` - OrgBirthName string `bson:"orgBirthName"` - AgeEstimate AgeEstimate `bson:"ageEstimate"` - ClientIPProxyRiskLevel string `bson:"clientIpProxyRiskLevel"` - DuplicateFaces []string `bson:"duplicateFaces"` - DuplicateDocFaces []string `bson:"duplicateDocFaces"` - AdditionalData interface{} `bson:"additionalData"` + DocFirstName string `bson:"docFirstName" json:"docFirstName"` + DocLastName string `bson:"docLastName" json:"docLastName"` + DocNumber string `bson:"docNumber" json:"docNumber"` + DocPersonalCode string `bson:"docPersonalCode" json:"docPersonalCode"` + DocExpiry string `bson:"docExpiry" json:"docExpiry"` + DocDOB string `bson:"docDob" json:"docDob"` + DocDateOfIssue string `bson:"docDateOfIssue" json:"docDateOfIssue"` + DocType *DocumentType `bson:"docType" json:"docType"` + DocSex *Sex `bson:"docSex" json:"docSex"` + DocNationality string `bson:"docNationality" json:"docNationality"` + DocIssuingCountry string `bson:"docIssuingCountry" json:"docIssuingCountry"` + BirthPlace string `bson:"birthPlace" json:"birthPlace"` + Authority string `bson:"authority" json:"authority"` + Address string `bson:"address" json:"address"` + DocTemporaryAddress string `bson:"docTemporaryAddress" json:"docTemporaryAddress"` + MothersMaidenName string `bson:"mothersMaidenName" json:"mothersMaidenName"` + DocBirthName string `bson:"docBirthName" json:"docBirthName"` + DriverLicenseCategory string `bson:"driverLicenseCategory" json:"driverLicenseCategory"` + ManuallyDataChanged *bool `bson:"manuallyDataChanged" json:"manuallyDataChanged"` + FullName string `bson:"fullName" json:"fullName"` + SelectedCountry string `bson:"selectedCountry" json:"selectedCountry"` + OrgFirstName string `bson:"orgFirstName" json:"orgFirstName"` + OrgLastName string `bson:"orgLastName" json:"orgLastName"` + OrgNationality string `bson:"orgNationality" json:"orgNationality"` + OrgBirthPlace string `bson:"orgBirthPlace" json:"orgBirthPlace"` + OrgAuthority string `bson:"orgAuthority" json:"orgAuthority"` + OrgAddress string `bson:"orgAddress" json:"orgAddress"` + OrgTemporaryAddress string `bson:"orgTemporaryAddress" json:"orgTemporaryAddress"` + OrgMothersMaidenName string `bson:"orgMothersMaidenName" json:"orgMothersMaidenName"` + OrgBirthName string `bson:"orgBirthName" json:"orgBirthName"` + AgeEstimate *AgeEstimate `bson:"ageEstimate" json:"ageEstimate"` + ClientIPProxyRiskLevel string `bson:"clientIpProxyRiskLevel" json:"clientIpProxyRiskLevel"` + DuplicateFaces []string `bson:"duplicateFaces" json:"duplicateFaces"` + DuplicateDocFaces []string `bson:"duplicateDocFaces" json:"duplicateDocFaces"` + AdditionalData interface{} `bson:"additionalData" json:"additionalData"` } type AMLCheck struct { - Status AMLStatus `bson:"status"` - Data []AMLData `bson:"data"` - ServiceName string `bson:"serviceName"` - ServiceGroupType string `bson:"serviceGroupType"` - UID string `bson:"uid"` - ErrorMessage string `bson:"errorMessage"` -} - -type AMLStatus struct { - ServiceSuspected bool `bson:"serviceSuspected"` - ServiceUsed bool `bson:"serviceUsed"` - ServiceFound bool `bson:"serviceFound"` - CheckSuccessful bool `bson:"checkSuccessful"` - OverallStatus string `bson:"overallStatus"` + Status ServiceStatus `bson:"status"` + Data []AMLData `bson:"data"` + ServiceName string `bson:"serviceName"` + ServiceGroupType string `bson:"serviceGroupType"` + UID string `bson:"uid"` + ErrorMessage string `bson:"errorMessage"` } type AMLData struct { @@ -175,9 +211,41 @@ type AMLData struct { CheckDate string `bson:"checkDate"` } +type LID struct { + Status *ServiceStatus `json:"status"` + Data []LIDData `json:"data"` + ServiceName string `json:"serviceName"` + ServiceGroupType string `json:"serviceGroupType"` + UID string `json:"uid"` + ErrorMessage string `json:"errorMessage"` +} + +type LIDData struct { + DocumentNumber string `json:"documentNumber"` + DocumentType *DocumentType `json:"documentType"` + Valid *bool `json:"valid"` + ExpiryDate string `json:"expiryDate"` + CheckDate string `json:"checkDate"` +} + +type ServiceStatus struct { + ServiceSuspected *bool `json:"serviceSuspected" bson:"serviceSuspected"` + CheckSuccessful *bool `json:"checkSuccessful" bson:"checkSuccessful"` + ServiceFound *bool `json:"serviceFound" bson:"serviceFound"` + ServiceUsed *bool `json:"serviceUsed" bson:"serviceUsed"` + OverallStatus string `json:"overallStatus" bson:"overallStatus"` +} + type VerificationOutcome struct { - Final bool `bson:"final"` - ClientID string `bson:"clientId"` - IdenfyRef string `bson:"idenfyRef"` - Outcome string `bson:"outcome"` + Final *bool `bson:"final"` + ClientID string `bson:"clientId"` + IdenfyRef string `bson:"idenfyRef"` + Outcome Outcome `bson:"outcome"` } + +type Outcome string + +const ( + OutcomeApproved Outcome = "APPROVED" + OutcomeRejected Outcome = "REJECTED" +) diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index ebc854e..fd84a85 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -60,14 +60,7 @@ func (r *MongoTokenRepository) GetToken(ctx context.Context, clientID string) (* } return nil, err } - // calculate duration between createdAt and now then updae expiry time with remaining time - duration := time.Since(token.CreatedAt) - // protect against overflow - if duration >= time.Duration(token.ExpiryTime)*time.Second { - return nil, nil - } - remainingTime := time.Duration(token.ExpiryTime)*time.Second - duration - token.ExpiryTime = int(remainingTime.Seconds()) + return &token, nil } diff --git a/internal/responses/responses.go b/internal/responses/responses.go index 458aa31..b718f9a 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -30,6 +30,13 @@ type TokenResponse struct { TokenType string `json:"tokenType"` } +type Outcome string + +const ( + OutcomeVerified Outcome = "VERIFIED" + OutcomeRejected Outcome = "REJECTED" +) + type VerificationStatusResponse struct { Final bool `json:"final"` IdenfyRef string `json:"idenfyRef"` @@ -56,7 +63,7 @@ type VerificationDataResponse struct { Address string `json:"address"` MotherMaidenName string `json:"mothersMaidenName"` DriverLicenseCategory string `json:"driverLicenseCategory"` - ManuallyDataChanged bool `json:"manuallyDataChanged"` + ManuallyDataChanged *bool `json:"manuallyDataChanged"` FullName string `json:"fullName"` OrgFirstName string `json:"orgFirstName"` OrgLastName string `json:"orgLastName"` @@ -74,11 +81,10 @@ type VerificationDataResponse struct { DuplicateDocFaces []string `json:"duplicateDocFaces"` AddressVerification interface{} `json:"addressVerification"` AdditionalData interface{} `json:"additionalData"` - ScanRef string `json:"scanRef"` + IdenfyRef string `json:"idenfyRef"` ClientID string `json:"clientId"` } -// implement from() method for TokenResponseWithStatus func NewTokenResponseWithStatus(token *models.Token, isNewToken bool) *TokenResponse { message := "Existing valid token retrieved." if isNewToken { @@ -96,27 +102,13 @@ func NewTokenResponseWithStatus(token *models.Token, isNewToken bool) *TokenResp } } -// Outcome enum can be VERIFIED or REJECTED -type Outcome string - -const ( - OutcomeVerified Outcome = "VERIFIED" - OutcomeRejected Outcome = "REJECTED" -) - func NewVerificationStatusResponse(verificationOutcome *models.VerificationOutcome) *VerificationStatusResponse { outcome := OutcomeVerified if verificationOutcome.Outcome == "REJECTED" { outcome = OutcomeRejected } return &VerificationStatusResponse{ - /* FraudTags: verification.Status.FraudTags, - MismatchTags: verification.Status.MismatchTags, - AutoDocument: verification.Status.AutoDocument, - ManualDocument: verification.Status.ManualDocument, - AutoFace: verification.Status.AutoFace, - ManualFace: verification.Status.ManualFace, */ - Final: verificationOutcome.Final, + Final: *verificationOutcome.Final, IdenfyRef: verificationOutcome.IdenfyRef, ClientID: verificationOutcome.ClientID, Status: outcome, @@ -124,6 +116,22 @@ func NewVerificationStatusResponse(verificationOutcome *models.VerificationOutco } func NewVerificationDataResponse(verification *models.Verification) *VerificationDataResponse { + var docType string + if verification.Data.DocType != nil { + docType = string(*verification.Data.DocType) + } + var docSex string + if verification.Data.DocSex != nil { + docSex = string(*verification.Data.DocSex) + } + var manuallyDataChanged *bool + if verification.Data.ManuallyDataChanged != nil { + manuallyDataChanged = verification.Data.ManuallyDataChanged + } + var ageEstimate string + if verification.Data.AgeEstimate != nil { + ageEstimate = string(*verification.Data.AgeEstimate) + } return &VerificationDataResponse{ DocFirstName: verification.Data.DocFirstName, DocLastName: verification.Data.DocLastName, @@ -132,8 +140,8 @@ func NewVerificationDataResponse(verification *models.Verification) *Verificatio DocExpiry: verification.Data.DocExpiry, DocDob: verification.Data.DocDOB, DocDateOfIssue: verification.Data.DocDateOfIssue, - DocType: string(verification.Data.DocType), - DocSex: string(verification.Data.DocSex), + DocType: docType, + DocSex: docSex, DocNationality: verification.Data.DocNationality, DocIssuingCountry: verification.Data.DocIssuingCountry, DocTemporaryAddress: verification.Data.DocTemporaryAddress, @@ -142,7 +150,7 @@ func NewVerificationDataResponse(verification *models.Verification) *Verificatio Authority: verification.Data.Authority, MotherMaidenName: verification.Data.MothersMaidenName, DriverLicenseCategory: verification.Data.DriverLicenseCategory, - ManuallyDataChanged: verification.Data.ManuallyDataChanged, + ManuallyDataChanged: manuallyDataChanged, FullName: verification.Data.FullName, OrgFirstName: verification.Data.OrgFirstName, OrgLastName: verification.Data.OrgLastName, @@ -154,13 +162,13 @@ func NewVerificationDataResponse(verification *models.Verification) *Verificatio OrgMothersMaidenName: verification.Data.OrgMothersMaidenName, OrgBirthName: verification.Data.OrgBirthName, SelectedCountry: verification.Data.SelectedCountry, - AgeEstimate: string(verification.Data.AgeEstimate), + AgeEstimate: ageEstimate, ClientIpProxyRiskLevel: verification.Data.ClientIPProxyRiskLevel, DuplicateFaces: verification.Data.DuplicateFaces, DuplicateDocFaces: verification.Data.DuplicateDocFaces, AddressVerification: verification.AddressVerification, AdditionalData: verification.Data.AdditionalData, - ScanRef: verification.ScanRef, + IdenfyRef: verification.IdenfyRef, ClientID: verification.ClientID, } } diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index f1a4345..d01dc5e 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "math/big" + "time" "example.com/tfgrid-kyc-service/internal/clients/idenfy" "example.com/tfgrid-kyc-service/internal/clients/substrate" @@ -45,10 +46,16 @@ func (s *kycService) GetorCreateVerificationToken(ctx context.Context, clientID s.logger.Error("Error getting token from database", zap.String("clientID", clientID), zap.Error(err)) return nil, false, err } - // check if token is not nil and not expired or near expiry (2 min) - if token != nil { //&& time.Since(token.CreatedAt)+2*time.Minute < time.Duration(token.ExpiryTime)*time.Second { - return token, false, nil + // 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 { @@ -111,12 +118,12 @@ func (s *kycService) GetVerificationStatus(ctx context.Context, clientID string) s.logger.Error("Error getting verification from database", zap.String("clientID", clientID), zap.Error(err)) return nil, err } - var outcome string + var outcome models.Outcome if verification != nil { - if verification.Status.Overall == "APPROVED" || (s.config.SuspiciousVerificationOutcome == "APPROVED" && verification.Status.Overall == "SUSPECTED") { - outcome = "APPROVED" + if verification.Status.Overall != nil && *verification.Status.Overall == models.OverallApproved || (s.config.SuspiciousVerificationOutcome == "APPROVED" && *verification.Status.Overall == models.OverallSuspected) { + outcome = models.OutcomeApproved } else { - outcome = "REJECTED" + outcome = models.OutcomeRejected } } else { return nil, nil @@ -124,7 +131,7 @@ func (s *kycService) GetVerificationStatus(ctx context.Context, clientID string) return &models.VerificationOutcome{ Final: verification.Final, ClientID: clientID, - IdenfyRef: verification.ScanRef, + IdenfyRef: verification.IdenfyRef, Outcome: outcome, }, nil } @@ -146,15 +153,15 @@ func (s *kycService) ProcessVerificationResult(ctx context.Context, body []byte, return err } // delete the token with the same clientID and same scanRef - err = s.tokenRepo.DeleteToken(ctx, result.ClientID, result.ScanRef) + err = s.tokenRepo.DeleteToken(ctx, result.ClientID, result.IdenfyRef) if err != nil { - s.logger.Warn("Error deleting verification token from database", zap.String("clientID", result.ClientID), zap.String("scanRef", result.ScanRef), zap.Error(err)) + s.logger.Warn("Error deleting verification token from database", zap.String("clientID", result.ClientID), zap.String("scanRef", result.IdenfyRef), zap.Error(err)) } // if the verification status is EXPIRED, we don't need to save it - if result.Status.Overall != "EXPIRED" { + if result.Status.Overall != nil && *result.Status.Overall != models.Overall("EXPIRED") { err = s.verificationRepo.SaveVerification(ctx, &result) if err != nil { - s.logger.Error("Error saving verification to database", zap.String("clientID", result.ClientID), zap.String("scanRef", result.ScanRef), zap.Error(err)) + s.logger.Error("Error saving verification to database", zap.String("clientID", result.ClientID), zap.String("scanRef", result.IdenfyRef), zap.Error(err)) return err } } @@ -174,5 +181,5 @@ func (s *kycService) IsUserVerified(ctx context.Context, clientID string) (bool, if verification == nil { return false, nil } - return verification.Status.Overall == "APPROVED" || (s.config.SuspiciousVerificationOutcome == "APPROVED" && verification.Status.Overall == "SUSPECTED"), nil + return verification.Status.Overall != nil && (*verification.Status.Overall == models.OverallApproved || (s.config.SuspiciousVerificationOutcome == "APPROVED" && *verification.Status.Overall == models.OverallSuspected)), nil } From d7e3123c826e0b581299afe036c246ae6ed1302d Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 01:33:57 +0300 Subject: [PATCH 022/105] test verification callback --- internal/clients/idenfy/idenfy_test.go | 111 ++++++++++++++++++ .../clients/idenfy/testdata/webhook.1.json | 109 +++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 internal/clients/idenfy/idenfy_test.go create mode 100644 internal/clients/idenfy/testdata/webhook.1.json diff --git a/internal/clients/idenfy/idenfy_test.go b/internal/clients/idenfy/idenfy_test.go new file mode 100644 index 0000000..2ae9bcb --- /dev/null +++ b/internal/clients/idenfy/idenfy_test.go @@ -0,0 +1,111 @@ +package idenfy + +import ( + "bytes" + "context" + "encoding/json" + "os" + "testing" + + "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/logger" + "example.com/tfgrid-kyc-service/internal/models" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" +) + +func TestClient_DecodeReaderIdentityCallback(t *testing.T) { + expectedSig := "249d9a838e9b981935324b02367ca72552aa430fc766f45f77fab7a81f9f3b9d" + logger.Init(configs.Log{}) + logger := logger.GetLogger() + client := New(configs.Idenfy{ + CallbackSignKey: "TestingKey", + }, logger) + defer logger.Sync() + + assert.NotNil(t, client, "Client is nil") + webhook1, err := os.ReadFile("testdata/webhook.1.json") + assert.NoError(t, err, "Could not open test data") + err = client.VerifyCallbackSignature(context.Background(), webhook1, expectedSig) + assert.NoError(t, err) + var resp models.Verification + decoder := json.NewDecoder(bytes.NewReader(webhook1)) + err = decoder.Decode(&resp) + assert.NoError(t, err) + // Basic verification info + logger.Info("resp", zap.Any("resp", resp)) + assert.Equal(t, "123", resp.ClientID) + assert.Equal(t, "scan-ref", resp.IdenfyRef) + assert.Equal(t, "external-ref", resp.ExternalRef) + assert.Equal(t, models.Platform("MOBILE_APP"), resp.Platform) + assert.Equal(t, int64(1554726960), resp.StartTime) + assert.Equal(t, int64(1554727002), resp.FinishTime) + assert.Equal(t, "192.0.2.0", resp.ClientIP) + assert.Equal(t, "LT", resp.ClientIPCountry) + assert.Equal(t, "Kaunas, Lithuania", resp.ClientLocation) + assert.False(t, *resp.Final) + + // Status checks + assert.Equal(t, models.Overall("APPROVED"), *resp.Status.Overall) + assert.Empty(t, resp.Status.SuspicionReasons) + assert.Empty(t, resp.Status.MismatchTags) + assert.Empty(t, resp.Status.FraudTags) + assert.Equal(t, "DOC_VALIDATED", resp.Status.AutoDocument) + assert.Equal(t, "FACE_MATCH", resp.Status.AutoFace) + assert.Equal(t, "DOC_VALIDATED", resp.Status.ManualDocument) + assert.Equal(t, "FACE_MATCH", resp.Status.ManualFace) + assert.Nil(t, resp.Status.AdditionalSteps) + + // Document data + assert.Equal(t, "FIRST-NAME-EXAMPLE", resp.Data.DocFirstName) + assert.Equal(t, "LAST-NAME-EXAMPLE", resp.Data.DocLastName) + assert.Equal(t, "XXXXXXXXX", resp.Data.DocNumber) + assert.Equal(t, "XXXXXXXXX", resp.Data.DocPersonalCode) + assert.Equal(t, "YYYY-MM-DD", resp.Data.DocExpiry) + assert.Equal(t, "YYYY-MM-DD", resp.Data.DocDOB) + assert.Equal(t, "2018-03-02", resp.Data.DocDateOfIssue) + assert.Equal(t, models.DocumentType("ID_CARD"), *resp.Data.DocType) + assert.Equal(t, models.Sex("UNDEFINED"), *resp.Data.DocSex) + assert.Equal(t, "LT", resp.Data.DocNationality) + assert.Equal(t, "LT", resp.Data.DocIssuingCountry) + assert.Equal(t, "BIRTH PLACE", resp.Data.BirthPlace) + assert.Equal(t, "AUTHORITY EXAMPLE", resp.Data.Authority) + assert.Equal(t, "ADDRESS EXAMPLE", resp.Data.Address) + assert.Equal(t, "FULL-NAME-EXAMPLE", resp.Data.FullName) + assert.Equal(t, "LT", resp.Data.SelectedCountry) + assert.False(t, *resp.Data.ManuallyDataChanged) + assert.Equal(t, models.AgeEstimate("OVER_25"), *resp.Data.AgeEstimate) + assert.Equal(t, "LOW", resp.Data.ClientIPProxyRiskLevel) + + // Original data + assert.Equal(t, "FIRST-NAME-EXAMPLE", resp.Data.OrgFirstName) + assert.Equal(t, "LAST-NAME-EXAMPLE", resp.Data.OrgLastName) + assert.Equal(t, "LIETUVOS", resp.Data.OrgNationality) + assert.Equal(t, "ŠILUVA", resp.Data.OrgBirthPlace) + + // File URLs + expectedURLs := map[string]string{ + "FRONT": "https://s3.eu-west-1.amazonaws.com/production.users.storage/users_storage/users//FRONT.png?AWSAccessKeyId=&Signature=&Expires=", + "BACK": "https://s3.eu-west-1.amazonaws.com/production.users.storage/users_storage/users//BACK.png?AWSAccessKeyId=&Signature=&Expires=", + "FACE": "https://s3.eu-west-1.amazonaws.com/production.users.storage/users_storage/users//FACE.png?AWSAccessKeyId=&Signature=&Expires=", + } + assert.Equal(t, expectedURLs, resp.FileUrls) + + // AML and LID checks + assert.Len(t, resp.AML, 1) + assert.Equal(t, "PilotApiAmlV2", resp.AML[0].ServiceName) + assert.Equal(t, "AML", resp.AML[0].ServiceGroupType) + assert.Equal(t, "OHT8GR5ESRF5XROWE5ZGCC123", resp.AML[0].UID) + assert.True(t, *resp.AML[0].Status.CheckSuccessful) + assert.Equal(t, "NOT_SUSPECTED", resp.AML[0].Status.OverallStatus) + + assert.Len(t, resp.LID, 1) + assert.Equal(t, "IrdInvalidPapers", resp.LID[0].ServiceName) + assert.Equal(t, "LID", resp.LID[0].ServiceGroupType) + assert.Equal(t, "OHT8GR5ESRF5XROWE5ZGCC123", resp.LID[0].UID) + assert.True(t, *resp.LID[0].Status.CheckSuccessful) + assert.Equal(t, "NOT_SUSPECTED", resp.LID[0].Status.OverallStatus) + + // Additional data + assert.Empty(t, resp.AdditionalStepPdfUrls) +} diff --git a/internal/clients/idenfy/testdata/webhook.1.json b/internal/clients/idenfy/testdata/webhook.1.json new file mode 100644 index 0000000..dd935f7 --- /dev/null +++ b/internal/clients/idenfy/testdata/webhook.1.json @@ -0,0 +1,109 @@ +{ + "clientId": "123", + "scanRef": "scan-ref", + "externalRef": "external-ref", + "platform": "MOBILE_APP", + "startTime": 1554726960, + "finishTime": 1554727002, + "clientIp": "192.0.2.0", + "clientIpCountry": "LT", + "clientLocation": "Kaunas, Lithuania", + "final": false, + "status": { + "overall": "APPROVED", + "suspicionReasons": [], + "mismatchTags": [], + "fraudTags": [], + "autoDocument": "DOC_VALIDATED", + "autoFace": "FACE_MATCH", + "manualDocument": "DOC_VALIDATED", + "manualFace": "FACE_MATCH", + "additionalSteps": null + }, + "data": { + "docFirstName": "FIRST-NAME-EXAMPLE", + "docLastName": "LAST-NAME-EXAMPLE", + "docNumber": "XXXXXXXXX", + "docPersonalCode": "XXXXXXXXX", + "docExpiry": "YYYY-MM-DD", + "docDob": "YYYY-MM-DD", + "docDateOfIssue": "2018-03-02", + "docType": "ID_CARD", + "docSex": "UNDEFINED", + "docNationality": "LT", + "docIssuingCountry": "LT", + "docTemporaryAddress": null, + "docBirthName": null, + "birthPlace": "BIRTH PLACE", + "authority": "AUTHORITY EXAMPLE", + "address": "ADDRESS EXAMPLE", + "fullName": "FULL-NAME-EXAMPLE", + "selectedCountry": "LT", + "mothersMaidenName": null, + "driverLicenseCategory": null, + "manuallyDataChanged": false, + "orgFirstName": "FIRST-NAME-EXAMPLE", + "orgLastName": "LAST-NAME-EXAMPLE", + "orgNationality": "LIETUVOS", + "orgBirthPlace": "ŠILUVA", + "orgAuthority": null, + "orgAddress": null, + "orgTemporaryAddress": null, + "orgMothersMaidenName": null, + "orgBirthName": null, + "ageEstimate": "OVER_25", + "clientIpProxyRiskLevel": "LOW", + "duplicateDocFaces": null, + "duplicateFaces": null, + "additionalData": { + "UTILITY_BILL": { + "ssn": { + "value": "ssn number", + "status": "MATCH" + } + } + }, + "manualAddress": null, + "manualAddressMatch": false, + "registryCenterCheck": null, + "addressVerification": null + }, + "fileUrls": { + "FRONT": "https://s3.eu-west-1.amazonaws.com/production.users.storage/users_storage/users//FRONT.png?AWSAccessKeyId=&Signature=&Expires=", + "BACK": "https://s3.eu-west-1.amazonaws.com/production.users.storage/users_storage/users//BACK.png?AWSAccessKeyId=&Signature=&Expires=", + "FACE": "https://s3.eu-west-1.amazonaws.com/production.users.storage/users_storage/users//FACE.png?AWSAccessKeyId=&Signature=&Expires=" + }, + "additionalStepPdfUrls": {}, + "AML": [ + { + "status": { + "serviceSuspected": false, + "checkSuccessful": true, + "serviceFound": true, + "serviceUsed": true, + "overallStatus": "NOT_SUSPECTED" + }, + "data": [], + "serviceName": "PilotApiAmlV2", + "serviceGroupType": "AML", + "uid": "OHT8GR5ESRF5XROWE5ZGCC123", + "errorMessage": null + } + ], + "LID": [ + { + "status": { + "serviceSuspected": false, + "checkSuccessful": true, + "serviceFound": true, + "serviceUsed": true, + "overallStatus": "NOT_SUSPECTED" + }, + "data": [], + "serviceName": "IrdInvalidPapers", + "serviceGroupType": "LID", + "uid": "OHT8GR5ESRF5XROWE5ZGCC123", + "errorMessage": null + } + ] + } \ No newline at end of file From c86a1a14a7ec608d673f31cbacbfcec2869e9f0a Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 02:44:55 +0300 Subject: [PATCH 023/105] implement proper error handling system --- api/docs/docs.go | 6 +-- api/docs/swagger.json | 6 +-- api/docs/swagger.yaml | 4 +- internal/configs/config.go | 5 +- internal/errors/errors.go | 88 +++++++++++++++++++++++++++++++ internal/handlers/handlers.go | 44 ++++++++++++++-- internal/middleware/middleware.go | 24 ++++----- internal/services/kyc_service.go | 59 +++++++++++---------- internal/services/services.go | 2 +- 9 files changed, 181 insertions(+), 57 deletions(-) create mode 100644 internal/errors/errors.go diff --git a/api/docs/docs.go b/api/docs/docs.go index a123206..52de91e 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -328,6 +328,9 @@ const docTemplate = `{ "fullName": { "type": "string" }, + "idenfyRef": { + "type": "string" + }, "manuallyDataChanged": { "type": "boolean" }, @@ -361,9 +364,6 @@ const docTemplate = `{ "orgTemporaryAddress": { "type": "string" }, - "scanRef": { - "type": "string" - }, "selectedCountry": { "type": "string" } diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 601a76c..6dcd1b0 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -321,6 +321,9 @@ "fullName": { "type": "string" }, + "idenfyRef": { + "type": "string" + }, "manuallyDataChanged": { "type": "boolean" }, @@ -354,9 +357,6 @@ "orgTemporaryAddress": { "type": "string" }, - "scanRef": { - "type": "string" - }, "selectedCountry": { "type": "string" } diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 8f5c774..faee039 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -81,6 +81,8 @@ definitions: type: array fullName: type: string + idenfyRef: + type: string manuallyDataChanged: type: boolean mothersMaidenName: @@ -103,8 +105,6 @@ definitions: type: string orgTemporaryAddress: type: string - scanRef: - type: string selectedCountry: type: string type: object diff --git a/internal/configs/config.go b/internal/configs/config.go index 418c94a..42c5c2c 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -1,8 +1,7 @@ package configs import ( - "fmt" - + "example.com/tfgrid-kyc-service/internal/errors" "github.com/ilyakaznacheev/cleanenv" ) @@ -57,7 +56,7 @@ func LoadConfig() (*Config, error) { cfg := &Config{} err := cleanenv.ReadEnv(cfg) if err != nil { - return nil, fmt.Errorf("error loading config: %w", err) + return nil, errors.NewInternalError("error loading config", err) } return cfg, nil } diff --git a/internal/errors/errors.go b/internal/errors/errors.go new file mode 100644 index 0000000..fc0ea57 --- /dev/null +++ b/internal/errors/errors.go @@ -0,0 +1,88 @@ +package errors + +import "fmt" + +// ErrorType represents the type of error +type ErrorType string + +const ( + // Error types + ErrorTypeValidation ErrorType = "VALIDATION_ERROR" + ErrorTypeAuthorization ErrorType = "AUTHORIZATION_ERROR" + ErrorTypeNotFound ErrorType = "NOT_FOUND" + ErrorTypeConflict ErrorType = "CONFLICT" + ErrorTypeInternal ErrorType = "INTERNAL_ERROR" + ErrorTypeExternal ErrorType = "EXTERNAL_SERVICE_ERROR" + ErrorTypeNotSufficientBalance ErrorType = "NOT_SUFFICIENT_BALANCE" +) + +// ServiceError represents a service-level error +type ServiceError struct { + Type ErrorType + Message 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", e.Type, e.Message) +} + +// Error constructors +func NewValidationError(message string, err error) *ServiceError { + return &ServiceError{ + Type: ErrorTypeValidation, + Message: message, + Err: err, + } +} + +func NewAuthorizationError(message string, err error) *ServiceError { + return &ServiceError{ + Type: ErrorTypeAuthorization, + Message: message, + Err: err, + } +} + +func NewNotFoundError(message string, err error) *ServiceError { + return &ServiceError{ + Type: ErrorTypeNotFound, + Message: message, + Err: err, + } +} + +func NewConflictError(message string, err error) *ServiceError { + return &ServiceError{ + Type: ErrorTypeConflict, + Message: message, + Err: err, + } +} + +func NewInternalError(message string, err error) *ServiceError { + return &ServiceError{ + Type: ErrorTypeInternal, + Message: message, + Err: err, + } +} + +func NewExternalError(message string, err error) *ServiceError { + return &ServiceError{ + Type: ErrorTypeExternal, + Message: message, + Err: err, + } +} + +func NewNotSufficientBalanceError(message string, err error) *ServiceError { + return &ServiceError{ + Type: ErrorTypeNotSufficientBalance, + Message: message, + Err: err, + } +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 397edc3..470b0b0 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -7,6 +7,7 @@ import ( "github.com/gofiber/fiber/v2" "go.uber.org/zap" + "example.com/tfgrid-kyc-service/internal/errors" "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" "example.com/tfgrid-kyc-service/internal/responses" @@ -39,7 +40,7 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { token, isNewToken, err := h.kycService.GetorCreateVerificationToken(c.Context(), clientID) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return handleError(c, err) } response := responses.NewTokenResponseWithStatus(token, isNewToken) if isNewToken { @@ -62,9 +63,9 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { func (h *Handler) GetVerificationData() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") - verification, err := h.kycService.GetVerification(c.Context(), clientID) + verification, err := h.kycService.GetVerificationData(c.Context(), clientID) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return handleError(c, err) } if verification == nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Verification not found"}) @@ -107,7 +108,7 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { zap.String("twinID", twinID), zap.Error(err), ) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return handleError(c, err) } if verification == nil { h.logger.Info("Verification not found", @@ -150,7 +151,7 @@ func (h *Handler) ProcessVerificationResult() fiber.Handler { h.logger.Debug("Verification update after decoding", zap.Any("result", result)) err = h.kycService.ProcessVerificationResult(c.Context(), body, sigHeader, result) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return handleError(c, err) } return c.SendStatus(fiber.StatusOK) } @@ -170,3 +171,36 @@ func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { return c.SendStatus(fiber.StatusNotImplemented) } } + +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()}) +} + +func handleServiceError(c *fiber.Ctx, err *errors.ServiceError) error { + statusCode := getStatusCode(err.Type) + return c.Status(statusCode).JSON(fiber.Map{ + "error": err.Message, + }) +} + +func getStatusCode(errorType errors.ErrorType) int { + switch errorType { + case errors.ErrorTypeValidation: + return fiber.StatusBadRequest + case errors.ErrorTypeAuthorization: + return fiber.StatusUnauthorized + case errors.ErrorTypeNotFound: + return fiber.StatusNotFound + case errors.ErrorTypeConflict: + return fiber.StatusConflict + case errors.ErrorTypeExternal: + return fiber.StatusBadGateway + case errors.ErrorTypeNotSufficientBalance: + return fiber.StatusPaymentRequired + default: + return fiber.StatusInternalServerError + } +} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 31f226f..0963fa0 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -1,11 +1,11 @@ package middleware import ( - "fmt" "strconv" "strings" "time" + "example.com/tfgrid-kyc-service/internal/errors" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" @@ -63,33 +63,33 @@ func fromHex(hex string) ([]byte, bool) { func VerifySubstrateSignature(address, signature, challenge string) error { challengeBytes, success := fromHex(challenge) if !success { - return fmt.Errorf("malformed challenge: failed to decode hex-encoded challenge") + return errors.NewAuthorizationError("malformed challenge: failed to decode hex-encoded challenge", nil) } // hex to string sig, success := fromHex(signature) if !success { - return fmt.Errorf("malformed signature: failed to decode hex-encoded signature") + return errors.NewAuthorizationError("malformed signature: failed to decode hex-encoded signature", nil) } // Convert address to public key _, pubkeyBytes, err := subkey.SS58Decode(address) if err != nil { - return fmt.Errorf("malformed address:failed to decode ss58 address: %w", err) + return errors.NewAuthorizationError("malformed address:failed to decode ss58 address", err) } // Create a new ed25519 public key pubkeyEd25519, err := ed25519.Scheme{}.FromPublicKey(pubkeyBytes) if err != nil { - return fmt.Errorf("error: can't create ed25519 public key: %w", err) + return errors.NewAuthorizationError("error: can't create 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 fmt.Errorf("error: can't create sr25519 public key: %w", err) + return errors.NewAuthorizationError("error: can't create sr25519 public key", err) } if !pubkeySr25519.Verify(challengeBytes, sig) { - return fmt.Errorf("bad signature: signature does not match") + return errors.NewAuthorizationError("bad signature: signature does not match", nil) } } @@ -100,27 +100,27 @@ func ValidateChallenge(address, signature, challenge, expectedDomain string, cha // Parse and validate the challenge challengeBytes, success := fromHex(challenge) if !success { - return fmt.Errorf("malformed challenge: failed to decode hex-encoded challenge") + return errors.NewValidationError("malformed challenge: failed to decode hex-encoded challenge", nil) } parts := strings.Split(string(challengeBytes), ":") if len(parts) != 2 { - return fmt.Errorf("malformed challenge: invalid challenge format") + return errors.NewValidationError("malformed challenge: invalid challenge format", nil) } // Check the domain if parts[0] != expectedDomain { - return fmt.Errorf("bad challenge: unexpected domain") + return errors.NewValidationError("bad challenge: unexpected domain", nil) } // Check the timestamp timestamp, err := strconv.ParseInt(parts[1], 10, 64) if err != nil { - return fmt.Errorf("bad challenge: invalid timestamp") + return errors.NewValidationError("bad challenge: invalid timestamp", nil) } // Check if the timestamp is within an acceptable range (e.g., last 1 minutes) if time.Now().Unix()-timestamp > challengeWindow { - return fmt.Errorf("bad challenge: challenge expired") + return errors.NewValidationError("bad challenge: challenge expired", nil) } return nil } diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index d01dc5e..123eca6 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -2,13 +2,13 @@ package services import ( "context" - "errors" "math/big" "time" "example.com/tfgrid-kyc-service/internal/clients/idenfy" "example.com/tfgrid-kyc-service/internal/clients/substrate" "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/errors" "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" "example.com/tfgrid-kyc-service/internal/repository" @@ -36,15 +36,15 @@ 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", zap.String("clientID", clientID), zap.Error(err)) - return nil, false, err + return nil, false, errors.NewInternalError("error getting verification status from database", err) // db error } if isVerified { - return nil, false, errors.New("user already verified") // TODO: implement a custom error that can be converted in the handler to a 400 status code + 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", zap.String("clientID", clientID), zap.Error(err)) - return nil, false, err + token, err_ := s.tokenRepo.GetToken(ctx, clientID) + if err_ != nil { + s.logger.Error("Error getting token from database", zap.String("clientID", clientID), zap.Error(err_)) + return nil, false, errors.NewInternalError("error getting token from database", err_) // db error } // check if token is found and not expired if token != nil { @@ -57,21 +57,21 @@ 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", zap.String("clientID", clientID), zap.Error(err)) - return nil, false, err // todo: implement a custom error that can be converted in the handler to a 500 status code + hasRequiredBalance, err_ := s.AccountHasRequiredBalance(ctx, clientID) + if err_ != nil { + s.logger.Error("Error checking if user account has required balance", zap.String("clientID", clientID), zap.Error(err_)) + return nil, false, errors.NewExternalError("error checking if user account has required balance", err_) } if !hasRequiredBalance { - return nil, false, errors.New("account does not have the required balance") // todo: implement a custom error that can be converted in the handler to a 402 status code + return nil, false, errors.NewNotSufficientBalanceError("account does not have the required balance", nil) } - newToken, err := s.idenfy.CreateVerificationSession(ctx, clientID) - if err != nil { - s.logger.Error("Error creating iDenfy verification session", zap.String("clientID", clientID), zap.Error(err)) - return nil, false, err + newToken, err_ := s.idenfy.CreateVerificationSession(ctx, clientID) + if err_ != nil { + s.logger.Error("Error creating iDenfy verification session", zap.String("clientID", clientID), zap.Error(err_)) + return nil, false, errors.NewExternalError("error creating iDenfy verification session", err_) } - err = s.tokenRepo.SaveToken(ctx, &newToken) - if err != nil { + err_ = s.tokenRepo.SaveToken(ctx, &newToken) + if err_ != nil { s.logger.Error("Error saving verification token to database", zap.String("clientID", clientID), zap.Error(err)) } @@ -83,8 +83,9 @@ 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", zap.String("clientID", clientID), zap.String("scanRef", scanRef), zap.Error(err)) + return errors.NewInternalError("error deleting verification token from database", err) } - return err + return nil } func (s *kycService) AccountHasRequiredBalance(ctx context.Context, address string) (bool, error) { @@ -95,7 +96,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", zap.String("address", address), zap.Error(err)) - return false, err + return false, errors.NewExternalError("error getting account balance", err) } return balance.Cmp(big.NewInt(int64(s.config.MinBalanceToVerifyAccount))) >= 0, nil } @@ -104,19 +105,20 @@ func (s *kycService) AccountHasRequiredBalance(ctx context.Context, address stri // verification related methods // --------------------------------------------------------------------------------------------------------------------- -func (s *kycService) GetVerification(ctx context.Context, clientID string) (*models.Verification, error) { +func (s *kycService) GetVerificationData(ctx context.Context, clientID string) (*models.Verification, error) { verification, err := s.verificationRepo.GetVerification(ctx, clientID) if err != nil { - return nil, err + s.logger.Error("Error getting verification from database", zap.String("clientID", clientID), zap.Error(err)) + return nil, errors.NewInternalError("error getting verification from database", err) } return verification, nil } func (s *kycService) GetVerificationStatus(ctx context.Context, clientID string) (*models.VerificationOutcome, error) { - verification, err := s.GetVerification(ctx, clientID) + verification, err := s.verificationRepo.GetVerification(ctx, clientID) if err != nil { s.logger.Error("Error getting verification from database", zap.String("clientID", clientID), zap.Error(err)) - return nil, err + return nil, errors.NewInternalError("error getting verification from database", err) } var outcome models.Outcome if verification != nil { @@ -141,7 +143,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", zap.String("twinID", twinID), zap.Error(err)) - return nil, err + return nil, errors.NewExternalError("error looking up twinID address from TFChain", err) } return s.GetVerificationStatus(ctx, address) } @@ -150,7 +152,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", zap.String("sigHeader", sigHeader), zap.Error(err)) - return err + return errors.NewAuthorizationError("error verifying callback signature", err) } // delete the token with the same clientID and same scanRef err = s.tokenRepo.DeleteToken(ctx, result.ClientID, result.IdenfyRef) @@ -162,7 +164,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", zap.String("clientID", result.ClientID), zap.String("scanRef", result.IdenfyRef), zap.Error(err)) - return err + return errors.NewInternalError("error saving verification to database", err) } } s.logger.Debug("Verification result processed successfully", zap.Any("result", result)) @@ -176,7 +178,8 @@ 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 { - return false, err + s.logger.Error("Error getting verification from database", zap.String("clientID", clientID), zap.Error(err)) + return false, errors.NewInternalError("error getting verification from database", err) } if verification == nil { return false, nil diff --git a/internal/services/services.go b/internal/services/services.go index 09990a9..24ea209 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -10,7 +10,7 @@ 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) - GetVerification(ctx context.Context, clientID string) (*models.Verification, 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 From 8cfb642b125cd1591269ae14c3c9bbb0d58279c3 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 03:05:50 +0300 Subject: [PATCH 024/105] fix IdenfyRef --- internal/models/verification.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/models/verification.go b/internal/models/verification.go index e9ed685..7320f1d 100644 --- a/internal/models/verification.go +++ b/internal/models/verification.go @@ -14,7 +14,7 @@ type Verification struct { Status Status `bson:"status" json:"status"` // required Data PersonData `bson:"data" json:"data"` // required FileUrls map[string]string `bson:"fileUrls" json:"fileUrls"` // required - IdenfyRef string `bson:"idenfyRef" json:"scanRef"` // required + IdenfyRef string `bson:"scanRef" json:"scanRef"` // required ClientID string `bson:"clientId" json:"clientId"` // required StartTime int64 `bson:"startTime" json:"startTime"` // required FinishTime int64 `bson:"finishTime" json:"finishTime"` // required From 6239e46127bc08b43844c3463db3f5b4af9a1265 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 03:13:56 +0300 Subject: [PATCH 025/105] clean logs --- internal/handlers/handlers.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 470b0b0..697504e 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -86,7 +86,6 @@ func (h *Handler) GetVerificationData() fiber.Handler { // @Router /api/v1/status [get] func (h *Handler) GetVerificationStatus() fiber.Handler { return func(c *fiber.Ctx) error { - h.logger.Debug("GetVerificationStatus request received", zap.Any("query", c.Queries())) clientID := c.Query("client_id") twinID := c.Query("twin_id") @@ -117,10 +116,6 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { ) return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Verification not found"}) } - h.logger.Info("Verification status retrieved successfully", - zap.String("clientID", clientID), - zap.String("twinID", twinID), - ) response := responses.NewVerificationStatusResponse(verification) return c.JSON(fiber.Map{"result": response}) } From 5eee819fc95aa4c5ebb8e8cb1770c9b421e88276 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 03:41:56 +0300 Subject: [PATCH 026/105] implement logging middleware --- internal/middleware/middleware.go | 61 ++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 0963fa0..e8274ca 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -6,19 +6,15 @@ import ( "time" "example.com/tfgrid-kyc-service/internal/errors" + "example.com/tfgrid-kyc-service/internal/logger" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/logger" "github.com/vedhavyas/go-subkey/v2" "github.com/vedhavyas/go-subkey/v2/ed25519" "github.com/vedhavyas/go-subkey/v2/sr25519" + "go.uber.org/zap" ) -// Logger returns a logger middleware -func Logger() fiber.Handler { - return logger.New() -} - // CORS returns a CORS middleware func CORS() fiber.Handler { return cors.New() @@ -124,3 +120,56 @@ func ValidateChallenge(address, signature, challenge, expectedDomain string, cha } return nil } + +func NewLoggingMiddleware(logger *logger.Logger) fiber.Handler { + return func(c *fiber.Ctx) error { + start := time.Now() + path := c.Path() + method := c.Method() + ip := c.IP() + + // Log request + logger.Info("Incoming request", + zap.String("method", method), + zap.String("path", path), + zap.Any("queries", c.Queries()), + zap.String("ip", ip), + zap.String("user_agent", string(c.Request().Header.UserAgent())), + zap.String("x-client-id header", c.Get("X-Client-ID")), + ) + + // Handle request + err := c.Next() + + // Calculate duration + duration := time.Since(start) + status := c.Response().StatusCode() + + // Get response size + responseSize := len(c.Response().Body()) + + // Log the response + logFields := []zap.Field{ + zap.String("method", method), + zap.String("path", path), + zap.String("ip", ip), + zap.Int("status", status), + zap.Duration("duration", duration), + zap.Int("response_size", responseSize), + } + + // Add error if present + if err != nil { + logFields = append(logFields, zap.Error(err)) + if status >= 500 { + logger.Error("Request failed", logFields...) + } else { + logger.Info("Request failed", logFields...) + } + } else { + logger.Info("Request completed", logFields...) + } + + return err + } +} From 411a67f45afaf4efe0947d45471bec0f4f86d430 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 03:42:19 +0300 Subject: [PATCH 027/105] fix nil pointer dereference because s.logger is nil --- internal/server/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 4e78ee3..08a3f02 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -111,7 +111,7 @@ func New(config *configs.Config, logger *logger.Logger) *Server { ) // Global middlewares - app.Use(middleware.Logger()) + app.Use(middleware.NewLoggingMiddleware(logger)) app.Use(middleware.CORS()) app.Use(recover.New()) app.Use(helmet.New()) @@ -153,7 +153,7 @@ func New(config *configs.Config, logger *logger.Logger) *Server { webhooks.Post("/verification-update", handler.ProcessVerificationResult()) webhooks.Post("/id-expiration", handler.ProcessDocExpirationNotification()) - return &Server{app: app, config: config} + return &Server{app: app, config: config, logger: logger} } func (s *Server) Start() { From dd28b210e6c293f77530853a74e6fee20a6a50ec Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 04:32:24 +0300 Subject: [PATCH 028/105] add errors to swag doc --- api/docs/docs.go | 68 +++++++++++++++++++++++++++++++++ api/docs/swagger.json | 68 +++++++++++++++++++++++++++++++++ api/docs/swagger.yaml | 45 ++++++++++++++++++++++ internal/handlers/handlers.go | 16 ++++++-- internal/responses/responses.go | 15 +------- 5 files changed, 196 insertions(+), 16 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 52de91e..c561fff 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -66,6 +66,24 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/responses.VerificationDataResponse" } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } } } } @@ -106,6 +124,24 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/responses.VerificationStatusResponse" } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } } } } @@ -162,6 +198,30 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/responses.TokenResponse" } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "402": { + "description": "Payment Required", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } } } } @@ -208,6 +268,14 @@ const docTemplate = `{ } }, "definitions": { + "responses.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, "responses.Outcome": { "type": "string", "enum": [ diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 6dcd1b0..03b373c 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -59,6 +59,24 @@ "schema": { "$ref": "#/definitions/responses.VerificationDataResponse" } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } } } } @@ -99,6 +117,24 @@ "schema": { "$ref": "#/definitions/responses.VerificationStatusResponse" } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } } } } @@ -155,6 +191,30 @@ "schema": { "$ref": "#/definitions/responses.TokenResponse" } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "402": { + "description": "Payment Required", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } } } } @@ -201,6 +261,14 @@ } }, "definitions": { + "responses.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, "responses.Outcome": { "type": "string", "enum": [ diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index faee039..af8a790 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -1,5 +1,10 @@ basePath: / definitions: + responses.ErrorResponse: + properties: + error: + type: string + type: object responses.Outcome: enum: - VERIFIED @@ -161,6 +166,18 @@ paths: description: OK schema: $ref: '#/definitions/responses.VerificationDataResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/responses.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/responses.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/responses.ErrorResponse' summary: Get Verification Data tags: - Verification @@ -188,6 +205,18 @@ paths: description: OK schema: $ref: '#/definitions/responses.VerificationStatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/responses.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/responses.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/responses.ErrorResponse' summary: Get Verification Status tags: - Verification @@ -227,6 +256,22 @@ paths: description: New token created schema: $ref: '#/definitions/responses.TokenResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/responses.ErrorResponse' + "402": + description: Payment Required + schema: + $ref: '#/definitions/responses.ErrorResponse' + "409": + description: Conflict + schema: + $ref: '#/definitions/responses.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/responses.ErrorResponse' summary: Get or Generate iDenfy Verification Token tags: - Token diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 697504e..c3fa09e 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -33,6 +33,10 @@ func NewHandler(kycService services.KYCService, logger *logger.Logger) *Handler // @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 401 {object} responses.ErrorResponse +// @Failure 402 {object} responses.ErrorResponse +// @Failure 409 {object} responses.ErrorResponse +// @Failure 500 {object} responses.ErrorResponse // @Router /api/v1/token [post] func (h *Handler) GetorCreateVerificationToken() fiber.Handler { return func(c *fiber.Ctx) error { @@ -58,7 +62,10 @@ 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 +// @Success 200 {object} responses.VerificationDataResponse +// @Failure 401 {object} responses.ErrorResponse +// @Failure 404 {object} responses.ErrorResponse +// @Failure 500 {object} responses.ErrorResponse // @Router /api/v1/data [get] func (h *Handler) GetVerificationData() fiber.Handler { return func(c *fiber.Ctx) error { @@ -82,7 +89,10 @@ 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 +// @Success 200 {object} responses.VerificationStatusResponse +// @Failure 400 {object} responses.ErrorResponse +// @Failure 404 {object} responses.ErrorResponse +// @Failure 500 {object} responses.ErrorResponse // @Router /api/v1/status [get] func (h *Handler) GetVerificationStatus() fiber.Handler { return func(c *fiber.Ctx) error { @@ -192,7 +202,7 @@ func getStatusCode(errorType errors.ErrorType) int { case errors.ErrorTypeConflict: return fiber.StatusConflict case errors.ErrorTypeExternal: - return fiber.StatusBadGateway + return fiber.StatusInternalServerError case errors.ErrorTypeNotSufficientBalance: return fiber.StatusPaymentRequired default: diff --git a/internal/responses/responses.go b/internal/responses/responses.go index b718f9a..c8a96c7 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -2,21 +2,10 @@ package responses import ( "example.com/tfgrid-kyc-service/internal/models" - "github.com/gofiber/fiber/v2" ) -type Response struct { - Success bool `json:"success"` - Data interface{} `json:"data,omitempty"` - Message string `json:"message,omitempty"` -} - -func SuccessResponse(c *fiber.Ctx, statusCode int, data interface{}, message string) error { - return c.Status(statusCode).JSON(Response{ - Success: true, - Data: data, - Message: message, - }) +type ErrorResponse struct { + Error string `json:"error"` } type TokenResponse struct { From 0ef2d869013879179b271d16a620f77016053a27 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 05:24:38 +0300 Subject: [PATCH 029/105] add simple health endpoint --- internal/configs/config.go | 5 +++++ internal/handlers/handlers.go | 18 ++++++++++++++++++ internal/server/server.go | 36 +++++++++++++++++++++-------------- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/internal/configs/config.go b/internal/configs/config.go index 42c5c2c..6e3319f 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -15,6 +15,7 @@ type Config struct { IDLimiter IDLimiter ChallengeWindow int64 `env:"CHALLENGE_WINDOW" env-default:"8"` Log Log + Encryption Encryption } type MongoDB struct { @@ -52,6 +53,10 @@ type Log struct { Debug bool `env:"DEBUG" env-default:"false"` } +type Encryption struct { + Key string `env:"ENCRYPTION_KEY" env-required:"true"` +} + func LoadConfig() (*Config, error) { cfg := &Config{} err := cleanenv.ReadEnv(cfg) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index c3fa09e..f6863dd 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -3,6 +3,7 @@ package handlers import ( "bytes" "encoding/json" + "time" "github.com/gofiber/fiber/v2" "go.uber.org/zap" @@ -177,6 +178,23 @@ func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { } } +// @Summary Health Check +// @Description Returns the health status of the service +// @Tags Health +// @Success 200 {object} responses.HealthResponse +// @Router /health [get] +func (h *Handler) HealthCheck(c *fiber.Ctx) error { + health := struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + }{ + Status: "ok", + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + + return c.JSON(health) +} + func handleError(c *fiber.Ctx, err error) error { if serviceErr, ok := err.(*errors.ServiceError); ok { return handleServiceError(c, serviceErr) diff --git a/internal/server/server.go b/internal/server/server.go index 08a3f02..3917c0c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,7 +1,9 @@ package server import ( + "context" "net" + "net/http" "os" "os/signal" "strings" @@ -35,7 +37,12 @@ type Server struct { func New(config *configs.Config, logger *logger.Logger) *Server { // debug log - app := fiber.New() + app := fiber.New(fiber.Config{ + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + BodyLimit: 512 * 1024, // 512KB + }) // Setup Limter Config and store ipLimiterstore := mongodb.New(mongodb.Config{ ConnectionURI: config.MongoDB.URI, @@ -157,21 +164,22 @@ func New(config *configs.Config, logger *logger.Logger) *Server { } func (s *Server) Start() { - // Start server go func() { - if err := s.app.Listen(":" + s.config.Server.Port); err != nil { - s.logger.Fatal("Failed to start server", zap.Error(err)) + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + <-sigChan + // Graceful shutdown + 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:", zap.Error(err)) } }() - // Graceful shutdown - quit := make(chan os.Signal, 1) - signal.Notify(quit, os.Interrupt, syscall.SIGTERM) - <-quit - s.logger.Info("Shutting down server...") - - if err := s.app.Shutdown(); err != nil { - s.logger.Fatal("Server forced to shutdown", zap.Error(err)) - } - s.logger.Info("Server exiting") + // Start server + if err := s.app.Listen(":" + s.config.Server.Port); err != nil && err != http.ErrServerClosed { + s.logger.Fatal("Server startup failed", zap.Error(err)) + } } From 207192728d071fd74cc65f4d2cbacbb7093ecb7a Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 05:27:27 +0300 Subject: [PATCH 030/105] update swag doc and healthResponse --- api/docs/docs.go | 28 ++++++++++++++++++++++++++++ api/docs/swagger.json | 28 ++++++++++++++++++++++++++++ api/docs/swagger.yaml | 18 ++++++++++++++++++ internal/handlers/handlers.go | 5 +---- internal/responses/responses.go | 5 +++++ 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index c561fff..62e2271 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -226,6 +226,23 @@ const docTemplate = `{ } } }, + "/health": { + "get": { + "description": "Returns the health status of the service", + "tags": [ + "Health" + ], + "summary": "Health Check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.HealthResponse" + } + } + } + } + }, "/webhooks/idenfy/id-expiration": { "post": { "description": "Processes the doc expiration notification for a client", @@ -276,6 +293,17 @@ const docTemplate = `{ } } }, + "responses.HealthResponse": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, "responses.Outcome": { "type": "string", "enum": [ diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 03b373c..3356f79 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -219,6 +219,23 @@ } } }, + "/health": { + "get": { + "description": "Returns the health status of the service", + "tags": [ + "Health" + ], + "summary": "Health Check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.HealthResponse" + } + } + } + } + }, "/webhooks/idenfy/id-expiration": { "post": { "description": "Processes the doc expiration notification for a client", @@ -269,6 +286,17 @@ } } }, + "responses.HealthResponse": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, "responses.Outcome": { "type": "string", "enum": [ diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index af8a790..b3ab79c 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -5,6 +5,13 @@ definitions: error: type: string type: object + responses.HealthResponse: + properties: + status: + type: string + timestamp: + type: string + type: object responses.Outcome: enum: - VERIFIED @@ -275,6 +282,17 @@ paths: summary: Get or Generate iDenfy Verification Token tags: - Token + /health: + get: + description: Returns the health status of the service + responses: + "200": + description: OK + schema: + $ref: '#/definitions/responses.HealthResponse' + summary: Health Check + tags: + - Health /webhooks/idenfy/id-expiration: post: consumes: diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index f6863dd..dce4e27 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -184,10 +184,7 @@ func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { // @Success 200 {object} responses.HealthResponse // @Router /health [get] func (h *Handler) HealthCheck(c *fiber.Ctx) error { - health := struct { - Status string `json:"status"` - Timestamp string `json:"timestamp"` - }{ + health := responses.HealthResponse{ Status: "ok", Timestamp: time.Now().UTC().Format(time.RFC3339), } diff --git a/internal/responses/responses.go b/internal/responses/responses.go index c8a96c7..5db0e45 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -8,6 +8,11 @@ type ErrorResponse struct { Error string `json:"error"` } +type HealthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` +} + type TokenResponse struct { Message string `json:"message"` AuthToken string `json:"authToken"` From 008e24cc0a9f42e3826578d61dcf2d028b7e3c41 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 05:38:22 +0300 Subject: [PATCH 031/105] update swag doc and healthResponse --- .app.env.example | 3 ++- internal/handlers/handlers.go | 14 ++++++++------ internal/server/server.go | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.app.env.example b/.app.env.example index 71e3981..a05a4ae 100644 --- a/.app.env.example +++ b/.app.env.example @@ -16,4 +16,5 @@ IP_LIMITER_MAX_TOKEN_REQUESTS=5 IP_LIMITER_TOKEN_EXPIRATION=24 ID_LIMITER_MAX_TOKEN_REQUESTS=5 ID_LIMITER_TOKEN_EXPIRATION=24 -DEBUG=false \ No newline at end of file +DEBUG=false +ENCRYPTION_KEY= \ No newline at end of file diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index dce4e27..1b9cb48 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -183,13 +183,15 @@ func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { // @Tags Health // @Success 200 {object} responses.HealthResponse // @Router /health [get] -func (h *Handler) HealthCheck(c *fiber.Ctx) error { - health := responses.HealthResponse{ - Status: "ok", - Timestamp: time.Now().UTC().Format(time.RFC3339), - } +func (h *Handler) HealthCheck() fiber.Handler { + return func(c *fiber.Ctx) error { + health := responses.HealthResponse{ + Status: "ok", + Timestamp: time.Now().UTC().Format(time.RFC3339), + } - return c.JSON(health) + return c.JSON(health) + } } func handleError(c *fiber.Ctx, err error) error { diff --git a/internal/server/server.go b/internal/server/server.go index 3917c0c..fd64474 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -154,7 +154,7 @@ func New(config *configs.Config, logger *logger.Logger) *Server { v1.Get("/data", middleware.AuthMiddleware(config.ChallengeWindow), handler.GetVerificationData()) // status route accepts either client_id or twin_id as query parameters v1.Get("/status", handler.GetVerificationStatus()) - + v1.Get("/health", handler.HealthCheck()) // Webhook routes webhooks := app.Group("/webhooks/idenfy") webhooks.Post("/verification-update", handler.ProcessVerificationResult()) From 551ae240d76d31ad63482d0f86eef2ef87b9e7f3 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 05:39:55 +0300 Subject: [PATCH 032/105] update swag doc for healthcheck endpoint --- api/docs/docs.go | 34 +++++++++++++++++----------------- api/docs/swagger.json | 34 +++++++++++++++++----------------- api/docs/swagger.yaml | 22 +++++++++++----------- internal/handlers/handlers.go | 2 +- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 62e2271..f759d39 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -88,6 +88,23 @@ const docTemplate = `{ } } }, + "/api/v1/health": { + "get": { + "description": "Returns the health status of the service", + "tags": [ + "Health" + ], + "summary": "Health Check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.HealthResponse" + } + } + } + } + }, "/api/v1/status": { "get": { "description": "Returns the verification status for a client", @@ -226,23 +243,6 @@ const docTemplate = `{ } } }, - "/health": { - "get": { - "description": "Returns the health status of the service", - "tags": [ - "Health" - ], - "summary": "Health Check", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.HealthResponse" - } - } - } - } - }, "/webhooks/idenfy/id-expiration": { "post": { "description": "Processes the doc expiration notification for a client", diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 3356f79..b37a9cd 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -81,6 +81,23 @@ } } }, + "/api/v1/health": { + "get": { + "description": "Returns the health status of the service", + "tags": [ + "Health" + ], + "summary": "Health Check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.HealthResponse" + } + } + } + } + }, "/api/v1/status": { "get": { "description": "Returns the verification status for a client", @@ -219,23 +236,6 @@ } } }, - "/health": { - "get": { - "description": "Returns the health status of the service", - "tags": [ - "Health" - ], - "summary": "Health Check", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.HealthResponse" - } - } - } - } - }, "/webhooks/idenfy/id-expiration": { "post": { "description": "Processes the doc expiration notification for a client", diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index b3ab79c..c2ae29a 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -188,6 +188,17 @@ paths: summary: Get Verification Data tags: - Verification + /api/v1/health: + get: + description: Returns the health status of the service + responses: + "200": + description: OK + schema: + $ref: '#/definitions/responses.HealthResponse' + summary: Health Check + tags: + - Health /api/v1/status: get: consumes: @@ -282,17 +293,6 @@ paths: summary: Get or Generate iDenfy Verification Token tags: - Token - /health: - get: - description: Returns the health status of the service - responses: - "200": - description: OK - schema: - $ref: '#/definitions/responses.HealthResponse' - summary: Health Check - tags: - - Health /webhooks/idenfy/id-expiration: post: consumes: diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 1b9cb48..5dd59c9 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -182,7 +182,7 @@ func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { // @Description Returns the health status of the service // @Tags Health // @Success 200 {object} responses.HealthResponse -// @Router /health [get] +// @Router /api/v1/health [get] func (h *Handler) HealthCheck() fiber.Handler { return func(c *fiber.Ctx) error { health := responses.HealthResponse{ From 7a41d444f9b68c38f3953fde7ed3a2bf35c284a1 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 05:54:24 +0300 Subject: [PATCH 033/105] return 0 balance when account is new --- internal/clients/substrate/substrate.go | 3 +++ internal/configs/config.go | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index 1c6e35e..506bf76 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -43,6 +43,9 @@ func (c *Substrate) GetAccountBalance(address string) (*big.Int, error) { 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 nil, fmt.Errorf("failed to get balance: %w", err) } diff --git a/internal/configs/config.go b/internal/configs/config.go index 6e3319f..aa3a216 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -37,9 +37,9 @@ type TFChain struct { WsProviderURL string `env:"TFCHAIN_WS_PROVIDER_URL" env-default:"wss://tfchain.grid.tf"` } type Verification struct { - SuspiciousVerificationOutcome string `env:"SUSPICIOUS_VERIFICATION_OUTCOME" env-default:"verified"` - ExpiredDocumentOutcome string `env:"EXPIRED_DOCUMENT_OUTCOME" env-default:"unverified"` - MinBalanceToVerifyAccount uint64 `env:"MIN_BALANCE_TO_VERIFY_ACCOUNT" env-default:"10000000"` + SuspiciousVerificationOutcome string `env:"VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME" env-default:"verified"` + ExpiredDocumentOutcome string `env:"VERIFICATION_EXPIRED_DOCUMENT_OUTCOME" env-default:"unverified"` + MinBalanceToVerifyAccount uint64 `env:"VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT" env-default:"10000000"` } type IPLimiter struct { MaxTokenRequests int `env:"IP_LIMITER_MAX_TOKEN_REQUESTS" env-default:"4"` From 2281d211a26f5e668d8863647e47096738d16188 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 06:35:12 +0300 Subject: [PATCH 034/105] log body as json in the callback handler --- internal/handlers/handlers.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 5dd59c9..3d964f3 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -2,6 +2,7 @@ package handlers import ( "bytes" + "encoding/base64" "encoding/json" "time" @@ -141,7 +142,11 @@ 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", zap.Any("body", c.Body()), zap.Any("headers", &c.Request().Header)) + // decode base64 to string + dst := make([]byte, base64.StdEncoding.DecodedLen(len(c.Body()))) + base64.StdEncoding.Decode(dst, c.Body()) + h.logger.Debug("Received verification update", zap.Any("body", string(dst)), zap.Any("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"}) From 37d549fa022c624258fb5c6727373883e3273e38 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 14:55:10 +0300 Subject: [PATCH 035/105] Update error staus code --- internal/middleware/middleware.go | 24 +++++++++++++++++------- internal/responses/responses.go | 12 ++++++++++-- internal/server/server.go | 6 +++--- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index e8274ca..4d8dbd6 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -6,6 +6,7 @@ import ( "time" "example.com/tfgrid-kyc-service/internal/errors" + "example.com/tfgrid-kyc-service/internal/handlers" "example.com/tfgrid-kyc-service/internal/logger" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" @@ -28,7 +29,7 @@ func AuthMiddleware(challengeWindow int64) fiber.Handler { challenge := c.Get("X-Challenge") if clientID == "" || signature == "" || challenge == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Missing authentication credentials", }) } @@ -36,13 +37,22 @@ func AuthMiddleware(challengeWindow int64) fiber.Handler { // Verify the clientID and signature here err := ValidateChallenge(clientID, signature, challenge, "kyc1.gent01.dev.grid.tf", challengeWindow) if err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + // cast error to service error and convert it to http status code + serviceError, ok := err.(*errors.ServiceError) + if ok { + return handlers.HandleServiceError(c, serviceError) + } + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": err.Error(), }) } // Verify the signature err = VerifySubstrateSignature(clientID, signature, challenge) if err != nil { + serviceError, ok := err.(*errors.ServiceError) + if ok { + return handlers.HandleServiceError(c, serviceError) + } return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": err.Error(), }) @@ -59,30 +69,30 @@ func fromHex(hex string) ([]byte, bool) { func VerifySubstrateSignature(address, signature, challenge string) error { challengeBytes, success := fromHex(challenge) if !success { - return errors.NewAuthorizationError("malformed challenge: failed to decode hex-encoded challenge", nil) + return errors.NewValidationError("malformed challenge: failed to decode hex-encoded challenge", nil) } // hex to string sig, success := fromHex(signature) if !success { - return errors.NewAuthorizationError("malformed signature: failed to decode hex-encoded signature", nil) + return errors.NewValidationError("malformed signature: failed to decode hex-encoded signature", nil) } // Convert address to public key _, pubkeyBytes, err := subkey.SS58Decode(address) if err != nil { - return errors.NewAuthorizationError("malformed address:failed to decode ss58 address", err) + return errors.NewValidationError("malformed address:failed to decode ss58 address", err) } // Create a new ed25519 public key pubkeyEd25519, err := ed25519.Scheme{}.FromPublicKey(pubkeyBytes) if err != nil { - return errors.NewAuthorizationError("error: can't create ed25519 public key", err) + return errors.NewValidationError("error: can't create 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.NewAuthorizationError("error: can't create sr25519 public key", err) + return errors.NewValidationError("error: can't create sr25519 public key", err) } if !pubkeySr25519.Verify(challengeBytes, sig) { return errors.NewAuthorizationError("bad signature: signature does not match", nil) diff --git a/internal/responses/responses.go b/internal/responses/responses.go index 5db0e45..d1fc14d 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -8,9 +8,17 @@ type ErrorResponse struct { Error string `json:"error"` } +type HealthStatus string + +const ( + HealthStatusHealthy HealthStatus = "Healthy" + HealthStatusDegraded HealthStatus = "Degraded" +) + type HealthResponse struct { - Status string `json:"status"` - Timestamp string `json:"timestamp"` + Status HealthStatus `json:"status"` + Timestamp string `json:"timestamp"` + Errors []string `json:"errors"` } type TokenResponse struct { diff --git a/internal/server/server.go b/internal/server/server.go index fd64474..c6b58fb 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -50,7 +50,7 @@ func New(config *configs.Config, logger *logger.Logger) *Server { Collection: "ip_limit", Reset: false, }) - ipLimiterConfig := limiter.Config{ // TODO: use configurable parameters, also check if it works well after passing the request through an SSL gateway + ipLimiterConfig := limiter.Config{ Max: config.IPLimiter.MaxTokenRequests, Expiration: time.Duration(config.IPLimiter.TokenExpiration) * time.Minute, SkipFailedRequests: false, @@ -100,7 +100,7 @@ func New(config *configs.Config, logger *logger.Logger) *Server { Reset: false, }) - idLimiterConfig := limiter.Config{ // TODO: use configurable parameters + idLimiterConfig := limiter.Config{ Max: config.IDLimiter.MaxTokenRequests, Expiration: time.Duration(config.IDLimiter.TokenExpiration) * time.Minute, SkipFailedRequests: false, @@ -154,7 +154,7 @@ func New(config *configs.Config, logger *logger.Logger) *Server { v1.Get("/data", middleware.AuthMiddleware(config.ChallengeWindow), handler.GetVerificationData()) // status route accepts either client_id or twin_id as query parameters v1.Get("/status", handler.GetVerificationStatus()) - v1.Get("/health", handler.HealthCheck()) + v1.Get("/health", handler.HealthCheck(db)) // Webhook routes webhooks := app.Group("/webhooks/idenfy") webhooks.Post("/verification-update", handler.ProcessVerificationResult()) From 9c15d5389a470137da42a903550d26b66add9c63 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 14:55:28 +0300 Subject: [PATCH 036/105] Ping db in health check --- internal/handlers/handlers.go | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 3d964f3..2867152 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -2,11 +2,14 @@ package handlers import ( "bytes" + "context" "encoding/base64" "encoding/json" "time" "github.com/gofiber/fiber/v2" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/readpref" "go.uber.org/zap" "example.com/tfgrid-kyc-service/internal/errors" @@ -46,7 +49,7 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { token, isNewToken, err := h.kycService.GetorCreateVerificationToken(c.Context(), clientID) if err != nil { - return handleError(c, err) + return HandleError(c, err) } response := responses.NewTokenResponseWithStatus(token, isNewToken) if isNewToken { @@ -74,7 +77,7 @@ func (h *Handler) GetVerificationData() fiber.Handler { clientID := c.Get("X-Client-ID") verification, err := h.kycService.GetVerificationData(c.Context(), clientID) if err != nil { - return handleError(c, err) + return HandleError(c, err) } if verification == nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Verification not found"}) @@ -119,7 +122,7 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { zap.String("twinID", twinID), zap.Error(err), ) - return handleError(c, err) + return HandleError(c, err) } if verification == nil { h.logger.Info("Verification not found", @@ -162,7 +165,7 @@ func (h *Handler) ProcessVerificationResult() fiber.Handler { h.logger.Debug("Verification update after decoding", zap.Any("result", result)) err = h.kycService.ProcessVerificationResult(c.Context(), body, sigHeader, result) if err != nil { - return handleError(c, err) + return HandleError(c, err) } return c.SendStatus(fiber.StatusOK) } @@ -188,25 +191,38 @@ func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { // @Tags Health // @Success 200 {object} responses.HealthResponse // @Router /api/v1/health [get] -func (h *Handler) HealthCheck() fiber.Handler { +func (h *Handler) HealthCheck(dbClient *mongo.Client) fiber.Handler { return func(c *fiber.Ctx) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + err := dbClient.Ping(ctx, readpref.Primary()) + if err != nil { + // status degraded + health := responses.HealthResponse{ + Status: responses.HealthStatusDegraded, + Timestamp: time.Now().UTC().Format(time.RFC3339), + Errors: []string{err.Error()}, + } + return c.JSON(health) + } health := responses.HealthResponse{ - Status: "ok", + Status: responses.HealthStatusHealthy, Timestamp: time.Now().UTC().Format(time.RFC3339), + Errors: []string{}, } return c.JSON(health) } } -func handleError(c *fiber.Ctx, err error) error { +func HandleError(c *fiber.Ctx, err error) error { if serviceErr, ok := err.(*errors.ServiceError); ok { - return handleServiceError(c, serviceErr) + return HandleServiceError(c, serviceErr) } return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } -func handleServiceError(c *fiber.Ctx, err *errors.ServiceError) error { +func HandleServiceError(c *fiber.Ctx, err *errors.ServiceError) error { statusCode := getStatusCode(err.Type) return c.Status(statusCode).JSON(fiber.Map{ "error": err.Message, From 81eb7096efa4056ee7d8bd034f16eee06442dfdb Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 15:02:08 +0300 Subject: [PATCH 037/105] remove encryption config --- internal/configs/config.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/configs/config.go b/internal/configs/config.go index aa3a216..f483839 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -15,7 +15,6 @@ type Config struct { IDLimiter IDLimiter ChallengeWindow int64 `env:"CHALLENGE_WINDOW" env-default:"8"` Log Log - Encryption Encryption } type MongoDB struct { @@ -53,10 +52,6 @@ type Log struct { Debug bool `env:"DEBUG" env-default:"false"` } -type Encryption struct { - Key string `env:"ENCRYPTION_KEY" env-required:"true"` -} - func LoadConfig() (*Config, error) { cfg := &Config{} err := cleanenv.ReadEnv(cfg) From 3d28d95c1fd6cbdcf9b1a5235f38a4575b3de16d Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 16:10:05 +0300 Subject: [PATCH 038/105] sperating idenfy data per tfchain network --- .app.env.example | 2 +- cmd/api/main.go | 2 +- internal/clients/idenfy/idenfy.go | 3 +++ internal/clients/substrate/substrate.go | 13 +++++++++++ internal/configs/config.go | 1 + internal/server/server.go | 5 ----- internal/services/kyc_service.go | 20 ++++++++++++++--- scripts/dev/chain/chain_name.go | 29 +++++++++++++++++++++++++ 8 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 scripts/dev/chain/chain_name.go diff --git a/.app.env.example b/.app.env.example index a05a4ae..2a70fbc 100644 --- a/.app.env.example +++ b/.app.env.example @@ -17,4 +17,4 @@ IP_LIMITER_TOKEN_EXPIRATION=24 ID_LIMITER_MAX_TOKEN_REQUESTS=5 ID_LIMITER_TOKEN_EXPIRATION=24 DEBUG=false -ENCRYPTION_KEY= \ No newline at end of file +IDENFY_CALLBACK_URL=https://[TF_KYC_SERVICE_DOMAIN_NAME_HERE]/webhooks/idenfy/verification-update \ No newline at end of file diff --git a/cmd/api/main.go b/cmd/api/main.go index c10e270..62e26b5 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -29,7 +29,7 @@ func main() { logger := logger.GetLogger() defer logger.Sync() - logger.Debug("Configuration loaded successfully", zap.Any("config", config)) + logger.Debug("Configuration loaded successfully", zap.Any("config", config)) // TODO: remove me after testing server := server.New(config, logger) logger.Info("Starting server on port:", zap.String("port", config.Server.Port)) diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index 416bcea..2872746 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -23,6 +23,7 @@ type Idenfy struct { secretKey string baseURL string callbackSignKey []byte + callbackUrl string devMode bool logger *logger.Logger } @@ -38,6 +39,7 @@ func New(config configs.Idenfy, logger *logger.Logger) *Idenfy { accessKey: config.APIKey, secretKey: config.APISecret, callbackSignKey: []byte(config.CallbackSignKey), + callbackUrl: config.CallbackUrl, devMode: config.DevMode, logger: logger, } @@ -112,6 +114,7 @@ func (c *Idenfy) createVerificationSessionRequestBody(clientID string, devMode b RequestBody := map[string]interface{}{ "clientId": clientID, "generateDigitString": true, + "callbackUrl": c.callbackUrl, } if devMode { RequestBody["expiryTime"] = 30 diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index 506bf76..284b3a7 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -63,3 +63,16 @@ func (c *Substrate) GetAddressByTwinID(twinID string) (string, error) { } return twin.Account.String(), nil } + +// get chain name from ws provider url +func (c *Substrate) GetChainName() (string, error) { + api, _, err := c.api.GetClient() + if err != nil { + return "", fmt.Errorf("failed to get substrate client: %w", err) + } + chain, err := api.RPC.System.Chain() + if err != nil { + return "", fmt.Errorf("failed to get chain: %w", err) + } + return string(chain), nil +} diff --git a/internal/configs/config.go b/internal/configs/config.go index f483839..9984231 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -31,6 +31,7 @@ type Idenfy struct { CallbackSignKey string `env:"IDENFY_CALLBACK_SIGN_KEY" env-required:"true"` WhitelistedIPs []string `env:"IDENFY_WHITELISTED_IPS" env-separator:","` DevMode bool `env:"IDENFY_DEV_MODE" env-default:"false"` + CallbackUrl string `env:"IDENFY_CALLBACK_URL" env-required:"false"` } type TFChain struct { WsProviderURL string `env:"TFCHAIN_WS_PROVIDER_URL" env-default:"wss://tfchain.grid.tf"` diff --git a/internal/server/server.go b/internal/server/server.go index c6b58fb..4335496 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -112,11 +112,6 @@ func New(config *configs.Config, logger *logger.Logger) *Server { }, } - logger.Info("Limiter configurations", - zap.Any("ipLimiter", ipLimiterConfig), - zap.Any("idLimiter", idLimiterConfig), - ) - // Global middlewares app.Use(middleware.NewLoggingMiddleware(logger)) app.Use(middleware.CORS()) diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index 123eca6..5ef82cf 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -3,6 +3,7 @@ package services import ( "context" "math/big" + "strings" "time" "example.com/tfgrid-kyc-service/internal/clients/idenfy" @@ -22,10 +23,17 @@ type kycService struct { substrate *substrate.Substrate config *configs.Verification logger *logger.Logger + IdenfySuffix string } func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.Verification, logger *logger.Logger) KYCService { - return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: config, logger: logger} + chainName, err := substrateClient.GetChainName() + if err != nil { + panic(errors.NewInternalError("error getting chain name", err)) + } + chainNameParts := strings.Split(chainName, " ") + chainNetworkName := strings.ToLower(chainNameParts[len(chainNameParts)-1]) + return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: config, logger: logger, IdenfySuffix: chainNetworkName} } // --------------------------------------------------------------------------------------------------------------------- @@ -65,11 +73,15 @@ func (s *kycService) GetorCreateVerificationToken(ctx context.Context, clientID if !hasRequiredBalance { return nil, false, errors.NewNotSufficientBalanceError("account does not have the required balance", nil) } - newToken, err_ := s.idenfy.CreateVerificationSession(ctx, clientID) + // 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", zap.String("clientID", clientID), zap.Error(err_)) + s.logger.Error("Error creating iDenfy verification session", zap.String("clientID", clientID), zap.String("uniqueClientID", uniqueClientID), zap.Error(err_)) return nil, false, errors.NewExternalError("error 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", zap.String("clientID", clientID), zap.Error(err)) @@ -161,6 +173,8 @@ func (s *kycService) ProcessVerificationResult(ctx context.Context, body []byte, } // 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 + result.ClientID = strings.Split(result.ClientID, ":")[0] // TODO: should we check if it have correct suffix? callback misconfiguration maybe? err = s.verificationRepo.SaveVerification(ctx, &result) if err != nil { s.logger.Error("Error saving verification to database", zap.String("clientID", result.ClientID), zap.String("scanRef", result.IdenfyRef), zap.Error(err)) diff --git a/scripts/dev/chain/chain_name.go b/scripts/dev/chain/chain_name.go new file mode 100644 index 0000000..cc5599f --- /dev/null +++ b/scripts/dev/chain/chain_name.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + + "example.com/tfgrid-kyc-service/internal/clients/substrate" + "example.com/tfgrid-kyc-service/internal/configs" + "example.com/tfgrid-kyc-service/internal/logger" +) + +func main() { + config, err := configs.LoadConfig() + if err != nil { + panic(err) + } + logger.Init(config.Log) + logger := logger.GetLogger() + substrateClient, err := substrate.New(config.TFChain, logger) + if err != nil { + panic(err) + } + + chainName, err := substrateClient.GetChainName() + if err != nil { + panic(err) + } + fmt.Println(chainName) + +} From d881f60a9c3714e552e82864a022cef2fc133805 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 16:32:22 +0300 Subject: [PATCH 039/105] fix body logging --- internal/handlers/handlers.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 2867152..dc02ea0 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -3,7 +3,6 @@ package handlers import ( "bytes" "context" - "encoding/base64" "encoding/json" "time" @@ -145,11 +144,7 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { // @Router /webhooks/idenfy/verification-update [post] func (h *Handler) ProcessVerificationResult() fiber.Handler { return func(c *fiber.Ctx) error { - // decode base64 to string - dst := make([]byte, base64.StdEncoding.DecodedLen(len(c.Body()))) - base64.StdEncoding.Decode(dst, c.Body()) - h.logger.Debug("Received verification update", zap.Any("body", string(dst)), zap.Any("headers", &c.Request().Header)) - + h.logger.Debug("Received verification update", zap.Any("body", string(c.Body())), zap.Any("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"}) From 559c18cfbd7610e49b3e1231fab67af6deae8e1c Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 16:41:10 +0300 Subject: [PATCH 040/105] update swag docs --- api/docs/docs.go | 31 ++++++++++++++++++++++++++++++- api/docs/swagger.json | 31 ++++++++++++++++++++++++++++++- api/docs/swagger.yaml | 22 +++++++++++++++++++++- internal/handlers/handlers.go | 2 ++ 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index f759d39..44b4626 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -67,6 +67,12 @@ const docTemplate = `{ "$ref": "#/definitions/responses.VerificationDataResponse" } }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, "401": { "description": "Unauthorized", "schema": { @@ -216,6 +222,12 @@ const docTemplate = `{ "$ref": "#/definitions/responses.TokenResponse" } }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, "401": { "description": "Unauthorized", "schema": { @@ -296,14 +308,31 @@ const docTemplate = `{ "responses.HealthResponse": { "type": "object", "properties": { + "errors": { + "type": "array", + "items": { + "type": "string" + } + }, "status": { - "type": "string" + "$ref": "#/definitions/responses.HealthStatus" }, "timestamp": { "type": "string" } } }, + "responses.HealthStatus": { + "type": "string", + "enum": [ + "Healthy", + "Degraded" + ], + "x-enum-varnames": [ + "HealthStatusHealthy", + "HealthStatusDegraded" + ] + }, "responses.Outcome": { "type": "string", "enum": [ diff --git a/api/docs/swagger.json b/api/docs/swagger.json index b37a9cd..ff3e325 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -60,6 +60,12 @@ "$ref": "#/definitions/responses.VerificationDataResponse" } }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, "401": { "description": "Unauthorized", "schema": { @@ -209,6 +215,12 @@ "$ref": "#/definitions/responses.TokenResponse" } }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.ErrorResponse" + } + }, "401": { "description": "Unauthorized", "schema": { @@ -289,14 +301,31 @@ "responses.HealthResponse": { "type": "object", "properties": { + "errors": { + "type": "array", + "items": { + "type": "string" + } + }, "status": { - "type": "string" + "$ref": "#/definitions/responses.HealthStatus" }, "timestamp": { "type": "string" } } }, + "responses.HealthStatus": { + "type": "string", + "enum": [ + "Healthy", + "Degraded" + ], + "x-enum-varnames": [ + "HealthStatusHealthy", + "HealthStatusDegraded" + ] + }, "responses.Outcome": { "type": "string", "enum": [ diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index c2ae29a..dc4febc 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -7,11 +7,23 @@ definitions: type: object responses.HealthResponse: properties: + errors: + items: + type: string + type: array status: - type: string + $ref: '#/definitions/responses.HealthStatus' timestamp: type: string type: object + responses.HealthStatus: + enum: + - Healthy + - Degraded + type: string + x-enum-varnames: + - HealthStatusHealthy + - HealthStatusDegraded responses.Outcome: enum: - VERIFIED @@ -173,6 +185,10 @@ paths: description: OK schema: $ref: '#/definitions/responses.VerificationDataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/responses.ErrorResponse' "401": description: Unauthorized schema: @@ -274,6 +290,10 @@ paths: description: New token created schema: $ref: '#/definitions/responses.TokenResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/responses.ErrorResponse' "401": description: Unauthorized schema: diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index dc02ea0..7fd2b98 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -37,6 +37,7 @@ func NewHandler(kycService services.KYCService, logger *logger.Logger) *Handler // @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 @@ -67,6 +68,7 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { // @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 From f46e520c12bed445b25d8daf31d151b1eb9bac4b Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 18:31:40 +0300 Subject: [PATCH 041/105] fix deleting token in ProcessVerificationResult --- internal/services/kyc_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index 5ef82cf..de5011e 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -167,6 +167,7 @@ func (s *kycService) ProcessVerificationResult(ctx context.Context, body []byte, return errors.NewAuthorizationError("error verifying callback signature", err) } // delete the token with the same clientID and same scanRef + result.ClientID = strings.Split(result.ClientID, ":")[0] // TODO: should we check if it have correct suffix? callback misconfiguration maybe? err = s.tokenRepo.DeleteToken(ctx, result.ClientID, result.IdenfyRef) if err != nil { s.logger.Warn("Error deleting verification token from database", zap.String("clientID", result.ClientID), zap.String("scanRef", result.IdenfyRef), zap.Error(err)) @@ -174,7 +175,6 @@ func (s *kycService) ProcessVerificationResult(ctx context.Context, body []byte, // 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 - result.ClientID = strings.Split(result.ClientID, ":")[0] // TODO: should we check if it have correct suffix? callback misconfiguration maybe? err = s.verificationRepo.SaveVerification(ctx, &result) if err != nil { s.logger.Error("Error saving verification to database", zap.String("clientID", result.ClientID), zap.String("scanRef", result.IdenfyRef), zap.Error(err)) From f8131dff21fa3a1375e7c0de81b3a6d8a0c8bea0 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 20:25:35 +0300 Subject: [PATCH 042/105] use the OutcomeRejected enum in place of string value --- internal/responses/responses.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/responses/responses.go b/internal/responses/responses.go index d1fc14d..dfdf9ba 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -106,7 +106,7 @@ func NewTokenResponseWithStatus(token *models.Token, isNewToken bool) *TokenResp func NewVerificationStatusResponse(verificationOutcome *models.VerificationOutcome) *VerificationStatusResponse { outcome := OutcomeVerified - if verificationOutcome.Outcome == "REJECTED" { + if verificationOutcome.Outcome == models.OutcomeRejected { outcome = OutcomeRejected } return &VerificationStatusResponse{ From f586dc4aafa955b9e4b2759288af18d9be10deee Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 20:25:59 +0300 Subject: [PATCH 043/105] add CI workflow to build docker image --- .github/workflows/docker-build.yml | 58 ++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..dc9cdb7 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,58 @@ +name: Build and Publish Docker Image + +on: + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Generate tags + id: tags + run: | + if [ "${{ github.event_name }}" = "release" ]; then + VERSION=${GITHUB_REF#refs/tags/} + echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_OUTPUT + else + SHA=$(git rev-parse --short HEAD) + echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:edge-${SHA},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:edge-latest" >> $GITHUB_OUTPUT + fi + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.tags.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 18c9ac040b6da17e508698d2c75b8f3d568a5a3a Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 20:53:25 +0300 Subject: [PATCH 044/105] make expected domain in challenge configurabl --- .app.env.example | 1 + internal/configs/config.go | 22 +++++++++++++--------- internal/middleware/middleware.go | 5 +++-- internal/server/server.go | 4 ++-- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.app.env.example b/.app.env.example index 2a70fbc..56f5569 100644 --- a/.app.env.example +++ b/.app.env.example @@ -2,6 +2,7 @@ MONGO_URI=mongodb://root:password@mongodb:27017 DATABASE_NAME=tfgrid-kyc-db PORT=8080 CHALLENGE_WINDOW=120 +CHALLENGE_DOMAIN=kyc1.gent01.dev.grid.tf VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME=verified VERIFICATION_EXPIRED_DOCUMENT_OUTCOME=verified VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT=1000000 diff --git a/internal/configs/config.go b/internal/configs/config.go index 9984231..92c20ba 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -6,15 +6,15 @@ import ( ) type Config struct { - MongoDB MongoDB - Server Server - Idenfy Idenfy - TFChain TFChain - Verification Verification - IPLimiter IPLimiter - IDLimiter IDLimiter - ChallengeWindow int64 `env:"CHALLENGE_WINDOW" env-default:"8"` - Log Log + MongoDB MongoDB + Server Server + Idenfy Idenfy + TFChain TFChain + Verification Verification + IPLimiter IPLimiter + IDLimiter IDLimiter + Challenge Challenge + Log Log } type MongoDB struct { @@ -52,6 +52,10 @@ type IDLimiter struct { type Log struct { Debug bool `env:"DEBUG" env-default:"false"` } +type Challenge struct { + Window int64 `env:"CHALLENGE_WINDOW" env-default:"8"` + Domain string `env:"CHALLENGE_DOMAIN" env-required:"true"` +} func LoadConfig() (*Config, error) { cfg := &Config{} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 4d8dbd6..7abf0bf 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "example.com/tfgrid-kyc-service/internal/configs" "example.com/tfgrid-kyc-service/internal/errors" "example.com/tfgrid-kyc-service/internal/handlers" "example.com/tfgrid-kyc-service/internal/logger" @@ -22,7 +23,7 @@ func CORS() fiber.Handler { } // AuthMiddleware is a middleware that validates the authentication credentials -func AuthMiddleware(challengeWindow int64) fiber.Handler { +func AuthMiddleware(config configs.Challenge) fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") signature := c.Get("X-Signature") @@ -35,7 +36,7 @@ func AuthMiddleware(challengeWindow int64) fiber.Handler { } // Verify the clientID and signature here - err := ValidateChallenge(clientID, signature, challenge, "kyc1.gent01.dev.grid.tf", challengeWindow) + err := ValidateChallenge(clientID, signature, challenge, config.Domain, config.Window) if err != nil { // cast error to service error and convert it to http status code serviceError, ok := err.(*errors.ServiceError) diff --git a/internal/server/server.go b/internal/server/server.go index 4335496..eb0140c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -145,8 +145,8 @@ func New(config *configs.Config, logger *logger.Logger) *Server { app.Get("/docs/*", swagger.HandlerDefault) v1 := app.Group("/api/v1") - v1.Post("/token", middleware.AuthMiddleware(config.ChallengeWindow), limiter.New(idLimiterConfig), limiter.New(ipLimiterConfig), handler.GetorCreateVerificationToken()) - v1.Get("/data", middleware.AuthMiddleware(config.ChallengeWindow), handler.GetVerificationData()) + v1.Post("/token", middleware.AuthMiddleware(config.Challenge), limiter.New(idLimiterConfig), limiter.New(ipLimiterConfig), handler.GetorCreateVerificationToken()) + v1.Get("/data", middleware.AuthMiddleware(config.Challenge), handler.GetVerificationData()) // status route accepts either client_id or twin_id as query parameters v1.Get("/status", handler.GetVerificationStatus()) v1.Get("/health", handler.HealthCheck(db)) From 9dcf0c73a48e8647ba2fb27aef13d51a15464e9d Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 21:28:46 +0300 Subject: [PATCH 045/105] Update services names and default db name --- docker-compose.yml | 12 ++++++------ internal/configs/config.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index bba1b52..dd20893 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.8' services: - tfgrid_kyc_api: + tf_kyc_api: build: context: . dockerfile: Dockerfile @@ -10,13 +10,13 @@ services: ports: - "8080:8080" depends_on: - mongo: + tf_kyc_db: condition: service_healthy env_file: - .app.env - mongo: + tf_kyc_db: image: mongo:latest container_name: tf_kyc_db ports: @@ -36,10 +36,10 @@ services: image: mongo-express:latest container_name: mongo_express environment: - - ME_CONFIG_MONGODB_SERVER=mongo + - ME_CONFIG_MONGODB_SERVER=tf_kyc_db - ME_CONFIG_MONGODB_PORT=27017 depends_on: - - mongo + - tf_kyc_db ports: - "8888:8081" env_file: @@ -50,5 +50,5 @@ volumes: networks: default: - name: tfgrid_kyc_network + name: tf_kyc_network driver: bridge \ No newline at end of file diff --git a/internal/configs/config.go b/internal/configs/config.go index 92c20ba..ae52d1e 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -19,7 +19,7 @@ type Config struct { type MongoDB struct { URI string `env:"MONGO_URI" env-default:"mongodb://localhost:27017"` - DatabaseName string `env:"DATABASE_NAME" env-default:"tfgrid-kyc-db"` + DatabaseName string `env:"DATABASE_NAME" env-default:"tf-kyc-db"` } type Server struct { Port string `env:"PORT" env-default:"8080"` From 3187358fd3f5c80cba48766b48b6566aac657019 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 21:29:03 +0300 Subject: [PATCH 046/105] Update docs --- README.md | 169 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 133 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 2314014..e7f7c0b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# TFGrid KYC Service +# TF KYC Service ## Overview -TFGrid KYC Service is a Go-based service that provides Know Your Customer (KYC) functionality for the TFGrid. It integrates with iDenfy for identity verification. +TF KYC Service is a Go-based service that provides Know Your Customer (KYC) functionality for the TF Grid. It integrates with iDenfy for identity verification. ## Features @@ -25,8 +25,8 @@ TFGrid KYC Service is a Go-based service that provides Know Your Customer (KYC) 1. Clone the repository: ```bash - git clone https://github.com/yourusername/tfgrid-kyc-verifier.git - cd tfgrid-kyc-verifier + git clone https://github.com/yourusername/tf-kyc-verifier.git + cd tf-kyc-verifier ``` 2. Set up your environment variables: @@ -45,7 +45,7 @@ The application uses environment variables for configuration. Here's a list of a ### Database Configuration - `MONGO_URI`: MongoDB connection URI (default: "mongodb://localhost:27017") -- `DATABASE_NAME`: Name of the MongoDB database (default: "tfgrid-kyc-db") +- `DATABASE_NAME`: Name of the MongoDB database (default: "tf-kyc-db") ### Server Configuration @@ -53,30 +53,48 @@ The application uses environment variables for configuration. Here's a list of a ### iDenfy Configuration -- `IDENFY_API_KEY`: API key for iDenfy service -- `IDENFY_API_SECRET`: API secret for iDenfy service -- `IDENFY_BASE_URL`: Base URL for iDenfy API (default: "") -- `IDENFY_CALLBACK_SIGN_KEY`: Callback signing key for iDenfy webhooks +- `IDENFY_API_KEY`: API key for iDenfy service (required) (note: make sure to use correct iDenfy API key for the environment dev, test, and production) (iDenfy dev -> TFChain Devnet, iDenfy test -> TFChain QAnet, iDenfy prod -> TFChain Testnet and Mainnet) +- `IDENFY_API_SECRET`: API secret for iDenfy service (required) +- `IDENFY_BASE_URL`: Base URL for iDenfy API (default: "") +- `IDENFY_CALLBACK_SIGN_KEY`: Callback signing key for iDenfy webhooks (required) (note: should match the signing key in iDenfy dashboard for the related environment) - `IDENFY_WHITELISTED_IPS`: Comma-separated list of whitelisted IPs for iDenfy callbacks +- `IDENFY_DEV_MODE`: Enable development mode for iDenfy integration (default: false) (note: works only in iDenfy dev environment, enabling it in test or production environment will cause iDenfy to reject the requests) +- `IDENFY_CALLBACK_URL`: URL for iDenfy verification update callbacks. (example: `https://{KYC-SERVICE-DOMAIN}/webhooks/idenfy/verification-update`) ### TFChain Configuration - `TFCHAIN_WS_PROVIDER_URL`: WebSocket provider URL for TFChain (default: "wss://tfchain.grid.tf") +### Verification Settings + +- `VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME`: Outcome for suspicious verifications (default: "verified") +- `VERIFICATION_EXPIRED_DOCUMENT_OUTCOME`: Outcome for expired documents (default: "unverified") +- `VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT`: Minimum balance required to verify an account (default: 10000000) + ### Rate Limiting -- `MAX_TOKEN_REQUESTS_PER_MINUTE`: Maximum number of token requests allowed per minute for same IP address (default: 4) +#### IP-based Rate Limiting -### Verification Settings +- `IP_LIMITER_MAX_TOKEN_REQUESTS`: Maximum number of token requests per IP (default: 4) +- `IP_LIMITER_TOKEN_EXPIRATION`: Token expiration time in hours (default: 24) + +#### ID-based Rate Limiting + +- `ID_LIMITER_MAX_TOKEN_REQUESTS`: Maximum number of token requests per ID (default: 4) +- `ID_LIMITER_TOKEN_EXPIRATION`: Token expiration time in hours (default: 24) + +### Challenge Configuration -- `SUSPICIOUS_VERIFICATION_OUTCOME`: Outcome for suspicious verifications (default: "verified") -- `EXPIRED_DOCUMENT_OUTCOME`: Outcome for expired documents (default: "unverified") -- `CHALLENGE_WINDOW`: Time window (in seconds) for challenge validation (default: 120) -- `MIN_BALANCE_TO_VERIFY_ACCOUNT`: Minimum balance (in units TFT) required to verify an account (default: 10000000) +- `CHALLENGE_WINDOW`: Time window in seconds for challenge validation (default: 8) +- `CHALLENGE_DOMAIN`: Current service domain name for challenge validation (required) (example: `tfkyc.dev.grid.tf`) + +### Logging + +- `DEBUG`: Enable debug logging (default: false) To configure these options, you can either set them as environment variables or include them in your `.env` file. -Refer to `internal/configs/config.go` for a full list of configuration options. +Refer to `internal/configs/config.go` for the implementation details of these configuration options. ## Running the Application @@ -109,18 +127,83 @@ To run the application locally: ## API Endpoints -### Client endpoints - -- `POST /api/v1/token`: Get or create a verification token -- `GET /api/v1/data`: Get verification data -- `GET /api/v1/status`: Get verification status - -### Webhook endpoints - -- `POST /webhooks/idenfy/verification-update`: Process verification update (webhook) -- `POST /webhooks/idenfy/id-expiration`: Process document expiration notification (webhook) - -Refer to the Swagger documentation for detailed information on request/response formats. +### Client Endpoints + +#### Token Management + +- `POST /api/v1/token` + - Get or create a verification token + - Required Headers: + - `X-Client-ID`: TFChain SS58Address (48 chars) + - `X-Challenge`: Hex-encoded message `{api-domain}:{timestamp}` + - `X-Signature`: Hex-encoded sr25519|ed25519 signature (128 chars) + - Responses: + - `200`: Existing token retrieved + - `201`: New token created + - `400`: Bad request + - `401`: Unauthorized + - `402`: Payment required + - `409`: Conflict + - `500`: Internal server error + +#### Verification + +- `GET /api/v1/data` + - Get verification data for a client + - Required Headers: + - `X-Client-ID`: TFChain SS58Address (48 chars) + - `X-Challenge`: Hex-encoded message `{api-domain}:{timestamp}` + - `X-Signature`: Hex-encoded sr25519|ed25519 signature (128 chars) + - Responses: + - `200`: Success + - `400`: Bad request + - `401`: Unauthorized + - `404`: Not found + - `500`: Internal server error + +- `GET /api/v1/status` + - Get verification status + - Query Parameters (at least one required): + - `client_id`: TFChain SS58Address (48 chars) + - `twin_id`: Twin ID + - Responses: + - `200`: Success + - `400`: Bad request + - `404`: Not found + - `500`: Internal server error + +### Webhook Endpoints + +- `POST /webhooks/idenfy/verification-update` + - Process verification update from iDenfy + - Required Headers: + - `Idenfy-Signature`: Verification signature + - Responses: + - `200`: Success + - `400`: Bad request + - `500`: Internal server error + +- `POST /webhooks/idenfy/id-expiration` + - Process document expiration notification (Not implemented) + - Responses: + - `501`: Not implemented + +### Health Check + +- `GET /api/v1/health` + - Check service health status + - Responses: + - `200`: Returns health status + - `healthy`: All systems operational + - `degraded`: Some systems experiencing issues + +### Documentation + +- `GET /docs` + - Swagger documentation interface + - Provides interactive API documentation and testing interface + +Refer to the Swagger documentation at `/docs` endpoint for detailed information about request/response formats and examples. ## Swagger Documentation @@ -129,20 +212,34 @@ Swagger documentation is available. To view it, run the application and navigate ## Project Structure - `cmd/`: Application entrypoints + - `api/`: Main API server - `internal/`: Internal packages + - `clients/`: External service clients - `configs/`: Configuration handling + - `errors/`: Custom error types - `handlers/`: HTTP request handlers + - `logger/`: Logging configuration + - `middlewares/`: HTTP middlewares - `models/`: Data models + - `repositories/`: Data access layer - `responses/`: API response structures + - `server/`: Server setup and routing - `services/`: Business logic - - `repositories/`: Data repositories - - `middlewares/`: Middlewares - - `clients/`: External clients - - `server/`: Server and router setup -- `api/docs/`: Swagger documentation -- `scripts/`: Development and utility scripts +- `api/`: API documentation + - `docs/`: Swagger documentation files +- `.github/`: GitHub specific files + - `workflows/`: GitHub Actions workflows +- `scripts/`: Utility and Development scripts - `docs/`: Documentation +- Configuration files: + - `.app.env.example`: Example application environment variables + - `.db.env.example`: Example database environment variables + - `Dockerfile`: Container build instructions + - `docker-compose.yml`: Multi-container Docker setup + - `go.mod`: Go module definition + - `go.sum`: Go module checksums + ## Development ### Running Tests @@ -156,7 +253,7 @@ TODO: Add tests To build the Docker image: ```bash -docker build -t tfgrid-kyc-service . +docker build -t tf_kyc_verifier . ``` ### Running the Docker Container @@ -164,7 +261,7 @@ docker build -t tfgrid-kyc-service . To run the Docker container and use .env variables: ```bash -docker run -d -p 8080:8080 --env-file .app.env tfgrid-kyc-service +docker run -d -p 8080:8080 --env-file .app.env tf_kyc_verifier ``` ## Contributing From 29cd12fe11240032e72f4a96541d7694215bb223 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 21:36:28 +0300 Subject: [PATCH 047/105] configure health check for KYC api in docker-compose --- Dockerfile | 1 + docker-compose.yml | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9fbb7fc..4b9744d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o tfgrid-kyc cmd/api/main.go FROM alpine:3.19 COPY --from=builder /app/tfgrid-kyc . +RUN apk --no-cache add curl ENTRYPOINT ["/tfgrid-kyc"] diff --git a/docker-compose.yml b/docker-compose.yml index dd20893..2c1fb84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,12 @@ services: condition: service_healthy env_file: - .app.env - + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health"] + interval: 10s + timeout: 10s + retries: 3 + start_period: 10s tf_kyc_db: image: mongo:latest @@ -29,7 +34,7 @@ services: test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet interval: 10s timeout: 10s - retries: 5 + retries: 3 start_period: 10s mongo-express: From faa02f3f27a3a116dfcd3a2876ee1748edd891be Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 21:52:02 +0300 Subject: [PATCH 048/105] update docs --- .app.env.example | 2 +- README.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.app.env.example b/.app.env.example index 56f5569..2e9cfca 100644 --- a/.app.env.example +++ b/.app.env.example @@ -1,4 +1,4 @@ -MONGO_URI=mongodb://root:password@mongodb:27017 +MONGO_URI=mongodb://root:password@tf_kyc_db:27017 DATABASE_NAME=tfgrid-kyc-db PORT=8080 CHALLENGE_WINDOW=120 diff --git a/README.md b/README.md index e7f7c0b..396c35e 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,14 @@ To run the Docker container and use .env variables: docker run -d -p 8080:8080 --env-file .app.env tf_kyc_verifier ``` +### Creating database dump + +Most of the normal tools will work, although their usage might be a little convoluted in some cases to ensure they have access to the mongod server. A simple way to ensure this is to use docker exec and run the tool from the same container, similar to the following: + +```bash +docker exec sh -c 'exec mongodump -d --archive' > /some/path/on/your/host/all-collections.archive +``` + ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. From da113db21ae17fcbfdefd83a582f4799fc5e0ecb Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 28 Oct 2024 22:12:12 +0300 Subject: [PATCH 049/105] use shorter names for docker compose services --- docker-compose.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2c1fb84..66272db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,5 @@ -version: '3.8' - services: - tf_kyc_api: + api: build: context: . dockerfile: Dockerfile @@ -10,7 +8,7 @@ services: ports: - "8080:8080" depends_on: - tf_kyc_db: + db: condition: service_healthy env_file: - .app.env @@ -21,7 +19,7 @@ services: retries: 3 start_period: 10s - tf_kyc_db: + db: image: mongo:latest container_name: tf_kyc_db ports: @@ -41,10 +39,10 @@ services: image: mongo-express:latest container_name: mongo_express environment: - - ME_CONFIG_MONGODB_SERVER=tf_kyc_db + - ME_CONFIG_MONGODB_SERVER=db - ME_CONFIG_MONGODB_PORT=27017 depends_on: - - tf_kyc_db + - db ports: - "8888:8081" env_file: From 2d2718c1b65d8b1811985be31f0fe1ea55c788cf Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Tue, 29 Oct 2024 12:03:21 +0300 Subject: [PATCH 050/105] adjust healthcheck in docker-compose --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 66272db..2f0bc23 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: env_file: - .app.env healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health"] + test: ["CMD", "curl", "-f", "-s", "http://localhost:8080/api/v1/health"] interval: 10s timeout: 10s retries: 3 @@ -34,6 +34,7 @@ services: timeout: 10s retries: 3 start_period: 10s + restart: unless-stopped mongo-express: image: mongo-express:latest From 56c063ba0fe80634dedee9fbe631f398286e2ea3 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Tue, 29 Oct 2024 12:48:59 +0300 Subject: [PATCH 051/105] docs: add production setup guideline --- README.md | 4 ++++ docs/production.md | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 docs/production.md diff --git a/README.md b/README.md index 396c35e..838f7e2 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,10 @@ Most of the normal tools will work, although their usage might be a little convo docker exec sh -c 'exec mongodump -d --archive' > /some/path/on/your/host/all-collections.archive ``` +## Production + +Refer to the [Production Setup](./docs/production.md) documentation for production setup details. + ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/docs/production.md b/docs/production.md new file mode 100644 index 0000000..2a00680 --- /dev/null +++ b/docs/production.md @@ -0,0 +1,27 @@ +# Production Setup + +In an ideal production setup, we will need multiple instances of the service running behind a load balancer to distribute traffic evenly. This ensures scalability, prevents any single instance from being overwhelmed, and provides fault tolerance. + +The services should connect to a self-hosted MongoDB database configured for high availability. This involves setting up a replica set to ensure data redundancy and automatic failover, minimizing downtime in case of node failures. + +## Key Components + +### Load Balancer + +- Distributes incoming requests across multiple service instances to optimize performance and prevent overload. +- Provides fault tolerance by rerouting traffic if any instance goes offline. +- Example: Nginx, HAProxy, or Traefik. + +### High-Availability MongoDB Setup (Self-Hosted) + +- Replica Set: + - Consists of one primary node (for reads/writes) and one or more secondary nodes (for replication). + - If the primary node fails, a secondary is automatically promoted to primary. +- Arbiter Node: (Optional) + - Participates in elections without storing data, ensuring smooth primary promotion in case of failure. +- Backup Strategy: + - Regular backups to avoid data loss. Backups should be stored encrypted in a remote location to ensure data security and durability. +- Example Setup: 1 primary, 2 secondary nodes, and 1 optional arbiter for quorum. +- Hosted on virtual machines or bare metal servers to ensure full control over the infrastructure. + +This architecture ensures that the system remains operational even during node failures, while the load balancer helps manage traffic efficiently. Regular backups and monitoring of the MongoDB cluster will further enhance reliability and minimize the risk of data loss. From 3df3c4e8800be1507eacb3278411128437d612cb Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Tue, 29 Oct 2024 14:58:24 +0300 Subject: [PATCH 052/105] Add filed names to bson.D struct literal --- internal/repository/token_repository.go | 2 +- internal/repository/verification_repository.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index fd84a85..c160b6d 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -34,7 +34,7 @@ func (r *MongoTokenRepository) createTTLIndex() { _, err := r.collection.Indexes().CreateOne( ctx, mongo.IndexModel{ - Keys: bson.D{{"expiresAt", 1}}, + Keys: bson.D{{Key: "expiresAt", Value: 1}}, Options: options.Index().SetExpireAfterSeconds(0), }, ) diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go index ac7658f..cb137c7 100644 --- a/internal/repository/verification_repository.go +++ b/internal/repository/verification_repository.go @@ -32,7 +32,7 @@ func (r *MongoVerificationRepository) SaveVerification(ctx context.Context, veri func (r *MongoVerificationRepository) GetVerification(ctx context.Context, clientID string) (*models.Verification, error) { var verification models.Verification // return the latest verification - opts := options.FindOne().SetSort(bson.D{{"createdAt", -1}}) + opts := options.FindOne().SetSort(bson.D{{Key: "createdAt", Value: -1}}) err := r.collection.FindOne(ctx, bson.M{"clientId": clientID}, opts).Decode(&verification) if err != nil { if err == mongo.ErrNoDocuments { From 452cff43b49c18f533703b9bdf893f993efb7ff7 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Tue, 29 Oct 2024 15:28:11 +0300 Subject: [PATCH 053/105] Update limter middleware default EXPIRATION to 1440 --- README.md | 6 +++--- internal/configs/config.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 838f7e2..e1979c5 100644 --- a/README.md +++ b/README.md @@ -69,19 +69,19 @@ The application uses environment variables for configuration. Here's a list of a - `VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME`: Outcome for suspicious verifications (default: "verified") - `VERIFICATION_EXPIRED_DOCUMENT_OUTCOME`: Outcome for expired documents (default: "unverified") -- `VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT`: Minimum balance required to verify an account (default: 10000000) +- `VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT`: Minimum balance in unitTFT required to verify an account (default: 10000000) ### Rate Limiting #### IP-based Rate Limiting - `IP_LIMITER_MAX_TOKEN_REQUESTS`: Maximum number of token requests per IP (default: 4) -- `IP_LIMITER_TOKEN_EXPIRATION`: Token expiration time in hours (default: 24) +- `IP_LIMITER_TOKEN_EXPIRATION`: Token expiration time in minutes (default: 1440) #### ID-based Rate Limiting - `ID_LIMITER_MAX_TOKEN_REQUESTS`: Maximum number of token requests per ID (default: 4) -- `ID_LIMITER_TOKEN_EXPIRATION`: Token expiration time in hours (default: 24) +- `ID_LIMITER_TOKEN_EXPIRATION`: Token expiration time in minutes (default: 1440) ### Challenge Configuration diff --git a/internal/configs/config.go b/internal/configs/config.go index ae52d1e..86493f7 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -43,11 +43,11 @@ type Verification struct { } type IPLimiter struct { MaxTokenRequests int `env:"IP_LIMITER_MAX_TOKEN_REQUESTS" env-default:"4"` - TokenExpiration int `env:"IP_LIMITER_TOKEN_EXPIRATION" env-default:"24"` + TokenExpiration int `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:"24"` + TokenExpiration int `env:"ID_LIMITER_TOKEN_EXPIRATION" env-default:"1440"` } type Log struct { Debug bool `env:"DEBUG" env-default:"false"` From 4abcec53d255875b035198e921fa3c0d5033135b Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Wed, 30 Oct 2024 10:46:45 +0300 Subject: [PATCH 054/105] update docker-compose file --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2f0bc23..e77eb38 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,7 @@ services: context: . dockerfile: Dockerfile container_name: tf_kyc_api + image: ghcr.io/threefoldtech/tf-kyc-verifier:latest restart: unless-stopped ports: - "8080:8080" @@ -20,7 +21,7 @@ services: start_period: 10s db: - image: mongo:latest + image: mongo:8 container_name: tf_kyc_db ports: - "27017:27017" From ad0fb2fd2922686e99b709046fce0963715d2cbb Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Wed, 30 Oct 2024 10:48:39 +0300 Subject: [PATCH 055/105] Update docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1979c5..e616d52 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Refer to `internal/configs/config.go` for the implementation details of these co To start the server and MongoDB using Docker Compose: ```bash -docker-compose up -d --build +docker-compose up -d ``` ### Running Locally From 7c26812739c719949bfbddfefa5d2c40c080fc9b Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Wed, 30 Oct 2024 11:17:14 +0300 Subject: [PATCH 056/105] Update docs --- README.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e616d52..a12f8ed 100644 --- a/README.md +++ b/README.md @@ -100,10 +100,20 @@ Refer to `internal/configs/config.go` for the implementation details of these co ### Using Docker Compose -To start the server and MongoDB using Docker Compose: +First make sure to create and set the environment variables in the `.app.env`, `.db.env` files. +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: + +```bash +docker compose up -d db api +``` + +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: ```bash -docker-compose up -d +docker compose up -d ``` ### Running Locally @@ -144,7 +154,6 @@ To run the application locally: - `401`: Unauthorized - `402`: Payment required - `409`: Conflict - - `500`: Internal server error #### Verification @@ -159,7 +168,6 @@ To run the application locally: - `400`: Bad request - `401`: Unauthorized - `404`: Not found - - `500`: Internal server error - `GET /api/v1/status` - Get verification status @@ -170,7 +178,6 @@ To run the application locally: - `200`: Success - `400`: Bad request - `404`: Not found - - `500`: Internal server error ### Webhook Endpoints @@ -181,7 +188,6 @@ To run the application locally: - Responses: - `200`: Success - `400`: Bad request - - `500`: Internal server error - `POST /webhooks/idenfy/id-expiration` - Process document expiration notification (Not implemented) From f45cc7c806b4a4ebc3f7b1899c40f6cb50f8410a Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Wed, 30 Oct 2024 20:06:52 +0300 Subject: [PATCH 057/105] Update docs and env examples --- .app.env.example | 18 +++++++++--------- README.md | 4 ++-- internal/configs/config.go | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.app.env.example b/.app.env.example index 2e9cfca..37f4ba5 100644 --- a/.app.env.example +++ b/.app.env.example @@ -1,21 +1,21 @@ -MONGO_URI=mongodb://root:password@tf_kyc_db:27017 +MONGO_URI=mongodb://root:password@db:27017 DATABASE_NAME=tfgrid-kyc-db PORT=8080 CHALLENGE_WINDOW=120 -CHALLENGE_DOMAIN=kyc1.gent01.dev.grid.tf -VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME=verified -VERIFICATION_EXPIRED_DOCUMENT_OUTCOME=verified +CHALLENGE_DOMAIN=kyc.dev.grid.tf +VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME=APPROVED +VERIFICATION_EXPIRED_DOCUMENT_OUTCOME=APPROVED VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT=1000000 -IDENFY_BASE_URL=https://ivs.idenfy.com/api/v2 +IDENFY_BASE_URL=https://ivs.idenfy.com IDENFY_API_KEY= IDENFY_API_SECRET= IDENFY_CALLBACK_SIGN_KEY= IDENFY_WHITELISTED_IPS= IDENFY_DEV_MODE=false -TFCHAIN_WS_PROVIDER_URL=wss://tfchain.grid.tf +TFCHAIN_WS_PROVIDER_URL=wss://tfchain.dev.grid.tf IP_LIMITER_MAX_TOKEN_REQUESTS=5 -IP_LIMITER_TOKEN_EXPIRATION=24 +IP_LIMITER_TOKEN_EXPIRATION=1440 ID_LIMITER_MAX_TOKEN_REQUESTS=5 -ID_LIMITER_TOKEN_EXPIRATION=24 +ID_LIMITER_TOKEN_EXPIRATION=1440 DEBUG=false -IDENFY_CALLBACK_URL=https://[TF_KYC_SERVICE_DOMAIN_NAME_HERE]/webhooks/idenfy/verification-update \ No newline at end of file +IDENFY_CALLBACK_URL=https://kyc.dev.grid.tf/webhooks/idenfy/verification-update \ No newline at end of file diff --git a/README.md b/README.md index a12f8ed..fe0042b 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ The application uses environment variables for configuration. Here's a list of a ### Verification Settings -- `VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME`: Outcome for suspicious verifications (default: "verified") -- `VERIFICATION_EXPIRED_DOCUMENT_OUTCOME`: Outcome for expired documents (default: "unverified") +- `VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME`: Outcome for suspicious verifications (default: "APPROVED") +- `VERIFICATION_EXPIRED_DOCUMENT_OUTCOME`: Outcome for expired documents (default: "REJECTED") - `VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT`: Minimum balance in unitTFT required to verify an account (default: 10000000) ### Rate Limiting diff --git a/internal/configs/config.go b/internal/configs/config.go index 86493f7..428b89b 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -37,8 +37,8 @@ type TFChain struct { WsProviderURL string `env:"TFCHAIN_WS_PROVIDER_URL" env-default:"wss://tfchain.grid.tf"` } type Verification struct { - SuspiciousVerificationOutcome string `env:"VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME" env-default:"verified"` - ExpiredDocumentOutcome string `env:"VERIFICATION_EXPIRED_DOCUMENT_OUTCOME" env-default:"unverified"` + SuspiciousVerificationOutcome string `env:"VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME" env-default:"APPROVED"` + ExpiredDocumentOutcome string `env:"VERIFICATION_EXPIRED_DOCUMENT_OUTCOME" env-default:"REJECTED"` MinBalanceToVerifyAccount uint64 `env:"VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT" env-default:"10000000"` } type IPLimiter struct { From 44507efe421d84d82d86f55641e71d19688df58c Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 31 Oct 2024 14:46:43 +0300 Subject: [PATCH 058/105] implement alwaysAllowedIDs --- .app.env.example | 4 +- README.md | 9 ++- internal/clients/idenfy/idenfy.go | 36 ++++------ internal/configs/config.go | 50 ++++++++++++- internal/server/server.go | 77 ++++++++++++--------- internal/services/kyc_service.go | 18 ++++- scripts/dev/auth/generate-test-auth-data.go | 2 +- 7 files changed, 131 insertions(+), 65 deletions(-) diff --git a/.app.env.example b/.app.env.example index 37f4ba5..ef425f6 100644 --- a/.app.env.example +++ b/.app.env.example @@ -18,4 +18,6 @@ IP_LIMITER_TOKEN_EXPIRATION=1440 ID_LIMITER_MAX_TOKEN_REQUESTS=5 ID_LIMITER_TOKEN_EXPIRATION=1440 DEBUG=false -IDENFY_CALLBACK_URL=https://kyc.dev.grid.tf/webhooks/idenfy/verification-update \ No newline at end of file +IDENFY_CALLBACK_URL=https://kyc.dev.grid.tf/webhooks/idenfy/verification-update +IDENFY_NAMESPACE= +VERIFICATION_ALWAYS_VERIFIED_IDS= \ No newline at end of file diff --git a/README.md b/README.md index fe0042b..a3f53c6 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ The application uses environment variables for configuration. Here's a list of a - `IDENFY_API_KEY`: API key for iDenfy service (required) (note: make sure to use correct iDenfy API key for the environment dev, test, and production) (iDenfy dev -> TFChain Devnet, iDenfy test -> TFChain QAnet, iDenfy prod -> TFChain Testnet and Mainnet) - `IDENFY_API_SECRET`: API secret for iDenfy service (required) - `IDENFY_BASE_URL`: Base URL for iDenfy API (default: "") -- `IDENFY_CALLBACK_SIGN_KEY`: Callback signing key for iDenfy webhooks (required) (note: should match the signing key in iDenfy dashboard for the related environment) +- `IDENFY_CALLBACK_SIGN_KEY`: Callback signing key for iDenfy webhooks (required) (note: should match the signing key in iDenfy dashboard for the related environment and should be at least 32 characters long) - `IDENFY_WHITELISTED_IPS`: Comma-separated list of whitelisted IPs for iDenfy callbacks - `IDENFY_DEV_MODE`: Enable development mode for iDenfy integration (default: false) (note: works only in iDenfy dev environment, enabling it in test or production environment will cause iDenfy to reject the requests) - `IDENFY_CALLBACK_URL`: URL for iDenfy verification update callbacks. (example: `https://{KYC-SERVICE-DOMAIN}/webhooks/idenfy/verification-update`) @@ -94,6 +94,13 @@ The application uses environment variables for configuration. Here's a list of a To configure these options, you can either set them as environment variables or include them in your `.env` file. +Regarding the iDenfy signing key, it's best to use key composed of alphanumeric characters to avoid such issues. +You can generate a random key using the following command: + +```bash +cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1 +``` + Refer to `internal/configs/config.go` for the implementation details of these configuration options. ## Running the Application diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index 2872746..7a31c50 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -18,14 +18,9 @@ import ( ) type Idenfy struct { - client *fasthttp.Client - accessKey string - secretKey string - baseURL string - callbackSignKey []byte - callbackUrl string - devMode bool - logger *logger.Logger + client *fasthttp.Client + config *configs.Idenfy + logger *logger.Logger } const ( @@ -34,19 +29,14 @@ const ( func New(config configs.Idenfy, logger *logger.Logger) *Idenfy { return &Idenfy{ - baseURL: config.BaseURL, - client: &fasthttp.Client{}, - accessKey: config.APIKey, - secretKey: config.APISecret, - callbackSignKey: []byte(config.CallbackSignKey), - callbackUrl: config.CallbackUrl, - devMode: config.DevMode, - logger: logger, + client: &fasthttp.Client{}, + config: &config, + logger: logger, } } func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) (models.Token, error) { // TODO: Refactor - url := c.baseURL + VerificationSessionEndpoint + url := c.config.BaseURL + VerificationSessionEndpoint req := fasthttp.AcquireRequest() defer fasthttp.ReleaseRequest(req) @@ -56,11 +46,11 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) req.Header.Set("Content-Type", "application/json") // Set basic auth - authStr := c.accessKey + ":" + c.secretKey + authStr := c.config.APIKey + ":" + c.config.APISecret auth := base64.StdEncoding.EncodeToString([]byte(authStr)) req.Header.Set("Authorization", "Basic "+auth) - RequestBody := c.createVerificationSessionRequestBody(clientID, c.devMode) + RequestBody := c.createVerificationSessionRequestBody(clientID, c.config.DevMode) jsonBody, err := json.Marshal(RequestBody) if err != nil { @@ -91,19 +81,19 @@ 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.callbackSignKey) < 1 { + if len(c.config.CallbackSignKey) < 1 { return errors.New("callback was received but no signature key was provided") } sig, err := hex.DecodeString(sigHeader) if err != nil { return err } - mac := hmac.New(sha256.New, c.callbackSignKey) + mac := hmac.New(sha256.New, []byte(c.config.CallbackSignKey)) mac.Write(body) if !hmac.Equal(sig, mac.Sum(nil)) { - c.logger.Error("Signature verification failed", zap.String("sigHeader", sigHeader), zap.String("key", string(c.callbackSignKey)), zap.String("mac", hex.EncodeToString(mac.Sum(nil)))) + c.logger.Error("Signature verification failed", zap.String("sigHeader", sigHeader), zap.String("key", string(c.config.CallbackSignKey)), zap.String("mac", hex.EncodeToString(mac.Sum(nil)))) return errors.New("signature verification failed") } return nil @@ -114,7 +104,7 @@ func (c *Idenfy) createVerificationSessionRequestBody(clientID string, devMode b RequestBody := map[string]interface{}{ "clientId": clientID, "generateDigitString": true, - "callbackUrl": c.callbackUrl, + "callbackUrl": c.config.CallbackUrl, } if devMode { RequestBody["expiryTime"] = 30 diff --git a/internal/configs/config.go b/internal/configs/config.go index 428b89b..dae147f 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -1,6 +1,9 @@ package configs import ( + "net/url" + "slices" + "example.com/tfgrid-kyc-service/internal/errors" "github.com/ilyakaznacheev/cleanenv" ) @@ -32,14 +35,16 @@ type Idenfy struct { WhitelistedIPs []string `env:"IDENFY_WHITELISTED_IPS" env-separator:","` DevMode bool `env:"IDENFY_DEV_MODE" env-default:"false"` CallbackUrl string `env:"IDENFY_CALLBACK_URL" env-required:"false"` + Namespace string `env:"IDENFY_NAMESPACE" env-default:""` } type TFChain struct { WsProviderURL string `env:"TFCHAIN_WS_PROVIDER_URL" env-default:"wss://tfchain.grid.tf"` } type Verification struct { - SuspiciousVerificationOutcome string `env:"VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME" env-default:"APPROVED"` - ExpiredDocumentOutcome string `env:"VERIFICATION_EXPIRED_DOCUMENT_OUTCOME" env-default:"REJECTED"` - MinBalanceToVerifyAccount uint64 `env:"VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT" env-default:"10000000"` + SuspiciousVerificationOutcome string `env:"VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME" env-default:"APPROVED"` + ExpiredDocumentOutcome string `env:"VERIFICATION_EXPIRED_DOCUMENT_OUTCOME" env-default:"REJECTED"` + MinBalanceToVerifyAccount uint64 `env:"VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT" env-default:"10000000"` + AlwaysVerifiedIDs []string `env:"VERIFICATION_ALWAYS_VERIFIED_IDS" env-separator:","` } type IPLimiter struct { MaxTokenRequests int `env:"IP_LIMITER_MAX_TOKEN_REQUESTS" env-default:"4"` @@ -63,5 +68,44 @@ func LoadConfig() (*Config, error) { if err != nil { return nil, errors.NewInternalError("error loading config", err) } + cfg.Validate() return cfg, nil } + +// validate config +func (c *Config) Validate() error { + // iDenfy base URL should be https://ivs.idenfy.com + if c.Idenfy.BaseURL != "https://ivs.idenfy.com" { + panic("invalid iDenfy base URL") + } + // CallbackUrl should be valid URL + parsedCallbackUrl, err := url.ParseRequestURI(c.Idenfy.CallbackUrl) + if err != nil { + panic("invalid CallbackUrl") + } + // CallbackSignKey should not be empty + if len(c.Idenfy.CallbackSignKey) < 16 { + panic("CallbackSignKey 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" { + panic("invalid WsProviderURL") + } + // domain should not be empty and same as domain in CallbackUrl + if parsedCallbackUrl.Host != c.Challenge.Domain { + panic("invalid Challenge Domain. It should be same as domain in CallbackUrl") + } + // Window should be greater than 2 + if c.Challenge.Window < 2 { + panic("invalid Challenge Window. It should be greater than 2 otherwise it will be too short and verification can fail in slow networks") + } + // SuspiciousVerificationOutcome should be either APPROVED or REJECTED + if !slices.Contains([]string{"APPROVED", "REJECTED"}, c.Verification.SuspiciousVerificationOutcome) { + panic("invalid SuspiciousVerificationOutcome") + } + // ExpiredDocumentOutcome should be either APPROVED or REJECTED + if !slices.Contains([]string{"APPROVED", "REJECTED"}, c.Verification.ExpiredDocumentOutcome) { + panic("invalid ExpiredDocumentOutcome") + } + return nil +} diff --git a/internal/server/server.go b/internal/server/server.go index eb0140c..19ff6cb 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -53,44 +53,22 @@ func New(config *configs.Config, logger *logger.Logger) *Server { ipLimiterConfig := limiter.Config{ Max: config.IPLimiter.MaxTokenRequests, Expiration: time.Duration(config.IPLimiter.TokenExpiration) * time.Minute, - SkipFailedRequests: false, + SkipFailedRequests: true, SkipSuccessfulRequests: false, Storage: ipLimiterstore, // skip the limiter for localhost Next: func(c *fiber.Ctx) bool { - return c.IP() == "127.0.0.1" + // skip the limiter if the keyGenerator returns "127.0.0.1" + return extractIPFromRequest(c) == "127.0.0.1" }, KeyGenerator: func(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 { - // return the first non-private ip in the list - for _, ip := range ips { - if net.ParseIP(strings.TrimSpace(ip)) != nil && !net.ParseIP(strings.TrimSpace(ip)).IsPrivate() { - return strings.TrimSpace(ip) - } - } - } - } - - // Check for X-Real-IP header if not a private IP - if ip := c.Get("X-Real-IP"); ip != "" { - if net.ParseIP(strings.TrimSpace(ip)) != nil && !net.ParseIP(strings.TrimSpace(ip)).IsPrivate() { - return strings.TrimSpace(ip) - } - } - - // Fall back to RemoteIP() if no proxy headers are present - ip := c.IP() - if parsedIP := net.ParseIP(ip); parsedIP != nil { - if !parsedIP.IsPrivate() { - return ip - } - } - - // If we still have a private IP, return a default value that will be skipped by the limiter - return "127.0.0.1" + logger.Debug("client IPs detected by the limiter", + zap.String("remoteIp", c.IP()), + zap.String("X-Forwarded-For", c.Get("X-Forwarded-For")), + zap.String("X-Real-IP", c.Get("X-Real-IP")), + zap.Strings("ips", c.IPs()), + ) + return extractIPFromRequest(c) }, } idLimiterStore := mongodb.New(mongodb.Config{ @@ -103,7 +81,7 @@ func New(config *configs.Config, logger *logger.Logger) *Server { idLimiterConfig := limiter.Config{ Max: config.IDLimiter.MaxTokenRequests, Expiration: time.Duration(config.IDLimiter.TokenExpiration) * time.Minute, - SkipFailedRequests: false, + SkipFailedRequests: true, SkipSuccessfulRequests: false, Storage: idLimiterStore, // Use client id as key to limit the number of requests per client @@ -136,7 +114,7 @@ func New(config *configs.Config, logger *logger.Logger) *Server { if err != nil { logger.Fatal("Failed to initialize substrate client", zap.Error(err)) } - kycService := services.NewKYCService(verificationRepo, tokenRepo, idenfyClient, substrateClient, &config.Verification, logger) + kycService := services.NewKYCService(verificationRepo, tokenRepo, idenfyClient, substrateClient, config, logger) // Initialize handler handler := handlers.NewHandler(kycService, logger) @@ -158,6 +136,37 @@ func New(config *configs.Config, logger *logger.Logger) *Server { return &Server{app: app, config: config, logger: logger} } +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) + } + } + } + } + // Check for X-Real-IP header if not a private IP + if ip := c.Get("X-Real-IP"); ip != "" { + if net.ParseIP(strings.TrimSpace(ip)) != nil && !net.ParseIP(strings.TrimSpace(ip)).IsPrivate() { + return strings.TrimSpace(ip) + } + } + // Fall back to RemoteIP() if no proxy headers are present + ip := c.IP() + if parsedIP := net.ParseIP(ip); parsedIP != nil { + if !parsedIP.IsPrivate() { + return ip + } + } + // If we still have a private IP, return a default value that will be skipped by the limiter + return "127.0.0.1" +} + func (s *Server) Start() { go func() { sigChan := make(chan os.Signal, 1) diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index de5011e..a4793b7 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -3,6 +3,7 @@ package services import ( "context" "math/big" + "slices" "strings" "time" @@ -26,14 +27,17 @@ type kycService struct { IdenfySuffix string } -func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.Verification, logger *logger.Logger) KYCService { +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.Config, logger *logger.Logger) KYCService { chainName, err := substrateClient.GetChainName() if err != nil { panic(errors.NewInternalError("error getting chain name", err)) } chainNameParts := strings.Split(chainName, " ") chainNetworkName := strings.ToLower(chainNameParts[len(chainNameParts)-1]) - return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: config, logger: logger, IdenfySuffix: chainNetworkName} + if config.Idenfy.Namespace != "" { + chainNetworkName = config.Idenfy.Namespace + ":" + chainNetworkName + } + return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: &config.Verification, logger: logger, IdenfySuffix: chainNetworkName} } // --------------------------------------------------------------------------------------------------------------------- @@ -127,6 +131,16 @@ func (s *kycService) GetVerificationData(ctx context.Context, clientID string) ( } 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 + 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", zap.String("clientID", clientID), zap.Error(err)) diff --git a/scripts/dev/auth/generate-test-auth-data.go b/scripts/dev/auth/generate-test-auth-data.go index 932dd9e..5305f99 100644 --- a/scripts/dev/auth/generate-test-auth-data.go +++ b/scripts/dev/auth/generate-test-auth-data.go @@ -12,7 +12,7 @@ import ( ) const ( - domain = "kyc1.gent01.dev.grid.tf" + domain = "kyc.qa.grid.tf" ) // Generate test auth data for development use From 2755c46156167a68cf021cd88990c619a9e5f82e Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 17:49:57 +0200 Subject: [PATCH 059/105] Get the domain from Args --- scripts/dev/auth/generate-test-auth-data.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/dev/auth/generate-test-auth-data.go b/scripts/dev/auth/generate-test-auth-data.go index 5305f99..ee44555 100644 --- a/scripts/dev/auth/generate-test-auth-data.go +++ b/scripts/dev/auth/generate-test-auth-data.go @@ -11,13 +11,15 @@ import ( "github.com/vedhavyas/go-subkey/v2/sr25519" ) -const ( - domain = "kyc.qa.grid.tf" -) - // Generate test auth data for development use func main() { - message := createSignMessage() + // get domain from first arg. error if not provided + if len(os.Args) < 2 { + fmt.Println("Error: Domain is required") + os.Exit(1) + } + domain := os.Args[1] + message := createSignMessage(domain) // if no arg provided, generate random keys krSr25519, krEd25519, err := loadKeys() if err != nil { @@ -55,7 +57,7 @@ func main() { fmt.Println("challenge string (plain text): ", string(bytes)) } -func createSignMessage() string { +func createSignMessage(domain string) string { // return a message with the domain and the current timestamp in hex message := fmt.Sprintf("%s:%d", domain, time.Now().Unix()) fmt.Println("message: ", message) @@ -63,7 +65,7 @@ func createSignMessage() string { } func loadKeys() (subkey.KeyPair, subkey.KeyPair, error) { - if len(os.Args) < 2 { + if len(os.Args) < 3 { krSr25519, err := sr25519.Scheme{}.Generate() if err != nil { return nil, nil, err @@ -74,11 +76,11 @@ func loadKeys() (subkey.KeyPair, subkey.KeyPair, error) { } return krSr25519, krEd25519, nil } else { - krSr25519, err := sr25519.Scheme{}.FromPhrase(os.Args[1], "") + krSr25519, err := sr25519.Scheme{}.FromPhrase(os.Args[2], "") if err != nil { return nil, nil, err } - krEd25519, err := ed25519.Scheme{}.FromPhrase(os.Args[1], "") + krEd25519, err := ed25519.Scheme{}.FromPhrase(os.Args[2], "") if err != nil { return nil, nil, err } From 1a5b1990fe03043eeefb0169361f49518fcd36ef Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 17:51:46 +0200 Subject: [PATCH 060/105] implement a logger wrapper for flexbility --- cmd/api/main.go | 7 +- internal/clients/idenfy/idenfy.go | 23 ++++--- internal/clients/idenfy/idenfy_test.go | 6 +- internal/clients/substrate/substrate.go | 7 +- internal/handlers/handlers.go | 40 ++++++----- internal/logger/interface.go | 9 +++ internal/logger/logger.go | 47 ++++++++----- internal/logger/zap_logger.go | 68 +++++++++++++++++++ internal/middleware/middleware.go | 41 ++++++----- internal/repository/token_repository.go | 7 +- .../repository/verification_repository.go | 4 +- internal/server/server.go | 21 ++---- internal/services/kyc_service.go | 37 +++++----- 13 files changed, 204 insertions(+), 113 deletions(-) create mode 100644 internal/logger/interface.go create mode 100644 internal/logger/zap_logger.go diff --git a/cmd/api/main.go b/cmd/api/main.go index 62e26b5..65cbcf7 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -7,7 +7,6 @@ import ( "example.com/tfgrid-kyc-service/internal/configs" "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/server" - "go.uber.org/zap" ) // @title TFGrid KYC API @@ -27,11 +26,11 @@ func main() { logger.Init(config.Log) logger := logger.GetLogger() - defer logger.Sync() + // defer logger.Sync() - logger.Debug("Configuration loaded successfully", zap.Any("config", config)) // TODO: remove me after testing + logger.Debug("Configuration loaded successfully", map[string]interface{}{"config": config}) // TODO: remove me after testing server := server.New(config, logger) - logger.Info("Starting server on port:", zap.String("port", config.Server.Port)) + logger.Info("Starting server on port:", map[string]interface{}{"port": config.Server.Port}) server.Start() } diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index 7a31c50..6442fa4 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -14,20 +14,19 @@ import ( "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" "github.com/valyala/fasthttp" - "go.uber.org/zap" ) type Idenfy struct { - client *fasthttp.Client - config *configs.Idenfy - logger *logger.Logger + client *fasthttp.Client // TODO: Interface + config *configs.Idenfy // TODO: Interface + logger *logger.LoggerW } const ( VerificationSessionEndpoint = "/api/v2/token" ) -func New(config configs.Idenfy, logger *logger.Logger) *Idenfy { +func New(config configs.Idenfy, logger *logger.LoggerW) *Idenfy { return &Idenfy{ client: &fasthttp.Client{}, config: &config, @@ -60,7 +59,9 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) - c.logger.Debug("Preparing iDenfy verification session request", zap.Any("request", req)) + c.logger.Debug("Preparing iDenfy verification session request", map[string]interface{}{ + "request": req, + }) err = c.client.Do(req, resp) if err != nil { return models.Token{}, fmt.Errorf("error sending request: %w", err) @@ -69,7 +70,9 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) if resp.StatusCode() < 200 || resp.StatusCode() >= 300 { return models.Token{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) } - c.logger.Debug("Received response from iDenfy", zap.Any("response", resp)) + c.logger.Debug("Received response from iDenfy", map[string]interface{}{ + "response": resp, + }) var result models.Token if err := json.Unmarshal(resp.Body(), &result); err != nil { @@ -93,7 +96,11 @@ func (c *Idenfy) VerifyCallbackSignature(ctx context.Context, body []byte, sigHe mac.Write(body) if !hmac.Equal(sig, mac.Sum(nil)) { - c.logger.Error("Signature verification failed", zap.String("sigHeader", sigHeader), zap.String("key", string(c.config.CallbackSignKey)), zap.String("mac", hex.EncodeToString(mac.Sum(nil)))) + c.logger.Error("Signature verification failed", map[string]interface{}{ + "sigHeader": sigHeader, + "key": string(c.config.CallbackSignKey), + "mac": hex.EncodeToString(mac.Sum(nil)), + }) return errors.New("signature verification failed") } return nil diff --git a/internal/clients/idenfy/idenfy_test.go b/internal/clients/idenfy/idenfy_test.go index 2ae9bcb..231abab 100644 --- a/internal/clients/idenfy/idenfy_test.go +++ b/internal/clients/idenfy/idenfy_test.go @@ -11,7 +11,6 @@ import ( "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" "github.com/stretchr/testify/assert" - "go.uber.org/zap" ) func TestClient_DecodeReaderIdentityCallback(t *testing.T) { @@ -21,7 +20,6 @@ func TestClient_DecodeReaderIdentityCallback(t *testing.T) { client := New(configs.Idenfy{ CallbackSignKey: "TestingKey", }, logger) - defer logger.Sync() assert.NotNil(t, client, "Client is nil") webhook1, err := os.ReadFile("testdata/webhook.1.json") @@ -33,7 +31,9 @@ func TestClient_DecodeReaderIdentityCallback(t *testing.T) { err = decoder.Decode(&resp) assert.NoError(t, err) // Basic verification info - logger.Info("resp", zap.Any("resp", resp)) + logger.Info("resp", map[string]interface{}{ + "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 284b3a7..edcbfba 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -4,7 +4,6 @@ import ( "fmt" "math/big" "strconv" - "sync" "example.com/tfgrid-kyc-service/internal/configs" "example.com/tfgrid-kyc-service/internal/logger" @@ -16,11 +15,10 @@ import ( type Substrate struct { api *tfchain.Substrate - mu sync.Mutex // TODO: Check if SubstrateAPI is thread safe - logger *logger.Logger + logger *logger.LoggerW } -func New(config configs.TFChain, logger *logger.Logger) (*Substrate, error) { +func New(config configs.TFChain, logger *logger.LoggerW) (*Substrate, error) { mgr := tfchain.NewManager(config.WsProviderURL) api, err := mgr.Substrate() if err != nil { @@ -29,7 +27,6 @@ func New(config configs.TFChain, logger *logger.Logger) (*Substrate, error) { c := &Substrate{ api: api, - mu: sync.Mutex{}, logger: logger, } return c, nil diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 7fd2b98..14e4a5f 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -9,7 +9,6 @@ import ( "github.com/gofiber/fiber/v2" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/readpref" - "go.uber.org/zap" "example.com/tfgrid-kyc-service/internal/errors" "example.com/tfgrid-kyc-service/internal/logger" @@ -20,10 +19,10 @@ import ( type Handler struct { kycService services.KYCService - logger *logger.Logger + logger *logger.LoggerW } -func NewHandler(kycService services.KYCService, logger *logger.Logger) *Handler { +func NewHandler(kycService services.KYCService, logger *logger.LoggerW) *Handler { return &Handler{kycService: kycService, logger: logger} } @@ -106,7 +105,7 @@ 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") + h.logger.Warn("Bad request: missing client_id and twin_id", map[string]interface{}{}) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Either client_id or twin_id must be provided"}) } var verification *models.VerificationOutcome @@ -118,18 +117,18 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { verification, err = h.kycService.GetVerificationStatusByTwinID(c.Context(), twinID) } if err != nil { - h.logger.Error("Failed to get verification status", - zap.String("clientID", clientID), - zap.String("twinID", twinID), - zap.Error(err), - ) + h.logger.Error("Failed to get verification status", map[string]interface{}{ + "clientID": clientID, + "twinID": twinID, + "error": err, + }) return HandleError(c, err) } if verification == nil { - h.logger.Info("Verification not found", - zap.String("clientID", clientID), - zap.String("twinID", twinID), - ) + h.logger.Info("Verification not found", map[string]interface{}{ + "clientID": clientID, + "twinID": twinID, + }) return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Verification not found"}) } response := responses.NewVerificationStatusResponse(verification) @@ -146,7 +145,10 @@ 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", zap.Any("body", string(c.Body())), zap.Any("headers", &c.Request().Header)) + h.logger.Debug("Received verification update", map[string]interface{}{ + "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"}) @@ -156,10 +158,14 @@ func (h *Handler) ProcessVerificationResult() fiber.Handler { decoder := json.NewDecoder(bytes.NewReader(body)) err := decoder.Decode(&result) if err != nil { - h.logger.Error("Error decoding verification update", zap.Error(err)) + h.logger.Error("Error decoding verification update", map[string]interface{}{ + "error": err, + }) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } - h.logger.Debug("Verification update after decoding", zap.Any("result", result)) + h.logger.Debug("Verification update after decoding", map[string]interface{}{ + "result": result, + }) err = h.kycService.ProcessVerificationResult(c.Context(), body, sigHeader, result) if err != nil { return HandleError(c, err) @@ -178,7 +184,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") + h.logger.Error("Received ID expiration notification but not implemented", map[string]interface{}{}) return c.SendStatus(fiber.StatusNotImplemented) } } diff --git a/internal/logger/interface.go b/internal/logger/interface.go new file mode 100644 index 0000000..a9da16c --- /dev/null +++ b/internal/logger/interface.go @@ -0,0 +1,9 @@ +package logger + +type Logger interface { + Debug(msg string, fields map[string]interface{}) + Info(msg string, fields map[string]interface{}) + Warn(msg string, fields map[string]interface{}) + Error(msg string, fields map[string]interface{}) + Fatal(msg string, fields map[string]interface{}) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 0b1d7a8..19ba698 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -1,36 +1,51 @@ package logger import ( + "context" + "fmt" + "example.com/tfgrid-kyc-service/internal/configs" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" ) -type Logger struct { - *zap.Logger +type LoggerW struct { + logger Logger } -var log *Logger +var log *LoggerW func Init(config configs.Log) { - debug := config.Debug - zapConfig := zap.NewProductionConfig() - if debug { - zapConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel) - } - zapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder - var err error - - zapLog, err := zapConfig.Build() + zapLogger, err := NewZapLogger(config.Debug, context.Background()) if err != nil { panic(err) } - log = &Logger{zapLog} + + log = &LoggerW{logger: zapLogger} } -func GetLogger() *Logger { +func GetLogger() *LoggerW { if log == nil { panic("logger not initialized") } return log } + +func (lw *LoggerW) Debug(msg string, fields map[string]interface{}) { + fmt.Println("Debug", msg, fields) + lw.logger.Debug(msg, fields) +} + +func (lw *LoggerW) Info(msg string, fields map[string]interface{}) { + lw.logger.Info(msg, fields) +} + +func (lw *LoggerW) Warn(msg string, fields map[string]interface{}) { + lw.logger.Warn(msg, fields) +} + +func (lw *LoggerW) Error(msg string, fields map[string]interface{}) { + lw.logger.Error(msg, fields) +} + +func (lw *LoggerW) Fatal(msg string, fields map[string]interface{}) { + lw.logger.Fatal(msg, fields) +} diff --git a/internal/logger/zap_logger.go b/internal/logger/zap_logger.go new file mode 100644 index 0000000..a78c045 --- /dev/null +++ b/internal/logger/zap_logger.go @@ -0,0 +1,68 @@ +package logger + +import ( + "context" + + "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, err + } + + return &ZapLogger{logger: zapLog, ctx: ctx}, nil +} + +func (l *ZapLogger) Debug(msg string, fields map[string]interface{}) { + l.addContextCommonFields(fields) + + l.logger.Debug(msg, zap.Any("args", fields)) +} + +func (l *ZapLogger) Info(msg string, fields map[string]interface{}) { + l.addContextCommonFields(fields) + + l.logger.Info(msg, zap.Any("args", fields)) +} + +func (l *ZapLogger) Warn(msg string, fields map[string]interface{}) { + l.addContextCommonFields(fields) + + l.logger.Warn(msg, zap.Any("args", fields)) +} + +func (l *ZapLogger) Error(msg string, fields map[string]interface{}) { + l.addContextCommonFields(fields) + + l.logger.Error(msg, zap.Any("args", fields)) +} + +func (l *ZapLogger) Fatal(msg string, fields map[string]interface{}) { + l.addContextCommonFields(fields) + + l.logger.Fatal(msg, zap.Any("args", fields)) +} + +func (l *ZapLogger) addContextCommonFields(fields map[string]interface{}) { + if l.ctx != nil && l.ctx.Value("commonFields") != 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 7abf0bf..63100f1 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -14,7 +14,6 @@ import ( "github.com/vedhavyas/go-subkey/v2" "github.com/vedhavyas/go-subkey/v2/ed25519" "github.com/vedhavyas/go-subkey/v2/sr25519" - "go.uber.org/zap" ) // CORS returns a CORS middleware @@ -132,7 +131,7 @@ func ValidateChallenge(address, signature, challenge, expectedDomain string, cha return nil } -func NewLoggingMiddleware(logger *logger.Logger) fiber.Handler { +func NewLoggingMiddleware(logger *logger.LoggerW) fiber.Handler { return func(c *fiber.Ctx) error { start := time.Now() path := c.Path() @@ -140,14 +139,14 @@ func NewLoggingMiddleware(logger *logger.Logger) fiber.Handler { ip := c.IP() // Log request - logger.Info("Incoming request", - zap.String("method", method), - zap.String("path", path), - zap.Any("queries", c.Queries()), - zap.String("ip", ip), - zap.String("user_agent", string(c.Request().Header.UserAgent())), - zap.String("x-client-id header", c.Get("X-Client-ID")), - ) + logger.Info("Incoming request", map[string]interface{}{ + "method": method, + "path": path, + "queries": c.Queries(), + "ip": ip, + "user_agent": string(c.Request().Header.UserAgent()), + "headers": c.GetReqHeaders(), + }) // Handle request err := c.Next() @@ -160,25 +159,25 @@ func NewLoggingMiddleware(logger *logger.Logger) fiber.Handler { responseSize := len(c.Response().Body()) // Log the response - logFields := []zap.Field{ - zap.String("method", method), - zap.String("path", path), - zap.String("ip", ip), - zap.Int("status", status), - zap.Duration("duration", duration), - zap.Int("response_size", responseSize), + logFields := map[string]interface{}{ + "method": method, + "path": path, + "ip": ip, + "status": status, + "duration": duration, + "response_size": responseSize, } // Add error if present if err != nil { - logFields = append(logFields, zap.Error(err)) + logFields["error"] = err if status >= 500 { - logger.Error("Request failed", logFields...) + logger.Error("Request failed", logFields) } else { - logger.Info("Request failed", logFields...) + logger.Info("Request failed", logFields) } } else { - logger.Info("Request completed", logFields...) + logger.Info("Request completed", logFields) } return err diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index c160b6d..7cb12c8 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -7,7 +7,6 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "go.uber.org/zap" "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" @@ -15,10 +14,10 @@ import ( type MongoTokenRepository struct { collection *mongo.Collection - logger *logger.Logger + logger *logger.LoggerW } -func NewMongoTokenRepository(db *mongo.Database, logger *logger.Logger) TokenRepository { +func NewMongoTokenRepository(db *mongo.Database, logger *logger.LoggerW) TokenRepository { repo := &MongoTokenRepository{ collection: db.Collection("tokens"), logger: logger, @@ -40,7 +39,7 @@ func (r *MongoTokenRepository) createTTLIndex() { ) if err != nil { - r.logger.Error("Error creating TTL index", zap.Error(err)) + r.logger.Error("Error creating TTL index", map[string]interface{}{"error": err}) } } diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go index cb137c7..48cb9b2 100644 --- a/internal/repository/verification_repository.go +++ b/internal/repository/verification_repository.go @@ -13,10 +13,10 @@ import ( type MongoVerificationRepository struct { collection *mongo.Collection - logger *logger.Logger + logger *logger.LoggerW } -func NewMongoVerificationRepository(db *mongo.Database, logger *logger.Logger) VerificationRepository { +func NewMongoVerificationRepository(db *mongo.Database, logger *logger.LoggerW) VerificationRepository { return &MongoVerificationRepository{ collection: db.Collection("verifications"), logger: logger, diff --git a/internal/server/server.go b/internal/server/server.go index 19ff6cb..0e6c25a 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -25,17 +25,16 @@ import ( "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/storage/mongodb" "github.com/gofiber/swagger" - "go.uber.org/zap" ) // implement server struct that have fiber app and config type Server struct { app *fiber.App config *configs.Config - logger *logger.Logger + logger *logger.LoggerW } -func New(config *configs.Config, logger *logger.Logger) *Server { +func New(config *configs.Config, logger *logger.LoggerW) *Server { // debug log app := fiber.New(fiber.Config{ ReadTimeout: 15 * time.Second, @@ -62,12 +61,6 @@ func New(config *configs.Config, logger *logger.Logger) *Server { return extractIPFromRequest(c) == "127.0.0.1" }, KeyGenerator: func(c *fiber.Ctx) string { - logger.Debug("client IPs detected by the limiter", - zap.String("remoteIp", c.IP()), - zap.String("X-Forwarded-For", c.Get("X-Forwarded-For")), - zap.String("X-Real-IP", c.Get("X-Real-IP")), - zap.Strings("ips", c.IPs()), - ) return extractIPFromRequest(c) }, } @@ -99,7 +92,7 @@ func New(config *configs.Config, logger *logger.Logger) *Server { // Database connection db, err := repository.ConnectToMongoDB(config.MongoDB.URI) if err != nil { - logger.Fatal("Failed to connect to MongoDB", zap.Error(err)) + logger.Fatal("Failed to connect to MongoDB", map[string]interface{}{"error": err}) } database := db.Database(config.MongoDB.DatabaseName) @@ -112,7 +105,7 @@ func New(config *configs.Config, logger *logger.Logger) *Server { substrateClient, err := substrate.New(config.TFChain, logger) if err != nil { - logger.Fatal("Failed to initialize substrate client", zap.Error(err)) + logger.Fatal("Failed to initialize substrate client", map[string]interface{}{"error": err}) } kycService := services.NewKYCService(verificationRepo, tokenRepo, idenfyClient, substrateClient, config, logger) @@ -173,17 +166,17 @@ func (s *Server) Start() { signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) <-sigChan // Graceful shutdown - s.logger.Info("Shutting down server...") + s.logger.Info("Shutting down server...", map[string]interface{}{}) 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:", zap.Error(err)) + s.logger.Error("Server forced to shutdown:", map[string]interface{}{"error": err}) } }() // Start server if err := s.app.Listen(":" + s.config.Server.Port); err != nil && err != http.ErrServerClosed { - s.logger.Fatal("Server startup failed", zap.Error(err)) + s.logger.Fatal("Server startup failed", map[string]interface{}{"error": err}) } } diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index a4793b7..e631d8f 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -14,7 +14,6 @@ import ( "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" "example.com/tfgrid-kyc-service/internal/repository" - "go.uber.org/zap" ) type kycService struct { @@ -23,11 +22,11 @@ type kycService struct { idenfy *idenfy.Idenfy substrate *substrate.Substrate config *configs.Verification - logger *logger.Logger + logger *logger.LoggerW IdenfySuffix string } -func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.Config, logger *logger.Logger) KYCService { +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.Config, logger *logger.LoggerW) KYCService { chainName, err := substrateClient.GetChainName() if err != nil { panic(errors.NewInternalError("error getting chain name", err)) @@ -47,7 +46,7 @@ func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo 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", zap.String("clientID", clientID), zap.Error(err)) + s.logger.Error("Error checking if user is verified", map[string]interface{}{"clientID": clientID, "error": err}) return nil, false, errors.NewInternalError("error getting verification status from database", err) // db error } if isVerified { @@ -55,7 +54,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", zap.String("clientID", clientID), zap.Error(err_)) + s.logger.Error("Error getting token from database", map[string]interface{}{"clientID": clientID, "error": err_}) return nil, false, errors.NewInternalError("error getting token from database", err_) // db error } // check if token is found and not expired @@ -71,7 +70,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", zap.String("clientID", clientID), zap.Error(err_)) + s.logger.Error("Error checking if user account has required balance", map[string]interface{}{"clientID": clientID, "error": err_}) return nil, false, errors.NewExternalError("error checking if user account has required balance", err_) } if !hasRequiredBalance { @@ -81,14 +80,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", zap.String("clientID", clientID), zap.String("uniqueClientID", uniqueClientID), zap.Error(err_)) + s.logger.Error("Error creating iDenfy verification session", map[string]interface{}{"clientID": clientID, "uniqueClientID": uniqueClientID, "error": err_}) return nil, false, errors.NewExternalError("error 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", zap.String("clientID", clientID), zap.Error(err)) + s.logger.Error("Error saving verification token to database", map[string]interface{}{"clientID": clientID, "error": err_}) } return &newToken, true, nil @@ -98,7 +97,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", zap.String("clientID", clientID), zap.String("scanRef", scanRef), zap.Error(err)) + s.logger.Error("Error deleting verification token from database", map[string]interface{}{"clientID": clientID, "scanRef": scanRef, "error": err}) return errors.NewInternalError("error deleting verification token from database", err) } return nil @@ -106,12 +105,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", zap.String("address", address)) + s.logger.Warn("Minimum balance to verify account is 0 which is not recommended", map[string]interface{}{"address": address}) return true, nil } balance, err := s.substrate.GetAccountBalance(address) if err != nil { - s.logger.Error("Error getting account balance", zap.String("address", address), zap.Error(err)) + s.logger.Error("Error getting account balance", map[string]interface{}{"address": address, "error": err}) return false, errors.NewExternalError("error getting account balance", err) } return balance.Cmp(big.NewInt(int64(s.config.MinBalanceToVerifyAccount))) >= 0, nil @@ -124,7 +123,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", zap.String("clientID", clientID), zap.Error(err)) + s.logger.Error("Error getting verification from database", map[string]interface{}{"clientID": clientID, "error": err}) return nil, errors.NewInternalError("error getting verification from database", err) } return verification, nil @@ -143,7 +142,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", zap.String("clientID", clientID), zap.Error(err)) + s.logger.Error("Error getting verification from database", map[string]interface{}{"clientID": clientID, "error": err}) return nil, errors.NewInternalError("error getting verification from database", err) } var outcome models.Outcome @@ -168,7 +167,7 @@ func (s *kycService) GetVerificationStatusByTwinID(ctx context.Context, twinID s // get the address from the twinID address, err := s.substrate.GetAddressByTwinID(twinID) if err != nil { - s.logger.Error("Error getting address from twinID", zap.String("twinID", twinID), zap.Error(err)) + s.logger.Error("Error getting address from twinID", map[string]interface{}{"twinID": twinID, "error": err}) return nil, errors.NewExternalError("error looking up twinID address from TFChain", err) } return s.GetVerificationStatus(ctx, address) @@ -177,25 +176,25 @@ 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", zap.String("sigHeader", sigHeader), zap.Error(err)) + s.logger.Error("Error verifying callback signature", map[string]interface{}{"sigHeader": sigHeader, "error": err}) return errors.NewAuthorizationError("error verifying callback signature", err) } // delete the token with the same clientID and same scanRef result.ClientID = strings.Split(result.ClientID, ":")[0] // TODO: should we check if it have correct suffix? callback misconfiguration maybe? err = s.tokenRepo.DeleteToken(ctx, result.ClientID, result.IdenfyRef) if err != nil { - s.logger.Warn("Error deleting verification token from database", zap.String("clientID", result.ClientID), zap.String("scanRef", result.IdenfyRef), zap.Error(err)) + s.logger.Warn("Error deleting verification token from database", map[string]interface{}{"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", zap.String("clientID", result.ClientID), zap.String("scanRef", result.IdenfyRef), zap.Error(err)) + s.logger.Error("Error saving verification to database", map[string]interface{}{"clientID": result.ClientID, "scanRef": result.IdenfyRef, "error": err}) return errors.NewInternalError("error saving verification to database", err) } } - s.logger.Debug("Verification result processed successfully", zap.Any("result", result)) + s.logger.Debug("Verification result processed successfully", map[string]interface{}{"result": result}) return nil } @@ -206,7 +205,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", zap.String("clientID", clientID), zap.Error(err)) + s.logger.Error("Error getting verification from database", map[string]interface{}{"clientID": clientID, "error": err}) return false, errors.NewInternalError("error getting verification from database", err) } if verification == nil { From 5b423403fffe3a4a65ae78009acbd776f3f0b86f Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 21:52:43 +0200 Subject: [PATCH 061/105] update .gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 70a08e7..2907874 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ go.work go.work.sum -# env file +# env files .env .*.env + +# other files +main From e2fbbbd213665ba1b56f33119261dbdcd85130d2 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 21:59:00 +0200 Subject: [PATCH 062/105] use logger and config interfaces --- go.mod | 2 +- go.sum | 3 +- internal/clients/idenfy/idenfy.go | 23 +++++---- internal/clients/idenfy/idenfy_test.go | 2 +- internal/clients/idenfy/interface.go | 12 +++++ internal/clients/substrate/interface.go | 5 ++ internal/clients/substrate/substrate.go | 7 ++- internal/configs/config.go | 48 ++++++++++++++++++- internal/middleware/middleware.go | 2 +- internal/repository/token_repository.go | 4 +- .../repository/verification_repository.go | 4 +- internal/server/server.go | 12 +++-- internal/services/kyc_service.go | 4 +- scripts/dev/balance/check-account-balance.go | 48 +++++++++++++++---- scripts/dev/chain/chain_name.go | 48 +++++++++++++++---- scripts/dev/twin/get-address-by-twin-id.go | 48 +++++++++++++++---- 16 files changed, 216 insertions(+), 56 deletions(-) create mode 100644 internal/clients/idenfy/interface.go create mode 100644 internal/clients/substrate/interface.go diff --git a/go.mod b/go.mod index 42d6b4e..4204192 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/pierrec/xxHash v0.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/rs/cors v1.8.2 // indirect github.com/rs/zerolog v1.33.0 // indirect diff --git a/go.sum b/go.sum index f65c65f..7eba6b4 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,9 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index 6442fa4..a68b3c8 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -10,7 +10,6 @@ import ( "errors" "fmt" - "example.com/tfgrid-kyc-service/internal/configs" "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" "github.com/valyala/fasthttp" @@ -18,24 +17,24 @@ import ( type Idenfy struct { client *fasthttp.Client // TODO: Interface - config *configs.Idenfy // TODO: Interface - logger *logger.LoggerW + config IdenfyConfig // TODO: Interface + logger logger.Logger } const ( VerificationSessionEndpoint = "/api/v2/token" ) -func New(config configs.Idenfy, logger *logger.LoggerW) *Idenfy { +func New(config IdenfyConfig, logger logger.Logger) *Idenfy { return &Idenfy{ client: &fasthttp.Client{}, - config: &config, + config: config, logger: logger, } } func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) (models.Token, error) { // TODO: Refactor - url := c.config.BaseURL + VerificationSessionEndpoint + url := c.config.GetBaseURL() + VerificationSessionEndpoint req := fasthttp.AcquireRequest() defer fasthttp.ReleaseRequest(req) @@ -45,11 +44,11 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) req.Header.Set("Content-Type", "application/json") // Set basic auth - authStr := c.config.APIKey + ":" + c.config.APISecret + authStr := c.config.GetAPIKey() + ":" + c.config.GetAPISecret() auth := base64.StdEncoding.EncodeToString([]byte(authStr)) req.Header.Set("Authorization", "Basic "+auth) - RequestBody := c.createVerificationSessionRequestBody(clientID, c.config.DevMode) + RequestBody := c.createVerificationSessionRequestBody(clientID, c.config.GetDevMode()) jsonBody, err := json.Marshal(RequestBody) if err != nil { @@ -84,21 +83,21 @@ 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.CallbackSignKey) < 1 { + 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 } - mac := hmac.New(sha256.New, []byte(c.config.CallbackSignKey)) + mac := hmac.New(sha256.New, []byte(c.config.GetCallbackSignKey())) mac.Write(body) if !hmac.Equal(sig, mac.Sum(nil)) { c.logger.Error("Signature verification failed", map[string]interface{}{ "sigHeader": sigHeader, - "key": string(c.config.CallbackSignKey), + "key": c.config.GetCallbackSignKey(), "mac": hex.EncodeToString(mac.Sum(nil)), }) return errors.New("signature verification failed") @@ -111,7 +110,7 @@ func (c *Idenfy) createVerificationSessionRequestBody(clientID string, devMode b RequestBody := map[string]interface{}{ "clientId": clientID, "generateDigitString": true, - "callbackUrl": c.config.CallbackUrl, + "callbackUrl": c.config.GetCallbackUrl(), } if devMode { RequestBody["expiryTime"] = 30 diff --git a/internal/clients/idenfy/idenfy_test.go b/internal/clients/idenfy/idenfy_test.go index 231abab..64361d8 100644 --- a/internal/clients/idenfy/idenfy_test.go +++ b/internal/clients/idenfy/idenfy_test.go @@ -17,7 +17,7 @@ func TestClient_DecodeReaderIdentityCallback(t *testing.T) { expectedSig := "249d9a838e9b981935324b02367ca72552aa430fc766f45f77fab7a81f9f3b9d" logger.Init(configs.Log{}) logger := logger.GetLogger() - client := New(configs.Idenfy{ + client := New(&configs.Idenfy{ CallbackSignKey: "TestingKey", }, logger) diff --git a/internal/clients/idenfy/interface.go b/internal/clients/idenfy/interface.go new file mode 100644 index 0000000..9016240 --- /dev/null +++ b/internal/clients/idenfy/interface.go @@ -0,0 +1,12 @@ +package idenfy + +type IdenfyConfig interface { + GetBaseURL() string + GetCallbackUrl() string + GetNamespace() string + GetDevMode() bool + GetWhitelistedIPs() []string + GetAPIKey() string + GetAPISecret() string + GetCallbackSignKey() string +} diff --git a/internal/clients/substrate/interface.go b/internal/clients/substrate/interface.go new file mode 100644 index 0000000..5ba9517 --- /dev/null +++ b/internal/clients/substrate/interface.go @@ -0,0 +1,5 @@ +package substrate + +type SubstrateConfig interface { + GetWsProviderURL() string +} diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index edcbfba..ad0ff2b 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -5,7 +5,6 @@ import ( "math/big" "strconv" - "example.com/tfgrid-kyc-service/internal/configs" "example.com/tfgrid-kyc-service/internal/logger" // use tfchain go client @@ -15,11 +14,11 @@ import ( type Substrate struct { api *tfchain.Substrate - logger *logger.LoggerW + logger logger.Logger } -func New(config configs.TFChain, logger *logger.LoggerW) (*Substrate, error) { - mgr := tfchain.NewManager(config.WsProviderURL) +func New(config SubstrateConfig, 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) diff --git a/internal/configs/config.go b/internal/configs/config.go index dae147f..101f182 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -1,6 +1,7 @@ package configs import ( + "fmt" "net/url" "slices" @@ -37,9 +38,42 @@ type Idenfy struct { CallbackUrl string `env:"IDENFY_CALLBACK_URL" env-required:"false"` Namespace string `env:"IDENFY_NAMESPACE" env-default:""` } + +// implement getter for Idenfy +func (c *Idenfy) GetCallbackUrl() string { + return c.CallbackUrl +} +func (c *Idenfy) GetNamespace() string { + return c.Namespace +} +func (c *Idenfy) GetDevMode() bool { + return c.DevMode +} +func (c *Idenfy) GetWhitelistedIPs() []string { + return c.WhitelistedIPs +} +func (c *Idenfy) GetAPIKey() string { + return c.APIKey +} +func (c *Idenfy) GetAPISecret() string { + return c.APISecret +} +func (c *Idenfy) GetBaseURL() string { + return c.BaseURL +} +func (c *Idenfy) GetCallbackSignKey() string { + return c.CallbackSignKey +} + type TFChain struct { WsProviderURL string `env:"TFCHAIN_WS_PROVIDER_URL" env-default:"wss://tfchain.grid.tf"` } + +// implement getter for TFChain +func (c *TFChain) GetWsProviderURL() string { + return c.WsProviderURL +} + type Verification struct { SuspiciousVerificationOutcome string `env:"VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME" env-default:"APPROVED"` ExpiredDocumentOutcome string `env:"VERIFICATION_EXPIRED_DOCUMENT_OUTCOME" env-default:"REJECTED"` @@ -68,10 +102,22 @@ func LoadConfig() (*Config, error) { if err != nil { return nil, errors.NewInternalError("error loading config", err) } - cfg.Validate() + // cfg.Validate() return cfg, nil } +func (c Config) GetPublicConfig() Config { + // deducting the secret fields + // copy the config to avoid modifying the original + config := c + config.Idenfy.APIKey = "[REDACTED]" + config.Idenfy.APISecret = "[REDACTED]" + config.Idenfy.CallbackSignKey = "[REDACTED]" + config.MongoDB.URI = "[REDACTED]" + fmt.Println("config", config) + return config +} + // validate config func (c *Config) Validate() error { // iDenfy base URL should be https://ivs.idenfy.com diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 63100f1..d65e7b7 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -131,7 +131,7 @@ func ValidateChallenge(address, signature, challenge, expectedDomain string, cha return nil } -func NewLoggingMiddleware(logger *logger.LoggerW) fiber.Handler { +func NewLoggingMiddleware(logger logger.Logger) fiber.Handler { return func(c *fiber.Ctx) error { start := time.Now() path := c.Path() diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index 7cb12c8..6af3359 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -14,10 +14,10 @@ import ( type MongoTokenRepository struct { collection *mongo.Collection - logger *logger.LoggerW + logger logger.Logger } -func NewMongoTokenRepository(db *mongo.Database, logger *logger.LoggerW) TokenRepository { +func NewMongoTokenRepository(db *mongo.Database, logger logger.Logger) TokenRepository { repo := &MongoTokenRepository{ collection: db.Collection("tokens"), logger: logger, diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go index 48cb9b2..94728a5 100644 --- a/internal/repository/verification_repository.go +++ b/internal/repository/verification_repository.go @@ -13,10 +13,10 @@ import ( type MongoVerificationRepository struct { collection *mongo.Collection - logger *logger.LoggerW + logger logger.Logger } -func NewMongoVerificationRepository(db *mongo.Database, logger *logger.LoggerW) VerificationRepository { +func NewMongoVerificationRepository(db *mongo.Database, logger logger.Logger) VerificationRepository { return &MongoVerificationRepository{ collection: db.Collection("verifications"), logger: logger, diff --git a/internal/server/server.go b/internal/server/server.go index 0e6c25a..d412888 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -31,10 +31,10 @@ import ( type Server struct { app *fiber.App config *configs.Config - logger *logger.LoggerW + logger logger.Logger } -func New(config *configs.Config, logger *logger.LoggerW) *Server { +func New(config *configs.Config, logger logger.Logger) *Server { // debug log app := fiber.New(fiber.Config{ ReadTimeout: 15 * time.Second, @@ -101,16 +101,16 @@ func New(config *configs.Config, logger *logger.LoggerW) *Server { verificationRepo := repository.NewMongoVerificationRepository(database, logger) // Initialize services - idenfyClient := idenfy.New(config.Idenfy, logger) + idenfyClient := idenfy.New(&config.Idenfy, logger) - substrateClient, err := substrate.New(config.TFChain, logger) + substrateClient, err := substrate.New(&config.TFChain, logger) if err != nil { logger.Fatal("Failed to initialize substrate client", map[string]interface{}{"error": err}) } kycService := services.NewKYCService(verificationRepo, tokenRepo, idenfyClient, substrateClient, config, logger) // Initialize handler - handler := handlers.NewHandler(kycService, logger) + handler := handlers.NewHandler(kycService, config, logger) // Routes app.Get("/docs/*", swagger.HandlerDefault) @@ -121,6 +121,8 @@ func New(config *configs.Config, logger *logger.LoggerW) *Server { // status route accepts either client_id or twin_id as query parameters v1.Get("/status", handler.GetVerificationStatus()) v1.Get("/health", handler.HealthCheck(db)) + v1.Get("/configs", handler.GetAppConfigs()) + v1.Get("/version", handler.GetAppVersion()) // Webhook routes webhooks := app.Group("/webhooks/idenfy") webhooks.Post("/verification-update", handler.ProcessVerificationResult()) diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index e631d8f..6222ee4 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -22,11 +22,11 @@ type kycService struct { idenfy *idenfy.Idenfy substrate *substrate.Substrate config *configs.Verification - logger *logger.LoggerW + logger logger.Logger IdenfySuffix string } -func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.Config, logger *logger.LoggerW) KYCService { +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.Config, logger logger.Logger) KYCService { chainName, err := substrateClient.GetChainName() if err != nil { panic(errors.NewInternalError("error getting chain name", err)) diff --git a/scripts/dev/balance/check-account-balance.go b/scripts/dev/balance/check-account-balance.go index 49fb879..9de26e5 100644 --- a/scripts/dev/balance/check-account-balance.go +++ b/scripts/dev/balance/check-account-balance.go @@ -3,20 +3,18 @@ package main import ( "fmt" + "log" "example.com/tfgrid-kyc-service/internal/clients/substrate" - "example.com/tfgrid-kyc-service/internal/configs" - "example.com/tfgrid-kyc-service/internal/logger" ) func main() { - config, err := configs.LoadConfig() - if err != nil { - panic(err) + config := &TFChainConfig{ + WsProviderURL: "wss://tfchain.dev.grid.tf", } - logger.Init(config.Log) - logger := logger.GetLogger() - substrateClient, err := substrate.New(config.TFChain, logger) + + logger := &LoggerW{log.Default()} + substrateClient, err := substrate.New(config, logger) if err != nil { panic(err) } @@ -26,3 +24,37 @@ func main() { } fmt.Println(free_balance) } + +// implement logger.LoggerW for log.Logger +type LoggerW struct { + *log.Logger +} + +func (l *LoggerW) Debug(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Info(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Warn(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Error(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Fatal(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +type TFChainConfig struct { + WsProviderURL string +} + +// implement SubstrateConfig for configs.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 cc5599f..45d780c 100644 --- a/scripts/dev/chain/chain_name.go +++ b/scripts/dev/chain/chain_name.go @@ -2,20 +2,18 @@ package main import ( "fmt" + "log" "example.com/tfgrid-kyc-service/internal/clients/substrate" - "example.com/tfgrid-kyc-service/internal/configs" - "example.com/tfgrid-kyc-service/internal/logger" ) func main() { - config, err := configs.LoadConfig() - if err != nil { - panic(err) + config := &TFChainConfig{ + WsProviderURL: "wss://tfchain.dev.grid.tf", } - logger.Init(config.Log) - logger := logger.GetLogger() - substrateClient, err := substrate.New(config.TFChain, logger) + + logger := &LoggerW{log.Default()} + substrateClient, err := substrate.New(config, logger) if err != nil { panic(err) } @@ -27,3 +25,37 @@ func main() { fmt.Println(chainName) } + +// implement logger.LoggerW for log.Logger +type LoggerW struct { + *log.Logger +} + +func (l *LoggerW) Debug(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Info(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Warn(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Error(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Fatal(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +type TFChainConfig struct { + WsProviderURL string +} + +// implement SubstrateConfig for configs.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 ccf5ac0..eb75e32 100644 --- a/scripts/dev/twin/get-address-by-twin-id.go +++ b/scripts/dev/twin/get-address-by-twin-id.go @@ -2,20 +2,18 @@ package main import ( "fmt" + "log" "example.com/tfgrid-kyc-service/internal/clients/substrate" - "example.com/tfgrid-kyc-service/internal/configs" - "example.com/tfgrid-kyc-service/internal/logger" ) func main() { - config, err := configs.LoadConfig() - if err != nil { - panic(err) + config := &TFChainConfig{ + WsProviderURL: "wss://tfchain.dev.grid.tf", } - logger.Init(config.Log) - logger := logger.GetLogger() - substrateClient, err := substrate.New(config.TFChain, logger) + + logger := &LoggerW{log.Default()} + substrateClient, err := substrate.New(config, logger) if err != nil { panic(err) } @@ -27,3 +25,37 @@ func main() { fmt.Println(address) } + +// implement logger.LoggerW for log.Logger +type LoggerW struct { + *log.Logger +} + +func (l *LoggerW) Debug(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Info(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Warn(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Error(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +func (l *LoggerW) Fatal(msg string, fields map[string]interface{}) { + l.Println(msg) +} + +type TFChainConfig struct { + WsProviderURL string +} + +// implement SubstrateConfig for configs.TFChain +func (c *TFChainConfig) GetWsProviderURL() string { + return c.WsProviderURL +} From 46db5f798757ee1551002554701c6620f6c77bd6 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 21:59:38 +0200 Subject: [PATCH 063/105] implement configs and version endpoints --- Dockerfile | 6 ++++-- api/docs/docs.go | 32 ++++++++++++++++++++++++++++++++ api/docs/swagger.json | 32 ++++++++++++++++++++++++++++++++ api/docs/swagger.yaml | 21 +++++++++++++++++++++ internal/build/build.go | 3 +++ internal/handlers/handlers.go | 31 ++++++++++++++++++++++++++++--- internal/responses/responses.go | 8 ++++++++ 7 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 internal/build/build.go diff --git a/Dockerfile b/Dockerfile index 4b9744d..20048dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,15 @@ FROM golang:1.22-alpine AS builder WORKDIR /app +RUN apk add --no-cache git + COPY go.mod go.sum ./ RUN go mod download COPY . . - -RUN CGO_ENABLED=0 GOOS=linux go build -o tfgrid-kyc cmd/api/main.go +RUN VERSION=`git describe --tags` && \ + CGO_ENABLED=0 GOOS=linux go build -o tfgrid-kyc -ldflags "-X example.com/tfgrid-kyc-service/internal/build.Version=$VERSION" cmd/api/main.go FROM alpine:3.19 diff --git a/api/docs/docs.go b/api/docs/docs.go index 44b4626..70d6e46 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -20,6 +20,21 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/api/v1/configs": { + "get": { + "description": "Returns the app configs", + "tags": [ + "Misc" + ], + "summary": "Get App Configs", + "responses": { + "200": { + "description": "OK", + "schema": {} + } + } + } + }, "/api/v1/data": { "get": { "description": "Returns the verification data for a client", @@ -255,6 +270,23 @@ const docTemplate = `{ } } }, + "/api/v1/version": { + "get": { + "description": "Returns the app version", + "tags": [ + "Misc" + ], + "summary": "Get App Version", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, "/webhooks/idenfy/id-expiration": { "post": { "description": "Processes the doc expiration notification for a client", diff --git a/api/docs/swagger.json b/api/docs/swagger.json index ff3e325..60af865 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -13,6 +13,21 @@ }, "basePath": "/", "paths": { + "/api/v1/configs": { + "get": { + "description": "Returns the app configs", + "tags": [ + "Misc" + ], + "summary": "Get App Configs", + "responses": { + "200": { + "description": "OK", + "schema": {} + } + } + } + }, "/api/v1/data": { "get": { "description": "Returns the verification data for a client", @@ -248,6 +263,23 @@ } } }, + "/api/v1/version": { + "get": { + "description": "Returns the app version", + "tags": [ + "Misc" + ], + "summary": "Get App Version", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, "/webhooks/idenfy/id-expiration": { "post": { "description": "Processes the doc expiration notification for a client", diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index dc4febc..fce046d 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -153,6 +153,16 @@ info: title: TFGrid KYC API version: 0.1.0 paths: + /api/v1/configs: + get: + description: Returns the app configs + responses: + "200": + description: OK + schema: {} + summary: Get App Configs + tags: + - Misc /api/v1/data: get: consumes: @@ -313,6 +323,17 @@ paths: summary: Get or Generate iDenfy Verification Token tags: - Token + /api/v1/version: + get: + description: Returns the app version + responses: + "200": + description: OK + schema: + type: string + summary: Get App Version + tags: + - Misc /webhooks/idenfy/id-expiration: post: consumes: diff --git a/internal/build/build.go b/internal/build/build.go new file mode 100644 index 0000000..cb4d279 --- /dev/null +++ b/internal/build/build.go @@ -0,0 +1,3 @@ +package build + +var Version string diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 14e4a5f..9ce2bce 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -10,6 +10,8 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/readpref" + "example.com/tfgrid-kyc-service/internal/build" + "example.com/tfgrid-kyc-service/internal/configs" "example.com/tfgrid-kyc-service/internal/errors" "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" @@ -19,11 +21,12 @@ import ( type Handler struct { kycService services.KYCService - logger *logger.LoggerW + config *configs.Config + logger logger.Logger } -func NewHandler(kycService services.KYCService, logger *logger.LoggerW) *Handler { - return &Handler{kycService: kycService, logger: logger} +func NewHandler(kycService services.KYCService, config *configs.Config, logger logger.Logger) *Handler { + return &Handler{kycService: kycService, config: config, logger: logger} } // @Summary Get or Generate iDenfy Verification Token @@ -218,6 +221,28 @@ func (h *Handler) HealthCheck(dbClient *mongo.Client) fiber.Handler { } } +// @Summary Get App Configs +// @Description Returns the app configs +// @Tags Misc +// @Success 200 {object} responses.AppConfigsResponse +// @Router /api/v1/configs [get] +func (h *Handler) GetAppConfigs() fiber.Handler { + return func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{"result": h.config.GetPublicConfig()}) + } +} + +// @Summary Get App Version +// @Description Returns the app version +// @Tags Misc +// @Success 200 {object} string +// @Router /api/v1/version [get] +func (h *Handler) GetAppVersion() fiber.Handler { + return func(c *fiber.Ctx) error { + return c.JSON(responses.AppVersionResponse{Version: build.Version}) + } +} + func HandleError(c *fiber.Ctx, err error) error { if serviceErr, ok := err.(*errors.ServiceError); ok { return HandleServiceError(c, serviceErr) diff --git a/internal/responses/responses.go b/internal/responses/responses.go index dfdf9ba..0a9870b 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -174,3 +174,11 @@ func NewVerificationDataResponse(verification *models.Verification) *Verificatio ClientID: verification.ClientID, } } + +// appConfigsResponse +type AppConfigsResponse interface{} + +// appVersionResponse +type AppVersionResponse struct { + Version string `json:"version"` +} From 0118a95d6fff4924d71e1ca6fafc93fed3a97387 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 22:09:41 +0200 Subject: [PATCH 064/105] Update README.md --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index a3f53c6..af477d4 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ The application uses environment variables for configuration. Here's a list of a - `IDENFY_WHITELISTED_IPS`: Comma-separated list of whitelisted IPs for iDenfy callbacks - `IDENFY_DEV_MODE`: Enable development mode for iDenfy integration (default: false) (note: works only in iDenfy dev environment, enabling it in test or production environment will cause iDenfy to reject the requests) - `IDENFY_CALLBACK_URL`: URL for iDenfy verification update callbacks. (example: `https://{KYC-SERVICE-DOMAIN}/webhooks/idenfy/verification-update`) +- `IDENFY_NAMESPACE`: Namespace for isolating diffrent TF KYC verifier services data in same iDenfy backend (default: "") (note: if you are using the same iDenfy backend for multiple services on same tfchain network, you can set this to the unique identifier of the service to isolate the data. don't touch unless you know what you are doing) ### TFChain Configuration @@ -70,6 +71,7 @@ The application uses environment variables for configuration. Here's a list of a - `VERIFICATION_SUSPICIOUS_VERIFICATION_OUTCOME`: Outcome for suspicious verifications (default: "APPROVED") - `VERIFICATION_EXPIRED_DOCUMENT_OUTCOME`: Outcome for expired documents (default: "REJECTED") - `VERIFICATION_MIN_BALANCE_TO_VERIFY_ACCOUNT`: Minimum balance in unitTFT required to verify an account (default: 10000000) +- `VERIFICATION_ALWAYS_VERIFIED_IDS`: Comma-separated list of TFChain SS58Addresses that are always verified (default: "") ### Rate Limiting @@ -210,6 +212,19 @@ To run the application locally: - `healthy`: All systems operational - `degraded`: Some systems experiencing issues +### Miscellaneous + +- `GET /api/v1/version` + - Get application version + - Responses: + - `200`: Returns application version + - `version`: Application version + +- `GET /api/v1/configs` + - Get application configurations + - Responses: + - `200`: Returns application configurations + ### Documentation - `GET /docs` From c24b9615486319d5cc324e0efa7410847f79cdf5 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 22:23:07 +0200 Subject: [PATCH 065/105] update version endpoint response --- internal/build/build.go | 2 +- internal/configs/config.go | 2 -- internal/handlers/handlers.go | 3 ++- internal/server/server.go | 4 +++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index cb4d279..4f365c1 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -1,3 +1,3 @@ package build -var Version string +var Version string = "unknown" diff --git a/internal/configs/config.go b/internal/configs/config.go index 101f182..87173be 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -1,7 +1,6 @@ package configs import ( - "fmt" "net/url" "slices" @@ -114,7 +113,6 @@ func (c Config) GetPublicConfig() Config { config.Idenfy.APISecret = "[REDACTED]" config.Idenfy.CallbackSignKey = "[REDACTED]" config.MongoDB.URI = "[REDACTED]" - fmt.Println("config", config) return config } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 9ce2bce..923d194 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -239,7 +239,8 @@ func (h *Handler) GetAppConfigs() fiber.Handler { // @Router /api/v1/version [get] func (h *Handler) GetAppVersion() fiber.Handler { return func(c *fiber.Ctx) error { - return c.JSON(responses.AppVersionResponse{Version: build.Version}) + response := responses.AppVersionResponse{Version: build.Version} + return c.JSON(fiber.Map{"result": response}) } } diff --git a/internal/server/server.go b/internal/server/server.go index d412888..d3170ba 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -86,7 +86,9 @@ func New(config *configs.Config, logger logger.Logger) *Server { // Global middlewares app.Use(middleware.NewLoggingMiddleware(logger)) app.Use(middleware.CORS()) - app.Use(recover.New()) + recoverConfig := recover.ConfigDefault + recoverConfig.EnableStackTrace = true + app.Use(recover.New(recoverConfig)) app.Use(helmet.New()) // Database connection From 72236688b0de4461b01a4e21d7ac74ceb1ab2181 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 23:08:49 +0200 Subject: [PATCH 066/105] use public config in logs --- cmd/api/main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 65cbcf7..f4c0901 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -26,9 +26,8 @@ func main() { logger.Init(config.Log) logger := logger.GetLogger() - // defer logger.Sync() - logger.Debug("Configuration loaded successfully", map[string]interface{}{"config": config}) // TODO: remove me after testing + logger.Debug("Configuration loaded successfully", map[string]interface{}{"config": config.GetPublicConfig()}) server := server.New(config, logger) logger.Info("Starting server on port:", map[string]interface{}{"port": config.Server.Port}) From 4da6b0e91c737cf119eaa60a5666efb5341fa972 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 23:10:03 +0200 Subject: [PATCH 067/105] clean cred from idenfy client debug logs --- internal/clients/idenfy/idenfy.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index a68b3c8..fac634a 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) resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) c.logger.Debug("Preparing iDenfy verification session request", map[string]interface{}{ - "request": req, + "request": jsonBody, }) err = c.client.Do(req, resp) if err != nil { @@ -67,10 +67,14 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) } if resp.StatusCode() < 200 || resp.StatusCode() >= 300 { + c.logger.Debug("Received unexpected status code from iDenfy", map[string]interface{}{ + "status": resp.StatusCode(), + "error": string(resp.Body()), + }) return models.Token{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) } c.logger.Debug("Received response from iDenfy", map[string]interface{}{ - "response": resp, + "response": string(resp.Body()), }) var result models.Token @@ -95,11 +99,6 @@ func (c *Idenfy) VerifyCallbackSignature(ctx context.Context, body []byte, sigHe mac.Write(body) if !hmac.Equal(sig, mac.Sum(nil)) { - c.logger.Error("Signature verification failed", map[string]interface{}{ - "sigHeader": sigHeader, - "key": c.config.GetCallbackSignKey(), - "mac": hex.EncodeToString(mac.Sum(nil)), - }) return errors.New("signature verification failed") } return nil From 68f0efd48b2cbee8ef435d285c7b7fae6ed6a7f7 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 23:11:00 +0200 Subject: [PATCH 068/105] update health endpoint response --- internal/handlers/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 923d194..9789f8d 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -217,7 +217,7 @@ func (h *Handler) HealthCheck(dbClient *mongo.Client) fiber.Handler { Errors: []string{}, } - return c.JSON(health) + return c.JSON(fiber.Map{"result": health}) } } From 319faa5d25946d46c0a47da122a1ee32afc0cb18 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 23:12:40 +0200 Subject: [PATCH 069/105] error on reciving callback with client id that has diffrent network suffix --- internal/services/kyc_service.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index 6222ee4..c844e89 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -2,6 +2,7 @@ package services import ( "context" + "fmt" "math/big" "slices" "strings" @@ -16,6 +17,8 @@ import ( "example.com/tfgrid-kyc-service/internal/repository" ) +const TFT_CONVERSION_FACTOR = 10000000 + type kycService struct { verificationRepo repository.VerificationRepository tokenRepo repository.TokenRepository @@ -74,7 +77,8 @@ func (s *kycService) GetorCreateVerificationToken(ctx context.Context, clientID return nil, false, errors.NewExternalError("error checking if user account has required balance", err_) } if !hasRequiredBalance { - return nil, false, errors.NewNotSufficientBalanceError("account does not have the required balance", nil) + 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 @@ -133,6 +137,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", map[string]interface{}{"clientID": clientID}) return &models.VerificationOutcome{ Final: &final, ClientID: clientID, @@ -179,8 +184,19 @@ func (s *kycService) ProcessVerificationResult(ctx context.Context, body []byte, s.logger.Error("Error verifying callback signature", map[string]interface{}{"sigHeader": sigHeader, "error": err}) return errors.NewAuthorizationError("error verifying callback signature", err) } + clientIDParts := strings.Split(result.ClientID, ":") + if len(clientIDParts) < 2 { + s.logger.Error("clientID have no network suffix", map[string]interface{}{"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", map[string]interface{}{"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 = strings.Split(result.ClientID, ":")[0] // TODO: should we check if it have correct suffix? callback misconfiguration maybe? + 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", map[string]interface{}{"clientID": result.ClientID, "scanRef": result.IdenfyRef, "error": err}) From 5e07827980bd65012866f9b211fe66ad1c773c92 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 23:18:12 +0200 Subject: [PATCH 070/105] fix duplicated debug logs --- internal/logger/logger.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 19ba698..c70bf50 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -2,7 +2,6 @@ package logger import ( "context" - "fmt" "example.com/tfgrid-kyc-service/internal/configs" ) @@ -30,7 +29,6 @@ func GetLogger() *LoggerW { } func (lw *LoggerW) Debug(msg string, fields map[string]interface{}) { - fmt.Println("Debug", msg, fields) lw.logger.Debug(msg, fields) } From 15dba745fbf08046c20be2f2c3ccd0f1accbb72e Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 23:22:43 +0200 Subject: [PATCH 071/105] update swagger doc --- internal/handlers/handlers.go | 12 ++++++------ internal/server/server.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 9789f8d..a46a23e 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -221,23 +221,23 @@ func (h *Handler) HealthCheck(dbClient *mongo.Client) fiber.Handler { } } -// @Summary Get App Configs -// @Description Returns the app configs +// @Summary Get Service Configs +// @Description Returns the service configs // @Tags Misc // @Success 200 {object} responses.AppConfigsResponse // @Router /api/v1/configs [get] -func (h *Handler) GetAppConfigs() fiber.Handler { +func (h *Handler) GetServiceConfigs() fiber.Handler { return func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"result": h.config.GetPublicConfig()}) } } -// @Summary Get App Version -// @Description Returns the app version +// @Summary Get Service Version +// @Description Returns the service version // @Tags Misc // @Success 200 {object} string // @Router /api/v1/version [get] -func (h *Handler) GetAppVersion() fiber.Handler { +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}) diff --git a/internal/server/server.go b/internal/server/server.go index d3170ba..0dac2d2 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -123,8 +123,8 @@ func New(config *configs.Config, logger logger.Logger) *Server { // status route accepts either client_id or twin_id as query parameters v1.Get("/status", handler.GetVerificationStatus()) v1.Get("/health", handler.HealthCheck(db)) - v1.Get("/configs", handler.GetAppConfigs()) - v1.Get("/version", handler.GetAppVersion()) + v1.Get("/configs", handler.GetServiceConfigs()) + v1.Get("/version", handler.GetServiceVersion()) // Webhook routes webhooks := app.Group("/webhooks/idenfy") webhooks.Post("/verification-update", handler.ProcessVerificationResult()) From 5f64516f77bf4cfab39e97b25c3364283c234dae Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Fri, 1 Nov 2024 23:23:06 +0200 Subject: [PATCH 072/105] update swagger doc --- api/docs/docs.go | 8 ++++---- api/docs/swagger.json | 8 ++++---- api/docs/swagger.yaml | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 70d6e46..4a27d07 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -22,11 +22,11 @@ const docTemplate = `{ "paths": { "/api/v1/configs": { "get": { - "description": "Returns the app configs", + "description": "Returns the service configs", "tags": [ "Misc" ], - "summary": "Get App Configs", + "summary": "Get Service Configs", "responses": { "200": { "description": "OK", @@ -272,11 +272,11 @@ const docTemplate = `{ }, "/api/v1/version": { "get": { - "description": "Returns the app version", + "description": "Returns the service version", "tags": [ "Misc" ], - "summary": "Get App Version", + "summary": "Get Service Version", "responses": { "200": { "description": "OK", diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 60af865..9fb4b78 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -15,11 +15,11 @@ "paths": { "/api/v1/configs": { "get": { - "description": "Returns the app configs", + "description": "Returns the service configs", "tags": [ "Misc" ], - "summary": "Get App Configs", + "summary": "Get Service Configs", "responses": { "200": { "description": "OK", @@ -265,11 +265,11 @@ }, "/api/v1/version": { "get": { - "description": "Returns the app version", + "description": "Returns the service version", "tags": [ "Misc" ], - "summary": "Get App Version", + "summary": "Get Service Version", "responses": { "200": { "description": "OK", diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index fce046d..43e5113 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -155,12 +155,12 @@ info: paths: /api/v1/configs: get: - description: Returns the app configs + description: Returns the service configs responses: "200": description: OK schema: {} - summary: Get App Configs + summary: Get Service Configs tags: - Misc /api/v1/data: @@ -325,13 +325,13 @@ paths: - Token /api/v1/version: get: - description: Returns the app version + description: Returns the service version responses: "200": description: OK schema: type: string - summary: Get App Version + summary: Get Service Version tags: - Misc /webhooks/idenfy/id-expiration: From 42ab797d26ab6673eebc315581a2cb6a1ade667e Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Sat, 2 Nov 2024 03:50:19 +0200 Subject: [PATCH 073/105] refactor --- cmd/api/main.go | 18 +- go.mod | 2 +- internal/clients/idenfy/idenfy.go | 12 +- internal/clients/idenfy/idenfy_test.go | 6 +- internal/configs/config.go | 20 +- internal/handlers/handlers.go | 36 +-- internal/logger/interface.go | 10 +- internal/logger/logger.go | 12 +- internal/logger/zap_logger.go | 14 +- internal/middleware/middleware.go | 12 +- internal/repository/mongo.go | 6 +- internal/repository/token_repository.go | 4 +- internal/server/server.go | 219 +++++++++++++------ internal/services/kyc_service.go | 38 ++-- scripts/dev/balance/check-account-balance.go | 11 +- scripts/dev/chain/chain_name.go | 11 +- scripts/dev/twin/get-address-by-twin-id.go | 11 +- 17 files changed, 277 insertions(+), 165 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index f4c0901..88d76ee 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -25,11 +25,21 @@ func main() { } logger.Init(config.Log) - logger := logger.GetLogger() + log := logger.GetLogger() - logger.Debug("Configuration loaded successfully", map[string]interface{}{"config": config.GetPublicConfig()}) + log.Debug("Configuration loaded successfully", logger.Fields{ + "config": config.GetPublicConfig(), + }) - server := server.New(config, logger) - logger.Info("Starting server on port:", map[string]interface{}{"port": config.Server.Port}) + server, err := server.New(config, log) + if err != nil { + log.Fatal("Failed to create server:", logger.Fields{ + "error": err, + }) + } + + log.Info("Starting server on port:", logger.Fields{ + "port": config.Server.Port, + }) server.Start() } diff --git a/go.mod b/go.mod index 4204192..fa3d2e9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module example.com/tfgrid-kyc-service -go 1.22.1 +go 1.22 require ( github.com/gofiber/fiber/v2 v2.52.5 diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index fac634a..a1179f3 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "time" "example.com/tfgrid-kyc-service/internal/logger" "example.com/tfgrid-kyc-service/internal/models" @@ -55,10 +56,15 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) return models.Token{}, fmt.Errorf("error marshaling request body: %w", err) } req.SetBody(jsonBody) + // Set deadline from context + deadline, ok := ctx.Deadline() + if ok { + req.SetTimeout(time.Until(deadline)) + } resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) - c.logger.Debug("Preparing iDenfy verification session request", map[string]interface{}{ + c.logger.Debug("Preparing iDenfy verification session request", logger.Fields{ "request": jsonBody, }) err = c.client.Do(req, resp) @@ -67,13 +73,13 @@ func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) } if resp.StatusCode() < 200 || resp.StatusCode() >= 300 { - c.logger.Debug("Received unexpected status code from iDenfy", map[string]interface{}{ + c.logger.Debug("Received unexpected status code from iDenfy", logger.Fields{ "status": resp.StatusCode(), "error": string(resp.Body()), }) return models.Token{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode()) } - c.logger.Debug("Received response from iDenfy", map[string]interface{}{ + c.logger.Debug("Received response from iDenfy", logger.Fields{ "response": string(resp.Body()), }) diff --git a/internal/clients/idenfy/idenfy_test.go b/internal/clients/idenfy/idenfy_test.go index 64361d8..569047f 100644 --- a/internal/clients/idenfy/idenfy_test.go +++ b/internal/clients/idenfy/idenfy_test.go @@ -16,10 +16,10 @@ import ( func TestClient_DecodeReaderIdentityCallback(t *testing.T) { expectedSig := "249d9a838e9b981935324b02367ca72552aa430fc766f45f77fab7a81f9f3b9d" logger.Init(configs.Log{}) - logger := logger.GetLogger() + log := logger.GetLogger() client := New(&configs.Idenfy{ CallbackSignKey: "TestingKey", - }, logger) + }, log) assert.NotNil(t, client, "Client is nil") webhook1, err := os.ReadFile("testdata/webhook.1.json") @@ -31,7 +31,7 @@ func TestClient_DecodeReaderIdentityCallback(t *testing.T) { err = decoder.Decode(&resp) assert.NoError(t, err) // Basic verification info - logger.Info("resp", map[string]interface{}{ + log.Info("resp", logger.Fields{ "resp": resp, }) assert.Equal(t, "123", resp.ClientID) diff --git a/internal/configs/config.go b/internal/configs/config.go index 87173be..78b9efd 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -1,10 +1,10 @@ package configs import ( + "errors" "net/url" "slices" - "example.com/tfgrid-kyc-service/internal/errors" "github.com/ilyakaznacheev/cleanenv" ) @@ -99,7 +99,7 @@ func LoadConfig() (*Config, error) { cfg := &Config{} err := cleanenv.ReadEnv(cfg) if err != nil { - return nil, errors.NewInternalError("error loading config", err) + return nil, errors.Join(errors.New("error loading config"), err) } // cfg.Validate() return cfg, nil @@ -120,36 +120,36 @@ func (c Config) GetPublicConfig() Config { func (c *Config) Validate() error { // iDenfy base URL should be https://ivs.idenfy.com if c.Idenfy.BaseURL != "https://ivs.idenfy.com" { - panic("invalid iDenfy base URL") + return errors.New("invalid iDenfy base URL") } // CallbackUrl should be valid URL parsedCallbackUrl, err := url.ParseRequestURI(c.Idenfy.CallbackUrl) if err != nil { - panic("invalid CallbackUrl") + return errors.New("invalid CallbackUrl") } // CallbackSignKey should not be empty if len(c.Idenfy.CallbackSignKey) < 16 { - panic("CallbackSignKey should be at least 16 characters long") + return errors.New("CallbackSignKey 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" { - panic("invalid WsProviderURL") + return errors.New("invalid WsProviderURL") } // domain should not be empty and same as domain in CallbackUrl if parsedCallbackUrl.Host != c.Challenge.Domain { - panic("invalid Challenge Domain. It should be same as domain in CallbackUrl") + return errors.New("invalid Challenge Domain. It should be same as domain in CallbackUrl") } // Window should be greater than 2 if c.Challenge.Window < 2 { - panic("invalid Challenge Window. It should be greater than 2 otherwise it will be too short and verification can fail in slow networks") + return errors.New("invalid Challenge Window. It should be greater than 2 otherwise it will be too short and verification can fail in slow networks") } // SuspiciousVerificationOutcome should be either APPROVED or REJECTED if !slices.Contains([]string{"APPROVED", "REJECTED"}, c.Verification.SuspiciousVerificationOutcome) { - panic("invalid SuspiciousVerificationOutcome") + return errors.New("invalid SuspiciousVerificationOutcome") } // ExpiredDocumentOutcome should be either APPROVED or REJECTED if !slices.Contains([]string{"APPROVED", "REJECTED"}, c.Verification.ExpiredDocumentOutcome) { - panic("invalid ExpiredDocumentOutcome") + return errors.New("invalid ExpiredDocumentOutcome") } return nil } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index a46a23e..a777c3f 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -48,8 +48,9 @@ func NewHandler(kycService services.KYCService, config *configs.Config, logger l func (h *Handler) GetorCreateVerificationToken() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") - - token, isNewToken, err := h.kycService.GetorCreateVerificationToken(c.Context(), clientID) + ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) + defer cancel() + token, isNewToken, err := h.kycService.GetorCreateVerificationToken(ctx, clientID) if err != nil { return HandleError(c, err) } @@ -78,7 +79,9 @@ func (h *Handler) GetorCreateVerificationToken() fiber.Handler { func (h *Handler) GetVerificationData() fiber.Handler { return func(c *fiber.Ctx) error { clientID := c.Get("X-Client-ID") - verification, err := h.kycService.GetVerificationData(c.Context(), clientID) + ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) + defer cancel() + verification, err := h.kycService.GetVerificationData(ctx, clientID) if err != nil { return HandleError(c, err) } @@ -108,19 +111,20 @@ 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", map[string]interface{}{}) + 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"}) } var verification *models.VerificationOutcome var err error - + ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) + defer cancel() if clientID != "" { - verification, err = h.kycService.GetVerificationStatus(c.Context(), clientID) + verification, err = h.kycService.GetVerificationStatus(ctx, clientID) } else { - verification, err = h.kycService.GetVerificationStatusByTwinID(c.Context(), twinID) + verification, err = h.kycService.GetVerificationStatusByTwinID(ctx, twinID) } if err != nil { - h.logger.Error("Failed to get verification status", map[string]interface{}{ + h.logger.Error("Failed to get verification status", logger.Fields{ "clientID": clientID, "twinID": twinID, "error": err, @@ -128,7 +132,7 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { return HandleError(c, err) } if verification == nil { - h.logger.Info("Verification not found", map[string]interface{}{ + h.logger.Info("Verification not found", logger.Fields{ "clientID": clientID, "twinID": twinID, }) @@ -148,7 +152,7 @@ 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", map[string]interface{}{ + h.logger.Debug("Received verification update", logger.Fields{ "body": string(c.Body()), "headers": &c.Request().Header, }) @@ -161,15 +165,17 @@ func (h *Handler) ProcessVerificationResult() fiber.Handler { decoder := json.NewDecoder(bytes.NewReader(body)) err := decoder.Decode(&result) if err != nil { - h.logger.Error("Error decoding verification update", map[string]interface{}{ + h.logger.Error("Error decoding verification update", logger.Fields{ "error": err, }) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } - h.logger.Debug("Verification update after decoding", map[string]interface{}{ + h.logger.Debug("Verification update after decoding", logger.Fields{ "result": result, }) - err = h.kycService.ProcessVerificationResult(c.Context(), body, sigHeader, 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) } @@ -187,7 +193,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", map[string]interface{}{}) + h.logger.Error("Received ID expiration notification but not implemented", nil) return c.SendStatus(fiber.StatusNotImplemented) } } @@ -199,7 +205,7 @@ func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { // @Router /api/v1/health [get] func (h *Handler) HealthCheck(dbClient *mongo.Client) fiber.Handler { return func(c *fiber.Ctx) error { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) defer cancel() err := dbClient.Ping(ctx, readpref.Primary()) if err != nil { diff --git a/internal/logger/interface.go b/internal/logger/interface.go index a9da16c..2c36ee8 100644 --- a/internal/logger/interface.go +++ b/internal/logger/interface.go @@ -1,9 +1,9 @@ package logger type Logger interface { - Debug(msg string, fields map[string]interface{}) - Info(msg string, fields map[string]interface{}) - Warn(msg string, fields map[string]interface{}) - Error(msg string, fields map[string]interface{}) - Fatal(msg string, fields map[string]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 index c70bf50..99267b0 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -10,6 +10,8 @@ type LoggerW struct { logger Logger } +type Fields map[string]interface{} + var log *LoggerW func Init(config configs.Log) { @@ -28,22 +30,22 @@ func GetLogger() *LoggerW { return log } -func (lw *LoggerW) Debug(msg string, fields map[string]interface{}) { +func (lw *LoggerW) Debug(msg string, fields Fields) { lw.logger.Debug(msg, fields) } -func (lw *LoggerW) Info(msg string, fields map[string]interface{}) { +func (lw *LoggerW) Info(msg string, fields Fields) { lw.logger.Info(msg, fields) } -func (lw *LoggerW) Warn(msg string, fields map[string]interface{}) { +func (lw *LoggerW) Warn(msg string, fields Fields) { lw.logger.Warn(msg, fields) } -func (lw *LoggerW) Error(msg string, fields map[string]interface{}) { +func (lw *LoggerW) Error(msg string, fields Fields) { lw.logger.Error(msg, fields) } -func (lw *LoggerW) Fatal(msg string, fields map[string]interface{}) { +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 index a78c045..2f53803 100644 --- a/internal/logger/zap_logger.go +++ b/internal/logger/zap_logger.go @@ -27,38 +27,38 @@ func NewZapLogger(debug bool, ctx context.Context) (*ZapLogger, error) { return &ZapLogger{logger: zapLog, ctx: ctx}, nil } -func (l *ZapLogger) Debug(msg string, fields map[string]interface{}) { +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 map[string]interface{}) { +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 map[string]interface{}) { +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 map[string]interface{}) { +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 map[string]interface{}) { +func (l *ZapLogger) Fatal(msg string, fields Fields) { l.addContextCommonFields(fields) l.logger.Fatal(msg, zap.Any("args", fields)) } -func (l *ZapLogger) addContextCommonFields(fields map[string]interface{}) { - if l.ctx != nil && l.ctx.Value("commonFields") != nil { +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 d65e7b7..0869e1f 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -131,7 +131,7 @@ func ValidateChallenge(address, signature, challenge, expectedDomain string, cha return nil } -func NewLoggingMiddleware(logger logger.Logger) fiber.Handler { +func NewLoggingMiddleware(log logger.Logger) fiber.Handler { return func(c *fiber.Ctx) error { start := time.Now() path := c.Path() @@ -139,7 +139,7 @@ func NewLoggingMiddleware(logger logger.Logger) fiber.Handler { ip := c.IP() // Log request - logger.Info("Incoming request", map[string]interface{}{ + log.Info("Incoming request", logger.Fields{ "method": method, "path": path, "queries": c.Queries(), @@ -159,7 +159,7 @@ func NewLoggingMiddleware(logger logger.Logger) fiber.Handler { responseSize := len(c.Response().Body()) // Log the response - logFields := map[string]interface{}{ + logFields := logger.Fields{ "method": method, "path": path, "ip": ip, @@ -172,12 +172,12 @@ func NewLoggingMiddleware(logger logger.Logger) fiber.Handler { if err != nil { logFields["error"] = err if status >= 500 { - logger.Error("Request failed", logFields) + log.Error("Request failed", logFields) } else { - logger.Info("Request failed", logFields) + log.Info("Request failed", logFields) } } else { - logger.Info("Request completed", logFields) + log.Info("Request completed", logFields) } return err diff --git a/internal/repository/mongo.go b/internal/repository/mongo.go index 2374355..6f89485 100644 --- a/internal/repository/mongo.go +++ b/internal/repository/mongo.go @@ -2,16 +2,12 @@ package repository import ( "context" - "time" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) -func ConnectToMongoDB(mongoURI string) (*mongo.Client, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - +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 diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index 6af3359..24af9dd 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -27,7 +27,7 @@ func NewMongoTokenRepository(db *mongo.Database, logger logger.Logger) TokenRepo } func (r *MongoTokenRepository) createTTLIndex() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := r.collection.Indexes().CreateOne( @@ -39,7 +39,7 @@ func (r *MongoTokenRepository) createTTLIndex() { ) if err != nil { - r.logger.Error("Error creating TTL index", map[string]interface{}{"error": err}) + r.logger.Error("Error creating TTL index", logger.Fields{"error": err}) } } diff --git a/internal/server/server.go b/internal/server/server.go index 0dac2d2..114d2e7 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -2,6 +2,8 @@ package server import ( "context" + "errors" + "fmt" "net" "net/http" "os" @@ -25,112 +27,200 @@ import ( "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/storage/mongodb" "github.com/gofiber/swagger" + "go.mongodb.org/mongo-driver/mongo" ) -// implement server struct that have fiber app and config +// Server represents the HTTP server and its dependencies type Server struct { app *fiber.App config *configs.Config logger logger.Logger } -func New(config *configs.Config, logger logger.Logger) *Server { - // debug log - app := fiber.New(fiber.Config{ +// New creates a new server instance with the given configuration and options +func New(config *configs.Config, log logger.Logger) (*Server, error) { + // Create base context for initialization + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Initialize server with base configuration + server := &Server{ + config: config, + logger: log, + } + + // Initialize Fiber app with base configuration + server.app = fiber.New(fiber.Config{ ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, - IdleTimeout: 60 * time.Second, + IdleTimeout: 20 * time.Second, BodyLimit: 512 * 1024, // 512KB }) - // Setup Limter Config and store - ipLimiterstore := mongodb.New(mongodb.Config{ - ConnectionURI: config.MongoDB.URI, - Database: config.MongoDB.DatabaseName, + + // Initialize core components + if err := server.initializeCore(ctx); err != nil { + return nil, fmt.Errorf("failed to initialize core components: %w", err) + } + + return server, nil +} + +// initializeCore sets up the core components of the server +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) + } + + // Setup database + dbClient, db, err := s.setupDatabase(ctx) + if err != nil { + return fmt.Errorf("failed to setup database: %w", err) + } + + // Setup repositories + repos, err := s.setupRepositories(db) + if err != nil { + return fmt.Errorf("failed to setup repositories: %w", err) + } + + // Setup services + service, err := s.setupServices(repos) + if err != nil { + return fmt.Errorf("failed to setup services: %w", err) + } + + // Setup routes + if err := s.setupRoutes(service, dbClient); err != nil { + return fmt.Errorf("failed to setup routes: %w", err) + } + + return nil +} + +func (s *Server) setupMiddleware() error { + s.logger.Debug("Setting up middleware", nil) + + // Setup rate limiter stores + ipLimiterStore := mongodb.New(mongodb.Config{ + ConnectionURI: s.config.MongoDB.URI, + Database: s.config.MongoDB.DatabaseName, Collection: "ip_limit", Reset: false, }) + + idLimiterStore := mongodb.New(mongodb.Config{ + ConnectionURI: s.config.MongoDB.URI, + Database: s.config.MongoDB.DatabaseName, + Collection: "id_limit", + Reset: false, + }) + + // Configure rate limiters ipLimiterConfig := limiter.Config{ - Max: config.IPLimiter.MaxTokenRequests, - Expiration: time.Duration(config.IPLimiter.TokenExpiration) * time.Minute, - SkipFailedRequests: true, - SkipSuccessfulRequests: false, - Storage: ipLimiterstore, - // skip the limiter for localhost - Next: func(c *fiber.Ctx) bool { - // skip the limiter if the keyGenerator returns "127.0.0.1" - return extractIPFromRequest(c) == "127.0.0.1" - }, + Max: s.config.IPLimiter.MaxTokenRequests, + Expiration: time.Duration(s.config.IPLimiter.TokenExpiration) * time.Minute, + Storage: ipLimiterStore, KeyGenerator: func(c *fiber.Ctx) string { return extractIPFromRequest(c) }, + Next: func(c *fiber.Ctx) bool { + return extractIPFromRequest(c) == "127.0.0.1" + }, + SkipFailedRequests: true, } - idLimiterStore := mongodb.New(mongodb.Config{ - ConnectionURI: config.MongoDB.URI, - Database: config.MongoDB.DatabaseName, - Collection: "id_limit", - Reset: false, - }) idLimiterConfig := limiter.Config{ - Max: config.IDLimiter.MaxTokenRequests, - Expiration: time.Duration(config.IDLimiter.TokenExpiration) * time.Minute, - SkipFailedRequests: true, - SkipSuccessfulRequests: false, - Storage: idLimiterStore, - // Use client id as key to limit the number of requests per client + Max: s.config.IDLimiter.MaxTokenRequests, + Expiration: time.Duration(s.config.IDLimiter.TokenExpiration) * time.Minute, + Storage: idLimiterStore, KeyGenerator: func(c *fiber.Ctx) string { return c.Get("X-Client-ID") }, + SkipFailedRequests: true, } - // Global middlewares - app.Use(middleware.NewLoggingMiddleware(logger)) - app.Use(middleware.CORS()) - recoverConfig := recover.ConfigDefault - recoverConfig.EnableStackTrace = true - app.Use(recover.New(recoverConfig)) - app.Use(helmet.New()) + // Apply middleware + s.app.Use(middleware.NewLoggingMiddleware(s.logger)) + s.app.Use(middleware.CORS()) + s.app.Use(recover.New(recover.Config{ + EnableStackTrace: true, + })) + s.app.Use(helmet.New()) + s.app.Use(limiter.New(ipLimiterConfig)) + s.app.Use(limiter.New(idLimiterConfig)) + + return nil +} - // Database connection - db, err := repository.ConnectToMongoDB(config.MongoDB.URI) +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) if err != nil { - logger.Fatal("Failed to connect to MongoDB", map[string]interface{}{"error": err}) + return nil, nil, errors.Join(fmt.Errorf("failed to connect to MongoDB: %w", err)) } - database := db.Database(config.MongoDB.DatabaseName) - // Initialize repositories - tokenRepo := repository.NewMongoTokenRepository(database, logger) - verificationRepo := repository.NewMongoVerificationRepository(database, logger) + return client, client.Database(s.config.MongoDB.DatabaseName), nil +} + +type repositories struct { + token repository.TokenRepository + verification repository.VerificationRepository +} + +func (s *Server) setupRepositories(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), + }, nil +} - // Initialize services - idenfyClient := idenfy.New(&config.Idenfy, logger) +func (s *Server) setupServices(repos *repositories) (services.KYCService, error) { + s.logger.Debug("Setting up services", nil) - substrateClient, err := substrate.New(&config.TFChain, logger) + idenfyClient := idenfy.New(&s.config.Idenfy, s.logger) + + substrateClient, err := substrate.New(&s.config.TFChain, s.logger) if err != nil { - logger.Fatal("Failed to initialize substrate client", map[string]interface{}{"error": err}) + return nil, fmt.Errorf("failed to initialize substrate client: %w", err) } - kycService := services.NewKYCService(verificationRepo, tokenRepo, idenfyClient, substrateClient, config, logger) - // Initialize handler - handler := handlers.NewHandler(kycService, config, logger) + return services.NewKYCService( + repos.verification, + repos.token, + idenfyClient, + substrateClient, + s.config, + s.logger, + ), nil +} + +func (s *Server) setupRoutes(kycService services.KYCService, mongoCl *mongo.Client) error { + s.logger.Debug("Setting up routes", nil) - // Routes - app.Get("/docs/*", swagger.HandlerDefault) + handler := handlers.NewHandler(kycService, s.config, s.logger) - v1 := app.Group("/api/v1") - v1.Post("/token", middleware.AuthMiddleware(config.Challenge), limiter.New(idLimiterConfig), limiter.New(ipLimiterConfig), handler.GetorCreateVerificationToken()) - v1.Get("/data", middleware.AuthMiddleware(config.Challenge), handler.GetVerificationData()) - // status route accepts either client_id or twin_id as query parameters + // API routes + v1 := s.app.Group("/api/v1") + 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(db)) + v1.Get("/health", handler.HealthCheck(mongoCl)) v1.Get("/configs", handler.GetServiceConfigs()) v1.Get("/version", handler.GetServiceVersion()) + // Webhook routes - webhooks := app.Group("/webhooks/idenfy") + webhooks := s.app.Group("/webhooks/idenfy") webhooks.Post("/verification-update", handler.ProcessVerificationResult()) webhooks.Post("/id-expiration", handler.ProcessDocExpirationNotification()) - return &Server{app: app, config: config, logger: logger} + // Documentation + s.app.Get("/docs/*", swagger.HandlerDefault) + + return nil } func extractIPFromRequest(c *fiber.Ctx) string { @@ -170,17 +260,16 @@ func (s *Server) Start() { signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) <-sigChan // Graceful shutdown - s.logger.Info("Shutting down server...", map[string]interface{}{}) + s.logger.Info("Shutting down server...", nil) 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:", map[string]interface{}{"error": err}) + s.logger.Error("Server forced to shutdown:", logger.Fields{"error": err}) } }() // Start server if err := s.app.Listen(":" + s.config.Server.Port); err != nil && err != http.ErrServerClosed { - s.logger.Fatal("Server startup failed", map[string]interface{}{"error": err}) + s.logger.Fatal("Server startup failed", logger.Fields{"error": err}) } } diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index c844e89..59ae850 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -49,7 +49,7 @@ func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo 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", map[string]interface{}{"clientID": clientID, "error": err}) + 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 } if isVerified { @@ -57,7 +57,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", map[string]interface{}{"clientID": clientID, "error": err_}) + 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 } // check if token is found and not expired @@ -73,7 +73,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", map[string]interface{}{"clientID": clientID, "error": err_}) + 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_) } if !hasRequiredBalance { @@ -84,14 +84,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", map[string]interface{}{"clientID": clientID, "uniqueClientID": uniqueClientID, "error": err_}) + 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_) } // 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", map[string]interface{}{"clientID": clientID, "error": err_}) + s.logger.Error("Error saving verification token to database", logger.Fields{"clientID": clientID, "error": err_}) } return &newToken, true, nil @@ -101,7 +101,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", map[string]interface{}{"clientID": clientID, "scanRef": scanRef, "error": err}) + 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 nil @@ -109,12 +109,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", map[string]interface{}{"address": address}) + 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", map[string]interface{}{"address": address, "error": err}) + 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 @@ -127,7 +127,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", map[string]interface{}{"clientID": clientID, "error": err}) + 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 verification, nil @@ -137,7 +137,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", map[string]interface{}{"clientID": clientID}) + s.logger.Info("ClientID is in always verified addresses. skipping verification", logger.Fields{"clientID": clientID}) return &models.VerificationOutcome{ Final: &final, ClientID: clientID, @@ -147,7 +147,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", map[string]interface{}{"clientID": clientID, "error": err}) + s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) return nil, errors.NewInternalError("error getting verification from database", err) } var outcome models.Outcome @@ -172,7 +172,7 @@ func (s *kycService) GetVerificationStatusByTwinID(ctx context.Context, twinID s // get the address from the twinID address, err := s.substrate.GetAddressByTwinID(twinID) if err != nil { - s.logger.Error("Error getting address from twinID", map[string]interface{}{"twinID": twinID, "error": err}) + 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 s.GetVerificationStatus(ctx, address) @@ -181,17 +181,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", map[string]interface{}{"sigHeader": sigHeader, "error": err}) + s.logger.Error("Error verifying callback signature", logger.Fields{"sigHeader": sigHeader, "error": err}) return errors.NewAuthorizationError("error verifying callback signature", err) } clientIDParts := strings.Split(result.ClientID, ":") if len(clientIDParts) < 2 { - s.logger.Error("clientID have no network suffix", map[string]interface{}{"clientID": result.ClientID}) + 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", map[string]interface{}{"clientID": result.ClientID, "expectedSuffix": s.IdenfySuffix, "actualSuffix": networkSuffix}) + 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 @@ -199,18 +199,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", map[string]interface{}{"clientID": result.ClientID, "scanRef": result.IdenfyRef, "error": err}) + 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", map[string]interface{}{"clientID": result.ClientID, "scanRef": result.IdenfyRef, "error": err}) + 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) } } - s.logger.Debug("Verification result processed successfully", map[string]interface{}{"result": result}) + s.logger.Debug("Verification result processed successfully", logger.Fields{"result": result}) return nil } @@ -221,7 +221,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", map[string]interface{}{"clientID": clientID, "error": err}) + s.logger.Error("Error getting verification from database", logger.Fields{"clientID": clientID, "error": err}) return false, errors.NewInternalError("error 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 9de26e5..9b11c73 100644 --- a/scripts/dev/balance/check-account-balance.go +++ b/scripts/dev/balance/check-account-balance.go @@ -6,6 +6,7 @@ import ( "log" "example.com/tfgrid-kyc-service/internal/clients/substrate" + "example.com/tfgrid-kyc-service/internal/logger" ) func main() { @@ -30,23 +31,23 @@ type LoggerW struct { *log.Logger } -func (l *LoggerW) Debug(msg string, fields map[string]interface{}) { +func (l *LoggerW) Debug(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Info(msg string, fields map[string]interface{}) { +func (l *LoggerW) Info(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Warn(msg string, fields map[string]interface{}) { +func (l *LoggerW) Warn(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Error(msg string, fields map[string]interface{}) { +func (l *LoggerW) Error(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Fatal(msg string, fields map[string]interface{}) { +func (l *LoggerW) Fatal(msg string, fields logger.Fields) { l.Println(msg) } diff --git a/scripts/dev/chain/chain_name.go b/scripts/dev/chain/chain_name.go index 45d780c..5a7e71c 100644 --- a/scripts/dev/chain/chain_name.go +++ b/scripts/dev/chain/chain_name.go @@ -5,6 +5,7 @@ import ( "log" "example.com/tfgrid-kyc-service/internal/clients/substrate" + "example.com/tfgrid-kyc-service/internal/logger" ) func main() { @@ -31,23 +32,23 @@ type LoggerW struct { *log.Logger } -func (l *LoggerW) Debug(msg string, fields map[string]interface{}) { +func (l *LoggerW) Debug(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Info(msg string, fields map[string]interface{}) { +func (l *LoggerW) Info(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Warn(msg string, fields map[string]interface{}) { +func (l *LoggerW) Warn(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Error(msg string, fields map[string]interface{}) { +func (l *LoggerW) Error(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Fatal(msg string, fields map[string]interface{}) { +func (l *LoggerW) Fatal(msg string, fields logger.Fields) { l.Println(msg) } diff --git a/scripts/dev/twin/get-address-by-twin-id.go b/scripts/dev/twin/get-address-by-twin-id.go index eb75e32..7136efc 100644 --- a/scripts/dev/twin/get-address-by-twin-id.go +++ b/scripts/dev/twin/get-address-by-twin-id.go @@ -5,6 +5,7 @@ import ( "log" "example.com/tfgrid-kyc-service/internal/clients/substrate" + "example.com/tfgrid-kyc-service/internal/logger" ) func main() { @@ -31,23 +32,23 @@ type LoggerW struct { *log.Logger } -func (l *LoggerW) Debug(msg string, fields map[string]interface{}) { +func (l *LoggerW) Debug(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Info(msg string, fields map[string]interface{}) { +func (l *LoggerW) Info(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Warn(msg string, fields map[string]interface{}) { +func (l *LoggerW) Warn(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Error(msg string, fields map[string]interface{}) { +func (l *LoggerW) Error(msg string, fields logger.Fields) { l.Println(msg) } -func (l *LoggerW) Fatal(msg string, fields map[string]interface{}) { +func (l *LoggerW) Fatal(msg string, fields logger.Fields) { l.Println(msg) } From 229b1f58fbcbefe7e281a82a7cac80ab2fd162b6 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Sat, 2 Nov 2024 04:29:27 +0200 Subject: [PATCH 074/105] Usd interfaces for the clients --- internal/clients/idenfy/interface.go | 11 + internal/clients/substrate/interface.go | 8 + .../{repository.go => interface.go} | 0 .../services/{services.go => interface.go} | 0 internal/services/kyc_service.go | 216 ++---------------- internal/services/tokens.go | 86 +++++++ internal/services/verification.go | 117 ++++++++++ 7 files changed, 238 insertions(+), 200 deletions(-) rename internal/repository/{repository.go => interface.go} (100%) rename internal/services/{services.go => interface.go} (100%) create mode 100644 internal/services/tokens.go create mode 100644 internal/services/verification.go diff --git a/internal/clients/idenfy/interface.go b/internal/clients/idenfy/interface.go index 9016240..2e7c8d9 100644 --- a/internal/clients/idenfy/interface.go +++ b/internal/clients/idenfy/interface.go @@ -1,5 +1,11 @@ package idenfy +import ( + "context" + + "example.com/tfgrid-kyc-service/internal/models" +) + type IdenfyConfig interface { GetBaseURL() string GetCallbackUrl() string @@ -10,3 +16,8 @@ type IdenfyConfig interface { GetAPISecret() string GetCallbackSignKey() string } + +type IdenfyClient interface { + CreateVerificationSession(ctx context.Context, clientID string) (models.Token, error) + VerifyCallbackSignature(ctx context.Context, body []byte, sigHeader string) error +} diff --git a/internal/clients/substrate/interface.go b/internal/clients/substrate/interface.go index 5ba9517..92da7d3 100644 --- a/internal/clients/substrate/interface.go +++ b/internal/clients/substrate/interface.go @@ -1,5 +1,13 @@ package substrate +import "math/big" + type SubstrateConfig interface { GetWsProviderURL() string } + +type SubstrateClient interface { + GetChainName() (string, error) + GetAddressByTwinID(twinID string) (string, error) + GetAccountBalance(address string) (*big.Int, error) +} diff --git a/internal/repository/repository.go b/internal/repository/interface.go similarity index 100% rename from internal/repository/repository.go rename to internal/repository/interface.go diff --git a/internal/services/services.go b/internal/services/interface.go similarity index 100% rename from internal/services/services.go rename to internal/services/interface.go diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index 59ae850..899a01d 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -1,19 +1,13 @@ package services import ( - "context" - "fmt" - "math/big" - "slices" "strings" - "time" "example.com/tfgrid-kyc-service/internal/clients/idenfy" "example.com/tfgrid-kyc-service/internal/clients/substrate" "example.com/tfgrid-kyc-service/internal/configs" "example.com/tfgrid-kyc-service/internal/errors" "example.com/tfgrid-kyc-service/internal/logger" - "example.com/tfgrid-kyc-service/internal/models" "example.com/tfgrid-kyc-service/internal/repository" ) @@ -22,210 +16,32 @@ const TFT_CONVERSION_FACTOR = 10000000 type kycService struct { verificationRepo repository.VerificationRepository tokenRepo repository.TokenRepository - idenfy *idenfy.Idenfy - substrate *substrate.Substrate + idenfy idenfy.IdenfyClient + substrate substrate.SubstrateClient config *configs.Verification logger logger.Logger IdenfySuffix string } -func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy *idenfy.Idenfy, substrateClient *substrate.Substrate, config *configs.Config, logger logger.Logger) KYCService { - chainName, err := substrateClient.GetChainName() - if err != nil { - panic(errors.NewInternalError("error getting chain name", err)) - } - chainNameParts := strings.Split(chainName, " ") - chainNetworkName := strings.ToLower(chainNameParts[len(chainNameParts)-1]) - if config.Idenfy.Namespace != "" { - chainNetworkName = config.Idenfy.Namespace + ":" + chainNetworkName - } - return &kycService{verificationRepo: verificationRepo, tokenRepo: tokenRepo, idenfy: idenfy, substrate: substrateClient, config: &config.Verification, logger: logger, IdenfySuffix: chainNetworkName} -} - -// --------------------------------------------------------------------------------------------------------------------- -// 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("error 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("error 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("error 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("error 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("error 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("error getting account balance", err) - } - return balance.Cmp(big.NewInt(int64(s.config.MinBalanceToVerifyAccount))) >= 0, nil -} - -// --------------------------------------------------------------------------------------------------------------------- -// verification 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("error 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("error 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 - 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 s.GetVerificationStatus(ctx, address) +func NewKYCService(verificationRepo repository.VerificationRepository, tokenRepo repository.TokenRepository, idenfy idenfy.IdenfyClient, substrateClient substrate.SubstrateClient, config *configs.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 (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("error 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("error saving verification to database", err) - } +func GetIdenfySuffix(substrateClient substrate.SubstrateClient, config *configs.Config) string { + idenfySuffix := GetChainNetworkName(substrateClient) + if config.Idenfy.Namespace != "" { + idenfySuffix = config.Idenfy.Namespace + ":" + idenfySuffix } - s.logger.Debug("Verification result processed successfully", logger.Fields{"result": result}) - return nil + return idenfySuffix } -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) +func GetChainNetworkName(substrateClient substrate.SubstrateClient) string { + chainName, err := substrateClient.GetChainName() 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) - } - if verification == nil { - return false, nil + panic(errors.NewInternalError("error getting chain name", err)) } - return verification.Status.Overall != nil && (*verification.Status.Overall == models.OverallApproved || (s.config.SuspiciousVerificationOutcome == "APPROVED" && *verification.Status.Overall == models.OverallSuspected)), nil + chainNameParts := strings.Split(chainName, " ") + chainNetworkName := strings.ToLower(chainNameParts[len(chainNameParts)-1]) + return chainNetworkName } diff --git a/internal/services/tokens.go b/internal/services/tokens.go new file mode 100644 index 0000000..6330740 --- /dev/null +++ b/internal/services/tokens.go @@ -0,0 +1,86 @@ +package services + +import ( + "context" + "fmt" + "math/big" + "time" + + "example.com/tfgrid-kyc-service/internal/errors" + "example.com/tfgrid-kyc-service/internal/logger" + "example.com/tfgrid-kyc-service/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("error 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("error 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("error 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("error 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("error 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("error getting account balance", err) + } + return balance.Cmp(big.NewInt(int64(s.config.MinBalanceToVerifyAccount))) >= 0, nil +} diff --git a/internal/services/verification.go b/internal/services/verification.go new file mode 100644 index 0000000..8b0e06c --- /dev/null +++ b/internal/services/verification.go @@ -0,0 +1,117 @@ +package services + +import ( + "context" + "slices" + "strings" + + "example.com/tfgrid-kyc-service/internal/errors" + "example.com/tfgrid-kyc-service/internal/logger" + "example.com/tfgrid-kyc-service/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("error 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("error 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 + 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 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("error 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("error 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("error 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 679596507faca4c3822c6bcdab3cb2be4a99ae19 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Sun, 3 Nov 2024 14:50:53 +0200 Subject: [PATCH 075/105] allow to disable IP and ID limiters for token endpoint --- Dockerfile | 2 +- cmd/api/main.go | 18 +++++------ go.mod | 4 +-- internal/clients/idenfy/idenfy.go | 4 +-- internal/clients/idenfy/idenfy_test.go | 6 ++-- internal/clients/idenfy/interface.go | 2 +- internal/clients/substrate/substrate.go | 2 +- internal/configs/config.go | 18 +++++++++-- internal/handlers/handlers.go | 14 ++++----- internal/logger/logger.go | 2 +- internal/middleware/middleware.go | 8 ++--- internal/repository/interface.go | 2 +- internal/repository/token_repository.go | 4 +-- .../repository/verification_repository.go | 4 +-- internal/responses/responses.go | 2 +- internal/server/server.go | 31 +++++++++++-------- internal/services/interface.go | 2 +- internal/services/kyc_service.go | 12 +++---- internal/services/tokens.go | 6 ++-- internal/services/verification.go | 6 ++-- scripts/dev/balance/check-account-balance.go | 4 +-- scripts/dev/chain/chain_name.go | 4 +-- scripts/dev/twin/get-address-by-twin-id.go | 4 +-- 23 files changed, 89 insertions(+), 72 deletions(-) diff --git a/Dockerfile b/Dockerfile index 20048dd..cfb0799 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN go mod download COPY . . RUN VERSION=`git describe --tags` && \ - CGO_ENABLED=0 GOOS=linux go build -o tfgrid-kyc -ldflags "-X example.com/tfgrid-kyc-service/internal/build.Version=$VERSION" cmd/api/main.go + 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 FROM alpine:3.19 diff --git a/cmd/api/main.go b/cmd/api/main.go index 88d76ee..af176a1 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -3,10 +3,10 @@ package main import ( "log" - _ "example.com/tfgrid-kyc-service/api/docs" - "example.com/tfgrid-kyc-service/internal/configs" - "example.com/tfgrid-kyc-service/internal/logger" - "example.com/tfgrid-kyc-service/internal/server" + _ "github.com/threefoldtech/tf-kyc-verifier/api/docs" + "github.com/threefoldtech/tf-kyc-verifier/internal/configs" + "github.com/threefoldtech/tf-kyc-verifier/internal/logger" + "github.com/threefoldtech/tf-kyc-verifier/internal/server" ) // @title TFGrid KYC API @@ -25,20 +25,20 @@ func main() { } logger.Init(config.Log) - log := logger.GetLogger() + srvLogger := logger.GetLogger() - log.Debug("Configuration loaded successfully", logger.Fields{ + srvLogger.Debug("Configuration loaded successfully", logger.Fields{ "config": config.GetPublicConfig(), }) - server, err := server.New(config, log) + server, err := server.New(config, srvLogger) if err != nil { - log.Fatal("Failed to create server:", logger.Fields{ + srvLogger.Error("Failed to create server:", logger.Fields{ "error": err, }) } - log.Info("Starting server on port:", logger.Fields{ + srvLogger.Info("Starting server on port:", logger.Fields{ "port": config.Server.Port, }) server.Start() diff --git a/go.mod b/go.mod index fa3d2e9..6fa8884 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module example.com/tfgrid-kyc-service +module github.com/threefoldtech/tf-kyc-verifier go 1.22 @@ -81,4 +81,4 @@ require ( olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) -replace example.com/tfgrid-kyc-service => ./ +replace github.com/threefoldtech/tf-kyc-verifier => ./ diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index a1179f3..a8ecd1d 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -11,8 +11,8 @@ import ( "fmt" "time" - "example.com/tfgrid-kyc-service/internal/logger" - "example.com/tfgrid-kyc-service/internal/models" + "github.com/threefoldtech/tf-kyc-verifier/internal/logger" + "github.com/threefoldtech/tf-kyc-verifier/internal/models" "github.com/valyala/fasthttp" ) diff --git a/internal/clients/idenfy/idenfy_test.go b/internal/clients/idenfy/idenfy_test.go index 569047f..e494a74 100644 --- a/internal/clients/idenfy/idenfy_test.go +++ b/internal/clients/idenfy/idenfy_test.go @@ -7,10 +7,10 @@ import ( "os" "testing" - "example.com/tfgrid-kyc-service/internal/configs" - "example.com/tfgrid-kyc-service/internal/logger" - "example.com/tfgrid-kyc-service/internal/models" "github.com/stretchr/testify/assert" + "github.com/threefoldtech/tf-kyc-verifier/internal/configs" + "github.com/threefoldtech/tf-kyc-verifier/internal/logger" + "github.com/threefoldtech/tf-kyc-verifier/internal/models" ) func TestClient_DecodeReaderIdentityCallback(t *testing.T) { diff --git a/internal/clients/idenfy/interface.go b/internal/clients/idenfy/interface.go index 2e7c8d9..36ef539 100644 --- a/internal/clients/idenfy/interface.go +++ b/internal/clients/idenfy/interface.go @@ -3,7 +3,7 @@ package idenfy import ( "context" - "example.com/tfgrid-kyc-service/internal/models" + "github.com/threefoldtech/tf-kyc-verifier/internal/models" ) type IdenfyConfig interface { diff --git a/internal/clients/substrate/substrate.go b/internal/clients/substrate/substrate.go index ad0ff2b..9c7e9ca 100644 --- a/internal/clients/substrate/substrate.go +++ b/internal/clients/substrate/substrate.go @@ -5,7 +5,7 @@ import ( "math/big" "strconv" - "example.com/tfgrid-kyc-service/internal/logger" + "github.com/threefoldtech/tf-kyc-verifier/internal/logger" // use tfchain go client diff --git a/internal/configs/config.go b/internal/configs/config.go index 78b9efd..3210e2a 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -2,6 +2,7 @@ package configs import ( "errors" + "log" "net/url" "slices" @@ -107,7 +108,6 @@ func LoadConfig() (*Config, error) { func (c Config) GetPublicConfig() Config { // deducting the secret fields - // copy the config to avoid modifying the original config := c config.Idenfy.APIKey = "[REDACTED]" config.Idenfy.APISecret = "[REDACTED]" @@ -118,9 +118,9 @@ func (c Config) GetPublicConfig() Config { // validate config func (c *Config) Validate() error { - // iDenfy base URL should be https://ivs.idenfy.com + // 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") + 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) @@ -151,5 +151,17 @@ func (c *Config) Validate() error { if !slices.Contains([]string{"APPROVED", "REJECTED"}, c.Verification.ExpiredDocumentOutcome) { return errors.New("invalid ExpiredDocumentOutcome") } + // 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.") + } + // 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.") + } + // 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.") + } return nil } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index a777c3f..a003699 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -10,13 +10,13 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/readpref" - "example.com/tfgrid-kyc-service/internal/build" - "example.com/tfgrid-kyc-service/internal/configs" - "example.com/tfgrid-kyc-service/internal/errors" - "example.com/tfgrid-kyc-service/internal/logger" - "example.com/tfgrid-kyc-service/internal/models" - "example.com/tfgrid-kyc-service/internal/responses" - "example.com/tfgrid-kyc-service/internal/services" + "github.com/threefoldtech/tf-kyc-verifier/internal/build" + "github.com/threefoldtech/tf-kyc-verifier/internal/configs" + "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" ) type Handler struct { diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 99267b0..33e056e 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -3,7 +3,7 @@ package logger import ( "context" - "example.com/tfgrid-kyc-service/internal/configs" + "github.com/threefoldtech/tf-kyc-verifier/internal/configs" ) type LoggerW struct { diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 0869e1f..c51a13e 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -5,12 +5,12 @@ import ( "strings" "time" - "example.com/tfgrid-kyc-service/internal/configs" - "example.com/tfgrid-kyc-service/internal/errors" - "example.com/tfgrid-kyc-service/internal/handlers" - "example.com/tfgrid-kyc-service/internal/logger" "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/errors" + "github.com/threefoldtech/tf-kyc-verifier/internal/handlers" + "github.com/threefoldtech/tf-kyc-verifier/internal/logger" "github.com/vedhavyas/go-subkey/v2" "github.com/vedhavyas/go-subkey/v2/ed25519" "github.com/vedhavyas/go-subkey/v2/sr25519" diff --git a/internal/repository/interface.go b/internal/repository/interface.go index 8138a8e..1a4ee36 100644 --- a/internal/repository/interface.go +++ b/internal/repository/interface.go @@ -3,7 +3,7 @@ package repository import ( "context" - "example.com/tfgrid-kyc-service/internal/models" + "github.com/threefoldtech/tf-kyc-verifier/internal/models" ) type TokenRepository interface { diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index 24af9dd..f3999f9 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -8,8 +8,8 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "example.com/tfgrid-kyc-service/internal/logger" - "example.com/tfgrid-kyc-service/internal/models" + "github.com/threefoldtech/tf-kyc-verifier/internal/logger" + "github.com/threefoldtech/tf-kyc-verifier/internal/models" ) type MongoTokenRepository struct { diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go index 94728a5..e563818 100644 --- a/internal/repository/verification_repository.go +++ b/internal/repository/verification_repository.go @@ -4,8 +4,8 @@ import ( "context" "time" - "example.com/tfgrid-kyc-service/internal/logger" - "example.com/tfgrid-kyc-service/internal/models" + "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" "go.mongodb.org/mongo-driver/mongo/options" diff --git a/internal/responses/responses.go b/internal/responses/responses.go index 0a9870b..e6d6a56 100644 --- a/internal/responses/responses.go +++ b/internal/responses/responses.go @@ -1,7 +1,7 @@ package responses import ( - "example.com/tfgrid-kyc-service/internal/models" + "github.com/threefoldtech/tf-kyc-verifier/internal/models" ) type ErrorResponse struct { diff --git a/internal/server/server.go b/internal/server/server.go index 114d2e7..54edbe5 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -12,21 +12,21 @@ import ( "syscall" "time" - _ "example.com/tfgrid-kyc-service/api/docs" - "example.com/tfgrid-kyc-service/internal/clients/idenfy" - "example.com/tfgrid-kyc-service/internal/clients/substrate" - "example.com/tfgrid-kyc-service/internal/configs" - "example.com/tfgrid-kyc-service/internal/handlers" - "example.com/tfgrid-kyc-service/internal/logger" - "example.com/tfgrid-kyc-service/internal/middleware" - "example.com/tfgrid-kyc-service/internal/repository" - "example.com/tfgrid-kyc-service/internal/services" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/helmet" "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/storage/mongodb" "github.com/gofiber/swagger" + _ "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/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" "go.mongodb.org/mongo-driver/mongo" ) @@ -38,7 +38,7 @@ type Server struct { } // New creates a new server instance with the given configuration and options -func New(config *configs.Config, log logger.Logger) (*Server, error) { +func New(config *configs.Config, srvLogger logger.Logger) (*Server, error) { // Create base context for initialization ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -46,7 +46,7 @@ func New(config *configs.Config, log logger.Logger) (*Server, error) { // Initialize server with base configuration server := &Server{ config: config, - logger: log, + logger: srvLogger, } // Initialize Fiber app with base configuration @@ -147,8 +147,13 @@ func (s *Server) setupMiddleware() error { EnableStackTrace: true, })) s.app.Use(helmet.New()) - s.app.Use(limiter.New(ipLimiterConfig)) - s.app.Use(limiter.New(idLimiterConfig)) + + if s.config.IPLimiter.MaxTokenRequests > 0 { + s.app.Use("/api/v1/token", limiter.New(ipLimiterConfig)) + } + if s.config.IDLimiter.MaxTokenRequests > 0 { + s.app.Use("/api/v1/token", limiter.New(idLimiterConfig)) + } return nil } diff --git a/internal/services/interface.go b/internal/services/interface.go index 24ea209..ad0b264 100644 --- a/internal/services/interface.go +++ b/internal/services/interface.go @@ -3,7 +3,7 @@ package services import ( "context" - "example.com/tfgrid-kyc-service/internal/models" + "github.com/threefoldtech/tf-kyc-verifier/internal/models" ) type KYCService interface { diff --git a/internal/services/kyc_service.go b/internal/services/kyc_service.go index 899a01d..7974c96 100644 --- a/internal/services/kyc_service.go +++ b/internal/services/kyc_service.go @@ -3,12 +3,12 @@ package services import ( "strings" - "example.com/tfgrid-kyc-service/internal/clients/idenfy" - "example.com/tfgrid-kyc-service/internal/clients/substrate" - "example.com/tfgrid-kyc-service/internal/configs" - "example.com/tfgrid-kyc-service/internal/errors" - "example.com/tfgrid-kyc-service/internal/logger" - "example.com/tfgrid-kyc-service/internal/repository" + "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/errors" + "github.com/threefoldtech/tf-kyc-verifier/internal/logger" + "github.com/threefoldtech/tf-kyc-verifier/internal/repository" ) const TFT_CONVERSION_FACTOR = 10000000 diff --git a/internal/services/tokens.go b/internal/services/tokens.go index 6330740..f9dc13c 100644 --- a/internal/services/tokens.go +++ b/internal/services/tokens.go @@ -6,9 +6,9 @@ import ( "math/big" "time" - "example.com/tfgrid-kyc-service/internal/errors" - "example.com/tfgrid-kyc-service/internal/logger" - "example.com/tfgrid-kyc-service/internal/models" + "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) { diff --git a/internal/services/verification.go b/internal/services/verification.go index 8b0e06c..920ec85 100644 --- a/internal/services/verification.go +++ b/internal/services/verification.go @@ -5,9 +5,9 @@ import ( "slices" "strings" - "example.com/tfgrid-kyc-service/internal/errors" - "example.com/tfgrid-kyc-service/internal/logger" - "example.com/tfgrid-kyc-service/internal/models" + "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) { diff --git a/scripts/dev/balance/check-account-balance.go b/scripts/dev/balance/check-account-balance.go index 9b11c73..ff50348 100644 --- a/scripts/dev/balance/check-account-balance.go +++ b/scripts/dev/balance/check-account-balance.go @@ -5,8 +5,8 @@ import ( "fmt" "log" - "example.com/tfgrid-kyc-service/internal/clients/substrate" - "example.com/tfgrid-kyc-service/internal/logger" + "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" + "github.com/threefoldtech/tf-kyc-verifier/internal/logger" ) func main() { diff --git a/scripts/dev/chain/chain_name.go b/scripts/dev/chain/chain_name.go index 5a7e71c..90b6d95 100644 --- a/scripts/dev/chain/chain_name.go +++ b/scripts/dev/chain/chain_name.go @@ -4,8 +4,8 @@ import ( "fmt" "log" - "example.com/tfgrid-kyc-service/internal/clients/substrate" - "example.com/tfgrid-kyc-service/internal/logger" + "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" + "github.com/threefoldtech/tf-kyc-verifier/internal/logger" ) func main() { diff --git a/scripts/dev/twin/get-address-by-twin-id.go b/scripts/dev/twin/get-address-by-twin-id.go index 7136efc..174bb68 100644 --- a/scripts/dev/twin/get-address-by-twin-id.go +++ b/scripts/dev/twin/get-address-by-twin-id.go @@ -4,8 +4,8 @@ import ( "fmt" "log" - "example.com/tfgrid-kyc-service/internal/clients/substrate" - "example.com/tfgrid-kyc-service/internal/logger" + "github.com/threefoldtech/tf-kyc-verifier/internal/clients/substrate" + "github.com/threefoldtech/tf-kyc-verifier/internal/logger" ) func main() { From 6f852aacc9ca2d7ac727fc460cb8e0786d43de64 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Sun, 3 Nov 2024 15:14:34 +0200 Subject: [PATCH 076/105] update swagger doc --- api/docs/docs.go | 2 +- api/docs/swagger.json | 2 +- api/docs/swagger.yaml | 2 +- cmd/api/main.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 4a27d07..b6becdc 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -548,7 +548,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "0.1.0", + Version: "0.3.0", Host: "", BasePath: "/", Schemes: []string{}, diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 9fb4b78..826d74d 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -9,7 +9,7 @@ "url": "https://codescalers-egypt.com", "email": "info@codescalers.com" }, - "version": "0.1.0" + "version": "0.3.0" }, "basePath": "/", "paths": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 43e5113..69afa43 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -151,7 +151,7 @@ info: description: This is a KYC service for TFGrid. termsOfService: http://swagger.io/terms/ title: TFGrid KYC API - version: 0.1.0 + version: 0.3.0 paths: /api/v1/configs: get: diff --git a/cmd/api/main.go b/cmd/api/main.go index af176a1..78d269f 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -10,7 +10,7 @@ import ( ) // @title TFGrid KYC API -// @version 0.1.0 +// @version 0.3.0 // @description This is a KYC service for TFGrid. // @termsOfService http://swagger.io/terms/ From ebbd6981d1fec11c24760e4f7c18548bd2f0fa48 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Sun, 3 Nov 2024 15:49:50 +0200 Subject: [PATCH 077/105] update docs --- api/docs/docs.go | 2 +- api/docs/swagger.json | 2 +- api/docs/swagger.yaml | 2 +- cmd/api/main.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index b6becdc..cfdcdf4 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -548,7 +548,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "0.3.0", + Version: "0.2.0", Host: "", BasePath: "/", Schemes: []string{}, diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 826d74d..42ec22a 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -9,7 +9,7 @@ "url": "https://codescalers-egypt.com", "email": "info@codescalers.com" }, - "version": "0.3.0" + "version": "0.2.0" }, "basePath": "/", "paths": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 69afa43..1e97399 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -151,7 +151,7 @@ info: description: This is a KYC service for TFGrid. termsOfService: http://swagger.io/terms/ title: TFGrid KYC API - version: 0.3.0 + version: 0.2.0 paths: /api/v1/configs: get: diff --git a/cmd/api/main.go b/cmd/api/main.go index 78d269f..aef9266 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -10,7 +10,7 @@ import ( ) // @title TFGrid KYC API -// @version 0.3.0 +// @version 0.2.0 // @description This is a KYC service for TFGrid. // @termsOfService http://swagger.io/terms/ From 917eb7281f7db933a586daf751c28d4f83637223 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 4 Nov 2024 01:58:11 +0200 Subject: [PATCH 078/105] 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 079/105] 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 080/105] 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 081/105] 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 082/105] 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 083/105] 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 084/105] 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 085/105] 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 086/105] 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 087/105] 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 088/105] 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 089/105] 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 090/105] 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 091/105] 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 092/105] 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 093/105] 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 094/105] 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 095/105] 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 096/105] 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 097/105] 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 098/105] 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 099/105] 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 100/105] 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 101/105] 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 102/105] 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 103/105] 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 104/105] 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 105/105] 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) } }