diff --git a/package-lock.json b/package-lock.json index 0253097..6237ef2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "volunchain-backend", "version": "1.0.0", "dependencies": { - "@aws-sdk/client-s3": "^3.798.0", "@aws-sdk/s3-request-presigner": "^3.798.0", "@prisma/client": "^6.4.1", "@stellar/stellar-sdk": "^13.3.0", @@ -18,7 +17,6 @@ "@types/swagger-ui-express": "^4.1.7", "@types/uuid": "^10.0.0", "axios": "^1.7.9", - "backblaze-b2": "^1.7.0", "bcryptjs": "^3.0.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -29,7 +27,6 @@ "express-rate-limit": "^7.5.0", "express-validator": "^7.2.0", "jsonwebtoken": "^9.0.2", - "multer": "^1.4.5-lts.2", "node-cron": "^3.0.3", "nodemailer": "^6.10.0", "pdf-lib": "^1.17.1", @@ -133,715 +130,47 @@ "openapi-types": ">=7" } }, - "node_modules/@aws-crypto/crc32": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/crc32c": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", - "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha1-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", - "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-s3": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.798.0.tgz", - "integrity": "sha512-MmVWlRK8n7cAfdtcNK7KAhiSbMt2KBLAcnvIpDx37jpQGjEe/ahQ6xOi9r1JdNaIg4SyMZsbmeLVtvDntwlxqQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha1-browser": "5.2.0", - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.798.0", - "@aws-sdk/credential-provider-node": "3.798.0", - "@aws-sdk/middleware-bucket-endpoint": "3.775.0", - "@aws-sdk/middleware-expect-continue": "3.775.0", - "@aws-sdk/middleware-flexible-checksums": "3.798.0", - "@aws-sdk/middleware-host-header": "3.775.0", - "@aws-sdk/middleware-location-constraint": "3.775.0", - "@aws-sdk/middleware-logger": "3.775.0", - "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-sdk-s3": "3.798.0", - "@aws-sdk/middleware-ssec": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.798.0", - "@aws-sdk/region-config-resolver": "3.775.0", - "@aws-sdk/signature-v4-multi-region": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@aws-sdk/util-endpoints": "3.787.0", - "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.798.0", - "@aws-sdk/xml-builder": "3.775.0", - "@smithy/config-resolver": "^4.1.0", - "@smithy/core": "^3.3.0", - "@smithy/eventstream-serde-browser": "^4.0.2", - "@smithy/eventstream-serde-config-resolver": "^4.1.0", - "@smithy/eventstream-serde-node": "^4.0.2", - "@smithy/fetch-http-handler": "^5.0.2", - "@smithy/hash-blob-browser": "^4.0.2", - "@smithy/hash-node": "^4.0.2", - "@smithy/hash-stream-node": "^4.0.2", - "@smithy/invalid-dependency": "^4.0.2", - "@smithy/md5-js": "^4.0.2", - "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.1", - "@smithy/middleware-retry": "^4.1.1", - "@smithy/middleware-serde": "^4.0.3", - "@smithy/middleware-stack": "^4.0.2", - "@smithy/node-config-provider": "^4.0.2", - "@smithy/node-http-handler": "^4.0.4", - "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.1", - "@smithy/types": "^4.2.0", - "@smithy/url-parser": "^4.0.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.9", - "@smithy/util-defaults-mode-node": "^4.0.9", - "@smithy/util-endpoints": "^3.0.2", - "@smithy/util-middleware": "^4.0.2", - "@smithy/util-retry": "^4.0.2", - "@smithy/util-stream": "^4.2.0", - "@smithy/util-utf8": "^4.0.0", - "@smithy/util-waiter": "^4.0.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.798.0.tgz", - "integrity": "sha512-Si4W7kFflNXC48lr05n2Fc5nrD6whbfgR7c5/7hYSXP52DOqy2kMle+bZx5EkmQ/e/5nAPW0DS4ABeLprVSghw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.798.0", - "@aws-sdk/middleware-host-header": "3.775.0", - "@aws-sdk/middleware-logger": "3.775.0", - "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.798.0", - "@aws-sdk/region-config-resolver": "3.775.0", - "@aws-sdk/types": "3.775.0", - "@aws-sdk/util-endpoints": "3.787.0", - "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.798.0", - "@smithy/config-resolver": "^4.1.0", - "@smithy/core": "^3.3.0", - "@smithy/fetch-http-handler": "^5.0.2", - "@smithy/hash-node": "^4.0.2", - "@smithy/invalid-dependency": "^4.0.2", - "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.1", - "@smithy/middleware-retry": "^4.1.1", - "@smithy/middleware-serde": "^4.0.3", - "@smithy/middleware-stack": "^4.0.2", - "@smithy/node-config-provider": "^4.0.2", - "@smithy/node-http-handler": "^4.0.4", - "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.1", - "@smithy/types": "^4.2.0", - "@smithy/url-parser": "^4.0.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.9", - "@smithy/util-defaults-mode-node": "^4.0.9", - "@smithy/util-endpoints": "^3.0.2", - "@smithy/util-middleware": "^4.0.2", - "@smithy/util-retry": "^4.0.2", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/core": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.798.0.tgz", - "integrity": "sha512-hITxDE4pVkeJqz0LXjQRDgR+noxJ5oOxG38fgmQXjPXsdwVKnNIiMJ5S2WFMVSszU7ebGSyHdPHENQKu6TReVA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.775.0", - "@smithy/core": "^3.3.0", - "@smithy/node-config-provider": "^4.0.2", - "@smithy/property-provider": "^4.0.2", - "@smithy/protocol-http": "^5.1.0", - "@smithy/signature-v4": "^5.1.0", - "@smithy/smithy-client": "^4.2.1", - "@smithy/types": "^4.2.0", - "@smithy/util-middleware": "^4.0.2", - "fast-xml-parser": "4.4.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.798.0.tgz", - "integrity": "sha512-EsfzTEeoaHY1E+g3S6AmC3bF6euZN5SrLcLh5Oxhx5q2qjWUsKEK0fwek+jlt2GH7zB3F9IArV4z+8CsDQdKYw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@smithy/property-provider": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.798.0.tgz", - "integrity": "sha512-bw5TmcJqpBVQlXzkL63545iHQ9mxwQeXTS/rgUQ5rmNNS3yiGDekVZOLXo/Gs4wmt2/59UN/sWIRFxvxDpMQEg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@smithy/fetch-http-handler": "^5.0.2", - "@smithy/node-http-handler": "^4.0.4", - "@smithy/property-provider": "^4.0.2", - "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.1", - "@smithy/types": "^4.2.0", - "@smithy/util-stream": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.798.0.tgz", - "integrity": "sha512-zqWwKhhdf5CVRL6+4vNNTZVHWH9OiiwUWA3ka44jJaAMBRbbryjRedzwkWbgDaL1EbfTbcBZTYzE7N/vK7UUVA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.798.0", - "@aws-sdk/credential-provider-env": "3.798.0", - "@aws-sdk/credential-provider-http": "3.798.0", - "@aws-sdk/credential-provider-process": "3.798.0", - "@aws-sdk/credential-provider-sso": "3.798.0", - "@aws-sdk/credential-provider-web-identity": "3.798.0", - "@aws-sdk/nested-clients": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@smithy/credential-provider-imds": "^4.0.2", - "@smithy/property-provider": "^4.0.2", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.798.0.tgz", - "integrity": "sha512-Mrhl4wS4lMpuw2NCga5/rtQehNfyRs8NUHfvrLK5bZvJbjanrh8QtdRVhrAjw71OwFh3GK49QMByGkUssALJ+g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.798.0", - "@aws-sdk/credential-provider-http": "3.798.0", - "@aws-sdk/credential-provider-ini": "3.798.0", - "@aws-sdk/credential-provider-process": "3.798.0", - "@aws-sdk/credential-provider-sso": "3.798.0", - "@aws-sdk/credential-provider-web-identity": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@smithy/credential-provider-imds": "^4.0.2", - "@smithy/property-provider": "^4.0.2", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.798.0.tgz", - "integrity": "sha512-BbRq8bhCHC94OTRIg5edgGTaWUzBH0h/IZJZ0vERle8A9nfl+5jUplvC8cvh3/8cNgHIRXj5HzlDjeSVe9dySg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@smithy/property-provider": "^4.0.2", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.798.0.tgz", - "integrity": "sha512-MLpQRb7xkqI9w0slEA76QiHGzM0PDMcpVcQG0wFHrpLKkQYjYlD9H3VfxdYGUh+FPOaR1fFpRZb18Gz9MR/2eQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.798.0", - "@aws-sdk/core": "3.798.0", - "@aws-sdk/token-providers": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@smithy/property-provider": "^4.0.2", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.798.0.tgz", - "integrity": "sha512-OWBDy/ZiC0pxLzp1Nhah5jxDZ/onLTjouIVGPyc9E8/KzUJxqQbR6fk43VqhpYdVp/S7yDDbaOpO072RRZJQrw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.798.0", - "@aws-sdk/nested-clients": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@smithy/property-provider": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.775.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.775.0.tgz", - "integrity": "sha512-qogMIpVChDYr4xiUNC19/RDSw/sKoHkAhouS6Skxiy6s27HBhow1L3Z1qVYXuBmOZGSWPU0xiyZCvOyWrv9s+Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.775.0", - "@aws-sdk/util-arn-parser": "3.723.0", - "@smithy/node-config-provider": "^4.0.2", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "@smithy/util-config-provider": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.775.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.775.0.tgz", - "integrity": "sha512-Apd3owkIeUW5dnk3au9np2IdW2N0zc9NjTjHiH+Mx3zqwSrc+m+ANgJVgk9mnQjMzU/vb7VuxJ0eqdEbp5gYsg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.775.0", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.798.0.tgz", - "integrity": "sha512-NR3YdVq2IiVYq5mLn5DpTLFSvz3+G78dE7RtWk6Jv2FCflXiWersU9lg4zisOQxHkwDy10lVlxQUlEJdJk0EQg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@aws-crypto/crc32c": "5.2.0", - "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/node-config-provider": "^4.0.2", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "@smithy/util-middleware": "^4.0.2", - "@smithy/util-stream": "^4.2.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.775.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.775.0.tgz", - "integrity": "sha512-tkSegM0Z6WMXpLB8oPys/d+umYIocvO298mGvcMCncpRl77L9XkvSLJIFzaHes+o7djAgIduYw8wKIMStFss2w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.775.0", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.775.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.775.0.tgz", - "integrity": "sha512-8TMXEHZXZTFTckQLyBT5aEI8fX11HZcwZseRifvBKKpj0RZDk4F0EEYGxeNSPpUQ7n+PRWyfAEnnZNRdAj/1NQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.775.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.775.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.775.0.tgz", - "integrity": "sha512-FaxO1xom4MAoUJsldmR92nT1G6uZxTdNYOFYtdHfd6N2wcNaTuxgjIvqzg5y7QIH9kn58XX/dzf1iTjgqUStZw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.775.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.775.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.775.0.tgz", - "integrity": "sha512-GLCzC8D0A0YDG5u3F5U03Vb9j5tcOEFhr8oc6PDk0k0vm5VwtZOE6LvK7hcCSoAB4HXyOUM0sQuXrbaAh9OwXA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.775.0", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.798.0.tgz", - "integrity": "sha512-UvficjDO/Vrxd1xufR+5ey8NdDUrvIuiEWP/uu4kOkUTXk3IR89F+5UbYWSadhkysbyWN3tui09bWbIbq2Vhag==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@aws-sdk/util-arn-parser": "3.723.0", - "@smithy/core": "^3.3.0", - "@smithy/node-config-provider": "^4.0.2", - "@smithy/protocol-http": "^5.1.0", - "@smithy/signature-v4": "^5.1.0", - "@smithy/smithy-client": "^4.2.1", - "@smithy/types": "^4.2.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.2", - "@smithy/util-stream": "^4.2.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.775.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.775.0.tgz", - "integrity": "sha512-Iw1RHD8vfAWWPzBBIKaojO4GAvQkHOYIpKdAfis/EUSUmSa79QsnXnRqsdcE0mCB0Ylj23yi+ah4/0wh9FsekA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.775.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.798.0.tgz", - "integrity": "sha512-nb3YvLokpu/2meKVH5hGVLNg+hz3IyFCESEJW+SpK7bW/SfaKpukGY1lqwqbf+edl+s20MRXeK/by1rvBChixQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@aws-sdk/util-endpoints": "3.787.0", - "@smithy/core": "^3.3.0", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { + "node_modules/@aws-sdk/core": { "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.798.0.tgz", - "integrity": "sha512-14iBJgg2Qqf74IeUY+z1nP5GIJIBZj8lv9mdpXrHlK8k+FcMXjpHg/B+JguSMhb2sbLeb5N0H8HLJGIRNALVWw==", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.798.0.tgz", + "integrity": "sha512-hITxDE4pVkeJqz0LXjQRDgR+noxJ5oOxG38fgmQXjPXsdwVKnNIiMJ5S2WFMVSszU7ebGSyHdPHENQKu6TReVA==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.798.0", - "@aws-sdk/middleware-host-header": "3.775.0", - "@aws-sdk/middleware-logger": "3.775.0", - "@aws-sdk/middleware-recursion-detection": "3.775.0", - "@aws-sdk/middleware-user-agent": "3.798.0", - "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", - "@aws-sdk/util-endpoints": "3.787.0", - "@aws-sdk/util-user-agent-browser": "3.775.0", - "@aws-sdk/util-user-agent-node": "3.798.0", - "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.3.0", - "@smithy/fetch-http-handler": "^5.0.2", - "@smithy/hash-node": "^4.0.2", - "@smithy/invalid-dependency": "^4.0.2", - "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.1", - "@smithy/middleware-retry": "^4.1.1", - "@smithy/middleware-serde": "^4.0.3", - "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", - "@smithy/node-http-handler": "^4.0.4", + "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", + "@smithy/signature-v4": "^5.1.0", "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", - "@smithy/url-parser": "^4.0.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.9", - "@smithy/util-defaults-mode-node": "^4.0.9", - "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", - "@smithy/util-retry": "^4.0.2", - "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.775.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.775.0.tgz", - "integrity": "sha512-40iH3LJjrQS3LKUJAl7Wj0bln7RFPEvUYKFxtP8a+oKFDO0F65F52xZxIJbPn6sHkxWDAnZlGgdjZXM3p2g5wQ==", + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.798.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.798.0.tgz", + "integrity": "sha512-UvficjDO/Vrxd1xufR+5ey8NdDUrvIuiEWP/uu4kOkUTXk3IR89F+5UbYWSadhkysbyWN3tui09bWbIbq2Vhag==", "license": "Apache-2.0", "dependencies": { + "@aws-sdk/core": "3.798.0", "@aws-sdk/types": "3.775.0", + "@aws-sdk/util-arn-parser": "3.723.0", + "@smithy/core": "^3.3.0", "@smithy/node-config-provider": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/signature-v4": "^5.1.0", + "@smithy/smithy-client": "^4.2.1", "@smithy/types": "^4.2.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.2", + "@smithy/util-stream": "^4.2.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { @@ -884,23 +213,6 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.798.0.tgz", - "integrity": "sha512-iYhNmHXfWLUwcMP9ldb/H+RMRLHZbBUWBgsoQqfb7sl6z24nH0qBJyL+oXHTCVBUYLP20CvUrVkcwlejDzyoRw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/nested-clients": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@smithy/property-provider": "^4.0.2", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@aws-sdk/types": { "version": "3.775.0", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.775.0.tgz", @@ -926,21 +238,6 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.787.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.787.0.tgz", - "integrity": "sha512-fd3zkiOkwnbdbN0Xp9TsP5SWrmv0SpT70YEdbb8wAj2DWQwiCmFszaSs+YCvhoCdmlR3Wl9Spu0pGpSAGKeYvQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.775.0", - "@smithy/types": "^4.2.0", - "@smithy/util-endpoints": "^3.0.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@aws-sdk/util-format-url": { "version": "3.775.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.775.0.tgz", @@ -956,67 +253,6 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.723.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.723.0.tgz", - "integrity": "sha512-Yf2CS10BqK688DRsrKI/EO6B8ff5J86NXe4C+VCysK7UOgN0l1zOTeTukZ3H8Q9tYYX3oaF1961o8vRkFm7Nmw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.775.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.775.0.tgz", - "integrity": "sha512-txw2wkiJmZKVdDbscK7VBK+u+TJnRtlUjRTLei+elZg2ADhpQxfVAQl436FUeIv6AhB/oRHW6/K/EAGXUSWi0A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.775.0", - "@smithy/types": "^4.2.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.798.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.798.0.tgz", - "integrity": "sha512-yncgNd2inI+y5kdfn2i0oBwgCxwdtcVShNNVQ+5b/nuC1Lgjgcb+hmHAeTFMge7vhDP2Md8I+ih6bPMpK79lQQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.798.0", - "@aws-sdk/types": "3.775.0", - "@smithy/node-config-provider": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.775.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.775.0.tgz", - "integrity": "sha512-b9NGO6FKJeLGYnV7Z1yvcP1TNU4dkD5jNsLWOF1/sygZoASaQhNOlaiJ/1OH331YQ1R1oWk38nBb0frsYkDsOQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -3125,241 +2361,54 @@ "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@smithy/abort-controller": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.2.tgz", - "integrity": "sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/chunked-blob-reader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz", - "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/chunked-blob-reader-native": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", - "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-base64": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/config-resolver": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.0.tgz", - "integrity": "sha512-8smPlwhga22pwl23fM5ew4T9vfLUCeFXlcqNOCD5M5h8VmNPNUE9j6bQSuRXpDSV11L/E/SwEBQuW8hr6+nS1A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.0.2", - "@smithy/types": "^4.2.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/core": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.3.0.tgz", - "integrity": "sha512-r6gvs5OfRq/w+9unPm7B3po4rmWaGh0CIL/OwHntGGux7+RhOOZLGuurbeMgWV6W55ZuyMTypJLeH0vn/ZRaWQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/middleware-serde": "^4.0.3", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.2", - "@smithy/util-stream": "^4.2.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.2.tgz", - "integrity": "sha512-32lVig6jCaWBHnY+OEQ6e6Vnt5vDHaLiydGrwYMW9tPqO688hPGTYRamYJ1EptxEC2rAwJrHWmPoKRBl4iTa8w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.0.2", - "@smithy/property-provider": "^4.0.2", - "@smithy/types": "^4.2.0", - "@smithy/url-parser": "^4.0.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-codec": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.2.tgz", - "integrity": "sha512-p+f2kLSK7ZrXVfskU/f5dzksKTewZk8pJLPvER3aFHPt76C2MxD9vNatSfLzzQSQB4FNO96RK4PSXfhD1TTeMQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.2.0", - "@smithy/util-hex-encoding": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.2.tgz", - "integrity": "sha512-CepZCDs2xgVUtH7ZZ7oDdZFH8e6Y2zOv8iiX6RhndH69nlojCALSKK+OXwZUgOtUZEUaZ5e1hULVCHYbCn7pug==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.0.tgz", - "integrity": "sha512-1PI+WPZ5TWXrfj3CIoKyUycYynYJgZjuQo8U+sphneOtjsgrttYybdqESFReQrdWJ+LKt6NEdbYzmmfDBmjX2A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-node": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.2.tgz", - "integrity": "sha512-C5bJ/C6x9ENPMx2cFOirspnF9ZsBVnBMtP6BdPl/qYSuUawdGQ34Lq0dMcf42QTjUZgWGbUIZnz6+zLxJlb9aw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.2.tgz", - "integrity": "sha512-St8h9JqzvnbB52FtckiHPN4U/cnXcarMniXRXTKn0r4b4XesZOGiAyUdj1aXbqqn1icSqBlzzUsCl6nPB018ng==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-codec": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.2.tgz", - "integrity": "sha512-+9Dz8sakS9pe7f2cBocpJXdeVjMopUDLgZs1yWeu7h++WqSbjUYv/JAJwKwXw1HV6gq1jyWjxuyn24E2GhoEcQ==", - "license": "Apache-2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@smithy/protocol-http": "^5.1.0", - "@smithy/querystring-builder": "^4.0.2", - "@smithy/types": "^4.2.0", - "@smithy/util-base64": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "type-detect": "4.0.8" } }, - "node_modules/@smithy/hash-blob-browser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.2.tgz", - "integrity": "sha512-3g188Z3DyhtzfBRxpZjU8R9PpOQuYsbNnyStc/ZVS+9nVX1f6XeNOa9IrAh35HwwIZg+XWk8bFVtNINVscBP+g==", - "license": "Apache-2.0", + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@smithy/chunked-blob-reader": "^5.0.0", - "@smithy/chunked-blob-reader-native": "^4.0.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@smithy/hash-node": { + "node_modules/@smithy/abort-controller": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.2.tgz", - "integrity": "sha512-VnTpYPnRUE7yVhWozFdlxcYknv9UN7CeOqSrMH+V877v4oqtVYuoqhIhtSjmGPvYrYnAkaM61sLMKHvxL138yg==", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.2.tgz", + "integrity": "sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.2.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/hash-stream-node": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.2.tgz", - "integrity": "sha512-POWDuTznzbIwlEXEvvXoPMS10y0WKXK790soe57tFRfvf4zBHyzE529HpZMqmDdwG9MfFflnyzndUQ8j78ZdSg==", + "node_modules/@smithy/core": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.3.0.tgz", + "integrity": "sha512-r6gvs5OfRq/w+9unPm7B3po4rmWaGh0CIL/OwHntGGux7+RhOOZLGuurbeMgWV6W55ZuyMTypJLeH0vn/ZRaWQ==", "license": "Apache-2.0", "dependencies": { + "@smithy/middleware-serde": "^4.0.3", + "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-stream": "^4.2.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -3367,13 +2416,16 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.2.tgz", - "integrity": "sha512-GatB4+2DTpgWPday+mnUkoumP54u/MDM/5u44KF9hIu8jF0uafZtQLcdfIKkIcUNuF/fBojpLEHZS/56JqPeXQ==", + "node_modules/@smithy/fetch-http-handler": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.2.tgz", + "integrity": "sha512-+9Dz8sakS9pe7f2cBocpJXdeVjMopUDLgZs1yWeu7h++WqSbjUYv/JAJwKwXw1HV6gq1jyWjxuyn24E2GhoEcQ==", "license": "Apache-2.0", "dependencies": { + "@smithy/protocol-http": "^5.1.0", + "@smithy/querystring-builder": "^4.0.2", "@smithy/types": "^4.2.0", + "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" }, "engines": { @@ -3392,34 +2444,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/md5-js": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.2.tgz", - "integrity": "sha512-Hc0R8EiuVunUewCse2syVgA2AfSRco3LyAv07B/zCOMa+jpXI9ll+Q21Nc6FAlYPcpNcAXqBzMhNs1CD/pP2bA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.2.tgz", - "integrity": "sha512-hAfEXm1zU+ELvucxqQ7I8SszwQ4znWMbNv6PLMndN83JJN41EPuS93AIyh2N+gJ6x8QFhzSO6b7q2e6oClDI8A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/middleware-endpoint": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.1.tgz", @@ -3439,39 +2463,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/middleware-retry": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.1.tgz", - "integrity": "sha512-mBJOxn9aUYwcBUPQpKv9ifzrCn4EbhPUFguEZv3jB57YOMh0caS4P8HoLvUeNUI1nx4bIVH2SIbogbDfFI9DUA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.0.2", - "@smithy/protocol-http": "^5.1.0", - "@smithy/service-error-classification": "^4.0.2", - "@smithy/smithy-client": "^4.2.1", - "@smithy/types": "^4.2.0", - "@smithy/util-middleware": "^4.0.2", - "@smithy/util-retry": "^4.0.2", - "tslib": "^2.6.2", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@smithy/middleware-serde": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.3.tgz", @@ -3582,18 +2573,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/service-error-classification": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.2.tgz", - "integrity": "sha512-LA86xeFpTKn270Hbkixqs5n73S+LVM0/VZco8dqd+JT75Dyx3Lcw/MraL7ybjmz786+160K8rPOmhsq0SocoJQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/shared-ini-file-loader": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.2.tgz", @@ -3696,18 +2675,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", - "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/util-buffer-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", @@ -3733,54 +2700,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.9.tgz", - "integrity": "sha512-B8j0XsElvyhv6+5hlFf6vFV/uCSyLKcInpeXOGnOImX2mGXshE01RvPoGipTlRpIk53e6UfYj7WdDdgbVfXDZw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.1", - "@smithy/types": "^4.2.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.9.tgz", - "integrity": "sha512-wTDU8P/zdIf9DOpV5qm64HVgGRXvqjqB/fJZTEQbrz3s79JHM/E7XkMm/876Oq+ZLHJQgnXM9QHDo29dlM62eA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/config-resolver": "^4.1.0", - "@smithy/credential-provider-imds": "^4.0.2", - "@smithy/node-config-provider": "^4.0.2", - "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.1", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.2.tgz", - "integrity": "sha512-6QSutU5ZyrpNbnd51zRTL7goojlcnuOB55+F9VBD+j8JpRY50IGamsjlycrmpn8PQkmJucFW8A0LSfXj7jjtLQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/util-hex-encoding": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", @@ -3806,20 +2725,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/util-retry": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.2.tgz", - "integrity": "sha512-Qryc+QG+7BCpvjloFLQrmlSd0RsVRHejRXd78jNO3+oREueCjwG1CCEH1vduw/ZkM1U9TztwIKVIi3+8MJScGg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/service-error-classification": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/util-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.0.tgz", @@ -3864,20 +2769,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/util-waiter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.3.tgz", - "integrity": "sha512-JtaY3FxmD+te+KSI2FJuEcfNC9T/DGGVf551babM7fAaXhjJUt7oSYurH1Devxd2+BOSUACCgt3buinx4UnmEA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/abort-controller": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@sqltools/formatter": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", @@ -4869,12 +3760,6 @@ "node": ">= 6.0.0" } }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "license": "MIT" - }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -4948,18 +3833,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/axios-retry": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", - "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", - "license": "Apache-2.0", - "dependencies": { - "is-retry-allowed": "^2.2.0" - }, - "peerDependencies": { - "axios": "0.x || 1.x" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -5086,20 +3959,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/backblaze-b2": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/backblaze-b2/-/backblaze-b2-1.7.1.tgz", - "integrity": "sha512-ns+oIO0NO5ysIIcs4xn2PfG34GlkGFLUckDWfQcsaCE2THDNRWzc5lWjR3LUrq6DA0E5KUGOhgkAs/q+QrgpNw==", - "license": "MIT", - "dependencies": { - "axios": "^1.9.0", - "axios-retry": "^4.5.0", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=10.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5301,12 +4160,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/bowser": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", - "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -5420,19 +4273,9 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -5935,51 +4778,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/concat-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/concat-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -6038,12 +4836,6 @@ "dev": true, "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -8110,18 +6902,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -8134,12 +6914,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9736,6 +8510,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9883,36 +8658,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/multer": { - "version": "1.4.5-lts.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", - "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", - "license": "MIT", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/multer/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -10735,12 +9480,6 @@ } } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -11973,14 +10712,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -12805,12 +11536,6 @@ "node": ">= 0.6" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, "node_modules/typeorm": { "version": "0.3.21", "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.21.tgz", diff --git a/package.json b/package.json index fe84f03..a896734 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "s3:setup": "ts-node src/utils/setup-s3-bucket.ts" }, "dependencies": { - "@aws-sdk/client-s3": "^3.798.0", "@aws-sdk/s3-request-presigner": "^3.798.0", "@prisma/client": "^6.4.1", "@stellar/stellar-sdk": "^13.3.0", @@ -32,7 +31,6 @@ "@types/swagger-ui-express": "^4.1.7", "@types/uuid": "^10.0.0", "axios": "^1.7.9", - "backblaze-b2": "^1.7.0", "bcryptjs": "^3.0.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -43,7 +41,6 @@ "express-rate-limit": "^7.5.0", "express-validator": "^7.2.0", "jsonwebtoken": "^9.0.2", - "multer": "^1.4.5-lts.2", "node-cron": "^3.0.3", "nodemailer": "^6.10.0", "pdf-lib": "^1.17.1", diff --git a/readme.md b/readme.md index ccdf2a7..cfe8f0b 100644 --- a/readme.md +++ b/readme.md @@ -236,19 +236,6 @@ npm run db:seed --- -## 🔌 Supabase Integration - -This project uses Supabase for external data access and future integrations. - -Update your `.env` file with: - -```bash -SUPABASE_URL=... -SUPABASE_ANON_KEY=... -``` - ---- - ## 📁 Module Overview ### Core Modules diff --git a/src/Server.ts b/src/Server.ts new file mode 100644 index 0000000..2bfc95a --- /dev/null +++ b/src/Server.ts @@ -0,0 +1,120 @@ +import express, { Router, Application, ErrorRequestHandler } from "express"; +import cors from "cors"; +import { Logger } from "./utils"; + +interface ServerOptions { + port?: number; + environment?: string; + routes: Router; + middlewares?: Array<(app: Application) => void>; + errorHandlers?: ErrorRequestHandler[]; + enableCors?: boolean; +} + +export class Server { + public readonly app: Application = express(); + private readonly port: number; + private readonly environment: string; + private readonly routes: Router; + private readonly middlewares: Array<(app: Application) => void>; + private readonly errorHandlers: ErrorRequestHandler[]; + private readonly logger: Logger; + private readonly enableCors: boolean; + + constructor(options: ServerOptions) { + const { + port = 3000, + environment = "development", + routes, + middlewares = [], + errorHandlers = [], + enableCors = true, + } = options; + + this.port = port; + this.environment = environment; + this.routes = routes; + this.middlewares = middlewares; + this.errorHandlers = errorHandlers; + this.enableCors = enableCors; + this.logger = new Logger("VolunChain-Server"); + } + + private setupBaseMiddlewares(): void { + // Middleware for parsing JSON requests + this.app.use(express.json()); + this.app.use(express.urlencoded({ extended: true })); + + // CORS if enabled + if (this.enableCors) { + this.app.use(cors()); + } + } + + private setupCustomMiddlewares(): void { + // Apply custom middlewares in order + this.middlewares.forEach((middleware) => { + middleware(this.app); + }); + } + + private setupRoutes(): void { + // Setup routes + this.app.use(this.routes); + } + + private setupHealthCheck(): void { + // Basic health check route + this.app.get("/", (req, res) => { + res.json({ + message: "VolunChain API is running!", + environment: this.environment, + timestamp: new Date().toISOString(), + }); + }); + } + + private setupErrorHandlers(): void { + // Apply error handlers (must be last) + this.errorHandlers.forEach((errorHandler) => { + this.app.use(errorHandler); + }); + } + + public async start(): Promise { + try { + // Setup middlewares in order + this.setupBaseMiddlewares(); + this.setupCustomMiddlewares(); + this.setupHealthCheck(); + this.setupRoutes(); + this.setupErrorHandlers(); // Error handlers must be last + + // Start server + return new Promise((resolve) => { + this.app.listen(this.port, () => { + this.logger.info( + `Server is running on http://localhost:${this.port}`, + { + port: this.port, + environment: this.environment, + nodeVersion: process.version, + } + ); + resolve(); + }); + }); + } catch (error: unknown) { + this.logger.error("Failed to start server", error ?? "error"); + throw error; + } + } + + public getApp(): Application { + return this.app; + } + + public getPort(): number { + return this.port; + } +} diff --git a/src/config/BcryptAdapter.ts b/src/config/BcryptAdapter.ts new file mode 100644 index 0000000..6d3714c --- /dev/null +++ b/src/config/BcryptAdapter.ts @@ -0,0 +1,11 @@ +import { compareSync, hashSync } from "bcryptjs"; + +export class BcryptAdapter { + generateHash(password: string): string { + return hashSync(password); + } + + compareHash(password: string, hashed: string): boolean { + return compareSync(password, hashed); + } +} diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..c6022e0 --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,37 @@ +import { get } from "env-var"; + +// Use 'get' directly instead of 'env.get' +export const env = { + NODE_ENV: get("NODE_ENV").default("development").asString(), + PORT: get("PORT").default("3000").asPortNumber(), + LOG_LEVEL: get("LOG_LEVEL").default("debug").asString(), + + // Database + DATABASE_URL: get("DATABASE_URL").required().asUrlString(), + + // JWT / Security + JWT_SECRET: get("JWT_SECRET").required().asString(), + + // Supabase + SUPABASE_URL: get("SUPABASE_URL").required().asUrlString(), + SUPABASE_ANON_KEY: get("SUPABASE_ANON_KEY").required().asString(), + + // Redis + REDIS_URL: get("REDIS_URL").default("redis://localhost:6379").asString(), + + // Stellar / Horizon + HORIZON_URL: get("HORIZON_URL") + .default("https://horizon-testnet.stellar.org") + .asUrlString(), + STELLAR_NETWORK: get("STELLAR_NETWORK").default("testnet").asString(), + + // Email + EMAIL_SERVICE: get("EMAIL_SERVICE").default("gmail").asString(), + EMAIL_USER: get("EMAIL_USER").required().asString(), + EMAIL_PASSWORD: get("EMAIL_PASSWORD").required().asString(), + BASE_URL: get("BASE_URL").default("http://localhost:3000").asUrlString(), + + // Soroban + SOROBAN_RPC_URL: get("SOROBAN_RPC_URL").required().asUrlString(), + SOROBAN_SERVER_SECRET: get("SOROBAN_SERVER_SECRET").required().asString(), +}; diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/config/jwtAdapter.ts b/src/config/jwtAdapter.ts new file mode 100644 index 0000000..5c4deda --- /dev/null +++ b/src/config/jwtAdapter.ts @@ -0,0 +1,36 @@ +import jwt from "jsonwebtoken"; +import { env } from "./env"; + +const JWT_SEED = env.JWT_SECRET; + +export class JwtAdapter { + static async generateToken( + payload: object, + durationInHours: number = 2 + ): Promise { + return new Promise((resolve) => { + jwt.sign( + payload, + JWT_SEED, + { + expiresIn: `${durationInHours}h`, + }, + (error, token) => { + if (error) return resolve(null); + + return resolve(token!); + } + ); + }); + } + + static validateToken(token: string): Promise { + return new Promise((resolve) => { + jwt.verify(token, JWT_SEED, (error: unknown, decoded: unknown) => { + if (error) return resolve(null); + + resolve(decoded as T); + }); + }); + } +} diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts index c49bcb4..54c2348 100644 --- a/src/config/swagger.config.ts +++ b/src/config/swagger.config.ts @@ -1,6 +1,6 @@ +import { Application } from "express"; import swaggerUi from "swagger-ui-express"; import YAML from "yaml"; -import { Express } from "express"; import fs from "fs"; export class SwaggerConfig { @@ -8,7 +8,7 @@ export class SwaggerConfig { fs.readFileSync("./openapi.yaml", "utf8") ); - static setup(app: Express): void { + static setup(app: Application): void { if (process.env.NODE_ENV !== "development") { console.log("⚠️ Swagger is disabled in production mode."); return; diff --git a/src/index.ts b/src/index.ts index e20d455..59e2c6a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,35 +1,39 @@ +//Import envs import "dotenv/config"; import "reflect-metadata"; -import express from "express"; + +//Import express +import { Application } from "express"; +import { Server } from "./Server"; + +//Import Prisma config import { prisma, dbMonitor } from "./config/prisma"; + +//Import Swagger config import { SwaggerConfig } from "./config/swagger.config"; +import { requestLoggerMiddleware } from "./middlewares/requestLogger.middleware"; +import { Logger } from "./utils/logger"; + +//Import redis client import { redisClient } from "./config/redis"; -import cors from "cors"; + +//Import middlewares import { errorHandler } from "./middlewares/errorHandler"; import { dbPerformanceMiddleware } from "./middlewares/dbPerformanceMiddleware"; -import { setupRateLimiting } from "./middleware/rateLimitMiddleware"; -import { cronManager } from "./utils/cron"; -import apiRouter from "./routes"; +import { rateLimiterMiddleware } from "@/middlewares/rateLimit.middleware"; import { traceIdMiddleware } from "./middlewares/traceId.middleware"; -import { requestLoggerMiddleware } from "./middlewares/requestLogger.middleware"; -import authRoutes from "./routes/authRoutes"; -import router from "./routes/nftRoutes"; -import userRoutes from "./routes/userRoutes"; -import metricsRoutes from "./modules/metrics/routes/metrics.routes"; -import certificateRoutes from "./routes/certificatesRoutes"; -import volunteerRoutes from "./routes/VolunteerRoutes"; -import projectRoutes from "./routes/ProjectRoutes"; -import organizationRoutes from "./routes/OrganizationRoutes"; -import messageRoutes from "./modules/messaging/routes/messaging.routes"; -import testRoutes from "./routes/testRoutes"; -import { Logger } from "./utils/logger"; -const globalLogger = new Logger("VolunChain"); +//Import cron manager +import { cronManager } from "./utils/cron"; + +// Import routes (you'll need to create this) +import { AppRoutes } from "./routes"; + import fs from "fs"; import path from "path"; -const app = express(); -const PORT = process.env.PORT || 3000; +const globalLogger = new Logger("VolunChain"); +const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000; const ENV = process.env.NODE_ENV || "development"; // Ensure logs directory exists @@ -44,184 +48,151 @@ globalLogger.info("Starting VolunChain API...", { nodeVersion: process.version, }); -// Trace ID middleware (must be first to ensure all requests have trace IDs) -app.use(traceIdMiddleware); - -// Request logging middleware (after trace ID) -app.use(requestLoggerMiddleware); +// Health check route function +const setupHealthRoute = (app: Application) => { + app.get("/health", async (req, res) => { + type ServiceStatus = { + status: string; + responseTime?: string; + metrics?: { + averageQueryTime?: number; + activeConnections?: number; + }; + }; -// Middleware for parsing JSON requests -app.use(express.json()); + type HealthStatus = { + status: string; + responseTime?: string; + services: Record; + }; -// Database performance monitoring -app.use(dbPerformanceMiddleware); + const healthStatus: HealthStatus = { + status: "ok", + services: {}, + }; + const startTime = Date.now(); + + // Checking database + try { + const start_time = Date.now(); + await prisma.$queryRaw`SELECT 1`; + const response_time = Date.now() - start_time; + healthStatus.services.database = { + status: "connected", + responseTime: `${response_time}ms`, + metrics: { + averageQueryTime: dbMonitor.getAverageQueryTime(), + }, + }; + } catch (err) { + healthStatus.status = "unhealthy"; + healthStatus.services.database = { status: "disconnected" }; + console.error("Database connection failed:", err); + } + + // Checking cache + try { + const start_time = Date.now(); + const redisPing = await redisClient.ping(); + const redis_response_time = Date.now() - start_time; + healthStatus.services.cache = { + status: redisPing === "PONG" ? "connected" : "disconnected", + responseTime: `${redis_response_time}ms`, + }; + } catch (err) { + healthStatus.status = "unhealthy"; + healthStatus.services.cache = { status: "disconnected" }; + console.error("Redis connection failed:", err); + } + + const total_responseTime = Date.now() - startTime; + healthStatus.responseTime = `${total_responseTime}ms`; + + const httpStatus = healthStatus.status === "ok" ? 200 : 503; + res.status(httpStatus).json(healthStatus); + }); +}; -// Rate limiting -setupRateLimiting(app); +// Middleware setup functions +const setupTraceId = (app: Application) => { + app.use(traceIdMiddleware); +}; -app.use(cors()); +const setupRequestLogger = (app: Application) => { + app.use(requestLoggerMiddleware); +}; -// Setup Swagger only for development environment -if (ENV === "development") { - SwaggerConfig.setup(app); -} +const setupDbPerformance = (app: Application) => { + app.use(dbPerformanceMiddleware); +}; -// Health check route -app.get("/", (req, res) => { - res.send("VolunChain API is running!"); -}); +const setupRateLimit = (app: Application) => { + app.use(rateLimiterMiddleware); +}; -// Error handler middleware -app.use( - ( - err: Error, - req: express.Request, - res: express.Response, - next: express.NextFunction - ) => { - errorHandler(err, req, res, next); - } -); - -// Health check route -app.get("/health", async (req, res) => { - type ServiceStatus = { - status: string; - responseTime?: string; - metrics?: { - averageQueryTime?: number; - activeConnections?: number; - }; - }; - - type HealthStatus = { - status: string; - responseTime?: string; - services: Record; - }; - const healthStatus: HealthStatus = { - status: "ok", - services: {}, - }; - const startTime = Date.now(); - - // Checking database - try { - const start_time = Date.now(); - await prisma.$queryRaw`SELECT 1`; - const response_time = Date.now() - start_time; - healthStatus.services.database = { - status: "connected", - responseTime: `${response_time}ms`, - metrics: { - averageQueryTime: dbMonitor.getAverageQueryTime(), - }, - }; - } catch (err) { - healthStatus.status = "unhealthy"; - healthStatus.services.database = { status: "disconnected" }; - console.error("Database connection failed:", err); +const setupSwagger = (app: Application) => { + if (ENV === "development") { + SwaggerConfig.setup(app); + globalLogger.info( + `📚 Swagger docs available at http://localhost:${PORT}/api/docs` + ); } +}; - // Checking cache +// Function to initialize Redis +const initializeRedis = async (): Promise => { try { - const start_time = Date.now(); - const redisPing = await redisClient.ping(); - const redis_response_time = Date.now() - start_time; - healthStatus.services.cache = { - status: redisPing === "PONG" ? "connected" : "disconnected", - responseTime: `${redis_response_time}ms`, - }; - } catch (err) { - healthStatus.status = "unhealthy"; - healthStatus.services.cache = { status: "disconnected" }; - console.error("Redis connection failed:", err); + await redisClient.connect(); + globalLogger.info("Redis connected successfully!"); + } catch (error) { + globalLogger.error("Error during Redis initialization:", error ?? "error"); + throw error; } +}; - const total_responseTime = Date.now() - startTime; - healthStatus.responseTime = `${total_responseTime}ms`; - - const httpStatus = healthStatus.status === "ok" ? 200 : 503; - res.status(httpStatus).json(healthStatus); -}); - -// API Routes with versioning -app.use("/api", apiRouter); - -// Authentication routes -app.use("/auth", authRoutes); - -// NFT routes -app.use("/nft", router); - -// User routes -app.use("/users", userRoutes); - -// Metrics routes -app.use("/metrics", metricsRoutes); - -// Other routes -app.use("/certificate", certificateRoutes); -app.use("/projects", projectRoutes); -app.use("/volunteers", volunteerRoutes); -app.use("/organizations", organizationRoutes); -router.use("/messages", messageRoutes); - -// Test routes -app.use("/test", testRoutes); - -// Initialize the database and start the server -prisma - .$connect() - .then(() => { +// Initialize server function +const initializeServer = async (): Promise => { + try { + // Connect to database + await prisma.$connect(); globalLogger.info("Database connected successfully!"); - // initialize Redis - initializeRedis() - .then(() => { - globalLogger.info("Redis connected successfully!"); - - // Initialize scheduled tasks - cronManager.initCronJobs(); - globalLogger.info("Cron jobs initialized successfully!"); - - app.listen(PORT, () => { - globalLogger.info( - `Server is running on http://localhost:${PORT}`, - { - port: PORT, - environment: ENV, - } - ); - - if (ENV === "development") { - globalLogger.info( - `📚 Swagger docs available at http://localhost:${PORT}/api/docs` - ); - } - }); - }) - .catch((error) => { - globalLogger.error( - "Server failed to start due to Redis initialization error", - error - ); - process.exit(1); - }); - }) - .catch((error: Error) => { - globalLogger.error("Error during database initialization", error); + // Initialize Redis + await initializeRedis(); + + // Initialize scheduled tasks + cronManager.initCronJobs(); + globalLogger.info("Cron jobs initialized successfully!"); + + // Create server instance with middlewares in order + const server = new Server({ + port: PORT, + environment: ENV, + routes: AppRoutes.routes, + middlewares: [ + setupTraceId, // Must be first + setupRequestLogger, // After trace ID + setupDbPerformance, // Database monitoring + setupRateLimit, // Rate limiting + setupSwagger, // Swagger docs (dev only) + setupHealthRoute, // Health check endpoint + ], + errorHandlers: [errorHandler], + enableCors: true, + }); + + // Start the server with error handler + await server.start(); + } catch (error: unknown) { + globalLogger.error("Server failed to start", error ?? "error"); process.exit(1); - }); - -// Function to initialize Redis -const initializeRedis = async () => { - try { - await redisClient.connect(); - console.log("Redis connected successfully!"); - } catch (error) { - console.error("Error during Redis initialization:", error); } }; -export default app; +// Start the application +initializeServer().catch((error) => { + globalLogger.error("Failed to initialize server", error); + process.exit(1); +}); + +export default initializeServer; diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts deleted file mode 100644 index 5a0384a..0000000 --- a/src/middleware/authMiddleware.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import jwt from "jsonwebtoken"; -import { PrismaUserRepository } from "../modules/user/repositories/PrismaUserRepository"; -import { - AuthenticatedRequest, - DecodedUser, - toAuthenticatedUser, -} from "../types/auth.types"; - -const SECRET_KEY = process.env.JWT_SECRET || "defaultSecret"; -const userRepository = new PrismaUserRepository(); - -export const authMiddleware = async ( - req: Request, - res: Response, - next: NextFunction -): Promise => { - const token = req.headers.authorization?.split(" ")[1]; - - if (!token) { - res.status(401).json({ message: "No token provided" }); - return; - } - - try { - const decoded = jwt.verify(token, SECRET_KEY) as { - id: string; - role: string; - }; - const user = await userRepository.findById(`${decoded.id}`); - - if (!user) { - res.status(401).json({ message: "User not found" }); - return; - } - - if (!user.isVerified) { - res.status(403).json({ - message: "Email not verified. Please verify your email to proceed.", - }); - return; - } - - (req as AuthenticatedRequest).user = { - id: user.id, - email: user.email, - role: decoded.role, - isVerified: user.isVerified, - }; - - next(); - } catch (error) { - console.error("Error during authentication:", error); - res.status(401).json({ message: "Invalid token" }); - } -}; - -export const requireVerifiedEmail = async ( - req: Request, - res: Response, - next: NextFunction -): Promise => { - try { - const authenticatedReq = req as AuthenticatedRequest; - - if (!authenticatedReq.user) { - res - .status(401) - .json({ message: "Unauthorized - Authentication required" }); - return; - } - - const isVerified = await userRepository.isUserVerified( - authenticatedReq.user.id.toString() - ); - - if (!isVerified) { - res.status(403).json({ - message: "Forbidden - Email verification required", - verificationNeeded: true, - }); - return; - } - - authenticatedReq.user.isVerified = true; - next(); - } catch (error) { - // Use basic console.error here to avoid circular dependencies - console.error("Error checking email verification status:", error); - res.status(500).json({ - message: "Internal server error", - ...(req.traceId && { traceId: req.traceId }), - }); - } -}; - -export default { - requireVerifiedEmail, - authMiddleware, -}; diff --git a/src/middleware/rateLimitMiddleware.ts b/src/middleware/rateLimitMiddleware.ts deleted file mode 100644 index a598425..0000000 --- a/src/middleware/rateLimitMiddleware.ts +++ /dev/null @@ -1,78 +0,0 @@ -import express, { Request, Response, NextFunction, Router } from "express"; -import { RateLimitUseCase } from "./../modules/shared/middleware/rate-limit/use-cases/rate-limit-use-case"; -import { Logger } from "../utils/logger"; - -export class RateLimitMiddleware { - private rateLimitUseCase: RateLimitUseCase; - private logger: Logger; - - constructor(rateLimitUseCase?: RateLimitUseCase) { - this.rateLimitUseCase = rateLimitUseCase || new RateLimitUseCase(); - this.logger = new Logger("RATE_LIMIT_MIDDLEWARE"); - } - - // Returning a middleware function - rateLimiter = (req: Request, res: Response, next: NextFunction) => { - const checkRateLimit = async () => { - try { - const { allowed, remaining, retryAfter } = - await this.rateLimitUseCase.checkRateLimit(req); - console.log(allowed, remaining); - - // Add rate limit headers - res.setHeader("X-RateLimit-Remaining", remaining.toString()); - - if (!allowed) { - this.logger.warn( - `Rate limit exceeded for ${req.ip} on ${req.path}`, - { - remaining, - retryAfter, - ip: req.ip, - path: req.path, - traceId: req.traceId, - } - ); - return res.status(429).json({ - error: "Too Many Requests", - message: - "You have exceeded the rate limit. Please try again later.", - retryAfter: retryAfter * 60 + " seconds", // Default retry after 1 minute - ...(req.traceId && { traceId: req.traceId }), - }); - } - - next(); - } catch (error) { - this.logger.error("Rate limit check failed", { - error: error instanceof Error ? error.message : error, - stack: error instanceof Error ? error.stack : undefined, - path: req.path, - method: req.method, - ip: req.ip, - traceId: req.traceId, - }); - next(error); - } - }; - - checkRateLimit(); - }; - - //Method to apply middleware to specific routes - applyToRoutes(router: Router, routes: string[]) { - routes.forEach((route) => { - router.use(route, this.rateLimiter); - }); - return router; - } -} - -export function setupRateLimiting(app: express.Application) { - const rateLimitMiddleware = new RateLimitMiddleware(); - - // Explicit typing and direct method application - app.use("/auth", rateLimitMiddleware.rateLimiter); - app.use("/wallet", rateLimitMiddleware.rateLimiter); - app.use("/email", rateLimitMiddleware.rateLimiter); -} diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index f3441cf..0471dfa 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -3,7 +3,7 @@ import { CustomError, InternalServerError, } from "../modules/shared/application/errors"; -import { Logger } from "../utils/logger"; +import { Logger } from "../utils"; const logger = new Logger("ERROR_HANDLER"); @@ -51,7 +51,7 @@ export const errorHandler = ( // Handle different types of errors if (err instanceof CustomError) { - return res.status(err.statusCode).json({ + res.status(err.statusCode).json({ code: err.code, message: err.message, ...(err.details && { details: err.details }), @@ -64,8 +64,9 @@ export const errorHandler = ( err.message || "An unexpected error occurred" ); - return res.status(internalError.statusCode).json({ + res.status(internalError.statusCode).json({ ...internalError.toJSON(), ...(req.traceId && { traceId: req.traceId }), }); + next(); }; diff --git a/src/modules/auth/use-cases/email-verification.usecase.ts b/src/modules/auth/application/use-cases/email-verification.use-case.ts similarity index 74% rename from src/modules/auth/use-cases/email-verification.usecase.ts rename to src/modules/auth/application/use-cases/email-verification.use-case.ts index 961fa76..8ad3b5d 100644 --- a/src/modules/auth/use-cases/email-verification.usecase.ts +++ b/src/modules/auth/application/use-cases/email-verification.use-case.ts @@ -1,6 +1,6 @@ -import { IUserRepository } from "../../../repository/IUserRepository"; import { randomBytes } from "crypto"; -import { sendVerificationEmail } from "../../../utils/email.utils"; +import { IUserRepository } from "@/modules/user/application/repository/user.repository"; +import { sendVerificationEmail } from "../../../../utils/email.utils"; export class EmailVerificationUseCase { constructor(private userRepository: IUserRepository) {} @@ -14,14 +14,14 @@ export class EmailVerificationUseCase { const expires = new Date(); expires.setHours(expires.getHours() + 1); - await this.userRepository.updateVerificationToken(user.id, token, expires); + await this.userRepository.saveVerificationToken(user.id, token, expires); const verificationLink = `http://localhost:3000/auth/verify-email?token=${token}`; await sendVerificationEmail(user.email, verificationLink); } - async verifyEmail(token: string): Promise { + async verifyToken(token: string): Promise { const user = await this.userRepository.findByVerificationToken(token); if (!user) throw new Error("Invalid or expired token"); @@ -34,6 +34,6 @@ export class EmailVerificationUseCase { ); } - await this.userRepository.updateVerificationStatus(user.id); + return await this.userRepository.isUserVerified(user.id); } } diff --git a/src/modules/auth/application/use-cases/index.ts b/src/modules/auth/application/use-cases/index.ts new file mode 100644 index 0000000..915aca1 --- /dev/null +++ b/src/modules/auth/application/use-cases/index.ts @@ -0,0 +1,4 @@ +export { VerifyWalletUseCase } from "./verify-wallet.usecase"; +export { ResendVerificationEmailUseCase } from "./resend-verification-email.use-case"; +export { EmailVerificationUseCase } from "./email-verification.use-case"; +export { SendVerificationEmailUseCase } from "./send-verification-email.use-case"; diff --git a/src/modules/auth/use-cases/resend-verification-email.usecase.ts b/src/modules/auth/application/use-cases/resend-verification-email.use-case.ts similarity index 77% rename from src/modules/auth/use-cases/resend-verification-email.usecase.ts rename to src/modules/auth/application/use-cases/resend-verification-email.use-case.ts index 55ed100..9e71a9b 100644 --- a/src/modules/auth/use-cases/resend-verification-email.usecase.ts +++ b/src/modules/auth/application/use-cases/resend-verification-email.use-case.ts @@ -1,17 +1,15 @@ import jwt from "jsonwebtoken"; -import { IUserRepository } from "../../user/domain/interfaces/IUserRepository"; -import { - ResendVerificationEmailRequestDTO, - ResendVerificationEmailResponseDTO, -} from "../dto/email-verification.dto"; -import { sendEmail } from "../utils/email.utils"; +import { sendEmail } from "../../utils/email.utils"; +import { IUserRepository } from "@/modules/user/application/repository/user.repository"; +import { EmailDto } from "../../presentation/dto"; export class ResendVerificationEmailUseCase { constructor(private userRepository: IUserRepository) {} - async execute( - dto: ResendVerificationEmailRequestDTO - ): Promise { + async execute(dto: EmailDto): Promise<{ + success: boolean; + message: string; + }> { const { email } = dto; const EMAIL_SECRET = process.env.EMAIL_SECRET || "emailSecret"; @@ -35,7 +33,7 @@ export class ResendVerificationEmailUseCase { tokenExpires.setHours(tokenExpires.getHours() + 24); // Token expires in 24 hours // Save verification token - await this.userRepository.setVerificationToken( + await this.userRepository.saveVerificationToken( user.id, token, tokenExpires diff --git a/src/modules/auth/use-cases/send-verification-email.usecase.ts b/src/modules/auth/application/use-cases/send-verification-email.use-case.ts similarity index 78% rename from src/modules/auth/use-cases/send-verification-email.usecase.ts rename to src/modules/auth/application/use-cases/send-verification-email.use-case.ts index 73229f6..685fbf5 100644 --- a/src/modules/auth/use-cases/send-verification-email.usecase.ts +++ b/src/modules/auth/application/use-cases/send-verification-email.use-case.ts @@ -1,17 +1,12 @@ import jwt from "jsonwebtoken"; -import { IUserRepository } from "../../user/domain/interfaces/IUserRepository"; -import { - EmailVerificationRequestDTO, - EmailVerificationResponseDTO, -} from "../dto/email-verification.dto"; -import { sendEmail } from "../utils/email.utils"; +import { IUserRepository } from "@/modules/user/application/repository/user.repository"; +import { sendEmail } from "../../utils/email.utils"; +import { EmailDto } from "../../presentation/dto"; export class SendVerificationEmailUseCase { constructor(private userRepository: IUserRepository) {} - async execute( - dto: EmailVerificationRequestDTO - ): Promise { + async execute(dto: EmailDto): Promise { const { email } = dto; const EMAIL_SECRET = process.env.EMAIL_SECRET || "emailSecret"; @@ -35,7 +30,7 @@ export class SendVerificationEmailUseCase { tokenExpires.setHours(tokenExpires.getHours() + 24); // Token expires in 24 hours // Save verification token - await this.userRepository.setVerificationToken( + await this.userRepository.saveVerificationToken( user.id, token, tokenExpires diff --git a/src/modules/auth/use-cases/verify-wallet.usecase.ts b/src/modules/auth/application/use-cases/verify-wallet.usecase.ts similarity index 91% rename from src/modules/auth/use-cases/verify-wallet.usecase.ts rename to src/modules/auth/application/use-cases/verify-wallet.usecase.ts index a615ee4..0f54ec0 100644 --- a/src/modules/auth/use-cases/verify-wallet.usecase.ts +++ b/src/modules/auth/application/use-cases/verify-wallet.usecase.ts @@ -1,6 +1,6 @@ import { Keypair, StrKey, Horizon } from "@stellar/stellar-sdk"; -import { VerifyWalletDto } from "../dto/wallet-validation.dto"; -import { horizonConfig } from "../../../config/horizon.config"; +import { horizonConfig } from "../../../../config/horizon.config"; +import { WalletDto } from "../../presentation/dto"; type WalletVerificationResult = { verified: boolean; @@ -9,7 +9,7 @@ type WalletVerificationResult = { }; export class VerifyWalletUseCase { - async execute(input: VerifyWalletDto): Promise { + async execute(input: WalletDto): Promise { const { walletAddress, signature, message } = input; // Validate public key format diff --git a/src/modules/auth/domain/entities/auth.entitiy.ts b/src/modules/auth/domain/entities/auth.entitiy.ts new file mode 100644 index 0000000..d34486e --- /dev/null +++ b/src/modules/auth/domain/entities/auth.entitiy.ts @@ -0,0 +1,59 @@ +import { BaseEntity } from "@/modules/shared/domain/entities/base.entity"; +import { AuthExceptions } from "../exceptions/auth.exceptions"; + +export interface IAuthProps extends Partial { + wallet: string; + isVerified: boolean; + verificationToken?: string | null; + verificationTokenExpires?: Date | null; +} + +export class AuthEntity extends BaseEntity { + public readonly wallet: string; + public readonly isVerified: boolean; + public readonly verificationToken?: string | null; + public readonly verificationTokenExpires?: Date | null; + + constructor( + props: IAuthProps, + id?: string, + createdAt?: Date, + updatedAt?: Date + ) { + super(); + + if (id) this.id = id; + if (createdAt) this.createdAt = createdAt; + if (updatedAt) this.updatedAt = updatedAt; + + if (!props.wallet) { + throw new AuthExceptions("wallet", "Wallet is required"); + } + + // ===== Asignación ===== + this.wallet = props.wallet; + this.isVerified = props.isVerified; + this.verificationToken = props.verificationToken ?? null; + this.verificationTokenExpires = props.verificationTokenExpires ?? null; + } + + public static create(props: IAuthProps, id?: string): AuthEntity { + return new AuthEntity(props, id); + } + + public update(props: Partial): AuthEntity { + return new AuthEntity( + { + id: props.id ?? this.id, + wallet: props.wallet ?? this.wallet, + verificationToken: props.verificationToken ?? this.verificationToken, + verificationTokenExpires: + props.verificationTokenExpires ?? this.verificationTokenExpires, + isVerified: props.isVerified ?? this.isVerified, + }, + this.id, + this.createdAt, + new Date() + ); + } +} diff --git a/src/modules/auth/domain/exceptions/auth.exceptions.ts b/src/modules/auth/domain/exceptions/auth.exceptions.ts new file mode 100644 index 0000000..0dd963e --- /dev/null +++ b/src/modules/auth/domain/exceptions/auth.exceptions.ts @@ -0,0 +1,7 @@ +import { DomainException } from "@/modules/shared/domain/exceptions/domain.exception"; + +export class AuthExceptions extends DomainException { + constructor(field: string, value: string) { + super(`Invalid ${field}: ${value}`); + } +} diff --git a/src/modules/auth/dto/email-verification.dto.ts b/src/modules/auth/dto/email-verification.dto.ts deleted file mode 100644 index 99be356..0000000 --- a/src/modules/auth/dto/email-verification.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface EmailVerificationRequestDTO { - email: string; -} - -export interface EmailVerificationResponseDTO { - success: boolean; - message: string; -} - -export interface VerifyEmailRequestDTO { - token: string; -} - -export interface VerifyEmailResponseDTO { - success: boolean; - message: string; - verified: boolean; -} - -export interface ResendVerificationEmailRequestDTO { - email: string; -} - -export interface ResendVerificationEmailResponseDTO { - success: boolean; - message: string; -} diff --git a/src/modules/auth/dto/emailVerification.dto.ts b/src/modules/auth/dto/emailVerification.dto.ts deleted file mode 100644 index 66ab2cf..0000000 --- a/src/modules/auth/dto/emailVerification.dto.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - IsString, - IsEmail, - IsNotEmpty, - MinLength, - validate, -} from "class-validator"; - -export class RegisterDTO { - @IsString() - @IsNotEmpty() - name: string; - - @IsEmail() - @IsNotEmpty() - email: string; - - @IsString() - @IsNotEmpty() - @MinLength(8) - password: string; - - @IsString() - @IsNotEmpty() - wallet: string; - - constructor(partial: Partial = {}) { - Object.assign(this, partial); - this.name = this.name || ""; - this.email = this.email || ""; - this.password = this.password || ""; - this.wallet = this.wallet || ""; - } - - static validate(dto: RegisterDTO) { - return validate(dto); - } -} - -export interface VerifyEmailDTO { - token: string; -} - -export interface ResendVerificationDTO { - email: string; -} diff --git a/src/modules/auth/dto/resendVerificationDTO.ts b/src/modules/auth/dto/resendVerificationDTO.ts deleted file mode 100644 index 27b7d98..0000000 --- a/src/modules/auth/dto/resendVerificationDTO.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsEmail } from "class-validator"; - -export class ResendVerificationDTO { - @IsEmail({}, { message: "Please provide a valid email address" }) - email: string; -} diff --git a/src/modules/auth/dto/wallet-validation.dto.ts b/src/modules/auth/dto/wallet-validation.dto.ts deleted file mode 100644 index 3caf8cb..0000000 --- a/src/modules/auth/dto/wallet-validation.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { IsString, MinLength, MaxLength, Matches } from "class-validator"; - -export class ValidateWalletFormatDto { - @IsString({ message: "Wallet address must be a string" }) - @MinLength(56, { - message: "Stellar wallet address must be 56 characters long", - }) - @MaxLength(56, { - message: "Stellar wallet address must be 56 characters long", - }) - @Matches(/^G[A-Z2-7]{55}$/, { - message: "Invalid Stellar wallet address format", - }) - walletAddress: string; -} - -export class VerifyWalletDto { - @IsString({ message: "Wallet address must be a string" }) - @MinLength(56, { - message: "Stellar wallet address must be 56 characters long", - }) - @MaxLength(56, { - message: "Stellar wallet address must be 56 characters long", - }) - @Matches(/^G[A-Z2-7]{55}$/, { - message: "Invalid Stellar wallet address format", - }) - walletAddress: string; - - @IsString({ message: "Signature must be a string" }) - signature: string; - - @IsString({ message: "Message must be a string" }) - message: string; -} diff --git a/src/modules/auth/presentation/controllers/Auth.controller.disabled b/src/modules/auth/presentation/controllers/Auth.controller.disabled deleted file mode 100644 index 797fba6..0000000 --- a/src/modules/auth/presentation/controllers/Auth.controller.disabled +++ /dev/null @@ -1,169 +0,0 @@ -import { Request, Response } from "express"; -import AuthService from "../../../../services/AuthService"; -import { AuthenticatedRequest } from "../../../../types/auth.types"; - -class AuthController { - private authService: AuthService; - - constructor() { - this.authService = new AuthService(); - } - - register = async (req: Request, res: Response): Promise => { - const { name, lastName, email, password, wallet } = req.body; - - try { - const response = await this.authService.register( - name, - lastName, - email, - password, - wallet - ); - res.status(201).json(response); - } catch (error) { - res.status(400).json({ - message: error instanceof Error ? error.message : "Registration failed", - }); - } - }; - - verifyEmail = async (req: Request, res: Response): Promise => { - const token = - typeof req.params.token === "string" - ? req.params.token - : typeof req.query.token === "string" - ? req.query.token - : undefined; - - if (!token || typeof token !== "string") { - res.status(400).json({ message: "Token is required" }); - return; - } - - try { - const response = await this.authService.verifyEmail(token); - res.json(response); - } catch (error) { - res.status(400).json({ - message: error instanceof Error ? error.message : "Verification failed", - }); - } - }; - - resendVerificationEmail = async ( - req: Request, - res: Response - ): Promise => { - const { email } = req.body; - - if (!email) { - res.status(400).json({ message: "Email is required" }); - return; - } - - try { - const response = await this.authService.resendVerificationEmail(email); - res.json(response); - } catch (error) { - res.status(400).json({ - message: - error instanceof Error ? error.message : "Could not resend email", - }); - } - }; - - login = async (req: Request, res: Response): Promise => { - const { walletAddress } = req.body; - - try { - const token = await this.authService.authenticate(walletAddress); - res.json({ token }); - } catch (error) { - res.status(401).json({ - message: error instanceof Error ? error.message : "Unknown error", - }); - } - }; - - checkVerificationStatus = async ( - req: AuthenticatedRequest, - res: Response - ): Promise => { - if (!req.user) { - res.status(401).json({ message: "User not authenticated" }); - return; - } - - try { - const status = await this.authService.checkVerificationStatus( - req.user.id.toString() - ); - res.json(status); - } catch (error) { - res.status(400).json({ - message: - error instanceof Error - ? error.message - : "Could not check verification status", - }); - } - }; - - protectedRoute = (req: AuthenticatedRequest, res: Response): void => { - if (!req.user) { - res.status(401).json({ message: "User not authenticated" }); - return; - } - - res.json({ - message: `Hello ${req.user.role}`, - userId: req.user.id, - isVerified: req.user.isVerified, - }); - }; - - verifyWallet = async (req: Request, res: Response): Promise => { - const { walletAddress } = req.body; - - if (!walletAddress) { - res.status(400).json({ message: "Wallet address is required" }); - return; - } - - try { - const verification = - await this.authService.verifyWalletAddress(walletAddress); - res.json(verification); - } catch (error) { - res.status(400).json({ - message: - error instanceof Error ? error.message : "Wallet verification failed", - }); - } - }; - - validateWalletFormat = async (req: Request, res: Response): Promise => { - const { walletAddress } = req.body; - - if (!walletAddress) { - res.status(400).json({ message: "Wallet address is required" }); - return; - } - - try { - const validation = - await this.authService.validateWalletFormat(walletAddress); - res.json(validation); - } catch (error) { - res.status(400).json({ - message: - error instanceof Error - ? error.message - : "Wallet format validation failed", - }); - } - }; -} - -export default new AuthController(); diff --git a/src/modules/auth/presentation/controllers/Auth.controller.ts b/src/modules/auth/presentation/controllers/Auth.controller.ts index 1ba7a6d..42d7d85 100644 --- a/src/modules/auth/presentation/controllers/Auth.controller.ts +++ b/src/modules/auth/presentation/controllers/Auth.controller.ts @@ -1,172 +1,111 @@ import { Request, Response } from "express"; -// imports for DTO validator -import { plainToInstance } from "class-transformer"; -import { validate } from "class-validator"; - -// Necessary DTOs -import { RegisterDto } from "../../dto/register.dto"; -import { LoginDto } from "../../dto/login.dto"; -import { ResendVerificationDTO } from "../../dto/resendVerificationDTO"; -import { - VerifyWalletDto, - ValidateWalletFormatDto, -} from "../../dto/wallet-validation.dto"; - // Use cases -import { PrismaUserRepository } from "../../../user/repositories/PrismaUserRepository"; -import { SendVerificationEmailUseCase } from "../../use-cases/send-verification-email.usecase"; -import { ResendVerificationEmailUseCase } from "../../use-cases/resend-verification-email.usecase"; -import { VerifyEmailUseCase } from "../../use-cases/verify-email.usecase"; -import { ValidateWalletFormatUseCase } from "../../use-cases/wallet-format-validation.usecase"; -import { VerifyWalletUseCase } from "../../use-cases/verify-wallet.usecase"; - -const userRepository = new PrismaUserRepository(); -const sendVerificationEmailUseCase = new SendVerificationEmailUseCase( - userRepository -); -const resendVerificationEmailUseCase = new ResendVerificationEmailUseCase( - userRepository -); -const verifyEmailUseCase = new VerifyEmailUseCase(userRepository); -const validateWalletFormatUseCase = new ValidateWalletFormatUseCase(); -const verifyWalletUseCase = new VerifyWalletUseCase(); - -// DTO validator -async function validateOr400( - Cls: new () => T, - payload: unknown, - res: Response -): Promise { - const dto = plainToInstance(Cls, payload); - const errors = await validate(dto as object, { - whitelist: true, - forbidNonWhitelisted: true, - }); - - // dto not verified, throw a Bad Request - if (errors.length) { - res.status(400).json({ message: "Validation failed", errors }); - return; - } - - return dto; -} - -const register = async (req: Request, res: Response) => { - const dto = await validateOr400(RegisterDto, req.body, res); - if (!dto) return; - - try { - // Send verification email to provided address - await sendVerificationEmailUseCase.execute({ email: dto.email }); - res.status(200).json({ message: "Verification email sent" }); - } catch (err) { - const message = - err instanceof Error ? err.message : "Failed to send verification email"; - const status = message === "User not found" ? 400 : 500; - res.status(status).json({ error: message }); - } -}; - -const login = async (req: Request, res: Response) => { - const dto = await validateOr400(LoginDto, req.body, res); - if (!dto) return; - - // TODO: Implement Wallet auth logic as a use case - res.status(501).json({ - message: "Login service temporarily disabled", - error: "Wallet auth logic not implemented yet", +import { validateDto } from "@/shared/middleware/validation.middleware"; +import { EmailDto, LoginDto, RegisterDto, WalletDto } from "../dto"; +import { asyncHandler } from "@/utils/asyncHandler"; +import { + SendVerificationEmailUseCase, + EmailVerificationUseCase, + ResendVerificationEmailUseCase, + VerifyWalletUseCase, +} from "../../application/use-cases"; + +export class AuthController { + constructor( + private readonly sendVerificationEmailUseCase: SendVerificationEmailUseCase, + private readonly emailVerificationUseCase: EmailVerificationUseCase, + private readonly resendVerificationEmailUseCase: ResendVerificationEmailUseCase, + private readonly verifyWalletUseCase: VerifyWalletUseCase + ) {} + + register = asyncHandler(async (req: Request, res: Response) => { + const dto = await validateDto(RegisterDto); + if (!dto) return; + + try { + // Send verification email to provided address + await this.sendVerificationEmailUseCase.execute({ + email: req.body.email, + }); + res.status(200).json({ message: "Verification email sent" }); + } catch (err) { + const message = + err instanceof Error + ? err.message + : "Failed to send verification email"; + const status = message === "User not found" ? 400 : 500; + res.status(status).json({ error: message }); + } }); -}; - -const resendVerificationEmail = async (req: Request, res: Response) => { - const dto = await validateOr400(ResendVerificationDTO, req.body, res); - if (!dto) return; - try { - // Resends verification email to provided address - await resendVerificationEmailUseCase.execute({ email: dto.email }); - res.status(200).json({ message: "Verification email resent" }); - } catch (err) { - const message = - err instanceof Error - ? err.message - : "Failed to resend verification email"; - const status = message === "User not found" ? 404 : 500; - res.status(status).json({ error: message }); - } -}; + login = asyncHandler(async (req: Request, res: Response) => { + const dto = await validateDto(LoginDto); + if (!dto) return; -const verifyEmail = async (req: Request, res: Response) => { - const tokenParam = - typeof req.params.token === "string" ? req.params.token : undefined; - const tokenQuery = - typeof req.query.token === "string" - ? (req.query.token as string) - : undefined; - const token = tokenParam || tokenQuery; - - // if token is not given in the request - if (!token) { - res.status(400).json({ - success: false, - message: "Token in URL is required", - verified: false, + // TODO: Implement Wallet auth logic as a use case + res.status(501).json({ + message: "Login service temporarily disabled", + error: "Wallet auth logic not implemented yet", }); - return; - } + }); - try { - // Verifies email using use case - const result = await verifyEmailUseCase.execute({ token }); - const status = result.success ? 200 : 400; + resendVerificationEmail = asyncHandler( + async (req: Request, res: Response) => { + const dto = await validateDto(EmailDto); + if (!dto) return; + + try { + // Resends verification email to provided address + await this.resendVerificationEmailUseCase.execute({ + email: req.body.email, + }); + res.status(200).json({ message: "Verification email resent" }); + } catch (err) { + const message = + err instanceof Error + ? err.message + : "Failed to resend verification email"; + const status = message === "User not found" ? 404 : 500; + res.status(status).json({ error: message }); + } + } + ); + + verifyEmail = asyncHandler(async (req: Request, res: Response) => { + const tokenParam = + typeof req.params.token === "string" ? req.params.token : undefined; + const tokenQuery = + typeof req.query.token === "string" + ? (req.query.token as string) + : undefined; + const token = tokenParam || tokenQuery; + + // if token is not given in the request + if (!token) { + res.status(400).json({ + success: false, + message: "Token in URL is required", + verified: false, + }); + return; + } + + const result = await this.emailVerificationUseCase.verifyToken(token); + const status = result ? 200 : 400; res.status(status).json(result); - } catch { - res.status(400).json({ - success: false, - message: "Invalid or expired verification token", - verified: false, - }); - } -}; + }); -const verifyWallet = async (req: Request, res: Response) => { - const dto = await validateOr400(VerifyWalletDto, req.body, res); - if (!dto) return; + verifyWallet = asyncHandler(async (req: Request, res: Response) => { + const dto = await validateDto(WalletDto); + if (!dto) return; - try { - const result = await verifyWalletUseCase.execute(dto); + const result = await this.verifyWalletUseCase.execute({ + walletAddress: req.body.wallet, + signature: req.body.signature, + message: req.body.message, + }); const status = result.verified ? 200 : 400; res.status(status).json(result); - } catch (err) { - const message = - err instanceof Error ? err.message : "Wallet verification failed"; - res.status(500).json({ error: message }); - } -}; - -const validateWalletFormat = async (req: Request, res: Response) => { - const dto = await validateOr400(ValidateWalletFormatDto, req.body, res); - if (!dto) return; - - try { - // Validates wallet format using use case - const result = await validateWalletFormatUseCase.execute(dto); - const status = result.valid ? 200 : 400; - res.status(status).json(result); - } catch (err) { - const message = - err instanceof Error ? err.message : "Wallet format validation failed"; - res.status(500).json({ error: message }); - } -}; - -export default { - register, - login, - resendVerificationEmail, - verifyEmail, - verifyWallet, - validateWalletFormat, -}; + }); +} diff --git a/src/modules/auth/presentation/dto/email.dto.ts b/src/modules/auth/presentation/dto/email.dto.ts new file mode 100644 index 0000000..61c053f --- /dev/null +++ b/src/modules/auth/presentation/dto/email.dto.ts @@ -0,0 +1,7 @@ +import { IsEmail, IsNotEmpty } from "class-validator"; + +export class EmailDto { + @IsEmail() + @IsNotEmpty() + email: string; +} diff --git a/src/modules/auth/presentation/dto/index.ts b/src/modules/auth/presentation/dto/index.ts new file mode 100644 index 0000000..112d13c --- /dev/null +++ b/src/modules/auth/presentation/dto/index.ts @@ -0,0 +1,5 @@ +// Class-based DTOs with validation +export { EmailDto } from "./email.dto"; +export { WalletDto } from "./wallet.dto"; +export { LoginDto } from "./login.dto"; +export { RegisterDto } from "./register.dto"; diff --git a/src/modules/auth/dto/login.dto.ts b/src/modules/auth/presentation/dto/login.dto.ts similarity index 63% rename from src/modules/auth/dto/login.dto.ts rename to src/modules/auth/presentation/dto/login.dto.ts index ec4080c..0d9e614 100644 --- a/src/modules/auth/dto/login.dto.ts +++ b/src/modules/auth/presentation/dto/login.dto.ts @@ -1,6 +1,8 @@ import { IsString, IsEmail, IsNotEmpty } from "class-validator"; +import { ILoginRequestDto } from "./interfaces/request/login.request"; -export class LoginDto { +// Class-based DTO with validation +export class LoginDto implements ILoginRequestDto { @IsEmail({}, { message: "Please provide a valid email address" }) email: string; diff --git a/src/modules/auth/dto/register.dto.ts b/src/modules/auth/presentation/dto/register.dto.ts similarity index 74% rename from src/modules/auth/dto/register.dto.ts rename to src/modules/auth/presentation/dto/register.dto.ts index 0e7dbf2..6b173c8 100644 --- a/src/modules/auth/dto/register.dto.ts +++ b/src/modules/auth/presentation/dto/register.dto.ts @@ -5,7 +5,9 @@ import { MaxLength, IsOptional, } from "class-validator"; +import { IsStellarPublicKey } from "@/shared/validators/StellarPublicKey"; +// Class-based DTO with validation export class RegisterDto { @IsString({ message: "Name must be a string" }) @MinLength(2, { message: "Name must be at least 2 characters long" }) @@ -21,12 +23,6 @@ export class RegisterDto { password: string; @IsOptional() - @IsString({ message: "Wallet address must be a string" }) - @MinLength(56, { - message: "Stellar wallet address must be 56 characters long", - }) - @MaxLength(56, { - message: "Stellar wallet address must be 56 characters long", - }) + @IsStellarPublicKey() walletAddress?: string; } diff --git a/src/modules/auth/dto/verifyEmailDTO.ts b/src/modules/auth/presentation/dto/token.dto.ts similarity index 85% rename from src/modules/auth/dto/verifyEmailDTO.ts rename to src/modules/auth/presentation/dto/token.dto.ts index ff74d77..37d7a2c 100644 --- a/src/modules/auth/dto/verifyEmailDTO.ts +++ b/src/modules/auth/presentation/dto/token.dto.ts @@ -1,6 +1,6 @@ import { IsString, IsNotEmpty } from "class-validator"; -export class VerifyEmailDTO { +export class TokenDto { @IsString({ message: "Token must be a string" }) @IsNotEmpty({ message: "Token is required" }) token: string; diff --git a/src/modules/auth/presentation/dto/wallet.dto.ts b/src/modules/auth/presentation/dto/wallet.dto.ts new file mode 100644 index 0000000..552076b --- /dev/null +++ b/src/modules/auth/presentation/dto/wallet.dto.ts @@ -0,0 +1,14 @@ +import { IsStellarPublicKey } from "@/shared/validators/StellarPublicKey"; +import { IsString } from "class-validator"; + +export class WalletDto { + @IsString({ message: "Wallet address must be a string" }) + @IsStellarPublicKey() + walletAddress: string; + + @IsString({ message: "Signature must be a base64 string" }) + signature: string; + + @IsString({ message: "Message must be provided" }) + message: string; +} diff --git a/src/modules/auth/presentation/routes.ts b/src/modules/auth/presentation/routes.ts new file mode 100644 index 0000000..a595fca --- /dev/null +++ b/src/modules/auth/presentation/routes.ts @@ -0,0 +1,35 @@ +import { Router } from "express"; + +import { AuthController } from "./controllers/Auth.controller"; + +import { + EmailVerificationUseCase, + ResendVerificationEmailUseCase, + SendVerificationEmailUseCase, + VerifyWalletUseCase, +} from "../application/use-cases/index"; + +import { UserRepositoryImpl } from "@/modules/user/infrastructure/repositories/user.repository.impl"; + +export class AuthRoutes { + static get routes(): Router { + const router = Router(); + + //Repository + const repository = new UserRepositoryImpl(prisma); + + const controller = new AuthController( + new SendVerificationEmailUseCase(repository), + new EmailVerificationUseCase(repository), + new ResendVerificationEmailUseCase(repository), + new VerifyWalletUseCase() + ); + + router.post("/login", controller.login); + router.post("/verifiedEmail", controller.register); + router.post("/resendVerificationEmail", controller.resendVerificationEmail); + router.post("/verifyEmail", controller.verifyEmail); + + return router; + } +} diff --git a/src/modules/auth/use-cases/resend-email-verification.usecase.ts b/src/modules/auth/use-cases/resend-email-verification.usecase.ts deleted file mode 100644 index 9a98aaa..0000000 --- a/src/modules/auth/use-cases/resend-email-verification.usecase.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IUserRepository } from "../../../repository/IUserRepository"; -import { randomBytes } from "crypto"; -// import { sendVerificationEmail } from "../utils/email.utils"; // Function not found, commented out - -export class ResendVerificationUseCase { - constructor(private userRepository: IUserRepository) {} - - async resendVerificationEmail(email: string): Promise { - const user = await this.userRepository.findByEmail(email); - if (!user) throw new Error("User not found"); - if (user.isVerified) throw new Error("User is already verified"); - - const token = randomBytes(32).toString("hex"); - const expires = new Date(); - expires.setHours(expires.getHours() + 1); - - await this.userRepository.updateVerificationToken(user.id, token, expires); - - const verificationLink = `http://localhost:3000/auth/verify-email?token=${token}`; - - // TODO: Implement email sending functionality - console.log(`Verification email would be sent to ${user.email} with link: ${verificationLink}`); - } -} diff --git a/src/modules/auth/use-cases/verify-email.usecase.ts b/src/modules/auth/use-cases/verify-email.usecase.ts deleted file mode 100644 index 95c2d07..0000000 --- a/src/modules/auth/use-cases/verify-email.usecase.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { IUserRepository } from "../../user/domain/interfaces/IUserRepository"; -import { - VerifyEmailRequestDTO, - VerifyEmailResponseDTO, -} from "../dto/email-verification.dto"; - -export class VerifyEmailUseCase { - constructor(private userRepository: IUserRepository) {} - - async execute(dto: VerifyEmailRequestDTO): Promise { - try { - const { token } = dto; - - // Find user by verification token - const user = await this.userRepository.findByVerificationToken(token); - if (!user) { - return { - success: false, - message: "Invalid or expired verification token", - verified: false, - }; - } - - // If user is already verified - if (user.isVerified) { - return { - success: true, - message: "Email already verified", - verified: true, - }; - } - - // check if token has expired - const now = new Date(); - if ( - user.verificationTokenExpires && - new Date(user.verificationTokenExpires) < now - ) { - throw new Error("Verification token has expired"); - } - - // Verify user - await this.userRepository.verifyUser(user.id); - - return { - success: true, - message: "Email verified successfully", - verified: true, - }; - } catch (error) { - throw new Error("Invalid or expired verification token"); - } - } -} diff --git a/src/modules/auth/use-cases/wallet-format-validation.usecase.ts b/src/modules/auth/use-cases/wallet-format-validation.usecase.ts deleted file mode 100644 index eb89ebd..0000000 --- a/src/modules/auth/use-cases/wallet-format-validation.usecase.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { plainToInstance } from "class-transformer"; -import { validate } from "class-validator"; -import { ValidateWalletFormatDto } from "../dto/wallet-validation.dto"; - -type WalletFormatValidationResult = { - valid: boolean; - errors?: string[]; -}; - -export class ValidateWalletFormatUseCase { - async execute(input: unknown): Promise { - const dto = plainToInstance(ValidateWalletFormatDto, input); - const errors = await validate(dto as object, { - whitelist: true, - forbidNonWhitelisted: true, - }); - - if (errors.length) { - const messages = errors.flatMap((e) => - Object.values(e.constraints ?? {}) - ); - return { valid: false, errors: messages }; - } - return { valid: true }; - } -} diff --git a/src/modules/nft/application/repositorys/INFTRepository.ts b/src/modules/nft/application/repositorys/INFTRepository.ts new file mode 100644 index 0000000..4f6e33e --- /dev/null +++ b/src/modules/nft/application/repositorys/INFTRepository.ts @@ -0,0 +1,14 @@ +import { NFTEntity } from "../../domain/entities/nft.entity"; +import { INFT } from "../../domain/interfaces/nft.interface"; + +export interface INFTRepository { + create(nft: INFT): Promise; + findById(id: string): Promise; + findByUserId( + userId: string, + page: number, + pageSize: number + ): Promise<{ nfts: NFTEntity[]; total: number }>; + update(id: string, nft: Partial): Promise; + delete(id: string): Promise; +} diff --git a/src/modules/nft/application/use-cases/createNFT.use-case.ts b/src/modules/nft/application/use-cases/createNFT.use-case.ts new file mode 100644 index 0000000..0d8dab5 --- /dev/null +++ b/src/modules/nft/application/use-cases/createNFT.use-case.ts @@ -0,0 +1,20 @@ +import { INFTRepository } from "../repositorys/INFTRepository"; +import { NFTEntity } from "../../domain/entities/nft.entity"; +import { CreateNFTDto } from "../../presentation/dto/CreateNFT.dto"; + +export class CreateNFTUseCase { + constructor(private readonly nftRepository: INFTRepository) {} + + async execute(data: CreateNFTDto): Promise { + return await this.nftRepository.create({ + id: data.id, + description: data.description, + createdAt: new Date(), + isMinted: data.isMinted ?? false, + metadataUri: data.metadataUri, + organizationId: data.organizationId, + tokenId: data.tokenId, + userId: data.userId, + }); + } +} diff --git a/src/modules/nft/use-cases/deleteNFT.ts b/src/modules/nft/application/use-cases/deleteNFT.use-case.ts similarity index 61% rename from src/modules/nft/use-cases/deleteNFT.ts rename to src/modules/nft/application/use-cases/deleteNFT.use-case.ts index c026e53..c3e435a 100644 --- a/src/modules/nft/use-cases/deleteNFT.ts +++ b/src/modules/nft/application/use-cases/deleteNFT.use-case.ts @@ -1,6 +1,6 @@ -import { INFTRepository } from "../repositories/INFTRepository"; +import { INFTRepository } from "../repositorys/INFTRepository"; -export class DeleteNFT { +export class DeleteNFTUseCase { constructor(private readonly nftRepository: INFTRepository) {} async execute(id: string) { diff --git a/src/modules/nft/use-cases/getNFT.ts b/src/modules/nft/application/use-cases/getNFT.ts similarity index 62% rename from src/modules/nft/use-cases/getNFT.ts rename to src/modules/nft/application/use-cases/getNFT.ts index 16b9f00..16680d1 100644 --- a/src/modules/nft/use-cases/getNFT.ts +++ b/src/modules/nft/application/use-cases/getNFT.ts @@ -1,6 +1,6 @@ -import { INFTRepository } from "../repositories/INFTRepository"; +import { INFTRepository } from "../repositorys/INFTRepository"; -export class GetNFT { +export class GetNFTUseCase { constructor(private readonly nftRepository: INFTRepository) {} async execute(id: string) { diff --git a/src/modules/nft/use-cases/getNFTByUserId.ts b/src/modules/nft/application/use-cases/getNFTByUserId.ts similarity index 66% rename from src/modules/nft/use-cases/getNFTByUserId.ts rename to src/modules/nft/application/use-cases/getNFTByUserId.ts index 9caa809..f49cfef 100644 --- a/src/modules/nft/use-cases/getNFTByUserId.ts +++ b/src/modules/nft/application/use-cases/getNFTByUserId.ts @@ -1,6 +1,6 @@ -import { INFTRepository } from "../repositories/INFTRepository"; +import { INFTRepository } from "../repositorys/INFTRepository"; -export class GetNFTByUserId { +export class GetNFTByUserIdUseCase { constructor(private readonly nftRepository: INFTRepository) {} async execute(id: string, page: number, pageSize: number) { diff --git a/src/modules/nft/application/use-cases/index.ts b/src/modules/nft/application/use-cases/index.ts new file mode 100644 index 0000000..95e0873 --- /dev/null +++ b/src/modules/nft/application/use-cases/index.ts @@ -0,0 +1,4 @@ +export { CreateNFTUseCase } from "./createNFT.use-case"; +export { DeleteNFTUseCase } from "./deleteNFT.use-case"; +export { GetNFTUseCase } from "./getNFT"; +export { GetNFTByUserIdUseCase } from "./getNFTByUserId"; diff --git a/src/modules/nft/domain/entities/nft.entity.ts b/src/modules/nft/domain/entities/nft.entity.ts index 1b0654f..f9c1313 100644 --- a/src/modules/nft/domain/entities/nft.entity.ts +++ b/src/modules/nft/domain/entities/nft.entity.ts @@ -1,71 +1,35 @@ -import { Entity, Column, ManyToOne, JoinColumn } from "typeorm"; import { BaseEntity } from "../../../shared/domain/entities/base.entity"; -import { Organization } from "../../../organization/domain/entities/organization.entity"; -import { User } from "@/modules/user/domain/entities/User.entity"; +import { INFT } from "../interfaces/nft.interface"; -@Entity("nfts") -export class NFT extends BaseEntity { - @ManyToOne(() => User, { nullable: false }) - @JoinColumn({ name: "userId" }) - user: User; - - @Column({ type: "uuid", nullable: false }) - userId: string; - - @ManyToOne(() => Organization, { nullable: false }) - @JoinColumn({ name: "organizationId" }) - organization: Organization; - - @Column({ type: "uuid", nullable: false }) - organizationId: string; - - @Column({ type: "text", nullable: false }) - description: string; - - @Column({ type: "varchar", length: 255, nullable: true }) - tokenId?: string; - - @Column({ type: "varchar", length: 255, nullable: true }) - contractAddress?: string; - - @Column({ type: "varchar", length: 500, nullable: true }) - metadataUri?: string; - - @Column({ type: "boolean", default: false }) - isMinted: boolean; - - // Domain methods - public mint( - tokenId: string, - contractAddress: string, - metadataUri?: string - ): void { - if (this.isMinted) { - throw new Error("NFT is already minted"); - } - - this.tokenId = tokenId; - this.contractAddress = contractAddress; - this.metadataUri = metadataUri; - this.isMinted = true; +export class NFTEntity extends BaseEntity { + constructor( + public readonly id: string, + public readonly description: string, + public readonly createdAt: Date, + public readonly isMinted?: boolean, + public readonly metadataUri?: string, + public readonly tokenId?: string, + public readonly userId?: string, + public readonly organizationId?: string + ) { + super(); } - public updateMetadata(metadataUri: string): void { - this.metadataUri = metadataUri; + public static create(props: INFT): NFTEntity { + const nft = new NFTEntity( + props.id, + props.description, + props.createdAt, + props.isMinted, + props.metadataUri, + props.tokenId, + props.userId, + props.organizationId + ); + return nft; } public isOwnedBy(userId: string): boolean { return this.userId === userId; } } - -// Keep the simple domain class for use cases -export class NFTDomain { - constructor( - public readonly id: string, - public readonly userId: string, - public readonly organizationId: string, - public readonly description: string, - public readonly createdAt: Date - ) {} -} diff --git a/src/modules/nft/domain/exceptions/nft.exceptions.ts b/src/modules/nft/domain/exceptions/nft.exceptions.ts new file mode 100644 index 0000000..0dd963e --- /dev/null +++ b/src/modules/nft/domain/exceptions/nft.exceptions.ts @@ -0,0 +1,7 @@ +import { DomainException } from "@/modules/shared/domain/exceptions/domain.exception"; + +export class AuthExceptions extends DomainException { + constructor(field: string, value: string) { + super(`Invalid ${field}: ${value}`); + } +} diff --git a/src/modules/nft/domain/interfaces/nft.interface.ts b/src/modules/nft/domain/interfaces/nft.interface.ts index 4ff2f06..499a367 100644 --- a/src/modules/nft/domain/interfaces/nft.interface.ts +++ b/src/modules/nft/domain/interfaces/nft.interface.ts @@ -1,7 +1,10 @@ export interface INFT { id: string; - userId: string; - organizationId: string; description: string; + isMinted: boolean; createdAt: Date; + userId?: string; + organizationId?: string; + metadataUri?: string; + tokenId?: string; } diff --git a/src/modules/nft/dto/create-nft.dto.ts b/src/modules/nft/dto/create-nft.dto.ts deleted file mode 100644 index 57c235b..0000000 --- a/src/modules/nft/dto/create-nft.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IsString, IsUUID, MinLength, MaxLength } from "class-validator"; - -export class CreateNFTDto { - @IsUUID(4, { message: "User ID must be a valid UUID" }) - userId: string; - - @IsUUID(4, { message: "Organization ID must be a valid UUID" }) - organizationId: string; - - @IsString({ message: "Description must be a string" }) - @MinLength(10, { message: "Description must be at least 10 characters long" }) - @MaxLength(1000, { message: "Description cannot exceed 1000 characters" }) - description: string; -} diff --git a/src/modules/nft/dto/response-nft.dto.ts b/src/modules/nft/dto/response-nft.dto.ts deleted file mode 100644 index 994096f..0000000 --- a/src/modules/nft/dto/response-nft.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface NFTResponseDto { - id: string; - userId: string; - organizationId: string; - description: string; - createdAt: Date; - updatedAt: Date; -} diff --git a/src/modules/nft/infrastructure/repositorys/nft.repository.ts b/src/modules/nft/infrastructure/repositorys/nft.repository.ts new file mode 100644 index 0000000..750ac85 --- /dev/null +++ b/src/modules/nft/infrastructure/repositorys/nft.repository.ts @@ -0,0 +1,65 @@ +import { PrismaClient } from "@prisma/client"; +import { INFTRepository } from "../../application/repositorys/INFTRepository"; +import { NFTEntity } from "../../domain/entities/nft.entity"; +import { UpdateNFTDto, CreateNFTDto } from "../../presentation/dto"; +import { INFT } from "../../domain/interfaces/nft.interface"; + +export class NFTRepository implements INFTRepository { + constructor(private readonly prisma: PrismaClient) {} + + async create(nft: CreateNFTDto): Promise { + const newNFT: INFT = await this.prisma.NFT.create({ + data: { + description: nft.description, + isMinted: nft.isMinted ?? false, + metadataUri: nft.metadataUri, + tokenId: nft.tokenId, + userId: nft.userId, + organizationId: nft.organizationId, + }, + }); + + return NFTEntity.create(newNFT); + } + + async findById(id: string): Promise { + const nft = await this.prisma.NFT.findUnique({ where: { id } }); + return nft ? NFTEntity.create(nft) : null; + } + + async findByUserId( + userId: string, + page: number, + pageSize: number + ): Promise<{ nfts: NFTEntity[]; total: number }> { + const skip = (page - 1) * pageSize; + + const [nfts, total] = await Promise.all([ + this.prisma.NFT.findMany({ + where: { userId }, + skip, + take: pageSize, + orderBy: { createdAt: "desc" }, + }), + this.prisma.NFT.count({ where: { userId } }), + ]); + + return { + nfts: nfts.map((nft: INFT) => NFTEntity.create(nft)), + total, + }; + } + + async update(id: string, nft: Partial): Promise { + const updatedNFT = await this.prisma.NFT.update({ + where: { id }, + data: nft, + }); + + return NFTEntity.create(updatedNFT); + } + + async delete(id: string): Promise { + await this.prisma.NFT.delete({ where: { id } }); + } +} diff --git a/src/modules/nft/presentation/controllers/NFTController.disabled b/src/modules/nft/presentation/controllers/NFTController.disabled deleted file mode 100644 index 9fda7e4..0000000 --- a/src/modules/nft/presentation/controllers/NFTController.disabled +++ /dev/null @@ -1,49 +0,0 @@ -import { Request, Response } from "express"; -import NFTService from "../../../../services/NFTService"; - -class NFTController { - // Creates_a_new_NFT_and_returns_the_created_NFT_data_OKK!! - async createNFT(req: Request, res: Response) { - try { - const nft = await NFTService.createNFT(req.body); - res.status(201).json(nft); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - } - // Fetches_an_NFT_by_its_ID_OKK!! - async getNFTById(req: Request, res: Response) { - try { - const nft = await NFTService.getNFTById(req.params.id); - if (nft) { - res.json(nft); - } else { - res.status(404).json({ error: "NFT not found" }); - } - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - } - - // Retrieves_all_NFTs_owned_by_a_specific_user_OKK!! - async getNFTsByUserId(req: Request, res: Response) { - try { - const nfts = await NFTService.getNFTsByUserId(req.params.userId); - res.json(nfts); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - } - - // delete_NFTs_by_a_specific_NFT_id_OKK!! - async deleteNFT(req: Request, res: Response) { - try { - await NFTService.DeleteNFT(req.params.id); - res.json(`succefully delete NFT ${req.params.id}`); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - } -} - -export default new NFTController(); diff --git a/src/modules/nft/presentation/controllers/NFTController.stub.ts b/src/modules/nft/presentation/controllers/NFTController.stub.ts deleted file mode 100644 index 2bbc811..0000000 --- a/src/modules/nft/presentation/controllers/NFTController.stub.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Request, Response } from "express"; - -/** - * Stub controller for NFT functionality - * This replaces the original controller that referenced deleted services - * TODO: Implement proper NFT controller using new modular architecture - */ -class NFTController { - async createNFT(req: Request, res: Response) { - res.status(501).json({ - message: "NFT service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async getNFTById(req: Request, res: Response) { - res.status(501).json({ - message: "NFT service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async getNFTsByUserId(req: Request, res: Response) { - res.status(501).json({ - message: "NFT service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async deleteNFT(req: Request, res: Response) { - res.status(501).json({ - message: "NFT service temporarily disabled during migration", - error: "Service migration in progress" - }); - } -} - -export default new NFTController(); \ No newline at end of file diff --git a/src/modules/nft/presentation/controllers/NFTController.ts b/src/modules/nft/presentation/controllers/NFTController.ts new file mode 100644 index 0000000..de5625d --- /dev/null +++ b/src/modules/nft/presentation/controllers/NFTController.ts @@ -0,0 +1,95 @@ +import { Request, Response } from "express"; +import { asyncHandler } from "@/utils/asyncHandler"; +import { validateDto } from "@/shared/middleware/validation.middleware"; + +import { + CreateNFTUseCase, + DeleteNFTUseCase, + GetNFTByUserIdUseCase, + GetNFTUseCase, +} from "../../application/use-cases"; + +import { CreateNFTDto, UpdateNFTDto } from "../dto"; + +export class NFTController { + constructor( + private readonly createNFTUseCase: CreateNFTUseCase, + private readonly deleteNFTUseCase: DeleteNFTUseCase, + private readonly getNFTByUserIdUseCase: GetNFTByUserIdUseCase, + private readonly getNFTUseCase: GetNFTUseCase + ) {} + + create = asyncHandler(async (req: Request, res: Response) => { + const dto = await validateDto(CreateNFTDto); + if (!dto) return; + + try { + const nft = await this.createNFTUseCase.execute({ + ...req.body, + }); + res.status(201).json(nft); + } catch (err) { + const message = + err instanceof Error ? err.message : "Failed to create NFT"; + res.status(500).json({ error: message }); + } + }); + + delete = asyncHandler(async (req: Request, res: Response) => { + const dto = await validateDto(UpdateNFTDto); + if (!dto) return; + + try { + await this.deleteNFTUseCase.execute(req.body.id); + res.status(200).json({ message: "NFT deleted successfully" }); + } catch (err) { + const message = + err instanceof Error ? err.message : "Failed to delete NFT"; + res.status(500).json({ error: message }); + } + }); + + getNFTById = asyncHandler(async (req: Request, res: Response) => { + const NFTId = typeof req.params.id === "string" ? req.params.id : undefined; + + if (!NFTId) { + res.status(400).json({ error: "NFT ID is required" }); + return; + } + try { + const nfts = await this.getNFTUseCase.execute(NFTId); + res.status(200).json(nfts); + } catch (err) { + const message = + err instanceof Error ? err.message : "Failed to fetch NFTs"; + res.status(500).json({ error: message }); + } + }); + + getNftByUserid = asyncHandler(async (req: Request, res: Response) => { + const userId = + typeof req.params.id === "string" ? req.params.id : undefined; + + if (!userId) { + res.status(400).json({ error: "User ID is required" }); + return; + } + + try { + const nft = await this.getNFTByUserIdUseCase.execute( + userId, + req.body.page, + req.body.pageSize + ); + if (!nft) { + res.status(404).json({ error: "NFT not found for user" }); + return; + } + res.status(200).json(nft); + } catch (err) { + const message = + err instanceof Error ? err.message : "Failed to fetch NFT by userId"; + res.status(500).json({ error: message }); + } + }); +} diff --git a/src/modules/nft/presentation/dto/CreateNFT.dto.ts b/src/modules/nft/presentation/dto/CreateNFT.dto.ts new file mode 100644 index 0000000..e9dfea2 --- /dev/null +++ b/src/modules/nft/presentation/dto/CreateNFT.dto.ts @@ -0,0 +1,43 @@ +import { + IsString, + IsOptional, + IsUrl, + IsDateString, + IsBoolean, + IsUUID, + Length, +} from "class-validator"; + +export class CreateNFTDto { + @IsOptional() + @IsUUID("4", { message: "Invalid NFT ID format" }) + id: string; + + @IsString() + @Length(5, 400) + description: string; + + @IsOptional() + @IsUUID("4", { message: "Invalid userId format" }) + userId?: string; + + @IsOptional() + @IsUUID("4", { message: "Invalid organizationId format" }) + organizationId?: string; + + @IsOptional() + @IsString({ message: "Token ID must be a string" }) + tokenId?: string; + + @IsOptional() + @IsUrl({}, { message: "metadataUri must be a valid URL" }) + metadataUri?: string; + + @IsOptional() + @IsBoolean({ message: "isMinted must be a boolean" }) + isMinted?: boolean; + + @IsOptional() + @IsDateString({}, { message: "createdAt must be a valid date" }) + createdAt?: Date; +} diff --git a/src/modules/nft/presentation/dto/UpdateNFT.dto.ts b/src/modules/nft/presentation/dto/UpdateNFT.dto.ts new file mode 100644 index 0000000..49cfacb --- /dev/null +++ b/src/modules/nft/presentation/dto/UpdateNFT.dto.ts @@ -0,0 +1,38 @@ +import { + IsString, + IsOptional, + IsUrl, + IsBoolean, + IsDateString, + IsUUID, +} from "class-validator"; + +export class UpdateNFTDto { + @IsOptional() + @IsUUID("4", { message: "Invalid NFT ID format" }) + id?: string; + + @IsOptional() + @IsUUID("4", { message: "Invalid userId format" }) + userId?: string; + + @IsOptional() + @IsUUID("4", { message: "Invalid organizationId format" }) + organizationId?: string; + + @IsOptional() + @IsString({ message: "Token ID must be a string" }) + tokenId?: string; + + @IsOptional() + @IsUrl({}, { message: "metadataUri must be a valid URL" }) + metadataUri?: string; + + @IsOptional() + @IsBoolean({ message: "isMinted must be a boolean" }) + isMinted?: boolean; + + @IsOptional() + @IsDateString({}, { message: "createdAt must be a valid date" }) + createdAt?: Date; +} diff --git a/src/modules/nft/presentation/dto/index.ts b/src/modules/nft/presentation/dto/index.ts new file mode 100644 index 0000000..c33bec3 --- /dev/null +++ b/src/modules/nft/presentation/dto/index.ts @@ -0,0 +1,2 @@ +export { CreateNFTDto } from "./CreateNFT.dto"; +export { UpdateNFTDto } from "./UpdateNFT.dto"; diff --git a/src/modules/nft/presentation/routes.ts b/src/modules/nft/presentation/routes.ts new file mode 100644 index 0000000..35d2f7f --- /dev/null +++ b/src/modules/nft/presentation/routes.ts @@ -0,0 +1,35 @@ +import { Router } from "express"; + +import { NFTController } from "./controllers/NFTController"; + +import { + CreateNFTUseCase, + DeleteNFTUseCase, + GetNFTByUserIdUseCase, + GetNFTUseCase, +} from "../application/use-cases"; + +import { NFTRepository } from "../infrastructure/repositorys/nft.repository"; + +export class AuthRoutes { + static get routes(): Router { + const router = Router(); + + //Repository + const repository = new NFTRepository(prisma); + + const controller = new NFTController( + new CreateNFTUseCase(repository), + new DeleteNFTUseCase(repository), + new GetNFTByUserIdUseCase(repository), + new GetNFTUseCase(repository) + ); + + router.post("/", controller.create); + router.delete("/", controller.delete); + router.get("/getById/:id", controller.getNFTById); + router.get("/getByUserId/:id", controller.getNftByUserid); + + return router; + } +} diff --git a/src/modules/nft/repositories/INFTRepository.ts b/src/modules/nft/repositories/INFTRepository.ts deleted file mode 100644 index a9956ff..0000000 --- a/src/modules/nft/repositories/INFTRepository.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NFTDomain as NFT } from "../domain/entities/nft.entity"; -import { INFT } from "../domain/interfaces/nft.interface"; - -export interface INFTRepository { - create(nft: INFT): Promise; - findById(id: string): Promise; - findByUserId( - userId: string, - page: number, - pageSize: number - ): Promise<{ nfts: NFT[]; total: number }>; - update(id: string, nft: Partial): Promise; - delete(id: string): Promise; -} diff --git a/src/modules/nft/repositories/nft.repository.ts b/src/modules/nft/repositories/nft.repository.ts deleted file mode 100644 index 05c7bbb..0000000 --- a/src/modules/nft/repositories/nft.repository.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { PrismaClient } from "@prisma/client"; -import { INFTRepository } from "./INFTRepository"; -import { NFTDomain as NFT } from "../domain/entities/nft.entity"; - -// Define our own types based on the Prisma schema -interface PrismaNFT { - id: string; - createdAt: Date; - updatedAt: Date; - userId: string; - organizationId: string; - description: string; -} - -export class NFTRepository implements INFTRepository { - private prisma = new PrismaClient(); - - async create(nft: NFT): Promise { - const newNFT = (await this.prisma.nFT.create({ - data: { - userId: nft.userId, - organizationId: nft.organizationId, - description: nft.description, - }, - })) as unknown as PrismaNFT; - - return new NFT( - newNFT.id, - newNFT.userId, - newNFT.organizationId, - newNFT.description, - newNFT.createdAt - ); - } - - async findById(id: string): Promise { - const nft = (await this.prisma.nFT.findUnique({ - where: { id }, - })) as unknown as PrismaNFT | null; - return nft - ? new NFT( - nft.id, - nft.userId, - nft.organizationId, - nft.description, - nft.createdAt - ) - : null; - } - - async findByUserId( - userId: string, - page: number, - pageSize: number - ): Promise<{ nfts: NFT[]; total: number }> { - const skip = (page - 1) * pageSize; - - const [nfts, total] = await Promise.all([ - this.prisma.nFT.findMany({ - where: { userId }, - skip, - take: pageSize, - orderBy: { createdAt: "desc" }, - }), - this.prisma.nFT.count({ where: { userId } }), - ]); - - return { - nfts: (nfts as unknown as PrismaNFT[]).map( - (nft) => - new NFT( - nft.id, - nft.userId, - nft.organizationId, - nft.description, - nft.createdAt - ) - ), - total, - }; - } - - async update(id: string, nft: Partial): Promise { - const updatedNFT = (await this.prisma.nFT.update({ - where: { id }, - data: nft, - })) as unknown as PrismaNFT; - - return new NFT( - updatedNFT.id, - updatedNFT.userId, - updatedNFT.organizationId, - updatedNFT.description, - updatedNFT.createdAt - ); - } - - async delete(id: string): Promise { - await this.prisma.nFT.delete({ where: { id } }); - } -} diff --git a/src/modules/nft/use-cases/createNFT.ts b/src/modules/nft/use-cases/createNFT.ts deleted file mode 100644 index 3971afb..0000000 --- a/src/modules/nft/use-cases/createNFT.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { INFTRepository } from "../repositories/INFTRepository"; -import { NFTDomain as NFT } from "../domain/entities/nft.entity"; -import { CreateNFTDto } from "../dto/create-nft.dto"; - -export class CreateNFT { - constructor(private readonly nftRepository: INFTRepository) {} - - async execute(data: CreateNFTDto): Promise { - const nft = new NFT( - Date.now().toString(), - data.userId, - data.organizationId, - data.description, - new Date() - ); - return await this.nftRepository.create(nft); - } -} diff --git a/src/modules/organization/domain/entities/organization.entity.ts b/src/modules/organization/domain/entities/organization.entity.ts index e193e70..d6bb487 100644 --- a/src/modules/organization/domain/entities/organization.entity.ts +++ b/src/modules/organization/domain/entities/organization.entity.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { BaseEntity } from "../../../shared/domain/entities/base.entity"; export interface OrganizationProps { @@ -12,9 +13,12 @@ export interface OrganizationProps { isVerified: boolean; logoUrl?: string; walletAddress?: string; + createdAt?: Date; + updatedAt?: Date; } -export class Organization extends BaseEntity { +export class OrganizationEntity extends BaseEntity { + public readonly id: string; public readonly name: string; public readonly email: string; public readonly description: string; @@ -28,7 +32,7 @@ export class Organization extends BaseEntity { constructor( props: OrganizationProps, - id?: string, + id: string, createdAt?: Date, updatedAt?: Date ) { @@ -43,14 +47,19 @@ export class Organization extends BaseEntity { this.isVerified = props.isVerified; this.logoUrl = props.logoUrl; this.walletAddress = props.walletAddress; + this.createdAt = props.createdAt ?? new Date(); + this.updatedAt = props.updatedAt ?? new Date(); } - public static create(props: OrganizationProps, id?: string): Organization { - return new Organization(props, id); + public static create( + props: OrganizationProps, + id: string + ): OrganizationEntity { + return new OrganizationEntity(props, id); } - public update(props: Partial): Organization { - return new Organization( + public update(props: Partial): OrganizationEntity { + return new OrganizationEntity( { id: this.id, name: props.name ?? this.name, diff --git a/src/modules/organization/presentation/controllers/OrganizationController.disabled b/src/modules/organization/presentation/controllers/OrganizationController.disabled deleted file mode 100644 index 2c8029d..0000000 --- a/src/modules/organization/presentation/controllers/OrganizationController.disabled +++ /dev/null @@ -1,86 +0,0 @@ -import { Request, Response } from "express"; -import { OrganizationService } from "../../../../services/OrganizationService"; -import { asyncHandler } from "../../../../utils/asyncHandler"; - -class OrganizationController { - private organizationService: OrganizationService; - - constructor() { - this.organizationService = new OrganizationService(); - } - - createOrganization = asyncHandler( - async (req: Request, res: Response): Promise => { - const { name, email, password, category, wallet } = req.body; - const organization = await this.organizationService.createOrganization( - name, - email, - password, - category, - wallet - ); - res.status(201).json(organization); - } - ); - - getOrganizationById = asyncHandler( - async (req: Request, res: Response): Promise => { - const { id } = req.params; - const organization = - await this.organizationService.getOrganizationById(id); - - if (!organization) { - res.status(404).json({ error: "Organization not found" }); - return; - } - - res.status(200).json(organization); - } - ); - - getOrganizationByEmail = asyncHandler( - async (req: Request, res: Response): Promise => { - const { email } = req.params; - const organization = - await this.organizationService.getOrganizationByEmail(email); - - if (!organization) { - res.status(404).json({ error: "Organization not found" }); - return; - } - - res.status(200).json(organization); - } - ); - - updateOrganization = asyncHandler( - async (req: Request, res: Response): Promise => { - const { id } = req.params; - const updateData = req.body; - - const organization = await this.organizationService.updateOrganization( - id, - updateData - ); - res.status(200).json(organization); - } - ); - - deleteOrganization = asyncHandler( - async (req: Request, res: Response): Promise => { - const { id } = req.params; - await this.organizationService.deleteOrganization(id); - res.status(204).send(); - } - ); - - getAllOrganizations = asyncHandler( - async (req: Request, res: Response): Promise => { - const organizations = - await this.organizationService.getAllOrganizations(); - res.status(200).json(organizations); - } - ); -} - -export default new OrganizationController(); diff --git a/src/modules/organization/presentation/controllers/organization.controller.ts b/src/modules/organization/presentation/controllers/organization.controller.ts index e306815..e1fc62c 100644 --- a/src/modules/organization/presentation/controllers/organization.controller.ts +++ b/src/modules/organization/presentation/controllers/organization.controller.ts @@ -8,10 +8,7 @@ import { GetAllOrganizationsUseCase } from "../../application/use-cases/get-all- import { CreateOrganizationDto } from "../dto/create-organization.dto"; import { UpdateOrganizationDto } from "../dto/update-organization.dto"; import { OrganizationNotFoundException } from "../../domain/exceptions/organization-not-found.exception"; -import { - UuidParamsDto, - PaginationQueryDto, -} from "../../../shared/dto/base.dto"; +import { PaginationQueryDto, UuidParamsDto } from "@/shared/dto/base.dto"; export class OrganizationController { constructor( @@ -40,7 +37,7 @@ export class OrganizationController { ); getOrganizationById = asyncHandler( - async (req: Request, res: Response): Promise => { + async (req: Request, res: Response): Promise => { const { id } = req.params; try { diff --git a/src/modules/photo/README.md b/src/modules/photo/README.md new file mode 100644 index 0000000..a9741a1 --- /dev/null +++ b/src/modules/photo/README.md @@ -0,0 +1,36 @@ +# 📸 Photos Module + +This module handles uploading, fetching, and deleting images. +Unlike other modules in the application, its architecture is slightly different, since file handling introduces particular requirements (e.g., Supabase Storage, Multer, public URLs). + +# 🔑 Key Architectural Differences + +No use cases or repositories are used, only maintained Entity to get easy access on this information, but this Entity is generate for mi adapter, adapter interface required a return of PhotoEntity. +The flow is more direct: + +Controller → calls the Adapter for Supabase Storage and Prisma for persistence. + +Entity → ensures photo consistency (valid URL, userId, metadata). + +Dedicated Adapters: + +photo-service.adapter.interface.ts → defines the generic interface for a storage service. + +supabase-service.adapter.ts → concrete implementation using Supabase Storage. + +Simplified Controllers: +Controllers use asyncHandler, validators, and Multer for file processing. + +# 📂 Structure + +```text +modules/ + photo/ + domain/entities/interfaces/ #interfaces of entity + domain/entitites/photo.entity.ts #Entity + infrastructure/ + presentation/controllers/ + presentation/dto + routes.ts +README.md +``` diff --git a/src/modules/photo/domain/entities/interfaces/photo.interface.ts b/src/modules/photo/domain/entities/interfaces/photo.interface.ts new file mode 100644 index 0000000..ab53da3 --- /dev/null +++ b/src/modules/photo/domain/entities/interfaces/photo.interface.ts @@ -0,0 +1,49 @@ +export interface IStorageAdapter { + upload(file: Buffer, filename: string): Promise<{ url: string; key: string }>; + delete(key: string): Promise; +} + +export interface IPhotoMetadata { + fileSize?: number; + mimeType?: string; + dimensions?: { + width: number; + height: number; + }; + camera?: { + make?: string; + model?: string; + settings?: { + iso?: number; + aperture?: string; + shutterSpeed?: string; + }; + }; + location?: { + latitude?: number; + longitude?: number; + address?: string; + }; + tags?: string[]; + description?: string; + [key: string]: unknown; // Allow additional properties +} + +export interface IPhotoProps { + id?: string; + url: string; + userId: string; + uploadedAt?: Date; + metadata?: IPhotoMetadata; +} + +export interface IPhoto { + id: string; + url: string; + userId: string; + uploadedAt: Date; + metadata?: IPhotoMetadata; + validate(): boolean; + updateMetadata(newMetadata: Partial): void; + toObject(): IPhotoProps; +} diff --git a/src/modules/photo/domain/entities/photo.entity.ts b/src/modules/photo/domain/entities/photo.entity.ts index f8a3693..1180a09 100644 --- a/src/modules/photo/domain/entities/photo.entity.ts +++ b/src/modules/photo/domain/entities/photo.entity.ts @@ -1,44 +1,36 @@ -import { Entity, Column } from "typeorm"; import { BaseEntity } from "../../../shared/domain/entities/base.entity"; +import { IPhotoMetadata, IPhotoProps } from "./interfaces/photo.interface"; +import { + InvalidPhotoUrlException, + MissingUserIdException, +} from "../exceptions/domain.exception"; -export interface PhotoProps { - id?: string; - url: string; - userId: string; - uploadedAt?: Date; - metadata?: Record; -} - -@Entity("photos") -export class Photo extends BaseEntity { - @Column({ type: "varchar", length: 500, nullable: false }) +export class PhotoEntity extends BaseEntity { url: string; - @Column({ type: "uuid", nullable: false }) userId: string; - @Column({ type: "jsonb", nullable: true }) - metadata?: Record; + metadata?: IPhotoMetadata; // Domain logic and validation - public validate(): boolean { + protected validate(): boolean { if (!this.url || this.url.trim() === "") { - throw new Error("Photo URL is required"); + throw new InvalidPhotoUrlException(this.url); } if (!/^https?:\/\/.+$/.test(this.url)) { - throw new Error("Photo URL must be a valid HTTP/HTTPS URL"); + throw new InvalidPhotoUrlException(this.url); } if (!this.userId || this.userId.trim() === "") { - throw new Error("User ID is required"); + throw new MissingUserIdException(); } return true; } // Update metadata - public updateMetadata(newMetadata: Record): void { + public updateMetadata(newMetadata: IPhotoMetadata): void { this.metadata = { ...this.metadata, ...newMetadata, @@ -46,8 +38,8 @@ export class Photo extends BaseEntity { } // Static factory method - public static create(props: PhotoProps): Photo { - const photo = new Photo(); + public static create(props: IPhotoProps): PhotoEntity { + const photo = new PhotoEntity(); photo.url = props.url; photo.userId = props.userId; photo.metadata = props.metadata ?? {}; @@ -56,7 +48,7 @@ export class Photo extends BaseEntity { } // Convert to plain object for persistence - public toObject(): PhotoProps { + public toObject(): IPhotoProps { return { id: this.id, url: this.url, diff --git a/src/modules/photo/domain/exceptions/domain.exception.ts b/src/modules/photo/domain/exceptions/domain.exception.ts new file mode 100644 index 0000000..4faa74f --- /dev/null +++ b/src/modules/photo/domain/exceptions/domain.exception.ts @@ -0,0 +1,13 @@ +import { DomainException } from "../../../shared/domain/exceptions/domain.exception"; + +export class InvalidPhotoUrlException extends DomainException { + constructor(url: string) { + super(`Invalid photo URL: ${url}`); + } +} + +export class MissingUserIdException extends DomainException { + constructor() { + super("User ID is required"); + } +} diff --git a/src/modules/photo/entities/photo.entity.ts b/src/modules/photo/entities/photo.entity.ts deleted file mode 100644 index 5de9c11..0000000 --- a/src/modules/photo/entities/photo.entity.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export the domain entity -export { Photo, PhotoProps } from "../domain/entities/photo.entity"; diff --git a/src/modules/photo/infrastructure/adapters/interface/photo-service.adapter.ts b/src/modules/photo/infrastructure/adapters/interface/photo-service.adapter.ts new file mode 100644 index 0000000..df7bf02 --- /dev/null +++ b/src/modules/photo/infrastructure/adapters/interface/photo-service.adapter.ts @@ -0,0 +1,16 @@ +import { IPhotoMetadata } from "@/modules/photo/domain/entities/interfaces/photo.interface"; +import { PhotoEntity } from "@/modules/photo/domain/entities/photo.entity"; + +export interface IPhotoServiceAdapter { + upload( + file: Express.Multer.File, + userId: string, + metadata?: IPhotoMetadata + ): Promise; + getById(photoId: string): Promise<{ id: string; url: string } | null>; + delete(photoUrl: string): Promise; + updateMetadata( + photoId: string, + metadata: IPhotoMetadata + ): Promise; +} diff --git a/src/modules/photo/infrastructure/adapters/supabase-service.adapter.ts b/src/modules/photo/infrastructure/adapters/supabase-service.adapter.ts new file mode 100644 index 0000000..39062ed --- /dev/null +++ b/src/modules/photo/infrastructure/adapters/supabase-service.adapter.ts @@ -0,0 +1,105 @@ +import { prisma } from "@/config/prisma"; +import { IPhotoMetadata } from "../../domain/entities/interfaces/photo.interface"; +import { IPhotoServiceAdapter } from "./interface/photo-service.adapter"; +import { supabase } from "@/config/supabase"; +import { PhotoEntity } from "../../domain/entities/photo.entity"; + +export class SupabasePhotoService implements IPhotoServiceAdapter { + //Metadata options are enabled if needed later + async upload( + file: Express.Multer.File, + userId: string, + metadata?: IPhotoMetadata + ): Promise { + const fileName = `${Date.now()}-${file.originalname}`; + + // Subir archivo a Supabase Storage + const { error: uploadError } = await supabase.storage + .from("photos") + .upload(fileName, file.buffer, { + contentType: file.mimetype, + upsert: false, + }); + + if (uploadError) throw uploadError; + + // Obtener URL pública + const { data: publicUrl } = supabase.storage + .from("photos") + .getPublicUrl(fileName); + + if (!publicUrl) throw new Error("Could not get public URL"); + + // Guardar en DB con Prisma + const photo = await prisma.photo.create({ + data: { + userId, + url: publicUrl.publicUrl, + metadata: { + ...metadata, + fileName: file.originalname, + fileSize: file.size, + mimeType: file.mimetype, + }, + uploadedAt: new Date(), + }, + }); + + return PhotoEntity.create({ + id: photo.url, + url: photo.url, + userId, + metadata, + uploadedAt: photo.uploadedAt ?? new Date(), + }); + } + + async delete(photoId: string): Promise { + // Find in DB + const photo = await prisma.photo.findUnique({ + where: { id: photoId }, + }); + + if (!photo) { + throw new Error("Photo not found"); + } + + // Extract fileName from public URL (what follows after /photos/) + const urlParts = photo.url.split("/"); + const fileName = urlParts[urlParts.length - 1]; + + // Delete from Supabase Storage using supabaseStorage + const { error: deleteError } = await supabase.storage + .from("photos") + .remove([fileName]); + + if (deleteError) throw deleteError; + + // Delete from DB + await prisma.photo.delete({ where: { id: photoId } }); + } + + async getById(photoId: string): Promise<{ id: string; url: string } | null> { + const photo = await prisma.photo.findUnique({ + where: { id: photoId }, + }); + + if (!photo) return null; + + return { id: photo.id, url: photo.url }; + } + + async updateMetadata( + photoId: string, + metadata: IPhotoMetadata + ): Promise { + const updatedPhoto = await prisma.photo.update({ + where: { id: photoId }, + data: { metadata }, + select: { metadata: true }, + }); + + // Assuming metadata is not null after update + return updatedPhoto.metadata as IPhotoMetadata; + } +} diff --git a/src/modules/photo/infrastructure/services/BackblazeService.ts b/src/modules/photo/infrastructure/services/BackblazeService.ts deleted file mode 100644 index a197fc7..0000000 --- a/src/modules/photo/infrastructure/services/BackblazeService.ts +++ /dev/null @@ -1,45 +0,0 @@ -import B2 from "backblaze-b2"; -import fs from "fs"; - -export class BackblazeService { - private b2: B2; - - constructor() { - this.b2 = new B2({ - applicationKeyId: process.env.BACKBLAZE_APPLICATION_KEY_ID!, - applicationKey: process.env.BACKBLAZE_APPLICATION_KEY!, - }); - } - - async authorize() { - await this.b2.authorize(); - } - - async uploadFile(filePath: string, fileName: string): Promise { - await this.authorize(); - - const { data } = await this.b2.getUploadUrl({ - bucketId: process.env.BACKBLAZE_BUCKET_ID!, - }); - - const buffer = fs.readFileSync(filePath); - - const upload = await this.b2.uploadFile({ - uploadUrl: data.uploadUrl, - uploadAuthToken: data.authorizationToken, - fileName, - data: buffer, - }); - - return upload.data.fileId; - } - - async deleteFile(fileId: string, fileName: string): Promise { - await this.authorize(); - - await this.b2.deleteFileVersion({ - fileId, - fileName, - }); - } -} diff --git a/src/modules/photo/interfaces/photo.interface.ts b/src/modules/photo/interfaces/photo.interface.ts deleted file mode 100644 index 2c4fd74..0000000 --- a/src/modules/photo/interfaces/photo.interface.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface IPhotoProps { - id?: string; - url: string; - userId: string; - uploadedAt?: Date; - metadata?: Record; -} - -export interface IPhoto { - id: string; - url: string; - userId: string; - uploadedAt: Date; - metadata?: Record; - - validate(): boolean; - updateMetadata(newMetadata: Record): void; - toObject(): IPhotoProps; -} diff --git a/src/modules/photo/presentation/controllers/PhotoController.ts b/src/modules/photo/presentation/controllers/PhotoController.ts deleted file mode 100644 index df20197..0000000 --- a/src/modules/photo/presentation/controllers/PhotoController.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Request, Response, Router, NextFunction } from "express"; -import multer from "multer"; -import { PrismaClient } from "@prisma/client"; -import { body, param, validationResult } from "express-validator"; -import path from "path"; - -// Extend the Request interface to include the 'file' property -interface MulterRequest extends Request { - file?: Express.Multer.File; -} - -const prisma = new PrismaClient(); -const router = Router(); -const upload = multer({ dest: "uploads/" }); - -// Middleware for handling validation errors -const validate = - ( - validations: { - run: ( - req: Request - ) => Promise<{ isEmpty: () => boolean; array: () => any[] }>; - }[] - ) => - async (req: Request, res: Response, next: NextFunction): Promise => { - for (const validation of validations) { - const result = await validation.run(req); - if (!result.isEmpty()) { - res.status(400).json({ errors: result.array() }); - return; - } - } - next(); - }; - -router.post( - "/upload", - upload.single("photo"), - validate([body("userId").isInt().withMessage("userId must be an integer")]), - async (req: MulterRequest, res: Response): Promise => { - try { - if (!req.file) { - res.status(400).json({ error: "Photo is required" }); - return; - } - const { userId } = req.body; - const photo = await prisma.photo.create({ - data: { - userId: parseInt(userId, 10).toString(), - uploadedAt: req.file.path, - url: req.file.path, // Assuming the file path is used as the URL - }, - }); - res.status(201).json(photo); - } catch (error) { - res.status(500).json({ error: "Internal Server Error" }); - } - } -); - -// Get photo by ID -router.get( - "/:id", - validate([param("id").isInt().withMessage("Photo ID must be an integer")]), - async (req: Request, res: Response): Promise => { - try { - const { id } = req.params; - const photo = await prisma.photo.findUnique({ where: { id } }); - - if (!photo) { - res.status(404).json({ error: "Photo not found" }); - return; - } - - res.json(photo); - } catch (error) { - res.status(500).json({ error: "Internal Server Error" }); - } - } -); - -export default router; diff --git a/src/modules/photo/presentation/controllers/photo-controller.ts b/src/modules/photo/presentation/controllers/photo-controller.ts new file mode 100644 index 0000000..fcfcb42 --- /dev/null +++ b/src/modules/photo/presentation/controllers/photo-controller.ts @@ -0,0 +1,106 @@ +import { Request, Response } from "express"; +import { asyncHandler } from "@/utils/asyncHandler"; +import { SupabasePhotoService } from "../../infrastructure/adapters/supabase-service.adapter"; + +// Extend the Request interface to include the 'file' property +interface MulterRequest extends Request { + file?: Express.Multer.File; +} + +export class PhotoController { + constructor(private readonly supabasePhotoService: SupabasePhotoService) {} + + uploadPhoto = asyncHandler( + async (req: MulterRequest, res: Response): Promise => { + if (!req.file) { + res.status(400).json({ error: "Photo is required" }); + return; + } + + const { userId } = req.body; + + if (!userId) { + res.status(400).json({ error: "User ID is required" }); + return; + } + + const photo = await this.supabasePhotoService.upload(req.file, userId); + res.status(201).json(photo); + } + ); + + deletePhoto = asyncHandler( + async (req: Request, res: Response): Promise => { + const { id } = req.params; + + // Verificar si la foto existe usando el servicio + const photo = await this.supabasePhotoService.getById(id); + + if (!photo) { + res.status(404).json({ error: "Photo not found" }); + return; + } + + // Eliminar la foto usando el servicio + await this.supabasePhotoService.delete(id); + res.status(204).send(); // No content + } + ); + + getPhotoById = asyncHandler( + async (req: Request, res: Response): Promise => { + const { id } = req.params; + + const photo = await this.supabasePhotoService.getById(id); + + if (!photo) { + res.status(404).json({ error: "Photo not found" }); + return; + } + + res.json(photo); + } + ); + + updatePhoto = asyncHandler( + async (req: Request, res: Response): Promise => { + const { id } = req.params; + const updateData = req.body; + + // Verificar si la foto existe + const existingPhoto = await this.supabasePhotoService.getById(id); + + if (!existingPhoto) { + res.status(404).json({ error: "Photo not found" }); + return; + } + + const updatedPhoto = await this.supabasePhotoService.upload( + updateData, + id + ); + res.json(updatedPhoto); + } + ); + + updatePhotoMetadata = asyncHandler( + async (req: Request, res: Response): Promise => { + const { id } = req.params; + const { metadata } = req.body; + + // Verificar si la foto existe + const existingPhoto = await this.supabasePhotoService.getById(id); + + if (!existingPhoto) { + res.status(404).json({ error: "Photo not found" }); + return; + } + + const updatedPhoto = await this.supabasePhotoService.updateMetadata( + id, + metadata + ); + res.json(updatedPhoto); + } + ); +} diff --git a/src/modules/photo/presentation/dto/delete-photo.dto.ts b/src/modules/photo/presentation/dto/delete-photo.dto.ts new file mode 100644 index 0000000..9f6517d --- /dev/null +++ b/src/modules/photo/presentation/dto/delete-photo.dto.ts @@ -0,0 +1,6 @@ +import { IsInt } from "class-validator"; + +export class DeletePhotoDto { + @IsInt({ message: "Photo ID must be an integer" }) + id!: number; +} diff --git a/src/modules/photo/presentation/dto/get-photo.dto.ts b/src/modules/photo/presentation/dto/get-photo.dto.ts new file mode 100644 index 0000000..e11f397 --- /dev/null +++ b/src/modules/photo/presentation/dto/get-photo.dto.ts @@ -0,0 +1,6 @@ +import { IsInt } from "class-validator"; + +export class GetPhotoDto { + @IsInt({ message: "Photo ID must be an integer" }) + id!: number; +} diff --git a/src/modules/photo/presentation/dto/index.ts b/src/modules/photo/presentation/dto/index.ts new file mode 100644 index 0000000..ea67d02 --- /dev/null +++ b/src/modules/photo/presentation/dto/index.ts @@ -0,0 +1,3 @@ +export { DeletePhotoDto } from "./delete-photo.dto"; +export { GetPhotoDto } from "./get-photo.dto"; +export { UploadPhotoDto } from "./upload-photo.dto"; diff --git a/src/modules/photo/presentation/dto/upload-photo.dto.ts b/src/modules/photo/presentation/dto/upload-photo.dto.ts new file mode 100644 index 0000000..a1d1bd8 --- /dev/null +++ b/src/modules/photo/presentation/dto/upload-photo.dto.ts @@ -0,0 +1,6 @@ +import { IsInt } from "class-validator"; + +export class UploadPhotoDto { + @IsInt({ message: "userId must be an integer" }) + userId!: number; +} diff --git a/src/modules/photo/presentation/routes.ts b/src/modules/photo/presentation/routes.ts new file mode 100644 index 0000000..ae1ddc1 --- /dev/null +++ b/src/modules/photo/presentation/routes.ts @@ -0,0 +1,25 @@ +import { Router } from "express"; +import multer from "multer"; +import { PhotoController } from "./controllers/photo-controller"; +import { validateDto } from "@/shared/middleware/validation.middleware"; +import { DeletePhotoDto, GetPhotoDto, UploadPhotoDto } from "./dto"; +import { SupabasePhotoService } from "../infrastructure/adapters/supabase-service.adapter"; + +const router = Router(); +const upload = multer({ dest: "uploads/" }); +const service = new SupabasePhotoService(); +const photoController = new PhotoController(service); + +// Middleware for handling validation errors +router.post( + "/upload", + upload.single("photo"), + validateDto(UploadPhotoDto), + photoController.uploadPhoto +); + +router.delete("/:id", validateDto(DeletePhotoDto), photoController.deletePhoto); + +router.get("/:id", validateDto(GetPhotoDto), photoController.getPhotoById); + +export default router; diff --git a/src/modules/project/application/repositories/IProjectRepository.ts b/src/modules/project/application/repositories/IProjectRepository.ts new file mode 100644 index 0000000..b864a80 --- /dev/null +++ b/src/modules/project/application/repositories/IProjectRepository.ts @@ -0,0 +1,10 @@ +import { IProject, ProjectEntity } from "../../domain/entities/project.entity"; + +export interface IProjectRepository { + findById(id: string): Promise; + findAll(): Promise; + findByOrganizationId(organizationId: string): Promise; + save(project: IProject): Promise; + update(project: ProjectEntity): Promise; + delete(id: string): Promise; +} diff --git a/src/modules/project/application/use-cases/AssignVolunteersToProject.use-case.ts b/src/modules/project/application/use-cases/AssignVolunteersToProject.use-case.ts new file mode 100644 index 0000000..9bd13c5 --- /dev/null +++ b/src/modules/project/application/use-cases/AssignVolunteersToProject.use-case.ts @@ -0,0 +1,18 @@ +import { IProjectRepository } from "../repositories/IProjectRepository"; +import { ProjectEntity } from "../../domain/entities/project.entity"; +import { UpdateProjectDto } from "../../presentation/dto/UpdateProjectDto"; + +export class AssignVolunteersToProjectUseCase { + constructor(private projectRepository: IProjectRepository) {} + + async execute(id: string, dto: UpdateProjectDto): Promise { + const project = await this.projectRepository.findById(id); + + if (!project) { + throw new Error("Project not found"); + } + + project.update(dto); + return this.projectRepository.update(project); + } +} diff --git a/src/modules/project/application/use-cases/CreateProject.use-case.ts b/src/modules/project/application/use-cases/CreateProject.use-case.ts new file mode 100644 index 0000000..cc552dc --- /dev/null +++ b/src/modules/project/application/use-cases/CreateProject.use-case.ts @@ -0,0 +1,40 @@ +import { IProjectRepository } from "../repositories/IProjectRepository"; +import { ProjectEntity } from "../../domain/entities/project.entity"; +import { ProjectStatus } from "../../domain/enum/ProjectStatus.enum"; +import { UserVolunteer } from "@/modules/user/domain/entities/user-volunteer.entity"; + +export interface PropsCreateProject { + id: string; + name: string; + description: string; + location: string; + startDate: Date; + endDate: Date; + organizationId: string; + status?: ProjectStatus; + volunteers?: UserVolunteer[] | UserVolunteer; +} + +export class CreateProjectUseCase { + constructor(private projectRepository: IProjectRepository) {} + + async execute(props: PropsCreateProject): Promise { + const project = ProjectEntity.create( + props.id, + props.name, + props.description, + props.location, + props.startDate, + props.endDate, + props.organizationId, + props.status || ProjectStatus.DRAFT, + props.volunteers + ? Array.isArray(props.volunteers) + ? props.volunteers + : [props.volunteers] + : [] + ); + + return this.projectRepository.save(project); + } +} diff --git a/src/modules/project/use-cases/DeleteProjectUseCase.ts b/src/modules/project/application/use-cases/DeleteProjectUseCase.ts similarity index 100% rename from src/modules/project/use-cases/DeleteProjectUseCase.ts rename to src/modules/project/application/use-cases/DeleteProjectUseCase.ts diff --git a/src/modules/project/use-cases/GetProjectUseCase.ts b/src/modules/project/application/use-cases/GetProjectUseCase.ts similarity index 72% rename from src/modules/project/use-cases/GetProjectUseCase.ts rename to src/modules/project/application/use-cases/GetProjectUseCase.ts index 5923acf..eacea7b 100644 --- a/src/modules/project/use-cases/GetProjectUseCase.ts +++ b/src/modules/project/application/use-cases/GetProjectUseCase.ts @@ -1,10 +1,10 @@ import { IProjectRepository } from "../repositories/IProjectRepository"; -import { Project } from "../domain/Project"; +import { ProjectEntity } from "../../domain/entities/project.entity"; export class GetProjectUseCase { constructor(private projectRepository: IProjectRepository) {} - async execute(id: string): Promise { + async execute(id: string): Promise { const project = await this.projectRepository.findById(id); if (!project) { diff --git a/src/modules/project/use-cases/ListProjectsUseCase.ts b/src/modules/project/application/use-cases/ListProjectsUseCase.ts similarity index 70% rename from src/modules/project/use-cases/ListProjectsUseCase.ts rename to src/modules/project/application/use-cases/ListProjectsUseCase.ts index 01bd1a6..9ebfc7a 100644 --- a/src/modules/project/use-cases/ListProjectsUseCase.ts +++ b/src/modules/project/application/use-cases/ListProjectsUseCase.ts @@ -1,10 +1,10 @@ import { IProjectRepository } from "../repositories/IProjectRepository"; -import { Project } from "../domain/Project"; +import { ProjectEntity } from "../../domain/entities/project.entity"; export class ListProjectsUseCase { constructor(private projectRepository: IProjectRepository) {} - async execute(organizationId?: string): Promise { + async execute(organizationId?: string): Promise { if (organizationId) { return this.projectRepository.findByOrganizationId(organizationId); } diff --git a/src/modules/project/use-cases/UpdateProjectUseCase.ts b/src/modules/project/application/use-cases/UpdateProjectUseCase.ts similarity index 74% rename from src/modules/project/use-cases/UpdateProjectUseCase.ts rename to src/modules/project/application/use-cases/UpdateProjectUseCase.ts index b7a428d..969ce53 100644 --- a/src/modules/project/use-cases/UpdateProjectUseCase.ts +++ b/src/modules/project/application/use-cases/UpdateProjectUseCase.ts @@ -1,11 +1,11 @@ import { IProjectRepository } from "../repositories/IProjectRepository"; -import { Project } from "../domain/Project"; -import { UpdateProjectDto } from "../dto/UpdateProjectDto"; +import { ProjectEntity } from "../../domain/entities/project.entity"; +import { UpdateProjectDto } from "../../presentation/dto/UpdateProjectDto"; export class UpdateProjectUseCase { constructor(private projectRepository: IProjectRepository) {} - async execute(id: string, dto: UpdateProjectDto): Promise { + async execute(id: string, dto: UpdateProjectDto): Promise { const project = await this.projectRepository.findById(id); if (!project) { diff --git a/src/modules/project/application/use-cases/index.ts b/src/modules/project/application/use-cases/index.ts new file mode 100644 index 0000000..dfed234 --- /dev/null +++ b/src/modules/project/application/use-cases/index.ts @@ -0,0 +1,6 @@ +export { CreateProjectUseCase } from "./CreateProject.use-case"; +export { DeleteProjectUseCase } from "./DeleteProjectUseCase"; +export { GetProjectUseCase } from "./GetProjectUseCase"; +export { ListProjectsUseCase } from "./ListProjectsUseCase"; +export { UpdateProjectUseCase } from "./UpdateProjectUseCase"; +export { AssignVolunteersToProjectUseCase } from "./AssignVolunteersToProject.use-case"; diff --git a/src/modules/project/domain/Project.ts b/src/modules/project/domain/Project.ts deleted file mode 100644 index 2392bfa..0000000 --- a/src/modules/project/domain/Project.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Entity } from "../../shared/domain/entities/base.entity"; - -export interface IProject { - id: string; - title: string; - description: string; - organizationId: string; - status: ProjectStatus; - createdAt: Date; - updatedAt: Date; -} - -export enum ProjectStatus { - DRAFT = "DRAFT", - ACTIVE = "ACTIVE", - COMPLETED = "COMPLETED", - CANCELLED = "CANCELLED", -} - -export class Project extends Entity { - private constructor(props: IProject) { - super(props); - } - - public static create( - props: Omit - ): Project { - return new Project({ - ...props, - id: crypto.randomUUID(), - createdAt: new Date(), - updatedAt: new Date(), - }); - } - - public update( - props: Partial> - ): void { - Object.assign(this.props, { - ...props, - updatedAt: new Date(), - }); - } - - public get id(): string { - return this.props.id; - } - - public get title(): string { - return this.props.title; - } - - public get description(): string { - return this.props.description; - } - - public get organizationId(): string { - return this.props.organizationId; - } - - public get status(): ProjectStatus { - return this.props.status; - } -} diff --git a/src/modules/project/domain/entities/project.entity.ts b/src/modules/project/domain/entities/project.entity.ts index 77ca96e..1094233 100644 --- a/src/modules/project/domain/entities/project.entity.ts +++ b/src/modules/project/domain/entities/project.entity.ts @@ -1,43 +1,111 @@ -import { Entity, Column, OneToMany } from "typeorm"; import { BaseEntity } from "../../../shared/domain/entities/base.entity"; -import { Volunteer } from "@/modules/volunteer/domain/entities/volunteer.entity"; +import { ProjectStatus } from "../enum/ProjectStatus.enum"; +import { UserVolunteer } from "@/modules/user/domain/entities/user-volunteer.entity"; +import { ProjectExceptions } from "../exceptions/project.exceptions"; -export enum ProjectStatus { - DRAFT = "DRAFT", - ACTIVE = "ACTIVE", - COMPLETED = "COMPLETED", - CANCELLED = "CANCELLED", +export interface IProjectProps { + id: string; + name: string; + description: string; + location: string; + startDate: Date; + endDate: Date; + organizationId: string; + status: ProjectStatus; + createdAt: Date; + updatedAt: Date; + volunteers?: UserVolunteer[] | UserVolunteer | undefined; } -@Entity("projects") -export class Project extends BaseEntity { - @Column({ type: "varchar", length: 255, nullable: false }) +export class ProjectEntity extends BaseEntity { name: string; - - @Column({ type: "text", nullable: false }) description: string; - - @Column({ type: "varchar", length: 255, nullable: false }) location: string; - - @Column({ type: "date", nullable: false }) startDate: Date; - - @Column({ type: "date", nullable: false }) endDate: Date; - - @Column({ type: "uuid", nullable: false }) organizationId: string; - - @Column({ - type: "enum", - enum: ProjectStatus, - default: ProjectStatus.DRAFT, - }) status: ProjectStatus; + volunteers?: UserVolunteer[] | UserVolunteer | undefined; - @OneToMany(() => Volunteer, (volunteer) => volunteer.project) - volunteers?: Volunteer[]; + constructor(props: IProjectProps) { + super(); + this.id = props.id; + this.name = props.name; + this.description = props.description; + this.location = props.location; + this.startDate = props.startDate; + this.endDate = props.endDate; + this.organizationId = props.organizationId; + this.createdAt = props.createdAt ?? new Date(); + this.updatedAt = props.updatedAt ?? new Date(); + this.status = props.status; + this.volunteers = props.volunteers; + } + + public static create( + id: string, + name: string, + description: string, + location: string, + startDate: Date, + endDate: Date, + organizationId: string, + status: ProjectStatus = ProjectStatus.DRAFT, + volunteers?: UserVolunteer[] | UserVolunteer + ): ProjectEntity { + // Validate required fields + if (!name || name.trim().length === 0) { + throw new ProjectExceptions( + "name", + "Project name is required and cannot be empty" + ); + } + if (!description || description.trim().length === 0) { + throw new ProjectExceptions( + "description", + "Project description is required and cannot be empty" + ); + } + if (!location || location.trim().length === 0) { + throw new ProjectExceptions( + "location", + "Project location is required and cannot be empty" + ); + } + if ( + !startDate || + !(startDate instanceof Date) || + isNaN(startDate.getTime()) + ) { + throw new ProjectExceptions("date", "Valid start date is required"); + } + if (!endDate || !(endDate instanceof Date) || isNaN(endDate.getTime())) { + throw new ProjectExceptions("date", "Valid end date is required"); + } + if (endDate <= startDate) { + throw new ProjectExceptions("date", "End date must be after start date"); + } + if (!organizationId || organizationId.trim().length === 0) { + throw new ProjectExceptions( + "organization", + "Organization ID is required and cannot be empty" + ); + } + + return new ProjectEntity({ + id, + name, + location, + description, + startDate, + endDate, + organizationId, + volunteers, + status, + createdAt: new Date(), + updatedAt: new Date(), + }); + } // Domain methods public activate(): void { @@ -47,6 +115,15 @@ export class Project extends BaseEntity { this.status = ProjectStatus.ACTIVE; } + public update( + props: Partial> + ): void { + Object.assign(this, { + ...props, + updatedAt: new Date(), + }); + } + public complete(): void { if (this.status !== ProjectStatus.ACTIVE) { throw new Error("Only active projects can be completed"); diff --git a/src/modules/project/domain/enum/ProjectStatus.enum.ts b/src/modules/project/domain/enum/ProjectStatus.enum.ts new file mode 100644 index 0000000..ceafb3a --- /dev/null +++ b/src/modules/project/domain/enum/ProjectStatus.enum.ts @@ -0,0 +1,6 @@ +export enum ProjectStatus { + DRAFT = "DRAFT", + ACTIVE = "ACTIVE", + COMPLETED = "COMPLETED", + CANCELLED = "CANCELLED", +} diff --git a/src/modules/project/domain/exceptions/project.exceptions.ts b/src/modules/project/domain/exceptions/project.exceptions.ts new file mode 100644 index 0000000..4a48bd5 --- /dev/null +++ b/src/modules/project/domain/exceptions/project.exceptions.ts @@ -0,0 +1,7 @@ +import { DomainException } from "@/modules/shared/domain/exceptions/domain.exception"; + +export class ProjectExceptions extends DomainException { + constructor(field: string, value: string) { + super(`Invalid ${field}: ${value}`); + } +} diff --git a/src/modules/project/index.ts b/src/modules/project/index.ts deleted file mode 100644 index 3281cd5..0000000 --- a/src/modules/project/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "./domain/Project"; -export * from "./repositories/IProjectRepository"; -// export * from './repositories/PrismaProjectRepository'; -export * from "./dto/CreateProjectDto"; -export * from "./dto/UpdateProjectDto"; -export * from "./use-cases/CreateProjectUseCase"; -export * from "./use-cases/UpdateProjectUseCase"; -export * from "./use-cases/GetProjectUseCase"; -export * from "./use-cases/ListProjectsUseCase"; -export * from "./use-cases/DeleteProjectUseCase"; diff --git a/src/modules/project/infrastructure/ProjectRepository.ts b/src/modules/project/infrastructure/ProjectRepository.ts new file mode 100644 index 0000000..2fb8fa7 --- /dev/null +++ b/src/modules/project/infrastructure/ProjectRepository.ts @@ -0,0 +1,126 @@ +import { PrismaClient } from "@prisma/client"; +import { IProjectRepository } from "../application/repositories/IProjectRepository"; +import { + IProjectProps, + ProjectEntity, +} from "../domain/entities/project.entity"; + +export class ProjectRepository implements IProjectRepository { + constructor(private prisma: PrismaClient) {} + + async findById(id: string): Promise { + const project = await this.prisma.project.findUnique({ + where: { id }, + }); + + if (!project) return null; + + return ProjectEntity.create( + project.id, + project.name, + project.description, + project.organizationId, + project.startDate, + project.endDate, + project.organizationId, + project.status, + project.volunteers + ); + } + + async findAll(): Promise { + const projects = await this.prisma.project.findMany(); + return projects.map((project: IProjectProps) => + ProjectEntity.create( + project.id, + project.name, + project.description, + project.organizationId, + project.startDate, + project.endDate, + project.organizationId, + project.status, + project.volunteers + ) + ); + } + + async findByOrganizationId(organizationId: string): Promise { + const projects = await this.prisma.project.findMany({ + where: { organizationId }, + }); + return projects.map((project: IProjectProps) => + ProjectEntity.create( + project.id, + project.name, + project.description, + project.organizationId, + project.startDate, + project.endDate, + project.organizationId, + project.status, + project.volunteers + ) + ); + } + + async save(project: ProjectEntity): Promise { + await this.prisma.project.create({ + data: { + id: project.id, + name: project.name, + description: project.description, + organizationId: project.organizationId, + status: project.status, + createdAt: new Date(), + updatedAt: new Date(), + }, + }); + + return ProjectEntity.create( + project.id, + project.name, + project.description, + project.organizationId, + project.startDate, + project.endDate, + project.organizationId, + project.status, + project.volunteers + ); + } + + async update(project: ProjectEntity): Promise { + const updatedProject = await this.prisma.project.update({ + where: { id: project.id }, + data: { + name: project.name, // mapeo correcto + description: project.description, + organizationId: project.organizationId, + status: project.status, + // ojo con volunteers: depende de tu modelo Prisma + updatedAt: new Date(), + }, + include: { + volunteers: true, // para traer la relación + }, + }); + + return ProjectEntity.create( + updatedProject.id, + updatedProject.name, // title + updatedProject.description, + updatedProject.organizationId, + updatedProject.status, + updatedProject.startDate, + updatedProject.endDate, + updatedProject.volunteers + ); + } + + async delete(id: string): Promise { + await this.prisma.project.delete({ + where: { id }, + }); + } +} diff --git a/src/modules/project/presentation/controllers/Project.controller.disabled b/src/modules/project/presentation/controllers/Project.controller.disabled deleted file mode 100644 index 1d15d56..0000000 --- a/src/modules/project/presentation/controllers/Project.controller.disabled +++ /dev/null @@ -1,85 +0,0 @@ -import { Request, Response } from "express"; -import ProjectService from "../../../../services/ProjectService"; - -class ProjectController { - private projectService = new ProjectService(); - - async createProject(req: Request, res: Response): Promise { - try { - const { - name, - description, - location, - startDate, - endDate, - organizationId, - } = req.body; - const project = await this.projectService.createProject({ - name, - description, - location, - startDate: new Date(startDate), - endDate: new Date(endDate), - organizationId, - }); - res.status(201).json(project); - } catch (error) { - res.status(400).json({ - error: - error instanceof Error ? error.message : "An unknown error occurred", - }); - } - } - - async getProjectById(req: Request, res: Response): Promise { - try { - const { id } = req.params; - const project = await this.projectService.getProjectById(id); - if (!project) { - res.status(404).json({ error: "Project not found" }); - return; - } - res.status(200).json(project); - } catch (error) { - res.status(400).json({ - error: - error instanceof Error ? error.message : "An unknown error occurred", - }); - } - } - - async getProjectsByOrganizationId( - req: Request, - res: Response - ): Promise { - try { - const { organizationId } = req.params; - const page = parseInt(req.query.page as string) || 1; - const pageSize = parseInt(req.query.pageSize as string) || 10; - - const { projects, total } = - await this.projectService.getProjectsByOrganizationId( - organizationId, - page, - pageSize - ); - - res.status(200).json({ - data: projects, - pagination: { - total, - page, - pageSize, - totalPages: Math.ceil(total / pageSize), - }, - }); - } catch (error) { - res.status(400).json({ - error: - error instanceof Error ? error.message : "An unknown error occurred", - }); - } - } -} - -export default ProjectController; diff --git a/src/modules/project/dto/CreateProjectDto.ts b/src/modules/project/presentation/dto/CreateProjectDto.ts similarity index 51% rename from src/modules/project/dto/CreateProjectDto.ts rename to src/modules/project/presentation/dto/CreateProjectDto.ts index b371123..f40ee3e 100644 --- a/src/modules/project/dto/CreateProjectDto.ts +++ b/src/modules/project/presentation/dto/CreateProjectDto.ts @@ -5,20 +5,33 @@ import { MinLength, MaxLength, IsEnum, + IsDate, } from "class-validator"; -import { ProjectStatus } from "../domain/Project"; +import { ProjectStatus } from "../../domain/enum/ProjectStatus.enum"; +import { Transform } from "class-transformer"; export class CreateProjectDto { - @IsString({ message: "Title must be a string" }) - @MinLength(3, { message: "Title must be at least 3 characters long" }) - @MaxLength(200, { message: "Title cannot exceed 200 characters" }) - title: string; + @IsString({ message: "Name must be a string" }) + @MinLength(3, { message: "Name must be at least 3 characters long" }) + @MaxLength(200, { message: "Name cannot exceed 200 characters" }) + name: string; @IsString({ message: "Description must be a string" }) @MinLength(10, { message: "Description must be at least 10 characters long" }) @MaxLength(2000, { message: "Description cannot exceed 2000 characters" }) description: string; + @IsString({ message: "Location must be a string" }) + location: string; + + @Transform(({ value }) => new Date(value)) + @IsDate() + startDate: Date; + + @Transform(({ value }) => new Date(value)) + @IsDate() + endDate: Date; + @IsUUID(4, { message: "Organization ID must be a valid UUID" }) organizationId: string; diff --git a/src/modules/project/dto/UpdateProjectDto.ts b/src/modules/project/presentation/dto/UpdateProjectDto.ts similarity index 100% rename from src/modules/project/dto/UpdateProjectDto.ts rename to src/modules/project/presentation/dto/UpdateProjectDto.ts diff --git a/src/modules/project/presentation/dto/index.ts b/src/modules/project/presentation/dto/index.ts new file mode 100644 index 0000000..1db8d1a --- /dev/null +++ b/src/modules/project/presentation/dto/index.ts @@ -0,0 +1,2 @@ +export { CreateProjectDto } from "./CreateProjectDto"; +export { UpdateProjectDto } from "./UpdateProjectDto"; diff --git a/src/modules/project/repositories/IProjectRepository.ts b/src/modules/project/repositories/IProjectRepository.ts deleted file mode 100644 index d9519c0..0000000 --- a/src/modules/project/repositories/IProjectRepository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Project } from "../domain/Project"; - -export interface IProjectRepository { - findById(id: string): Promise; - findAll(): Promise; - findByOrganizationId(organizationId: string): Promise; - save(project: Project): Promise; - update(project: Project): Promise; - delete(id: string): Promise; -} diff --git a/src/modules/project/repositories/PrismaProjectRepository.ts b/src/modules/project/repositories/PrismaProjectRepository.ts deleted file mode 100644 index 93c1158..0000000 --- a/src/modules/project/repositories/PrismaProjectRepository.ts +++ /dev/null @@ -1,95 +0,0 @@ -// import { PrismaClient } from '@prisma/client'; -// import { IProjectRepository } from './IProjectRepository'; -// import { Project, ProjectStatus } from '../domain/Project'; - -// export class PrismaProjectRepository implements IProjectRepository { -// constructor(private prisma: PrismaClient) {} - -// async findById(id: string): Promise { -// const project = await this.prisma.project.findUnique({ -// where: { id } -// }); - -// if (!project) return null; - -// return Project.create({ -// title: project.title, -// description: project.description, -// organizationId: project.organizationId, -// status: project.status as ProjectStatus -// }); -// } - -// async findAll(): Promise { -// const projects = await this.prisma.project.findMany(); -// return projects.map(project => -// Project.create({ -// title: project.title, -// description: project.description, -// organizationId: project.organizationId, -// status: project.status as ProjectStatus -// }) -// ); -// } - -// async findByOrganizationId(organizationId: string): Promise { -// const projects = await this.prisma.project.findMany({ -// where: { organizationId } -// }); -// return projects.map(project => -// Project.create({ -// title: project.title, -// description: project.description, -// organizationId: project.organizationId, -// status: project.status as ProjectStatus -// }) -// ); -// } - -// async save(project: Project): Promise { -// const savedProject = await this.prisma.project.create({ -// data: { -// id: project.id, -// title: project.title, -// description: project.description, -// organizationId: project.organizationId, -// status: project.status, -// createdAt: new Date(), -// updatedAt: new Date() -// } -// }); - -// return Project.create({ -// title: savedProject.title, -// description: savedProject.description, -// organizationId: savedProject.organizationId, -// status: savedProject.status as ProjectStatus -// }); -// } - -// async update(project: Project): Promise { -// const updatedProject = await this.prisma.project.update({ -// where: { id: project.id }, -// data: { -// title: project.title, -// description: project.description, -// organizationId: project.organizationId, -// status: project.status, -// updatedAt: new Date() -// } -// }); - -// return Project.create({ -// title: updatedProject.title, -// description: updatedProject.description, -// organizationId: updatedProject.organizationId, -// status: updatedProject.status as ProjectStatus -// }); -// } - -// async delete(id: string): Promise { -// await this.prisma.project.delete({ -// where: { id } -// }); -// } -// } diff --git a/src/modules/project/use-cases/CreateProjectUseCase.ts b/src/modules/project/use-cases/CreateProjectUseCase.ts deleted file mode 100644 index e14998c..0000000 --- a/src/modules/project/use-cases/CreateProjectUseCase.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IProjectRepository } from "../repositories/IProjectRepository"; -import { Project, ProjectStatus } from "../domain/Project"; -import { CreateProjectDto } from "../dto/CreateProjectDto"; - -export class CreateProjectUseCase { - constructor(private projectRepository: IProjectRepository) {} - - async execute(dto: CreateProjectDto): Promise { - const project = Project.create({ - title: dto.title, - description: dto.description, - organizationId: dto.organizationId, - status: dto.status || ProjectStatus.DRAFT, - }); - - return this.projectRepository.save(project); - } -} diff --git a/src/modules/shared/domain/entities/base.entity.ts b/src/modules/shared/domain/entities/base.entity.ts index 4cb71e1..b8efafc 100644 --- a/src/modules/shared/domain/entities/base.entity.ts +++ b/src/modules/shared/domain/entities/base.entity.ts @@ -1,17 +1,8 @@ -import { - PrimaryGeneratedColumn, - CreateDateColumn, - UpdateDateColumn, -} from "typeorm"; - export abstract class BaseEntity { - @PrimaryGeneratedColumn("uuid") id: string; - @CreateDateColumn() createdAt: Date; - @UpdateDateColumn() updatedAt: Date; } diff --git a/src/modules/user/application/repository/user.repository.ts b/src/modules/user/application/repository/user.repository.ts new file mode 100644 index 0000000..104a1cb --- /dev/null +++ b/src/modules/user/application/repository/user.repository.ts @@ -0,0 +1,29 @@ +import { UserEntity } from "../../domain/entities/User.entity"; +import { UpdateUserDto } from "../../presentation/dto"; + +export interface IUserRepository { + createUser( + name: string, + email: string, + password: string, + wallet: string + ): Promise; + findByEmail(email: string): Promise; + findById(userId: string): Promise; + saveVerificationToken( + email: string, + token: string, + tokenExpires: Date + ): Promise; + updateVerificationToken( + userId: string, + token: string, + expires: Date + ): Promise; + updateUser(id: string, data: UpdateUserDto): Promise; + deleteUser(id: string): Promise; + findByVerificationToken(token: string): Promise; + findAll(page: number, pageSize: number): Promise; + updateVerificationStatus(userId: string): Promise; + isUserVerified(userId: string): Promise; +} diff --git a/src/modules/user/application/use-cases/CreateUser.use-case.ts b/src/modules/user/application/use-cases/CreateUser.use-case.ts new file mode 100644 index 0000000..59be3ba --- /dev/null +++ b/src/modules/user/application/use-cases/CreateUser.use-case.ts @@ -0,0 +1,30 @@ +import { BcryptAdapter } from "@/config/BcryptAdapter"; +import { CreateUserDto } from "../../presentation/dto"; +import { IUserRepository } from "../repository/user.repository"; +import { UserEntity } from "../../domain/entities/User.entity"; + +export class CreateUserUseCase { + constructor( + private userRepository: IUserRepository, + private readonly encryptionAdapter: BcryptAdapter + ) {} + + async execute(data: CreateUserDto) { + const hashedPassword = this.encryptionAdapter.generateHash(data.password); + + const user = UserEntity.create({ + id: crypto.randomUUID(), + name: data.name, + lastName: data.lastName, + email: data.email, + password: hashedPassword, + wallet: data.wallet, + }); + return this.userRepository.createUser( + user.id, + user.email, + user.password, + user.wallet + ); + } +} diff --git a/src/modules/user/application/use-cases/CreateVolunteer.entity.ts b/src/modules/user/application/use-cases/CreateVolunteer.entity.ts new file mode 100644 index 0000000..59be3ba --- /dev/null +++ b/src/modules/user/application/use-cases/CreateVolunteer.entity.ts @@ -0,0 +1,30 @@ +import { BcryptAdapter } from "@/config/BcryptAdapter"; +import { CreateUserDto } from "../../presentation/dto"; +import { IUserRepository } from "../repository/user.repository"; +import { UserEntity } from "../../domain/entities/User.entity"; + +export class CreateUserUseCase { + constructor( + private userRepository: IUserRepository, + private readonly encryptionAdapter: BcryptAdapter + ) {} + + async execute(data: CreateUserDto) { + const hashedPassword = this.encryptionAdapter.generateHash(data.password); + + const user = UserEntity.create({ + id: crypto.randomUUID(), + name: data.name, + lastName: data.lastName, + email: data.email, + password: hashedPassword, + wallet: data.wallet, + }); + return this.userRepository.createUser( + user.id, + user.email, + user.password, + user.wallet + ); + } +} diff --git a/src/modules/user/application/use-cases/DeleteUser.use-case.ts b/src/modules/user/application/use-cases/DeleteUser.use-case.ts new file mode 100644 index 0000000..1c52cc8 --- /dev/null +++ b/src/modules/user/application/use-cases/DeleteUser.use-case.ts @@ -0,0 +1,9 @@ +import { IUserRepository } from "../repository/user.repository"; + +export class DeleteUserUseCase { + constructor(private userRepository: IUserRepository) {} + + async execute(id: string): Promise { + await this.userRepository.deleteUser(id); + } +} diff --git a/src/modules/user/application/use-cases/GetAllUsers.use-case.ts b/src/modules/user/application/use-cases/GetAllUsers.use-case.ts new file mode 100644 index 0000000..b39dbe9 --- /dev/null +++ b/src/modules/user/application/use-cases/GetAllUsers.use-case.ts @@ -0,0 +1,13 @@ +import { IUserRepository } from "../repository/user.repository"; + +export class GetAllUsersUseCase { + constructor(private userRepository: IUserRepository) {} + + async execute(page: number = 1, pageSize: number = 10) { + if (page < 1 || pageSize < 1) { + throw new Error("Invalid pagination parameters."); + } + + return this.userRepository.findAll(page, pageSize); + } +} diff --git a/src/modules/user/application/use-cases/GetUserByEmail.use-case.ts b/src/modules/user/application/use-cases/GetUserByEmail.use-case.ts new file mode 100644 index 0000000..02d0be1 --- /dev/null +++ b/src/modules/user/application/use-cases/GetUserByEmail.use-case.ts @@ -0,0 +1,18 @@ +import { IUserRepository } from "../repository/user.repository"; + +export class GetUserByEmailUseCase { + constructor(private userRepository: IUserRepository) {} + + async execute(email: string) { + if (!email) { + throw new Error("Email is required."); + } + + const user = await this.userRepository.findByEmail(email); + if (!user) { + throw new Error("User not found."); + } + + return user; + } +} diff --git a/src/modules/user/application/use-cases/GetUserById.use-case.ts b/src/modules/user/application/use-cases/GetUserById.use-case.ts new file mode 100644 index 0000000..9a02211 --- /dev/null +++ b/src/modules/user/application/use-cases/GetUserById.use-case.ts @@ -0,0 +1,18 @@ +import { IUserRepository } from "../repository/user.repository"; + +export class GetUserByIdUseCase { + constructor(private userRepository: IUserRepository) {} + + async execute(id: string) { + if (!id) { + throw new Error("User ID is required."); + } + + const user = await this.userRepository.findById(id); + if (!user) { + throw new Error("User not found."); + } + + return user; + } +} diff --git a/src/modules/user/application/use-cases/UpdateUser.use-case.ts b/src/modules/user/application/use-cases/UpdateUser.use-case.ts new file mode 100644 index 0000000..a17934e --- /dev/null +++ b/src/modules/user/application/use-cases/UpdateUser.use-case.ts @@ -0,0 +1,10 @@ +import { UpdateUserDto } from "../../presentation/dto"; +import { IUserRepository } from "../repository/user.repository"; + +export class UpdateUserUseCase { + constructor(private userRepository: IUserRepository) {} + + async execute(data: UpdateUserDto): Promise { + await this.userRepository.updateUser(data); + } +} diff --git a/src/modules/user/application/use-cases/index.ts b/src/modules/user/application/use-cases/index.ts new file mode 100644 index 0000000..a89a284 --- /dev/null +++ b/src/modules/user/application/use-cases/index.ts @@ -0,0 +1,6 @@ +export { CreateUserUseCase } from "./CreateUser.use-case"; +export { DeleteUserUseCase } from "./DeleteUser.use-case"; +export { GetAllUsersUseCase } from "./GetAllUsers.use-case"; +export { GetUserByEmailUseCase } from "./GetUserByEmail.use-case"; +export { GetUserByIdUseCase } from "./GetUserById.use-case"; +export { UpdateUserUseCase } from "./UpdateUser.use-case"; diff --git a/src/modules/user/domain/entities/User.entity.ts b/src/modules/user/domain/entities/User.entity.ts index ee6664b..c540aad 100644 --- a/src/modules/user/domain/entities/User.entity.ts +++ b/src/modules/user/domain/entities/User.entity.ts @@ -1,31 +1,90 @@ -import { Entity, Column, BaseEntity, PrimaryGeneratedColumn } from "typeorm"; +import { BaseEntity } from "@/modules/shared/domain/entities/base.entity"; -@Entity("users") -export class User extends BaseEntity { - @PrimaryGeneratedColumn("uuid") +export interface IUserProps { id: string; - - @Column() name: string; - - @Column() lastName: string; - - @Column({ unique: true }) email: string; - - @Column() password: string; - - @Column({ unique: true }) wallet: string; + isVerified?: boolean; + verificationToken?: string | null; + verificationTokenExpires?: Date | null; +} - @Column({ default: false }) +export class UserEntity extends BaseEntity { + id: string; + name: string; + lastName: string; + email: string; + password: string; + wallet: string; isVerified: boolean; + verificationToken: string | null; + verificationTokenExpires: Date | null; + + constructor(props: { + id: string; + name: string; + lastName: string; + email: string; + password: string; + wallet: string; + isVerified?: boolean; + verificationToken?: string | null; + verificationTokenExpires?: Date | null; + }) { + super(); + this.id = props.id; + this.name = props.name; + this.lastName = props.lastName; + this.email = props.email; + this.password = props.password; + this.wallet = props.wallet; + this.isVerified = props.isVerified ?? false; + this.verificationToken = props.verificationToken ?? null; + this.verificationTokenExpires = props.verificationTokenExpires ?? null; + } + + public static create(props: { + id: string; + name: string; + lastName: string; + email: string; + password: string; + wallet: string; + }): UserEntity { + // Validate fields + if (!props.name || props.name.trim().length === 0) { + throw new Error("name is required"); + } + if (!props.email || props.email.trim().length === 0) { + throw new Error("email is required"); + } + if (!props.password || props.password.trim().length === 0) { + throw new Error("password is required"); + } + if (!props.wallet || props.wallet.trim().length === 0) { + throw new Error("wallet is required"); + } - @Column({ nullable: true }) - verificationToken: string; + const user = new UserEntity(props); + return user; + } - @Column({ type: "timestamp", nullable: true }) - verificationTokenExpires: Date; + // // Ejemplo de regla de negocio dentro de la entidad: + // verifyAccount(token: string): boolean { + // if ( + // this.verificationToken && + // this.verificationToken === token && + // this.verificationTokenExpires && + // this.verificationTokenExpires > new Date() + // ) { + // this.isVerified = true; + // this.verificationToken = null; + // this.verificationTokenExpires = null; + // return true; + // } + // return false; + // } } diff --git a/src/modules/user/domain/entities/User.ts b/src/modules/user/domain/entities/User.ts deleted file mode 100644 index 4446958..0000000 --- a/src/modules/user/domain/entities/User.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export the main user entity -export { User } from "./User.entity"; diff --git a/src/modules/user/domain/entities/user-volunteer.entity.ts b/src/modules/user/domain/entities/user-volunteer.entity.ts index 511c354..b071333 100644 --- a/src/modules/user/domain/entities/user-volunteer.entity.ts +++ b/src/modules/user/domain/entities/user-volunteer.entity.ts @@ -1,33 +1,75 @@ -import { Entity, PrimaryColumn, CreateDateColumn } from "typeorm"; -import { BaseEntity } from "../../../shared/domain/entities/base.entity"; +import { IUserProps, UserEntity } from "./User.entity"; +import { InvalidUserDataException } from "../exceptions/user.exceptions"; -@Entity("user_volunteers") -export class UserVolunteer extends BaseEntity { - @PrimaryColumn("uuid") - userId: string; - - @PrimaryColumn("uuid") +export class UserVolunteer extends UserEntity { volunteerId: string; - - @CreateDateColumn() joinedAt: Date; - // Domain methods - public static create(userId: string, volunteerId: string): UserVolunteer { - if (!userId || !volunteerId) { - throw new Error("User ID and Volunteer ID are required"); - } + private constructor(props: { + id: string; + name: string; + lastName: string; + email: string; + password: string; + wallet: string; + isVerified?: boolean; + verificationToken?: string | null; + verificationTokenExpires?: Date | null; + volunteerId: string; + joinedAt?: Date; + }) { + super(props); + this.volunteerId = props.volunteerId; + this.joinedAt = props.joinedAt ?? new Date(); + } - const userVolunteer = new UserVolunteer(); - userVolunteer.userId = userId; - userVolunteer.volunteerId = volunteerId; - userVolunteer.joinedAt = new Date(); + // Overload 1: Maintains compatibility with UserEntity + public static override create(props: { + id: string; + name: string; + lastName: string; + email: string; + password: string; + wallet: string; + volunteerId: string; // Agregar volunteerId como requerido + joinedAt?: Date; + }): UserVolunteer; + + // Overload 2: Your original method to create from existing UserEntity + public static create( + baseUser: UserEntity, + volunteerId: string + ): UserVolunteer; + + // Implementación que maneja ambos casos + public static create( + propsOrBaseUser: IUserProps, + volunteerId?: string + ): UserVolunteer { + if (!volunteerId) { + throw new InvalidUserDataException( + "volunteerId", + "Volunteer ID is required" + ); + } - return userVolunteer; + return new UserVolunteer({ + id: propsOrBaseUser.id, + name: propsOrBaseUser.name, + lastName: propsOrBaseUser.lastName, + email: propsOrBaseUser.email, + password: propsOrBaseUser.password, + wallet: propsOrBaseUser.wallet, + isVerified: propsOrBaseUser.isVerified, + verificationToken: propsOrBaseUser.verificationToken, + verificationTokenExpires: propsOrBaseUser.verificationTokenExpires, + volunteerId, + joinedAt: new Date(), + }); } public isUserAssigned(userId: string): boolean { - return this.userId === userId; + return this.id === userId; } public isVolunteerAssigned(volunteerId: string): boolean { diff --git a/src/modules/user/domain/exceptions/user.exceptions.ts b/src/modules/user/domain/exceptions/user.exceptions.ts new file mode 100644 index 0000000..679aec5 --- /dev/null +++ b/src/modules/user/domain/exceptions/user.exceptions.ts @@ -0,0 +1,7 @@ +import { DomainException } from "@/modules/shared/domain/exceptions/domain.exception"; + +export class InvalidUserDataException extends DomainException { + constructor(field: string, value: string) { + super(`Invalid ${field}: ${value}`); + } +} diff --git a/src/modules/user/infrastructure/repositories/user.repository.impl.ts b/src/modules/user/infrastructure/repositories/user.repository.impl.ts new file mode 100644 index 0000000..bd63b15 --- /dev/null +++ b/src/modules/user/infrastructure/repositories/user.repository.impl.ts @@ -0,0 +1,172 @@ +import { PrismaClient } from "@prisma/client"; +import { IUserRepository } from "../../application/repository/user.repository"; +import { UserEntity } from "../../domain/entities/User.entity"; +import { UpdateUserDto } from "../../presentation/dto"; + +export class UserRepositoryImpl implements IUserRepository { + constructor(private readonly prisma: PrismaClient) {} + + async updateUser(id: string, data: UpdateUserDto): Promise { + await this.prisma.user.update({ + where: { id }, + data: { + name: data.name, + lastName: data.lastName, + email: data.email, + password: data.password, + wallet: data.wallet, + updatedAt: new Date(), + }, + }); + } + + async deleteUser(id: string): Promise { + await this.prisma.user.delete({ + where: { id }, + }); + } + + async findAll(page: number, pageSize: number): Promise { + const users = await this.prisma.user.findMany({ + skip: (page - 1) * pageSize, + take: pageSize, + }); + + return users.map((user: UserEntity) => + UserEntity.create({ + id: user.id, + name: user.name, + lastName: user.lastName, + email: user.email, + password: user.password, + wallet: user.wallet, + }) + ); + } + + async createUser( + name: string, + email: string, + password: string, + wallet: string + ): Promise { + const user = await this.prisma.user.create({ + data: { + name, + email, + password, + wallet, + isVerified: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + }); + + return UserEntity.create({ + id: user.id, + lastName: user.lastName, + name: user.name, + email: user.email, + password: user.password, + wallet: user.wallet, + }); + } + + async findByEmail(email: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { email }, + }); + + if (!user) return null; + + return UserEntity.create({ + id: user.id, + name: user.name, + lastName: user.lastName, + email: user.email, + password: user.password, + wallet: user.wallet, + }); + } + + async findById(userId: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) return null; + + return UserEntity.create({ + id: user.id, + name: user.name, + lastName: user.lastName, + email: user.email, + password: user.password, + wallet: user.wallet, + }); + } + + async saveVerificationToken(email: string, token: string): Promise { + await this.prisma.user.update({ + where: { email }, + data: { + verificationToken: token, + verificationTokenExpires: new Date(Date.now() + 1000 * 60 * 60), // opcional, depende tu lógica + updatedAt: new Date(), + }, + }); + } + + async updateVerificationToken( + userId: string, + token: string, + expires: Date + ): Promise { + await this.prisma.user.update({ + where: { id: userId }, + data: { + verificationToken: token, + verificationTokenExpires: expires, + updatedAt: new Date(), + }, + }); + } + + async findByVerificationToken(token: string): Promise { + const user = await this.prisma.user.findFirst({ + where: { verificationToken: token }, + }); + + if (!user) return null; + + return UserEntity.create({ + id: user.id, + name: user.name, + lastName: user.lastName, + email: user.email, + password: user.password, + wallet: user.wallet, + }); + } + + async updateVerificationStatus(userId: string): Promise { + await this.prisma.user.update({ + where: { id: userId }, + data: { + isVerified: true, + verificationToken: null, + verificationTokenExpires: null, + updatedAt: new Date(), + }, + }); + } + + async isUserVerified(userId: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + select: { isVerified: true }, + }); + + return user ? user.isVerified : false; + } +} diff --git a/src/modules/user/presentation/controller/user.controller.ts b/src/modules/user/presentation/controller/user.controller.ts new file mode 100644 index 0000000..349cbef --- /dev/null +++ b/src/modules/user/presentation/controller/user.controller.ts @@ -0,0 +1,74 @@ +import { Request, Response } from "express"; +import { + UpdateUserUseCase, + CreateUserUseCase, + DeleteUserUseCase, + GetAllUsersUseCase, + GetUserByEmailUseCase, + GetUserByIdUseCase, +} from "../../application/use-cases"; + +export class UserController { + constructor( + private readonly createUserUseCase: CreateUserUseCase, + private readonly updateUserUseCase: UpdateUserUseCase, + private readonly deleteUserUseCase: DeleteUserUseCase, + private readonly getAllUsersUseCase: GetAllUsersUseCase, + private readonly getUserByEmailUseCase: GetUserByEmailUseCase, + private readonly getUserByIdUseCase: GetUserByIdUseCase + ) {} + + async create(req: Request, res: Response): Promise { + try { + const user = await this.createUserUseCase.execute(req.body); + return res.status(201).json(user); + } catch (error: unknown) { + return res.status(400).json({ message: error }); + } + } + + async update(req: Request, res: Response): Promise { + try { + const user = await this.updateUserUseCase.execute(req.body); + return res.status(200).json(user); + } catch (error: unknown) { + return res.status(400).json({ message: error }); + } + } + + async delete(req: Request, res: Response): Promise { + try { + await this.deleteUserUseCase.execute(req.params.id); + return res.status(204).send(); + } catch (error: unknown) { + return res.status(400).json({ message: error }); + } + } + + async findAll(req: Request, res: Response): Promise { + try { + const users = await this.getAllUsersUseCase.execute(); + return res.status(200).json(users); + } catch (error: unknown) { + return res.status(400).json({ message: error }); + } + } + + async findById(req: Request, res: Response): Promise { + try { + const user = await this.getUserByIdUseCase.execute(req.params.id); + return res.status(200).json(user); + } catch (error: unknown) { + return res.status(404).json({ message: error }); + } + } + + async findByEmail(req: Request, res: Response): Promise { + try { + const user = await this.getUserByEmailUseCase.execute(req.params.email); + return res.status(200).json(user); + } catch (error: unknown) { + return res.status(404).json({ message: error }); + } + } +} diff --git a/src/modules/user/presentation/controllers/UserController.disabled b/src/modules/user/presentation/controllers/UserController.disabled deleted file mode 100644 index 1ef2aa5..0000000 --- a/src/modules/user/presentation/controllers/UserController.disabled +++ /dev/null @@ -1,80 +0,0 @@ -import { CreateUserDto } from "../../../../modules/user/dto/CreateUserDto"; -import { UserService } from "../../../../services/UserService"; -import { Response } from "express"; -import { UpdateUserDto } from "../../../../modules/user/dto/UpdateUserDto"; -import { AuthenticatedRequest } from "../../../../types/auth.types"; - -class UserController { - private userService = new UserService(); - - async createUser(req: AuthenticatedRequest, res: Response): Promise { - try { - const userDto = new CreateUserDto(); - Object.assign(userDto, req.body); - - const user = await this.userService.createUser(userDto); - res.status(201).json(user); - } catch (error: unknown) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Unknown error", - }); - } - } - - async getUserById(req: AuthenticatedRequest, res: Response): Promise { - try { - const { id } = req.params; - const user = await this.userService.getUserById(id); - if (!user) { - res.status(404).json({ error: "User not found" }); - return; - } - res.status(200).json(user); - } catch (error: unknown) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Unknown error", - }); - } - } - - async getUserByEmail( - req: AuthenticatedRequest, - res: Response - ): Promise { - try { - const { email } = req.query; - if (!email) { - res.status(400).json({ error: "Email is required" }); - return; - } - const user = await this.userService.getUserByEmail(email as string); - if (!user) { - res.status(404).json({ error: "User not found" }); - return; - } - res.status(200).json(user); - } catch (error: unknown) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Unknown error", - }); - } - } - - async updateUser(req: AuthenticatedRequest, res: Response): Promise { - try { - const { id } = req.params; - const userDto = new UpdateUserDto(); - Object.assign(userDto, req.body); - userDto.id = id; - - await this.userService.updateUser(userDto); - res.status(200).json({ message: "User updated successfully" }); - } catch (error: unknown) { - res.status(400).json({ - error: error instanceof Error ? error.message : "Unknown error", - }); - } - } -} - -export default UserController; diff --git a/src/modules/user/presentation/controllers/UserController.stub.ts b/src/modules/user/presentation/controllers/UserController.stub.ts deleted file mode 100644 index ce59984..0000000 --- a/src/modules/user/presentation/controllers/UserController.stub.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Request, Response } from "express"; - -/** - * Stub controller for User functionality - * This replaces the original controller that referenced deleted services - * TODO: Implement proper user controller using new modular architecture - */ -export default class UserController { - async createUser(req: Request, res: Response) { - res.status(501).json({ - message: "User service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async getUserById(req: Request, res: Response) { - res.status(501).json({ - message: "User service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async getUserByEmail(req: Request, res: Response) { - res.status(501).json({ - message: "User service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async updateUser(req: Request, res: Response) { - res.status(501).json({ - message: "User service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async deleteUser(req: Request, res: Response) { - res.status(501).json({ - message: "User service temporarily disabled during migration", - error: "Service migration in progress" - }); - } -} \ No newline at end of file diff --git a/src/modules/user/presentation/controllers/userVolunteer.controller.disabled b/src/modules/user/presentation/controllers/userVolunteer.controller.disabled deleted file mode 100644 index 8254504..0000000 --- a/src/modules/user/presentation/controllers/userVolunteer.controller.disabled +++ /dev/null @@ -1,44 +0,0 @@ -import { Request, Response } from "express"; -import { UserVolunteerService } from "../../../../services/userVolunteer.service"; -import { prisma } from "../../../../config/prisma"; - -class UserVolunteerController { - private userVolunteerService = new UserVolunteerService(prisma); - - async addUserToVolunteer(req: Request, res: Response): Promise { - try { - const { userId, volunteerId } = req.params; - const userVolunteer = await this.userVolunteerService.addUserToVolunteer( - userId, - volunteerId - ); - return res.status(201).json(userVolunteer); - } catch (error) { - return res.status(500).json({ error: (error as Error).message }); - } - } - - async getVolunteersByUserId(req: Request, res: Response): Promise { - try { - const { userId } = req.params; - const volunteers = - await this.userVolunteerService.getVolunteersByUserId(userId); - return res.status(200).json(volunteers); - } catch (error) { - return res.status(500).json({ error: (error as Error).message }); - } - } - - async getUsersByVolunteerId(req: Request, res: Response): Promise { - try { - const { volunteerId } = req.params; - const users = - await this.userVolunteerService.getUsersByVolunteerId(volunteerId); - return res.status(200).json(users); - } catch (error) { - return res.status(500).json({ error: (error as Error).message }); - } - } -} - -export default UserVolunteerController; diff --git a/src/modules/user/presentation/controllers/userVolunteer.controller.stub.ts b/src/modules/user/presentation/controllers/userVolunteer.controller.stub.ts deleted file mode 100644 index bdf3121..0000000 --- a/src/modules/user/presentation/controllers/userVolunteer.controller.stub.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Request, Response } from "express"; - -/** - * Stub controller for UserVolunteer functionality - * This replaces the original controller that referenced deleted services - * TODO: Implement proper userVolunteer controller using new modular architecture - */ -class UserVolunteerController { - async createUserVolunteer(req: Request, res: Response) { - res.status(501).json({ - message: "UserVolunteer service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async getUserVolunteers(req: Request, res: Response) { - res.status(501).json({ - message: "UserVolunteer service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async updateUserVolunteer(req: Request, res: Response) { - res.status(501).json({ - message: "UserVolunteer service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async deleteUserVolunteer(req: Request, res: Response) { - res.status(501).json({ - message: "UserVolunteer service temporarily disabled during migration", - error: "Service migration in progress" - }); - } -} - -export default new UserVolunteerController(); \ No newline at end of file diff --git a/src/modules/user/dto/CreateUserDto.ts b/src/modules/user/presentation/dto/CreateUserDto.ts similarity index 71% rename from src/modules/user/dto/CreateUserDto.ts rename to src/modules/user/presentation/dto/CreateUserDto.ts index 6706c42..ac04d35 100644 --- a/src/modules/user/dto/CreateUserDto.ts +++ b/src/modules/user/presentation/dto/CreateUserDto.ts @@ -1,10 +1,10 @@ +import { IsStellarPublicKey } from "@/shared/validators/StellarPublicKey"; import { IsString, IsEmail, MinLength, MaxLength, IsOptional, - Matches, } from "class-validator"; export class CreateUserDto { @@ -27,15 +27,6 @@ export class CreateUserDto { password: string; @IsOptional() - @IsString({ message: "Wallet address must be a string" }) - @MinLength(56, { - message: "Stellar wallet address must be 56 characters long", - }) - @MaxLength(56, { - message: "Stellar wallet address must be 56 characters long", - }) - @Matches(/^G[A-Z2-7]{55}$/, { - message: "Invalid Stellar wallet address format", - }) - wallet?: string; + @IsStellarPublicKey() + wallet: string; } diff --git a/src/modules/user/dto/UpdateUserDto.ts b/src/modules/user/presentation/dto/UpdateUserDto.ts similarity index 73% rename from src/modules/user/dto/UpdateUserDto.ts rename to src/modules/user/presentation/dto/UpdateUserDto.ts index 8687e70..93d97aa 100644 --- a/src/modules/user/dto/UpdateUserDto.ts +++ b/src/modules/user/presentation/dto/UpdateUserDto.ts @@ -1,10 +1,10 @@ +import { IsStellarPublicKey } from "@/shared/validators/StellarPublicKey"; import { IsString, IsEmail, MinLength, MaxLength, IsOptional, - Matches, } from "class-validator"; export class UpdateUserDto { @@ -31,15 +31,6 @@ export class UpdateUserDto { password?: string; @IsOptional() - @IsString({ message: "Wallet address must be a string" }) - @MinLength(56, { - message: "Stellar wallet address must be 56 characters long", - }) - @MaxLength(56, { - message: "Stellar wallet address must be 56 characters long", - }) - @Matches(/^G[A-Z2-7]{55}$/, { - message: "Invalid Stellar wallet address format", - }) + @IsStellarPublicKey() wallet?: string; } diff --git a/src/modules/user/presentation/dto/index.ts b/src/modules/user/presentation/dto/index.ts new file mode 100644 index 0000000..726877d --- /dev/null +++ b/src/modules/user/presentation/dto/index.ts @@ -0,0 +1,2 @@ +export { CreateUserDto } from "./CreateUserDto"; +export { UpdateUserDto } from "./UpdateUserDto"; diff --git a/src/modules/user/repositories/PrismaUserRepository.ts b/src/modules/user/repositories/PrismaUserRepository.ts deleted file mode 100644 index 714e4f6..0000000 --- a/src/modules/user/repositories/PrismaUserRepository.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { PrismaClient } from "@prisma/client"; -import { IUserRepository } from "../domain/interfaces/IUserRepository"; -import { IUser } from "../domain/interfaces/IUser"; - -const prisma = new PrismaClient(); - -export class PrismaUserRepository implements IUserRepository { - async create(user: IUser): Promise { - return prisma.user.create({ data: user }); - } - - async findById(id: string): Promise { - return prisma.user.findUnique({ where: { id } }); - } - - async findByEmail(email: string): Promise { - return prisma.user.findUnique({ where: { email } }); - } - - async update(user: IUser): Promise { - return prisma.user.update({ where: { id: user.id }, data: user }); - } - - async findAll( - page: number, - pageSize: number - ): Promise<{ users: any[]; total: number }> { - const skip = (page - 1) * pageSize; - const [users, total] = await Promise.all([ - prisma.user.findMany({ - skip, - take: pageSize, - orderBy: { createdAt: "desc" }, - }), - prisma.user.count(), - ]); - return { users, total }; - } - - async delete(id: string): Promise { - await prisma.user.delete({ where: { id } }); - } - - async findByVerificationToken(token: string): Promise { - return prisma.user.findFirst({ where: { verificationToken: token } }); - } - - async setVerificationToken( - userId: string, - token: string, - expires: Date - ): Promise { - await prisma.user.update({ - where: { id: userId }, - data: { - verificationToken: token, - verificationTokenExpires: expires, - }, - }); - } - - async verifyUser(userId: string): Promise { - await prisma.user.update({ - where: { id: userId }, - data: { - isVerified: true, - verificationToken: null, - verificationTokenExpires: null, - }, - }); - } - - async isUserVerified(userId: string): Promise { - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { isVerified: true }, - }); - return user?.isVerified || false; - } -} diff --git a/src/modules/user/use-cases/userUseCase.ts b/src/modules/user/use-cases/userUseCase.ts deleted file mode 100644 index fe4e8a0..0000000 --- a/src/modules/user/use-cases/userUseCase.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { IUserRepository } from "../domain/interfaces/IUserRepository"; -import { CreateUserDto } from "../dto/CreateUserDto"; -import { User } from "../domain/entities/User.entity"; -import bcrypt from "bcryptjs"; -import { UpdateUserDto } from "../dto/UpdateUserDto"; - -export class CreateUserUseCase { - constructor(private userRepository: IUserRepository) {} - - async execute(data: CreateUserDto) { - const hashedPassword = bcrypt.hashSync(data.password, 10); - - const user = new User(); - user.id = crypto.randomUUID(); - user.name = data.name; - user.lastName = data.lastName; - user.email = data.email; - user.password = hashedPassword; - user.wallet = data.wallet; - return this.userRepository.create(user); - } -} - -export class GetUserByIdUseCase { - constructor(private userRepository: IUserRepository) {} - - async execute(id: string) { - if (!id) { - throw new Error("User ID is required."); - } - - const user = await this.userRepository.findById(id); - if (!user) { - throw new Error("User not found."); - } - - return user; - } -} - -export class GetUserByEmailUseCase { - constructor(private userRepository: IUserRepository) {} - - async execute(email: string) { - if (!email) { - throw new Error("Email is required."); - } - - const user = await this.userRepository.findByEmail(email); - if (!user) { - throw new Error("User not found."); - } - - return user; - } -} - -export class GetUsersUseCase { - constructor(private userRepository: IUserRepository) {} - - async execute(page: number = 1, pageSize: number = 10) { - if (page < 1 || pageSize < 1) { - throw new Error("Invalid pagination parameters."); - } - - return this.userRepository.findAll(page, pageSize); - } -} - -export class DeleteUserUseCase { - constructor(private userRepository: IUserRepository) {} - - async execute(id: string): Promise { - await this.userRepository.delete(id); - } -} - -export class UpdateUserUseCase { - constructor(private userRepository: IUserRepository) {} - - async execute(data: UpdateUserDto): Promise { - await this.userRepository.update(data); - } -} diff --git a/src/modules/volunteer/repositories/interfaces/volunteer-repository.interface.ts b/src/modules/volunteer/application/repository/volunteer-repository.ts similarity index 83% rename from src/modules/volunteer/repositories/interfaces/volunteer-repository.interface.ts rename to src/modules/volunteer/application/repository/volunteer-repository.ts index 42346f3..9db2c76 100644 --- a/src/modules/volunteer/repositories/interfaces/volunteer-repository.interface.ts +++ b/src/modules/volunteer/application/repository/volunteer-repository.ts @@ -1,4 +1,4 @@ -import { Volunteer } from "../../domain/entities/volunteer.entity"; +import { Volunteer } from "../../../project/domain/entities/volunteer.entity"; export interface IVolunteerRepository { create(volunteer: Volunteer): Promise; diff --git a/src/modules/volunteer/use-cases/create-volunteer.use-case.ts b/src/modules/volunteer/application/use-cases/create-volunteer.use-case.ts similarity index 56% rename from src/modules/volunteer/use-cases/create-volunteer.use-case.ts rename to src/modules/volunteer/application/use-cases/create-volunteer.use-case.ts index d1a19cb..0b00916 100644 --- a/src/modules/volunteer/use-cases/create-volunteer.use-case.ts +++ b/src/modules/volunteer/application/use-cases/create-volunteer.use-case.ts @@ -1,6 +1,6 @@ -import { Volunteer } from "../domain/entities/volunteer.entity"; -import { IVolunteerRepository } from "../repositories/interfaces/volunteer-repository.interface"; -import { CreateVolunteerDTO } from "../dto/volunteer.dto"; +import { Volunteer } from "../../project/domain/entities/volunteer.entity"; +import { IVolunteerRepository } from "../../repositories/interfaces/volunteer-repository.interface"; +import { CreateVolunteerDTO } from "../../presentation/dto/volunteer.dto"; export class CreateVolunteerUseCase { constructor(private volunteerRepository: IVolunteerRepository) {} diff --git a/src/modules/volunteer/use-cases/delete-volunteer.use-case.ts b/src/modules/volunteer/application/use-cases/delete-volunteer.use-case.ts similarity index 81% rename from src/modules/volunteer/use-cases/delete-volunteer.use-case.ts rename to src/modules/volunteer/application/use-cases/delete-volunteer.use-case.ts index b1fd66a..9eead70 100644 --- a/src/modules/volunteer/use-cases/delete-volunteer.use-case.ts +++ b/src/modules/volunteer/application/use-cases/delete-volunteer.use-case.ts @@ -1,4 +1,4 @@ -import { IVolunteerRepository } from "../repositories/interfaces/volunteer-repository.interface"; +import { IVolunteerRepository } from "../../repositories/interfaces/volunteer-repository.interface"; export class DeleteVolunteerUseCase { constructor(private volunteerRepository: IVolunteerRepository) {} diff --git a/src/modules/volunteer/use-cases/get-volunteers-by-project.use-case.ts b/src/modules/volunteer/application/use-cases/get-volunteers-by-project.use-case.ts similarity index 76% rename from src/modules/volunteer/use-cases/get-volunteers-by-project.use-case.ts rename to src/modules/volunteer/application/use-cases/get-volunteers-by-project.use-case.ts index cd80f9e..65e8db7 100644 --- a/src/modules/volunteer/use-cases/get-volunteers-by-project.use-case.ts +++ b/src/modules/volunteer/application/use-cases/get-volunteers-by-project.use-case.ts @@ -1,5 +1,5 @@ -import { Volunteer } from "../domain/entities/volunteer.entity"; -import { IVolunteerRepository } from "../repositories/interfaces/volunteer-repository.interface"; +import { Volunteer } from "../../project/domain/entities/volunteer.entity"; +import { IVolunteerRepository } from "../../repositories/interfaces/volunteer-repository.interface"; export class GetVolunteersByProjectUseCase { constructor(private volunteerRepository: IVolunteerRepository) {} diff --git a/src/modules/volunteer/use-cases/update-volunteer.use-case.ts b/src/modules/volunteer/application/use-cases/update-volunteer.use-case.ts similarity index 72% rename from src/modules/volunteer/use-cases/update-volunteer.use-case.ts rename to src/modules/volunteer/application/use-cases/update-volunteer.use-case.ts index 26ad726..f22492f 100644 --- a/src/modules/volunteer/use-cases/update-volunteer.use-case.ts +++ b/src/modules/volunteer/application/use-cases/update-volunteer.use-case.ts @@ -1,6 +1,6 @@ -import { Volunteer } from "../domain/entities/volunteer.entity"; -import { IVolunteerRepository } from "../repositories/interfaces/volunteer-repository.interface"; -import { UpdateVolunteerDTO } from "../dto/volunteer.dto"; +import { Volunteer } from "../../project/domain/entities/volunteer.entity"; +import { IVolunteerRepository } from "../../repositories/interfaces/volunteer-repository.interface"; +import { UpdateVolunteerDTO } from "../../presentation/dto/volunteer.dto"; export class UpdateVolunteerUseCase { constructor(private volunteerRepository: IVolunteerRepository) {} diff --git a/src/modules/volunteer/domain/entities/volunteer.entity.ts b/src/modules/volunteer/domain/entities/volunteer.entity.ts deleted file mode 100644 index cb7dd08..0000000 --- a/src/modules/volunteer/domain/entities/volunteer.entity.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Entity, Column, JoinColumn, ManyToOne } from "typeorm"; -import { BaseEntity } from "../../../shared/domain/entities/base.entity"; -import { Project } from "../../../project/domain/entities/project.entity"; - -export interface VolunteerProps { - name: string; - description: string; - requirements: string; - projectId: string; - incentive?: string; -} - -@Entity("volunteers") -export class Volunteer extends BaseEntity { - @Column({ type: "varchar", length: 255, nullable: false }) - name!: string; - - @Column({ type: "varchar", length: 255, nullable: false }) - description!: string; - - @Column({ type: "varchar", length: 255, nullable: false }) - requirements!: string; - - @Column({ nullable: true }) - incentive?: string; - - @Column({ type: "uuid", nullable: false }) - projectId!: string; - - @ManyToOne(() => Project, (project) => project.volunteers, { - nullable: false, - }) - @JoinColumn({ name: "projectId" }) - project!: Project; - - // Domain methods - public static create(props: VolunteerProps): Volunteer { - const volunteer = new Volunteer(); - volunteer.validateProps(props); - - volunteer.name = props.name; - volunteer.description = props.description; - volunteer.requirements = props.requirements; - volunteer.projectId = props.projectId; - volunteer.incentive = props.incentive; - - return volunteer; - } - - public update(props: Partial): void { - if (props.name !== undefined) { - if (!props.name.trim()) { - throw new Error("Name is required"); - } - this.name = props.name; - } - - if (props.description !== undefined) { - if (!props.description.trim()) { - throw new Error("Description is required"); - } - this.description = props.description; - } - - if (props.requirements !== undefined) { - if (!props.requirements.trim()) { - throw new Error("Requirements are required"); - } - this.requirements = props.requirements; - } - - if (props.incentive !== undefined) { - this.incentive = props.incentive; - } - } - - public toObject(): VolunteerProps & { - id: string; - createdAt: Date; - updatedAt: Date; - } { - return { - id: this.id, - name: this.name, - description: this.description, - requirements: this.requirements, - projectId: this.projectId, - incentive: this.incentive, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - }; - } - - private validateProps(props: VolunteerProps): void { - if (!props.name || !props.name.trim()) { - throw new Error("Name is required"); - } - - if (!props.description || !props.description.trim()) { - throw new Error("Description is required"); - } - - if (!props.requirements || !props.requirements.trim()) { - throw new Error("Requirements are required"); - } - - if (!props.projectId || !props.projectId.trim()) { - throw new Error("Project ID is required"); - } - } -} diff --git a/src/modules/volunteer/repositories/implementations/volunteer-prisma.repository.ts b/src/modules/volunteer/infrastructure/repositorys/volunteer-prisma.repository.ts similarity index 94% rename from src/modules/volunteer/repositories/implementations/volunteer-prisma.repository.ts rename to src/modules/volunteer/infrastructure/repositorys/volunteer-prisma.repository.ts index e790727..ba36d04 100644 --- a/src/modules/volunteer/repositories/implementations/volunteer-prisma.repository.ts +++ b/src/modules/volunteer/infrastructure/repositorys/volunteer-prisma.repository.ts @@ -1,5 +1,5 @@ import { PrismaClient } from "@prisma/client"; -import { Volunteer } from "../../domain/entities/volunteer.entity"; +import { Volunteer } from "../../../project/domain/entities/volunteer.entity"; import { IVolunteerRepository } from "../interfaces/volunteer-repository.interface"; export class VolunteerPrismaRepository implements IVolunteerRepository { @@ -51,7 +51,7 @@ export class VolunteerPrismaRepository implements IVolunteerRepository { take: pageSize, }); - return volunteers.map((v: any) => + return volunteers.map((v: { id: string }) => Volunteer.create({ ...v, id: v.id, diff --git a/src/modules/volunteer/presentation/controllers/VolunteerController.disabled b/src/modules/volunteer/presentation/controllers/VolunteerController.disabled deleted file mode 100644 index 1cdd09b..0000000 --- a/src/modules/volunteer/presentation/controllers/VolunteerController.disabled +++ /dev/null @@ -1,52 +0,0 @@ -import { Request, Response } from "express"; -import VolunteerService from "../../../../services/VolunteerService"; -import { CreateVolunteerDTO } from "../../../../modules/volunteer/dto/volunteer.dto"; - -export default class VolunteerController { - private volunteerService = new VolunteerService(); - - async createVolunteer(req: Request, res: Response): Promise { - try { - const volunteerData: CreateVolunteerDTO = req.body; - const volunteer = - await this.volunteerService.createVolunteer(volunteerData); - res.status(201).json(volunteer); - } catch (error) { - res.status(400).json({ - error: - error instanceof Error ? error.message : "An unknown error occurred", - }); - } - } - - async getVolunteerById(req: Request, res: Response): Promise { - try { - const { id } = req.params; - const volunteer = await this.volunteerService.getVolunteerById(id); - if (!volunteer) { - res.status(404).json({ error: "Volunteer not found" }); - return; - } - res.status(200).json(volunteer); - } catch (error) { - res.status(400).json({ - error: - error instanceof Error ? error.message : "An unknown error occurred", - }); - } - } - - async getVolunteersByProjectId(req: Request, res: Response): Promise { - try { - const { projectId } = req.params; - const volunteers = - await this.volunteerService.getVolunteersByProjectId(projectId); - res.status(200).json(volunteers); - } catch (error) { - res.status(400).json({ - error: - error instanceof Error ? error.message : "An unknown error occurred", - }); - } - } -} diff --git a/src/modules/volunteer/presentation/controllers/VolunteerController.stub.ts b/src/modules/volunteer/presentation/controllers/VolunteerController.ts similarity index 83% rename from src/modules/volunteer/presentation/controllers/VolunteerController.stub.ts rename to src/modules/volunteer/presentation/controllers/VolunteerController.ts index bd0b172..ee56219 100644 --- a/src/modules/volunteer/presentation/controllers/VolunteerController.stub.ts +++ b/src/modules/volunteer/presentation/controllers/VolunteerController.ts @@ -9,35 +9,35 @@ export default class VolunteerController { async createVolunteer(req: Request, res: Response) { res.status(501).json({ message: "Volunteer service temporarily disabled during migration", - error: "Service migration in progress" + error: "Service migration in progress", }); } async getVolunteerById(req: Request, res: Response) { res.status(501).json({ message: "Volunteer service temporarily disabled during migration", - error: "Service migration in progress" + error: "Service migration in progress", }); } async getVolunteersByProjectId(req: Request, res: Response) { res.status(501).json({ message: "Volunteer service temporarily disabled during migration", - error: "Service migration in progress" + error: "Service migration in progress", }); } async updateVolunteer(req: Request, res: Response) { res.status(501).json({ message: "Volunteer service temporarily disabled during migration", - error: "Service migration in progress" + error: "Service migration in progress", }); } async deleteVolunteer(req: Request, res: Response) { res.status(501).json({ message: "Volunteer service temporarily disabled during migration", - error: "Service migration in progress" + error: "Service migration in progress", }); } -} \ No newline at end of file +} diff --git a/src/modules/volunteer/dto/volunteer.dto.ts b/src/modules/volunteer/presentation/dto/volunteer.dto.ts similarity index 100% rename from src/modules/volunteer/dto/volunteer.dto.ts rename to src/modules/volunteer/presentation/dto/volunteer.dto.ts diff --git a/src/modules/wallet/repositories/HorizonWalletRepository.ts b/src/modules/wallet/application/repositories/HorizonWalletRepository.ts similarity index 87% rename from src/modules/wallet/repositories/HorizonWalletRepository.ts rename to src/modules/wallet/application/repositories/HorizonWalletRepository.ts index f504935..1f3c917 100644 --- a/src/modules/wallet/repositories/HorizonWalletRepository.ts +++ b/src/modules/wallet/application/repositories/HorizonWalletRepository.ts @@ -1,8 +1,9 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Horizon, Networks } from "@stellar/stellar-sdk"; -import { IWalletRepository } from "../domain/interfaces/IWalletRepository"; -import { WalletVerification } from "../domain/entities/WalletVerification"; -import { StellarAddress } from "../domain/value-objects/StellarAddress"; -import { horizonConfig } from "../../../config/horizon.config"; +import { IWalletRepository } from "../../domain/interfaces/IWalletRepository"; +import { WalletVerification } from "../../domain/entities/WalletVerification"; +import { StellarAddress } from "../../domain/value-objects/StellarAddress"; +import { horizonConfig } from "../../../../config/horizon.config"; export class HorizonWalletRepository implements IWalletRepository { private server: Horizon.Server; diff --git a/src/modules/wallet/use-cases/ValidateWalletFormatUseCase.ts b/src/modules/wallet/application/use-cases/ValidateWalletFormatUseCase.ts similarity index 71% rename from src/modules/wallet/use-cases/ValidateWalletFormatUseCase.ts rename to src/modules/wallet/application/use-cases/ValidateWalletFormatUseCase.ts index 163d447..65a53e3 100644 --- a/src/modules/wallet/use-cases/ValidateWalletFormatUseCase.ts +++ b/src/modules/wallet/application/use-cases/ValidateWalletFormatUseCase.ts @@ -1,6 +1,6 @@ import { StellarAddress } from "../domain/value-objects/StellarAddress"; -import { WalletVerificationRequestDto } from "../dto/WalletVerificationRequestDto"; -import { WalletVerificationResponseDto } from "../dto/WalletVerificationResponseDto"; +import { WalletVerificationRequestDto } from "../presentation/dto/WalletVerificationRequestDto"; +import { WalletVerificationResponseDto } from "../presentation/dto/WalletVerificationResponseDto"; export class ValidateWalletFormatUseCase { async execute( @@ -18,10 +18,10 @@ export class ValidateWalletFormatUseCase { "Wallet address format is valid", new Date() ); - } catch (error: any) { + } catch (error: unknown) { return WalletVerificationResponseDto.createError( dto.walletAddress, - error.message || "Invalid wallet address format" + error || "Invalid wallet address format" ); } } diff --git a/src/modules/wallet/use-cases/VerifyWalletUseCase.ts b/src/modules/wallet/application/use-cases/VerifyWalletUseCase.ts similarity index 74% rename from src/modules/wallet/use-cases/VerifyWalletUseCase.ts rename to src/modules/wallet/application/use-cases/VerifyWalletUseCase.ts index 9a75ff8..de576fe 100644 --- a/src/modules/wallet/use-cases/VerifyWalletUseCase.ts +++ b/src/modules/wallet/application/use-cases/VerifyWalletUseCase.ts @@ -1,7 +1,7 @@ import { IWalletRepository } from "../domain/interfaces/IWalletRepository"; import { StellarAddress } from "../domain/value-objects/StellarAddress"; -import { WalletVerificationRequestDto } from "../dto/WalletVerificationRequestDto"; -import { WalletVerificationResponseDto } from "../dto/WalletVerificationResponseDto"; +import { WalletVerificationRequestDto } from "../presentation/dto/WalletVerificationRequestDto"; +import { WalletVerificationResponseDto } from "../presentation/dto/WalletVerificationResponseDto"; export class VerifyWalletUseCase { constructor(private walletRepository: IWalletRepository) {} @@ -18,10 +18,10 @@ export class VerifyWalletUseCase { await this.walletRepository.verifyWallet(stellarAddress); return WalletVerificationResponseDto.fromWalletVerification(verification); - } catch (error: any) { + } catch (error: unknown) { return WalletVerificationResponseDto.createError( dto.walletAddress, - error.message || "Wallet verification failed" + error || "Wallet verification failed" ); } } diff --git a/src/modules/wallet/index.ts b/src/modules/wallet/index.ts index a1f4e5a..fe123ab 100644 --- a/src/modules/wallet/index.ts +++ b/src/modules/wallet/index.ts @@ -4,15 +4,15 @@ export { StellarAddress } from "./domain/value-objects/StellarAddress"; export { IWalletRepository } from "./domain/interfaces/IWalletRepository"; // DTOs -export { WalletVerificationRequestDto } from "./dto/WalletVerificationRequestDto"; -export { WalletVerificationResponseDto } from "./dto/WalletVerificationResponseDto"; +export { WalletVerificationRequestDto } from "./presentation/dto/WalletVerificationRequestDto"; +export { WalletVerificationResponseDto } from "./presentation/dto/WalletVerificationResponseDto"; // Use Cases export { VerifyWalletUseCase } from "./use-cases/VerifyWalletUseCase"; export { ValidateWalletFormatUseCase } from "./use-cases/ValidateWalletFormatUseCase"; // Repositories -export { HorizonWalletRepository } from "./repositories/HorizonWalletRepository"; +export { HorizonWalletRepository } from "./application/repositories/HorizonWalletRepository"; // Services export { WalletService } from "./services/WalletService"; diff --git a/src/modules/wallet/presentation/controller/walletController.ts b/src/modules/wallet/presentation/controller/walletController.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/wallet/dto/WalletVerificationRequestDto.ts b/src/modules/wallet/presentation/dto/WalletVerificationRequestDto.ts similarity index 100% rename from src/modules/wallet/dto/WalletVerificationRequestDto.ts rename to src/modules/wallet/presentation/dto/WalletVerificationRequestDto.ts diff --git a/src/modules/wallet/dto/WalletVerificationResponseDto.ts b/src/modules/wallet/presentation/dto/WalletVerificationResponseDto.ts similarity index 95% rename from src/modules/wallet/dto/WalletVerificationResponseDto.ts rename to src/modules/wallet/presentation/dto/WalletVerificationResponseDto.ts index 5e98170..22c0dd9 100644 --- a/src/modules/wallet/dto/WalletVerificationResponseDto.ts +++ b/src/modules/wallet/presentation/dto/WalletVerificationResponseDto.ts @@ -32,7 +32,7 @@ export class WalletVerificationResponseDto { } public static fromWalletVerification( - verification: import("../domain/entities/WalletVerification").WalletVerification + verification: import("../../domain/entities/WalletVerification").WalletVerification ): WalletVerificationResponseDto { const success = verification.isValid; const message = verification.isVerified() diff --git a/src/repository/IPhotoRepository.ts b/src/repository/IPhotoRepository.ts deleted file mode 100644 index 061fd4b..0000000 --- a/src/repository/IPhotoRepository.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Photo } from "../modules/photo/domain/entities/photo.entity"; - -export interface IPhotoRepository { - findById(id: string): Promise; - findAll(): Promise; - create(data: Partial): Promise; - update(id: string, data: Partial): Promise; - delete(id: string): Promise; -} diff --git a/src/repository/IUserRepository.ts b/src/repository/IUserRepository.ts deleted file mode 100644 index 67db0b1..0000000 --- a/src/repository/IUserRepository.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { User } from "../modules/user/domain/entities/User.entity"; - -export interface IUserRepository { - createUser( - name: string, - email: string, - password: string, - wallet: string - ): Promise; - findByEmail(email: string): Promise; - findById(userId: string): Promise; - saveVerificationToken(email: string, token: string): Promise; - updateVerificationToken( - userId: string, - token: string, - expires: Date - ): Promise; - findByVerificationToken(token: string): Promise; - updateVerificationStatus(userId: string): Promise; - isUserVerified(userId: string): Promise; -} diff --git a/src/repository/PhotoRepository.ts b/src/repository/PhotoRepository.ts deleted file mode 100644 index 75a6ad5..0000000 --- a/src/repository/PhotoRepository.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { PrismaClient } from "@prisma/client"; -import { IPhotoRepository } from "./IPhotoRepository"; -import { Photo } from "../modules/photo/domain/entities/photo.entity"; - -// Define our own types based on the Prisma schema -interface PrismaPhoto { - id: string; - url: string; - userId: string; - uploadedAt: Date; - metadata: any; -} - -const prisma = new PrismaClient(); - -export class PhotoRepository implements IPhotoRepository { - async findById(id: string): Promise { - const record = (await prisma.photo.findUnique({ - where: { id }, - })) as unknown as PrismaPhoto | null; - return record ? Photo.create({ - id: record.id, - url: record.url, - userId: record.userId, - uploadedAt: record.uploadedAt, - metadata: record.metadata - }) : null; - } - - async findAll(): Promise { - const records = (await prisma.photo.findMany()) as unknown as PrismaPhoto[]; - return records.map((r) => Photo.create({ - id: r.id, - url: r.url, - userId: r.userId, - uploadedAt: r.uploadedAt, - metadata: r.metadata - })); - } - - async create(data: Partial): Promise { - const photo = Photo.create({ - url: data.url!, - userId: data.userId!, - uploadedAt: new Date(), - metadata: data.metadata || {}, - }); - const created = (await prisma.photo.create({ - data: { - url: photo.url, - userId: photo.userId, - metadata: photo.metadata || {}, - }, - })) as unknown as PrismaPhoto; - return Photo.create({ - id: created.id, - url: created.url, - userId: created.userId, - uploadedAt: created.uploadedAt, - metadata: created.metadata - }); - } - - async update(id: string, data: Partial): Promise { - const updated = (await prisma.photo.update({ - where: { id }, - data: { - url: data.url, - userId: data.userId, - metadata: data.metadata, - }, - })) as unknown as PrismaPhoto; - return Photo.create({ - id: updated.id, - url: updated.url, - userId: updated.userId, - uploadedAt: updated.uploadedAt, - metadata: updated.metadata - }); - } - - async delete(id: string): Promise { - await prisma.photo.delete({ where: { id } }); - } -} diff --git a/src/repository/user.repository.ts b/src/repository/user.repository.ts deleted file mode 100644 index d1ce11e..0000000 --- a/src/repository/user.repository.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Repository, DataSource } from "typeorm"; -import { User } from "../modules/user/domain/entities/User.entity"; -import { IUserRepository } from "./IUserRepository"; - -export class UserRepository - extends Repository - implements IUserRepository -{ - constructor(dataSource: DataSource) { - super(User, dataSource.manager); - } - - async createUser( - name: string, - email: string, - password: string, - wallet: string - ): Promise { - const user = this.create({ name, email, password, wallet }); - return await this.save(user); - } - - async findByEmail(email: string): Promise { - return this.findOne({ where: { email } }); - } - - async findById(userId: string): Promise { - return this.findOne({ where: { id: userId } }); - } - - async saveVerificationToken(email: string, token: string): Promise { - await this.update({ email }, { verificationToken: token }); - } - - async updateVerificationToken( - userId: string, - token: string, - expires: Date - ): Promise { - await this.update( - { id: userId }, - { verificationToken: token, verificationTokenExpires: expires } - ); - } - - async findByVerificationToken(token: string): Promise { - return this.findOne({ where: { verificationToken: token } }); - } - - async updateVerificationStatus(userId: string): Promise { - await this.update( - { id: userId }, - { - isVerified: true, - verificationToken: undefined, - verificationTokenExpires: undefined, - } - ); - } - - async isUserVerified(userId: string): Promise { - const user = await this.findById(userId); - return user ? user.isVerified : false; - } -} diff --git a/src/routes.ts b/src/routes.ts new file mode 100644 index 0000000..dc11b97 --- /dev/null +++ b/src/routes.ts @@ -0,0 +1,12 @@ +import { Router } from "express"; +import { AuthRoutes } from "./modules/auth/presentation/routes"; + +export class AppRoutes { + static get routes(): Router { + const router = Router(); + + router.use("/api/auth", AuthRoutes.routes); + + return router; + } +} diff --git a/src/routes/OrganizationRoutes.ts b/src/routes/OrganizationRoutes.ts deleted file mode 100644 index 4f91a38..0000000 --- a/src/routes/OrganizationRoutes.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Router } from "express"; -import OrganizationController from "../modules/organization/presentation/controllers/OrganizationController.stub"; -import auth from "../middleware/authMiddleware"; - -const router = Router(); - -// Public routes -router.post("/", OrganizationController.createOrganization); -router.get("/", OrganizationController.getAllOrganizations); -router.get("/:id", OrganizationController.getOrganizationById); -router.get("/email/:email", OrganizationController.getOrganizationByEmail); - -// Protected routes -router.put( - "/:id", - auth.authMiddleware, - OrganizationController.updateOrganization -); -router.delete( - "/:id", - auth.authMiddleware, - OrganizationController.deleteOrganization -); - -export default router; diff --git a/src/routes/ProjectRoutes.ts b/src/routes/ProjectRoutes.ts deleted file mode 100644 index d48c4bd..0000000 --- a/src/routes/ProjectRoutes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Router } from "express"; -import ProjectController from "../modules/project/presentation/controllers/Project.controller.stub"; - -const router = Router(); -const projectController = new ProjectController(); - -router.post("/", async (req, res) => projectController.createProject(req, res)); -router.get("/:id", async (req, res) => - projectController.getProjectById(req, res) -); -router.get("/organizations/:organizationId", async (req, res) => - projectController.getProjectsByOrganizationId(req, res) -); - -export default router; diff --git a/src/routes/VolunteerRoutes.ts b/src/routes/VolunteerRoutes.ts deleted file mode 100644 index 24fe3b9..0000000 --- a/src/routes/VolunteerRoutes.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Router } from "express"; -import VolunteerController from "../modules/volunteer/presentation/controllers/VolunteerController.stub"; - -const router = Router(); -const volunteerController = new VolunteerController(); - -router.post("/", async (req, res) => - volunteerController.createVolunteer(req, res) -); -router.get("/:id", async (req, res) => - volunteerController.getVolunteerById(req, res) -); -router.get("/projects/:projectId", async (req, res) => - volunteerController.getVolunteersByProjectId(req, res) -); - -export default router; diff --git a/src/routes/authRoutes.ts b/src/routes/authRoutes.ts deleted file mode 100644 index 6336aeb..0000000 --- a/src/routes/authRoutes.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Router } from "express"; -import AuthController from "../modules/auth/presentation/controllers/Auth.controller"; -// import authMiddleware from "../middleware/authMiddleware"; // Temporarily disabled - -const router = Router(); - -// Health check for auth module -router.get("/health", (req, res) => { - res.json({ status: "Auth module is available", module: "auth" }); -}); - -// Public routes (now using functional controller) -router.post("/register", AuthController.register); -router.post("/login", AuthController.login); - -router.post("/send-verification-email", AuthController.resendVerificationEmail); -router.get("/verify-email/:token", AuthController.verifyEmail); -router.get("/verify-email", AuthController.verifyEmail); // Support query param method -router.post("/resend-verification", AuthController.resendVerificationEmail); - -// Wallet verification routes -router.post("/verify-wallet", AuthController.verifyWallet); -router.post("/validate-wallet-format", AuthController.validateWalletFormat); - -// Protected routes - temporarily commented out due to interface mismatch -// router.get('/protected', authMiddleware.authMiddleware, AuthController.protectedRoute); -// router.get('/verification-status', authMiddleware.authMiddleware, AuthController.checkVerificationStatus); - -// Routes requiring verified email - temporarily commented out due to interface mismatch -// router.get('/verified-only', -// authMiddleware.authMiddleware, -// authMiddleware.requireVerifiedEmail, -// (req, res) => { -// res.json({ message: "You have access to this protected route because your email is verified!" }); -// } -// ); - -export default router; diff --git a/src/routes/certificatesRoutes.ts b/src/routes/certificatesRoutes.ts deleted file mode 100644 index 87ec7aa..0000000 --- a/src/routes/certificatesRoutes.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Router } from "express"; -import { - downloadCertificate, - createCertificate, -} from "../modules/certificate/presentation/controllers/certificate.controller"; -import auth from "../middleware/authMiddleware"; - -const router = Router(); - -router.get("/volunteers/:id", auth.authMiddleware, downloadCertificate); -router.post("/volunteers/:id", auth.authMiddleware, createCertificate); - -export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts deleted file mode 100644 index 21cc85c..0000000 --- a/src/routes/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Router } from "express"; -import v1Router from "./v1"; - -const apiRouter = Router(); - -/** - * API Versioning Router - * - * This router handles API versioning by namespacing routes under version prefixes. - * - * Current versions: - * - /v1/ - Current stable API version - * - /v2/ - Reserved for future expansion - */ - -// V1 API Routes -apiRouter.use("/v1", v1Router); - -// V2 API Routes (Reserved for future expansion) -// apiRouter.use("/v2", v2Router); - -// Version info endpoint -apiRouter.get("/", (req, res) => { - res.json({ - message: "VolunChain API", - versions: { - v1: { - status: "stable", - description: "Current stable API version", - endpoints: "/v1/", - }, - v2: { - status: "reserved", - description: "Reserved for future expansion", - endpoints: "/v2/", - }, - }, - documentation: "/api/docs", - }); -}); - -export default apiRouter; diff --git a/src/routes/nftRoutes.ts b/src/routes/nftRoutes.ts deleted file mode 100644 index 279cf05..0000000 --- a/src/routes/nftRoutes.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Router } from "express"; -import NFTController from "../modules/nft/presentation/controllers/NFTController.stub"; -import { - validateDto, - validateParamsDto, -} from "../shared/middleware/validation.middleware"; -import { CreateNFTDto } from "../modules/nft/dto/create-nft.dto"; -import { UuidParamsDto } from "../shared/dto/base.dto"; - -const router = Router(); - -router.post("/nfts", validateDto(CreateNFTDto), NFTController.createNFT); - -router.get( - "/nfts/:id", - validateParamsDto(UuidParamsDto), - NFTController.getNFTById -); - -router.get( - "/users/:userId/nfts", - validateParamsDto(UuidParamsDto), - NFTController.getNFTsByUserId -); - -router.delete( - "/nfts/:id", - validateParamsDto(UuidParamsDto), - NFTController.deleteNFT -); - -export default router; diff --git a/src/routes/testRoutes.ts b/src/routes/testRoutes.ts deleted file mode 100644 index 56101bc..0000000 --- a/src/routes/testRoutes.ts +++ /dev/null @@ -1,40 +0,0 @@ -import express from "express"; -import { prisma } from "../config/prisma"; - -const router = express.Router(); - -// Test route for database performance -router.get("/db-test", async (req, res) => { - try { - // Perform multiple queries to test connection pooling - const startTime = Date.now(); - - // Parallel queries to test connection pool - const [users, organizations, projects] = await Promise.all([ - prisma.user.findMany({ take: 5 }), - prisma.organization.findMany({ take: 5 }), - prisma.project.findMany({ take: 5 }), - ]); - - const endTime = Date.now(); - const duration = endTime - startTime; - - res.json({ - success: true, - duration: `${duration}ms`, - results: { - users: users.length, - organizations: organizations.length, - projects: projects.length, - }, - }); - } catch (error) { - console.error("Database test error:", error); - res.status(500).json({ - success: false, - error: "Database test failed", - }); - } -}); - -export default router; diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts deleted file mode 100644 index a30447a..0000000 --- a/src/routes/userRoutes.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - Router, - Request, - Response, - RequestHandler, - NextFunction, -} from "express"; -import UserController from "../modules/user/presentation/controllers/UserController.stub"; -import { authMiddleware } from "../middleware/authMiddleware"; -import { AuthenticatedRequest } from "../types/auth.types"; - -const userController = new UserController(); -const router = Router(); - -type AuthenticatedHandler = ( - req: AuthenticatedRequest, - res: Response -) => Promise; - -const wrapHandler = (handler: AuthenticatedHandler): RequestHandler => { - return ((req: Request, res: Response, next: NextFunction) => { - handler(req as AuthenticatedRequest, res).catch(next); - }) as unknown as RequestHandler; -}; - -// Public routes -router.post( - "/users", - wrapHandler(userController.createUser.bind(userController)) -); -router.get( - "/users/:id", - wrapHandler(userController.getUserById.bind(userController)) -); -router.get( - "/users/:email", - wrapHandler(userController.getUserByEmail.bind(userController)) -); - -// Protected routes -router.put( - "/users/:id", - authMiddleware, - wrapHandler(userController.updateUser.bind(userController)) -); - -export default router; diff --git a/src/routes/v1/index.ts b/src/routes/v1/index.ts deleted file mode 100644 index 2dea990..0000000 --- a/src/routes/v1/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Router } from "express"; -import authRoutes from "../authRoutes"; -import nftRoutes from "../nftRoutes"; -import userRoutes from "../userRoutes"; -import metricsRoutes from "../../modules/metrics/routes/metrics.routes"; -import certificateRoutes from "../certificatesRoutes"; -import volunteerRoutes from "../VolunteerRoutes"; -import projectRoutes from "../ProjectRoutes"; -import organizationRoutes from "../OrganizationRoutes"; - -const v1Router = Router(); - -/** - * V1 API Routes - * All routes are namespaced under /v1/ - */ - -// Authentication routes - /v1/auth/* -v1Router.use("/auth", authRoutes); - -// NFT routes - /v1/nft/* -v1Router.use("/nft", nftRoutes); - -// User routes - /v1/users/* -v1Router.use("/users", userRoutes); - -// Metrics routes - /v1/metrics/* -v1Router.use("/metrics", metricsRoutes); - -// Certificate routes - /v1/certificate/* -v1Router.use("/certificate", certificateRoutes); - -// Project routes - /v1/projects/* -v1Router.use("/projects", projectRoutes); - -// Volunteer routes - /v1/volunteers/* -v1Router.use("/volunteers", volunteerRoutes); - -// Organization routes - /v1/organizations/* -v1Router.use("/organizations", organizationRoutes); - -export default v1Router; diff --git a/src/routes/v2/auth.routes.ts b/src/routes/v2/auth.routes.ts deleted file mode 100644 index 61ffa43..0000000 --- a/src/routes/v2/auth.routes.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Router } from "express"; -import { validateDto } from "../../shared/middleware/validation.middleware"; -import { RegisterDto } from "../../modules/auth/dto/register.dto"; -import { LoginDto } from "../../modules/auth/dto/login.dto"; -import { ResendVerificationDTO } from "../../modules/auth/dto/resendVerificationDTO"; -import { VerifyEmailDTO } from "../../modules/auth/dto/verifyEmailDTO"; -import { - ValidateWalletFormatDto, - VerifyWalletDto, -} from "../../modules/auth/dto/wallet-validation.dto"; - -const router = Router(); - -// Note: This is an example of how to properly integrate validation middleware -// The controller would need to be properly instantiated with dependencies - -// POST /auth/register - User registration -router.post( - "/register", - validateDto(RegisterDto) - // authController.register -); - -// POST /auth/login - User login -router.post( - "/login", - validateDto(LoginDto) - // authController.login -); - -// POST /auth/resend-verification - Resend email verification -router.post( - "/resend-verification", - validateDto(ResendVerificationDTO) - // authController.resendVerificationEmail -); - -// POST /auth/verify-email - Verify email with token -router.post( - "/verify-email", - validateDto(VerifyEmailDTO) - // authController.verifyEmail -); - -// POST /auth/validate-wallet-format - Validate wallet address format -router.post( - "/validate-wallet-format", - validateDto(ValidateWalletFormatDto) - // authController.validateWalletFormat -); - -// POST /auth/verify-wallet - Verify wallet ownership -router.post( - "/verify-wallet", - validateDto(VerifyWalletDto) - // authController.verifyWallet -); - -export default router; diff --git a/src/routes/v2/organization.routes.ts b/src/routes/v2/organization.routes.ts deleted file mode 100644 index b58e338..0000000 --- a/src/routes/v2/organization.routes.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Router } from "express"; -import { - validateDto, - validateParamsDto, - validateQueryDto, -} from "../../shared/middleware/validation.middleware"; -import { CreateOrganizationDto } from "../../modules/organization/presentation/dto/create-organization.dto"; -import { UpdateOrganizationDto } from "../../modules/organization/presentation/dto/update-organization.dto"; -import { UuidParamsDto, PaginationQueryDto } from "../../shared/dto/base.dto"; -import auth from "../../middleware/authMiddleware"; - -const router = Router(); - -// Note: This is an example of how to properly integrate validation middleware -// The controller would need to be properly instantiated with dependencies - -// POST /organizations - Create organization -router.post( - "/", - validateDto(CreateOrganizationDto) - // organizationController.createOrganization -); - -// GET /organizations - Get all organizations with pagination -router.get( - "/", - validateQueryDto(PaginationQueryDto) - // organizationController.getAllOrganizations -); - -// GET /organizations/:id - Get organization by ID -router.get( - "/:id", - validateParamsDto(UuidParamsDto) - // organizationController.getOrganizationById -); - -// PUT /organizations/:id - Update organization (protected) -router.put( - "/:id", - auth.authMiddleware, - validateParamsDto(UuidParamsDto), - validateDto(UpdateOrganizationDto) - // organizationController.updateOrganization -); - -// DELETE /organizations/:id - Delete organization (protected) -router.delete( - "/:id", - auth.authMiddleware, - validateParamsDto(UuidParamsDto) - // organizationController.deleteOrganization -); - -export default router; diff --git a/src/shared/README.md b/src/shared/README.md new file mode 100644 index 0000000..d7f1fc4 --- /dev/null +++ b/src/shared/README.md @@ -0,0 +1,199 @@ +# Shared Module + +## Overview + +The `shared` directory contains common utilities, components, and functionality that are used across multiple modules in the VolunChain application. This module promotes code reusability, maintainability, and consistent implementation patterns throughout the application. + +## Directory Structure + +``` +├── README.md +├── dto/ +│ └── base.dto.ts # Base Data Transfer Object classes +├── exceptions/ +│ ├── AppException.ts # Base exception class and common constants +│ └── DomainExceptions.ts # Domain-specific exception classes +├── infrastructure/ +│ └── utils/ +│ └── qrGenerator.ts # QR code generation utility +└── middleware/ + ├── __tests__/ + │ └── validation.middleware.test.ts + ├── errorHandler.ts # Global error handling middleware + └── validation.middleware.ts # Request validation middleware +``` + +## Modules + +### DTO (Data Transfer Objects) + +The `dto` directory contains base classes for data transfer objects used throughout the application. + +- `base.dto.ts` - Provides common DTO classes including: + - `UuidParamsDto` - For validating UUID parameters + - `PaginationQueryDto` - For handling pagination parameters + - `BaseResponseDto` - Standard response structure + - `ErrorResponseDto` - Standard error response structure + +#### Usage + +```typescript +import { PaginationQueryDto } from "@shared/dto/base.dto"; + +// In your controller +export class ProjectController { + @Get() + async getProjects(@Query() query: PaginationQueryDto) { + // query.page, query.limit, and query.search are validated + // ... + } +} +``` + +### Exceptions + +The exceptions directory contains the centralized error handling system for the VolunChain application. The system provides a consistent way to handle and format errors across the application. + +- `AppException.ts` - Base exception class and common constants +- `DomainExceptions.ts` - Domain-specific exception classes +- `errorHandler.ts` - Modules Error handler + +#### Throwing Errors + +```typescript +// In your controllers/services +import { + ValidationException, + NotFoundException, +} from "@shared/exceptions/DomainExceptions"; + +function updateUser(id: string, data: UserUpdateDto) { + const user = await userRepository.findById(id); + if (!user) { + throw new NotFoundException("User not found"); + } + + if (!isValid(data)) { + throw new ValidationException("Invalid user data", { + invalidFields: ["email", "name"], + }); + } + // ... +} +``` + +#### Error Response Format + +All errors are returned in the following format: + +```json +{ + "statusCode": 400, + "message": "Invalid user data", + "errorCode": "VALIDATION_ERROR", + "details": { + "invalidFields": ["email", "name"] + } +} +``` + +#### Available Exception Classes + +1. `ValidationException` (400) + + - For invalid input data + - Include details about validation failures + +2. `AuthenticationException` (401) + + - For authentication failures + - Use when user is not authenticated + +3. `AuthorizationException` (403) + + - For permission issues + - Use when user lacks required permissions + +4. `NotFoundException` (404) + + - For missing resources + - Use when requested entity doesn't exist + +5. `ConflictException` (409) + + - For resource conflicts + - Use for unique constraint violations + +6. `InternalServerException` (500) + - For unexpected server errors + - Use as a last resort + +### Infrastructure + +The `infrastructure` directory contains utilities and services that interact with external systems or provide core functionality. + +- `utils/qrGenerator.ts` - Utility for generating QR codes + +#### Usage + +```typescript +import { generateQRCode } from "@shared/infrastructure/utils/qrGenerator"; + +async function createCertificateQR(certificateId: string) { + const url = `https://volunchain.org/verify/${certificateId}`; + const qrBuffer = await generateQRCode(url); + // Use the QR code buffer... +} +``` + +### Middleware + +The `middleware` directory contains Express middleware functions used throughout the application. + +- `errorHandler.ts` - Global error handling middleware +- `validation.middleware.ts` - Request validation middleware using class-validator + +#### Validation Middleware Usage + +```typescript +import { validateDto } from "@shared/middleware/validation.middleware"; +import { CreateUserDto } from "./dto/create-user.dto"; + +// In your routes file +router.post("/users", validateDto(CreateUserDto), userController.create); +``` + +## Best Practices + +1. Always use the most specific exception class available +2. Include meaningful error messages +3. Add relevant details when available +4. Don't expose sensitive information in error messages +5. Log errors appropriately before throwing +6. Use DTOs for request validation and response formatting +7. Keep shared utilities focused and well-tested + +## Development Mode + +In development mode, additional information is included in the error response: + +- Stack trace +- Request body +- Request query parameters + +## Testing + +Run the test suite: + +```bash +npm test -- --grep "shared" +``` + +## Contributing + +When adding new shared components: + +1. Ensure they are truly reusable across multiple modules +2. Add appropriate tests +3. Update this documentation +4. Follow the established patterns and naming conventions diff --git a/src/shared/infrastructure/container.ts b/src/shared/infrastructure/container.ts deleted file mode 100644 index f31aa07..0000000 --- a/src/shared/infrastructure/container.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { CertificateService } from "../../modules/certificate/application/services/CertificateService"; -import { ICertificateService } from "../../modules/certificate/domain/interfaces/ICertificateService"; - -export const container = { - certificateService: new CertificateService() as ICertificateService, -}; diff --git a/src/shared/infrastructure/utils/setup-s3-bucket.ts b/src/shared/infrastructure/utils/setup-s3-bucket.ts deleted file mode 100644 index 24fb851..0000000 --- a/src/shared/infrastructure/utils/setup-s3-bucket.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - S3Client, - CreateBucketCommand, - PutBucketCorsCommand, -} from "@aws-sdk/client-s3"; -import dotenv from "dotenv"; - -// Load environment variables -dotenv.config(); - -async function setupS3Bucket() { - const region = process.env.AWS_REGION || "us-east-1"; - const bucketName = process.env.S3_BUCKET_NAME; - - if (!bucketName) { - console.error("❌ S3_BUCKET_NAME environment variable is not set"); - process.exit(1); - } - - const s3Client = new S3Client({ - region, - credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID || "", - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "", - }, - }); - - try { - console.log(`Creating bucket: ${bucketName}`); - - const bucketParams: any = { - Bucket: bucketName, - }; - - if (region !== "us-east-1") { - bucketParams.CreateBucketConfiguration = { - LocationConstraint: region as any, - }; - } - - await s3Client.send(new CreateBucketCommand(bucketParams)); - console.log(`✅ Bucket created successfully`); - - // Set up CORS configuration - console.log("Setting up CORS policy..."); - await s3Client.send( - new PutBucketCorsCommand({ - Bucket: bucketName, - CORSConfiguration: { - CORSRules: [ - { - AllowedHeaders: ["*"], - AllowedMethods: ["GET", "PUT", "POST", "DELETE", "HEAD"], - AllowedOrigins: ["*"], - ExposeHeaders: ["ETag"], - MaxAgeSeconds: 3000, - }, - ], - }, - }) - ); - console.log(`✅ CORS policy configured successfully`); - console.log(`🎉 S3 bucket "${bucketName}" is ready for use!`); - } catch (error: any) { - if (error.name === "BucketAlreadyExists") { - console.log(`✅ Bucket "${bucketName}" already exists`); - } else if (error.name === "BucketAlreadyOwnedByYou") { - console.log( - `✅ Bucket "${bucketName}" already exists and is owned by you` - ); - } else { - console.error("❌ Error setting up S3 bucket:", error); - } - } -} - -setupS3Bucket(); diff --git a/src/shared/middleware/validation.middleware.ts b/src/shared/middleware/validation.middleware.ts index 54fb7cc..17243c3 100644 --- a/src/shared/middleware/validation.middleware.ts +++ b/src/shared/middleware/validation.middleware.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { validate, ValidationError } from "class-validator"; -import { plainToClass } from "class-transformer"; +import { plainToInstance } from "class-transformer"; export interface ValidationErrorResponse { success: false; @@ -19,7 +19,7 @@ export function validateDto(dtoClass: new () => T) { next: NextFunction ): Promise => { try { - const dto = plainToClass(dtoClass, req.body); + const dto = plainToInstance(dtoClass, req.body); const errors = await validate(dto); if (errors.length > 0) { @@ -57,7 +57,7 @@ export function validateQueryDto(dtoClass: new () => T) { next: NextFunction ): Promise => { try { - const dto = plainToClass(dtoClass, req.query); + const dto = plainToInstance(dtoClass, req.query); const errors = await validate(dto); if (errors.length > 0) { @@ -77,7 +77,9 @@ export function validateQueryDto(dtoClass: new () => T) { return; } - req.query = dto as Record; + Object.keys(req.query).forEach((key) => delete req.query[key]); + Object.assign(req.query, dto); + next(); } catch { res.status(500).json({ @@ -95,7 +97,7 @@ export function validateParamsDto(dtoClass: new () => T) { next: NextFunction ): Promise => { try { - const dto = plainToClass(dtoClass, req.params); + const dto = plainToInstance(dtoClass, req.params); const errors = await validate(dto); if (errors.length > 0) { diff --git a/src/shared/infrastructure/utils/qrGenerator.ts b/src/shared/utils/qrGenerator.ts similarity index 100% rename from src/shared/infrastructure/utils/qrGenerator.ts rename to src/shared/utils/qrGenerator.ts diff --git a/src/shared/validators/StellarPublicKey.ts b/src/shared/validators/StellarPublicKey.ts new file mode 100644 index 0000000..1245c69 --- /dev/null +++ b/src/shared/validators/StellarPublicKey.ts @@ -0,0 +1,21 @@ +import { registerDecorator, ValidationOptions } from "class-validator"; + +export function IsStellarPublicKey(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: "isStellarPublicKey", + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: unknown) { + if (typeof value !== "string") return false; + return /^G[A-Z2-7]{55}$/.test(value); // formato Stellar + }, + defaultMessage() { + return "Wallet must be a valid Stellar public key (starts with G, 56 chars base32)"; + }, + }, + }); + }; +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..01e48dd --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,5 @@ +export { errorHandler } from "@/shared/middleware/errorHandler"; +export { dbMonitor } from "@/config/prisma"; +export { CronManager } from "./cron"; +export { sendVerificationEmail } from "./email.utils"; +export { Logger } from "./logger"; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 7c897d0..aabd701 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// src/utils/logger.ts export class Logger { private context: string; @@ -7,7 +5,7 @@ export class Logger { this.context = context; } - private formatMessage(level: string, message: string, meta?: any): string { + private formatMessage(level: string, message: string, meta?: object): string { const timestamp = new Date().toISOString(); return JSON.stringify({ timestamp, @@ -18,15 +16,15 @@ export class Logger { }); } - info(message: string, meta?: any) { + info(message: string, meta?: object) { console.log(this.formatMessage("INFO", message, meta)); } - warn(message: string, meta?: any) { + warn(message: string, meta?: object) { console.warn(this.formatMessage("WARN", message, meta)); } - error(message: string, error?: any) { + error(message: string, error?: object) { console.error(this.formatMessage("ERROR", message, error)); } }