diff --git a/package-lock.json b/package-lock.json index cd5a68d..4d74889 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1430,6 +1430,43 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", @@ -2544,19 +2581,71 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@rollup/rollup-openbsd-x64": { "version": "4.62.2", @@ -3230,7 +3319,11 @@ "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, "node_modules/@types/body-parser": { "version": "1.19.6", @@ -3289,14 +3382,25 @@ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, "node_modules/@types/express": { "version": "5.0.6", @@ -3328,7 +3432,11 @@ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -3397,226 +3505,372 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", - "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/type-utils": "8.63.0", - "@typescript-eslint/utils": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.63.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">= 4" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@typescript-eslint/parser": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", - "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", - "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", - "dev": true, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.63.0", - "@typescript-eslint/types": "^8.63.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", - "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@stellar/js-xdr": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", + "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", + "license": "Apache-2.0" + }, + "node_modules/@stellar/stellar-base": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.1.0.tgz", + "integrity": "sha512-90EArG+eCCEzDGj3OJNoCtwpWDwxjv+rs/RNPhvg4bulpjN/CSRj+Ys/SalRcfM4/WRC5/qAfjzmJBAuquWhkA==", + "license": "Apache-2.0", "dependencies": { - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0" + "@stellar/js-xdr": "^3.1.2", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "optionalDependencies": { + "sodium-native": "^4.3.3" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", - "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "dependencies": { + "@types/connect": "*", + "@types/node": "*" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", - "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/utils": "8.63.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", - "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "dependencies": { + "@types/node": "*" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", - "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.63.0", - "@typescript-eslint/tsconfig-utils": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "@types/express": "*" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { @@ -3629,10 +3883,24 @@ "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", "dependencies": { @@ -3682,10 +3950,17 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", - "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "dev": true, "license": "MIT", "dependencies": { @@ -3700,17 +3975,114 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/expect": { @@ -3909,11 +4281,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } }, "node_modules/argparse": { "version": "2.0.1", @@ -4394,6 +4777,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4652,15 +5036,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -4948,6 +5323,43 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -5935,6 +6347,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -7628,6 +8047,138 @@ "node": ">=14.0.0" } }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -7657,25 +8208,6 @@ "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", "license": "MIT" }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/tsx": { "version": "4.22.5", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.5.tgz", @@ -7963,6 +8495,37 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", @@ -8036,6 +8599,19 @@ } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/src/config/index.ts b/src/config/index.ts index 374787a..feee094 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -5,7 +5,9 @@ export const config = { nodeEnv: process.env["NODE_ENV"] ?? "development", jwt: { secret: process.env["JWT_SECRET"] ?? "", - expiresIn: process.env["JWT_EXPIRES_IN"] ?? "7d", + expiresIn: 900, + refreshSecret: process.env["JWT_REFRESH_SECRET"] ?? "", + refreshExpiresIn: 604800, }, stellar: { network: process.env["STELLAR_NETWORK"] ?? "testnet", diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts new file mode 100644 index 0000000..4e6aa96 --- /dev/null +++ b/src/controllers/auth.controller.ts @@ -0,0 +1,200 @@ +import type { Request, Response } from "express"; +import bcrypt from "bcryptjs"; +import jwt from "jsonwebtoken"; +import { prisma } from "../config/prisma.js"; +import { config } from "../config/index.js"; +import { ConflictError, UnauthorizedError, NotFoundError } from "../utils/errors.js"; +import type { AuthRequest } from "../middleware/auth.js"; + +function generateTokens(payload: { userId: string; email: string; role: string }) { + const accessToken = jwt.sign(payload, config.jwt.secret, { + expiresIn: config.jwt.expiresIn, + }); + const refreshToken = jwt.sign(payload, config.jwt.refreshSecret, { + expiresIn: config.jwt.refreshExpiresIn, + }); + return { accessToken, refreshToken }; +} + +function setRefreshTokenCookie(res: Response, refreshToken: string): void { + res.cookie("refreshToken", refreshToken, { + httpOnly: true, + secure: config.nodeEnv === "production", + sameSite: "strict", + path: "/", + maxAge: 7 * 24 * 60 * 60 * 1000, + }); +} + +export async function register(req: Request, res: Response): Promise { + const { fullname, email, password, role, walletAddress, country } = req.body; + + const existingUser = await prisma.user.findUnique({ where: { email } }); + if (existingUser) { + throw new ConflictError("Email already registered"); + } + + if (walletAddress) { + const existingWallet = await prisma.user.findUnique({ where: { walletAddress } }); + if (existingWallet) { + throw new ConflictError("Wallet address already registered"); + } + } + + const hashedPassword = await bcrypt.hash(password, 12); + + const user = await prisma.user.create({ + data: { + fullname, + email, + password: hashedPassword, + role, + ...(walletAddress != null && { walletAddress }), + ...(country != null && { country }), + }, + select: { + id: true, + fullname: true, + email: true, + role: true, + walletAddress: true, + country: true, + isVerified: true, + createdAt: true, + }, + }); + + const tokens = generateTokens({ + userId: user.id, + email: user.email, + role: user.role, + }); + + setRefreshTokenCookie(res, tokens.refreshToken); + + res.status(201).json({ + success: true, + data: { + user, + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + }, + }); +} + +export async function login(req: Request, res: Response): Promise { + const { email, password } = req.body; + + const user = await prisma.user.findUnique({ where: { email } }); + if (!user || !user.password) { + throw new UnauthorizedError("Invalid email or password"); + } + + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + throw new UnauthorizedError("Invalid email or password"); + } + + const tokens = generateTokens({ + userId: user.id, + email: user.email, + role: user.role, + }); + + setRefreshTokenCookie(res, tokens.refreshToken); + + res.json({ + success: true, + data: { + user: { + id: user.id, + fullname: user.fullname, + email: user.email, + role: user.role, + walletAddress: user.walletAddress, + country: user.country, + isVerified: user.isVerified, + }, + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + }, + }); +} + +export function logout(_req: Request, res: Response): void { + res.clearCookie("refreshToken", { + httpOnly: true, + secure: config.nodeEnv === "production", + sameSite: "strict", + path: "/", + }); + + res.json({ + success: true, + data: { message: "Logged out successfully" }, + }); +} + +export async function refreshToken(req: Request, res: Response): Promise { + const token = req.cookies?.refreshToken ?? req.body?.refreshToken; + + if (!token) { + throw new UnauthorizedError("No refresh token provided"); + } + + let decoded: { userId: string; email: string; role: string }; + try { + decoded = jwt.verify(token, config.jwt.refreshSecret) as { userId: string; email: string; role: string }; + } catch { + throw new UnauthorizedError("Invalid or expired refresh token"); + } + + const user = await prisma.user.findUnique({ where: { id: decoded.userId } }); + if (!user) { + throw new NotFoundError("User not found"); + } + + const tokens = generateTokens({ + userId: user.id, + email: user.email, + role: user.role, + }); + + setRefreshTokenCookie(res, tokens.refreshToken); + + res.json({ + success: true, + data: { + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + }, + }); +} + +export async function getMe(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const user = await prisma.user.findUnique({ + where: { id: authReq.user.userId }, + select: { + id: true, + fullname: true, + email: true, + role: true, + walletAddress: true, + profileImage: true, + country: true, + isVerified: true, + createdAt: true, + updatedAt: true, + }, + }); + + if (!user) { + throw new NotFoundError("User not found"); + } + + res.json({ + success: true, + data: { user }, + }); +} diff --git a/src/controllers/escrow.controller.ts b/src/controllers/escrow.controller.ts new file mode 100644 index 0000000..2dce1db --- /dev/null +++ b/src/controllers/escrow.controller.ts @@ -0,0 +1,336 @@ +import type { Request, Response } from "express"; +import { prisma } from "../config/prisma.js"; +import { NotFoundError, ForbiddenError, BadRequestError } from "../utils/errors.js"; +import type { AuthRequest } from "../middleware/auth.js"; +import * as stellarService from "../services/stellar.service.js"; + +const VALID_ESCROW_TRANSITIONS: Record = { + PENDING: ["FUNDED", "REFUNDED"], + FUNDED: ["RELEASED", "REFUNDED", "DISPUTED"], + RELEASED: [], + REFUNDED: [], + DISPUTED: [], +}; + +export async function createEscrow(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + + if (authReq.user.role !== "CLIENT" && authReq.user.role !== "ADMIN") { + throw new ForbiddenError("Only clients can create escrows"); + } + + const { invoiceId, contractId } = req.body; + + const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } }); + if (!invoice) { + throw new NotFoundError("Invoice not found"); + } + + if (invoice.recipientId !== authReq.user.userId && authReq.user.role !== "ADMIN") { + throw new ForbiddenError("You can only create escrows for invoices addressed to you"); + } + + if (invoice.status !== "PENDING" && invoice.status !== "DRAFT") { + throw new BadRequestError("Invoice must be in DRAFT or PENDING status to create escrow"); + } + + const existingEscrow = await prisma.escrow.findUnique({ where: { invoiceId } }); + if (existingEscrow) { + throw new BadRequestError("Escrow already exists for this invoice"); + } + + const stellarResult = await stellarService.createEscrowContract( + invoice.recipientId, + invoice.creatorId, + invoice.amount.toString(), + invoice.currency + ); + + if (!stellarResult.success) { + throw new BadRequestError("Failed to create escrow contract on Stellar"); + } + + const escrow = await prisma.escrow.create({ + data: { + invoiceId, + clientId: invoice.recipientId, + freelancerId: invoice.creatorId, + contractId: stellarResult.contractId ?? contractId, + amount: invoice.amount, + currency: invoice.currency, + }, + include: { + invoice: { + select: { id: true, title: true, amount: true, status: true }, + }, + client: { + select: { id: true, fullname: true, email: true }, + }, + freelancer: { + select: { id: true, fullname: true, email: true }, + }, + }, + }); + + await prisma.invoice.update({ + where: { id: invoiceId }, + data: { status: "IN_ESCROW", contractId: escrow.contractId }, + }); + + res.status(201).json({ + success: true, + data: { escrow }, + }); +} + +export async function fundEscrow(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const id = String(req.params["id"]); + const { txHash } = req.body; + + const escrow = await prisma.escrow.findUnique({ where: { id } }); + if (!escrow) { + throw new NotFoundError("Escrow not found"); + } + + if (escrow.clientId !== authReq.user.userId && authReq.user.role !== "ADMIN") { + throw new ForbiddenError("Only the client can fund this escrow"); + } + + const allowedTransitions = VALID_ESCROW_TRANSITIONS[escrow.status] ?? []; + if (!allowedTransitions.includes("FUNDED")) { + throw new BadRequestError(`Cannot fund escrow in ${escrow.status} status`); + } + + const stellarResult = await stellarService.fundEscrow( + escrow.contractId, + escrow.amount.toString(), + escrow.currency, + txHash + ); + + if (!stellarResult.success) { + throw new BadRequestError("Failed to fund escrow on Stellar"); + } + + const updatedEscrow = await prisma.escrow.update({ + where: { id }, + data: { + status: "FUNDED", + fundedAt: new Date(), + txHash, + }, + include: { + invoice: { + select: { id: true, title: true, amount: true, status: true }, + }, + client: { + select: { id: true, fullname: true, email: true }, + }, + freelancer: { + select: { id: true, fullname: true, email: true }, + }, + }, + }); + + await prisma.payment.create({ + data: { + userId: escrow.clientId, + escrowId: escrow.id, + amount: escrow.amount, + currency: escrow.currency, + txHash, + status: "SUCCESS", + description: "Funded escrow for invoice", + }, + }); + + await prisma.invoice.update({ + where: { id: escrow.invoiceId }, + data: { status: "FUNDED", txHash }, + }); + + res.json({ + success: true, + data: { escrow: updatedEscrow }, + }); +} + +export async function releaseEscrow(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const id = String(req.params["id"]); + const { txHash } = req.body; + + const escrow = await prisma.escrow.findUnique({ where: { id } }); + if (!escrow) { + throw new NotFoundError("Escrow not found"); + } + + if (authReq.user.role !== "ADMIN") { + if (escrow.freelancerId !== authReq.user.userId) { + throw new ForbiddenError("Only the freelancer or admin can release escrow"); + } + } + + const allowedTransitions = VALID_ESCROW_TRANSITIONS[escrow.status] ?? []; + if (!allowedTransitions.includes("RELEASED")) { + throw new BadRequestError(`Cannot release escrow in ${escrow.status} status`); + } + + const stellarResult = await stellarService.releaseEscrowFunds(escrow.contractId, txHash); + + if (!stellarResult.success) { + throw new BadRequestError("Failed to release escrow funds on Stellar"); + } + + const updatedEscrow = await prisma.escrow.update({ + where: { id }, + data: { + status: "RELEASED", + releasedAt: new Date(), + txHash, + }, + include: { + invoice: { + select: { id: true, title: true, amount: true, status: true }, + }, + client: { + select: { id: true, fullname: true, email: true }, + }, + freelancer: { + select: { id: true, fullname: true, email: true }, + }, + }, + }); + + await prisma.payment.create({ + data: { + userId: escrow.freelancerId, + escrowId: escrow.id, + amount: escrow.amount, + currency: escrow.currency, + txHash, + status: "SUCCESS", + description: "Released escrow payment", + }, + }); + + await prisma.invoice.update({ + where: { id: escrow.invoiceId }, + data: { status: "COMPLETED", paidAt: new Date(), txHash }, + }); + + res.json({ + success: true, + data: { escrow: updatedEscrow }, + }); +} + +export async function refundEscrow(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const id = String(req.params["id"]); + const { txHash } = req.body; + + const escrow = await prisma.escrow.findUnique({ where: { id } }); + if (!escrow) { + throw new NotFoundError("Escrow not found"); + } + + if (authReq.user.role !== "ADMIN") { + throw new ForbiddenError("Only admins can refund escrow"); + } + + const allowedTransitions = VALID_ESCROW_TRANSITIONS[escrow.status] ?? []; + if (!allowedTransitions.includes("REFUNDED")) { + throw new BadRequestError(`Cannot refund escrow in ${escrow.status} status`); + } + + const stellarResult = await stellarService.refundEscrowFunds(escrow.contractId, txHash); + + if (!stellarResult.success) { + throw new BadRequestError("Failed to refund escrow on Stellar"); + } + + const updatedEscrow = await prisma.escrow.update({ + where: { id }, + data: { + status: "REFUNDED", + refundedAt: new Date(), + txHash, + }, + include: { + invoice: { + select: { id: true, title: true, amount: true, status: true }, + }, + client: { + select: { id: true, fullname: true, email: true }, + }, + freelancer: { + select: { id: true, fullname: true, email: true }, + }, + }, + }); + + await prisma.payment.create({ + data: { + userId: escrow.clientId, + escrowId: escrow.id, + amount: escrow.amount, + currency: escrow.currency, + txHash, + status: "SUCCESS", + description: "Refunded escrow payment", + }, + }); + + await prisma.invoice.update({ + where: { id: escrow.invoiceId }, + data: { status: "CANCELLED", txHash }, + }); + + res.json({ + success: true, + data: { escrow: updatedEscrow }, + }); +} + +export async function getEscrow(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const id = String(req.params["id"]); + + const escrow = await prisma.escrow.findUnique({ + where: { id }, + include: { + invoice: { + select: { id: true, title: true, amount: true, status: true }, + }, + client: { + select: { id: true, fullname: true, email: true }, + }, + freelancer: { + select: { id: true, fullname: true, email: true }, + }, + payments: { + orderBy: { createdAt: "desc" }, + take: 5, + }, + }, + }); + + if (!escrow) { + throw new NotFoundError("Escrow not found"); + } + + if ( + escrow.clientId !== authReq.user.userId && + escrow.freelancerId !== authReq.user.userId && + authReq.user.role !== "ADMIN" + ) { + throw new ForbiddenError("You don't have access to this escrow"); + } + + res.json({ + success: true, + data: { escrow }, + }); +} diff --git a/src/controllers/invoice.controller.ts b/src/controllers/invoice.controller.ts index b71aec5..92fed04 100644 --- a/src/controllers/invoice.controller.ts +++ b/src/controllers/invoice.controller.ts @@ -1,3 +1,12 @@ +import type { Request, Response } from "express"; +import { prisma } from "../config/prisma.js"; +import { NotFoundError, ForbiddenError, BadRequestError } from "../utils/errors.js"; +import type { AuthRequest } from "../middleware/auth.js"; +import { getPaginationParams, getPaginationMeta, getSkipTake } from "../utils/pagination.js"; + +const VALID_STATUS_TRANSITIONS: Record = { + DRAFT: ["PENDING", "CANCELLED"], + PENDING: ["FUNDED", "CANCELLED", "DISPUTED"], import type { Request, Response, NextFunction } from "express"; import { prisma } from "../config/prisma.js"; import { NotFoundError, ForbiddenError, ConflictError } from "../utils/AppError.js"; @@ -13,189 +22,210 @@ const VALID_TRANSITIONS: Record = { DISPUTED: [], }; -export async function createInvoice(req: Request, res: Response, next: NextFunction): Promise { - try { - const userId = req.user!.userId; - const data = req.body as CreateInvoiceInput; +export async function createInvoice(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; - const recipient = await prisma.user.findUnique({ where: { id: data.recipientId } }); - if (!recipient) { - throw new NotFoundError("Recipient"); - } + if (authReq.user.role !== "FREELANCER" && authReq.user.role !== "ADMIN") { + throw new ForbiddenError("Only freelancers can create invoices"); + } + + const { recipientId, title, description, amount, currency, dueDate } = req.body; - const invoice = await prisma.invoice.create({ - data: { - creatorId: userId, - recipientId: data.recipientId, - title: data.title, - description: data.description, - amount: data.amount, - currency: data.currency, - dueDate: data.dueDate ? new Date(data.dueDate) : null, - lineItems: data.lineItems ?? [], - status: "DRAFT", + const recipient = await prisma.user.findUnique({ where: { id: recipientId } }); + if (!recipient) { + throw new NotFoundError("Recipient not found"); + } + + if (recipientId === authReq.user.userId) { + throw new BadRequestError("Cannot create invoice for yourself"); + } + + const invoice = await prisma.invoice.create({ + data: { + creatorId: authReq.user.userId, + recipientId, + title, + description, + amount, + currency: currency ?? "USDC", + dueDate: dueDate ? new Date(dueDate) : null, + }, + include: { + creator: { + select: { id: true, fullname: true, email: true }, }, - include: { - creator: { select: { id: true, fullname: true, email: true, walletAddress: true } }, - recipient: { select: { id: true, fullname: true, email: true, walletAddress: true } }, + recipient: { + select: { id: true, fullname: true, email: true }, }, - }); + }, + }); - res.status(201).json({ success: true, data: invoice }); - } catch (error) { - next(error); - } + res.status(201).json({ + success: true, + data: { invoice }, + }); } -export async function getInvoices(req: Request, res: Response, next: NextFunction): Promise { - try { - const userId = req.user!.userId; - const userRole = req.user!.role; - const { page, limit, status, search, sortBy, sortOrder } = req.query as unknown as InvoiceQueryInput; +export async function getInvoices(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const { page: rawPage, limit: rawLimit, status, search, creatorId, recipientId } = req.query; - const where: Record = {}; + const pagination = getPaginationParams({ + page: String(rawPage ?? ""), + limit: String(rawLimit ?? ""), + }); + const { skip, take } = getSkipTake(pagination.page, pagination.limit); - if (userRole === "FREELANCER") { - where.recipientId = userId; - } else if (userRole === "CLIENT") { - where.creatorId = userId; - } + const where: Record = {}; - if (status) { - where.status = status; - } + if (authReq.user.role === "FREELANCER") { + where.creatorId = authReq.user.userId; + } else if (authReq.user.role === "CLIENT") { + where.recipientId = authReq.user.userId; + } - if (search) { - where.OR = [ - { title: { contains: search, mode: "insensitive" } }, - { description: { contains: search, mode: "insensitive" } }, - ]; - } + if (creatorId) where.creatorId = String(creatorId); + if (recipientId) where.recipientId = String(recipientId); + if (status) where.status = String(status); - const [invoices, total] = await Promise.all([ - prisma.invoice.findMany({ - where, - include: { - creator: { select: { id: true, fullname: true, email: true, walletAddress: true } }, - recipient: { select: { id: true, fullname: true, email: true, walletAddress: true } }, + if (search) { + where.OR = [ + { title: { contains: String(search), mode: "insensitive" } }, + { description: { contains: String(search), mode: "insensitive" } }, + ]; + } + + const [invoices, total] = await Promise.all([ + prisma.invoice.findMany({ + where, + skip, + take, + include: { + creator: { + select: { id: true, fullname: true, email: true }, + }, + recipient: { + select: { id: true, fullname: true, email: true }, + }, + escrow: { + select: { id: true, status: true, amount: true }, }, - orderBy: { [sortBy]: sortOrder }, - skip: (page - 1) * limit, - take: limit, - }), - prisma.invoice.count({ where }), - ]); - - res.json({ - success: true, - data: invoices, - pagination: { - page, - limit, - total, - totalPages: Math.ceil(total / limit), }, - }); - } catch (error) { - next(error); - } + orderBy: { createdAt: "desc" }, + }), + prisma.invoice.count({ where }), + ]); + + res.json({ + success: true, + data: { + invoices, + pagination: getPaginationMeta(pagination.page, pagination.limit, total), + }, + }); } -export async function getInvoiceById(req: Request, res: Response, next: NextFunction): Promise { - try { - const id = req.params["id"] as string; - const userId = req.user!.userId; +export async function getInvoice(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const id = String(req.params["id"]); - const invoice = await prisma.invoice.findUnique({ - where: { id }, - include: { - creator: { select: { id: true, fullname: true, email: true, walletAddress: true } }, - recipient: { select: { id: true, fullname: true, email: true, walletAddress: true } }, - escrow: true, + const invoice = await prisma.invoice.findUnique({ + where: { id }, + include: { + creator: { + select: { id: true, fullname: true, email: true }, }, - }); - - if (!invoice) { - throw new NotFoundError("Invoice"); - } + recipient: { + select: { id: true, fullname: true, email: true }, + }, + escrow: true, + }, + }); - if (invoice.creatorId !== userId && invoice.recipientId !== userId) { - throw new ForbiddenError("You do not have access to this invoice"); - } + if (!invoice) { + throw new NotFoundError("Invoice not found"); + } - res.json({ success: true, data: invoice }); - } catch (error) { - next(error); + if (invoice.creatorId !== authReq.user.userId && invoice.recipientId !== authReq.user.userId) { + throw new ForbiddenError("You don't have access to this invoice"); } + + res.json({ + success: true, + data: { invoice }, + }); } -export async function updateInvoice(req: Request, res: Response, next: NextFunction): Promise { - try { - const id = req.params["id"] as string; - const userId = req.user!.userId; - const data = req.body as UpdateInvoiceInput; +export async function updateInvoice(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const id = String(req.params["id"]); + const { title, description, amount, status, dueDate } = req.body; - const invoice = await prisma.invoice.findUnique({ where: { id } }); - if (!invoice) { - throw new NotFoundError("Invoice"); - } + const invoice = await prisma.invoice.findUnique({ where: { id } }); + if (!invoice) { + throw new NotFoundError("Invoice not found"); + } - if (invoice.creatorId !== userId) { - throw new ForbiddenError("Only the invoice creator can update this invoice"); - } + if (invoice.creatorId !== authReq.user.userId) { + throw new ForbiddenError("Only the invoice creator can update it"); + } - if (data.status && invoice.status !== data.status) { - const allowed = VALID_TRANSITIONS[invoice.status] ?? []; - if (!allowed.includes(data.status)) { - throw new ConflictError(`Cannot transition from ${invoice.status} to ${data.status}`); - } + if (status && status !== invoice.status) { + const allowedTransitions = VALID_STATUS_TRANSITIONS[invoice.status] ?? []; + if (!allowedTransitions.includes(status)) { + throw new BadRequestError( + `Cannot transition from ${invoice.status} to ${status}` + ); } + } - const updated = await prisma.invoice.update({ - where: { id }, - data: { - ...(data.title && { title: data.title }), - ...(data.description && { description: data.description }), - ...(data.amount && { amount: data.amount }), - ...(data.currency && { currency: data.currency }), - ...(data.dueDate && { dueDate: new Date(data.dueDate) }), - ...(data.lineItems && { lineItems: data.lineItems }), - ...(data.status && { status: data.status }), + const updatedInvoice = await prisma.invoice.update({ + where: { id }, + data: { + ...(title != null && { title }), + ...(description != null && { description }), + ...(amount != null && { amount }), + ...(status != null && { status }), + ...(dueDate != null && { dueDate: new Date(dueDate) }), + }, + include: { + creator: { + select: { id: true, fullname: true, email: true }, }, - include: { - creator: { select: { id: true, fullname: true, email: true, walletAddress: true } }, - recipient: { select: { id: true, fullname: true, email: true, walletAddress: true } }, + recipient: { + select: { id: true, fullname: true, email: true }, }, - }); - - res.json({ success: true, data: updated }); - } catch (error) { - next(error); - } + escrow: true, + }, + }); + + res.json({ + success: true, + data: { invoice: updatedInvoice }, + }); } -export async function deleteInvoice(req: Request, res: Response, next: NextFunction): Promise { - try { - const id = req.params["id"] as string; - const userId = req.user!.userId; +export async function deleteInvoice(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const id = String(req.params["id"]); - const invoice = await prisma.invoice.findUnique({ where: { id } }); - if (!invoice) { - throw new NotFoundError("Invoice"); - } + const invoice = await prisma.invoice.findUnique({ where: { id } }); + if (!invoice) { + throw new NotFoundError("Invoice not found"); + } - if (invoice.creatorId !== userId) { - throw new ForbiddenError("Only the invoice creator can delete this invoice"); - } + if (invoice.creatorId !== authReq.user.userId) { + throw new ForbiddenError("Only the invoice creator can delete it"); + } - if (invoice.status !== "DRAFT") { - throw new ConflictError("Only draft invoices can be deleted"); - } + if (invoice.status !== "DRAFT") { + throw new BadRequestError("Only draft invoices can be deleted"); + } - await prisma.invoice.delete({ where: { id } }); + await prisma.invoice.delete({ where: { id } }); - res.json({ success: true, message: "Invoice deleted" }); - } catch (error) { - next(error); - } + res.json({ + success: true, + data: { message: "Invoice deleted successfully" }, + }); } diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts new file mode 100644 index 0000000..2bb31e8 --- /dev/null +++ b/src/controllers/user.controller.ts @@ -0,0 +1,154 @@ +import type { Request, Response } from "express"; +import { prisma } from "../config/prisma.js"; +import { NotFoundError, ForbiddenError } from "../utils/errors.js"; +import type { AuthRequest } from "../middleware/auth.js"; + +function calculateProfileCompleteness(user: { + fullname: string; + email: string; + profileImage: string | null; + country: string | null; + walletAddress: string | null; +}): number { + const fields = [user.fullname, user.email, user.profileImage, user.country, user.walletAddress]; + const filled = fields.filter((f) => f !== null && f !== "").length; + return Math.round((filled / fields.length) * 100); +} + +export async function getProfile(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const user = await prisma.user.findUnique({ + where: { id: authReq.user.userId }, + select: { + id: true, + fullname: true, + email: true, + role: true, + walletAddress: true, + profileImage: true, + country: true, + isVerified: true, + createdAt: true, + updatedAt: true, + }, + }); + + if (!user) { + throw new NotFoundError("User not found"); + } + + const completeness = calculateProfileCompleteness(user); + + res.json({ + success: true, + data: { + user, + profileCompleteness: completeness, + }, + }); +} + +export async function updateProfile(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const { fullname, email, country, walletAddress, profileImage } = req.body; + + if (email) { + const existingUser = await prisma.user.findFirst({ + where: { + email, + id: { not: authReq.user.userId }, + }, + }); + if (existingUser) { + throw new ForbiddenError("Email already in use"); + } + } + + if (walletAddress) { + const existingWallet = await prisma.user.findFirst({ + where: { + walletAddress, + id: { not: authReq.user.userId }, + }, + }); + if (existingWallet) { + throw new ForbiddenError("Wallet address already in use"); + } + } + + const user = await prisma.user.update({ + where: { id: authReq.user.userId }, + data: { + ...(fullname != null && { fullname }), + ...(email != null && { email }), + ...(country != null && { country }), + ...(walletAddress != null && { walletAddress }), + ...(profileImage != null && { profileImage }), + }, + select: { + id: true, + fullname: true, + email: true, + role: true, + walletAddress: true, + profileImage: true, + country: true, + isVerified: true, + createdAt: true, + updatedAt: true, + }, + }); + + const completeness = calculateProfileCompleteness(user); + + res.json({ + success: true, + data: { + user, + profileCompleteness: completeness, + }, + }); +} + +export async function deleteProfile(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + + const user = await prisma.user.findUnique({ where: { id: authReq.user.userId } }); + if (!user) { + throw new NotFoundError("User not found"); + } + + await prisma.user.delete({ where: { id: authReq.user.userId } }); + + res.json({ + success: true, + data: { message: "Profile deleted successfully" }, + }); +} + +export async function uploadAvatar(req: Request, res: Response): Promise { + const authReq = req as AuthRequest; + const { profileImage } = req.body; + + const user = await prisma.user.update({ + where: { id: authReq.user.userId }, + data: { profileImage }, + select: { + id: true, + fullname: true, + email: true, + role: true, + walletAddress: true, + profileImage: true, + country: true, + isVerified: true, + createdAt: true, + updatedAt: true, + }, + }); + + res.json({ + success: true, + data: { user }, + }); +} diff --git a/src/index.ts b/src/index.ts index 7e0a8b0..d1fb554 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,17 +4,21 @@ import helmet from "helmet"; import cors from "cors"; import morgan from "morgan"; import rateLimit from "express-rate-limit"; +import cookieParser from "cookie-parser"; +import authRoutes from "./routes/auth.routes.js"; +import userRoutes from "./routes/user.routes.js"; import invoiceRoutes from "./routes/invoice.routes.js"; -import uploadRoutes from "./routes/upload.routes.js"; +import escrowRoutes from "./routes/escrow.routes.js"; import { errorHandler } from "./middleware/errorHandler.js"; const app = express(); const PORT = process.env["PORT"] ?? 3001; app.use(helmet()); -app.use(cors({ origin: process.env["CORS_ORIGIN"] ?? "http://localhost:3000" })); +app.use(cors({ origin: process.env["CORS_ORIGIN"] ?? "http://localhost:3000", credentials: true })); app.use(morgan("dev")); app.use(express.json({ limit: "10mb" })); +app.use(cookieParser()); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, @@ -28,8 +32,10 @@ app.get("/health", (_req, res) => { res.json({ status: "ok", timestamp: new Date().toISOString() }); }); +app.use("/api/auth", authRoutes); +app.use("/api/users", userRoutes); app.use("/api/invoices", invoiceRoutes); -app.use("/api/uploads", uploadRoutes); +app.use("/api/escrows", escrowRoutes); app.use(errorHandler); diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts index b98a54c..8477b5d 100644 --- a/src/middleware/errorHandler.ts +++ b/src/middleware/errorHandler.ts @@ -21,9 +21,9 @@ export function errorHandler(err: Error, _req: Request, res: Response, _next: Ne return; } - console.error("Unexpected error:", err); + console.error("Unhandled error:", err); res.status(500).json({ success: false, - error: { message: "Internal server error" }, + error: "Internal server error", }); } diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts new file mode 100644 index 0000000..bdff257 --- /dev/null +++ b/src/routes/auth.routes.ts @@ -0,0 +1,15 @@ +import { Router } from "express"; +import { register, login, logout, refreshToken, getMe } from "../controllers/auth.controller.js"; +import { validate } from "../middleware/validate.js"; +import { authenticate } from "../middleware/auth.js"; +import { registerSchema, loginSchema } from "../schemas/auth.schema.js"; + +const router = Router(); + +router.post("/register", validate(registerSchema), register); +router.post("/login", validate(loginSchema), login); +router.post("/logout", logout); +router.post("/refresh-token", refreshToken); +router.get("/me", authenticate, getMe); + +export default router; diff --git a/src/routes/escrow.routes.ts b/src/routes/escrow.routes.ts new file mode 100644 index 0000000..af12595 --- /dev/null +++ b/src/routes/escrow.routes.ts @@ -0,0 +1,28 @@ +import { Router } from "express"; +import { + createEscrow, + fundEscrow, + releaseEscrow, + refundEscrow, + getEscrow, +} from "../controllers/escrow.controller.js"; +import { validate } from "../middleware/validate.js"; +import { authenticate } from "../middleware/auth.js"; +import { + createEscrowSchema, + fundEscrowSchema, + releaseEscrowSchema, + refundEscrowSchema, +} from "../schemas/escrow.schema.js"; + +const router = Router(); + +router.use(authenticate); + +router.post("/", validate(createEscrowSchema), createEscrow); +router.get("/:id", getEscrow); +router.post("/:id/fund", validate(fundEscrowSchema), fundEscrow); +router.post("/:id/release", validate(releaseEscrowSchema), releaseEscrow); +router.post("/:id/refund", validate(refundEscrowSchema), refundEscrow); + +export default router; diff --git a/src/routes/user.routes.ts b/src/routes/user.routes.ts new file mode 100644 index 0000000..72820eb --- /dev/null +++ b/src/routes/user.routes.ts @@ -0,0 +1,16 @@ +import { Router } from "express"; +import { getProfile, updateProfile, deleteProfile, uploadAvatar } from "../controllers/user.controller.js"; +import { validate } from "../middleware/validate.js"; +import { authenticate } from "../middleware/auth.js"; +import { updateProfileSchema, avatarUploadSchema } from "../schemas/user.schema.js"; + +const router = Router(); + +router.use(authenticate); + +router.get("/profile", getProfile); +router.put("/profile", validate(updateProfileSchema), updateProfile); +router.delete("/profile", deleteProfile); +router.post("/avatar", validate(avatarUploadSchema), uploadAvatar); + +export default router; diff --git a/src/schemas/auth.schema.ts b/src/schemas/auth.schema.ts new file mode 100644 index 0000000..f5143f7 --- /dev/null +++ b/src/schemas/auth.schema.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +export const registerSchema = z.object({ + fullname: z.string().min(2, "Name must be at least 2 characters"), + email: z.string().email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + role: z.enum(["FREELANCER", "CLIENT"]), + walletAddress: z.string().optional(), + country: z.string().optional(), +}); + +export const loginSchema = z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(1, "Password is required"), +}); + +export const refreshTokenSchema = z.object({ + refreshToken: z.string().min(1, "Refresh token is required"), +}); + +export type RegisterInput = z.infer; +export type LoginInput = z.infer; +export type RefreshTokenInput = z.infer; diff --git a/src/schemas/escrow.schema.ts b/src/schemas/escrow.schema.ts new file mode 100644 index 0000000..3193854 --- /dev/null +++ b/src/schemas/escrow.schema.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +export const createEscrowSchema = z.object({ + invoiceId: z.string().min(1, "Invoice ID is required"), + contractId: z.string().min(1, "Contract ID is required"), +}); + +export const fundEscrowSchema = z.object({ + txHash: z.string().min(1, "Transaction hash is required"), +}); + +export const releaseEscrowSchema = z.object({ + txHash: z.string().min(1, "Transaction hash is required"), +}); + +export const refundEscrowSchema = z.object({ + txHash: z.string().min(1, "Transaction hash is required"), +}); + +export const getEscrowSchema = z.object({ + id: z.string().min(1, "Escrow ID is required"), +}); + +export type CreateEscrowInput = z.infer; +export type FundEscrowInput = z.infer; +export type ReleaseEscrowInput = z.infer; +export type RefundEscrowInput = z.infer; diff --git a/src/schemas/invoice.schema.ts b/src/schemas/invoice.schema.ts new file mode 100644 index 0000000..815eb0d --- /dev/null +++ b/src/schemas/invoice.schema.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +export const createInvoiceSchema = z.object({ + recipientId: z.string().min(1, "Recipient ID is required"), + title: z.string().min(1, "Title is required"), + description: z.string().min(1, "Description is required"), + amount: z.number().positive("Amount must be positive"), + currency: z.string().default("USDC"), + dueDate: z.string().datetime().optional(), +}); + +export const updateInvoiceSchema = z.object({ + title: z.string().min(1).optional(), + description: z.string().min(1).optional(), + amount: z.number().positive().optional(), + status: z.enum(["DRAFT", "PENDING", "FUNDED", "IN_ESCROW", "COMPLETED", "CANCELLED", "DISPUTED"]).optional(), + dueDate: z.string().datetime().optional(), +}); + +export const listInvoicesSchema = z.object({ + page: z.string().optional(), + limit: z.string().optional(), + status: z.enum(["DRAFT", "PENDING", "FUNDED", "IN_ESCROW", "COMPLETED", "CANCELLED", "DISPUTED"]).optional(), + search: z.string().optional(), + creatorId: z.string().optional(), + recipientId: z.string().optional(), +}); + +export type CreateInvoiceInput = z.infer; +export type UpdateInvoiceInput = z.infer; +export type ListInvoicesInput = z.infer; diff --git a/src/schemas/user.schema.ts b/src/schemas/user.schema.ts new file mode 100644 index 0000000..b6c9a40 --- /dev/null +++ b/src/schemas/user.schema.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export const updateProfileSchema = z.object({ + fullname: z.string().min(2).optional(), + email: z.string().email().optional(), + country: z.string().optional(), + walletAddress: z.string().optional(), + profileImage: z.string().url().optional(), +}); + +export const avatarUploadSchema = z.object({ + profileImage: z.string().url("Invalid image URL"), +}); + +export type UpdateProfileInput = z.infer; +export type AvatarUploadInput = z.infer; diff --git a/src/services/stellar.service.ts b/src/services/stellar.service.ts new file mode 100644 index 0000000..2aa1394 --- /dev/null +++ b/src/services/stellar.service.ts @@ -0,0 +1,78 @@ +import { config } from "../config/index.js"; + +export interface StellarTransactionResult { + success: boolean; + txHash: string; + contractId?: string; +} + +export async function createEscrowContract( + clientId: string, + freelancerId: string, + amount: string, + currency: string +): Promise { + console.log("Stellar: Creating escrow contract", { + clientId, + freelancerId, + amount, + currency, + network: config.stellar.network, + horizonUrl: config.stellar.horizonUrl, + }); + + return { + success: true, + txHash: `tx_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + contractId: `contract_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + }; +} + +export async function fundEscrow( + contractId: string, + amount: string, + currency: string, + txHash: string +): Promise { + console.log("Stellar: Funding escrow", { + contractId, + amount, + currency, + txHash, + }); + + return { + success: true, + txHash, + }; +} + +export async function releaseEscrowFunds( + contractId: string, + txHash: string +): Promise { + console.log("Stellar: Releasing escrow funds", { + contractId, + txHash, + }); + + return { + success: true, + txHash, + }; +} + +export async function refundEscrowFunds( + contractId: string, + txHash: string +): Promise { + console.log("Stellar: Refunding escrow", { + contractId, + txHash, + }); + + return { + success: true, + txHash, + }; +} diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..1478d12 --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,41 @@ +export class AppError extends Error { + public readonly statusCode: number; + public readonly isOperational: boolean; + + constructor(message: string, statusCode: number, isOperational = true) { + super(message); + this.statusCode = statusCode; + this.isOperational = isOperational; + Object.setPrototypeOf(this, AppError.prototype); + } +} + +export class NotFoundError extends AppError { + constructor(message = "Resource not found") { + super(message, 404); + } +} + +export class UnauthorizedError extends AppError { + constructor(message = "Unauthorized") { + super(message, 401); + } +} + +export class ForbiddenError extends AppError { + constructor(message = "Forbidden") { + super(message, 403); + } +} + +export class BadRequestError extends AppError { + constructor(message = "Bad request") { + super(message, 400); + } +} + +export class ConflictError extends AppError { + constructor(message = "Resource already exists") { + super(message, 409); + } +} diff --git a/src/utils/pagination.ts b/src/utils/pagination.ts new file mode 100644 index 0000000..8310a58 --- /dev/null +++ b/src/utils/pagination.ts @@ -0,0 +1,35 @@ +export interface PaginationParams { + page: number; + limit: number; +} + +export interface PaginationMeta { + page: number; + limit: number; + total: number; + totalPages: number; +} + +export interface PaginatedResult { + data: T[]; + pagination: PaginationMeta; +} + +export function getPaginationParams(query: { page?: string; limit?: string }): PaginationParams { + const page = Math.max(1, parseInt(query.page ?? "1", 10) || 1); + const limit = Math.min(100, Math.max(1, parseInt(query.limit ?? "20", 10) || 20)); + return { page, limit }; +} + +export function getPaginationMeta(page: number, limit: number, total: number): PaginationMeta { + return { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }; +} + +export function getSkipTake(page: number, limit: number): { skip: number; take: number } { + return { skip: (page - 1) * limit, take: limit }; +}