diff --git a/README.md b/README.md index 4980cee..27e70f8 100644 --- a/README.md +++ b/README.md @@ -188,20 +188,9 @@ PORT=3000 ```bash # Run migrations to create database tables -# dist/data-source.js is your file data-source in dist directory -npm run typeorm migration:run -d dist/data-source.js - -# or - -# dist/data-source.js is your file data-source in dist directory -# The --fake flag tells TypeORM to mark the migration as executed without actually running it. -npm run typeorm migration:run -d dist/data-source.js --fake +npm run typeorm migration:run ``` -> [!WARNING] -> Is important that you have dist file to use migrations - - ### 6. Start the Application ```bash @@ -491,6 +480,27 @@ Examples: 4. Create PR with detailed description 5. Wait for review and approval +### Type Safety & Avoiding `any` + +Strict TypeScript options are enabled (`strict`, `noImplicitAny`, `strictNullChecks`). Do not introduce unchecked `any`. + +Guidelines: + +- Prefer `unknown` for opaque values instead of `any`. +- Use generics in helpers (e.g. async handlers, validation middleware) to propagate types. +- DTOs must define explicit field types. For collections of key/value attributes create a small interface (see `AttributeValueDTO`). +- Dynamic JSON blobs: `Record`. +- If interoperating with untyped libraries, narrow as soon as possible and add runtime guards. +- Only in tests you may coerce with `as unknown as T`; keep the cast local. + +Audit command: + +```bash +grep -R "any" src | grep -v spec || true +``` + +If you intentionally keep an `any`, annotate with `// INTENTIONAL_ANY: reason`. + ## 🚀 Deployment ### Production diff --git a/docker-compose.yml b/docker-compose.yml index 6024e78..02f09f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,24 +4,13 @@ services: image: postgres:latest container_name: starshop-db environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: starshop ports: - '5432:5432' volumes: - postgres_data:/var/lib/postgresql/data - redis: - image: redis:7-alpine - container_name: starshop-redis - ports: - - '6379:6379' - volumes: - - redis_data:/data - command: redis-server --appendonly yes - restart: unless-stopped - volumes: postgres_data: - redis_data: diff --git a/package-lock.json b/package-lock.json index cadfaa4..ae0320a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", "@supabase/supabase-js": "^2.46.1", - "@trustless-work/escrow": "^2.0.2", "@types/bcryptjs": "^2.4.6", "axios": "^1.7.9", "bcryptjs": "^3.0.2", @@ -28,7 +27,6 @@ "class-validator": "^0.14.2", "cloudinary": "^1.41.3", "dotenv": "^16.4.7", - "env-var": "^7.5.0", "express": "^4.21.2", "express-async-handler": "^1.2.0", "express-rate-limit": "^7.5.0", @@ -1853,16 +1851,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@inquirer/ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.0.tgz", - "integrity": "sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@inquirer/checkbox": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz", @@ -1911,15 +1899,15 @@ } }, "node_modules/@inquirer/core": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.2.tgz", - "integrity": "sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA==", + "version": "10.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", + "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.0", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", @@ -1939,15 +1927,15 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.20", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.20.tgz", - "integrity": "sha512-7omh5y5bK672Q+Brk4HBbnHNowOZwrb/78IFXdrEB9PfdxL3GudQyDk8O9vQ188wj3xrEebS2M9n18BjJoI83g==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.13.tgz", + "integrity": "sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.2.2", - "@inquirer/external-editor": "^1.0.2", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "external-editor": "^3.1.0" }, "engines": { "node": ">=18" @@ -1984,49 +1972,10 @@ } } }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", - "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^2.1.0", - "iconv-lite": "^0.7.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/@inquirer/figures": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", - "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", + "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", "dev": true, "license": "MIT", "engines": { @@ -2203,9 +2152,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", - "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", "dev": true, "license": "MIT", "engines": { @@ -4766,19 +4715,6 @@ "node": ">= 6" } }, - "node_modules/@trustless-work/escrow": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@trustless-work/escrow/-/escrow-2.0.2.tgz", - "integrity": "sha512-NUepatzB8ovv05s4PeDzDgG+pTNN7ADr7nVmsnphGPFKMPgJlUF80xPFWftIbAKzZPXc4x2KymwcbFJZvqCKjA==", - "license": "MIT", - "dependencies": { - "axios": "^1.9.0" - }, - "peerDependencies": { - "react": ">=18.0.0 <20.0.0", - "react-dom": ">=18.0.0 <20.0.0" - } - }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -5956,29 +5892,14 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, @@ -6580,24 +6501,6 @@ "license": "ISC", "optional": true }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -6696,9 +6599,9 @@ } }, "node_modules/chardet": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true, "license": "MIT" }, @@ -7422,23 +7325,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7739,15 +7625,6 @@ "node": ">=6" } }, - "node_modules/env-var": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/env-var/-/env-var-7.5.0.tgz", - "integrity": "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -8336,6 +8213,21 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8643,21 +8535,6 @@ } } }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -9155,18 +9032,6 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -9514,18 +9379,6 @@ "node": ">=8" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -9644,21 +9497,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -15049,6 +14887,16 @@ "dev": true, "license": "ISC" }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -15488,15 +15336,6 @@ "node": ">=4" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -15852,29 +15691,6 @@ "node": ">=0.10.0" } }, - "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.1" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -16238,13 +16054,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT", - "peer": true - }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -16391,23 +16200,6 @@ "license": "ISC", "optional": true }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -16415,23 +16207,16 @@ "license": "ISC" }, "node_modules/sha.js": { - "version": "2.4.12", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", - "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.0" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" }, "bin": { "sha.js": "bin.js" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shebang-command": { @@ -17606,32 +17391,25 @@ "dev": true, "license": "MIT" }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", "license": "MIT", "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" + "os-tmpdir": "~1.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=0.6.0" } }, - "node_modules/to-buffer/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -18173,20 +17951,6 @@ "node": ">= 0.6" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -18795,27 +18559,6 @@ "node": ">= 8" } }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", diff --git a/package.json b/package.json index 6d4bafd..dddd11b 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,6 @@ "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", - "test:ci": "jest --ci --coverage --watchAll=false", - "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", "dev": "nest start --watch", "build": "nest build", "start": "nest start", @@ -22,9 +19,10 @@ "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", "format": "prettier --write \"src/**/*.ts\"", - "format:check": "prettier --check \"src/**/*.ts\"", - "pre-commit": "npm run lint && npm run format && npm run test:ci", - "prepare": "husky install" + "prepare": "husky install", + "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" }, "lint-staged": { "*.ts": [ @@ -49,7 +47,6 @@ "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", "@supabase/supabase-js": "^2.46.1", - "@trustless-work/escrow": "^2.0.2", "@types/bcryptjs": "^2.4.6", "axios": "^1.7.9", "bcryptjs": "^3.0.2", @@ -57,7 +54,6 @@ "class-validator": "^0.14.2", "cloudinary": "^1.41.3", "dotenv": "^16.4.7", - "env-var": "^7.5.0", "express": "^4.21.2", "express-async-handler": "^1.2.0", "express-rate-limit": "^7.5.0", @@ -111,7 +107,6 @@ "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0", - "typescript": "^5.7.2", - "zod": "^3.22.4" + "typescript": "^5.7.2" } } diff --git a/src/app.module.ts b/src/app.module.ts index 0e33b0d..d6aa1a5 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,11 +16,7 @@ import { NotificationsModule } from './modules/notifications/notifications.modul import { OrdersModule } from './modules/orders/orders.module'; import { BuyerRequestsModule } from './modules/buyer-requests/buyer-requests.module'; import { OffersModule } from './modules/offers/offers.module'; -import { EscrowModule } from './modules/escrow/escrow.module'; import { SupabaseModule } from './modules/supabase/supabase.module'; -import { EscrowModule } from './modules/escrow/escrow.module'; -import { AppCacheModule } from './cache/cache.module'; -import { StoresModule } from './modules/stores/stores.module'; // Entities import { User } from './modules/users/entities/user.entity'; @@ -40,20 +36,11 @@ import { CouponUsage } from './modules/coupons/entities/coupon-usage.entity'; import { BuyerRequest } from './modules/buyer-requests/entities/buyer-request.entity'; import { Offer } from './modules/offers/entities/offer.entity'; import { OfferAttachment } from './modules/offers/entities/offer-attachment.entity'; -import { EscrowAccount } from './modules/escrow/entities/escrow-account.entity'; -import { Milestone } from './modules/escrow/entities/milestone.entity'; -import { Escrow } from './modules/escrow/entities/escrow.entity'; -import { EscrowFundingTx } from './modules/escrow/entities/escrow-funding-tx.entity'; -import { Store } from './modules/stores/entities/store.entity'; -import { Escrow } from './modules/escrows/entities/escrow.entity'; -import { Milestone } from './modules/escrows/entities/milestone.entity'; -import { EscrowsModule } from './modules/escrows/escrows.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), ScheduleModule.forRoot(), - AppCacheModule, TypeOrmModule.forRoot({ type: 'postgres', url: process.env.DATABASE_URL, @@ -76,16 +63,8 @@ import { EscrowsModule } from './modules/escrows/escrows.module'; BuyerRequest, Offer, OfferAttachment, - EscrowAccount, - Milestone, - - Escrow, - EscrowFundingTx, - Store, - Escrow, - Milestone, ], - synchronize: false, + synchronize: process.env.NODE_ENV !== 'production', logging: process.env.NODE_ENV === 'development', }), SharedModule, @@ -101,11 +80,7 @@ import { EscrowsModule } from './modules/escrows/escrows.module'; OrdersModule, BuyerRequestsModule, OffersModule, - EscrowModule, SupabaseModule, - EscrowModule, - StoresModule, - EscrowsModule, ], }) -export class AppModule { } +export class AppModule {} diff --git a/src/common/filters/http-exception.filter.ts b/src/common/filters/http-exception.filter.ts index 59ca1a8..70f7c08 100644 --- a/src/common/filters/http-exception.filter.ts +++ b/src/common/filters/http-exception.filter.ts @@ -6,15 +6,12 @@ import { HttpStatus, } from '@nestjs/common'; import { Response } from 'express'; -import logger from '../utils/logger'; @Catch() export class HttpExceptionFilter implements ExceptionFilter { catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); - const request = ctx.getRequest(); - const startTime = request?._startTime || Date.now(); // Determine status code const status = @@ -24,6 +21,7 @@ export class HttpExceptionFilter implements ExceptionFilter { // Determine error message let message = 'Internal server error'; + if (exception instanceof HttpException) { const exceptionResponse = exception.getResponse(); if (typeof exceptionResponse === 'string') { @@ -35,22 +33,6 @@ export class HttpExceptionFilter implements ExceptionFilter { message = exception.message; } - // Get controller and handler info for action logging - const action = host.getType() === 'http' - ? `${request?.route?.path || 'UnknownRoute'}:${request?.method || 'UNKNOWN'}` - : 'UnknownAction'; - - // Log error telemetry - logger.error('RPC Error', { - action, - latency: Date.now() - startTime, - success: false, - method: request?.method, - url: request?.originalUrl, - error: message, - stack: exception?.stack, - }); - // Format error response with global standard const errorResponse = { success: false, diff --git a/src/common/interceptors/response.interceptor.ts b/src/common/interceptors/response.interceptor.ts index 2245ee7..d2ad27d 100644 --- a/src/common/interceptors/response.interceptor.ts +++ b/src/common/interceptors/response.interceptor.ts @@ -5,8 +5,7 @@ import { NestInterceptor, } from '@nestjs/common'; import { Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; -import logger from '../utils/logger'; +import { map } from 'rxjs/operators'; import { Response } from 'express'; @Injectable() @@ -14,25 +13,11 @@ export class ResponseInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { const ctx = context.switchToHttp(); const res = ctx.getResponse(); - const req = ctx.getRequest(); - const startTime = Date.now(); - - // Get controller and handler info for action logging - const handler = context.getHandler(); - const controller = context.getClass(); - const action = `${controller?.name || 'UnknownController'}.${handler?.name || 'unknownMethod'}`; return next.handle().pipe( map((data) => { // If response already has the standard format, return it as is if (data && typeof data === 'object' && 'success' in data) { - logger.info('RPC Success', { - action, - latency: Date.now() - startTime, - success: true, - method: req?.method, - url: req?.originalUrl, - }); return data; } @@ -50,28 +35,8 @@ export class ResponseInterceptor implements NestInterceptor { formattedResponse.token = token; } - logger.info('RPC Success', { - action, - latency: Date.now() - startTime, - success: true, - method: req?.method, - url: req?.originalUrl, - }); return formattedResponse; }), - tap({ - error: (err) => { - logger.error('RPC Error', { - action, - latency: Date.now() - startTime, - success: false, - method: req?.method, - url: req?.originalUrl, - error: err?.message, - stack: err?.stack, - }); - }, - }) ); } } diff --git a/src/config/index.ts b/src/config/index.ts index 4a57a3b..362b66f 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -7,7 +7,7 @@ export const config = { username: process.env.DB_USERNAME || 'postgres', password: process.env.DB_PASSWORD || 'password', name: process.env.DB_DATABASE || 'starshop', - synchronize: false, + synchronize: process.env.NODE_ENV !== 'production', logging: process.env.NODE_ENV !== 'production', ssl: process.env.DB_SSL === 'true', }, @@ -26,7 +26,4 @@ export const config = { url: process.env.SUPABASE_URL, serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY, }, - featureFlags: { - sellerEscrows: process.env.FF_SELLER_ESCROWS === 'true', - }, }; diff --git a/src/dtos/ProductVariantDTO.ts b/src/dtos/ProductVariantDTO.ts index c8492b0..f0494e0 100644 --- a/src/dtos/ProductVariantDTO.ts +++ b/src/dtos/ProductVariantDTO.ts @@ -1,8 +1,14 @@ +export interface AttributeValueDTO { + attributeId: string; // UUID or numeric depending on schema + valueId: string; // references attribute value entity + value?: string; // optional human-readable value +} + export class ProductVariantDTO { id?: string; - productId: string; - sku: string; - price: number; - stock: number; - attributes?: any[]; + productId!: string; + sku!: string; + price!: number; + stock!: number; + attributes?: AttributeValueDTO[]; } diff --git a/src/dtos/UserDTO.ts b/src/dtos/UserDTO.ts index e4ef13d..9e77d87 100644 --- a/src/dtos/UserDTO.ts +++ b/src/dtos/UserDTO.ts @@ -6,55 +6,7 @@ import { Matches, MinLength, MaxLength, - IsObject, - registerDecorator, - ValidationOptions, - ValidationArguments, } from 'class-validator'; -import { ApiPropertyOptional } from '@nestjs/swagger'; - -// Custom validator to ensure role-specific data rules -function IsRoleSpecificData(validationOptions?: ValidationOptions) { - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isRoleSpecificData', - target: object.constructor, - propertyName: propertyName, - options: validationOptions, - validator: { - validate(value: any, args: ValidationArguments) { - const obj = args.object as any; - const role = obj.role; - - if (propertyName === 'buyerData') { - // buyerData is only allowed for buyers - if (role !== 'buyer' && value !== undefined) { - return false; - } - } - - if (propertyName === 'sellerData') { - // sellerData is only allowed for sellers - if (role !== 'seller' && value !== undefined) { - return false; - } - } - - return true; - }, - defaultMessage(args: ValidationArguments) { - if (args.property === 'buyerData') { - return 'buyerData is only allowed for buyers'; - } - if (args.property === 'sellerData') { - return 'sellerData is only allowed for sellers'; - } - return 'Invalid role-specific data'; - } - } - }); - }; -} export class CreateUserDto { @IsNotEmpty() @@ -73,32 +25,6 @@ export class CreateUserDto { @IsNotEmpty() @IsEnum(['buyer', 'seller', 'admin'], { message: 'Role must be buyer, seller, or admin' }) role: 'buyer' | 'seller' | 'admin'; - - @IsOptional() - @MaxLength(100, { message: 'Location is too long' }) - location?: string; - - @IsOptional() - @MaxLength(100, { message: 'Country is too long' }) - country?: string; - - @ApiPropertyOptional({ - description: 'Buyer-specific data (only allowed if role is buyer)', - example: { preferences: ['electronics', 'books'] }, - }) - @IsRoleSpecificData({ message: 'buyerData is only allowed for buyers' }) - @IsObject({ message: 'Buyer data must be an object' }) - @IsOptional() - buyerData?: any; - - @ApiPropertyOptional({ - description: 'Seller-specific data (only allowed if role is seller)', - example: { businessName: 'Tech Store', categories: ['electronics'] }, - }) - @IsRoleSpecificData({ message: 'sellerData is only allowed for sellers' }) - @IsObject({ message: 'Seller data must be an object' }) - @IsOptional() - sellerData?: any; } export class UpdateUserDto { diff --git a/src/main.ts b/src/main.ts index ba980fd..1dd3249 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,12 +10,6 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter'; async function bootstrap(): Promise { const app = await NestFactory.create(AppModule); - // Middleware to track request start time for latency - app.use((req, res, next) => { - req._startTime = Date.now(); - next(); - }); - // Enable CORS app.enableCors(); diff --git a/src/middleware/async-handler.ts b/src/middleware/async-handler.ts index 231ab93..c7005f6 100644 --- a/src/middleware/async-handler.ts +++ b/src/middleware/async-handler.ts @@ -1,7 +1,9 @@ import { Request, Response, NextFunction, RequestHandler } from 'express'; -export const asyncHandler = - (fn: (req: Request, res: Response, next: NextFunction) => Promise): RequestHandler => - (req, res, next) => { +export const asyncHandler = ( + fn: (req: Request, res: Response, next: NextFunction) => Promise +): RequestHandler => { + return (req, res, next) => { fn(req, res, next).catch(next); }; +}; diff --git a/src/middleware/auth.middleware.ts b/src/middleware/auth.middleware.ts index 63917e2..84c1c14 100644 --- a/src/middleware/auth.middleware.ts +++ b/src/middleware/auth.middleware.ts @@ -9,7 +9,7 @@ export const authMiddleware = (req: Request, res: Response, next: NextFunction) export const requireRole = (roleName: Role) => { return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - if (!req.user || !req.user.role.some(role => role === roleName)) { + if (!req.user || !req.user.role.includes(roleName)) { return res.status(403).json({ message: 'Forbidden' }); } next(); diff --git a/src/middleware/paramValidation.middleware.ts b/src/middleware/paramValidation.middleware.ts index 6dd1c4d..e1b3f59 100644 --- a/src/middleware/paramValidation.middleware.ts +++ b/src/middleware/paramValidation.middleware.ts @@ -1,11 +1,10 @@ -import { param } from 'express-validator'; +import { param, ValidationChain } from 'express-validator'; import { validateRequest } from '../middleware/validateRequest.middleware'; export const paramValidators = { isPositiveInt: param('id').isInt({ min: 1 }).toInt(), - // Agrega más validadores según sea necesario + // Add more validators as needed }; -export const paramValidationMiddleware = (validators: Record) => { - return validateRequest(Object.values(validators)); -}; +export const paramValidationMiddleware = (validators: Record) => + validateRequest(Object.values(validators)); diff --git a/src/middleware/userValidation.middleware.ts b/src/middleware/userValidation.middleware.ts index 329d0aa..6447ef6 100644 --- a/src/middleware/userValidation.middleware.ts +++ b/src/middleware/userValidation.middleware.ts @@ -6,23 +6,21 @@ import { Request, Response, NextFunction } from 'express'; * A validation middleware for validating request body data against a DTO class. * @param dtoClass The DTO class to validate against. */ -export function validationMiddleware(dtoClass: any) { +export type ClassType = new (...args: unknown[]) => T; + +export function validationMiddleware(dtoClass: ClassType) { return async (req: Request, res: Response, next: NextFunction): Promise => { const dtoInstance = plainToInstance(dtoClass, req.body); - const errors: ValidationError[] = await validate(dtoInstance); - + const errors: ValidationError[] = await validate(dtoInstance as object); if (errors.length > 0) { const errorDetails = errors.map((error) => ({ property: error.property, constraints: error.constraints, })); - - res.status(422).json({ - message: 'Validation failed', - errors: errorDetails, - }); - } else { - next(); + res.status(422).json({ message: 'Validation failed', errors: errorDetails }); + return; } + req.body = dtoInstance as unknown as T; // explicit assignment for downstream handlers + next(); }; } diff --git a/src/middleware/validateRequest.middleware.ts b/src/middleware/validateRequest.middleware.ts index fef2868..7b73a43 100644 --- a/src/middleware/validateRequest.middleware.ts +++ b/src/middleware/validateRequest.middleware.ts @@ -1,15 +1,17 @@ import { Request, Response, NextFunction } from 'express'; -import { validationResult } from 'express-validator'; +import { validationResult, ValidationChain } from 'express-validator'; -export const validateRequest = (validations: any[]) => { - return async (req: Request, res: Response, next: NextFunction) => { +/** + * Wraps an array of express-validator chains and sends 400 with aggregated errors when validation fails. + * Uses explicit ValidationChain type instead of any[] for stronger typing. + */ +export const validateRequest = (validations: ValidationChain[]) => { + return async (req: Request, res: Response, next: NextFunction): Promise => { await Promise.all(validations.map((validation) => validation.run(req))); - const errors = validationResult(req); if (errors.isEmpty()) { return next(); } - - return res.status(400).json({ errors: errors.array() }); + res.status(400).json({ errors: errors.array() }); }; }; diff --git a/src/middleware/validation.middleware.ts b/src/middleware/validation.middleware.ts index a56c18c..e951adc 100644 --- a/src/middleware/validation.middleware.ts +++ b/src/middleware/validation.middleware.ts @@ -1,29 +1,23 @@ import { Request, Response, NextFunction } from 'express'; import { validate } from 'class-validator'; -import { plainToClass } from 'class-transformer'; +import { plainToInstance } from 'class-transformer'; -export const validateRequest = (dtoClass: any) => { - return async (req: Request, res: Response, next: NextFunction): Promise => { - const dtoObject = plainToClass(dtoClass, req.body); - const errors = await validate(dtoObject); +export type Constructor = new (...args: unknown[]) => T; +export const validateRequest = (dtoClass: Constructor) => { + return async (req: Request, res: Response, next: NextFunction): Promise => { + const dtoObject = plainToInstance(dtoClass, req.body); + const errors = await validate(dtoObject as object); if (errors.length > 0) { const errorMessages = errors.map((error) => ({ property: error.property, constraints: error.constraints, })); - - res.status(400).json({ - status: 'error', - message: 'Validation failed', - errors: errorMessages, - }); - - return; // ✅ Solo return vacío + res.status(400).json({ status: 'error', message: 'Validation failed', errors: errorMessages }); + return; } - - req.body = dtoObject; - next(); // ✅ Deja continuar la cadena de middleware + req.body = dtoObject as unknown as T; + next(); }; }; diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 163f0a1..38be080 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -17,7 +17,6 @@ import { JwtStrategy } from './strategies/jwt.strategy'; import { JwtAuthGuard } from './guards/jwt-auth.guard'; import { RolesGuard } from './guards/roles.guard'; import { UsersModule } from '../users/users.module'; -import { StoresModule } from '../stores/stores.module'; @Module({ imports: [ @@ -32,7 +31,6 @@ import { StoresModule } from '../stores/stores.module'; inject: [ConfigService], }), forwardRef(() => UsersModule), - StoresModule, ], controllers: [AuthController, RoleController], providers: [AuthService, RoleService, JwtAuthGuard, RolesGuard, JwtStrategy], diff --git a/src/modules/auth/controllers/auth.controller.ts b/src/modules/auth/controllers/auth.controller.ts index f075423..e69bc28 100644 --- a/src/modules/auth/controllers/auth.controller.ts +++ b/src/modules/auth/controllers/auth.controller.ts @@ -106,6 +106,7 @@ export class AuthController { success: true, data: { user: { + id: result.user.id, walletAddress: result.user.walletAddress, name: result.user.name, email: result.user.email, @@ -150,7 +151,6 @@ export class AuthController { role: registerDto.role, name: registerDto.name, email: registerDto.email, - country: registerDto.country?.toUpperCase(), }); // Set JWT token using the helper function @@ -162,6 +162,7 @@ export class AuthController { success: true, data: { user: { + id: result.user.id, walletAddress: result.user.walletAddress, name: result.user.name, email: result.user.email, @@ -205,11 +206,11 @@ export class AuthController { return { success: true, data: { + id: user.id, walletAddress: user.walletAddress, name: user.name, email: user.email, role: user.userRoles?.[0]?.role?.name || 'buyer', - country: user?.country || null, createdAt: user.createdAt, updatedAt: user.updatedAt, }, diff --git a/src/modules/auth/controllers/role.controller.ts b/src/modules/auth/controllers/role.controller.ts index d3b6b6c..fda2dd0 100644 --- a/src/modules/auth/controllers/role.controller.ts +++ b/src/modules/auth/controllers/role.controller.ts @@ -9,10 +9,10 @@ export class RoleController { @Post('assign') @UseGuards(JwtAuthGuard) async assignRole( - @Body() body: { walletAddress: string; roleName: string } + @Body() body: { userId: number; roleName: number } ): Promise<{ success: boolean }> { - const { walletAddress, roleName } = body; - await this.roleService.assignRoleToUser(walletAddress, roleName); + const { userId, roleName } = body; + await this.roleService.assignRoleToUser(userId.toString(), roleName.toString()); return { success: true }; } diff --git a/src/modules/auth/dto/auth-response.dto.ts b/src/modules/auth/dto/auth-response.dto.ts index dc9c68a..5c90cb2 100644 --- a/src/modules/auth/dto/auth-response.dto.ts +++ b/src/modules/auth/dto/auth-response.dto.ts @@ -3,7 +3,7 @@ import { ApiProperty } from '@nestjs/swagger'; export class ChallengeResponseDto { @ApiProperty({ description: 'Success status', - example: true, + example: true }) success: boolean; @@ -12,8 +12,8 @@ export class ChallengeResponseDto { example: { challenge: 'Please sign this message to authenticate: 1234567890', walletAddress: 'GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890123456789012345678901234567890', - timestamp: 1640995200000, - }, + timestamp: 1640995200000 + } }) data: { challenge: string; @@ -23,27 +23,33 @@ export class ChallengeResponseDto { } export class UserDto { + @ApiProperty({ + description: 'User ID', + example: 1 + }) + id: number; + @ApiProperty({ description: 'Stellar wallet address', - example: 'GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890123456789012345678901234567890', + example: 'GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890123456789012345678901234567890' }) walletAddress: string; @ApiProperty({ description: 'User display name', - example: 'John Doe', + example: 'John Doe' }) name: string; @ApiProperty({ description: 'User email address', - example: 'john.doe@example.com', + example: 'john.doe@example.com' }) email: string; @ApiProperty({ description: 'User role', - example: 'buyer', + example: 'buyer' }) role: string; } @@ -51,7 +57,7 @@ export class UserDto { export class AuthResponseDto { @ApiProperty({ description: 'Success status', - example: true, + example: true }) success: boolean; @@ -59,13 +65,14 @@ export class AuthResponseDto { description: 'Authentication data', example: { user: { + id: 1, walletAddress: 'GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890123456789012345678901234567890', name: 'John Doe', email: 'john.doe@example.com', - role: 'buyer', + role: 'buyer' }, - expiresIn: 3600, - }, + expiresIn: 3600 + } }) data: { user: UserDto; @@ -76,29 +83,28 @@ export class AuthResponseDto { export class UserResponseDto { @ApiProperty({ description: 'Success status', - example: true, + example: true }) success: boolean; @ApiProperty({ description: 'User data', example: { + id: 1, walletAddress: 'GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890123456789012345678901234567890', name: 'John Doe', email: 'john.doe@example.com', role: 'buyer', - // Optional fields - country: 'US', createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z', - }, + updatedAt: '2024-01-01T00:00:00.000Z' + } }) data: { + id: number; walletAddress: string; name: string; email: string; role: string; - country?: string | null; createdAt: Date; updatedAt: Date; }; @@ -107,13 +113,13 @@ export class UserResponseDto { export class LogoutResponseDto { @ApiProperty({ description: 'Success status', - example: true, + example: true }) success: boolean; @ApiProperty({ description: 'Logout message', - example: 'Logged out successfully', + example: 'Logged out successfully' }) message: string; -} +} \ No newline at end of file diff --git a/src/modules/auth/dto/auth.dto.ts b/src/modules/auth/dto/auth.dto.ts index 1d9e50e..adef4ee 100644 --- a/src/modules/auth/dto/auth.dto.ts +++ b/src/modules/auth/dto/auth.dto.ts @@ -1,62 +1,5 @@ -import { - IsString, - IsOptional, - Matches, - IsNotEmpty, - IsEmail, - IsObject, - IsEnum, - Validate, - registerDecorator, - ValidationOptions, - ValidationArguments -} from 'class-validator'; +import { IsString, IsOptional, Matches, IsNotEmpty, IsEmail } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type, Transform } from 'class-transformer'; -import { CountryCode } from '@/modules/users/enums/country-code.enum'; - -// Custom validator to ensure role-specific data rules -function IsRoleSpecificData(validationOptions?: ValidationOptions) { - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isRoleSpecificData', - target: object.constructor, - propertyName: propertyName, - options: validationOptions, - validator: { - validate(value: any, args: ValidationArguments) { - const obj = args.object as any; - const role = obj.role; - - if (propertyName === 'buyerData') { - // buyerData is only allowed for buyers - if (role !== 'buyer' && value !== undefined) { - return false; - } - } - - if (propertyName === 'sellerData') { - // sellerData is only allowed for sellers - if (role !== 'seller' && value !== undefined) { - return false; - } - } - - return true; - }, - defaultMessage(args: ValidationArguments) { - if (args.property === 'buyerData') { - return 'buyerData is only allowed for buyers'; - } - if (args.property === 'sellerData') { - return 'sellerData is only allowed for sellers'; - } - return 'Invalid role-specific data'; - } - } - }); - }; -} export class StellarWalletLoginDto { @ApiProperty({ @@ -106,44 +49,6 @@ export class RegisterUserDto { @IsEmail() @IsOptional() email?: string; - - @ApiProperty({ - description: "Country code of the buyer request", - example: "US", - enum: CountryCode, - enumName: 'CountryCode' - }) - @Transform(({ value }) => value?.toUpperCase()) - @IsOptional() - @IsString() - @IsEnum(CountryCode, { message: 'Country must be a valid ISO 3166-1 alpha-2 country code' }) - country?: string; - - @ApiPropertyOptional({ - description: 'User location', - example: 'New York', - }) - @IsString() - @IsOptional() - location?: string; - - @ApiPropertyOptional({ - description: 'Buyer-specific data (only allowed if role is buyer)', - example: { preferences: ['electronics', 'books'] }, - }) - @IsRoleSpecificData({ message: 'buyerData is only allowed for buyers' }) - @IsObject() - @IsOptional() - buyerData?: any; - - @ApiPropertyOptional({ - description: 'Seller-specific data (only allowed if role is seller)', - example: { businessName: 'Tech Store', categories: ['electronics'], rating: 4.5 }, - }) - @IsRoleSpecificData({ message: 'sellerData is only allowed for sellers' }) - @IsObject() - @IsOptional() - sellerData?: any; } export class UpdateUserDto { @@ -162,44 +67,6 @@ export class UpdateUserDto { @IsEmail() @IsOptional() email?: string; - - @ApiPropertyOptional({ - description: 'User location', - example: 'New York', - }) - @IsString() - @IsOptional() - location?: string; - - @ApiPropertyOptional({ - description: "Country code of the buyer request", - example: "US", - enum: CountryCode, - enumName: 'CountryCode' - }) - @Transform(({ value }) => value?.toUpperCase()) - @IsOptional() - @IsString() - @IsEnum(CountryCode, { message: 'Country must be a valid ISO 3166-1 alpha-2 country code' }) - country?: string; - - @ApiPropertyOptional({ - description: 'Buyer-specific data (only allowed if role is buyer)', - example: { preferences: ['electronics', 'books'] }, - }) - @IsRoleSpecificData({ message: 'buyerData is only allowed for buyers' }) - @IsObject() - @IsOptional() - buyerData?: any; - - @ApiPropertyOptional({ - description: 'Seller-specific data (only allowed if role is seller)', - example: { businessName: 'Tech Store', categories: ['electronics'], rating: 4.5 }, - }) - @IsRoleSpecificData({ message: 'sellerData is only allowed for sellers' }) - @IsObject() - @IsOptional() - sellerData?: any; } export class ChallengeDto { diff --git a/src/modules/auth/entities/user-role.entity.ts b/src/modules/auth/entities/user-role.entity.ts index 6d46822..711b5e6 100644 --- a/src/modules/auth/entities/user-role.entity.ts +++ b/src/modules/auth/entities/user-role.entity.ts @@ -7,8 +7,8 @@ export class UserRole { @PrimaryGeneratedColumn() id: number; - @Column({ type: 'uuid' }) - userId: string; + @Column() + userId: number; @Column() roleId: number; diff --git a/src/modules/auth/index.ts b/src/modules/auth/index.ts index 3e5e775..c865ff6 100644 --- a/src/modules/auth/index.ts +++ b/src/modules/auth/index.ts @@ -1,8 +1,6 @@ export * from './guards/jwt-auth.guard'; export * from './guards/roles.guard'; -export * from './guards/wallet-ownership.guard'; export * from './decorators/roles.decorator'; -export * from './decorators/wallet-ownership.decorator'; export * from './services/role.service'; export * from './services/auth.service'; export * from './entities/role.entity'; diff --git a/src/modules/auth/middleware/authorize-roles.middleware.ts b/src/modules/auth/middleware/authorize-roles.middleware.ts index 4244979..24eb165 100644 --- a/src/modules/auth/middleware/authorize-roles.middleware.ts +++ b/src/modules/auth/middleware/authorize-roles.middleware.ts @@ -9,7 +9,7 @@ export const authorizeRoles = (allowedRoles: Role[]) => { throw new UnauthorizedException('User not authenticated'); } - const userRoles = req.user.role as Role[]; + const userRoles = req.user.role; const hasAllowedRole = userRoles.some((role) => allowedRoles.includes(role)); if (!hasAllowedRole) { diff --git a/src/modules/auth/middleware/jwt-auth.middleware.ts b/src/modules/auth/middleware/jwt-auth.middleware.ts index 1a1b089..afa6417 100644 --- a/src/modules/auth/middleware/jwt-auth.middleware.ts +++ b/src/modules/auth/middleware/jwt-auth.middleware.ts @@ -48,7 +48,7 @@ export const jwtAuthMiddleware = async (req: Request, res: Response, next: NextF walletAddress: user.walletAddress, name: user.name, role: user.userRoles?.map((ur) => ur.role.name as Role) || [decoded.role as Role], - } as any; + }; next(); } catch (error) { diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 7ff19d0..4f9b725 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -11,9 +11,6 @@ import { BadRequestError, UnauthorizedError } from '../../../utils/errors'; import { sign } from 'jsonwebtoken'; import { config } from '../../../config'; import { Keypair } from 'stellar-sdk'; -import { CountryCode } from '../../../modules/users/enums/country-code.enum'; -import { StoreService } from '../../stores/services/store.service'; - type RoleName = 'buyer' | 'seller' | 'admin'; @@ -31,8 +28,7 @@ export class AuthService { @Inject(forwardRef(() => UserService)) private readonly userService: UserService, private readonly jwtService: JwtService, - private readonly roleService: RoleService, - private readonly storeService: StoreService, + private readonly roleService: RoleService ) {} /** @@ -53,7 +49,6 @@ export class AuthService { process.env.NODE_ENV === 'development' && signature === 'base64-encoded-signature-string-here' ) { - // eslint-disable-next-line no-console console.log('Development mode: Bypassing signature verification for testing'); return true; } @@ -64,7 +59,6 @@ export class AuthService { return keypair.verify(messageBuffer, signatureBuffer); } catch (error) { - // eslint-disable-next-line no-console console.error('Signature verification error:', error); return false; } @@ -78,51 +72,29 @@ export class AuthService { role: 'buyer' | 'seller'; name?: string; email?: string; - location?: string; - country?: string; - buyerData?: any; - sellerData?: any; }): Promise<{ user: User; token: string; expiresIn: number }> { - // Validate that buyers can't have seller data and sellers can't have buyer data - if (data.role === 'buyer' && data.sellerData !== undefined) { - throw new BadRequestError('Buyers cannot have seller data'); - } - if (data.role === 'seller' && data.buyerData !== undefined) { - throw new BadRequestError('Sellers cannot have buyer data'); - } - // Check if user already exists const existingUser = await this.userRepository.findOne({ where: { walletAddress: data.walletAddress }, relations: ['userRoles', 'userRoles.role'], }); - // If user is not a buyer made country validations - if (!this.isBuyer(data)) { - data.country = null; - } - if (existingUser) { // Update existing user instead of throwing error existingUser.name = data.name || existingUser.name; existingUser.email = data.email || existingUser.email; - existingUser.location = data.location || existingUser.location; - existingUser.country = data.country || existingUser.country; - existingUser.buyerData = data.buyerData || existingUser.buyerData; - existingUser.sellerData = data.sellerData || existingUser.sellerData; - - const dataToValidate = { role: data.role, country: data.country }; - if(!this.isBuyer(dataToValidate)){ - existingUser.country = null; - } - - existingUser.country = data.country || existingUser.country; const updatedUser = await this.userRepository.save(existingUser); - const role = updatedUser.userRoles?.[0]?.role?.name || 'buyer'; // Generate JWT token - const token = this.generateJwtToken(updatedUser, role); + const role = updatedUser.userRoles?.[0]?.role?.name || 'buyer'; + const token = sign( + { id: updatedUser.id, walletAddress: updatedUser.walletAddress, role }, + config.jwtSecret, + { + expiresIn: '1h', + } + ); return { user: updatedUser, token, expiresIn: 3600 }; } @@ -132,85 +104,39 @@ export class AuthService { walletAddress: data.walletAddress, name: data.name, email: data.email, - country: data?.country || null, - location: data.location, - country: data.country, - buyerData: data.buyerData, - sellerData: data.sellerData, }); const savedUser = await this.userRepository.save(user); - // Assign user role to user_roles table + // Assign user role const userRole = await this.roleRepository.findOne({ where: { name: data.role } }); - if (!userRole) { - throw new BadRequestError(`Role ${data.role} does not exist`); - } - const userRoleEntity = this.userRoleRepository.create({ - userId: savedUser.id, - roleId: userRole.id, - user: savedUser, - role: userRole, - }); - await this.userRoleRepository.save(userRoleEntity); - - // Create default store for sellers - if (data.role === 'seller') { - try { - await this.storeService.createDefaultStore(savedUser.id, data.sellerData); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Failed to create default store for seller:', error); - // Don't fail the registration if store creation fails - } + if (userRole) { + const userRoleEntity = this.userRoleRepository.create({ + userId: savedUser.id, + roleId: userRole.id, + user: savedUser, + role: userRole, + }); + await this.userRoleRepository.save(userRoleEntity); } // Generate JWT token - const token = this.generateJwtToken(savedUser, userRole.name); - - return { user: savedUser, token, expiresIn: 3600 }; - } - - /** - * Generate JWT token for user - */ - private generateJwtToken(user: User, role: string): string { - return sign( - { id: user.id, walletAddress: user.walletAddress, role }, + const token = sign( + { id: savedUser.id, walletAddress: savedUser.walletAddress, role: data.role }, config.jwtSecret, { expiresIn: '1h', - }, + } ); - } - - /** - * Check if the user is a buyer and validate fields of buyer registration - */ - private isBuyer(data: { - role: 'buyer' | 'seller'; - country?: string; - }) { - if (data.role !== 'buyer') { - return false; - } - - if (!data.country) { - throw new BadRequestError('Country is required for buyer registration'); - } - if (!Object.values(CountryCode).includes(data.country as CountryCode)) { - throw new BadRequestError('Country must be a valid ISO 3166-1 alpha-2 country code'); - } - - return true; + return { user: savedUser, token, expiresIn: 3600 }; } /** * Login with Stellar wallet (no signature required) */ async loginWithWallet( - walletAddress: string, + walletAddress: string ): Promise<{ user: User; token: string; expiresIn: number }> { // Find user const user = await this.userRepository.findOne({ @@ -236,7 +162,7 @@ export class AuthService { */ async getUserById(id: string): Promise { const user = await this.userRepository.findOne({ - where: { id }, + where: { id: Number(id) }, relations: ['userRoles', 'userRoles.role'], }); @@ -248,21 +174,10 @@ export class AuthService { } /** - * Update user information (usar walletAddress como identificador primario) - * Mantiene todo lo de develop (location, country, buyerData, sellerData, etc.) + * Update user information */ - async updateUser( - walletAddress: string, - updateData: { - name?: string; - email?: string; - location?: string; - country?: string; - buyerData?: any; - sellerData?: any; - }, - ): Promise { - const user = await this.userRepository.findOne({ where: { walletAddress } }); + async updateUser(userId: number, updateData: { name?: string; email?: string }): Promise { + const user = await this.userRepository.findOne({ where: { id: userId } }); if (!user) { throw new BadRequestError('User not found'); @@ -272,42 +187,7 @@ export class AuthService { Object.assign(user, updateData); await this.userRepository.save(user); - return this.getUserByWalletAddress(walletAddress); - } - - /** - * (Compat) Update user by numeric ID — conserva compatibilidad con develop - * Preferir updateUser(walletAddress, …) - */ - async updateUserById( - userId: number, - updateData: { - name?: string; - email?: string; - location?: string; - country?: string; - buyerData?: any; - sellerData?: any; - }, - ): Promise { - const user = await this.userRepository.findOne({ where: { id: userId } }); - if (!user) { - throw new BadRequestError('User not found'); - } - Object.assign(user, updateData); - await this.userRepository.save(user); - return this.getUserByWalletAddress(user.walletAddress); - } - - async getUserByWalletAddress(walletAddress: string): Promise { - const user = await this.userRepository.findOne({ - where: { walletAddress }, - relations: ['userRoles', 'userRoles.role'], - }); - if (!user) { - throw new BadRequestError('User not found'); - } - return user; + return this.getUserById(String(userId)); } async authenticateUser(walletAddress: string): Promise<{ access_token: string }> { @@ -353,8 +233,8 @@ export class AuthService { return { access_token: this.jwtService.sign(payload) }; } - async assignRole(walletAddress: string, roleName: RoleName): Promise { - const user = await this.userService.getUserByWalletAddress(walletAddress); + async assignRole(userId: number, roleName: RoleName): Promise { + const user = await this.userService.getUserById(String(userId)); if (!user) { throw new UnauthorizedException('User not found'); } @@ -365,7 +245,7 @@ export class AuthService { } // Remove existing roles - await this.userRoleRepository.delete({ userId: user.id }); + await this.userRoleRepository.delete({ userId }); // Create new user role relationship const userRole = this.userRoleRepository.create({ @@ -376,17 +256,17 @@ export class AuthService { }); await this.userRoleRepository.save(userRole); - return this.userService.getUserByWalletAddress(walletAddress); + return this.userService.getUserById(String(userId)); } - async removeRole(walletAddress: string): Promise { - const user = await this.userService.getUserByWalletAddress(walletAddress); + async removeRole(userId: number): Promise { + const user = await this.userService.getUserById(String(userId)); if (!user) { throw new UnauthorizedException('User not found'); } - await this.userRoleRepository.delete({ userId: user.id }); + await this.userRoleRepository.delete({ userId }); - return this.userService.getUserByWalletAddress(walletAddress); + return this.userService.getUserById(String(userId)); } } diff --git a/src/modules/auth/services/role.service.ts b/src/modules/auth/services/role.service.ts index 9499f4c..7654e6c 100644 --- a/src/modules/auth/services/role.service.ts +++ b/src/modules/auth/services/role.service.ts @@ -47,30 +47,30 @@ export class RoleService { throw new Error(`Role ${roleName} not found`); } await this.userRoleRepository.save({ - userId, + userId: parseInt(userId), roleId: role.id, }); } - async removeRoleFromUser(userId: string, roleId: number): Promise { + async removeRoleFromUser(userId: number, roleId: number): Promise { await this.userRoleRepository.delete({ userId, roleId }); } async getUserRoles(userId: string): Promise { const userRoles = await this.userRoleRepository.find({ - where: { userId }, + where: { userId: parseInt(userId) }, relations: ['role'], }); return userRoles.map((ur) => ur.role); } - async hasRole(userId: string, roleName: RoleName): Promise { - const userRoles = await this.getUserRoles(userId); + async hasRole(userId: number, roleName: RoleName): Promise { + const userRoles = await this.getUserRoles(userId.toString()); return userRoles.some((role) => role.name === roleName); } - async hasAnyRole(userId: string, roleNames: RoleName[]): Promise { - const userRoles = await this.getUserRoles(userId); + async hasAnyRole(userId: number, roleNames: RoleName[]): Promise { + const userRoles = await this.getUserRoles(userId.toString()); return userRoles.some((role) => roleNames.includes(role.name)); } } diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index 827ee64..3a55ceb 100644 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -24,23 +24,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) { async validate(payload: any) { try { - // Try to get user by walletAddress first (preferred method) - let user; - if (payload.walletAddress) { - user = await this.authService.getUserByWalletAddress(payload.walletAddress); - } else if (payload.id) { - // Fallback to id for backward compatibility during migration - user = await this.authService.getUserById(payload.id); - } else { - throw new UnauthorizedException('Invalid token payload'); - } - + const user = await this.authService.getUserById(payload.id); if (!user) { throw new UnauthorizedException('User not found'); } - return { - id: user.id, // Keep UUID for internal use + id: user.id, walletAddress: user.walletAddress, role: user.userRoles?.[0]?.role?.name || 'buyer', }; diff --git a/src/modules/auth/tests/auth.service.spec.ts b/src/modules/auth/tests/auth.service.spec.ts index 52ac241..765781d 100644 --- a/src/modules/auth/tests/auth.service.spec.ts +++ b/src/modules/auth/tests/auth.service.spec.ts @@ -5,7 +5,6 @@ import { JwtService } from '@nestjs/jwt'; import { Keypair } from 'stellar-sdk'; import { BadRequestError, UnauthorizedError } from '../../../utils/errors'; import { User } from '../../users/entities/user.entity'; -import { Role as UserRoleEnum } from '../../../types/role'; // Mock dependencies jest.mock('../../users/services/user.service'); @@ -43,7 +42,6 @@ describe('AuthService', () => { const mockUserRepository = { findOne: jest.fn(), create: jest.fn(), save: jest.fn() } as any; const mockRoleRepository = { findOne: jest.fn(), create: jest.fn(), save: jest.fn() } as any; const mockUserRoleRepository = { create: jest.fn(), save: jest.fn() } as any; - const mockStoreService = { createDefaultStore: jest.fn() } as any; authService = new AuthService( mockUserRepository, @@ -51,8 +49,7 @@ describe('AuthService', () => { mockUserRoleRepository, userService, jwtService, - roleService, - mockStoreService + roleService ); }); @@ -119,10 +116,6 @@ describe('AuthService', () => { walletAddress: mockWalletAddress, name: 'Test User', email: 'test@example.com', - location: 'Test City', - country: 'Test Country', - buyerData: {}, - sellerData: null, userRoles: [{ role: { name: 'buyer' } }] as any, }; @@ -160,12 +153,7 @@ describe('AuthService', () => { walletAddress: mockWalletAddress, name: 'New User', email: 'new@example.com', - location: 'Test City', - country: 'Test Country', - buyerData: {}, - sellerData: null, userRoles: [{ role: { name: 'buyer' } }] as any, - country: 'US', }; beforeEach(() => { @@ -180,38 +168,13 @@ describe('AuthService', () => { create: jest.fn().mockReturnValue(mockNewUser), save: jest.fn().mockResolvedValue(mockNewUser), }; - - const mockRoleRepository = { - findOne: jest.fn().mockResolvedValue({ - id: 1, - name: 'buyer' - }), - }; - - // Add mock for userRoleRepository - const mockUserRoleRepository = { - create: jest.fn().mockReturnValue({ - id: 1, - userId: 1, - roleId: 1 - }), - save: jest.fn().mockResolvedValue({ - id: 1, - userId: 1, - roleId: 1 - }), - }; - (authService as any).userRepository = mockUserRepository; - (authService as any).roleRepository = mockRoleRepository; - (authService as any).userRoleRepository = mockUserRoleRepository; const result = await authService.registerWithWallet({ walletAddress: mockWalletAddress, - role: UserRoleEnum.BUYER, + role: 'buyer', name: 'New User', email: 'new@example.com', - country: 'CR' }); expect(result.user).toEqual(mockNewUser); @@ -228,7 +191,7 @@ describe('AuthService', () => { await expect( authService.registerWithWallet({ walletAddress: mockWalletAddress, - role: UserRoleEnum.BUYER, + role: 'buyer', name: 'New User', }) ).rejects.toThrow(BadRequestError); @@ -241,10 +204,9 @@ describe('AuthService', () => { await expect( authService.registerWithWallet({ walletAddress: mockWalletAddress, - country: 'CR', - role: UserRoleEnum.BUYER, + role: 'buyer', }) - ).rejects.toThrow(BadRequestError); + ).rejects.toThrow(UnauthorizedError); }); }); diff --git a/src/modules/buyer-requests/dto/buyer-request-response.dto.ts b/src/modules/buyer-requests/dto/buyer-request-response.dto.ts index ee3ffa4..523e462 100644 --- a/src/modules/buyer-requests/dto/buyer-request-response.dto.ts +++ b/src/modules/buyer-requests/dto/buyer-request-response.dto.ts @@ -8,12 +8,12 @@ export interface BuyerRequestResponseDto { budgetMax: number categoryId: number status: BuyerRequestStatus - userId: string + userId: number expiresAt?: Date createdAt: Date updatedAt: Date user?: { - id: string + id: number name: string walletAddress: string } diff --git a/src/modules/buyer-requests/entities/buyer-request.entity.ts b/src/modules/buyer-requests/entities/buyer-request.entity.ts index b807c94..291ae68 100644 --- a/src/modules/buyer-requests/entities/buyer-request.entity.ts +++ b/src/modules/buyer-requests/entities/buyer-request.entity.ts @@ -51,8 +51,8 @@ export class BuyerRequest { }) status: BuyerRequestStatus; - @Column({ type: 'uuid' }) - userId: string; + @Column() + userId: number; @Column({ type: 'timestamp', nullable: true }) expiresAt: Date; diff --git a/src/modules/buyer-requests/services/buyer-requests.service.ts b/src/modules/buyer-requests/services/buyer-requests.service.ts index 731d2c9..6bd5fcb 100644 --- a/src/modules/buyer-requests/services/buyer-requests.service.ts +++ b/src/modules/buyer-requests/services/buyer-requests.service.ts @@ -7,7 +7,6 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { Repository, SelectQueryBuilder } from 'typeorm'; import { BuyerRequest, BuyerRequestStatus } from '../entities/buyer-request.entity'; -import { User } from '../../users/entities/user.entity'; import { CreateBuyerRequestDto } from '../dto/create-buyer-request.dto'; import { UpdateBuyerRequestDto } from '../dto/update-buyer-request.dto'; import { GetBuyerRequestsQueryDto } from '../dto/get-buyer-requests-query.dto'; @@ -20,71 +19,13 @@ import { export class BuyerRequestsService { constructor( @InjectRepository(BuyerRequest) - private readonly buyerRequestRepository: Repository, - @InjectRepository(User) - private readonly userRepository: Repository + private readonly buyerRequestRepository: Repository ) {} - /** - * Validates that the user's wallet address exists for buyer operations. - * This is crucial for preventing unauthorized contract calls. - */ - private async validateBuyerWalletOwnership(userId: number): Promise { - const user = await this.userRepository.findOne({ - where: { id: userId }, - }); - - if (!user) { - throw new ForbiddenException('User not found'); - } - - if (!user.walletAddress) { - throw new ForbiddenException('User wallet address not found'); - } - } - - /** - * Validates that a user can only modify buyer requests they own (wallet ownership validation). - */ - private async validateBuyerRequestOwnership( - requestId: number, - userId: number - ): Promise { - const buyerRequest = await this.buyerRequestRepository.findOne({ - where: { id: requestId }, - relations: ['user'], - }); - - if (!buyerRequest) { - throw new NotFoundException('Buyer request not found'); - } - - if (buyerRequest.userId !== userId) { - throw new ForbiddenException('You can only modify your own buyer requests'); - } - - // Additional wallet validation to prevent contract call issues - const user = await this.userRepository.findOne({ where: { id: userId } }); - if (!user) { - throw new ForbiddenException('User not found'); - } - - if (buyerRequest.user.walletAddress !== user.walletAddress) { - throw new ForbiddenException( - 'Wallet ownership mismatch: This buyer request belongs to a different wallet' - ); - } - - return buyerRequest; - } - async create( createBuyerRequestDto: CreateBuyerRequestDto, - userId: string + userId: number ): Promise { - // Validate buyer wallet ownership to prevent unauthorized contract calls - await this.validateBuyerWalletOwnership(userId); - const { budgetMin, budgetMax, expiresAt } = createBuyerRequestDto; // Validate budget range @@ -194,10 +135,20 @@ export class BuyerRequestsService { async update( id: number, updateBuyerRequestDto: UpdateBuyerRequestDto, - userId: string + userId: number ): Promise { - // Validate wallet ownership before allowing updates that could trigger contract calls - const buyerRequest = await this.validateBuyerRequestOwnership(id, userId); + const buyerRequest = await this.buyerRequestRepository.findOne({ + where: { id }, + }); + + if (!buyerRequest) { + throw new NotFoundException('Buyer request not found'); + } + + // Check ownership + if (buyerRequest.userId !== userId) { + throw new ForbiddenException('You can only update your own buyer requests'); + } // Check if request is still open if (buyerRequest.status !== BuyerRequestStatus.OPEN) { @@ -236,8 +187,18 @@ export class BuyerRequestsService { } async remove(id: number, userId: number): Promise { - // Validate wallet ownership before allowing deletions that could affect contracts - const buyerRequest = await this.validateBuyerRequestOwnership(id, userId); + const buyerRequest = await this.buyerRequestRepository.findOne({ + where: { id }, + }); + + if (!buyerRequest) { + throw new NotFoundException('Buyer request not found'); + } + + // Check ownership + if (buyerRequest.userId !== userId) { + throw new ForbiddenException('You can only delete your own buyer requests'); + } await this.buyerRequestRepository.remove(buyerRequest); } @@ -325,7 +286,7 @@ export class BuyerRequestsService { /** * Manually close a buyer request (buyer-only access) */ - async closeRequest(id: number, userId: string): Promise { + async closeRequest(id: number, userId: number): Promise { const buyerRequest = await this.buyerRequestRepository.findOne({ where: { id }, relations: ['user'], diff --git a/src/modules/files/tests/file.controller.spec.ts b/src/modules/files/tests/file.controller.spec.ts index c0e9786..e8ecaaf 100644 --- a/src/modules/files/tests/file.controller.spec.ts +++ b/src/modules/files/tests/file.controller.spec.ts @@ -87,13 +87,10 @@ describe('FileController', () => { const mockUser = { id: 1, walletAddress: '0x123', + role: [Role.USER], name: 'Test User', email: 'test@example.com', password: 'hashed_password', - location: 'Test City', - country: 'Test Country', - buyerData: {}, - sellerData: null, orders: [], userRoles: [], notifications: [], diff --git a/src/modules/files/tests/file.service.spec.ts b/src/modules/files/tests/file.service.spec.ts index 868f5c8..ff188c7 100644 --- a/src/modules/files/tests/file.service.spec.ts +++ b/src/modules/files/tests/file.service.spec.ts @@ -52,15 +52,10 @@ describe('FileService', () => { name: 'Test User', email: 'test@example.com', password: 'hashed_password', - location: 'Test City', - country: 'Test Country', - buyerData: {}, - sellerData: null, orders: [], userRoles: [], notifications: [], wishlist: [], - stores: [], createdAt: new Date(), updatedAt: new Date(), }; @@ -113,15 +108,10 @@ describe('FileService', () => { name: 'Test User', email: 'test@example.com', password: 'hashed_password', - location: 'Test City', - country: 'Test Country', - buyerData: {}, - sellerData: null, orders: [], userRoles: [], notifications: [], wishlist: [], - stores: [], createdAt: new Date(), updatedAt: new Date(), }; @@ -176,15 +166,10 @@ describe('FileService', () => { name: 'Test User', email: 'test@example.com', password: 'hashed_password', - location: 'Test City', - country: 'Test Country', - buyerData: {}, - sellerData: null, orders: [], userRoles: [], notifications: [], wishlist: [], - stores: [], createdAt: new Date(), updatedAt: new Date(), }; @@ -232,15 +217,10 @@ describe('FileService', () => { name: 'Test User', email: 'test@example.com', password: 'hashed_password', - location: 'Test City', - country: 'Test Country', - buyerData: {}, - sellerData: null, orders: [], userRoles: [], notifications: [], wishlist: [], - stores: [], createdAt: new Date(), updatedAt: new Date(), }; @@ -303,10 +283,6 @@ describe('FileService', () => { name: 'Test User', email: 'test@example.com', password: 'hashed_password', - location: 'Test City', - country: 'Test Country', - buyerData: {}, - sellerData: null, orders: [], userRoles: [], notifications: [], @@ -347,10 +323,6 @@ describe('FileService', () => { name: 'Test User', email: 'test@example.com', password: 'hashed_password', - location: 'Test City', - country: 'Test Country', - buyerData: {}, - sellerData: null, orders: [], userRoles: [], notifications: [], diff --git a/src/modules/files/tests/test-utils.ts b/src/modules/files/tests/test-utils.ts index 3a37f3e..c881292 100644 --- a/src/modules/files/tests/test-utils.ts +++ b/src/modules/files/tests/test-utils.ts @@ -4,10 +4,6 @@ export const mockUser = { walletAddress: '0x123456789abcdef', name: 'Test User', email: 'test@example.com', - location: 'Test City', - country: 'Test Country', - buyerData: {}, - sellerData: null, }; // Helper function to create mock file objects for testing diff --git a/src/modules/notifications/dto/notification.dto.ts b/src/modules/notifications/dto/notification.dto.ts index 16fc687..85e48d6 100644 --- a/src/modules/notifications/dto/notification.dto.ts +++ b/src/modules/notifications/dto/notification.dto.ts @@ -15,7 +15,7 @@ export class NotificationDto { @IsOptional() @IsObject() - payload?: Record; + payload?: Record; } export class UserNotificationDto extends NotificationDto { diff --git a/src/modules/offers/services/offers.service.ts b/src/modules/offers/services/offers.service.ts index a39f1d3..ca30d01 100644 --- a/src/modules/offers/services/offers.service.ts +++ b/src/modules/offers/services/offers.service.ts @@ -10,7 +10,6 @@ import { Offer, OfferStatus } from '../entities/offer.entity'; import { CreateOfferDto } from '../dto/create-offer.dto'; import { UpdateOfferDto } from '../dto/update-offer.dto'; import { BuyerRequest, BuyerRequestStatus } from '../../buyer-requests/entities/buyer-request.entity'; -import { User } from '../../users/entities/user.entity'; @Injectable() export class OffersService { @@ -21,76 +20,10 @@ export class OffersService { @InjectRepository(BuyerRequest) private buyerRequestRepository: Repository, - @InjectRepository(User) - private userRepository: Repository, - private dataSource: DataSource ) {} - /** - * Validates that the user's wallet address matches the expected wallet for seller operations. - * This is crucial for preventing unauthorized contract calls. - */ - private async validateSellerWalletOwnership( - userId: number, - expectedWalletAddress?: string - ): Promise { - const user = await this.userRepository.findOne({ - where: { id: userId }, - }); - - if (!user) { - throw new ForbiddenException('User not found'); - } - - if (!user.walletAddress) { - throw new ForbiddenException('User wallet address not found'); - } - - // If an expected wallet address is provided, validate it matches - if (expectedWalletAddress && user.walletAddress !== expectedWalletAddress) { - throw new ForbiddenException( - 'Wallet address mismatch: You can only perform operations with your own wallet address' - ); - } - } - - /** - * Validates that a user can only modify offers they own (wallet ownership validation). - */ - private async validateOfferOwnership(offerId: string, userId: number): Promise { - const offer = await this.offerRepository.findOne({ - where: { id: offerId }, - relations: ['seller'], - }); - - if (!offer) { - throw new NotFoundException('Offer not found'); - } - - if (offer.sellerId !== userId) { - throw new ForbiddenException('You can only modify your own offers'); - } - - // Additional wallet validation to prevent contract call issues - const user = await this.userRepository.findOne({ where: { id: userId } }); - if (!user) { - throw new ForbiddenException('User not found'); - } - - if (offer.seller.walletAddress !== user.walletAddress) { - throw new ForbiddenException( - 'Wallet ownership mismatch: This offer belongs to a different wallet' - ); - } - - return offer; - } - async create(createOfferDto: CreateOfferDto, sellerId: number): Promise { - // Validate seller wallet ownership to prevent unauthorized contract calls - await this.validateSellerWalletOwnership(sellerId); - const buyerRequest = await this.buyerRequestRepository.findOne({ where: { id: createOfferDto.buyerRequestId }, }); @@ -214,8 +147,11 @@ export class OffersService { } async update(id: string, updateOfferDto: UpdateOfferDto, userId: number): Promise { - // Validate wallet ownership before allowing updates that could trigger contract calls - const offer = await this.validateOfferOwnership(id, userId); + const offer = await this.findOne(id); + + if (offer.sellerId !== userId) { + throw new ForbiddenException('You can only update your own offers'); + } if (offer.status !== OfferStatus.PENDING) { throw new BadRequestException('Can only update pending offers'); @@ -226,8 +162,11 @@ export class OffersService { } async remove(id: string, userId: number): Promise { - // Validate wallet ownership before allowing deletions that could affect contracts - const offer = await this.validateOfferOwnership(id, userId); + const offer = await this.findOne(id); + + if (offer.sellerId !== userId) { + throw new ForbiddenException('You can only delete your own offers'); + } if (offer.status === OfferStatus.ACCEPTED) { throw new BadRequestException('Cannot delete accepted offers'); @@ -260,67 +199,7 @@ export class OffersService { return { offers, total }; } - /** - * Validates that a buyer user can only confirm purchases for their own requests (wallet ownership). - */ - private async validateBuyerWalletOwnership( - offerId: string, - buyerId: string - ): Promise<{ offer: Offer; buyerUser: User }> { - const offer = await this.offerRepository.findOne({ - where: { id: offerId }, - relations: ['buyerRequest', 'buyerRequest.user'], - }); - - if (!offer) { - throw new NotFoundException('Offer not found'); - } - - if (offer.buyerRequest.userId.toString() !== buyerId) { - throw new ForbiddenException('You are not authorized to confirm this offer'); - } - - // Get the buyer user to validate wallet ownership - const buyerUser = await this.userRepository.findOne({ - where: { id: parseInt(buyerId) }, - }); - - if (!buyerUser) { - throw new ForbiddenException('Buyer not found'); - } - - // Validate that the authenticated buyer's wallet matches the request owner's wallet - if (offer.buyerRequest.user.walletAddress !== buyerUser.walletAddress) { - throw new ForbiddenException( - 'Wallet ownership mismatch: You can only confirm purchases with your own wallet' - ); - } - - return { offer, buyerUser }; - } - async confirmPurchase(offerId: string, buyerId: string): Promise { - // Validate buyer wallet ownership before confirming purchase (could trigger contract calls) - const { offer } = await this.validateBuyerWalletOwnership(offerId, buyerId); - - return this.dataSource.transaction(async (manager) => { - if (offer.wasPurchased) { - throw new BadRequestException('This offer has already been confirmed as purchased'); - } - - offer.wasPurchased = true; - await manager.save(offer); - - if (offer.buyerRequest.status !== BuyerRequestStatus.FULFILLED) { - offer.buyerRequest.status = BuyerRequestStatus.FULFILLED; - await manager.save(offer.buyerRequest); - } - - return offer; - }); - } -} -} return this.dataSource.transaction(async (manager) => { const offer = await manager.findOne(Offer, { where: { id: offerId }, diff --git a/src/modules/orders/dto/order.dto.ts b/src/modules/orders/dto/order.dto.ts index d516d73..c2123e8 100644 --- a/src/modules/orders/dto/order.dto.ts +++ b/src/modules/orders/dto/order.dto.ts @@ -1,5 +1,5 @@ import { Expose, Type } from 'class-transformer'; -import { OrderStatus, OnchainStatus } from '../entities/order.entity'; +import { OrderStatus } from '../entities/order.entity'; export class OrderItemDto { @Expose() @@ -28,15 +28,6 @@ export class OrderDto { @Expose() total_price: number; - @Expose() - escrow_contract_id?: string; - - @Expose() - payment_tx_hash?: string; - - @Expose() - onchain_status?: OnchainStatus; - @Expose() created_at: Date; diff --git a/src/modules/orders/entities/order-item.entity.ts b/src/modules/orders/entities/order-item.entity.ts index f49a2be..205c267 100644 --- a/src/modules/orders/entities/order-item.entity.ts +++ b/src/modules/orders/entities/order-item.entity.ts @@ -2,12 +2,6 @@ import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 't import { Order } from './order.entity'; import { Product } from '../../products/entities/product.entity'; -export enum OrderItemStatus { - ACTIVE = 'ACTIVE', - DISPUTED = 'DISPUTED', - COMPLETED = 'COMPLETED', -} - @Entity('order_items') export class OrderItem { @PrimaryGeneratedColumn('uuid') @@ -25,13 +19,6 @@ export class OrderItem { @Column({ type: 'decimal', precision: 10, scale: 2 }) price: number; - - @Column({ type: 'varchar', length: 255, nullable: true }) - milestone: string | null; - - @Column({ type: 'enum', enum: OrderItemStatus, default: OrderItemStatus.ACTIVE }) - status: OrderItemStatus; - @ManyToOne(() => Order, (order) => order.order_items) @JoinColumn({ name: 'order_id' }) order: Order; diff --git a/src/modules/orders/entities/order.entity.ts b/src/modules/orders/entities/order.entity.ts index 2cd9c42..e63dc59 100644 --- a/src/modules/orders/entities/order.entity.ts +++ b/src/modules/orders/entities/order.entity.ts @@ -17,16 +17,6 @@ export enum OrderStatus { CANCELLED = 'CANCELLED', } -export enum OnchainStatus { - PENDING = 'PENDING', - ESCROW_CREATED = 'ESCROW_CREATED', - PAYMENT_RECEIVED = 'PAYMENT_RECEIVED', - DELIVERED = 'DELIVERED', - COMPLETED = 'COMPLETED', - DISPUTED = 'DISPUTED', - REFUNDED = 'REFUNDED', -} - @Entity('orders') export class Order { @PrimaryGeneratedColumn('uuid') @@ -45,19 +35,6 @@ export class Order { @Column({ type: 'decimal', precision: 10, scale: 2 }) total_price: number; - @Column({ type: 'varchar', nullable: true }) - escrow_contract_id?: string; - - @Column({ type: 'varchar', nullable: true }) - payment_tx_hash?: string; - - @Column({ - type: 'enum', - enum: OnchainStatus, - nullable: true, - }) - onchain_status?: OnchainStatus; - @CreateDateColumn() created_at: Date; diff --git a/src/modules/products/products.module.ts b/src/modules/products/products.module.ts index 721458d..c2e632c 100644 --- a/src/modules/products/products.module.ts +++ b/src/modules/products/products.module.ts @@ -4,10 +4,9 @@ import { ProductController } from './controllers/product.controller'; import { ProductService } from './services/product.service'; import { Product } from './entities/product.entity'; import { SharedModule } from '../shared/shared.module'; -import { AppCacheModule } from '../../cache/cache.module'; @Module({ - imports: [TypeOrmModule.forFeature([Product]), SharedModule, AppCacheModule], + imports: [TypeOrmModule.forFeature([Product]), SharedModule], controllers: [ProductController], providers: [ProductService], exports: [ProductService], diff --git a/src/modules/products/services/product.service.ts b/src/modules/products/services/product.service.ts index d53e57f..fc5ec4a 100644 --- a/src/modules/products/services/product.service.ts +++ b/src/modules/products/services/product.service.ts @@ -4,8 +4,6 @@ import { ProductType } from '../../productTypes/entities/productTypes.entity'; import AppDataSource from '../../../config/ormconfig'; import { AppDataSource as DatabaseAppDataSource } from '../../../config/database'; import { NotFoundError } from '../../../utils/errors'; -import { CacheService } from '../../../cache/cache.service'; -import { Cacheable, CacheInvalidate } from '../../../cache/decorators/cache.decorator'; export interface ProductFilters { category?: number; @@ -46,7 +44,7 @@ export class ProductService { private repository: Repository; private productRepository: Repository; - constructor(private cacheService: CacheService) { + constructor() { this.repository = AppDataSource.getRepository(Product); this.productRepository = DatabaseAppDataSource.getRepository(Product); } @@ -63,17 +61,12 @@ export class ProductService { try { const response = await this.repository.save(product); if (!response?.id) throw new Error('Database error'); - - // Invalidate product cache after creation - await this.cacheService.invalidateEntity('product'); - return response; } catch (error) { throw new Error('Database error'); } } - @Cacheable({ key: 'products', entity: 'product', action: 'list' }) async getAll(filters?: { category?: number; minPrice?: number; @@ -139,39 +132,23 @@ export class ProductService { return await query.getMany(); } - @Cacheable({ key: 'product', entity: 'product', action: 'detail' }) async getById(id: number): Promise { return await this.repository.findOne({ where: { id }, relations: ['productType', 'variants'] }); } - @CacheInvalidate('product') async update(id: number, data: Partial): Promise { const product = await this.getById(id); if (!product) return null; Object.assign(product, data); - const updatedProduct = await this.repository.save(product); - - // Invalidate specific product cache - await this.cacheService.delete('product', 'detail', { id }); - - return updatedProduct; + return await this.repository.save(product); } - @CacheInvalidate('product') async delete(id: number): Promise { const result = await this.repository.delete(id); - - if (result.affected === 1) { - // Invalidate specific product cache - await this.cacheService.delete('product', 'detail', { id }); - return true; - } - - return false; + return result.affected === 1; } - @Cacheable({ key: 'products', entity: 'product', action: 'paginated' }) async getAllProducts( options: GetAllProductsOptions ): Promise<{ products: Product[]; total: number }> { @@ -202,7 +179,6 @@ export class ProductService { return { products, total }; } - @Cacheable({ key: 'product', entity: 'product', action: 'detail' }) async getProductById(id: number): Promise { const product = await this.productRepository.findOne({ where: { id } }); if (!product) { @@ -211,36 +187,19 @@ export class ProductService { return product; } - @CacheInvalidate('product') async createProduct(data: CreateProductData): Promise { const product = this.productRepository.create(data); - const savedProduct = await this.productRepository.save(product); - - // Invalidate product list cache - await this.cacheService.invalidateAction('product', 'list'); - await this.cacheService.invalidateAction('product', 'paginated'); - - return savedProduct; + return this.productRepository.save(product); } - @CacheInvalidate('product') async updateProduct(id: number, data: UpdateProductData): Promise { const product = await this.getProductById(id); Object.assign(product, data); - const updatedProduct = await this.productRepository.save(product); - - // Invalidate specific product cache - await this.cacheService.delete('product', 'detail', { id }); - - return updatedProduct; + return this.productRepository.save(product); } - @CacheInvalidate('product') async deleteProduct(id: number): Promise { const product = await this.getProductById(id); await this.productRepository.remove(product); - - // Invalidate specific product cache - await this.cacheService.delete('product', 'detail', { id }); } } diff --git a/src/modules/reviews/controllers/review.controller.ts b/src/modules/reviews/controllers/review.controller.ts index a82ca7e..3fa1336 100644 --- a/src/modules/reviews/controllers/review.controller.ts +++ b/src/modules/reviews/controllers/review.controller.ts @@ -13,7 +13,7 @@ export class ReviewController { async createReview(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { - const userId = req.user.id; + const userId = Number(req.user.id); if (!userId) { throw new BadRequestError('User ID is required'); } @@ -78,7 +78,7 @@ export class ReviewController { async deleteReview(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { - const userId = req.user.id; + const userId = Number(req.user.id); if (!userId) { throw new BadRequestError('User ID is required'); } diff --git a/src/modules/reviews/dto/review.dto.ts b/src/modules/reviews/dto/review.dto.ts index 02f1311..d51f71f 100644 --- a/src/modules/reviews/dto/review.dto.ts +++ b/src/modules/reviews/dto/review.dto.ts @@ -7,7 +7,7 @@ export class CreateReviewDTO { export class ReviewResponseDTO { id: string; - userId: string; + userId: number; productId: number; rating: number; comment?: string; diff --git a/src/modules/reviews/entities/review.entity.ts b/src/modules/reviews/entities/review.entity.ts index aa8f687..48b5e90 100644 --- a/src/modules/reviews/entities/review.entity.ts +++ b/src/modules/reviews/entities/review.entity.ts @@ -16,8 +16,8 @@ export class Review { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ name: 'userId', type: 'uuid' }) - userId: string; + @Column({ name: 'userId' }) + userId: number; @ManyToOne(() => User) @JoinColumn({ name: 'userId' }) diff --git a/src/modules/reviews/services/review.service.ts b/src/modules/reviews/services/review.service.ts index 1e02e68..168228d 100644 --- a/src/modules/reviews/services/review.service.ts +++ b/src/modules/reviews/services/review.service.ts @@ -18,7 +18,7 @@ export class ReviewService { } async createReview( - userId: string, + userId: number, productId: number, rating: number, comment?: string @@ -33,7 +33,7 @@ export class ReviewService { } try { - await this.userService.getUserById(userId); + await this.userService.getUserById(String(userId)); } catch (error) { throw new NotFoundError(`User with ID ${userId} not found`); } @@ -92,7 +92,7 @@ export class ReviewService { }; } - async deleteReview(userId: string, reviewId: string): Promise { + async deleteReview(userId: number, reviewId: string): Promise { const review = await this.repository.findOne({ where: { id: reviewId }, }); diff --git a/src/modules/shared/middleware/auth.middleware.ts b/src/modules/shared/middleware/auth.middleware.ts index 087f25e..7abc9fe 100644 --- a/src/modules/shared/middleware/auth.middleware.ts +++ b/src/modules/shared/middleware/auth.middleware.ts @@ -37,9 +37,9 @@ export class AuthMiddleware implements NestMiddleware { return Role.SELLER; case 'buyer': case 'user': - return Role.BUYER; + return Role.USER; default: - return Role.BUYER; + return Role.USER; } } @@ -61,9 +61,7 @@ export class AuthMiddleware implements NestMiddleware { } const userRoles = await this.roleService.getUserRoles(decoded.id); - // Map all user roles to Role enum values - const mappedRoles = userRoles.map(ur => this.mapRoleToEnum(ur.name)); - req.user = { ...decoded, role: mappedRoles }; + req.user = { ...decoded, role: userRoles.map((role) => this.mapRoleToEnum(role.name)) }; next(); } catch (error) { @@ -82,7 +80,7 @@ export const requireRole = ( ): ((req: AuthenticatedRequest, res: Response, next: NextFunction) => void) => { return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { const requiredRole = new AuthMiddleware(null, null).mapRoleToEnum(roleName); - if (!req.user || !req.user.role.some(role => role === requiredRole)) { + if (!req.user || !req.user.role.includes(requiredRole)) { throw new ReferenceError('Insufficient permissions'); } next(); diff --git a/src/modules/shared/middleware/session.middleware.ts b/src/modules/shared/middleware/session.middleware.ts index babd235..a1a5ae3 100644 --- a/src/modules/shared/middleware/session.middleware.ts +++ b/src/modules/shared/middleware/session.middleware.ts @@ -39,7 +39,7 @@ export const sessionMiddleware = async (req: Request, res: Response, next: NextF id: user.id, walletAddress: user.walletAddress, role: user.userRoles.map((ur) => ur.role.name as Role), - } as any; + }; next(); } catch (error) { diff --git a/src/modules/shared/types/auth-request.type.ts b/src/modules/shared/types/auth-request.type.ts index 2c5cc37..f1f5a27 100644 --- a/src/modules/shared/types/auth-request.type.ts +++ b/src/modules/shared/types/auth-request.type.ts @@ -8,10 +8,6 @@ export interface AuthenticatedRequest extends Request { name?: string; email?: string; role: Role[]; - location?: string; - country?: string; - buyerData?: any; - sellerData?: any; createdAt?: Date; updatedAt?: Date; }; diff --git a/src/modules/users/controllers/user.controller.ts b/src/modules/users/controllers/user.controller.ts index e82beea..4369651 100644 --- a/src/modules/users/controllers/user.controller.ts +++ b/src/modules/users/controllers/user.controller.ts @@ -10,6 +10,7 @@ import { UseGuards, HttpStatus, HttpCode, + ParseIntPipe, } from '@nestjs/common'; import { Request, Response } from 'express'; import { UserService } from '../services/user.service'; @@ -22,14 +23,11 @@ import { Roles } from '../../auth/decorators/roles.decorator'; import { Role } from '../../../types/role'; interface UserResponse { + id: number; walletAddress: string; name: string; email: string; role: string; - location?: string; - country?: string; - buyerData?: any; - sellerData?: any; createdAt?: Date; updatedAt?: Date; } @@ -62,7 +60,6 @@ export class UserController { */ @Post() @HttpCode(HttpStatus.CREATED) - async createUser( @Body() registerDto: RegisterUserDto, @Res({ passthrough: true }) res: Response @@ -72,10 +69,6 @@ export class UserController { role: registerDto.role, name: registerDto.name, email: registerDto.email, - location: registerDto.location, - country: registerDto.country, - buyerData: registerDto.buyerData, - sellerData: registerDto.sellerData, }); // Set JWT token in HttpOnly cookie @@ -90,14 +83,11 @@ export class UserController { success: true, data: { user: { + id: result.user.id, walletAddress: result.user.walletAddress, name: result.user.name, email: result.user.email, role: result.user.userRoles?.[0]?.role?.name || 'buyer', - location: result.user.location, - country: result.user.country, - buyerData: result.user.buyerData, - sellerData: result.user.sellerData, }, expiresIn: result.expiresIn, }, @@ -106,74 +96,68 @@ export class UserController { /** * Update user information - * PUT /users/update/:walletAddress + * PUT /users/update/:id */ - @Put('update/:walletAddress') + @Put('update/:id') @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) async updateUser( - @Param('walletAddress') walletAddress: string, + @Param('id', ParseIntPipe) userId: number, @Body() updateDto: UpdateUserDto, @Req() req: Request ): Promise { - const currentUserWalletAddress = req.user?.walletAddress; + const currentUserId = req.user?.id; const currentUserRole = req.user?.role?.[0]; // Check if user is updating their own profile or is admin - if (walletAddress !== currentUserWalletAddress && currentUserRole !== 'admin') { + if (userId !== currentUserId && currentUserRole !== 'admin') { throw new UnauthorizedError('You can only update your own profile'); } - const updatedUser = await this.userService.updateUser(walletAddress, updateDto); + const updatedUser = await this.authService.updateUser(userId, updateDto); return { success: true, data: { + id: updatedUser.id, walletAddress: updatedUser.walletAddress, name: updatedUser.name, email: updatedUser.email, role: updatedUser.userRoles?.[0]?.role?.name || 'buyer', - location: updatedUser.location, - country: updatedUser.country, - buyerData: updatedUser.buyerData, - sellerData: updatedUser.sellerData, updatedAt: updatedUser.updatedAt, }, }; } /** - * Get user by wallet address (admin only or own profile) - * GET /users/:walletAddress + * Get user by ID (admin only or own profile) + * GET /users/:id */ - @Get(':walletAddress') + @Get(':id') @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) - async getUserByWalletAddress( - @Param('walletAddress') walletAddress: string, + async getUserById( + @Param('id', ParseIntPipe) userId: number, @Req() req: Request ): Promise { - const currentUserWalletAddress = req.user?.walletAddress; + const currentUserId = req.user?.id; const currentUserRole = req.user?.role?.[0]; // Check if user is accessing their own profile or is admin - if (walletAddress !== currentUserWalletAddress && currentUserRole !== 'admin') { + if (userId !== currentUserId && currentUserRole !== 'admin') { throw new UnauthorizedError('Access denied'); } - const user = await this.userService.getUserByWalletAddress(walletAddress); + const user = await this.userService.getUserById(String(userId)); return { success: true, data: { + id: user.id, walletAddress: user.walletAddress, name: user.name, email: user.email, role: user.userRoles?.[0]?.role?.name || 'buyer', - location: user.location, - country: user.country, - buyerData: user.buyerData, - sellerData: user.sellerData, createdAt: user.createdAt, updatedAt: user.updatedAt, }, @@ -194,14 +178,11 @@ export class UserController { return { success: true, data: users.map((user) => ({ + id: user.id, walletAddress: user.walletAddress, name: user.name, email: user.email, role: user.userRoles?.[0]?.role?.name || 'buyer', - location: user.location, - country: user.country, - buyerData: user.buyerData, - sellerData: user.sellerData, createdAt: user.createdAt, updatedAt: user.updatedAt, })), diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 6d9b54e..f191c38 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -5,51 +5,25 @@ import { OneToMany, CreateDateColumn, UpdateDateColumn, - Index, } from 'typeorm'; import { Order } from '../../orders/entities/order.entity'; import { UserRole } from '../../auth/entities/user-role.entity'; import { Notification } from '../../notifications/entities/notification.entity'; import { Wishlist } from '../../wishlist/entities/wishlist.entity'; -import { CountryCode } from '../enums/country-code.enum'; -import { Store } from '../../stores/entities/store.entity'; @Entity('users') export class User { @PrimaryGeneratedColumn() id: number; - + @Column({ unique: true, nullable: true }) email?: string; - + @Column({ nullable: true }) name?: string; - + @Column({ unique: true }) - @Index() walletAddress: string; - - @Column({ unique: true, nullable: true }) - payoutWallet?: string; - - @Column({ default: false }) - sellerOnchainRegistered: boolean; - - - @Column({ length: 2, nullable: true, enum: CountryCode }) - country?: string; - - @Column({ nullable: true }) - location?: string; - - @Column({ nullable: true }) - country?: string; - - @Column({ type: 'json', nullable: true }) - buyerData?: any; - - @Column({ type: 'json', nullable: true }) - sellerData?: any; @OneToMany(() => Order, (order) => order.user) orders: Order[]; @@ -63,9 +37,6 @@ export class User { @OneToMany(() => Wishlist, (wishlist) => wishlist.user) wishlist: Wishlist[]; - @OneToMany(() => Store, (store) => store.seller) - stores: Store[]; - @CreateDateColumn() createdAt: Date; diff --git a/src/modules/users/services/user.service.ts b/src/modules/users/services/user.service.ts index a033c23..f82ab33 100644 --- a/src/modules/users/services/user.service.ts +++ b/src/modules/users/services/user.service.ts @@ -12,10 +12,6 @@ export class UserService { name?: string; email?: string; role: 'buyer' | 'seller' | 'admin'; - location?: string; - country?: string; - buyerData?: any; - sellerData?: any; }): Promise { const existing = await this.userRepository.findOne({ where: { walletAddress: data.walletAddress }, @@ -24,34 +20,14 @@ export class UserService { throw new BadRequestError('Wallet address already registered'); } - // Validate role-specific data - if (data.role === 'buyer' && data.buyerData === undefined) { - throw new BadRequestError('Buyer data is required for buyer role'); - } - if (data.role === 'seller' && data.sellerData === undefined) { - throw new BadRequestError('Seller data is required for seller role'); - } - - // Validate that buyers can't have seller data and sellers can't have buyer data - if (data.role === 'buyer' && data.sellerData !== undefined) { - throw new BadRequestError('Buyers cannot have seller data'); - } - if (data.role === 'seller' && data.buyerData !== undefined) { - throw new BadRequestError('Sellers cannot have buyer data'); - } - const user = this.userRepository.create({ walletAddress: data.walletAddress, name: data.name, email: data.email, - location: data.location, - country: data.country, - buyerData: data.buyerData, - sellerData: data.sellerData, }); const saved = await this.userRepository.save(user); - // assign role to user_roles table + // assign role const roleRepo = AppDataSource.getRepository(Role); const userRoleRepo = AppDataSource.getRepository(UserRole); const role = await roleRepo.findOne({ where: { name: data.role } }); @@ -67,20 +43,9 @@ export class UserService { return saved; } - async getUserByWalletAddress(walletAddress: string): Promise { - const user = await this.userRepository.findOne({ - where: { walletAddress }, - relations: ['userRoles', 'userRoles.role'], - }); - if (!user) { - throw new BadRequestError('User not found'); - } - return user; - } - async getUserById(id: string): Promise { const user = await this.userRepository.findOne({ - where: { id }, + where: { id: parseInt(id) }, relations: ['userRoles', 'userRoles.role'], }); if (!user) { @@ -93,67 +58,7 @@ export class UserService { return this.userRepository.find({ relations: ['userRoles', 'userRoles.role'] }); } - /** - * Update user using walletAddress as primary identifier - */ - async updateUser( - walletAddress: string, - data: { - name?: string; - email?: string; - location?: string; - country?: string; - buyerData?: any; - sellerData?: any; - }, - ): Promise { - const user = await this.getUserByWalletAddress(walletAddress); - - if (data.email) { - const existingUser = await this.userRepository.findOne({ where: { email: data.email } }); - if (existingUser && existingUser.id !== user.id) { - throw new BadRequestError('Email already in use'); - } - user.email = data.email; - } - - if (data.name) { - user.name = data.name; - } - - if (data.location !== undefined) { - user.location = data.location; - } - - if (data.country !== undefined) { - user.country = data.country; - } - - if (data.buyerData !== undefined) { - user.buyerData = data.buyerData; - } - - if (data.sellerData !== undefined) { - user.sellerData = data.sellerData; - } - - return this.userRepository.save(user); - } - - /** - * Compat method: Update by user ID (mantiene compatibilidad con develop) - */ - async updateUserById( - id: string, - data: { - name?: string; - email?: string; - location?: string; - country?: string; - buyerData?: any; - sellerData?: any; - }, - ): Promise { + async updateUser(id: string, data: { name?: string; email?: string }): Promise { const user = await this.getUserById(id); if (data.email) { @@ -168,28 +73,12 @@ export class UserService { user.name = data.name; } - if (data.location !== undefined) { - user.location = data.location; - } - - if (data.country !== undefined) { - user.country = data.country; - } - - if (data.buyerData !== undefined) { - user.buyerData = data.buyerData; - } - - if (data.sellerData !== undefined) { - user.sellerData = data.sellerData; - } - return this.userRepository.save(user); } - async getUserOrders(walletAddress: string): Promise { + async getUserOrders(id: string): Promise { const user = await this.userRepository.findOne({ - where: { walletAddress }, + where: { id: parseInt(id) }, relations: ['orders'], }); @@ -200,9 +89,9 @@ export class UserService { return user.orders; } - async getUserWishlist(walletAddress: string): Promise { + async getUserWishlist(id: string): Promise { const user = await this.userRepository.findOne({ - where: { walletAddress }, + where: { id: parseInt(id) }, relations: ['wishlist'], }); diff --git a/src/modules/wishlist/common/types/auth-request.type.ts b/src/modules/wishlist/common/types/auth-request.type.ts index 21b6e96..6822d2c 100644 --- a/src/modules/wishlist/common/types/auth-request.type.ts +++ b/src/modules/wishlist/common/types/auth-request.type.ts @@ -8,10 +8,6 @@ export interface AuthRequest extends Request { name?: string; email?: string; role: Role[]; - location?: string; - country?: string; - buyerData?: any; - sellerData?: any; createdAt?: Date; updatedAt?: Date; }; diff --git a/src/modules/wishlist/tests/wishlist.controller.spec.ts b/src/modules/wishlist/tests/wishlist.controller.spec.ts index 1686dcc..84ca350 100644 --- a/src/modules/wishlist/tests/wishlist.controller.spec.ts +++ b/src/modules/wishlist/tests/wishlist.controller.spec.ts @@ -82,7 +82,7 @@ describe('WishlistController', () => { user: { id: userId, walletAddress: 'test-wallet', - role: [Role.BUYER], + role: [Role.USER], }, }) as unknown as AuthRequest; @@ -99,7 +99,7 @@ describe('WishlistController', () => { user: { id: userId, walletAddress: 'test-wallet', - role: [Role.BUYER], + role: [Role.USER], }, }) as unknown as AuthRequest; @@ -119,7 +119,7 @@ describe('WishlistController', () => { user: { id: userId, walletAddress: 'test-wallet', - role: [Role.BUYER], + role: [Role.USER], }, }) as unknown as AuthRequest; @@ -137,7 +137,7 @@ describe('WishlistController', () => { user: { id: userId, walletAddress: 'test-wallet', - role: [Role.BUYER], + role: [Role.USER], }, }) as unknown as AuthRequest; const wishlistItems = [new Wishlist()]; diff --git a/src/types/auth-request.type.ts b/src/types/auth-request.type.ts index 924d2c0..8788764 100644 --- a/src/types/auth-request.type.ts +++ b/src/types/auth-request.type.ts @@ -2,7 +2,7 @@ import { Request } from 'express'; import { Role } from './role'; export interface AppUser { - id: string; // UUID + id: string; walletAddress: string; name?: string; email?: string; @@ -13,7 +13,7 @@ export interface AppUser { export interface AuthenticatedRequest extends Request { user: { - id: string; // UUID + id: string | number; walletAddress: string; name?: string; email?: string; diff --git a/src/types/express.d.ts b/src/types/express.d.ts index db91eb5..af7ff75 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -22,10 +22,6 @@ declare module 'express-serve-static-core' { name?: string; email?: string; role: Role[]; - location?: string; - country?: string; - buyerData?: any; - sellerData?: any; createdAt?: Date; updatedAt?: Date; }; @@ -42,10 +38,6 @@ declare global { name?: string; email?: string; role: Role[]; - location?: string; - country?: string; - buyerData?: any; - sellerData?: any; createdAt?: Date; updatedAt?: Date; } diff --git a/src/types/global-response.type.ts b/src/types/global-response.type.ts index 42768fb..030fe8f 100644 --- a/src/types/global-response.type.ts +++ b/src/types/global-response.type.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class GlobalSuccessResponse { +export class GlobalSuccessResponse { @ApiProperty({ description: 'Success status', example: true @@ -48,7 +48,7 @@ export class GlobalErrorResponse { timestamp?: string; } -export type GlobalResponse = GlobalSuccessResponse | GlobalErrorResponse; +export type GlobalResponse = GlobalSuccessResponse | GlobalErrorResponse; // Helper types for common response patterns export interface PaginatedResponse { diff --git a/tsconfig.json b/tsconfig.json index fcf7e8e..385b548 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,10 +15,11 @@ "@/*": ["src/*"] }, "incremental": true, - "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, + "skipLibCheck": true, + "strict": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, "esModuleInterop": true