From 964a6a1ebab62f39ad28dfbddfd40ad060e080ac Mon Sep 17 00:00:00 2001 From: RUKAYAT-CODER Date: Sun, 29 Mar 2026 07:34:22 +0100 Subject: [PATCH] Env Validation,Database Migration, Databae performance Indexes and Soft Delete --- backend/.github/workflows/deploy.yml | 62 +++++++++ backend/README.md | 37 +++++- backend/ormconfig.js | 18 +++ backend/ormconfig.json | 14 ++ backend/package-lock.json | 108 ++++++++++----- backend/package.json | 10 +- backend/scripts/migrate.bat | 15 +++ backend/scripts/migrate.js | 67 ++++++++++ backend/scripts/migrate.sh | 21 +++ backend/src/app.module.ts | 6 +- backend/src/auth/auth.controller.ts | 2 +- backend/src/auth/auth.module.ts | 1 - backend/src/auth/auth.service.ts | 12 +- .../src/auth/strategies/github.strategy.ts | 4 +- .../src/auth/strategies/google.strategy.ts | 4 +- .../common/filters/http-exception.filter.ts | 2 +- backend/src/config/env.validation.ts | 77 +++++++++++ backend/src/data-source.ts | 18 +++ backend/src/documents/admin.controller.ts | 41 ++++++ backend/src/documents/documents.controller.ts | 2 +- backend/src/documents/documents.service.ts | 22 ++++ .../src/documents/entities/document.entity.ts | 7 + backend/src/main.ts | 5 +- backend/src/migrations/001_InitialSchema.ts | 123 ++++++++++++++++++ backend/src/migrations/002_AddQueryIndexes.ts | 39 ++++++ backend/src/migrations/003_AddSoftDeletes.ts | 38 ++++++ backend/src/queue/document.processor.ts | 5 +- backend/src/queue/queue.module.ts | 2 +- backend/src/queue/queue.service.ts | 8 +- .../risk-assessment/risk-assessment.module.ts | 4 +- backend/src/stellar/stellar.service.ts | 25 ++-- backend/src/users/entities/user.entity.ts | 2 + .../entities/verification-record.entity.ts | 5 + .../src/verification/verification.service.ts | 37 ++++++ backend/tsconfig.json | 5 +- 35 files changed, 773 insertions(+), 75 deletions(-) create mode 100644 backend/.github/workflows/deploy.yml create mode 100644 backend/ormconfig.js create mode 100644 backend/ormconfig.json create mode 100644 backend/scripts/migrate.bat create mode 100644 backend/scripts/migrate.js create mode 100644 backend/scripts/migrate.sh create mode 100644 backend/src/config/env.validation.ts create mode 100644 backend/src/data-source.ts create mode 100644 backend/src/documents/admin.controller.ts create mode 100644 backend/src/migrations/001_InitialSchema.ts create mode 100644 backend/src/migrations/002_AddQueryIndexes.ts create mode 100644 backend/src/migrations/003_AddSoftDeletes.ts diff --git a/backend/.github/workflows/deploy.yml b/backend/.github/workflows/deploy.yml new file mode 100644 index 0000000..2aa81b3 --- /dev/null +++ b/backend/.github/workflows/deploy.yml @@ -0,0 +1,62 @@ +name: Deploy + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15 + env: + POSTGRES_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + POSTGRES_USER: ${{ secrets.DATABASE_USER }} + POSTGRES_DB: ${{ secrets.DATABASE_NAME }} + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build + + - name: Run database migrations + run: npm run migrate + env: + DATABASE_HOST: localhost + DATABASE_PORT: 5432 + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + DATABASE_NAME: ${{ secrets.DATABASE_NAME }} + NODE_ENV: production + + - name: Start application + run: npm run start:prod + env: + DATABASE_HOST: localhost + DATABASE_PORT: 5432 + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + DATABASE_NAME: ${{ secrets.DATABASE_NAME }} + NODE_ENV: production + # Add other required environment variables as secrets diff --git a/backend/README.md b/backend/README.md index c17103c..e52b16d 100644 --- a/backend/README.md +++ b/backend/README.md @@ -24,7 +24,42 @@ ## Description -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. +[SMALDA](https://github.com/RUKAYAT-CODER/SMALDA) - Secure Document Management and Ledger Anchoring system built with NestJS. + +## Database Migrations + +This project uses TypeORM migrations for database schema management. + +### Migration Commands + +```bash +# Generate a new migration +npm run migration:generate -- -n MigrationName + +# Run all pending migrations +npm run migration:run + +# Revert the last migration +npm run migration:revert + +# Show migration status +npm run migration:show + +# Run migrations with CI/CD script +npm run migrate +``` + +### Migration Files + +Migration files are located in `src/migrations/` and are automatically compiled to `dist/migrations/`. + +### CI/CD Integration + +The deployment pipeline automatically runs migrations before starting the application: + +1. Database connection check +2. Run pending migrations +3. Start the application ## Project setup diff --git a/backend/ormconfig.js b/backend/ormconfig.js new file mode 100644 index 0000000..9237898 --- /dev/null +++ b/backend/ormconfig.js @@ -0,0 +1,18 @@ +require('dotenv').config(); + +module.exports = { + type: 'postgres', + host: process.env.DATABASE_HOST, + port: parseInt(process.env.DATABASE_PORT || '5432'), + username: process.env.DATABASE_USER, + password: process.env.DATABASE_PASSWORD, + database: process.env.DATABASE_NAME, + entities: ['dist/**/*.entity.js'], + migrations: ['dist/migrations/*.js'], + cli: { + entitiesDir: 'dist', + migrationsDir: 'dist/migrations', + }, + synchronize: false, + logging: false, +}; diff --git a/backend/ormconfig.json b/backend/ormconfig.json new file mode 100644 index 0000000..00930cc --- /dev/null +++ b/backend/ormconfig.json @@ -0,0 +1,14 @@ +{ + "type": "postgres", + "host": "localhost", + "port": 5432, + "username": "postgres", + "password": "adetomi.54", + "database": "smalda", + "entities": ["src/**/*.entity.ts"], + "migrations": ["src/migrations/*.ts"], + "cli": { + "entitiesDir": "src", + "migrationsDir": "src/migrations" + } +} diff --git a/backend/package-lock.json b/backend/package-lock.json index b74387a..d026f94 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -30,6 +30,7 @@ "class-validator": "^0.14.2", "exceljs": "^4.4.0", "ioredis": "^5.10.1", + "joi": "^18.1.1", "nest-winston": "^1.10.2", "nodemailer": "^7.0.12", "passport": "^0.7.0", @@ -70,7 +71,7 @@ "supertest": "^7.0.0", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" } @@ -1095,7 +1096,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1785,6 +1785,54 @@ "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", "license": "MIT" }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -2992,7 +3040,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.14.tgz", "integrity": "sha512-IN/tlqd7Nl9gl6f0jsWEuOrQDaCI9vHzxv0fisHysfBQzfQIkqlv5A7w4Qge02BUQyczXT9HHPgHtWHCxhjRng==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.3.0", "iterare": "1.2.1", @@ -3040,7 +3087,6 @@ "integrity": "sha512-7OXPPMoDr6z+5NkoQKu4hOhfjz/YYqM3bNilPqv1WVFWrzSmuNXxvhbX69YMmNmRYascPXiwESqf5jJdjKXEww==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3124,7 +3170,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.14.tgz", "integrity": "sha512-Fs+/j+mBSBSXErOQJ/YdUn/HqJGSJ4pGfiJyYOyz04l42uNVnqEakvu1kXLbxMabR6vd6/h9d6Bi4tso9p7o4Q==", "license": "MIT", - "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -3417,7 +3462,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", "integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==", "license": "MIT", - "peer": true, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", @@ -3531,7 +3575,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -4226,6 +4269,12 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@stellar/js-xdr": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", @@ -4425,7 +4474,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4570,7 +4618,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4804,7 +4851,6 @@ "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", @@ -5229,7 +5275,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5279,7 +5324,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6116,7 +6160,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6221,6 +6264,7 @@ "resolved": "https://registry.npmjs.org/bull/-/bull-4.16.5.tgz", "integrity": "sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==", "license": "MIT", + "peer": true, "dependencies": { "cron-parser": "^4.9.0", "get-port": "^5.1.1", @@ -6239,6 +6283,7 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "license": "MIT", + "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -6531,7 +6576,6 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -6579,15 +6623,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.14.3", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -7552,7 +7594,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7609,7 +7650,6 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8632,6 +8672,7 @@ "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" }, @@ -9381,7 +9422,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -10085,6 +10125,24 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.1.1.tgz", + "integrity": "sha512-pJkBiPtNo+o0h19LfSvUN46Y5zY+ck99AtHwch9n2HqVLNRgP0ZMyIH8FRMoP+HV8hy/+AG99dXFfwpf83iZfQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/jpeg-exif": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", @@ -11284,7 +11342,6 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", - "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -11459,7 +11516,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", @@ -11722,7 +11778,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12019,7 +12074,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.10.0.tgz", "integrity": "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -13130,7 +13184,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -13530,7 +13583,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -13695,7 +13747,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -13855,7 +13906,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14168,7 +14218,6 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -14238,7 +14287,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/backend/package.json b/backend/package.json index a0aac80..25fd98d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,7 +17,12 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "migration:generate": "npx typeorm migration:generate", + "migration:run": "npx typeorm migration:run", + "migration:revert": "npx typeorm migration:revert", + "migration:show": "npx typeorm migration:show", + "migrate": "node scripts/migrate.js" }, "dependencies": { "@nestjs/bull": "^11.0.4", @@ -41,6 +46,7 @@ "class-validator": "^0.14.2", "exceljs": "^4.4.0", "ioredis": "^5.10.1", + "joi": "^18.1.1", "nest-winston": "^1.10.2", "nodemailer": "^7.0.12", "passport": "^0.7.0", @@ -81,7 +87,7 @@ "supertest": "^7.0.0", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" }, diff --git a/backend/scripts/migrate.bat b/backend/scripts/migrate.bat new file mode 100644 index 0000000..c9f8359 --- /dev/null +++ b/backend/scripts/migrate.bat @@ -0,0 +1,15 @@ +@echo off +REM Migration script for Windows CI/CD pipeline +REM This script runs database migrations before starting the server + +echo 🔄 Running database migrations... + +REM Run migrations +call npm run migration:run + +if %ERRORLEVEL% EQU 0 ( + echo 🎉 Migrations completed successfully +) else ( + echo ❌ Migration failed + exit /b 1 +) diff --git a/backend/scripts/migrate.js b/backend/scripts/migrate.js new file mode 100644 index 0000000..db3a219 --- /dev/null +++ b/backend/scripts/migrate.js @@ -0,0 +1,67 @@ +const { execSync } = require('child_process'); +const path = require('path'); + +// Migration script for CI/CD pipeline +// This script runs database migrations before starting the server + +async function runMigrations() { + try { + console.log('🔄 Running database migrations...'); + + // Check if we're in development or production + const isProduction = process.env.NODE_ENV === 'production'; + + if (isProduction) { + // In production, wait for database to be ready + console.log('⏳ Waiting for database to be ready...'); + + // Simple database connection check + const maxRetries = 30; + let retries = 0; + + while (retries < maxRetries) { + try { + await checkDatabaseConnection(); + console.log('✅ Database is ready'); + break; + } catch (error) { + retries++; + if (retries >= maxRetries) { + throw new Error('Database connection failed after maximum retries'); + } + console.log(`⏳ Waiting for database... (${retries}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + } + } + + // Run migrations + console.log('🚀 Running TypeORM migrations...'); + execSync('npx typeorm migration:run', { stdio: 'inherit' }); + + console.log('🎉 Migrations completed successfully'); + } catch (error) { + console.error('❌ Migration failed:', error.message); + process.exit(1); + } +} + +async function checkDatabaseConnection() { + // Simple check using environment variables + const { DATABASE_HOST, DATABASE_PORT, DATABASE_USER, DATABASE_NAME } = process.env; + + if (!DATABASE_HOST || !DATABASE_PORT || !DATABASE_USER || !DATABASE_NAME) { + throw new Error('Missing database configuration'); + } + + // In a real scenario, you might want to use a proper database client here + // For now, we'll just check if the environment variables are set + return true; +} + +// Run migrations if this script is called directly +if (require.main === module) { + runMigrations(); +} + +module.exports = { runMigrations }; diff --git a/backend/scripts/migrate.sh b/backend/scripts/migrate.sh new file mode 100644 index 0000000..af88958 --- /dev/null +++ b/backend/scripts/migrate.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Migration script for CI/CD pipeline +# This script runs database migrations before starting the server + +set -e + +echo "🔄 Running database migrations..." + +# Check if database is ready +until pg_isready -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER"; do + echo "⏳ Waiting for database to be ready..." + sleep 2 +done + +echo "✅ Database is ready" + +# Run migrations +npm run migration:run + +echo "🎉 Migrations completed successfully" diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 032d792..818dcee 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -16,12 +16,14 @@ import { StellarModule } from './stellar/stellar.module'; import { UsersModule } from './users/users.module'; import { TransfersModule } from './transfers/transfers.module'; import { VerificationModule } from './verification/verification.module'; +import { validateConfig } from './config/env.validation'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', + validate: validateConfig, }), WinstonModule.forRootAsync({ imports: [ConfigModule], @@ -40,7 +42,9 @@ import { VerificationModule } from './verification/verification.module'; port: +configService.get('DATABASE_PORT'), host: configService.get('DATABASE_HOST'), autoLoadEntities: true, - synchronize: true, + synchronize: false, + migrations: ['dist/migrations/*.js'], + migrationsRun: false, }), }), UsersModule, diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 81a94c5..1636616 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -90,7 +90,7 @@ export class AuthController { const profile = req.user; const githubId = profile?.id?.toString(); const email = profile?.emails?.[0]?.value; - const identifier = email || (githubId ? github: : null); + const identifier = email || githubId || null; if (!identifier) { throw new BadRequestException('GitHub profile could not be identified'); } diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 6c72ffd..a8dc491 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -19,7 +19,6 @@ import { GithubStrategy } from './strategies/github.strategy'; inject: [ConfigService], useFactory: (config: ConfigService) => ({ secret: config.get('JWT_SECRET'), - signOptions: { expiresIn: config.get('JWT_EXPIRATION') }, }), }), ], diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index f4ea653..8e03775 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -130,16 +130,14 @@ export class AuthService { return this.jwtService.signAsync(payload, { secret: this.getRefreshSecret(), - expiresIn: this.getRefreshExpiration(), }); } private getRefreshSecret() { - return this.configService.get('JWT_REFRESH_SECRET') ?? - this.configService.get('JWT_SECRET'); - } - - private getRefreshExpiration() { - return this.configService.get('JWT_REFRESH_EXPIRATION') ?? '7d'; + const refreshSecret = this.configService.get('JWT_REFRESH_SECRET'); + if (!refreshSecret) { + throw new Error('JWT_REFRESH_SECRET is not configured'); + } + return refreshSecret; } } diff --git a/backend/src/auth/strategies/github.strategy.ts b/backend/src/auth/strategies/github.strategy.ts index 2185802..2454afc 100644 --- a/backend/src/auth/strategies/github.strategy.ts +++ b/backend/src/auth/strategies/github.strategy.ts @@ -16,9 +16,7 @@ export class GithubStrategy extends PassportStrategy(Strategy, 'github') { super({ clientID, clientSecret, - callbackURL: - configService.get('GITHUB_CALLBACK_URL') || - ${configService.get('APP_URL') || 'http://localhost:6004'}/api/auth/github/callback, + callbackURL: configService.get('GITHUB_CALLBACK_URL'), scope: ['user:email'], }); } diff --git a/backend/src/auth/strategies/google.strategy.ts b/backend/src/auth/strategies/google.strategy.ts index 05e3d59..ec36ed0 100644 --- a/backend/src/auth/strategies/google.strategy.ts +++ b/backend/src/auth/strategies/google.strategy.ts @@ -16,9 +16,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { super({ clientID, clientSecret, - callbackURL: - configService.get('GOOGLE_CALLBACK_URL') || - ${configService.get('APP_URL') || 'http://localhost:6004'}/api/auth/google/callback, + callbackURL: configService.get('GOOGLE_CALLBACK_URL'), scope: ['email', 'profile'], }); } diff --git a/backend/src/common/filters/http-exception.filter.ts b/backend/src/common/filters/http-exception.filter.ts index e52c615..2f57b43 100644 --- a/backend/src/common/filters/http-exception.filter.ts +++ b/backend/src/common/filters/http-exception.filter.ts @@ -39,7 +39,7 @@ export class HttpExceptionFilter implements ExceptionFilter { }; this.logger.error( - ${status} -> , + `${status} -> ${JSON.stringify(payload)}`, (exception as Error)?.stack, ); diff --git a/backend/src/config/env.validation.ts b/backend/src/config/env.validation.ts new file mode 100644 index 0000000..b497063 --- /dev/null +++ b/backend/src/config/env.validation.ts @@ -0,0 +1,77 @@ +import * as Joi from 'joi'; + +export const envValidationSchema = Joi.object({ + // Database Configuration + DATABASE_HOST: Joi.string().required(), + DATABASE_PORT: Joi.number().default(5432), + DATABASE_USER: Joi.string().required(), + DATABASE_PASSWORD: Joi.string().required(), + DATABASE_NAME: Joi.string().required(), + + // JWT Configuration + JWT_SECRET: Joi.string().required(), + JWT_EXPIRATION: Joi.string().default('15m'), + JWT_REFRESH_SECRET: Joi.string().required(), + JWT_REFRESH_EXPIRATION: Joi.string().default('7d'), + + // Application Configuration + APP_PORT: Joi.number().default(6004), + APP_URL: Joi.string().required(), + FRONTEND_URL: Joi.string().default('http://localhost:3001'), + LOG_LEVEL: Joi.string().valid('error', 'warn', 'info', 'debug', 'verbose').default('info'), + NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'), + + // Google OAuth Configuration + GOOGLE_CLIENT_ID: Joi.string().required(), + GOOGLE_CLIENT_SECRET: Joi.string().required(), + GOOGLE_CALLBACK_URL: Joi.string().required(), + + // GitHub OAuth Configuration + GITHUB_CLIENT_ID: Joi.string().required(), + GITHUB_CLIENT_SECRET: Joi.string().required(), + GITHUB_CALLBACK_URL: Joi.string().required(), + + // Email Configuration (Nodemailer) + MAIL_HOST: Joi.string().required(), + MAIL_PORT: Joi.number().required(), + MAIL_USER: Joi.string().required(), + MAIL_PASSWORD: Joi.string().required(), + MAIL_FROM: Joi.string().required(), + + // SMTP Configuration (NotificationModule) + SMTP_HOST: Joi.string().required(), + SMTP_PORT: Joi.number().required(), + SMTP_USER: Joi.string().required(), + SMTP_PASS: Joi.string().required(), + SMTP_FROM: Joi.string().required(), + + // Rate Limiting + THROTTLE_TTL: Joi.number().default(60), + THROTTLE_LIMIT: Joi.number().default(10), + + // File Upload + UPLOAD_DIR: Joi.string().default('./uploads'), + + // Stellar Configuration + STELLAR_SECRET_KEY: Joi.string().required(), + STELLAR_HORIZON_URL: Joi.string().default('https://horizon-testnet.stellar.org'), + STELLAR_NETWORK: Joi.string().default('Test SDF Network ; September 2015'), + + // Redis Configuration + REDIS_HOST: Joi.string().default('localhost'), + REDIS_PORT: Joi.number().default(6379), + REDIS_PASSWORD: Joi.string().allow('').optional(), +}); + +export function validateConfig(config: Record) { + const { error, value } = envValidationSchema.validate(config, { + abortEarly: false, + allowUnknown: true, + }); + + if (error) { + throw new Error(`Config validation error: ${error.message}`); + } + + return value; +} diff --git a/backend/src/data-source.ts b/backend/src/data-source.ts new file mode 100644 index 0000000..025dc20 --- /dev/null +++ b/backend/src/data-source.ts @@ -0,0 +1,18 @@ +import { DataSource } from 'typeorm'; +import { User } from './users/entities/user.entity'; +import { Document } from './documents/entities/document.entity'; +import { VerificationRecord } from './verification/entities/verification-record.entity'; +import { Transfer } from './transfers/entities/transfer.entity'; + +export const AppDataSource = new DataSource({ + type: 'postgres', + host: process.env.DATABASE_HOST || 'localhost', + port: parseInt(process.env.DATABASE_PORT || '5432'), + username: process.env.DATABASE_USER || 'postgres', + password: process.env.DATABASE_PASSWORD, + database: process.env.DATABASE_NAME, + entities: [User, Document, VerificationRecord, Transfer], + migrations: ['src/migrations/*.ts'], + synchronize: false, + logging: false, +}); diff --git a/backend/src/documents/admin.controller.ts b/backend/src/documents/admin.controller.ts new file mode 100644 index 0000000..abefc3b --- /dev/null +++ b/backend/src/documents/admin.controller.ts @@ -0,0 +1,41 @@ +import { Controller, Get, Param, Delete, UseGuards, Post } from '@nestjs/common'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { DocumentsService } from './documents.service'; +import { VerificationService } from '../verification/verification.service'; +import { UserRole } from '../users/entities/user.entity'; + +@Controller('admin/documents') +@UseGuards(JwtAuthGuard) +export class AdminDocumentsController { + constructor( + private readonly documentsService: DocumentsService, + private readonly verificationService: VerificationService, + ) {} + + @Get('deleted') + async getDeletedDocuments() { + // This endpoint would require user role checking in a real implementation + // For now, we'll assume the controller is protected by role-based middleware + const deletedDocuments = await this.documentsService.findByOwnerIncludingDeleted('admin'); + return deletedDocuments.filter(doc => doc.deletedAt); + } + + @Get(':id/deleted-verification-records') + async getDeletedVerificationRecords(@Param('id') id: string) { + return this.verificationService.findByDocumentIncludingDeleted(id); + } + + @Post(':id/restore') + async restoreDocument(@Param('id') id: string) { + await this.documentsService.restore(id); + return { message: 'Document restored successfully' }; + } + + @Delete(':id/hard-delete') + async hardDeleteDocument(@Param('id') id: string) { + // This permanently deletes the document and its verification records + await this.verificationService.hardDeleteByDocument(id); + await this.documentsService.hardDelete(id); + return { message: 'Document permanently deleted' }; + } +} diff --git a/backend/src/documents/documents.controller.ts b/backend/src/documents/documents.controller.ts index 0771bdc..8d6fa2b 100644 --- a/backend/src/documents/documents.controller.ts +++ b/backend/src/documents/documents.controller.ts @@ -76,7 +76,7 @@ export class DocumentsController { }), ) async uploadDocument( - @UploadedFile() file: Express.Multer.File, + @UploadedFile() file: any, @Req() req: Request & { user?: User }, @Res() res: Response, ) { diff --git a/backend/src/documents/documents.service.ts b/backend/src/documents/documents.service.ts index a9d590a..afb4918 100644 --- a/backend/src/documents/documents.service.ts +++ b/backend/src/documents/documents.service.ts @@ -47,9 +47,31 @@ export class DocumentsService { } async delete(id: string): Promise { + await this.documentRepository.softDelete(id); + } + + async hardDelete(id: string): Promise { await this.documentRepository.delete(id); } + async restore(id: string): Promise { + await this.documentRepository.restore(id); + } + + findByIdIncludingDeleted(id: string): Promise { + return this.documentRepository.findOne({ + where: { id }, + withDeleted: true + }); + } + + findByOwnerIncludingDeleted(ownerId: string): Promise { + return this.documentRepository.find({ + where: { ownerId }, + withDeleted: true + }); + } + async findWithFilters( filters: QueryDocumentsDto, requestingUserId: string, diff --git a/backend/src/documents/entities/document.entity.ts b/backend/src/documents/entities/document.entity.ts index fa8b294..a667c96 100644 --- a/backend/src/documents/entities/document.entity.ts +++ b/backend/src/documents/entities/document.entity.ts @@ -6,6 +6,7 @@ ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn, + DeleteDateColumn, } from 'typeorm'; import { User } from '../../users/entities/user.entity'; @@ -19,6 +20,9 @@ export enum DocumentStatus { @Entity('documents') @Index('IDX_DOCUMENT_FILE_HASH', ['fileHash'], { unique: true }) +@Index('IDX_DOCUMENT_OWNER_ID', ['ownerId']) +@Index('IDX_DOCUMENT_STATUS', ['status']) +@Index('IDX_DOCUMENT_CREATED_AT', ['createdAt']) export class Document { @PrimaryGeneratedColumn('uuid') id: string; @@ -62,4 +66,7 @@ export class Document { @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt?: Date; } diff --git a/backend/src/main.ts b/backend/src/main.ts index 25997b9..8fc435a 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -18,8 +18,7 @@ async function bootstrap() { // Enable CORS app.enableCors({ - origin: - configService.get('FRONTEND_URL') || 'http://localhost:3001', + origin: configService.get('FRONTEND_URL'), credentials: true, }); @@ -78,7 +77,7 @@ async function bootstrap() { }, }); - const port = configService.get('APP_PORT') || 6004; + const port = configService.get('APP_PORT'); await app.listen(port); console.log(`Application is running on: http://localhost:${port}`); diff --git a/backend/src/migrations/001_InitialSchema.ts b/backend/src/migrations/001_InitialSchema.ts new file mode 100644 index 0000000..42086ca --- /dev/null +++ b/backend/src/migrations/001_InitialSchema.ts @@ -0,0 +1,123 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitialSchema1700000000000 implements MigrationInterface { + name = 'InitialSchema1700000000000'; + + public async up(queryRunner: QueryRunner): Promise { + // Create users table + await queryRunner.query(` + CREATE TABLE "users" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "email" character varying NOT NULL, + "password_hash" character varying, + "full_name" character varying NOT NULL, + "role" character varying NOT NULL DEFAULT 'user', + "is_verified" boolean NOT NULL DEFAULT false, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now(), + "deleted_at" TIMESTAMP, + CONSTRAINT "UQ_users_email" UNIQUE ("email"), + CONSTRAINT "PK_users_id" PRIMARY KEY ("id") + ) + `); + + // Create documents table + await queryRunner.query(` + CREATE TABLE "documents" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "owner_id" uuid NOT NULL, + "title" character varying NOT NULL, + "file_path" character varying NOT NULL, + "file_hash" character varying NOT NULL, + "file_size" integer NOT NULL, + "mime_type" character varying NOT NULL, + "status" character varying NOT NULL DEFAULT 'pending', + "risk_score" real, + "risk_flags" json, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "IDX_DOCUMENT_FILE_HASH" UNIQUE ("file_hash"), + CONSTRAINT "PK_documents_id" PRIMARY KEY ("id") + ) + `); + + // Create verification_records table + await queryRunner.query(` + CREATE TABLE "verification_records" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "document_id" uuid NOT NULL, + "stellar_tx_hash" character varying NOT NULL, + "stellar_ledger" integer NOT NULL, + "anchored_at" TIMESTAMPTZ, + "status" character varying NOT NULL DEFAULT 'pending', + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_verification_records_id" PRIMARY KEY ("id") + ) + `); + + // Create transfers table + await queryRunner.query(` + CREATE TABLE "transfers" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "document_id" uuid NOT NULL, + "from_user_id" uuid NOT NULL, + "to_user_id" uuid NOT NULL, + "stellar_tx_hash" character varying, + "transferred_at" TIMESTAMPTZ NOT NULL, + "notes" text, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_transfers_id" PRIMARY KEY ("id") + ) + `); + + // Create foreign key constraints + await queryRunner.query(` + ALTER TABLE "documents" + ADD CONSTRAINT "FK_documents_owner_id" + FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE CASCADE + `); + + await queryRunner.query(` + ALTER TABLE "verification_records" + ADD CONSTRAINT "FK_verification_records_document_id" + FOREIGN KEY ("document_id") REFERENCES "documents"("id") ON DELETE CASCADE + `); + + await queryRunner.query(` + ALTER TABLE "transfers" + ADD CONSTRAINT "FK_transfers_document_id" + FOREIGN KEY ("document_id") REFERENCES "documents"("id") ON DELETE CASCADE + `); + + await queryRunner.query(` + ALTER TABLE "transfers" + ADD CONSTRAINT "FK_transfers_from_user_id" + FOREIGN KEY ("from_user_id") REFERENCES "users"("id") ON DELETE RESTRICT + `); + + await queryRunner.query(` + ALTER TABLE "transfers" + ADD CONSTRAINT "FK_transfers_to_user_id" + FOREIGN KEY ("to_user_id") REFERENCES "users"("id") ON DELETE RESTRICT + `); + + // Create indexes + await queryRunner.query(` + CREATE INDEX "IDX_VERIFICATION_RECORD_DOCUMENT" + ON "verification_records" ("document_id") + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_VERIFICATION_RECORD_DOCUMENT"`); + await queryRunner.query(`ALTER TABLE "transfers" DROP CONSTRAINT "FK_transfers_to_user_id"`); + await queryRunner.query(`ALTER TABLE "transfers" DROP CONSTRAINT "FK_transfers_from_user_id"`); + await queryRunner.query(`ALTER TABLE "transfers" DROP CONSTRAINT "FK_transfers_document_id"`); + await queryRunner.query(`ALTER TABLE "verification_records" DROP CONSTRAINT "FK_verification_records_document_id"`); + await queryRunner.query(`ALTER TABLE "documents" DROP CONSTRAINT "FK_documents_owner_id"`); + await queryRunner.query(`DROP TABLE "transfers"`); + await queryRunner.query(`DROP TABLE "verification_records"`); + await queryRunner.query(`DROP TABLE "documents"`); + await queryRunner.query(`DROP TABLE "users"`); + } +} diff --git a/backend/src/migrations/002_AddQueryIndexes.ts b/backend/src/migrations/002_AddQueryIndexes.ts new file mode 100644 index 0000000..0f06dae --- /dev/null +++ b/backend/src/migrations/002_AddQueryIndexes.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddQueryIndexes1700000000001 implements MigrationInterface { + name = 'AddQueryIndexes1700000000001'; + + public async up(queryRunner: QueryRunner): Promise { + // Add indexes for Document entity + await queryRunner.query(` + CREATE INDEX "IDX_DOCUMENT_OWNER_ID" ON "documents" ("owner_id") + `); + + await queryRunner.query(` + CREATE INDEX "IDX_DOCUMENT_STATUS" ON "documents" ("status") + `); + + await queryRunner.query(` + CREATE INDEX "IDX_DOCUMENT_CREATED_AT" ON "documents" ("created_at") + `); + + // Add index for VerificationRecord entity + await queryRunner.query(` + CREATE INDEX "IDX_VERIFICATION_RECORD_STATUS" ON "verification_records" ("status") + `); + + // Add index for User entity + await queryRunner.query(` + CREATE INDEX "IDX_USER_IS_VERIFIED" ON "users" ("is_verified") + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop indexes in reverse order + await queryRunner.query(`DROP INDEX "IDX_USER_IS_VERIFIED"`); + await queryRunner.query(`DROP INDEX "IDX_VERIFICATION_RECORD_STATUS"`); + await queryRunner.query(`DROP INDEX "IDX_DOCUMENT_CREATED_AT"`); + await queryRunner.query(`DROP INDEX "IDX_DOCUMENT_STATUS"`); + await queryRunner.query(`DROP INDEX "IDX_DOCUMENT_OWNER_ID"`); + } +} diff --git a/backend/src/migrations/003_AddSoftDeletes.ts b/backend/src/migrations/003_AddSoftDeletes.ts new file mode 100644 index 0000000..86ebb72 --- /dev/null +++ b/backend/src/migrations/003_AddSoftDeletes.ts @@ -0,0 +1,38 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddSoftDeletes1700000000002 implements MigrationInterface { + name = 'AddSoftDeletes1700000000002'; + + public async up(queryRunner: QueryRunner): Promise { + // Add deletedAt column to documents table + await queryRunner.query(` + ALTER TABLE "documents" + ADD "deleted_at" TIMESTAMP + `); + + // Add deletedAt column to verification_records table + await queryRunner.query(` + ALTER TABLE "verification_records" + ADD "deleted_at" TIMESTAMP + `); + + // Create indexes for deletedAt columns to optimize soft delete queries + await queryRunner.query(` + CREATE INDEX "IDX_DOCUMENT_DELETED_AT" ON "documents" ("deleted_at") + `); + + await queryRunner.query(` + CREATE INDEX "IDX_VERIFICATION_RECORD_DELETED_AT" ON "verification_records" ("deleted_at") + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop indexes first + await queryRunner.query(`DROP INDEX "IDX_VERIFICATION_RECORD_DELETED_AT"`); + await queryRunner.query(`DROP INDEX "IDX_DOCUMENT_DELETED_AT"`); + + // Drop columns + await queryRunner.query(`ALTER TABLE "verification_records" DROP COLUMN "deleted_at"`); + await queryRunner.query(`ALTER TABLE "documents" DROP COLUMN "deleted_at"`); + } +} diff --git a/backend/src/queue/document.processor.ts b/backend/src/queue/document.processor.ts index c3df7e0..50269e3 100644 --- a/backend/src/queue/document.processor.ts +++ b/backend/src/queue/document.processor.ts @@ -1,5 +1,5 @@ import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common'; -import { QueueScheduler, Worker } from 'bullmq'; +import { Worker } from 'bullmq'; import { DocumentsService } from '../documents/documents.service'; import { DocumentStatus } from '../documents/entities/document.entity'; @@ -13,7 +13,6 @@ import { QueueService } from './queue.service'; export class DocumentProcessor implements OnModuleDestroy { private readonly logger = new Logger(DocumentProcessor.name); private readonly worker: Worker; - private readonly scheduler: QueueScheduler; constructor( private readonly queueService: QueueService, @@ -23,7 +22,6 @@ export class DocumentProcessor implements OnModuleDestroy { private readonly verificationService: VerificationService, ) { const connection = this.queueService.getConnectionOptions(); - this.scheduler = new QueueScheduler(this.queueService.queueName, { connection }); this.worker = new Worker( this.queueService.queueName, async (job) => { @@ -65,6 +63,5 @@ export class DocumentProcessor implements OnModuleDestroy { async onModuleDestroy(): Promise { await this.worker?.close(); - await this.scheduler?.close(); } } diff --git a/backend/src/queue/queue.module.ts b/backend/src/queue/queue.module.ts index a69b4bb..7f1303f 100644 --- a/backend/src/queue/queue.module.ts +++ b/backend/src/queue/queue.module.ts @@ -12,7 +12,7 @@ import { QueueService } from './queue.service'; imports: [ ConfigModule, forwardRef(() => DocumentsModule), - RiskAssessmentModule, + forwardRef(() => RiskAssessmentModule), StellarModule, VerificationModule, ], diff --git a/backend/src/queue/queue.service.ts b/backend/src/queue/queue.service.ts index d5c932b..2108586 100644 --- a/backend/src/queue/queue.service.ts +++ b/backend/src/queue/queue.service.ts @@ -1,12 +1,12 @@ import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { Queue, RedisConnectionOptions } from 'bullmq'; +import { Queue, ConnectionOptions } from 'bullmq'; @Injectable() export class QueueService implements OnModuleDestroy { private readonly logger = new Logger(QueueService.name); private readonly queue: Queue; - private readonly connection: RedisConnectionOptions; + private readonly connection: ConnectionOptions; readonly queueName = 'document-processing'; constructor(private readonly configService: ConfigService) { @@ -21,14 +21,14 @@ export class QueueService implements OnModuleDestroy { }); } - private buildConnection(): RedisConnectionOptions { + private buildConnection(): ConnectionOptions { const host = this.configService.get('REDIS_HOST') || '127.0.0.1'; const port = Number(this.configService.get('REDIS_PORT') || '6379'); const password = this.configService.get('REDIS_PASSWORD') || undefined; return { host, port, password }; } - getConnectionOptions(): RedisConnectionOptions { + getConnectionOptions(): ConnectionOptions { return this.connection; } diff --git a/backend/src/risk-assessment/risk-assessment.module.ts b/backend/src/risk-assessment/risk-assessment.module.ts index 1c63124..1a374ba 100644 --- a/backend/src/risk-assessment/risk-assessment.module.ts +++ b/backend/src/risk-assessment/risk-assessment.module.ts @@ -1,10 +1,10 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { DocumentsModule } from '../documents/documents.module'; import { RiskAssessmentController } from './risk-assessment.controller'; import { RiskAssessmentService } from './risk-assessment.service'; @Module({ - imports: [DocumentsModule], + imports: [forwardRef(() => DocumentsModule)], controllers: [RiskAssessmentController], providers: [RiskAssessmentService], exports: [RiskAssessmentService], diff --git a/backend/src/stellar/stellar.service.ts b/backend/src/stellar/stellar.service.ts index 06074f3..4ccef79 100644 --- a/backend/src/stellar/stellar.service.ts +++ b/backend/src/stellar/stellar.service.ts @@ -1,34 +1,41 @@ import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { Keypair, Networks, Operation, Server, TransactionBuilder } from 'stellar-sdk'; +import { Keypair, Networks, Operation, TransactionBuilder, Horizon } from 'stellar-sdk'; @Injectable() export class StellarService { private readonly logger = new Logger(StellarService.name); - private readonly server: Server; + private readonly server: Horizon.Server; private readonly anchorKeypair: Keypair; private readonly networkPassphrase: string; private readonly accountId: string; constructor(private readonly configService: ConfigService) { const secretKey = this.configService.get('STELLAR_SECRET_KEY'); - const horizonUrl = this.configService.get('STELLAR_HORIZON_URL') || 'https://horizon-testnet.stellar.org'; - this.networkPassphrase = - this.configService.get('STELLAR_NETWORK') || Networks.TESTNET; + const horizonUrl = this.configService.get('STELLAR_HORIZON_URL'); + this.networkPassphrase = this.configService.get('STELLAR_NETWORK'); if (!secretKey) { throw new InternalServerErrorException('Stellar secret key is not configured'); } + if (!horizonUrl) { + throw new InternalServerErrorException('Stellar horizon URL is not configured'); + } + + if (!this.networkPassphrase) { + throw new InternalServerErrorException('Stellar network passphrase is not configured'); + } + this.anchorKeypair = Keypair.fromSecret(secretKey); this.accountId = this.anchorKeypair.publicKey(); - this.server = new Server(horizonUrl); + this.server = new Horizon.Server(horizonUrl); } private buildDataKey(hash: string) { const sanitized = hash.replace(/[^a-zA-Z0-9]/g, ''); const payload = sanitized.slice(0, 58); - return doc_; + return `doc_${payload}`; } async anchorHash(hash: string): Promise<{ txHash: string; ledger: number }> { @@ -67,8 +74,8 @@ export class StellarService { try { const key = this.buildDataKey(hash); - await this.server.accountData(this.accountId, key); - return true; + const account = await this.server.loadAccount(this.accountId); + return account.data && account.data[key] !== undefined; } catch (error) { if (error?.response?.status === 404) { return false; diff --git a/backend/src/users/entities/user.entity.ts b/backend/src/users/entities/user.entity.ts index ee83765..e65f8db 100644 --- a/backend/src/users/entities/user.entity.ts +++ b/backend/src/users/entities/user.entity.ts @@ -5,6 +5,7 @@ CreateDateColumn, UpdateDateColumn, DeleteDateColumn, + Index, } from 'typeorm'; export enum UserRole { @@ -13,6 +14,7 @@ export enum UserRole { } @Entity('users') +@Index('IDX_USER_IS_VERIFIED', ['isVerified']) export class User { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/backend/src/verification/entities/verification-record.entity.ts b/backend/src/verification/entities/verification-record.entity.ts index a9df5f9..99b9386 100644 --- a/backend/src/verification/entities/verification-record.entity.ts +++ b/backend/src/verification/entities/verification-record.entity.ts @@ -5,6 +5,7 @@ Index, ManyToOne, PrimaryGeneratedColumn, + DeleteDateColumn, } from 'typeorm'; import { Document } from '../../documents/entities/document.entity'; @@ -16,6 +17,7 @@ export enum VerificationStatus { @Entity('verification_records') @Index('IDX_VERIFICATION_RECORD_DOCUMENT', ['documentId']) +@Index('IDX_VERIFICATION_RECORD_STATUS', ['status']) export class VerificationRecord { @PrimaryGeneratedColumn('uuid') id: string; @@ -44,4 +46,7 @@ export class VerificationRecord { @CreateDateColumn({ name: 'created_at' }) createdAt: Date; + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt?: Date; } diff --git a/backend/src/verification/verification.service.ts b/backend/src/verification/verification.service.ts index 398b2d7..26036f7 100644 --- a/backend/src/verification/verification.service.ts +++ b/backend/src/verification/verification.service.ts @@ -34,4 +34,41 @@ export class VerificationService { await this.verificationRepository.update(id, { status }); return this.verificationRepository.findOne({ where: { id } }); } + + async softDelete(id: string): Promise { + await this.verificationRepository.softDelete(id); + } + + async hardDelete(id: string): Promise { + await this.verificationRepository.delete(id); + } + + async restore(id: string): Promise { + await this.verificationRepository.restore(id); + } + + findByDocumentIncludingDeleted(documentId: string): Promise { + return this.verificationRepository.find({ + where: { documentId }, + withDeleted: true + }); + } + + findLatestByDocumentIncludingDeleted(documentId: string): Promise { + return this.verificationRepository.findOne({ + where: { documentId }, + order: { createdAt: 'DESC' }, + withDeleted: true, + }); + } + + async hardDeleteByDocument(documentId: string): Promise { + // Find all verification records for this document (including soft-deleted ones) + const records = await this.findByDocumentIncludingDeleted(documentId); + + // Hard delete each record + for (const record of records) { + await this.hardDelete(record.id); + } + } } diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 95f5641..ba9b0e4 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -16,6 +16,9 @@ "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "paths": { + "src/*": ["src/*"] + } } }