diff --git a/backend/.env b/backend/.env index 502590f2..ac362cb6 100644 --- a/backend/.env +++ b/backend/.env @@ -1,3 +1,5 @@ TOKEN_SECRET=27a0967eed44a78feec1ccd9225343258894288202c0a45a8f93adce6786dbc9ae5a7fc4df716f29eefed7067d9b94fa9f7c2ab7dd7781d36cb22206bf25b886 LINK_SECRET=ijr2iq34rfeiadsfkjq3ew CLIENT_SECRET=fOP43IDxXavjRogZMQWKW1qmJAz5zeEf +NEO4J_USERNAME=neo4j +NEO4J_PASSWORD=password diff --git a/backend/package-lock.json b/backend/package-lock.json index bc5c3137..8d1a9ba2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@keycloak/keycloak-admin-client": "^24.0.1", "@types/jsonwebtoken": "^9.0.5", - "axios": "^1.6.2", "bcrypt": "^5.1.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", @@ -22,7 +21,8 @@ "neo4j-driver": "^5.13.0", "socket.io": "^4.7.2", "unidecode": "^0.1.8", - "vite": "^5.2.6" + "vite": "^5.2.6", + "zod": "^3.22.4" }, "devDependencies": { "@types/bcrypt": "^5.0.2", @@ -1116,21 +1116,6 @@ "node": "*" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1177,12 +1162,12 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -1190,7 +1175,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -1262,12 +1247,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1327,17 +1318,6 @@ "color-support": "bin.js" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1374,9 +1354,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -1452,12 +1432,20 @@ "node": ">=6" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/delegates": { @@ -1599,6 +1587,25 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", @@ -1665,16 +1672,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -1722,38 +1729,6 @@ "node": ">= 0.8" } }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1853,14 +1828,18 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1897,18 +1876,32 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/has": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", - "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", - "engines": { - "node": ">= 0.4.0" + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -1932,6 +1925,17 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2670,9 +2674,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2834,11 +2838,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/ps-tree": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", @@ -2885,9 +2884,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -3064,6 +3063,22 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -3091,13 +3106,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3318,9 +3337,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -4272,6 +4291,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -4974,21 +5001,6 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", - "requires": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5014,12 +5026,12 @@ } }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -5027,7 +5039,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } @@ -5072,12 +5084,15 @@ "dev": true }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "camelize-ts": { @@ -5119,14 +5134,6 @@ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5157,9 +5164,9 @@ "dev": true }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" }, "cookie-parser": { "version": "1.4.6", @@ -5219,10 +5226,15 @@ "type-detect": "^4.0.0" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } }, "delegates": { "version": "1.0.0", @@ -5326,6 +5338,19 @@ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "esbuild": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", @@ -5382,16 +5407,16 @@ } }, "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -5433,21 +5458,6 @@ "unpipe": "~1.0.0" } }, - "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5521,14 +5531,15 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "get-tsconfig": { @@ -5553,15 +5564,26 @@ "path-is-absolute": "^1.0.0" } }, - "has": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", - "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==" + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } }, "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" }, "has-symbols": { "version": "1.0.3", @@ -5573,6 +5595,14 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6118,9 +6148,9 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" }, "on-finished": { "version": "2.4.1", @@ -6235,11 +6265,6 @@ "ipaddr.js": "1.9.1" } }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "ps-tree": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", @@ -6268,9 +6293,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -6399,6 +6424,19 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -6420,13 +6458,14 @@ "dev": true }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "sift": { @@ -6602,9 +6641,9 @@ } }, "tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -7121,6 +7160,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", "dev": true + }, + "zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==" } } } diff --git a/backend/package.json b/backend/package.json index cbca4a56..8cdf9c4d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,7 @@ "license": "ISC", "scripts": { "start": "tsc-watch --noEmit --onSuccess \"tsx ./src/index.ts\"", - "test": "vitest", + "test": "vitest --single-thread", "coverage": "vitest --coverage", "build": "tsc", "prod": "node build/index.js" @@ -17,7 +17,6 @@ "dependencies": { "@keycloak/keycloak-admin-client": "^24.0.1", "@types/jsonwebtoken": "^9.0.5", - "axios": "^1.6.2", "bcrypt": "^5.1.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", @@ -28,7 +27,8 @@ "neo4j-driver": "^5.13.0", "socket.io": "^4.7.2", "unidecode": "^0.1.8", - "vite": "^5.2.6" + "vite": "^5.2.6", + "zod": "^3.22.4" }, "devDependencies": { "@types/bcrypt": "^5.0.2", diff --git a/backend/src/data/users.ts b/backend/src/data/users.ts index a17d51d0..3827629a 100644 --- a/backend/src/data/users.ts +++ b/backend/src/data/users.ts @@ -3,7 +3,7 @@ const userData = [ "id": "e023f020-1f16-49f7-97eb-0efe144d42f0", "first_name": "Iga", "last_name": "Swiatek", - "country": "Poland", + "country": "PL", "profile_picture": "https://images.unsplash.com/photo-1581992652564-44c42f5ad3ad?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "mail": "1ga@wta.com", @@ -15,11 +15,11 @@ const userData = [ "id": "5facdc9e-2875-4300-a990-af0f6d9951a3", "first_name": "Adam", "last_name": "Małysz", - "country": "Poland", + "country": "PL", "profile_picture": "https://images.unsplash.com/photo-1639149888905-fb39731f2e6c?q=80&w=1364&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "mail": "adasko@malysz.pl", - "password": "hophop!", + "password": "hophop!23", "friend_ids": [], "chats": [], }, @@ -27,7 +27,7 @@ const userData = [ "id": "f3827527-4ecb-4c6c-a819-ba4d644704de", "first_name": "Robert", "last_name": "Lewandowski", - "country": "Poland", + "country": "PL", "profile_picture": "https://images.unsplash.com/photo-1519456264917-42d0aa2e0625?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "mail": "lewy.robi@pzpn.pl", @@ -51,7 +51,7 @@ const userData = [ "id": "d47ae6a5-a2ef-4289-b3fa-a5159f934c5b", "first_name": "Elon", "last_name": "Musk", - "country": "United States of America", + "country": "US", "profile_picture": "https://images.unsplash.com/photo-1587049352846-4a222e784d38?q=80&w=2080&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "mail": "elonmusk@spacex.com", @@ -75,7 +75,7 @@ const userData = [ "id": "629373ab-1d9f-44a5-9ed3-b3d5808353d4", "first_name": "Mark", "last_name": "Zuckerberg", - "country": "United States of America", + "country": "US", "profile_picture": "https://images.unsplash.com/photo-1546557036-57b741df8f5a?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "mail": "reptilian@meta.com", @@ -87,7 +87,7 @@ const userData = [ "id": "8ec319bf-6dc4-456a-8976-a57c2aecb5d2", "first_name": "Selena", "last_name": "Halso", - "country": "Venezuela", + "country": "MA", "profile_picture": "https://images.unsplash.com/photo-1491604612772-6853927639ef?q=80&w=1287&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "mail": "shalso0@nifty.com", @@ -99,11 +99,11 @@ const userData = [ "id": "48c1fc1d-3a3d-48c5-9f51-4ad49d1a617a", "first_name": "Silva", "last_name": "Hudghton", - "country": "San Marino", + "country": "CN", "profile_picture": "https://images.unsplash.com/photo-1618780179533-870736eaea58?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTQyfHxwcm9maWxlJTIwcGljdHVyZXxlbnwwfHwwfHx8MA%3D%3D", "mail": "shudghton1@geocities.com", - "password": "Cloned", + "password": "Huds1l12#", "friend_ids": [2, 3, 26], "chats": [], }, @@ -111,7 +111,7 @@ const userData = [ "id": "bcd120cc-446a-4686-9fe8-e7e8c85ae8b2", "first_name": "Bronson", "last_name": "Conford", - "country": "Norway", + "country": "RU", "profile_picture": "https://images.unsplash.com/photo-1503235930437-8c6293ba41f5?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTU2fHxwcm9maWxlJTIwcGljdHVyZXxlbnwwfHwwfHx8MA%3D%3D", "mail": "bconford2@wikimedia.org", @@ -123,11 +123,11 @@ const userData = [ "id": "885b500e-43f6-477e-bcde-d19e0945cbdc", "first_name": "Carole", "last_name": "Ruckman", - "country": "Libya", + "country": "FI", "profile_picture": "https://images.unsplash.com/photo-1614460132343-62aa9fa8d6f6?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTQ0fHxwcm9maWxlJTIwcGljdHVyZXxlbnwwfHwwfHx8MA%3D%3D", "mail": "cruckman3@archive.org", - "password": "core", + "password": "coreCar0l;", "friend_ids": [0, 11, 4, 15, 18, 13], "chats": [], }, @@ -135,7 +135,7 @@ const userData = [ "id": "cadde518-aac1-489e-987e-eda0e5dd7826", "first_name": "Gian", "last_name": "Blasik", - "country": "Guinea", + "country": "CN", "profile_picture": "https://images.unsplash.com/photo-1608493573324-b055427a503e?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTY0fHxwcm9maWxlJTIwcGljdHVyZXxlbnwwfHwwfHx8MA%3D%3D", "mail": "gblasik4@umich.edu", @@ -147,7 +147,7 @@ const userData = [ "id": "8a7063a4-f0ff-4cb1-add5-6fa972e14e58", "first_name": "Lyman", "last_name": "Truman", - "country": "Dominica", + "country": "PT", "profile_picture": "https://images.unsplash.com/photo-1520592978680-efbdeae5d036?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTY2fHxwcm9maWxlJTIwcGljdHVyZXxlbnwwfHwwfHx8MA%3D%3D", "mail": "ltruman5@hc360.com", @@ -159,7 +159,7 @@ const userData = [ "id": "2821b350-23ab-4efc-826e-2d0dc0470b87", "first_name": "Brooke", "last_name": "McMurty", - "country": "Cambodia", + "country": "GL", "profile_picture": "https://images.unsplash.com/photo-1522228115018-d838bcce5c3a?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTcxfHxwcm9maWxlJTIwcGljdHVyZXxlbnwwfHwwfHx8MA%3D%3D", "mail": "bmcmurty6@photobucket.com", @@ -171,7 +171,7 @@ const userData = [ "id": "1bc3c96c-1f5e-4114-a899-faccce69acad", "first_name": "Olwen", "last_name": "Helbeck", - "country": "Italy", + "country": "CN", "profile_picture": "https://images.unsplash.com/photo-1517849845537-4d257902454a?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8ZG9nfGVufDB8fDB8fHww", "mail": "ohelbeck7@sun.com", @@ -183,7 +183,7 @@ const userData = [ "id": "7575ee32-c98e-4e8f-bd45-20358a7587e7", "first_name": "Jo", "last_name": "Carver", - "country": "Micronesia", + "country": "CO", "profile_picture": "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8cHJvZmlsZSUyMHBpY3R1cmV8ZW58MHx8MHx8fDA%3D", "mail": "jcarver8@infoseek.co.jp", @@ -195,11 +195,11 @@ const userData = [ "id": "2a818ac0-f86a-4929-9033-20d1bd215c64", "first_name": "Trixy", "last_name": "Ogle", - "country": "Saudi Arabia", + "country": "CO", "profile_picture": "https://images.unsplash.com/photo-1536164261511-3a17e671d380?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NzB8fHByb2ZpbGUlMjBwaWN0dXJlfGVufDB8fDB8fHww", "mail": "togle9@engadget.com", - "password": "Focused", + "password": "trixxed3@!", "friend_ids": [20, 11, 1, 5, 3, 13], "chats": [], }, @@ -207,7 +207,7 @@ const userData = [ "id": "0760d7f8-daaf-4d07-9e1d-d84703d24c9d", "first_name": "Trefor", "last_name": "Kerby", - "country": "Saudi Arabia", + "country": "CO", "profile_picture": "https://images.unsplash.com/photo-1463453091185-61582044d556?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTR8fHByb2ZpbGUlMjBwaWN0dXJlJTIwb2xkZXIlMjBwZXJzb258ZW58MHx8MHx8fDA%3D", "mail": "tkerbya@shinystat.com", @@ -219,7 +219,7 @@ const userData = [ "id": "30db2a37-7025-438c-aa8c-b13c3b48995c", "first_name": "Dennis", "last_name": "Beinke", - "country": "Saudi Arabia", + "country": "GR", "profile_picture": "https://images.unsplash.com/photo-1601233749202-95d04d5b3c00?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDh8fHByb2ZpbGUlMjBwaWN0dXJlJTIwb2xkZXIlMjBwZXJzb258ZW58MHx8MHx8fDA%3D", "mail": "dbeinkeb@blogger.com", @@ -231,7 +231,7 @@ const userData = [ "id": "8ee5b4b4-405e-437a-9ce6-0ba015e9805e", "first_name": "Filippo", "last_name": "Melmore", - "country": "Zambia", + "country": "SW", "profile_picture": "https://images.unsplash.com/photo-1522529599102-193c0d76b5b6?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjR8fG1pZGRsZSUyMGVhc3Rlcm4lMjBwZXJzb258ZW58MHx8MHx8fDA%3D", "mail": "fmelmorec@elpais.com", @@ -243,7 +243,7 @@ const userData = [ "id": "add0a8dc-da6b-43b3-9459-1d2226508cf2", "first_name": "Ann", "last_name": "Mooring", - "country": "Palestine State", + "country": "MN", "profile_picture": "https://images.unsplash.com/photo-1477276266798-898214c677da?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTZ8fG1pZGRsZSUyMGVhc3Rlcm4lMjBwZXJzb258ZW58MHx8MHx8fDA%3D", "mail": "cmooringd@census.gov", @@ -255,7 +255,7 @@ const userData = [ "id": "1ed83de4-2496-4f96-883f-d891ebe2c2f9", "first_name": "Trever", "last_name": "Shillito", - "country": "Lithuania", + "country": "CN", "profile_picture": "https://images.unsplash.com/photo-1605664041952-4a2855d9363b?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjN8fGxhd3llcnxlbnwwfHwwfHx8MA%3D%3D", "mail": "tshillitoe@state.gov", @@ -267,7 +267,7 @@ const userData = [ "id": "77a97e29-b4d5-458d-a69a-6db84a2fc5e9", "first_name": "Heidi", "last_name": "Warlawe", - "country": "Lithuania", + "country": "RU", "profile_picture": "https://images.unsplash.com/photo-1519419691348-3b3433c4c20e?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTF8fG1pZGRsZSUyMGVhc3Rlcm4lMjBwZXJzb258ZW58MHx8MHx8fDA%3D", "mail": "hwarlawef@seattletimes.com", @@ -279,7 +279,7 @@ const userData = [ "id": "c650f5e8-fc5b-48a2-95fb-2c0855580f58", "first_name": "Ingeberg", "last_name": "Cargenven", - "country": "Iran", + "country": "BR", "profile_picture": "https://images.unsplash.com/photo-1506863530036-1efeddceb993?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjR8fHByb2ZpbGUlMjBwaWN0dXJlJTIwb2xkZXIlMjBwZXJzb258ZW58MHx8MHx8fDA%3D", "mail": "icargenveng@nasa.gov", @@ -291,7 +291,7 @@ const userData = [ "id": "ea3f3aaf-ce06-42d6-8602-57c0cf027fea", "first_name": "Abby", "last_name": "Godney", - "country": "Iran", + "country": "PL", "profile_picture": "https://images.unsplash.com/photo-1620553967344-c6d788133f17?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Njh8fHByb2ZpbGUlMjBwaWN0dXJlJTIwb2xkZXIlMjBwZXJzb258ZW58MHx8MHx8fDA%3D", "mail": "agodneyh@studiopress.com", @@ -303,11 +303,11 @@ const userData = [ "id": "eaccfdce-1795-4261-bf77-ddf56741c1b7", "first_name": "Olivier", "last_name": "Beckmann", - "country": "Denmark", + "country": "PH", "profile_picture": "https://images.unsplash.com/photo-1612349317150-e413f6a5b16d?w=1000&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Nnx8ZG9jdG9yfGVufDB8fDB8fHww", "mail": "obeckmanni@google.com.br", - "password": "product", + "password": "pro!duct3d", "friend_ids": [13, 15, 19], "chats": [], }, diff --git a/backend/src/db.ts b/backend/src/db.ts index c37cb439..841a062e 100644 --- a/backend/src/db.ts +++ b/backend/src/db.ts @@ -1,7 +1,8 @@ import driver from "./driver/driver.js"; import userData from "./data/users.js"; -import { registerUser } from "./users.js"; +import { registerUser, registerUserSchema } from "./users.js"; +import { addFriend } from "./userFriends.js"; export async function isDatabaseEmpty() { const session = driver.session(); @@ -31,29 +32,17 @@ export async function importInitialData() { const userIds: string[] = []; for (const user of userData) { - const newUser = await registerUser(user); - if (!("errors" in newUser)) { - userIds.push(newUser.id); - } + const parsedUser = registerUserSchema.parse(user); + const newUser = await registerUser(session, parsedUser); + userIds.push(newUser.id); } - // Create relationships - const createRelationshipQuery = ` - MATCH (u1:User {id: $userId}) - MATCH (u2:User {id: $friendId}) - CREATE (u1)-[:IS_FRIENDS_WITH]->(u2) - CREATE (u2)-[:IS_FRIENDS_WITH]->(u1) - `; - for (const [userIndex, user] of userData.entries()) { const userId = userIds[userIndex]; for (const friendIndex of user.friend_ids) { const friendId = userIds[friendIndex]; - await session.run(createRelationshipQuery, { - userId, - friendId, - }); + await addFriend(session, userId, friendId); } } @@ -72,7 +61,7 @@ export async function importInitialData() { return "Initial data has been imported into database."; } catch (error) { - return "Error importing data"; + return "Error importing data:" + error; } finally { await session.close(); } diff --git a/backend/src/driver/driver.ts b/backend/src/driver/driver.ts index b91a5fd3..b5807633 100644 --- a/backend/src/driver/driver.ts +++ b/backend/src/driver/driver.ts @@ -1,8 +1,19 @@ import neo4j from "neo4j-driver"; +const username = process.env.NEO4J_USERNAME; +const password = process.env.NEO4J_PASSWORD; + +if (!username) { + throw new Error("NEO4J_USERNAME environment variable not provided!"); +} + +if (!password) { + throw new Error("NEO4J_PASSWORD environment variable not provided!"); +} + const driver = neo4j.driver( process.env.NEO4J_URI || "neo4j://localhost:7687", - neo4j.auth.basic("neo4j", "password"), + neo4j.auth.basic(username, password), ); export default driver; diff --git a/backend/src/misc/fetchData.ts b/backend/src/misc/fetchData.ts new file mode 100644 index 00000000..4937b1a3 --- /dev/null +++ b/backend/src/misc/fetchData.ts @@ -0,0 +1,10 @@ +export const fetchData = async (url: string, method: string, options = {}) => { + try { + const response = await fetch(url, { ...options, method }); + const data = await response.json(); + return data; + } catch (error) { + console.error("Error ocurred during fetch data:", error); + throw error; + } +}; diff --git a/backend/src/misc/formatError.ts b/backend/src/misc/formatError.ts new file mode 100644 index 00000000..c00c1c00 --- /dev/null +++ b/backend/src/misc/formatError.ts @@ -0,0 +1,21 @@ +import { ZodError } from "zod"; +import { Errors } from "../models/Response.js"; + +export function formatError(error: ZodError): Errors { + const result: Errors = {}; + + for (const issue of error.issues) { + let obj = result; + + for (const key of issue.path.slice(0, -1)) { + const newObj = {}; + obj[key] = newObj; + obj = newObj; + } + + const [lastKey] = issue.path.slice(-1); + obj[lastKey] = issue.message; + } + + return result; +} diff --git a/backend/src/misc/verifyRequest.ts b/backend/src/misc/verifyRequest.ts deleted file mode 100644 index 556948a4..00000000 --- a/backend/src/misc/verifyRequest.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Errors } from "../models/Response.js"; - -export interface PageQuery { - page: string; - maxUsers: string; -} - -export interface Page { - page: number; - maxUsers: number; -} - -export interface SearchQuery extends PageQuery { - q: string; - country: string; -} - -export interface SearchVerified extends Page { - q: string; - country: string; -} - -export type VerifyResult = ValidResult | InvalidResult; - -export type ValidResult = { - valid: true; - verified: T; -}; - -export type InvalidResult = { - valid: false; - errors: Errors; -}; - -class Verifier { - errors: Errors = {}; - verified: Record = {} as any; - obj: T; - - constructor(obj: T) { - this.obj = obj; - } - - private fail(key: keyof T & string, message: string) { - this.errors[key] = message; - return null; - } - - private pass(key: keyof T & string, value: V): V { - this.verified[key] = value; - return value; - } - - getErrors(): Errors | null { - for (const _ in this.errors) { - return this.errors; - } - - return null; - } - - verifyAscii( - key: K & string, - allowEmpty: boolean = false, - ): string | null { - const value = this.obj[key]; - - if (!allowEmpty) { - if (!value) { - return this.fail(key, "not provided"); - } - } else if (value === undefined) { - return this.pass(key, ""); - } - - if (typeof value !== "string") { - return this.fail(key, "incorrect"); - } - - if (!/^[a-zA-Z ]*$/.test(value)) { - return this.fail(key, "not a valid string"); - } - - return this.pass(key, value); - } - - verifyInteger(key: K & string): number | null { - const value = this.obj[key]; - - if (!value) { - return this.fail(key, "not provided"); - } - - if (typeof value !== "string") { - return this.fail(key, "incorrect"); - } - - if (!/^[0-9]+$/.test(value)) { - return this.fail(key, "not a number"); - } - - return this.pass(key, Number(value)); - } -} - -export function verifyPageQuery( - query: T, -): VerifyResult { - const verifier = new Verifier(query); - verifier.verifyInteger("page"); - verifier.verifyInteger("maxUsers"); - - const errors = verifier.getErrors(); - - if (errors) { - return { valid: false, errors }; - } - - return { valid: true, verified: verifier.verified }; -} - -export function verifySearchQuery( - query: T, -): VerifyResult { - const verifier = new Verifier(query); - verifier.verifyInteger("page"); - verifier.verifyInteger("maxUsers"); - verifier.verifyAscii("q", true); - verifier.verifyAscii("country", true); - - const errors = verifier.getErrors(); - - if (errors) { - return { valid: false, errors }; - } - - return { valid: true, verified: verifier.verified }; -} diff --git a/backend/src/misc/wordToVec.ts b/backend/src/misc/wordToVec.ts index ce63feda..de91921e 100644 --- a/backend/src/misc/wordToVec.ts +++ b/backend/src/misc/wordToVec.ts @@ -6,11 +6,11 @@ const kb = [ ]; const kbValue = [...kb].map((c) => (c / 25) * 2 - 1); -const letterToKb = (c: string) => kbValue[c.charCodeAt(0) - 65]; +export const letterToKb = (c: string) => kbValue[c.charCodeAt(0) - 65]; -const lerp = (a: number, b: number, f: number) => (1 - f) * a + f * b; +export const lerp = (a: number, b: number, f: number) => (1 - f) * a + f * b; -const wordVecInterp = (word: string, vecLength: number) => { +export const wordVecInterp = (word: string, vecLength: number) => { const vec = []; const wordCapital = word.toUpperCase(); const factor = (word.length - 1) / (vecLength - 1); @@ -35,34 +35,30 @@ const wordVecInterp = (word: string, vecLength: number) => { }; const keepLettersRegex = /[^a-z]/; -const keepLetters = (s: string) => s.replace(keepLettersRegex, ""); -const sortLetters = (s: string) => [...s].sort().join(""); +export const keepLetters = (s: string) => s.replace(keepLettersRegex, ""); +export const sortLetters = (s: string) => [...s].sort().join(""); -function sum(lst: number[]): number { +export function sum(lst: number[]): number { return lst.reduce((a, b) => a + b); } -function zip(a: A[], b: B[]): [A, B][] { +export function zip(a: A[], b: B[]): [A, B][] { return a.map((e, i) => [e, b[i]]); } -function l1Norm(a: number[]): number { - return sum(a.map((x) => Math.abs(x))); -} - -function l2Norm(a: number[]): number { +export function l2Norm(a: number[]): number { return sum(a.map((x) => Math.pow(x, 2))); } -function dot(a: number[], b: number[]): number { +export function dot(a: number[], b: number[]): number { return sum(zip(a, b).map(([a, b]) => a * b)); } -function cosineSimilarity(a: number[], b: number[]): number { +export function cosineSimilarity(a: number[], b: number[]): number { return dot(a, b) / (l2Norm(a) * l2Norm(b)); } -function wordToVec(word: string) { +export function wordToVec(word: string) { const wordNormalized = unidecode(word); const wordFilter = keepLetters(wordNormalized.toLowerCase()); diff --git a/backend/src/models/ChangePasswordReq.ts b/backend/src/models/ChangePasswordReq.ts index 3245bc51..04665b5c 100644 --- a/backend/src/models/ChangePasswordReq.ts +++ b/backend/src/models/ChangePasswordReq.ts @@ -1,5 +1,24 @@ -export interface ChangePasswordReq { - old_password: string; - new_password: string; - repeat_password: string; -} +import { z } from "zod"; +import { userPasswordSchema } from "./User.js"; + +type ChangePasswordReq = + | { + old_password: string; + new_password: string; + repeat_password: string; + } + | {}; + +export const changePasswordReqSchema: z.ZodType = z + .object({ + old_password: userPasswordSchema, + new_password: userPasswordSchema, + repeat_password: z.string(), + }) + .refine((data) => data.new_password === data.repeat_password, { + message: "Passwords don't match", + path: ["repeat_password"], + }) + .or(z.object({})); + +export default ChangePasswordReq; diff --git a/backend/src/models/NativeUser.ts b/backend/src/models/NativeUser.ts index 3eb4d599..471733c7 100644 --- a/backend/src/models/NativeUser.ts +++ b/backend/src/models/NativeUser.ts @@ -1,5 +1,11 @@ +import { ZodType, z } from "zod"; + type NativeUser = { password: string; }; +export const nativeUserSchema = z.object({ + password: z.string().min(8).max(32), +}) satisfies ZodType; + export default NativeUser; diff --git a/backend/src/models/Response.ts b/backend/src/models/Response.ts index 8fc1863d..d0f80c37 100644 --- a/backend/src/models/Response.ts +++ b/backend/src/models/Response.ts @@ -8,7 +8,9 @@ export interface CustomResponse extends Response { json: Send; } -export type Errors = Record & { length?: never }; +export type Errors = { + [key: string]: Errors | string; +}; export interface ErrorResponse { status: "error"; diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 6998ba67..836a8ad6 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -1,3 +1,5 @@ +import { ZodType, z } from "zod"; + export default interface User { id: string; first_name: string; @@ -6,3 +8,16 @@ export default interface User { profile_picture: string; mail: string; } + +export const userCountrySchema = z.string().trim().min(2).max(32); + +export const userPasswordSchema = z.string().min(8).max(32); + +export const userSchema = z.object({ + id: z.string().uuid(), + first_name: z.string().trim().min(2).max(32), + last_name: z.string().trim().min(2).max(32), + country: userCountrySchema, + profile_picture: z.string().url(), + mail: z.string().email(), +}) satisfies ZodType; diff --git a/backend/src/models/routes/Page.ts b/backend/src/models/routes/Page.ts new file mode 100644 index 00000000..259afee9 --- /dev/null +++ b/backend/src/models/routes/Page.ts @@ -0,0 +1,15 @@ +import { ZodType, z } from "zod"; + +interface Page { + page: string | number; + maxUsers: string | number; +} + +export const pageSchema = z.object({ + page: z.union([z.number(), z.string()]).pipe(z.coerce.number().min(1)), + maxUsers: z + .union([z.number(), z.string()]) + .pipe(z.coerce.number().min(1).max(32)), +}) satisfies ZodType; + +export default Page; diff --git a/backend/src/models/routes/Search.ts b/backend/src/models/routes/Search.ts new file mode 100644 index 00000000..38b51b71 --- /dev/null +++ b/backend/src/models/routes/Search.ts @@ -0,0 +1,19 @@ +import { ZodType, z } from "zod"; +import Page, { pageSchema } from "./Page.js"; +import { userCountrySchema } from "../User.js"; + +interface Search extends Page { + q: string; + country: string; + userId?: string; +} + +export const searchSchema = z + .object({ + q: z.string().min(0).max(64), + country: z.union([userCountrySchema, z.literal("")]), + userId: z.optional(z.string().uuid()), + }) + .merge(pageSchema) satisfies ZodType; + +export default Search; diff --git a/backend/src/routes/authRoute.ts b/backend/src/routes/authRoute.ts index 7fcb98ab..9af001d9 100644 --- a/backend/src/routes/authRoute.ts +++ b/backend/src/routes/authRoute.ts @@ -15,6 +15,7 @@ import { TokenErrorResponse } from "../types/authResponse.js"; import { OkErrorResponse } from "../types/userResponse.js"; import { leaveMeeting } from "../meetings.js"; import { getDbUser } from "../users.js"; +import { Errors } from "../models/Response.js"; const authRouter = Router(); @@ -51,7 +52,7 @@ authRouter.post("/login", async (req: Request, res: TokenErrorResponse) => { res.json({ status: "ok", token }); } catch (err) { console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); + return res.status(404).json({ status: "error", errors: err as Errors }); } finally { await session.close(); } @@ -74,7 +75,7 @@ authRouter.post( await session.close(); } catch (err) { console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); + return res.status(404).json({ status: "error", errors: err as Errors }); } res.json({ status: "ok" }); diff --git a/backend/src/routes/userFriendsRoute.ts b/backend/src/routes/userFriendsRoute.ts new file mode 100644 index 00000000..a7f6dc55 --- /dev/null +++ b/backend/src/routes/userFriendsRoute.ts @@ -0,0 +1,330 @@ +import { Router, Request, Response } from "express"; +import { Session } from "neo4j-driver"; +import driver from "../driver/driver.js"; +import User from "../models/User.js"; +import { + OkErrorResponse, + FriendsPageErrorResponse, + FriendRequestsPageErrorResponse, + FriendSuggestionsPageErrorResponse, +} from "../types/userResponse.js"; +import { deleteFriend } from "../userFriends.js"; +import { declineFriendRequest } from "../userFriends.js"; +import { acceptFriendRequest } from "../userFriends.js"; +import { sendFriendRequest } from "../userFriends.js"; +import { getFriendSuggestionsCount } from "../userFriends.js"; +import { getFriendSuggestions } from "../userFriends.js"; +import { getFriendRequestsCount } from "../userFriends.js"; +import { getFriendRequests } from "../userFriends.js"; +import { getFriendsCount } from "../userFriends.js"; +import { getFriends } from "../userFriends.js"; +import { userNotFoundRes } from "./usersRoute.js"; +import { Errors } from "../models/Response.js"; +import Page, { pageSchema } from "../models/routes/Page.js"; +import { formatError } from "../misc/formatError.js"; + +const friendsRouter = Router(); + +friendsRouter.get( + "/:userId/friends", + async (req: Request, res: FriendsPageErrorResponse) => { + const userId = req.params.userId; + + const pageParse = pageSchema.safeParse(req.query); + if (!pageParse.success) { + const errors = formatError(pageParse.error); + return res.status(400).json({ status: "error", errors }); + } + + const { page, maxUsers }: Page = pageParse.data; + const maxUsersBig = BigInt(maxUsers); + + const session = driver.session(); + try { + const friends = await getFriends(session, userId, page - 1, maxUsers); + if (friends === null) { + console.log(friends); + return userNotFoundRes(res); + } + + const friendsCount = await getFriendsCount(session, userId); + if (friendsCount === null) { + return userNotFoundRes(res); + } + + const pageCount = Number( + (friendsCount.toBigInt() + maxUsersBig - 1n) / maxUsersBig, + ); + console.log(pageCount); + return res.json({ status: "ok", pageCount, friends }); + } catch (err) { + console.log("Error:", err); + return res.status(404).json({ status: "error", errors: err as Errors }); + } finally { + await session.close(); + } + }, +); + +friendsRouter.get( + "/:userId/friend-requests", + async (req: Request, res: FriendRequestsPageErrorResponse) => { + const userId = req.params.userId; + + const pageParse = pageSchema.safeParse(req.query); + if (!pageParse.success) { + const errors = formatError(pageParse.error); + return res.status(400).json({ status: "error", errors }); + } + + const { page, maxUsers }: Page = pageParse.data; + const maxUsersBig = BigInt(maxUsers); + + const session = driver.session(); + try { + const friendRequests = await getFriendRequests( + session, + userId, + page - 1, + maxUsers, + ); + if (friendRequests === null) { + return userNotFoundRes(res); + } + + const friendRequestsCount = await getFriendRequestsCount(session, userId); + if (friendRequestsCount === null) { + return userNotFoundRes(res); + } + + const pageCount = Number( + (friendRequestsCount.toBigInt() + maxUsersBig - 1n) / maxUsersBig, + ); + return res.json({ status: "ok", pageCount, friendRequests }); + } catch (err) { + console.log("Error:", err); + return res.status(404).json({ status: "error", errors: err as Errors }); + } finally { + await session.close(); + } + }, +); + +friendsRouter.get( + "/:userId/friend-suggestions", + async (req: Request, res: FriendSuggestionsPageErrorResponse) => { + const userId = req.params.userId; + + const pageParse = pageSchema.safeParse(req.query); + if (!pageParse.success) { + const errors = formatError(pageParse.error); + return res.status(400).json({ status: "error", errors }); + } + + const { page, maxUsers }: Page = pageParse.data; + const maxUsersBig = BigInt(maxUsers); + + const session = driver.session(); + try { + const friendSuggestions = await getFriendSuggestions( + session, + userId, + page - 1, + maxUsers, + ); + if (friendSuggestions === null) { + return userNotFoundRes(res); + } + + const friendSuggestionsCount = await getFriendSuggestionsCount( + session, + userId, + ); + if (friendSuggestionsCount === null) { + return userNotFoundRes(res); + } + + const pageCount = Number( + (friendSuggestionsCount.toBigInt() + maxUsersBig - 1n) / maxUsersBig, + ); + return res.json({ status: "ok", pageCount, friendSuggestions }); + } catch (err) { + console.log("Error:", err); + return res.status(404).json({ status: "error", errors: err as Errors }); + } finally { + await session.close(); + } + }, +); + +friendsRouter.post( + "/:userId1/send-friend-request/:userId2", + async (req: Request, res: OkErrorResponse) => { + const session = driver.session(); + const userId1 = req.params.userId1; + const userId2 = req.params.userId2; + + try { + const requestResult = await sendFriendRequest(session, userId1, userId2); + if (!requestResult.success) { + const { firstUserExists, secondUserExists } = requestResult; + const errors: Errors = {}; + + if (!firstUserExists) { + errors["userId1"] = "not found"; + } + + if (!secondUserExists) { + errors["userId2"] = "not found"; + } + + return res.status(400).json({ status: "error", errors }); + } + + return res.json({ status: "ok" }); + } catch (err) { + console.log("Error:", err); + return res.status(404).json({ status: "error", errors: err as Errors }); + } finally { + session.close(); + } + }, +); + +friendsRouter.post( + "/:userId1/accept-friend-request/:userId2", + async (req: Request, res: OkErrorResponse) => { + const session = driver.session(); + const userId1 = req.params.userId1; + const userId2 = req.params.userId2; + + try { + const requestResult = await acceptFriendRequest( + session, + userId1, + userId2, + ); + if (!requestResult.success) { + const { + firstUserExists, + secondUserExists, + sentInvite, + alreadyFriends, + } = requestResult; + const errors: Errors = {}; + + if (!firstUserExists) { + errors["userId1"] = "not found"; + } + + if (!secondUserExists) { + errors["userId2"] = "not found"; + } + + if (firstUserExists && secondUserExists && !sentInvite) { + errors["userId1"] = "not invited"; + } + + if (alreadyFriends) { + errors["userId1"] = "already friends"; + } + + return res.status(400).json({ status: "error", errors }); + } + + return res.json({ status: "ok" }); + } catch (err) { + console.log("Error:", err); + return res.status(404).json({ status: "error", errors: err as Errors }); + } finally { + session.close(); + } + }, +); + +friendsRouter.post( + "/:userId1/decline-friend-request/:userId2", + async (req: Request, res: OkErrorResponse) => { + const session = driver.session(); + const userId1 = req.params.userId1; + const userId2 = req.params.userId2; + + try { + const requestResult = await declineFriendRequest( + session, + userId1, + userId2, + ); + if (!requestResult.success) { + const { firstUserExists, secondUserExists, wasFriend, wasInvited } = + requestResult; + const errors: Errors = {}; + + if (!firstUserExists) { + errors["userId1"] = "not found"; + } + + if (!secondUserExists) { + errors["userId2"] = "not found"; + } + + if (firstUserExists && secondUserExists) { + if (wasFriend) { + errors["userId1"] = "already friends"; + } + if (!wasInvited) { + errors["userId1"] = "not invited"; + } + } + return res.status(400).json({ status: "error", errors }); + } + + return res.json({ status: "ok" }); + } catch (err) { + console.log("Error:", err); + return res.status(404).json({ status: "error", errors: err as Errors }); + } finally { + session.close(); + } + }, +); + +friendsRouter.delete( + "/:userId1/delete-friend/:userId2", + async (req: Request, res: OkErrorResponse) => { + const session = driver.session(); + const userId1 = req.params.userId1; + const userId2 = req.params.userId2; + + try { + const requestResult = await deleteFriend(session, userId1, userId2); + if (!requestResult.success) { + const { firstUserExists, secondUserExists, wasFriend } = requestResult; + const errors: Errors = {}; + + if (!firstUserExists) { + errors["userId1"] = "not found"; + } + + if (!secondUserExists) { + errors["userId2"] = "not found"; + } + + if (firstUserExists && secondUserExists && !wasFriend) { + errors["userId2"] = "not a friend"; + } + + return res.status(400).json({ status: "error", errors }); + } + + return res.json({ status: "ok" }); + } catch (err) { + console.log("Error:", err); + return res.status(404).json({ status: "error", errors: err as Errors }); + } finally { + session.close(); + } + }, +); + +export default friendsRouter; diff --git a/backend/src/routes/usersFriendsRoute.ts b/backend/src/routes/usersFriendsRoute.ts deleted file mode 100644 index 6c2c42ac..00000000 --- a/backend/src/routes/usersFriendsRoute.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { Router, Request, Response } from "express"; -import { Session } from "neo4j-driver"; -import driver from "../driver/driver.js"; -import User from "../models/User.js"; -import { - OkErrorResponse, - FriendsErrorResponse, - FriendsPageErrorResponse, - FriendRequestsPageErrorResponse, - FriendSuggestionsPageErrorResponse, -} from "../types/userResponse.js"; -import { - getFriendRequests, - getFriendSuggestions, - getFriends, -} from "../users.js"; -import { userNotFoundRes } from "./usersRoute.js"; -import { verifyPageQuery } from "../misc/verifyRequest.js"; - -const friendshipRouter = Router(); - -async function userExists( - session: Session, - res: Response, - userId: string, -): Promise { - const userExistsResult = await session.run( - `MATCH (u:User {id: $userId}) RETURN u`, - { userId }, - ); - - if (userExistsResult.records.length === 0) { - await session.close(); - const json = { status: "error", errors: { id: "not found" } } as const; - return res.status(404).json(json); - } - - return userExistsResult.records[0].get("u").properties as User; -} - -friendshipRouter.get( - "/:userId/friends", - async (req: Request, res: FriendsPageErrorResponse) => { - const userId = req.params.userId; - - const verify = verifyPageQuery(req.query as any); - if (!verify.valid) { - return res.status(400).json({ status: "error", errors: verify.errors }); - } - - const { page, maxUsers } = verify.verified; - - const session = driver.session(); - try { - const friends = await getFriends(session, userId, page - 1, maxUsers); - if (friends === null) { - return userNotFoundRes(res); - } - - return res.json({ status: "ok", pageCount: 10, friends }); - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); - } finally { - await session.close(); - } - }, -); - -friendshipRouter.get( - "/:userId/friend-requests", - async (req: Request, res: FriendRequestsPageErrorResponse) => { - const userId = req.params.userId; - - const verify = verifyPageQuery(req.query as any); - if (!verify.valid) { - return res.status(400).json({ status: "error", errors: verify.errors }); - } - - const { page, maxUsers } = verify.verified; - - const session = driver.session(); - try { - const friendRequests = await getFriendRequests( - session, - userId, - page - 1, - maxUsers, - ); - if (friendRequests === null) { - return userNotFoundRes(res); - } - - return res.json({ status: "ok", pageCount: 10, friendRequests }); - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); - } finally { - await session.close(); - } - }, -); - -friendshipRouter.get( - "/:userId/friend-suggestions", - async (req: Request, res: FriendSuggestionsPageErrorResponse) => { - const userId = req.params.userId; - - const verify = verifyPageQuery(req.query as any); - if (!verify.valid) { - return res.status(400).json({ status: "error", errors: verify.errors }); - } - - const { page, maxUsers } = verify.verified; - - const session = driver.session(); - try { - const friendSuggestions = await getFriendSuggestions( - session, - userId, - page - 1, - maxUsers, - ); - if (friendSuggestions === null) { - return userNotFoundRes(res); - } - - return res.json({ status: "ok", pageCount: 10, friendSuggestions }); - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); - } finally { - await session.close(); - } - }, -); - -friendshipRouter.delete( - "/:userId1/remove/:userId2", - async (req: Request, res: OkErrorResponse) => { - try { - const session = driver.session(); - const userId1 = req.params.userId1; - const userId2 = req.params.userId2; - - const user = await userExists(session, res, userId1); - if ("json" in user) { - await session.close(); - return res; - } - - // Deletes all types of relations - await session.run( - `MATCH (a:User {id: $userId1})-[r:IS_FRIENDS_WITH]-(b:User {id: $userId2}) - DELETE r`, - { userId1, userId2 }, - ); - - await session.run( - `MATCH (a:User {id: $userId1})-[r:SENT_INVITE_TO]-(b:User {id: $userId2}) - DELETE r`, - { userId1, userId2 }, - ); - await session.close(); - - return res.json({ status: "ok" }); - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); - } - }, -); - -friendshipRouter.post( - "/:userId1/add/:userId2", - async (req: Request, res: FriendsErrorResponse) => { - try { - const session = driver.session(); - const userId1 = req.params.userId1; - const userId2 = req.params.userId2; - - const user = await userExists(session, res, userId1); - if ("json" in user) { - await session.close(); - return res; - } - - const friendRequest = await session.run( - `MATCH (a:User {id: $userId1}), (b:User {id: $userId2}) - MERGE (a)-[:SENT_INVITE_TO]->(b)`, - { userId1, userId2 }, - ); - await session.close(); - - const friends = friendRequest.records.map((f) => f.get("f").properties); - return res.json({ status: "ok", friends }); - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); - } - }, -); - -friendshipRouter.post( - "/:userId1/accept/:userId2", - async (req: Request, res: OkErrorResponse) => { - try { - const session = driver.session(); - const userId1 = req.params.userId1; - const userId2 = req.params.userId2; - - const user = await userExists(session, res, userId1); - if ("json" in user) { - await session.close(); - return res; - } - - // Delete every SENT_INVITE_TO between 2 users - await session.run( - `MATCH (a:User {id: $userId1})-[r:SENT_INVITE_TO]-(b:User {id: $userId2}) - DELETE r`, - { userId1, userId2 }, - ); - - // Add IS_FRIENDS_WITH both ways - await session.run( - `MATCH (a:User {id: $userId1}), (b:User {id: $userId2}) - MERGE (a)-[:IS_FRIENDS_WITH]->(b) - MERGE (b)-[:IS_FRIENDS_WITH]->(a)`, - { userId1, userId2 }, - ); - - await session.close(); - - return res.json({ status: "ok" }); - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); - } - }, -); - -export default friendshipRouter; diff --git a/backend/src/routes/usersRoute.ts b/backend/src/routes/usersRoute.ts index 44e4d30d..9fe8b1ad 100644 --- a/backend/src/routes/usersRoute.ts +++ b/backend/src/routes/usersRoute.ts @@ -8,7 +8,7 @@ import { UsersErrorResponse, UsersSearchErrorResponse, } from "../types/userResponse.js"; -import usersFriendsRoute from "./usersFriendsRoute.js"; +import usersFriendsRoute from "./userFriendsRoute.js"; import { getAllUsers, searchUser as searchUsers, @@ -21,10 +21,16 @@ import { getDbUser, changePassword, getUsersCount, + registerUserSchema, + RegisterUser, + updateUserSchema, + UpdateUser, } from "../users.js"; import DbUser from "../models/DbUser.js"; -import { ChangePasswordReq } from "../models/ChangePasswordReq.js"; -import { verifySearchQuery } from "../misc/verifyRequest.js"; +import { changePasswordReqSchema } from "../models/ChangePasswordReq.js"; +import { formatError } from "../misc/formatError.js"; +import { Errors } from "../models/Response.js"; +import { searchSchema } from "../models/routes/Search.js"; const usersRouter = Router(); @@ -42,7 +48,7 @@ usersRouter.get("/", async (_req: Request, res: UsersErrorResponse) => { return res.json({ status: "ok", users }); } catch (err) { console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); + return res.status(404).json({ status: "error", errors: err as Errors }); } finally { await session.close(); } @@ -59,12 +65,13 @@ usersRouter.post( usersRouter.get( "/search", async (req: Request, res: UsersSearchErrorResponse) => { - const verify = verifySearchQuery(req.query as any); - if (!verify.valid) { - return res.status(400).json({ status: "error", errors: verify.errors }); + const searchParse = searchSchema.safeParse(req.query); + if (!searchParse.success) { + const errors = formatError(searchParse.error); + return res.status(400).json({ status: "error", errors }); } - const { page, maxUsers, q: searchTerm, country } = verify.verified; + const { page, maxUsers, q: searchTerm, country, userId } = searchParse.data; const maxUsersBig = BigInt(maxUsers); const session = driver.session(); @@ -75,6 +82,7 @@ usersRouter.get( country, page - 1, maxUsers, + userId, ); if (userScores === null) { return res @@ -82,16 +90,14 @@ usersRouter.get( .json({ status: "error", errors: { searchTerm: "incorrect" } }); } - const usersCount = await getUsersCount(session); - const pageCount = Number( - (usersCount.toBigInt() + maxUsersBig - 1n) / maxUsersBig, - ); + const usersCount = (await getUsersCount(session)).toBigInt() - 1n; + const pageCount = Number((usersCount + maxUsersBig - 1n) / maxUsersBig); const users = userScores.map((userScore) => userScore[0]); return res.json({ status: "ok", pageCount, users }); } catch (err) { console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); + return res.status(404).json({ status: "error", errors: err as Errors }); } finally { await session.close(); } @@ -124,7 +130,7 @@ usersRouter.get("/:userId", async (req: Request, res: UserErrorResponse) => { return res.json({ status: "ok", user }); } catch (err) { console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); + return res.status(404).json({ status: "error", errors: err as Errors }); } finally { await session.close(); } @@ -180,16 +186,22 @@ usersRouter.put("/meetings/:meetingId", async (req: Request, res) => { }); usersRouter.post("/", async (req: Request, res: UserErrorResponse) => { - // TODO: verify user fields - const { issuer, ...newUserProps } = req.body; + const userParse = registerUserSchema.safeParse(req.body); + if (!userParse.success) { + const errors = formatError(userParse.error); + return res.status(400).json({ status: "error", errors }); + } + + const parsedUser: RegisterUser = userParse.data; + const { issuer } = req.body; const session = driver.session(); try { let user: UserCreateResult; if (issuer) { - user = await registerUser(newUserProps); + user = await registerUser(session, parsedUser); } else { - user = await createUser(session, newUserProps); + user = await createUser(session, parsedUser); } if ("errors" in user) { @@ -200,20 +212,25 @@ usersRouter.post("/", async (req: Request, res: UserErrorResponse) => { return res.json({ status: "ok", user }); } catch (err) { console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); + return res.status(404).json({ status: "error", errors: err as Errors }); } finally { await session.close(); } }); usersRouter.put("/:userId", async (req: Request, res: OkErrorResponse) => { - // TODO: verify user fields + const userParse = updateUserSchema.safeParse(req.body); + if (!userParse.success) { + const errors = formatError(userParse.error); + return res.status(400).json({ status: "error", errors }); + } + + const parsedUser: UpdateUser = userParse.data; const userId = req.params.userId; - const newUserProps = req.body; const session = driver.session(); try { - const newUser = await updateUser(session, userId, newUserProps); + const newUser = await updateUser(session, userId, parsedUser); if (!newUser) { return userNotFoundRes(res); } @@ -221,7 +238,7 @@ usersRouter.put("/:userId", async (req: Request, res: OkErrorResponse) => { return res.json({ status: "ok" }); } catch (err) { console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); + return res.status(404).json({ status: "error", errors: err as Errors }); } finally { await session.close(); } @@ -233,64 +250,46 @@ usersRouter.post( async (req: JWTRequest, res: AuthOkErrorResponse) => { const userId = req.params.userId; - const passwords: ChangePasswordReq = req.body; - const { old_password, new_password, repeat_password } = passwords; + const passwordsParse = changePasswordReqSchema.safeParse(req.body); + if (!passwordsParse.success) { + const errors = formatError(passwordsParse.error); + return res.status(400).json({ status: "error", errors }); + } + + const parsedPasswords = passwordsParse.data; const session = driver.session(); try { - const user = await getDbUser(session, { id: userId }); - - if (!user) { - return userNotFoundRes(res); - } - - if ("password" in user) { - const errors: Record = {}; - - if (!old_password) { - errors["old_password"] = "is empty"; - } + const changePasswordResult = await changePassword( + session, + userId, + parsedPasswords, + req.token, + ); - if (!new_password) { - errors["new_password"] = "is empty"; - } + if (!changePasswordResult.success) { + const { userExists, isUserIssued, passwordCorrect } = + changePasswordResult; - if (!repeat_password) { - errors["repeat_password"] = "is empty"; + if (!userExists) { + return userNotFoundRes(res); } - for (const _ in errors) { - return res.status(400).json({ status: "error", errors }); - } - } else { - if (!req.token) { + if (isUserIssued) { return res.status(403).json({ status: "forbidden" }); } - } - const changeStatus = await changePassword( - session, - user, - old_password, - new_password, - repeat_password, - ); - - if (changeStatus == "verify") { - return res - .status(400) - .json({ status: "error", errors: { "old_password": "incorrect" } }); - } else if (changeStatus == "repeat") { - return res.status(400).json({ - status: "error", - errors: { "repeat_password": "passwords don't match" }, - }); + if (!passwordCorrect) { + return res + .status(400) + .json({ status: "error", errors: { "old_password": "incorrect" } }); + } } return res.json({ status: "ok" }); } catch (err) { console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); + return res.status(404).json({ status: "error", errors: err as Errors }); } finally { await session.close(); } @@ -310,7 +309,7 @@ usersRouter.delete("/:userId", async (req: Request, res: OkErrorResponse) => { return res.json({ status: "ok" }); } catch (err) { console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); + return res.status(404).json({ status: "error", errors: err as Errors }); } finally { await session.close(); } diff --git a/backend/src/socketServer.ts b/backend/src/socketServer.ts index 2a775c7c..5b42ef8f 100644 --- a/backend/src/socketServer.ts +++ b/backend/src/socketServer.ts @@ -7,7 +7,7 @@ import { disconnectFromSocket, getAllSockets, } from "./sockets.js"; -import { isFriend } from "./users.js"; +import { isFriend } from "./userFriends.js"; import Meeting from "./models/Meeting.js"; import { createMeeting, diff --git a/backend/src/userFriends.ts b/backend/src/userFriends.ts new file mode 100644 index 00000000..30ae7f2c --- /dev/null +++ b/backend/src/userFriends.ts @@ -0,0 +1,376 @@ +import neo4j, { Session } from "neo4j-driver"; +import User from "./models/User.js"; +import { filterUser, getUser } from "./users.js"; + +export async function getFriends( + session: Session, + userId: string, + pageIndex: number, + pageSize: number, +): Promise { + const user = await getUser(session, { id: userId }); + if (!user) { + return null; + } + + const querySkip = neo4j.int(pageIndex * pageSize); + const queryLimit = neo4j.int(pageSize); + + const friendRequest = await session.run( + `MATCH (u:User {id: $userId})-[:IS_FRIENDS_WITH]-(f:User) + RETURN DISTINCT f + SKIP $querySkip + LIMIT $queryLimit`, + { userId, querySkip, queryLimit }, + ); + + const friends = friendRequest.records.map((f) => + filterUser(f.get("f").properties), + ); + return friends; +} + +export async function getFriendsCount( + session: Session, + userId: string, +): Promise { + const user = await getUser(session, { id: userId }); + if (!user) { + return null; + } + + const friendsCountRequest = await session.run( + `MATCH (u:User {id: $userId})-[:IS_FRIENDS_WITH]-(f:User) + RETURN count(DISTINCT f)`, + { userId }, + ); + + const friendsCount = friendsCountRequest.records[0].get(0); + return friendsCount; +} + +export async function isFriend( + session: Session, + firstUserId: string, + secondUserId: string, +) { + try { + const request = await session.run( + ` + MATCH (u1:User {id: $firstUserId}) + MATCH (u2:User {id: $secondUserId}) + RETURN exists((u1)-[:IS_FRIENDS_WITH]-(u2)) + `, + { firstUserId, secondUserId }, + ); + + return request.records?.[0].get(0); + } catch (err) { + return false; + } +} + +export async function getFriendRequests( + session: Session, + userId: string, + pageIndex: number, + pageSize: number, +): Promise { + const user = await getUser(session, { id: userId }); + if (!user) { + return null; + } + + const querySkip = neo4j.int(pageIndex * pageSize); + const queryLimit = neo4j.int(pageSize); + + const friendRequestsRequest = await session.run( + `MATCH (u:User {id: $userId})<-[:SENT_INVITE_TO]-(f:User) + WITH f ORDER BY f.last_name, f.first_name + RETURN DISTINCT f + SKIP $querySkip + LIMIT $queryLimit`, + { userId, querySkip, queryLimit }, + ); + + const friends = friendRequestsRequest.records.map((f) => + filterUser(f.get("f").properties), + ); + return friends; +} + +export async function getFriendRequestsCount( + session: Session, + userId: string, +): Promise { + const user = await getUser(session, { id: userId }); + if (!user) { + return null; + } + + const friendRequestsCountRequest = await session.run( + `MATCH (u:User {id: $userId})<-[:SENT_INVITE_TO]-(f:User) + RETURN count(DISTINCT f)`, + { userId }, + ); + + const friendRequestsCount = friendRequestsCountRequest.records[0].get(0); + return friendRequestsCount; +} + +export async function getFriendSuggestions( + session: Session, + userId: string, + pageIndex: number, + pageSize: number, +): Promise { + const user = await getUser(session, { id: userId }); + if (!user) { + return null; + } + + const querySkip = neo4j.int(pageIndex * pageSize); + const queryLimit = neo4j.int(pageSize); + + const friendSuggestionsRequest = await session.run( + `MATCH (u:User {id: $userId})-[:IS_FRIENDS_WITH]-(f:User)-[:IS_FRIENDS_WITH]-(s:User) + WHERE NOT (u)-[:IS_FRIENDS_WITH]-(s) AND s.id <> $userId + RETURN DISTINCT s + SKIP $querySkip + LIMIT $queryLimit`, + { userId, querySkip, queryLimit }, + ); + + const friends = friendSuggestionsRequest.records.map((s) => + filterUser(s.get("s").properties), + ); + return friends; +} + +export async function getFriendSuggestionsCount( + session: Session, + userId: string, +): Promise { + const user = await getUser(session, { id: userId }); + if (!user) { + return null; + } + + const friendRequestsCountRequest = await session.run( + `MATCH (u:User {id: $userId})-[:IS_FRIENDS_WITH]-(f:User)-[:IS_FRIENDS_WITH]-(s:User) + WHERE NOT (u)-[:IS_FRIENDS_WITH]-(s) AND s.id <> $userId + RETURN count(DISTINCT s)`, + { userId }, + ); + + const friendRequestsCount = friendRequestsCountRequest.records[0].get(0); + return friendRequestsCount; +} + +export type CheckFriendsResult = { + firstUserExists: boolean; + secondUserExists: boolean; + areFriends: boolean; +}; + +export async function checkFriends( + session: Session, + userId1: string, + userId2: string, +): Promise { + const firstUser = await getUser(session, { id: userId1 }); + const secondUser = await getUser(session, { id: userId2 }); + + const firstUserExists = firstUser !== null; + const secondUserExists = secondUser !== null; + + if (!firstUserExists || !secondUserExists) { + return { firstUserExists, secondUserExists, areFriends: false }; + } + + const areFriends = await isFriend(session, userId1, userId2); + return { firstUserExists, secondUserExists, areFriends }; +} + +export type SendFriendInviteResult = { + success: boolean; + firstUserExists: boolean; + secondUserExists: boolean; +}; + +export async function sendFriendRequest( + session: Session, + userId1: string, + userId2: string, +): Promise { + const { firstUserExists, secondUserExists, areFriends } = await checkFriends( + session, + userId1, + userId2, + ); + const sameId = userId1 == userId2; + + const success = firstUserExists && secondUserExists && !areFriends && !sameId; + if (!success) { + return { success, firstUserExists, secondUserExists }; + } + + await session.run( + `MATCH (a:User {id: $userId1}), (b:User {id: $userId2}) + MERGE (a)-[:SENT_INVITE_TO]->(b)`, + { userId1, userId2 }, + ); + + return { success, firstUserExists, secondUserExists }; +} + +export type AcceptFriendRequestResult = { + success: boolean; + firstUserExists: boolean; + secondUserExists: boolean; + sentInvite: boolean; + alreadyFriends: boolean; +}; + +export async function acceptFriendRequest( + session: Session, + userId1: string, + userId2: string, +): Promise { + const { + firstUserExists, + secondUserExists, + areFriends: alreadyFriends, + } = await checkFriends(session, userId1, userId2); + + if (!firstUserExists || !secondUserExists || alreadyFriends) { + return { + success: false, + firstUserExists, + secondUserExists, + sentInvite: false, + alreadyFriends, + }; + } + + const acceptInviteRequest = await session.run( + `MATCH (u1:User {id: $userId1})<-[r:SENT_INVITE_TO]-(u2:User {id: $userId2}) + DELETE r + CREATE (u1)-[:IS_FRIENDS_WITH]->(u2) + RETURN true`, + { userId1, userId2 }, + ); + + const records = acceptInviteRequest.records; + const sentInvite = records.length > 0; + + return { + success: sentInvite, + firstUserExists, + secondUserExists, + sentInvite, + alreadyFriends, + }; +} + +export async function addFriend( + session: Session, + userId1: string, + userId2: string, +) { + await session.run( + `MATCH (u1:User {id: $userId1}), (u2:User {id: $userId2}) + MERGE (u1)-[:IS_FRIENDS_WITH]-(u2) + RETURN true`, + { userId1, userId2 }, + ); +} + +export type DeclineFriendRequestResult = { + success: boolean; + firstUserExists: boolean; + secondUserExists: boolean; + wasFriend: boolean; + wasInvited: boolean; +}; + +export async function declineFriendRequest( + session: Session, + userId1: string, + userId2: string, +): Promise { + const { + firstUserExists, + secondUserExists, + areFriends: wasFriend, + } = await checkFriends(session, userId1, userId2); + + if (!firstUserExists || !secondUserExists || wasFriend) { + return { + success: false, + firstUserExists, + secondUserExists, + wasFriend, + wasInvited: false, + }; + } + + const acceptInviteRequest = await session.run( + `MATCH (u1)<-[r:SENT_INVITE_TO]->(u2) + DELETE r + RETURN true`, + { userId1, userId2 }, + ); + + const wasInvited = acceptInviteRequest.records.length > 0; + + return { + success: wasInvited, + firstUserExists, + secondUserExists, + wasFriend, + wasInvited, + }; +} + +export type DeleteFriendResult = { + success: boolean; + firstUserExists: boolean; + secondUserExists: boolean; + wasFriend: boolean; +}; + +export async function deleteFriend( + session: Session, + userId1: string, + userId2: string, +): Promise { + const { + firstUserExists, + secondUserExists, + areFriends: wasFriend, + } = await checkFriends(session, userId1, userId2); + + if (!firstUserExists || !secondUserExists || !wasFriend) { + return { + success: false, + firstUserExists, + secondUserExists, + wasFriend: false, + }; + } + + await session.run( + `MATCH (u1:User {id: $userId1})-[r:IS_FRIENDS_WITH]-(u2:User {id: $userId2}) + DELETE r + RETURN true`, + { userId1, userId2 }, + ); + + return { + success: true, + firstUserExists, + secondUserExists, + wasFriend, + }; +} diff --git a/backend/src/users.ts b/backend/src/users.ts index 6cfc6b9b..9e5674a9 100644 --- a/backend/src/users.ts +++ b/backend/src/users.ts @@ -1,16 +1,19 @@ import neo4j, { Session } from "neo4j-driver"; import { v4 as uuidv4 } from "uuid"; import bcrypt from "bcrypt"; -import User from "./models/User.js"; +import User, { userSchema } from "./models/User.js"; import removeKeys from "./misc/removeKeys.js"; import wordToVec from "./misc/wordToVec.js"; import DbUser from "./models/DbUser.js"; import kcAdminClient from "./kcAdminClient.js"; import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation.js"; -import driver from "./driver/driver.js"; import { Either } from "./misc/Either.js"; -import NativeUser from "./models/NativeUser.js"; +import NativeUser, { nativeUserSchema } from "./models/NativeUser.js"; import ExternalUser from "./models/ExternalUser.js"; +import { ZodType } from "zod"; +import ChangePasswordReq from "./models/ChangePasswordReq.js"; +import jwt from "jsonwebtoken"; +import TokenPayload from "./models/TokenPayload.js"; export const filterUser = (user: DbUser): User => { if ("password" in user) { @@ -45,10 +48,21 @@ function getResponse(e: any): Response | null { return e.response; } -type RegisterUser = Omit & NativeUser; -type CreateUser = Omit & Either; +export type RegisterUser = Omit & NativeUser; +export type CreateUser = Omit & Either; +export type UpdateUser = Partial>; export type UserCreateResult = User | { errors: Record }; +export const registerUserSchema = userSchema + .omit({ id: true }) + .merge(nativeUserSchema) satisfies ZodType; + +export const updateUserSchema = userSchema + .omit({ + id: true, + }) + .partial() satisfies ZodType; + async function createUserQuery( session: Session, userData: DbUser, @@ -88,29 +102,28 @@ export async function createUser( } } - const firstNameEmbedding = wordToVec(userData.first_name); - const lastNameEmbedding = wordToVec(userData.last_name); + const nameEmbeddingResult = await generateNameEmbedding( + userData.first_name, + userData.last_name, + ); + const { success, firstNameCorrect, lastNameCorrect, nameEmbedding } = + nameEmbeddingResult; const errors: Record = {}; - if (firstNameEmbedding.length == 0) { - errors["first_name"] = "incorrect"; - } + if (!success) { + if (!firstNameCorrect) { + errors["first_name"] = "incorrect"; + } - if (lastNameEmbedding.length == 0) { - errors["last_name"] = "incorrect"; - } + if (!lastNameCorrect) { + errors["last_name"] = "incorrect"; + } - for (const _ in errors) { return { errors }; } const id = uuidv4(); - const nameEmbedding = firstNameEmbedding.map((e1, i) => { - const e2 = lastNameEmbedding[i]; - return (e1 + e2) / 2; - }); - let user = { ...userData, id, name_embedding: nameEmbedding } as DbUser; if ("password" in userData) { @@ -123,32 +136,41 @@ export async function createUser( } export async function registerUser( + session: Session, userData: RegisterUser, -): Promise { - const session = driver.session(); - try { - const { id: keycloakId } = await kcAdminClient.users.create( - registerUserToKeycloakUser(userData), - ); - - const dbUserData: CreateUser = { - ...removeKeys(userData, ["password"]), - issuer: "mercury", - issuer_id: keycloakId, - }; +): Promise { + let keycloakId: string = ""; - const user = await createUser(session, dbUserData); - return user; + try { + keycloakId = ( + await kcAdminClient.users.create(registerUserToKeycloakUser(userData)) + ).id; } catch (e) { const response = getResponse(e); - if (response != null && response.status == 409) { - return { errors: { id: "already exists" } }; - } else { + if (response == null || response.status != 409) { throw e; } - } finally { - await session.close(); } + + if (!keycloakId) { + keycloakId = ( + await kcAdminClient.users.find({ email: userData.mail, realm: "mercury" }) + )[0].id!; + } + + const dbUserData: CreateUser = { + ...removeKeys(userData, ["password"]), + issuer: "mercury", + issuer_id: keycloakId, + }; + + await createUser(session, dbUserData); + + const user = await getDbUser(session, { + mail: userData.mail, + issuer: "mercury", + }); + return filterUser(user!); } export async function getUser( @@ -200,6 +222,7 @@ export async function searchUser( country: string, pageIndex: number, pageSize: number, + userId: string = "", ): Promise { const queryElems = neo4j.int((pageIndex + 1) * pageSize); const querySkip = neo4j.int(pageIndex * pageSize); @@ -210,11 +233,11 @@ export async function searchUser( if (!searchTerm) { userRequest = await session.run( `MATCH (u:User) - WHERE $country = "" OR u.country = $country + WHERE ($country = "" OR u.country = $country) AND u.id <> $userId RETURN u as similarUser, 1.0 as score SKIP $querySkip LIMIT $queryLimit`, - { queryElems, country, querySkip, queryLimit }, + { queryElems, country, userId, querySkip, queryLimit }, ); } else { const wordVec = wordToVec(searchTerm); @@ -226,11 +249,11 @@ export async function searchUser( userRequest = await session.run( `CALL db.index.vector.queryNodes('user-names', $queryElems, $wordVec) YIELD node AS similarUser, score - WHERE $country = "" OR similarUser.country = $country + WHERE ($country = "" OR similarUser.country = $country) AND similarUser.id <> $userId RETURN similarUser, score SKIP $querySkip LIMIT $queryLimit`, - { wordVec, queryElems, country, querySkip, queryLimit }, + { wordVec, queryElems, userId, country, querySkip, queryLimit }, ); } @@ -250,6 +273,46 @@ export async function getUsersCount(session: Session): Promise { return usersCount.records[0].get(0); } +export type GenerateNameEmbeddingResult = { + success: boolean; + firstNameCorrect: boolean; + lastNameCorrect: boolean; + nameEmbedding: number[]; +}; + +export async function generateNameEmbedding( + firstName: string, + lastName: string, +): Promise { + const firstNameEmbedding = wordToVec(firstName); + const lastNameEmbedding = wordToVec(lastName); + + const firstNameCorrect = firstNameEmbedding.length > 0; + const lastNameCorrect = lastNameEmbedding.length > 0; + const success = firstNameCorrect && lastNameCorrect; + + if (!success) { + return { + success, + firstNameCorrect, + lastNameCorrect, + nameEmbedding: [], + }; + } + + const nameEmbedding = firstNameEmbedding.map((e1, i) => { + const e2 = lastNameEmbedding[i]; + return (e1 + e2) / 2; + }); + + return { + success, + firstNameCorrect, + lastNameCorrect, + nameEmbedding, + }; +} + export async function updateUser( session: Session, userId: string, @@ -276,7 +339,11 @@ export async function updateUser( } } - const newUser = { ...user, ...newUserProps }; + const { nameEmbedding } = await generateNameEmbedding( + newUserProps.first_name || user.first_name, + newUserProps.last_name || user.last_name, + ); + const newUser = { ...user, ...newUserProps, name_embedding: nameEmbedding }; await session.run(`MATCH (u:User {id: $userId}) SET u=$user`, { userId, user: newUser, @@ -285,18 +352,44 @@ export async function updateUser( return true; } -export type ChangePasswordSuccess = "success"; -export type ChangePasswordError = "verify" | "repeat"; -export type ChangePasswordResult = ChangePasswordSuccess | ChangePasswordError; +export type ChangePasswordResult = { + success: boolean; + userExists: boolean; + isUserIssued: boolean; + passwordCorrect: boolean; +}; export async function changePassword( session: Session, - user: DbUser, - oldPassword: string, - newPassword: string, - repeatPassword: string, + userId: string, + passwords: ChangePasswordReq, + token?: TokenPayload, ): Promise { - if (!("password" in user)) { + const user = await getDbUser(session, { id: userId }); + if (!user) { + return { + success: false, + userExists: false, + isUserIssued: false, + passwordCorrect: false, + }; + } + + const userExists = true; + const isUserIssued = !("password" in user); + + if (isUserIssued) { + const tokenInvalid = { + success: false, + userExists, + isUserIssued, + passwordCorrect: false, + }; + + if (!token) { + return tokenInvalid; + } + if (user.issuer == "mercury") { const keycloakUser = await kcAdminClient.users.findOne({ id: user.issuer_id, @@ -311,29 +404,39 @@ export async function changePassword( requiredActions, }, ); - return "success"; + + return { success: true, userExists, isUserIssued, passwordCorrect: true }; } else { throw new Error("not implemented"); } } - const match: boolean = await bcrypt.compare(oldPassword, user.password); - if (!match) { - return "verify"; + let isEmpty = true; + for (const _ in passwords) { + isEmpty = false; } - if (newPassword != repeatPassword) { - return "repeat"; + if (isEmpty) { + return { success: false, userExists, isUserIssued, passwordCorrect: false }; + } + + const { old_password, new_password } = passwords as any; + + const match: boolean = await bcrypt.compare(old_password, user.password); + if (!match) { + return { success: false, userExists, isUserIssued, passwordCorrect: false }; } - const passwordHashed = await bcrypt.hash(newPassword, 10); + const passwordCorrect = true; + const passwordHashed = await bcrypt.hash(new_password, 10); const updatedUser = { ...user, password: passwordHashed }; await session.run(`MATCH (u:User {id: $userId}) SET u=$user`, { userId: user.id, user: updatedUser, }); - return "success"; + + return { success: true, userExists, isUserIssued, passwordCorrect }; } export async function deleteUser( @@ -356,110 +459,3 @@ export async function deleteUser( return true; } - -export async function getFriends( - session: Session, - userId: string, - pageIndex: number, - pageSize: number, -): Promise { - const user = await getUser(session, { id: userId }); - if (!user) { - return null; - } - - const querySkip = neo4j.int(pageIndex * pageSize); - const queryLimit = neo4j.int(pageSize); - - const friendRequest = await session.run( - `MATCH (u:User {id: $userId})-[:IS_FRIENDS_WITH]-(f:User) - RETURN DISTINCT f - SKIP $querySkip - LIMIT $queryLimit`, - { userId, querySkip, queryLimit }, - ); - - const friends = friendRequest.records.map((f) => - filterUser(f.get("f").properties), - ); - return friends; -} - -export async function isFriend( - session: Session, - firstUserId: string, - secondUserId: string, -) { - try { - const request = await session.run( - ` - MATCH (u1:User {id: $firstUserId}) - MATCH (u2:User {id: $secondUserId}) - RETURN exists((u1)-[:IS_FRIENDS_WITH]-(u2)) - `, - { firstUserId, secondUserId }, - ); - - return request.records.length > 0; - } catch (err) { - return false; - } -} - -export async function getFriendRequests( - session: Session, - userId: string, - pageIndex: number, - pageSize: number, -): Promise { - const user = await getUser(session, { id: userId }); - if (!user) { - return null; - } - - const querySkip = neo4j.int(pageIndex * pageSize); - const queryLimit = neo4j.int(pageSize); - - const friendRequestsRequest = await session.run( - `MATCH (u:User {id: $userId})<-[:SENT_INVITE_TO]-(f:User) - WITH f ORDER BY f.last_name, f.first_name - RETURN DISTINCT f - SKIP $querySkip - LIMIT $queryLimit`, - { userId, querySkip, queryLimit }, - ); - - const friends = friendRequestsRequest.records.map((f) => - filterUser(f.get("f").properties), - ); - return friends; -} - -export async function getFriendSuggestions( - session: Session, - userId: string, - pageIndex: number, - pageSize: number, -): Promise { - const user = await getUser(session, { id: userId }); - if (!user) { - return null; - } - - const querySkip = neo4j.int(pageIndex * pageSize); - const queryLimit = neo4j.int(pageSize); - - const friendSuggestionsRequest = await session.run( - `MATCH (u:User {id: $userId})-[:IS_FRIENDS_WITH]-(f:User)-[:IS_FRIENDS_WITH]-(s:User) - WHERE NOT (u)-[:IS_FRIENDS_WITH]-(s) AND s.id <> $userId - RETURN DISTINCT s - SKIP $querySkip - LIMIT $queryLimit`, - { userId, querySkip, queryLimit }, - ); - - const friends = friendSuggestionsRequest.records.map((s) => - filterUser(s.get("s").properties), - ); - return friends; -} diff --git a/backend/test/chat.test.ts b/backend/test/chat.test.ts new file mode 100644 index 00000000..c81ba53d --- /dev/null +++ b/backend/test/chat.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from "vitest"; +import { fetchData } from "../src/misc/fetchData.js"; +import User from "../src/models/User.js"; + +let userId1: string = ""; +let userId2: string = ""; + +const getUsers = async () => { + const response = await fetchData(`http://localhost:5000/users`, "GET", {}); + userId1 = response.users.find( + (user: User) => user.mail === "bconford2@wikimedia.org", + ).id; + userId2 = response.users.find( + (user: User) => user.mail === "cruckman3@archive.org", + ).id; +}; + +await getUsers(); + +test("Get chat without ids", async () => { + const response = await fetch(`http://localhost:5000/chat`); + const data = response.headers.get("content-type"); + + expect(response.status).toBe(404); + expect(data).toBe("text/html; charset=utf-8"); +}); + +test("Get chat with ids", async () => { + const response = await fetchData( + `http://localhost:5000/chat/${userId1}/${userId2}`, + "GET", + {}, + ); + + const { status, messages } = response; + + expect(status).toBe("ok"); + expect(messages).toBeDefined(); + expect(messages.length).toBe(0); +}); diff --git a/backend/test/unit.test.ts b/backend/test/unit.test.ts new file mode 100644 index 00000000..031aba31 --- /dev/null +++ b/backend/test/unit.test.ts @@ -0,0 +1,171 @@ +import { expect, test } from "vitest"; +import { + letterToKb, + wordDifference, + cosineSimilarity, + sortLetters, + keepLetters, + lerp, + wordVecInterp, + sum, + zip, + l2Norm, + dot, +} from "../src/misc/wordToVec.js"; +import removeKeys from "../src/misc/removeKeys.js"; + +import wordToVec from "../src/misc/wordToVec.js"; + +test("Letter to Kb", async () => { + expect(letterToKb("Q")).toStrictEqual(-1); + expect(letterToKb("L")).toStrictEqual(1); + expect(letterToKb("G")).toStrictEqual(-letterToKb("Y")); + expect(letterToKb("R") + letterToKb("J")).toStrictEqual(0); + expect(letterToKb("Q") + letterToKb("W")).toStrictEqual(-1.92); + expect(letterToKb("a")).toBeUndefined(); + expect(letterToKb("1")).toBeUndefined(); + expect(letterToKb(",")).toBeUndefined(); + expect(letterToKb(" ")).toBeUndefined(); +}); + +test("Linear interpolation", async () => { + expect(lerp(1, 2, 0)).toStrictEqual(1); + expect(lerp(1, 2, 1)).toStrictEqual(2); + expect(lerp(1, 2, 0.5)).toStrictEqual(1.5); + expect(lerp(2, 1, 0.5)).toStrictEqual(1.5); + expect(lerp(0, 10, 2)).toStrictEqual(20); + expect(lerp(0, 10, -1)).toStrictEqual(-10); +}); + +test("WordVecInterp", async () => { + expect(wordVecInterp("Text", 0)).toStrictEqual([]); + expect(wordVecInterp("A", 1)).toStrictEqual([-0.84]); + expect(wordVecInterp("ABC", 2)).toStrictEqual([-0.84, -0.28]); + expect(wordVecInterp("ABC", 4)).toStrictEqual([ + -0.84, -0.14666666666666672, 0.04000000000000001, -0.28, + ]); + expect(wordVecInterp("0", 1)).toStrictEqual([undefined]); + expect(wordVecInterp("00", 1)).toStrictEqual([undefined]); + expect(wordVecInterp("00", 3)).toStrictEqual([NaN, NaN, undefined]); +}); + +test("Sum", async () => { + expect(sum([1])).toStrictEqual(1); + expect(sum([1, 2, 3, 4, 5])).toStrictEqual(15); + expect(sum([2, -4, 6, -8, 10])).toStrictEqual(6); + expect(sum([-0.5, 0.3, -0.3])).toStrictEqual(-0.5); + expect(sum([1, NaN, 2])).toBeNaN(); + expect(sum([1, Infinity, 2])).toStrictEqual(Infinity); +}); + +test("Zip", async () => { + expect(zip([], [])).toStrictEqual([]); + expect(zip(["A"], [])).toStrictEqual([["A", undefined]]); + expect(zip([], [-5])).toStrictEqual([]); + expect(zip(["a", "b", "c"], [1, 2, 3])).toStrictEqual([ + ["a", 1], + ["b", 2], + ["c", 3], + ]); + expect(zip(["a", "b"], [1, 2, 3])).toStrictEqual([ + ["a", 1], + ["b", 2], + ]); + expect(zip(["a", "b", "c"], [1, 2])).toStrictEqual([ + ["a", 1], + ["b", 2], + ["c", undefined], + ]); +}); + +test("L2Norm", async () => { + expect(l2Norm([8])).toStrictEqual(64); + expect(l2Norm([2, 3])).toStrictEqual(13); + expect(l2Norm([2, 3, 5])).toStrictEqual(38); + expect(l2Norm([-7, -2])).toStrictEqual(53); + expect(l2Norm([-0.5, 1.5])).toStrictEqual(2.5); + expect(l2Norm([4, NaN, 6])).toBeNaN(); + expect(l2Norm([4, Infinity, 6])).toStrictEqual(Infinity); +}); + +test("Dot", async () => { + expect(dot([6], [5])).toStrictEqual(30); + expect(dot([1, 2, 3], [1, 2, 3])).toStrictEqual(14); + expect(dot([1, 2, 3], [4, 5])).toBeNaN(); + expect(dot([0.2, 0.3], [0.7, 1.2])).toStrictEqual(0.5); + expect(dot([2, -5], [3, 8])).toStrictEqual(-34); + expect(dot([4, NaN, 6], [5, NaN, 2])).toBeNaN(); + expect(dot([1, Infinity, 8], [9, Infinity, 3])).toStrictEqual(Infinity); +}); + +test("Word Difference", async () => { + expect(wordDifference("a", "b")).toStrictEqual(-5.952380952380951); + expect(wordDifference("Adam", "Padam")).toBeGreaterThan(1); + expect(wordDifference("John", "John")).toBeGreaterThan(2.9); + expect( + wordDifference("Really long text idk", "Dinosaurs with mexican hats"), + ).toBeNaN(); +}); + +test("Keep Letters", async () => { + expect(keepLetters("Text with spaces")).toStrictEqual("ext with spaces"); + expect(keepLetters("S")).toStrictEqual(""); + expect(keepLetters("text")).toStrictEqual("text"); + expect(keepLetters("%$%$$%")).toStrictEqual("$%$$%"); +}); + +test("Sort Letters", async () => { + expect(sortLetters("Constantinople")).toStrictEqual("Caeilnnnoopstt"); + expect(sortLetters(" ")).toStrictEqual(" "); + expect(sortLetters("1611")).toStrictEqual("1116"); + expect(sortLetters("$#%")).toStrictEqual("#$%"); +}); + +test("Cosine similarity equal to word Difference", async () => { + expect(cosineSimilarity(wordToVec("a"), wordToVec("b"))).toStrictEqual( + wordDifference("a", "b") / 64, + ); + expect( + cosineSimilarity( + wordToVec("Really long text"), + wordToVec("also really long text"), + ), + ).toStrictEqual( + wordDifference("Really long text", "also really long text") / 64, + ); +}); + +test("Word to Vec", async () => { + expect(wordToVec("")).toStrictEqual([]); + expect(wordToVec("1")).toStrictEqual([]); + expect(wordToVec(" ")).toStrictEqual([]); + expect(wordToVec("$")).toStrictEqual([]); + expect(wordToVec("A")[0]).toStrictEqual(-0.84); +}); + +test("Remove keys", async () => { + const User = { + first_name: "First name", + last_name: "Last name", + mail: "mail", + password: "password", + }; + expect(removeKeys(User, [])).toStrictEqual({ + first_name: "First name", + last_name: "Last name", + mail: "mail", + password: "password", + }); + expect(removeKeys(User, ["password"])).toStrictEqual({ + first_name: "First name", + last_name: "Last name", + mail: "mail", + }); + expect(removeKeys(User, ["last_name", "mail"])).toStrictEqual({ + first_name: "First name", + password: "password", + }); + expect( + removeKeys(User, ["first_name", "last_name", "mail", "password"]), + ).toStrictEqual({}); +}); diff --git a/backend/test/userCRUD.test.ts b/backend/test/userCRUD.test.ts new file mode 100644 index 00000000..ea26cade --- /dev/null +++ b/backend/test/userCRUD.test.ts @@ -0,0 +1,315 @@ +import { expect, test } from "vitest"; +import { fetchData } from "../src/misc/fetchData.js"; + +let userId: number; +let userData = { + first_name: "John", + last_name: "Smith", + country: "USA", + profile_picture: "https://example.com/john_smith.jpg", + mail: "john_smith@example.com", + password: "12345678", +}; +let token: string; + +const login = async (mail: string, password: string) => { + const response = await fetchData(`http://localhost:5000/auth/login`, "POST", { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ mail: mail, password: password }), + }); + + token = response.token; +}; + +test("Get all users", async () => { + const response = await fetchData(`http://localhost:5000/users`, "GET", {}); + const { status, users } = response; + + expect(status).toBe("ok"); + expect(users.length).toBe(27); +}); + +test("Create user", async () => { + const response = await fetchData(`http://localhost:5000/users`, "POST", { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + const { status, user } = response; + + expect(status).toBe("ok"); + expect(user.first_name).toBe(userData.first_name); + expect(user.last_name).toBe(userData.last_name); + expect(user.mail).toBe(userData.mail); + + userId = user.id; +}); + +test("Create user with existing mail", async () => { + userData.mail = "shudghton1@geocities.com"; + + const response = await fetchData(`http://localhost:5000/users`, "POST", { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.id).toBe("already exists"); +}); + +test("Create user with short first name", async () => { + userData.first_name = "j"; + + const response = await fetchData(`http://localhost:5000/users`, "POST", { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.first_name).toBe("String must contain at least 2 character(s)"); +}); + +test("Create user with short last name", async () => { + userData.last_name = "s"; + + const response = await fetchData(`http://localhost:5000/users`, "POST", { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.last_name).toBe("String must contain at least 2 character(s)"); +}); + +test("Create user with short password", async () => { + userData.password = "1234"; + + const response = await fetchData(`http://localhost:5000/users`, "POST", { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.password).toBe("String must contain at least 8 character(s)"); +}); + +test("Get user by ID", async () => { + userData.mail = "john_smith@example.com"; + userData.first_name = "John"; + userData.last_name = "Smith"; + userData.password = "12345678"; + + const response = await fetchData( + `http://localhost:5000/users/${userId}`, + "GET", + {}, + ); + const { status, user } = response; + + expect(status).toBe("ok"); + expect(user.id).toBe(userId); + expect(user.first_name).toBe(userData.first_name); + expect(user.last_name).toBe(userData.last_name); + expect(user.mail).toBe(userData.mail); +}); + +test("Get user with incorrect ID", async () => { + const response = await fetchData(`http://localhost:5000/users/🐈`, "GET", {}); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.id).toBe("not found"); +}); + +test("Update user by ID", async () => { + userData.profile_picture = "https://example.com/new_john_smith.jpg"; + + const response = await fetchData( + `http://localhost:5000/users/${userId}`, + "PUT", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }, + ); + + const { status } = response; + + expect(status).toBe("ok"); +}); + +test("Update user with incorrect ID", async () => { + const response = await fetchData(`http://localhost:5000/users/0`, "PUT", { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.id).toBe("not found"); +}); + +test("Update user with short first name", async () => { + userData.first_name = "j"; + + const response = await fetchData( + `http://localhost:5000/users/${userId}`, + "PUT", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.first_name).toBe("String must contain at least 2 character(s)"); +}); + +test("Update user with short last name", async () => { + userData.last_name = "s"; + + const response = await fetchData( + `http://localhost:5000/users/${userId}`, + "PUT", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.last_name).toBe("String must contain at least 2 character(s)"); +}); + +test("Update user with short password", async () => { + await login(userData.mail, userData.password); + + const response = await fetchData( + `http://localhost:5000/users/${userId}/change-password`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + token: token, + old_password: userData.password, + new_password: "1234", + repeat_password: "1234", + }), + }, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.old_password).toBe("incorrect"); +}); + +test("Update user with same password", async () => { + await login(userData.mail, userData.password); + + const response = await fetchData( + `http://localhost:5000/users/${userId}/change-password`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + token: token, + old_password: userData.password, + new_password: "12345678", + repeat_password: "12345678", + }), + }, + ); + + const { status } = response; + + expect(status).toBe("ok"); +}); + +test("Update user with correct password", async () => { + await login(userData.mail, userData.password); + + const response = await fetchData( + `http://localhost:5000/users/${userId}/change-password`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + token: token, + old_password: userData.password, + new_password: "123456789", + repeat_password: "123456789", + }), + }, + ); + + const { status } = response; + + expect(status).toBe("ok"); +}); + +test("Delete user by ID", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}`, + "DELETE", + {}, + ); + + const { status } = response; + + expect(status).toBe("ok"); +}); + +test("Delete user with incorrect ID", async () => { + const response = await fetchData( + `http://localhost:5000/users/🐍`, + "DELETE", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.id).toBe("not found"); +}); diff --git a/backend/test/userEndpoints.test.ts b/backend/test/userEndpoints.test.ts deleted file mode 100644 index ddcbca99..00000000 --- a/backend/test/userEndpoints.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { assert, expect, test } from "vitest"; -import User from "../src/models/User.js"; - -let userId: number; - -test("Create user", async () => { - const userData = { - first_name: "Tom", - last_name: "Hanks", - country: "USA", - profile_picture: "https://example.com/tommy.jpg", - mail: "tom.hanks@example.com", - password: "12345", - }; - - const response = await fetch("http://localhost:5000/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(userData), - }); - - const responseData = await response.json(); - const user = responseData.user; - const status = responseData.status; - - expect(status).toBe("ok"); - - userId = user.id; -}); - -test("Try to create user with existing mail", async () => { - const userData = { - first_name: "Tom", - last_name: "Hanks", - country: "USA", - profile_picture: "https://example.com/tommy.jpg", - mail: "tom.hanks@example.com", - password: "12345", - }; - - const response = await fetch("http://localhost:5000/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(userData), - }); - - const responseData = await response.json(); - const status = responseData.status; - - expect(status).toBe("error"); -}); - -test("Fetch user by ID", async () => { - const response = await fetch(`http://localhost:5000/users/${userId}`); - const responseData = await response.json(); - const status = responseData.status; - - expect(status).toBe("ok"); -}); - -test("Update user by ID", async () => { - const userUpdateData = { - first_name: "Tommy", - last_name: "Hanks", - country: "Canada", - profile_picture: "https://example.com/tommy.jpg", - mail: "tommy.hanks@example.com", - password: "54321", - }; - - const response = await fetch(`http://localhost:5000/users/${userId}`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(userUpdateData), - }); - - const responseData = await response.json(); - const status = responseData.status; - - expect(status).toBe("ok"); -}); - -test("Delete user by ID", async () => { - const response = await fetch(`http://localhost:5000/users/${userId}`, { - method: "DELETE", - }); - - const responseData = await response.json(); - const status = responseData.status; - - expect(status).toBe("ok"); -}); - -test("Get user's friends", async () => { - const usersResponse = await fetch("http://localhost:5000/users"); - const usersResponseData = await usersResponse.json(); - const usersStatus = usersResponseData.status; - - expect(usersStatus).toBe("ok"); - - const zuck = usersResponseData.users.find( - (user: any) => user.mail == "reptilian@meta.com", - ); - const zuckId = zuck.id; - - const response = await fetch( - `http://localhost:5000/users/${zuckId}/friends?page=1&maxUsers=2`, - ); - const responseData = await response.json(); - assert.containsAllKeys(responseData, ["status", "friends", "pageCount"]); - - const { status, friends } = responseData; - expect(status).toBe("ok"); - expect(friends).toHaveLength(2); -}); - -async function searchUsers(lastPart: string) { - const usersResponse = await fetch( - "http://localhost:5000/users/search" + lastPart, - ); - const usersResponseData = await usersResponse.json(); - return usersResponseData; -} - -test("Search users", async () => { - const usersNoResponseData = await searchUsers(""); - const usersNoStatus = usersNoResponseData.status; - - expect(usersNoStatus).toBe("error"); - - const usersEmptyResponseData = await searchUsers("?q="); - const usersEmptyStatus = usersEmptyResponseData.status; - - expect(usersEmptyStatus).toBe("error"); - - const usersIncorrectResponseData = await searchUsers("?q=🐈"); - const usersIncorrectStatus = usersIncorrectResponseData.status; - - expect(usersIncorrectStatus).toBe("error"); - - const usersNoPageResponseData = await searchUsers("?q=zuckerberg"); - const usersNoPageStatus = usersNoPageResponseData.status; - const usersNoPageErrors = usersNoPageResponseData.errors; - - expect(usersNoPageStatus).toBe("error"); - expect(usersNoPageErrors["page"]).toBe("not provided"); - expect(usersNoPageErrors["maxUsers"]).toBe("not provided"); - - const usersWrongPageResponseData = await searchUsers( - "?q=zuckerberg&page=🐈&maxUsers=🐕", - ); - const usersWrongPageStatus = usersWrongPageResponseData.status; - const usersWrongPageErrors = usersWrongPageResponseData.errors; - - expect(usersWrongPageStatus).toBe("error"); - expect(usersWrongPageErrors["page"]).toBe("not a number"); - expect(usersWrongPageErrors["maxUsers"]).toBe("not a number"); - - const usersMultiplePageResponseData = await searchUsers( - "?q=zuckerberg&page=1&page=1&maxUsers=1&maxUsers=1", - ); - const usersMultiplePageStatus = usersMultiplePageResponseData.status; - const usersMultiplePageErrors = usersMultiplePageResponseData.errors; - - expect(usersMultiplePageStatus).toBe("error"); - expect(usersMultiplePageErrors["page"]).toBe("incorrect"); - expect(usersMultiplePageErrors["maxUsers"]).toBe("incorrect"); - - const usersResponseData = await searchUsers( - "?q=zuckerberg&page=1&maxUsers=10", - ); - const usersStatus = usersResponseData.status; - - expect(usersStatus).toBe("ok"); - - const zuck = usersResponseData.users.find( - (user: User) => user.mail == "reptilian@meta.com", - ); - - expect(zuck).toBeDefined(); -}); diff --git a/backend/test/userFriendRequests.test.ts b/backend/test/userFriendRequests.test.ts new file mode 100644 index 00000000..bb538be9 --- /dev/null +++ b/backend/test/userFriendRequests.test.ts @@ -0,0 +1,313 @@ +import { expect, test } from "vitest"; +import { fetchData } from "../src/misc/fetchData.js"; +import User from "../src/models/User.js"; + +let page: number = 1; +let maxUsers: number = 1; +let userId: string = ""; +let userId2: string = ""; + +const getUsers = async () => { + const response = await fetchData(`http://localhost:5000/users`, "GET", {}); + userId = response.users.find( + (user: User) => user.mail === "bconford2@wikimedia.org", + ).id; + userId2 = response.users.find( + (user: User) => user.mail === "cruckman3@archive.org", + ).id; +}; + +await getUsers(); + +test("Check current requests", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, pageCount, friendRequests } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(0); + expect(friendRequests.length).toBe(0); +}); + +test("Check current requests with incorrect ID", async () => { + const response = await fetchData( + `http://localhost:5000/users/0/friend-requests?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.id).toBe("not found"); +}); + +test("Missing page", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests?maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.page).toBe("Invalid input"); +}); + +test("Missing maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests?page=${page}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Invalid input"); +}); + +test("Missing page and maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); + expect(errors.maxUsers).toBe("Invalid input"); +}); + +test("maxUsers as a text", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests?page=text?page=${page}&maxUsers=text`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Expected number, received nan"); +}); + +test("maxUsers equals 0", async () => { + maxUsers = 0; + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-requests?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Number must be greater than or equal to 1"); +}); + +test("Send invite", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/send-friend-request/${userId2}`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }, + ); + + const { status } = response; + + expect(status).toBe("ok"); +}); + +test("Send invite with incorrect id", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/send-friend-request/0`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId2).toBe("not found"); +}); + +test("Decline friend request", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/decline-friend-request/${userId2}`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }, + ); + + const { status } = response; + + expect(status).toBe("ok"); +}); + +test("Decline friend request with incorrect id", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/decline-friend-request/0`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId2).toBe("not found"); +}); + +test("Accept not invited friend request", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/accept-friend-request/${userId2}`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId1).toBe("not invited"); +}); + +test("Accept invite", async () => { + await fetchData( + `http://localhost:5000/users/${userId}/send-friend-request/${userId2}`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }, + ); + + const response = await fetchData( + `http://localhost:5000/users/${userId2}/accept-friend-request/${userId}`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }, + ); + + const { status } = response; + + expect(status).toBe("ok"); + + const friendsResponse = await fetchData( + `http://localhost:5000/users/${userId2}/friends?page=1&maxUsers=10`, + "GET", + {}, + ); + + const friend = friendsResponse.friends.find( + (user: User) => user.id == userId, + ); + + expect(friend).toBeDefined(); +}); + +test("Accept invite with incorrect id", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/accept-friend-request/0`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId2).toBe("not found"); +}); + +test("Decline friend request when not invited", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId2}/decline-friend-request/${userId}`, + "POST", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId1).toBe("not invited"); +}); + +test("Delete friend", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId2}/delete-friend/${userId}`, + "DELETE", + {}, + ); + + const { status } = response; + + expect(status).toBe("ok"); +}); + +test("Delete friend with incorrect id", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/delete-friend/0`, + "DELETE", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.userId2).toBe("not found"); +}); diff --git a/backend/test/userFriendSuggestions.test.ts b/backend/test/userFriendSuggestions.test.ts new file mode 100644 index 00000000..3053b5da --- /dev/null +++ b/backend/test/userFriendSuggestions.test.ts @@ -0,0 +1,132 @@ +import { expect, test } from "vitest"; +import { fetchData } from "../src/misc/fetchData.js"; +import User from "../src/models/User.js"; + +let userId: number; +let page: number = 3; +let maxUsers: number = 5; + +const getFirstUser = async () => { + const response = await fetchData(`http://localhost:5000/users`, "GET", {}); + userId = response.users.find( + (user: User) => user.mail === "bconford2@wikimedia.org", + ).id; +}; + +await getFirstUser(); + +test("Get friend suggestions", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, pageCount, friendSuggestions } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(3); + expect(friendSuggestions).toBeDefined(); + expect(friendSuggestions.length).toBe(3); +}); + +test("Get friend suggestions with incorrect ID", async () => { + const response = await fetchData( + `http://localhost:5000/users/0/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.id).toBe("not found"); +}); + +test("Missing page", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.page).toBe("Invalid input"); +}); + +test("Missing maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Invalid input"); +}); + +test("Missing page and maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); + expect(errors.maxUsers).toBe("Invalid input"); +}); + +test("maxUsers as a text", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?page=text?page=${page}&maxUsers=text`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Expected number, received nan"); +}); + +test("First user", async () => { + page = 1; + maxUsers = 1; + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, pageCount, friendSuggestions } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(13); + expect(friendSuggestions).toBeDefined(); + expect(friendSuggestions.length).toBe(1); +}); + +test("maxUsers equals 0", async () => { + maxUsers = 0; + const response = await fetchData( + `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Number must be greater than or equal to 1"); +}); diff --git a/backend/test/userFriends.test.ts b/backend/test/userFriends.test.ts index d1bfb763..1f7f1e04 100644 --- a/backend/test/userFriends.test.ts +++ b/backend/test/userFriends.test.ts @@ -1,69 +1,145 @@ import { expect, test } from "vitest"; +import { fetchData } from "../src/misc/fetchData.js"; +import User from "../src/models/User.js"; -let userId: number; -let page: number = 3; -let maxUsers: number = 5; +let page: number = 1; +let maxUsers: number = 10; +let userId: string = ""; -test("Search user", async () => { - const response = await fetch( - "http://localhost:5000/users/search?q=a&page=1&maxUsers=10", +const getFirstUser = async () => { + const response = await fetchData(`http://localhost:5000/users`, "GET", {}); + userId = response.users.find( + (user: User) => user.mail === "bconford2@wikimedia.org", + ).id; +}; + +await getFirstUser(); + +test("Get friends", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, ); - const responseData = await response.json(); - const users = responseData.users; - const status = responseData.status; + const { status, pageCount, friends } = response; expect(status).toBe("ok"); - - userId = users[0].id; + expect(pageCount).toBe(1); + expect(friends.length).toBe(9); }); -test("Get friends", async () => { - const response = await fetch( - `http://localhost:5000/users/${userId}/friends?page=1&maxUsers=10`, +test("Get friends with incorrect ID", async () => { + const response = await fetchData( + `http://localhost:5000/users/0/friends?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, ); - const responseData = await response.json(); - const friends = responseData.friends; - const status = responseData.status; + const { status, errors } = response; - expect(status).toBe("ok"); - expect(friends.length).toBe(6); + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.id).toBe("not found"); }); test("Missing page", async () => { - const response = await fetch( + const response = await fetchData( `http://localhost:5000/users/${userId}/friends?maxUsers=${maxUsers}`, + "GET", + {}, ); - const responseData = await response.json(); - const status = responseData.status; + const { status, errors } = response; expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); +}); + +test("Page as a text", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=text?maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Expected number, received nan"); }); test("Missing maxUsers", async () => { - const response = await fetch( + const response = await fetchData( `http://localhost:5000/users/${userId}/friends?page=${page}`, + "GET", + {}, ); - const responseData = await response.json(); - const status = responseData.status; + const { status, errors } = response; expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Invalid input"); +}); + +test("maxUsers as a text", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=text?page=${page}&maxUsers=text`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Expected number, received nan"); +}); + +test("Missing page and maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); + expect(errors.maxUsers).toBe("Invalid input"); }); test("First user", async () => { page = 1; maxUsers = 1; - const response = await fetch( + const response = await fetchData( `http://localhost:5000/users/${userId}/friends?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, ); - const responseData = await response.json(); - const status = responseData.status; - const friends = responseData.friends; + const { status, pageCount, friends } = response; expect(status).toBe("ok"); + expect(pageCount).toBe(9); expect(friends.length).toBe(1); + expect(friends[0].country).toBe("CN"); + expect(friends[0].mail).toBe("tshillitoe@state.gov"); + expect(friends[0].first_name).toBe("Trever"); + expect(friends[0].last_name).toBe("Shillito"); +}); + +test("maxUsers equals 0", async () => { + maxUsers = 0; + const response = await fetchData( + `http://localhost:5000/users/${userId}/friends?page=${page}&maxUsers=${maxUsers}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Number must be greater than or equal to 1"); }); diff --git a/backend/test/userFriendsSuggestions.test.ts b/backend/test/userFriendsSuggestions.test.ts deleted file mode 100644 index fc66d36b..00000000 --- a/backend/test/userFriendsSuggestions.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { expect, test } from "vitest"; - -let userId: number; -let page: number = 3; -let maxUsers: number = 5; - -test("Search user", async () => { - const response = await fetch( - "http://localhost:5000/users/search?q=a&page=1&maxUsers=10", - ); - - const responseData = await response.json(); - const users = responseData.users; - const status = responseData.status; - - expect(status).toBe("ok"); - - userId = users[0].id; -}); - -test("Get friend suggestions", async () => { - const response = await fetch( - `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, - ); - - const responseData = await response.json(); - const status = responseData.status; - const pageCount = responseData.pageCount; - const friendSuggestionsLength = responseData.friendSuggestions.length; - - expect(status).toBe("ok"); - // expect(pageCount).toBe(3); - expect(friendSuggestionsLength).toBe(4); -}); - -test("Missing page", async () => { - const response = await fetch( - `http://localhost:5000/users/${userId}/friend-suggestions?maxUsers=${maxUsers}`, - ); - - const responseData = await response.json(); - const status = responseData.status; - const errors = responseData.errors; - - expect(status).toBe("error"); - expect(errors["page"]).toBe("not provided"); -}); - -test("Missing maxUsers", async () => { - const response = await fetch( - `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}`, - ); - - const responseData = await response.json(); - const status = responseData.status; - const errors = responseData.errors; - - expect(status).toBe("error"); - expect(errors["maxUsers"]).toBe("not provided"); -}); - -test("First user", async () => { - page = 1; - maxUsers = 1; - const response = await fetch( - `http://localhost:5000/users/${userId}/friend-suggestions?page=${page}&maxUsers=${maxUsers}`, - ); - - const responseData = await response.json(); - const status = responseData.status; - const friendSuggestions = responseData.friendSuggestions; - - expect(status).toBe("ok"); - expect(friendSuggestions.length).toBe(1); -}); diff --git a/backend/test/userMeetings.test.ts b/backend/test/userMeetings.test.ts new file mode 100644 index 00000000..88531a17 --- /dev/null +++ b/backend/test/userMeetings.test.ts @@ -0,0 +1,59 @@ +import { expect, test } from "vitest"; +import { fetchData } from "../src/misc/fetchData.js"; +import User from "../src/models/User.js"; + +let userId: string = ""; + +const getFirstUser = async () => { + const response = await fetchData(`http://localhost:5000/users`, "GET", {}); + userId = response.users.find( + (user: User) => user.mail === "bconford2@wikimedia.org", + ).id; +}; + +await getFirstUser(); + +test("Get users meetings", async () => { + const response = await fetchData( + `http://localhost:5000/users/meetings/${userId}`, + "GET", + {}, + ); + + const { status, meetings } = response; + + expect(status).toBe("ok"); + expect(meetings).toBeDefined(); + expect(meetings.length).toBe(0); +}); + +test("Get users meetings without id", async () => { + const response = await fetchData( + `http://localhost:5000/users/meetings`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.id).toBe("not found"); +}); + +test("Update meeting", async () => { + const response = await fetchData( + `http://localhost:5000/users/meetings/0`, + "PUT", + { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }, + ); + + const { status } = response; + + expect(status).toBe("ok"); +}); diff --git a/backend/test/userSearch.test.ts b/backend/test/userSearch.test.ts index b9a4c386..2fcca022 100644 --- a/backend/test/userSearch.test.ts +++ b/backend/test/userSearch.test.ts @@ -1,77 +1,227 @@ import { expect, test } from "vitest"; +import { fetchData } from "../src/misc/fetchData.js"; +import User from "../src/models/User.js"; let page: number = 1; -let maxUsers: number = 1; +let maxUsers: number = 32; +let query: string = "a"; +let country: string = "PL"; -test("Search all users", async () => { - const response = await fetch( - "http://localhost:5000/users/search?q=a&page=1&maxUsers=100", +test("Search all users from Poland", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, + "GET", + {}, ); - const responseData = await response.json(); - const users = responseData.users; - const status = responseData.status; + const { status, pageCount, users } = response; expect(status).toBe("ok"); - expect(users.length).toBe(27); + expect(pageCount).toBe(1); + expect(users).toBeDefined(); + expect(users.length).toBe(4); }); -test("Search all users from Poland", async () => { - const response = await fetch( - "http://localhost:5000/users/search?page=1&maxUsers=10&country=Poland", +test("Missing country", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&q=`, + "GET", + {}, ); - const responseData = await response.json(); - const users = responseData.users; - const status = responseData.status; + const { status, errors } = response; - expect(status).toBe("ok"); - expect(users.length).toBe(3); + expect(status).toBe("error"); + expect(errors.country).toBe("Invalid input"); }); test("Missing page", async () => { - const response = await fetch( - `http://localhost:5000/users/search?q=a&maxUsers=${maxUsers}`, + const response = await fetchData( + `http://localhost:5000/users/search?maxUsers=${maxUsers}`, + "GET", + {}, ); - const responseData = await response.json(); - const status = responseData.status; + const { status, errors } = response; expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); }); test("Missing maxUsers", async () => { - const response = await fetch( - `http://localhost:5000/users/search?q=a&page=${page}`, + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Invalid input"); +}); + +test("Missing page and maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/search`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors.page).toBe("Invalid input"); + expect(errors.maxUsers).toBe("Invalid input"); +}); + +test("maxUsers as a text", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=text?page=${page}&maxUsers=text`, + "GET", + {}, ); - const responseData = await response.json(); - const status = responseData.status; + const { status, errors } = response; expect(status).toBe("error"); + expect(errors.maxUsers).toBe("Expected number, received nan"); }); -test("First user from Lithuania", async () => { - const response = await fetch( - `http://localhost:5000/users/search?country=Lithuania&page=${page}&maxUsers=${maxUsers}`, +test("First user from Brazil", async () => { + country = "BR"; + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, + "GET", + {}, ); - const responseData = await response.json(); - const users = responseData.users; - const status = responseData.status; + const { status, pageCount, users } = response; expect(status).toBe("ok"); + expect(pageCount).toBe(1); + expect(users).toBeDefined(); expect(users.length).toBe(1); - expect(users[0].country).toBe("Lithuania"); + expect(users[0].country).toBe("BR"); +}); + +test("Not found users", async () => { + country = "GR"; + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, + "GET", + {}, + ); + + const { status, pageCount, users } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(1); + expect(users).toBeDefined(); + expect(users.length).toBe(1); +}); + +test("Search with polish characters", async () => { + query = "Małysz"; + country = "PL"; + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=${query}`, + "GET", + {}, + ); + + const { status, pageCount, users } = response; + + expect(status).toBe("ok"); + expect(pageCount).toBe(1); + expect(users).toBeDefined(); + expect(users.length).toBe(4); + + const malysz = users.find((user: User) => user.mail == "adasko@malysz.pl"); + + expect(malysz).toBeDefined(); +}); + +test("Repeated page", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&page=${page}&q=`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.page).toBe("Invalid input"); +}); + +test("Repeated maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&maxUsers=${maxUsers}&q=`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Invalid input"); +}); + +test("MaxUsers above 32", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=33&country=${country}&q=`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.maxUsers).toBe("Number must be less than or equal to 32"); +}); + +test("Repeated page and maxUsers", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&page=${page}&maxUsers=${maxUsers}&q=`, + "GET", + {}, + ); + + const { status, errors } = response; + + expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.page).toBe("Invalid input"); + expect(errors.maxUsers).toBe("Invalid input"); +}); + +test("Empty query", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}&q=`, + "GET", + {}, + ); + + const { status, users } = response; + + expect(status).toBe("ok"); + expect(users).toBeDefined(); }); -test("Not found user", async () => { - const response = await fetch( - `http://localhost:5000/users/search?country=Germany`, +test("Missing query", async () => { + const response = await fetchData( + `http://localhost:5000/users/search?page=${page}&maxUsers=${maxUsers}&country=${country}`, + "GET", + {}, ); - const responseData = await response.json(); - const status = responseData.status; + const { status, errors } = response; expect(status).toBe("error"); + expect(errors).toBeDefined(); + expect(errors.q).toBe("Required"); }); diff --git a/compose.yml b/compose.yml index 5054fc29..acc2fecb 100644 --- a/compose.yml +++ b/compose.yml @@ -10,7 +10,6 @@ services: - 7687:7687 volumes: - ./db/data:/data - - ./db/conf:/conf chats: image: mongo:4.4.26 diff --git a/frontend/cypress/e2e/spec.cy.ts b/frontend/cypress/e2e/spec.cy.ts index cf6a880c..d40215ec 100644 --- a/frontend/cypress/e2e/spec.cy.ts +++ b/frontend/cypress/e2e/spec.cy.ts @@ -64,6 +64,40 @@ describe("E2E tests", () => { cy.get('[data-testid="RemoveAccount"]').should("exist"); }); + it("Edit data", () => { + cy.visit("http://localhost:5173"); + + cy.get('[data-testid="WelcomeLogin"]').click(); + + cy.origin("http://localhost:3000", () => { + cy.get("#username").type("johnsmith@mail.com"); + cy.get("#password").type("password"); + cy.get("#kc-login").should("exist").click(); + }); + + cy.wait(3000); + + cy.get('[data-testid="Edit"]').should("exist").click(); + + cy.wait(5000); + + cy.get('input[name="first_name"]').clear().type("Johnny"); + cy.get('input[name="last_name"]').clear().type("Blacksmith"); + cy.get('input[name="mail"]').clear().type("johnnyblacksmith@mail.com"); + cy.get('[data-testid="Save"]').should("exist").click(); + + cy.wait(4000); + + cy.get('[data-testid="My Profile"]').should("exist").click(); + + cy.wait(1000); + + cy.contains("p", "Johnny Blacksmith").should("exist"); + cy.contains("p", "johnnyblacksmith@mail.com").should("exist"); + cy.get('[data-testid="Edit"]').should("exist"); + cy.get('[data-testid="RemoveAccount"]').should("exist"); + }); + it("Change password", () => { cy.visit("http://localhost:5173"); @@ -75,7 +109,7 @@ describe("E2E tests", () => { cy.get("#kc-login").should("exist").click(); }); - cy.wait(2000); + cy.wait(3000); cy.get('[data-testid="Edit"]').should("exist").click(); @@ -97,6 +131,96 @@ describe("E2E tests", () => { cy.get('[data-testid="RemoveAccount"]').should("exist"); }); + it("Search users", () => { + cy.visit("http://localhost:5173"); + + cy.get('[data-testid="WelcomeLogin"]').click(); + + cy.origin("http://localhost:3000", () => { + cy.get("#username").type("johnsmith@mail.com"); + cy.get("#password").type("password2"); + cy.get("#kc-login").should("exist").click(); + }); + + cy.wait(2000); + + cy.get('[data-testid="Search"]').should("exist").click(); + cy.wait(1000); + cy.get('[data-testid="SearchBox"]').type("Robert Lewandowski"); + cy.get('[data-testid="SearchButton"]').should("exist").click(); + + cy.wait(1000); + + cy.contains("p", "Robert Lewandowski").should("exist"); + cy.get('[data-testid="Robert_Lewandowski_button"]').should("exist").click(); + + cy.wait(1000); + }); + + it("Accept invite", () => { + cy.visit("http://localhost:5173"); + + cy.get('[data-testid="WelcomeLogin"]').click(); + + cy.origin("http://localhost:3000", () => { + cy.get("#username").type("lewy.robi@pzpn.pl"); + cy.get("#password").type("Euro2012"); + cy.get("#kc-login").should("exist").click(); + }); + + cy.wait(3000); + + cy.get('[data-testid="Friends"]').should("exist").click(); + cy.wait(4000); + + cy.contains("button", "Accept").should("exist").click(); + }); + + it("Send message", () => { + cy.visit("http://localhost:5173"); + + cy.get('[data-testid="WelcomeLogin"]').click(); + + cy.origin("http://localhost:3000", () => { + cy.get("#username").type("johnsmith@mail.com"); + cy.get("#password").type("password2"); + cy.get("#kc-login").should("exist").click(); + }); + + cy.wait(3000); + + cy.get('[data-testid="Friends"]').should("exist").click(); + cy.wait(4000); + + cy.get('[data-testid="Robert_Lewandowski_chat"]').should("exist").click(); + cy.get('[data-testid="ChatBox"]').should("exist").type("Hello!{enter}"); + }); + + it("Receive message", () => { + cy.visit("http://localhost:5173"); + + cy.get('[data-testid="WelcomeLogin"]').click(); + + cy.origin("http://localhost:3000", () => { + cy.get("#username").type("lewy.robi@pzpn.pl"); + cy.get("#password").type("Euro2012"); + cy.get("#kc-login").should("exist").click(); + }); + + cy.wait(3000); + + cy.get('[data-testid="Friends"]').should("exist").click(); + cy.wait(4000); + + cy.get('[data-testid="Johnny_Blacksmith_chat"]').should("exist").click(); + cy.contains("div", "Hello!").should("exist"); + + cy.get('[data-testid="Friends"]').should("exist").click(); + cy.wait(2000); + + cy.get('[data-testid="Johnny_Blacksmith_delete"]').should("exist").click(); + }); + it("Remove account", () => { cy.visit("http://localhost:5173"); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5cb7de86..f15487db 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2995,6 +2995,19 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5626,9 +5639,9 @@ } }, "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dev": true, "dependencies": { "esbuild": "^0.18.10", diff --git a/frontend/src/components/ChatBox.tsx b/frontend/src/components/ChatBox.tsx index dc4ea116..15f3a4e9 100644 --- a/frontend/src/components/ChatBox.tsx +++ b/frontend/src/components/ChatBox.tsx @@ -167,6 +167,7 @@ function ChatBox({ user, socket, friendId }: ChatBoxProps) { {messageElems}