diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index 66e1ed6a..90b258fa 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -1208,6 +1208,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -3273,6 +3274,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz", "integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==", "license": "MIT", + "peer": true, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "axios": "^1.3.1", @@ -3499,6 +3501,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", "integrity": "sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.3.2", "iterare": "1.2.1", @@ -3558,6 +3561,7 @@ "integrity": "sha512-lD5mAYekTTurF3vDaa8C2OKPnjiz4tsfxIc5XlcSUzOhkwWf6Ay3HKvt6FmvuWQam6uIIHX52Clg+e6tAvf/cg==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3641,6 +3645,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.17.tgz", "integrity": "sha512-mAf4eOsSBsTOn/VbrUO1gsjW6dVh91qqXPMXun4dN8SnNjf7PTQagM9o8d6ab8ZBpNe6UdZftdrZoDetU+n4Qg==", "license": "MIT", + "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -3662,6 +3667,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.17.tgz", "integrity": "sha512-BSOAsENdmTtsnDL0hb4takbWzPy9WoPybjlM57ab3/rQgm0biMFYUupH2uzmCjmmIXJL/EFbAWznVl8xw2Sa6Q==", "license": "MIT", + "peer": true, "dependencies": { "socket.io": "4.8.3", "tslib": "2.8.1" @@ -3878,6 +3884,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", "integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==", "license": "MIT", + "peer": true, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", @@ -3891,6 +3898,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.17.tgz", "integrity": "sha512-YbwQ0QfVj0lxkKQhdIIgk14ZSVWDqGk1J8nNSN6SLjf36sVv58Ma5ro+dtQua8wj3l2Ub7JJCVFixEhKtYc/rQ==", "license": "MIT", + "peer": true, "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -5235,6 +5243,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5514,6 +5523,7 @@ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", @@ -5945,6 +5955,7 @@ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5993,6 +6004,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6332,6 +6344,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -6761,6 +6774,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6839,6 +6853,7 @@ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.71.1.tgz", "integrity": "sha512-kOBfdcsHmO6wwmIjpersoVdYQ7jkjTgky4Yop0loc7QwSdgxliSzD69U9ijZuRrkyCJwz5p5eqxeGeQkJ0YGZQ==", "license": "MIT", + "peer": true, "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", @@ -6874,6 +6889,7 @@ "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.8.tgz", "integrity": "sha512-0HDaDLBBY/maa/LmUVAr70XUOwsiQD+jyzCBjmUErYZUKdMS9dT59PqW59PpVqfGM7ve6H0J6307JTpkCYefHQ==", "license": "MIT", + "peer": true, "dependencies": { "@cacheable/utils": "^2.3.3", "keyv": "^5.5.5" @@ -6913,6 +6929,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -7185,13 +7202,15 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/class-validator": { "version": "0.14.4", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", "license": "MIT", + "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -8079,14 +8098,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", + "integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -8189,12 +8208,11 @@ } }, "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -8286,6 +8304,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8346,6 +8365,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9570,7 +9590,6 @@ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", "license": "ISC", - "peer": true, "engines": { "node": ">=4" } @@ -9581,7 +9600,6 @@ "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "deprecated": "this library is no longer supported", "license": "MIT", - "peer": true, "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" @@ -10573,6 +10591,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -11390,6 +11409,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -11461,9 +11481,9 @@ } }, "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "dev": true, "license": "MIT", "engines": { @@ -12143,7 +12163,6 @@ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "license": "Apache-2.0", - "peer": true, "engines": { "node": "*" } @@ -12420,6 +12439,7 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", + "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -12559,6 +12579,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -12825,6 +12846,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13285,6 +13307,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -13400,7 +13423,6 @@ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "license": "Apache-2.0", - "peer": true, "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -13447,7 +13469,6 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "license": "MIT", - "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -13462,7 +13483,6 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", "license": "MIT", - "peer": true, "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -13478,7 +13498,6 @@ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "license": "MIT", - "peer": true, "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -13494,7 +13513,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -13504,7 +13522,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -13517,7 +13534,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.6" } @@ -13527,7 +13543,6 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -13540,9 +13555,8 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "license": "MIT", - "peer": true, "bin": { "uuid": "bin/uuid" } @@ -14729,9 +14743,9 @@ } }, "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -14771,9 +14785,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", - "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==", "dev": true, "license": "MIT", "dependencies": { @@ -14793,12 +14807,39 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, "@swc/core": { "optional": true }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, "esbuild": { "optional": true }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, "uglify-js": { "optional": true } @@ -14810,6 +14851,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15212,6 +15254,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15462,6 +15505,7 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", "license": "MIT", + "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -15622,6 +15666,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15914,14 +15959,13 @@ } }, "node_modules/webpack": { - "version": "5.105.4", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", - "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "version": "5.107.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", + "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", @@ -15931,21 +15975,20 @@ "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.20.0", - "es-module-lexer": "^2.0.0", + "enhanced-resolve": "^5.22.0", + "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.17", + "terser-webpack-plugin": "^5.5.0", "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" + "webpack-sources": "^3.5.0" }, "bin": { "webpack": "bin/webpack.js" @@ -15974,9 +16017,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", "dev": true, "license": "MIT", "engines": { @@ -16003,7 +16046,6 @@ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -16012,9 +16054,9 @@ } }, "node_modules/webpack/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "peer": true, @@ -16035,7 +16077,6 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -16049,7 +16090,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -16064,7 +16104,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -16074,33 +16113,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/webpack/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } + "license": "MIT" }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -16108,7 +16121,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index 1893574c..6d9fe017 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { JwtModule, JwtModuleOptions } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -16,7 +16,7 @@ import { RefreshToken } from './entities/refresh-token.entity'; @Module({ imports: [ TypeOrmModule.forFeature([User, PasswordResetToken, RefreshToken]), - UsersModule, + forwardRef(() => UsersModule), EmailModule, PassportModule, JwtModule.registerAsync({ diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index f7ec87e7..ce9e2b98 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -193,6 +193,64 @@ export class AuthService { }; } + /** + * Verify the signed challenge without creating a user session or issuing JWT. + * Used for secure account linking. + */ + async verifyChallengeOnly( + publicKey: string, + signedChallenge: string, + ): Promise { + await Promise.resolve(); + const storedChallenge = this.challengeStore.get(publicKey); + + if (!storedChallenge) { + throw new BadRequestException( + 'No challenge found for this public key. Please request a new challenge.', + ); + } + + if (Date.now() > storedChallenge.expiresAt) { + this.challengeStore.delete(publicKey); + throw new BadRequestException( + 'Challenge has expired. Please request a new challenge.', + ); + } + + const networkPassphrase = + this.stellarNetwork === 'testnet' ? Networks.TESTNET : Networks.PUBLIC; + + let transaction: Transaction; + + try { + transaction = new Transaction(signedChallenge, networkPassphrase); + } catch { + this.challengeStore.delete(publicKey); + throw new BadRequestException('Invalid transaction format'); + } + + // Verify the transaction was signed by the user + const userSignature = transaction.signatures.find((sig) => { + try { + const keypair = Keypair.fromPublicKey(publicKey); + return keypair.verify(transaction.hash(), sig.signature()); + } catch { + return false; + } + }); + + if (!userSignature) { + this.challengeStore.delete(publicKey); + throw new BadRequestException( + 'Invalid signature. Transaction was not signed by the provided public key.', + ); + } + + // Remove used challenge + this.challengeStore.delete(publicKey); + return true; + } + /** * Verify the signed challenge and issue a JWT */ diff --git a/apps/backend/src/stellar/stellar.service.ts b/apps/backend/src/stellar/stellar.service.ts index b9d3736d..0c1b36c1 100644 --- a/apps/backend/src/stellar/stellar.service.ts +++ b/apps/backend/src/stellar/stellar.service.ts @@ -160,16 +160,15 @@ export class StellarService { ): Promise { validateStellarPublicKey(publicKey); this.logger.debug(`Fetching transactions for account: ${publicKey}`); - try { - const payments = await this.server - .payments() + const operations = await this.server + .operations() .forAccount(publicKey) .order('desc') .limit(limit) .call(); - return payments.records; + return operations.records; } catch (error: unknown) { this.logger.error(`Error fetching transactions for ${publicKey}:`, error); throw new HorizonUnavailableException( diff --git a/apps/backend/src/users/dto/link-stellar-account.dto.ts b/apps/backend/src/users/dto/link-stellar-account.dto.ts index 1e98063f..3565745f 100644 --- a/apps/backend/src/users/dto/link-stellar-account.dto.ts +++ b/apps/backend/src/users/dto/link-stellar-account.dto.ts @@ -22,4 +22,12 @@ export class LinkStellarAccountDto { @IsOptional() @MaxLength(100) label?: string; + + @ApiProperty({ + description: + 'Signed challenge transaction XDR proving ownership of public key', + example: 'AAAAA...', + }) + @IsString() + signedChallenge: string; } diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts index 3c462d16..9439d8dc 100644 --- a/apps/backend/src/users/users.module.ts +++ b/apps/backend/src/users/users.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; @@ -6,9 +6,14 @@ import { User } from './entities/user.entity'; import { StellarAccount } from './entities/stellar-account.entity'; import { StellarService } from '../stellar/stellar.service'; import { UploadModule } from '../upload/upload.module'; +import { AuthModule } from '../auth/auth.module'; @Module({ - imports: [TypeOrmModule.forFeature([User, StellarAccount]), UploadModule], + imports: [ + TypeOrmModule.forFeature([User, StellarAccount]), + UploadModule, + forwardRef(() => AuthModule), + ], providers: [UsersService, StellarService], controllers: [UsersController], exports: [UsersService], diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 418adfb2..4b05ba13 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -4,6 +4,8 @@ import { BadRequestException, ConflictException, Logger, + Inject, + forwardRef, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -14,6 +16,7 @@ import { LinkStellarAccountDto } from './dto/link-stellar-account.dto'; import { StellarAccountResponseDto } from './dto/stellar-account-response.dto'; import { UpdateStellarAccountLabelDto } from './dto/update-stellar-account-label.dto'; import { UploadService } from '../upload/upload.service'; +import { AuthService } from '../auth/auth.service'; import crypto from 'crypto'; @Injectable() @@ -27,6 +30,8 @@ export class UsersService { private stellarAccountRepository: Repository, private stellarService: StellarService, private uploadService: UploadService, + @Inject(forwardRef(() => AuthService)) + private authService: AuthService, ) {} // --- BASIC CRUD --- @@ -78,6 +83,12 @@ export class UsersService { userId: string, dto: LinkStellarAccountDto, ): Promise { + // Verify ownership via challenge-response signature first + await this.authService.verifyChallengeOnly( + dto.publicKey, + dto.signedChallenge, + ); + this.stellarService.validatePublicKeyOrThrow(dto.publicKey); const user = await this.usersRepository.findOne({ where: { id: userId } }); diff --git a/apps/data-processing/src/ingestion/news_fetcher.py b/apps/data-processing/src/ingestion/news_fetcher.py index 0d3721bd..12715c8f 100644 --- a/apps/data-processing/src/ingestion/news_fetcher.py +++ b/apps/data-processing/src/ingestion/news_fetcher.py @@ -10,6 +10,7 @@ from dataclasses import dataclass, asdict from .news_deduplicator import NewsDeduplicator from datetime import datetime +from src.utils.translator import translate_and_normalize import requests from requests.exceptions import RequestException, Timeout @@ -115,64 +116,65 @@ def _fetch_cryptocompare(self, limit: int) -> List[NewsArticle]: articles = [] try: - self._respect_rate_limit() - - params = { - "lang": "EN", - "categories": "BTC,ETH,BLOCKCHAIN", - "excludeCategories": "Sponsored", - } - headers = {"Authorization": f"Apikey {self.cryptocompare_key}"} - response = self.session.get( - APIConfig.CRYPTOCOMPARE_URL, - params=params, - headers=headers, - timeout=APIConfig.TIMEOUT, - ) - - if response.status_code != 200: - self._handle_api_error(response, "CryptoCompare") + for lang in ["EN", "ES", "PT"]: + self._respect_rate_limit() - data = response.json() + params = { + "lang": lang, + "categories": "BTC,ETH,BLOCKCHAIN", + "excludeCategories": "Sponsored", + } - if data.get("Type") != 100: - raise ValueError( - f"CryptoCompare API returned error: {data.get('Message', 'Unknown error')}" + response = self.session.get( + APIConfig.CRYPTOCOMPARE_URL, + params=params, + headers=headers, + timeout=APIConfig.TIMEOUT, ) - # Parse articles - for item in data.get("Data", [])[:limit]: - try: - article = NewsArticle( - id=f"cc_{item['id']}", - title=item.get("title", ""), - content=item.get("body", ""), - summary=item.get("short_description", ""), - source=item.get("source", "Unknown"), - url=item.get("url", ""), - published_at=datetime.fromtimestamp( - item.get("published_on", 0) - ), - categories=( - item.get("categories", "").split("|") - if item.get("categories") - else [] - ), - tags=( - item.get("tags", "").split("|") if item.get("tags") else [] - ), - ) + if response.status_code != 200: + self._handle_api_error(response, "CryptoCompare") + + data = response.json() - # Avoid duplicates - if article.id not in self.seen_articles: - articles.append(article) - self.seen_articles.add(article.id) + if data.get("Type") != 100: + raise ValueError( + f"CryptoCompare API returned error: {data.get('Message', 'Unknown error')}" + ) - except KeyError as e: - print(f"Warning: Missing key in CryptoCompare data: {e}") - continue + # Parse articles + for item in data.get("Data", [])[:limit]: + try: + article = NewsArticle( + id=f"cc_{item['id']}", + title=translate_and_normalize(item.get("title", "")), + content=translate_and_normalize(item.get("body", "")), + summary=translate_and_normalize(item.get("short_description", "")), + source=item.get("source", "Unknown"), + url=item.get("url", ""), + published_at=datetime.fromtimestamp( + item.get("published_on", 0) + ), + categories=( + item.get("categories", "").split("|") + if item.get("categories") + else [] + ), + tags=( + item.get("tags", "").split("|") if item.get("tags") else [] + ), + ) + + # Avoid duplicates + if article.id not in self.seen_articles: + articles.append(article) + self.seen_articles.add(article.id) + + except KeyError as e: + print(f"Warning: Missing key in CryptoCompare data: {e}") + continue except RequestException as e: print(f"Error fetching from CryptoCompare: {e}") @@ -186,60 +188,61 @@ def _fetch_newsapi(self, limit: int) -> List[NewsArticle]: articles = [] try: - self._respect_rate_limit() - # Calculate date range (last 7 days for recent news) to_date = datetime.now() from_date = datetime.fromtimestamp(to_date.timestamp() - (7 * 24 * 3600)) - params = { - "q": "cryptocurrency OR blockchain OR bitcoin OR ethereum", - "language": "en", - "sortBy": "publishedAt", - "pageSize": min(limit, 100), # NewsAPI max is 100 - "from": from_date.strftime("%Y-%m-%d"), - "to": to_date.strftime("%Y-%m-%d"), - "apiKey": self.newsapi_key, - } - - response = self.session.get( - APIConfig.NEWSAPI_URL, params=params, timeout=APIConfig.TIMEOUT - ) - - if response.status_code != 200: - self._handle_api_error(response, "NewsAPI") - - data = response.json() - - # Parse articles - for item in data.get("articles", [])[:limit]: - try: - published_at = datetime.fromisoformat( - item["publishedAt"].replace("Z", "+00:00") - ) - - article = NewsArticle( - id=f"na_{hash(item['url']) & 0xFFFFFFFF}", - title=item.get("title", ""), - content=item.get("content", ""), - summary=item.get("description", ""), - source=item.get("source", {}).get("name", "Unknown"), - url=item.get("url", ""), - published_at=published_at, - categories=[ - "crypto", - "blockchain", - ], # NewsAPI doesn't provide categories - ) - - # Avoid duplicates - if article.id not in self.seen_articles: - articles.append(article) - self.seen_articles.add(article.id) + for lang in ["en", "es", "pt"]: + self._respect_rate_limit() + + params = { + "q": "cryptocurrency OR blockchain OR bitcoin OR ethereum", + "language": lang, + "sortBy": "publishedAt", + "pageSize": min(limit, 100), # NewsAPI max is 100 + "from": from_date.strftime("%Y-%m-%d"), + "to": to_date.strftime("%Y-%m-%d"), + "apiKey": self.newsapi_key, + } + + response = self.session.get( + APIConfig.NEWSAPI_URL, params=params, timeout=APIConfig.TIMEOUT + ) - except (KeyError, ValueError) as e: - print(f"Warning: Error parsing NewsAPI article: {e}") - continue + if response.status_code != 200: + self._handle_api_error(response, "NewsAPI") + + data = response.json() + + # Parse articles + for item in data.get("articles", [])[:limit]: + try: + published_at = datetime.fromisoformat( + item["publishedAt"].replace("Z", "+00:00") + ) + + article = NewsArticle( + id=f"na_{hash(item['url']) & 0xFFFFFFFF}", + title=translate_and_normalize(item.get("title", "")), + content=translate_and_normalize(item.get("content", "")), + summary=translate_and_normalize(item.get("description", "")), + source=item.get("source", {}).get("name", "Unknown"), + url=item.get("url", ""), + published_at=published_at, + categories=[ + "crypto", + "blockchain", + ], # NewsAPI doesn't provide categories + ) + + # Avoid duplicates + if article.id not in self.seen_articles: + articles.append(article) + self.seen_articles.add(article.id) + + except (KeyError, ValueError) as e: + print(f"Warning: Error parsing NewsAPI article: {e}") + continue except RequestException as e: print(f"Error fetching from NewsAPI: {e}") diff --git a/apps/data-processing/src/ingestion/social_fetcher.py b/apps/data-processing/src/ingestion/social_fetcher.py index 9d68e91d..0ffb81ee 100644 --- a/apps/data-processing/src/ingestion/social_fetcher.py +++ b/apps/data-processing/src/ingestion/social_fetcher.py @@ -16,6 +16,7 @@ import requests from requests.exceptions import RequestException +from src.utils.translator import translate_and_normalize logger = logging.getLogger(__name__) @@ -237,7 +238,7 @@ def fetch_hashtag( # Normalize hashtag query = hashtag if hashtag.startswith("#") else f"#{hashtag}" - query = f"{query} -is:retweet lang:en" # Exclude retweets, English only + query = f"{query} -is:retweet (lang:en OR lang:es OR lang:pt)" # Exclude retweets, English/Spanish/Portuguese params = { "query": query, @@ -287,7 +288,7 @@ def fetch_hashtag( post = SocialPost( id=tweet["id"], platform=SocialPlatform.TWITTER.value, - content=tweet.get("text", ""), + content=translate_and_normalize(tweet.get("text", "")), author=user.get("username", "unknown"), posted_at=datetime.fromisoformat(tweet["created_at"].replace("Z", "+00:00")), url=f"https://twitter.com/user/status/{tweet['id']}", @@ -406,7 +407,7 @@ def fetch_subreddit( post = SocialPost( id=post_data.get("id", ""), platform=SocialPlatform.REDDIT.value, - content=post_data.get("selftext", "") or post_data.get("title", ""), + content=translate_and_normalize(post_data.get("selftext", "") or post_data.get("title", "")), author=post_data.get("author", "[deleted]"), posted_at=datetime.fromtimestamp(post_data.get("created_utc", time.time()), tz=timezone.utc), url=f"https://reddit.com{post_data.get('permalink', '')}", @@ -475,7 +476,7 @@ def fetch_search( post = SocialPost( id=post_data.get("id", ""), platform=SocialPlatform.REDDIT.value, - content=post_data.get("selftext", "") or post_data.get("title", ""), + content=translate_and_normalize(post_data.get("selftext", "") or post_data.get("title", "")), author=post_data.get("author", "[deleted]"), posted_at=datetime.fromtimestamp(post_data.get("created_utc", time.time()), tz=timezone.utc), url=f"https://reddit.com{post_data.get('permalink', '')}", diff --git a/apps/data-processing/src/utils/translator.py b/apps/data-processing/src/utils/translator.py new file mode 100644 index 00000000..c8aa6b5e --- /dev/null +++ b/apps/data-processing/src/utils/translator.py @@ -0,0 +1,88 @@ +import logging +import unicodedata +import requests +from langdetect import detect + +logger = logging.getLogger(__name__) + +def normalize_text(text: str) -> str: + """ + Applies NFKD unicode normalization, normalizes spacing, and strips text. + Keeps casing intact as it is valuable for sentiment analysis. + """ + if not text: + return "" + + # NFKD normalization decomposes characters (e.g. accented characters) + normalized = unicodedata.normalize("NFKD", text) + + # Clean up whitespace and join + lines = normalized.splitlines() + cleaned_lines = [] + for line in lines: + cleaned_words = " ".join(line.split()) + if cleaned_words: + cleaned_lines.append(cleaned_words) + + return "\n".join(cleaned_lines).strip() + +def translate_to_english(text: str, source_lang: str = "auto") -> str: + """ + Translates non-English text to English using Google's public translation endpoint. + If the translation fails or times out, falls back to the original text. + """ + if not text or not text.strip(): + return text + + url = "https://translate.googleapis.com/translate_a/single" + params = { + "client": "gtx", + "sl": source_lang, + "tl": "en", + "dt": "t", + "q": text + } + + try: + response = requests.get(url, params=params, timeout=5) + response.raise_for_status() + data = response.json() + + # Parse the translation chunks returned by Google Translate + if data and len(data) > 0 and data[0]: + translated_chunks = [] + for chunk in data[0]: + if chunk and len(chunk) > 0 and chunk[0]: + translated_chunks.append(chunk[0]) + if translated_chunks: + return "".join(translated_chunks) + + except Exception as e: + logger.warning(f"Translation failed, falling back to original text. Error: {e}") + + return text + +def translate_and_normalize(text: str) -> str: + """ + Detects the language of the text. If it is not English, normalizes and + translates it to English. If it is English, just normalizes it. + """ + if not text or not text.strip(): + return "" + + # 1. Normalize first (helpful for language detection) + normalized = normalize_text(text) + + # 2. Detect language + try: + lang = detect(normalized) + except Exception: + # Default to English if detection fails (e.g. no letters) + lang = "en" + + # 3. Translate if not English + if lang != "en": + logger.info(f"Detected language '{lang}'. Translating to English.") + return translate_to_english(normalized, source_lang=lang) + + return normalized diff --git a/apps/data-processing/tests/test_news_fetcher.py b/apps/data-processing/tests/test_news_fetcher.py index cb216263..cd6630d2 100644 --- a/apps/data-processing/tests/test_news_fetcher.py +++ b/apps/data-processing/tests/test_news_fetcher.py @@ -83,8 +83,8 @@ def test_fetch_cryptocompare_success(self, mock_get): self.assertEqual(articles[0].title, "Bitcoin Hits All-Time High") self.assertEqual(articles[0].source, "CryptoNews") - # Verify API call was made correctly - mock_get.assert_called_once() + # Verify API calls were made correctly (for EN, ES, PT) + self.assertEqual(mock_get.call_count, 3) call_args = mock_get.call_args self.assertIn("Authorization", call_args[1]["headers"]) self.assertEqual( diff --git a/apps/webapp/app/community/page.tsx b/apps/webapp/app/community/page.tsx index 73921109..cc184be1 100644 --- a/apps/webapp/app/community/page.tsx +++ b/apps/webapp/app/community/page.tsx @@ -298,7 +298,7 @@ export default function CommunityPage() { > CONTRIBUTING.md - . Here's the short version. + . Here's the short version.

{/* vertical line */} diff --git a/apps/webapp/app/dashboard/page.tsx b/apps/webapp/app/dashboard/page.tsx index 61ec14a2..a43eca36 100644 --- a/apps/webapp/app/dashboard/page.tsx +++ b/apps/webapp/app/dashboard/page.tsx @@ -81,47 +81,163 @@ export default function DashboardPage() {
- {/* Recent Transactions */} -
-

- Recent Transactions -

+ {/* Recent Stellar Operations */} +
+
+
+
+
+

+ Recent Stellar Operations +

+
+ + Live Horizon feed + +
{!publicKey ? ( -

Connect your Stellar wallet to view transactions.

+
+
+ + + +
+

Connect your Stellar wallet to view recent account activity.

+
) : isLoadingStellar ? ( -

Loading transaction history...

- ) : !transactions || transactions.length === 0 ? ( -

No transactions found for this account.

- ) : ( -
- {transactions.map((tx) => ( -
-
-

- {tx.type.replace(/_/g, " ")} -

-

- {new Date(tx.created_at).toLocaleString()} -

-
-
- {tx.amount && ( -

- {parseFloat(tx.amount).toFixed(4)} {tx.asset_code || "XLM"} -

- )} - - View on Explorer - +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
))}
+ ) : !transactions || transactions.length === 0 ? ( +
+
+ + + +
+

No recent activities found for this account.

+
+ ) : ( +
+ {transactions.map((tx: any) => { + let title = "Stellar Operation"; + let desc = `Operation ID: ${tx.id.substring(0, 8)}...`; + let colorStyle = "text-gray-400 bg-gray-500/10 border-gray-500/20"; + let iconSvg = ( + + + + ); + + const isOutgoing = tx.from === publicKey || tx.source_account === publicKey; + const isIncoming = tx.to === publicKey || tx.account === publicKey; + + if (tx.type === "payment") { + if (isOutgoing) { + title = "Payment Sent"; + desc = `To ${tx.to ? `${tx.to.substring(0, 6)}...${tx.to.substring(50)}` : "unknown"}`; + colorStyle = "text-rose-400 bg-rose-500/10 border-rose-500/20"; + iconSvg = ( + + + + ); + } else { + title = "Payment Received"; + desc = `From ${tx.from ? `${tx.from.substring(0, 6)}...${tx.from.substring(50)}` : "unknown"}`; + colorStyle = "text-emerald-400 bg-emerald-500/10 border-emerald-500/20"; + iconSvg = ( + + + + ); + } + } else if (tx.type === "create_account") { + if (isIncoming) { + title = "Account Funded"; + desc = `Funded with ${tx.starting_balance || "0"} XLM`; + colorStyle = "text-emerald-400 bg-emerald-500/10 border-emerald-500/20"; + } else { + title = "Account Created"; + desc = `Created account ${tx.account ? `${tx.account.substring(0, 6)}...` : ""}`; + colorStyle = "text-blue-400 bg-blue-500/10 border-blue-500/20"; + } + iconSvg = ( + + + + ); + } else if (tx.type === "change_trust") { + title = "Trustline Updated"; + desc = `Asset: ${tx.asset_code || "Unknown"} | Limit: ${tx.limit ? parseFloat(tx.limit).toFixed(0) : "0"}`; + colorStyle = "text-cyan-400 bg-cyan-500/10 border-cyan-500/20"; + iconSvg = ( + + + + ); + } else if (tx.type === "set_options") { + title = "Settings Modified"; + desc = "Account options / thresholds updated"; + colorStyle = "text-amber-400 bg-amber-500/10 border-amber-500/20"; + iconSvg = ( + + + + + ); + } + + return ( +
+
+
+ {iconSvg} +
+
+

+ {title} +

+

+ {desc} +

+
+
+
+ {tx.amount && ( +

+ {parseFloat(tx.amount).toFixed(4)} {tx.asset_code || "XLM"} +

+ )} +
+ + {new Date(tx.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + Detail + + + + +
+
+
+ ); + })} +
)}
diff --git a/apps/webapp/app/providers.tsx b/apps/webapp/app/providers.tsx index 16786e2a..14d77cfb 100644 --- a/apps/webapp/app/providers.tsx +++ b/apps/webapp/app/providers.tsx @@ -49,13 +49,20 @@ function ConfigGate({ children }: { children: ReactNode }) { ); } +import { OnboardingProvider } from "@/lib/onboarding"; +import { ThemeProvider } from "@/components/theme-provider"; + export function Providers({ children }: { children: ReactNode }) { return ( - {children} + + + {children} + + diff --git a/apps/webapp/components/crypto-table.tsx b/apps/webapp/components/crypto-table.tsx index 61bd5e67..49c736e4 100644 --- a/apps/webapp/components/crypto-table.tsx +++ b/apps/webapp/components/crypto-table.tsx @@ -31,7 +31,9 @@ export function CryptoTable({ formatNumberAction, showWatchlistToggle = true }: const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [favorites, setFavorites] = useState([]); - const { toggleItem, isInWatchlist } = showWatchlistToggle ? useWatchlist() : { toggleItem: async () => ({ added: false }), isInWatchlist: () => false }; + const watchlist = useWatchlist(); + const toggleItem = showWatchlistToggle ? watchlist.toggleItem : async () => ({ added: false }); + const isInWatchlist = showWatchlistToggle ? watchlist.isInWatchlist : () => false; // Fetch real crypto data useEffect(() => { diff --git a/apps/webapp/components/onboarding/OnboardingModal.tsx b/apps/webapp/components/onboarding/OnboardingModal.tsx index e6505ae0..58ea44b9 100644 --- a/apps/webapp/components/onboarding/OnboardingModal.tsx +++ b/apps/webapp/components/onboarding/OnboardingModal.tsx @@ -1,8 +1,11 @@ "use client"; -import { useEffect, useRef, useCallback } from 'react'; +import { useEffect, useRef, useCallback, useState } from 'react'; import { useOnboarding } from '@/lib/onboarding'; import { useStellarWallet } from '@/app/providers'; +import { useStellarConfig } from '@/contexts/StellarConfigContext'; +import { signTransaction } from '@stellar/freighter-api'; +import { StellarApiService } from '@/lib/api-services'; import Link from 'next/link'; import dynamic from 'next/dynamic'; import gsap from 'gsap'; @@ -36,10 +39,14 @@ const steps = [ export default function OnboardingModal() { const { state, nextStep, prevStep, closeOnboarding, completeOnboarding, skipOnboarding } = useOnboarding(); - const { status: walletStatus, connect: connectWallet } = useStellarWallet(); + const { status: walletStatus, connect: connectWallet, publicKey } = useStellarWallet(); + const { config } = useStellarConfig(); const modalRef = useRef(null); const stepRef = useRef(null); + const [linkingState, setLinkingState] = useState<'idle' | 'fetching_challenge' | 'signing' | 'linking' | 'success' | 'error'>('idle'); + const [linkingError, setLinkingError] = useState(null); + // Auto-advance logic (optional, lightweight) useEffect(() => { if (state.step >= 1 && state.step <= 3) { @@ -70,13 +77,48 @@ export default function OnboardingModal() { } }, [state.step]); + const handleLinkWallet = useCallback(async () => { + if (!publicKey) return; + setLinkingState('fetching_challenge'); + setLinkingError(null); + try { + const challengeRes = await StellarApiService.getChallenge(publicKey); + const challengeXDR = challengeRes.challenge; + + setLinkingState('signing'); + const networkPassphrase = config?.networkPassphrase; + const result = await signTransaction(challengeXDR, { networkPassphrase }); + + if (result.error) { + throw new Error(result.error); + } + + setLinkingState('linking'); + await StellarApiService.linkAccount(publicKey, result.signedTxXdr, "Onboarding Wallet"); + + setLinkingState('success'); + setTimeout(() => { + nextStep(); + setLinkingState('idle'); + }, 1500); + } catch (err: any) { + console.error('Wallet linking error:', err); + setLinkingState('error'); + setLinkingError(err.message || 'Failed to sign or verify challenge.'); + } + }, [publicKey, config, nextStep]); + const handleNextOrConnect = useCallback(() => { - if (state.step === 2 && walletStatus !== 'connected') { - connectWallet(); + if (state.step === 2) { + if (walletStatus !== 'connected') { + connectWallet(); + } else { + handleLinkWallet(); + } } else { nextStep(); } - }, [state.step, walletStatus, connectWallet, nextStep]); + }, [state.step, walletStatus, connectWallet, nextStep, handleLinkWallet]); const handlePrimaryAction = useCallback(() => { if (state.step === 3) { @@ -134,8 +176,15 @@ export default function OnboardingModal() { />
+ {/* Link Error Message */} + {state.step === 2 && (linkingState === 'error' || linkingError) && ( +
+ {linkingError || "An error occurred during wallet verification."} +
+ )} + {/* Skip Button */} - {state.step < 3 && ( + {state.step < 3 && linkingState === 'idle' && ( - {state.step === 2 && ( + {state.step === 2 && linkingState === 'idle' && (
Or use email signup → diff --git a/apps/webapp/components/transaction-detail.tsx b/apps/webapp/components/transaction-detail.tsx index b47b44a6..deb879a5 100644 --- a/apps/webapp/components/transaction-detail.tsx +++ b/apps/webapp/components/transaction-detail.tsx @@ -172,7 +172,7 @@ export default function TransactionDetail({

Memo

-

"{transaction.memo}"

+

“{transaction.memo}”

)} diff --git a/apps/webapp/lib/api-services.ts b/apps/webapp/lib/api-services.ts index 88acaf47..da77af52 100644 --- a/apps/webapp/lib/api-services.ts +++ b/apps/webapp/lib/api-services.ts @@ -125,5 +125,51 @@ export class StellarApiService { return []; // Return empty array on transaction fetch error to fail gracefully } } + + private static getAuthHeaders(): Record { + if (typeof document === 'undefined') return {}; + const match = document.cookie + .split('; ') + .find((row) => row.startsWith('auth-token=')); + const token = match?.split('=')[1]; + return { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; + } + + static async getChallenge(publicKey: string): Promise<{ challenge: string }> { + const response = await fetch(`${this.BASE_URL}/auth/challenge?publicKey=${publicKey}`, { + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + const err = await response.json().catch(() => ({})); + throw new Error(err.message || 'Failed to get auth challenge'); + } + + return response.json(); + } + + static async linkAccount(publicKey: string, signedChallenge: string, label?: string): Promise { + const response = await fetch(`${this.BASE_URL}/users/me/accounts`, { + method: 'POST', + headers: this.getAuthHeaders(), + body: JSON.stringify({ + publicKey, + signedChallenge, + label, + }), + }); + + if (!response.ok) { + const err = await response.json().catch(() => ({})); + throw new Error(err.message || 'Failed to link Stellar account'); + } + + return response.json(); + } } diff --git a/apps/webapp/package-lock.json b/apps/webapp/package-lock.json index c87ae01d..c87c5d66 100644 --- a/apps/webapp/package-lock.json +++ b/apps/webapp/package-lock.json @@ -159,6 +159,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -550,6 +551,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -573,6 +575,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3017,6 +3020,7 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz", "integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.26.7", @@ -3729,7 +3733,8 @@ "version": "20.6.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/offscreencanvas": { "version": "2019.7.3", @@ -3748,6 +3753,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.22.tgz", "integrity": "sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3759,6 +3765,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" } @@ -3789,6 +3796,7 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.162.0.tgz", "integrity": "sha512-0j5yZcVukVIhrhSIC7+LmBPkkMoMuEJ1AfYBZfgNytdYqYREMuiyXWhYOMeZLBElTEAlJIZn7r2W3vqTIgjWlg==", "license": "MIT", + "peer": true, "dependencies": { "@tweenjs/tween.js": "~23.1.1", "@types/stats.js": "*", @@ -4426,6 +4434,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4908,6 +4917,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -5536,6 +5546,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -5781,7 +5792,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -6080,6 +6092,7 @@ "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6240,6 +6253,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7836,6 +7850,7 @@ "integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@asamuzakjp/dom-selector": "^2.0.1", "cssstyle": "^4.0.1", @@ -8888,6 +8903,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -9102,6 +9118,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -9140,6 +9157,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -9162,6 +9180,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -10470,7 +10489,8 @@ "version": "0.162.0", "resolved": "https://registry.npmjs.org/three/-/three-0.162.0.tgz", "integrity": "sha512-xfCYj4RnlozReCmUd+XQzj6/5OjDNHBy5nT6rVwrOKGENAvpXe2z1jL+DZYaMu4/9pNsjH/4Os/VvS9IrH7IOQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/three-mesh-bvh": { "version": "0.7.8", @@ -10550,6 +10570,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10844,6 +10865,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11079,6 +11101,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -11191,6 +11214,7 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1",