diff --git a/backend/package-lock.json b/backend/package-lock.json index d337168..41d4c57 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,13 +13,21 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/swagger": "^11.1.1", "@nestjs/typeorm": "^11.0.0", + "@types/passport-jwt": "^4.0.1", + "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "crypto": "^1.0.1", "dotenv": "^16.5.0", + "jsonwebtoken": "^9.0.2", + "nestjs-uuid": "^0.1.4", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "pg": "^8.14.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -34,6 +42,7 @@ "@nestjs/testing": "^11.0.1", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", + "@types/bcrypt": "^5.0.2", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", @@ -2265,6 +2274,28 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", + "integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.7", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/jwt/node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", @@ -2284,6 +2315,16 @@ } } }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "11.0.12", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.0.12.tgz", @@ -2556,6 +2597,30 @@ "npm": ">=5.10.0" } }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@nuxtjs/opencollective/node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2922,11 +2987,28 @@ "node": ">=14.16" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "dev": true + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" }, "node_modules/@tootallnate/once": { "version": "1.1.2", @@ -3003,11 +3085,20 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -3017,7 +3108,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -3058,7 +3148,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", - "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -3069,7 +3158,6 @@ "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", - "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -3095,8 +3183,7 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -3138,6 +3225,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -3147,35 +3244,65 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" }, "node_modules/@types/node": { "version": "22.13.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.17.tgz", "integrity": "sha512-nAJuQXoyPj04uLgu+obZcSmsfOenUg6DxPKogeUy6yNCFwWaj5sBF8/G/pNo8EtBJjAfSVgfIlugR/BCOleO+g==", - "devOptional": true, "dependencies": { "undici-types": "~6.20.0" } }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.18", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -3185,7 +3312,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -4061,6 +4187,14 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/array-timsort": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", @@ -4241,6 +4375,29 @@ } ] }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/bcrypt/node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/bin-version": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", @@ -4458,6 +4615,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4676,7 +4839,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5014,7 +5176,7 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, + "devOptional": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -5276,6 +5438,18 @@ "node": ">= 0.8" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -5365,6 +5539,15 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6051,6 +6234,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6771,7 +6960,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -8064,6 +8252,49 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8153,6 +8384,42 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -8165,6 +8432,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -8793,6 +9066,538 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/nestjs-uuid": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/nestjs-uuid/-/nestjs-uuid-0.1.4.tgz", + "integrity": "sha512-0wTk4FyKcDj1URX/9kGSKbOIN2kxjSDtyuGb0D6cBFrjnF0TS5NYXLFh/e6V9CtIqMEZ4M46RFQUoQZSnWG41A==", + "license": "MIT", + "dependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1", + "uuid": "^9.0.1" + } + }, + "node_modules/nestjs-uuid/node_modules/@nestjs/common": { + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.17.tgz", + "integrity": "sha512-NKzPA4Tb35XjlxizsT8KZb3CCX8tNKj5EnsXsTl/gZX//uAWccBsqB8BjU69x/u4/kQ0106/Kt6PP+yrHAez0w==", + "license": "MIT", + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/nestjs-uuid/node_modules/@nestjs/core": { + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.17.tgz", + "integrity": "sha512-Tk4J5aS082NUYrsJEDLvGdU+kRnHAMdOvsA4j62fP5THO6fN6vqv6jWHfydhCiPGUCJWLT6m+mNIhETMhMAs+Q==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/nestjs-uuid/node_modules/@nestjs/platform-express": { + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.17.tgz", + "integrity": "sha512-ovn4Wxney3QGBrqNPv0QLcCuH5QoAi6pb/GNWAz6B/NmBjZbs9/zl4a2beGDA2SaYre9w43YbfmHTm17PneP9w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "body-parser": "1.20.3", + "cors": "2.8.5", + "express": "4.21.2", + "multer": "1.4.4-lts.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/nestjs-uuid/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nestjs-uuid/node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/nestjs-uuid/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/nestjs-uuid/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/nestjs-uuid/node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/nestjs-uuid/node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/nestjs-uuid/node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/nestjs-uuid/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/nestjs-uuid/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nestjs-uuid/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nestjs-uuid/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nestjs-uuid/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nestjs-uuid/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nestjs-uuid/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nestjs-uuid/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nestjs-uuid/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/nestjs-uuid/node_modules/multer": { + "version": "1.4.4-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz", + "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/nestjs-uuid/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nestjs-uuid/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/nestjs-uuid/node_modules/peek-readable": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", + "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/nestjs-uuid/node_modules/raw-body": { + "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==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/nestjs-uuid/node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "license": "Apache-2.0" + }, + "node_modules/nestjs-uuid/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/nestjs-uuid/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/nestjs-uuid/node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/nestjs-uuid/node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/nestjs-uuid/node_modules/strtok3": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", + "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/nestjs-uuid/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nestjs-uuid/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/node-abi": { "version": "3.74.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", @@ -8826,6 +9631,26 @@ "lodash": "^4.17.21" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -8851,6 +9676,17 @@ "node": ">= 10.12.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-gyp/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -9181,6 +10017,42 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9255,6 +10127,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/peek-readable": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz", @@ -10844,7 +11721,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -11258,7 +12134,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", - "dev": true, "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" @@ -11271,6 +12146,12 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -11774,7 +12655,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", - "dev": true, "engines": { "node": ">=18" }, @@ -11795,8 +12675,7 @@ "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "devOptional": true + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" }, "node_modules/unique-filename": { "version": "1.1.1", @@ -11978,6 +12857,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/webpack": { "version": "5.98.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", @@ -12155,6 +13040,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index 585e4e0..7deec34 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,13 +24,21 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/swagger": "^11.1.1", "@nestjs/typeorm": "^11.0.0", + "@types/passport-jwt": "^4.0.1", + "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "crypto": "^1.0.1", "dotenv": "^16.5.0", + "jsonwebtoken": "^9.0.2", + "nestjs-uuid": "^0.1.4", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "pg": "^8.14.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -45,6 +53,7 @@ "@nestjs/testing": "^11.0.1", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", + "@types/bcrypt": "^5.0.2", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", diff --git a/backend/src/album/album.controller.ts b/backend/src/album/album.controller.ts index 2c187d9..19a5f45 100644 --- a/backend/src/album/album.controller.ts +++ b/backend/src/album/album.controller.ts @@ -14,6 +14,7 @@ import { AlbumService } from './album.service'; import { Album } from './album.entity'; import { CreateAlbumDto } from './dto/create-album.dto'; import { UpdateAlbumDto } from './dto/update-album.dto'; +import {UUID} from "crypto"; @Controller('album') export class AlbumController { @@ -24,9 +25,9 @@ export class AlbumController { return this.albumService.findAll(); } - @Get(':id') - findOneById(@Param('id') id: number): Promise { - return this.albumService.findOneById(id); + @Get(':uuid') + findOneById(@Param('uuid') uuid: UUID): Promise { + return this.albumService.findOneById(uuid); } @Post() @@ -35,17 +36,17 @@ export class AlbumController { return this.albumService.create(createAlbumDto); } - @Put(':id') + @Put(':uuid') @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) async update( - @Param('id') id: number, + @Param('uuid') uuid: UUID, @Body() updateAlbumDto: UpdateAlbumDto, ): Promise { - return this.albumService.update(id, updateAlbumDto); + return this.albumService.update(uuid, updateAlbumDto); } - @Delete(':id') - async remove(@Param('id') id: number): Promise { - return this.albumService.remove(id); + @Delete(':uuid') + async remove(@Param('uuid') uuid: UUID): Promise { + return this.albumService.remove(uuid); } } diff --git a/backend/src/album/album.entity.ts b/backend/src/album/album.entity.ts index b3b9781..7968e3d 100644 --- a/backend/src/album/album.entity.ts +++ b/backend/src/album/album.entity.ts @@ -1,11 +1,14 @@ -import { Entity, Column, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import {Entity, Column, OneToMany, PrimaryGeneratedColumn, Generated} from 'typeorm'; import { IsNotEmpty, IsString, IsOptional } from 'class-validator'; import { Song } from '../song/song.entity'; +import { UUID } from 'crypto'; @Entity('album') export class Album { + @PrimaryGeneratedColumn() - id: number; + @Generated("uuid") + uuid: UUID; @Column({ unique: true }) @IsString() diff --git a/backend/src/album/album.module.ts b/backend/src/album/album.module.ts index 3c924f2..3683ad7 100644 --- a/backend/src/album/album.module.ts +++ b/backend/src/album/album.module.ts @@ -6,7 +6,9 @@ import { AlbumService } from './album.service'; import { APP_PIPE } from '@nestjs/core'; @Module({ - imports: [TypeOrmModule.forFeature([Album])], + imports: [ + TypeOrmModule.forFeature([Album]), + ], controllers: [AlbumController], providers: [ AlbumService, diff --git a/backend/src/album/album.service.ts b/backend/src/album/album.service.ts index a812e91..b0e0c07 100644 --- a/backend/src/album/album.service.ts +++ b/backend/src/album/album.service.ts @@ -1,9 +1,10 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { DeleteResult, Repository } from 'typeorm'; -import { Album } from './album.entity'; -import { CreateAlbumDto } from './dto/create-album.dto'; -import { UpdateAlbumDto } from './dto/update-album.dto'; +import {Injectable} from '@nestjs/common'; +import {InjectRepository} from '@nestjs/typeorm'; +import {DeleteResult, Repository} from 'typeorm'; +import {Album} from './album.entity'; +import {CreateAlbumDto} from './dto/create-album.dto'; +import {UpdateAlbumDto} from './dto/update-album.dto'; +import {UUID} from "crypto"; @Injectable() export class AlbumService { @@ -28,11 +29,11 @@ export class AlbumService { }); } - findOneById(id: number): Promise { + findOneById(uuid: UUID): Promise { return this.albumRepository .findOne({ where: { - id: id, + uuid: uuid, }, relations: { songs: true, @@ -47,10 +48,10 @@ export class AlbumService { return album; }) .catch((error) => { - return `There was a problem creating the Album identified by ID ${id}: ${error}`; + return `There was a problem creating the Album identified by ID ${uuid}: ${error}`; }) .finally(() => { - return `There was a problem creating the Album identified by ID ${id}`; + return `There was a problem creating the Album identified by ID ${uuid}`; }); } @@ -71,48 +72,47 @@ export class AlbumService { } async update( - id: number, + uuid: string, updateAlbumDto: UpdateAlbumDto, ): Promise { - if (id == updateAlbumDto.id) { + if (uuid == updateAlbumDto.uuid) { const albumToUpdate = await this.albumRepository.findOneBy({ - id: updateAlbumDto.id, + uuid: updateAlbumDto.uuid, }); if (!albumToUpdate) { - console.error("AlbumService: update: Didn't find album: ", id); + console.error("AlbumService: update: Didn't find album: ", uuid); return null; } await this.albumRepository.update( - { id: updateAlbumDto.id }, + { uuid: updateAlbumDto.uuid }, { title: updateAlbumDto.title, artist: updateAlbumDto.artist, genre: updateAlbumDto.genre, }, ); - const album = await this.albumRepository.findOneBy({ - id: updateAlbumDto.id, + return await this.albumRepository.findOneBy({ + uuid: updateAlbumDto.uuid, }); - return album; } else { console.error( 'AlbumService: update: IDs do not match', - id, + uuid, updateAlbumDto, ); return null; } } - async remove(id: number): Promise { + async remove(uuid: UUID): Promise { return await this.albumRepository - .delete(id) + .delete(uuid) .then((deleteResult) => { return deleteResult; }) .catch((error) => { - return `There was a problem deleting the Album identified by ID ${id}: ${error}`; + return `There was a problem deleting the Album identified by ID ${uuid}: ${error}`; }); } } diff --git a/backend/src/album/dto/update-album.dto.ts b/backend/src/album/dto/update-album.dto.ts index 8b38f2e..6a5b8b9 100644 --- a/backend/src/album/dto/update-album.dto.ts +++ b/backend/src/album/dto/update-album.dto.ts @@ -1,8 +1,9 @@ -import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; +import {IsNotEmpty, IsString, IsUUID} from 'class-validator'; +import {UUID} from "crypto"; export class UpdateAlbumDto { - @IsNumber() - id: number; + @IsUUID() + uuid: UUID; @IsString() @IsNotEmpty() diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 24df38c..abb37ac 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -6,17 +6,31 @@ import { AppService } from './app.service'; import { DatabaseModule } from '@/database/database.module'; import { AlbumModule } from '@/album/album.module'; import { SongModule } from '@/song/song.module'; +import { AuthModule } from './auth/auth.module'; +import { UsersModule } from './users/users.module'; +import { PassportModule } from '@nestjs/passport'; +import {JwtGuard} from "@/auth/guards/jwt.guard"; +import {APP_GUARD} from "@nestjs/core"; @Module({ imports: [ ConfigModule.forRoot({ load: [configuration], }), + PassportModule.register({ defaultStrategy: 'jwt' }), DatabaseModule, AlbumModule, SongModule, + AuthModule, + UsersModule, ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { + provide: APP_GUARD, + useClass: JwtGuard, + }, + ], }) export class AppModule {} diff --git a/backend/src/auth/auth.controller.spec.ts b/backend/src/auth/auth.controller.spec.ts new file mode 100644 index 0000000..27a31e6 --- /dev/null +++ b/backend/src/auth/auth.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from './auth.controller'; + +describe('AuthController', () => { + let controller: AuthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + }).compile(); + + controller = module.get(AuthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts new file mode 100644 index 0000000..268eeb2 --- /dev/null +++ b/backend/src/auth/auth.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('auth') +export class AuthController {} diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts new file mode 100644 index 0000000..c0ea717 --- /dev/null +++ b/backend/src/auth/auth.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { JwtStrategy } from './jwt.strategy'; +import {UsersModule} from "@/users/users.module"; + +@Module({ + controllers: [AuthController], + providers: [AuthService, JwtStrategy], + imports: [ + PassportModule.register({ defaultStrategy: 'jwt' }), + JwtModule.register({ + secret: 'secret', + signOptions: { expiresIn: '60s' } + }), + UsersModule + ], + exports: [JwtModule, PassportModule] +}) +export class AuthModule {} diff --git a/backend/src/auth/auth.service.spec.ts b/backend/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..800ab66 --- /dev/null +++ b/backend/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts new file mode 100644 index 0000000..4443bd4 --- /dev/null +++ b/backend/src/auth/auth.service.ts @@ -0,0 +1,43 @@ +import {BadRequestException, Injectable, UnauthorizedException} from '@nestjs/common'; +import {UsersService} from "@/users/users.service"; +import * as bcrypt from 'bcrypt'; +import {User} from "@/users/user.entity"; +import {JwtService} from "@nestjs/jwt"; +import {AccessToken} from "@/auth/types/AccessToken.type"; +import {RegisterRequestDto} from "@/auth/dto/register-request.dto"; + +@Injectable() +export class AuthService { + constructor( + private usersService: UsersService, + private jwtService: JwtService, + ) {} + + async validateUser(email: string, password: string): Promise { + const user = await this.usersService.findOneByEmail(email); + if (!user) { + throw new BadRequestException('User not found'); + } + const isMatch: boolean = bcrypt.compareSync(password, user.password); + if (!isMatch) { + throw new BadRequestException('Password does not match'); + } + return user; + } + + async login(user: User): Promise { + const payload = { email: user.email, uuid: user.uuid }; + return { access_token: this.jwtService.sign(payload) }; + } + + async register(user: RegisterRequestDto): Promise { + const existingUser = await this.usersService.findOneByEmail(user.email); + if (existingUser) { + throw new BadRequestException('email already exists'); + } + const hashedPassword = await bcrypt.hash(user.password, 10); + const newUser: User = { ...user, password: hashedPassword }; + await this.usersService.create(newUser); + return this.login(newUser); + } +} diff --git a/backend/src/auth/dto/login-response.dto.ts b/backend/src/auth/dto/login-response.dto.ts new file mode 100644 index 0000000..8008620 --- /dev/null +++ b/backend/src/auth/dto/login-response.dto.ts @@ -0,0 +1,3 @@ +import { AccessToken } from '../types/AccessToken.type'; + +export type LoginResponseDTO = AccessToken; \ No newline at end of file diff --git a/backend/src/auth/dto/register-request.dto.ts b/backend/src/auth/dto/register-request.dto.ts new file mode 100644 index 0000000..1a3cdd0 --- /dev/null +++ b/backend/src/auth/dto/register-request.dto.ts @@ -0,0 +1,7 @@ +export type RegisterRequestDto = { + email: string; + username: string; + password: string; + firstName: string; + lastName: string; +}; \ No newline at end of file diff --git a/backend/src/auth/dto/register-response.dto.ts b/backend/src/auth/dto/register-response.dto.ts new file mode 100644 index 0000000..2282c5d --- /dev/null +++ b/backend/src/auth/dto/register-response.dto.ts @@ -0,0 +1,3 @@ +import { AccessToken } from '../types/AccessToken.type'; + +export type RegisterResponseDTO = AccessToken; \ No newline at end of file diff --git a/backend/src/auth/guards/jwt.guard.ts b/backend/src/auth/guards/jwt.guard.ts new file mode 100644 index 0000000..6525b88 --- /dev/null +++ b/backend/src/auth/guards/jwt.guard.ts @@ -0,0 +1,21 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtGuard extends AuthGuard('jwt') { + constructor(private reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + const isPublic = this.reflector.getAllAndOverride('isPublic', [ + context.getHandler(), + context.getClass(), + ]); + + if (isPublic) return true; + + return super.canActivate(context); + } +} \ No newline at end of file diff --git a/backend/src/auth/jwt.strategy.ts b/backend/src/auth/jwt.strategy.ts new file mode 100644 index 0000000..1f03803 --- /dev/null +++ b/backend/src/auth/jwt.strategy.ts @@ -0,0 +1,16 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: 'secretKey', + }); + } + + async validate(payload: any) { + return { userId: payload.sub, username: payload.username }; + } +} \ No newline at end of file diff --git a/backend/src/auth/types/AccessToken.type.ts b/backend/src/auth/types/AccessToken.type.ts new file mode 100644 index 0000000..beefed2 --- /dev/null +++ b/backend/src/auth/types/AccessToken.type.ts @@ -0,0 +1,3 @@ +export type AccessToken = { + access_token: string; +}; \ No newline at end of file diff --git a/backend/src/auth/types/AccessTokenPayload.type.ts b/backend/src/auth/types/AccessTokenPayload.type.ts new file mode 100644 index 0000000..0bff674 --- /dev/null +++ b/backend/src/auth/types/AccessTokenPayload.type.ts @@ -0,0 +1,6 @@ +import { UUID } from 'crypto'; + +export type AccessTokenPayload = { + userId: UUID; + email: string; +}; \ No newline at end of file diff --git a/backend/src/main.ts b/backend/src/main.ts index 1a98b42..a676b42 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,16 +1,18 @@ -import { NestFactory } from '@nestjs/core'; +import {NestFactory, Reflector} from '@nestjs/core'; import { BadRequestException, ValidationError, ValidationPipe } from '@nestjs/common'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; import { AlbumModule } from './album/album.module'; import { SongModule } from './song/song.module'; import { ConfigService } from '@nestjs/config'; +import {JwtGuard} from "@/auth/guards/jwt.guard"; async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: ['error', 'warn'], }); const configService = app.get(ConfigService); + app.useGlobalGuards(new JwtGuard(app.get(Reflector))); app.enableCors({ origin: [configService.get('app.frontend_url')], methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', diff --git a/backend/src/song/dto/create-song.dto.ts b/backend/src/song/dto/create-song.dto.ts index 3481a6c..f931690 100644 --- a/backend/src/song/dto/create-song.dto.ts +++ b/backend/src/song/dto/create-song.dto.ts @@ -1,4 +1,5 @@ -import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; +import {IsNotEmpty, IsNumber, IsString, IsUUID} from 'class-validator'; +import {UUID} from "crypto"; export class CreateSongDto { @IsString() @@ -11,6 +12,6 @@ export class CreateSongDto { @IsNumber() trackNumber: number; - @IsNumber() - albumId: number; + @IsUUID() + albumId: UUID; } diff --git a/backend/src/song/dto/update-song.dto.ts b/backend/src/song/dto/update-song.dto.ts index 25d2005..fda0dcf 100644 --- a/backend/src/song/dto/update-song.dto.ts +++ b/backend/src/song/dto/update-song.dto.ts @@ -1,8 +1,9 @@ -import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; +import {IsNotEmpty, IsNumber, IsString, IsUUID} from 'class-validator'; +import {UUID} from "crypto"; export class UpdateSongDto { - @IsNumber() - id: number; + @IsUUID() + uuid: UUID; @IsString() @IsNotEmpty() @@ -14,6 +15,6 @@ export class UpdateSongDto { @IsNumber() trackNumber: number; - @IsNumber() - albumId: number; + @IsUUID() + albumId: UUID; } diff --git a/backend/src/song/song.controller.ts b/backend/src/song/song.controller.ts index 5ec0952..3417568 100644 --- a/backend/src/song/song.controller.ts +++ b/backend/src/song/song.controller.ts @@ -14,6 +14,7 @@ import { SongService } from './song.service'; import { Song } from './song.entity'; import { CreateSongDto } from './dto/create-song.dto'; import { UpdateSongDto } from './dto/update-song.dto'; +import {UUID} from "crypto"; @Controller('song') export class SongController { @@ -24,9 +25,9 @@ export class SongController { return this.songService.findAll(); } - @Get(':id') - findOneById(@Param('id') id: number): Promise { - return this.songService.findOneById(id); + @Get(':uuid') + findOneById(@Param('uuid') uuid: UUID): Promise { + return this.songService.findOneById(uuid); } @Post() @@ -35,18 +36,18 @@ export class SongController { return this.songService.create(createSongDto); } - @Put(':id') + @Put(':uuid') @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) async update( - @Param('id') id: number, + @Param('uuid') uuid: UUID, @Body() updateSongDto: UpdateSongDto, ): Promise { console.log(updateSongDto); - return this.songService.update(id, updateSongDto); + return this.songService.update(uuid, updateSongDto); } - @Delete(':id') - async remove(@Param('id') id: number): Promise { - return this.songService.remove(id); + @Delete(':uuid') + async remove(@Param('uuid') uuid: UUID): Promise { + return this.songService.remove(uuid); } } diff --git a/backend/src/song/song.entity.ts b/backend/src/song/song.entity.ts index 15e958b..c1494ed 100644 --- a/backend/src/song/song.entity.ts +++ b/backend/src/song/song.entity.ts @@ -1,11 +1,13 @@ -import { Entity, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import {Entity, Column, ManyToOne, PrimaryGeneratedColumn, Generated} from 'typeorm'; import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; import { Album } from '../album/album.entity'; +import { UUID } from 'crypto'; @Entity('song') export class Song { @PrimaryGeneratedColumn() - id: number; + @Generated("uuid") + uuid: UUID; @Column({ unique: true }) @IsString() diff --git a/backend/src/song/song.service.ts b/backend/src/song/song.service.ts index b263497..b16ec5a 100644 --- a/backend/src/song/song.service.ts +++ b/backend/src/song/song.service.ts @@ -5,6 +5,7 @@ import { Album } from '../album/album.entity'; import { Song } from './song.entity'; import { CreateSongDto } from './dto/create-song.dto'; import { UpdateSongDto } from './dto/update-song.dto'; +import {UUID} from "crypto"; @Injectable() export class SongService { @@ -30,20 +31,20 @@ export class SongService { }); } - findOneById(id: number): Promise { + findOneById(uuid: UUID): Promise { return this.songRepository - .findOneBy({ id: id }) + .findOneBy({ uuid: uuid }) .then((albums) => { return albums; }) .catch((error) => { - return `There was a problem getting the song identified by ID ${id}: ${error}`; + return `There was a problem getting the song identified by ID ${uuid}: ${error}`; }); } async create(createSongDto: CreateSongDto): Promise { const album = await this.albumRepository.findOneBy({ - id: createSongDto.albumId, + uuid: createSongDto.albumId, }); if (album) { const song = new Song(); @@ -65,15 +66,15 @@ export class SongService { } async update( - id: number, + uuid: UUID, updateSongDto: UpdateSongDto, ): Promise { - if (id == updateSongDto.id) { + if (uuid == updateSongDto.uuid) { const album = await this.albumRepository.findOneBy({ - id: updateSongDto.albumId, + uuid: updateSongDto.albumId, }); const songToUpdate = await this.songRepository.findOneBy({ - id: updateSongDto.id, + uuid: updateSongDto.uuid, }); if (!songToUpdate || !album) { console.error('SongService: update: Song or Album not found'); @@ -81,7 +82,7 @@ export class SongService { } await this.songRepository.update( - { id: updateSongDto.id }, + { uuid: updateSongDto.uuid }, { title: updateSongDto.title, duration: updateSongDto.duration, @@ -90,7 +91,7 @@ export class SongService { }, ); const song = await this.songRepository.findOneBy({ - id: updateSongDto.id, + uuid: updateSongDto.uuid, }); return song; } else { @@ -99,14 +100,14 @@ export class SongService { } } - async remove(id: number): Promise { + async remove(uuid: UUID): Promise { return await this.songRepository - .delete(id) + .delete(uuid) .then((deleteResult) => { return deleteResult; }) .catch((error) => { - return `There was a problem deleting the Song identified by ID ${id}: ${error}`; + return `There was a problem deleting the Song identified by ID ${uuid}: ${error}`; }); } } diff --git a/backend/src/users/user.entity.ts b/backend/src/users/user.entity.ts new file mode 100644 index 0000000..3a02f3a --- /dev/null +++ b/backend/src/users/user.entity.ts @@ -0,0 +1,36 @@ +import {Entity, Column, PrimaryGeneratedColumn, Generated, BeforeInsert} from 'typeorm'; +import { IsNotEmpty, IsString } from 'class-validator'; +import { UUID } from 'crypto'; + +@Entity('user') +export class User { + @PrimaryGeneratedColumn() + @Generated("uuid") + uuid?: UUID; + + @Column({ unique: true }) + @IsString() + @IsNotEmpty() + email: string; + + @Column({ unique: true }) + @IsString() + @IsNotEmpty() + username: string; + + @Column('text') + @IsString() + @IsNotEmpty() + password: string; + + @Column() + @IsString() + @IsNotEmpty() + firstName: string; + + @Column() + @IsString() + @IsNotEmpty() + lastName: string; + +} \ No newline at end of file diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts new file mode 100644 index 0000000..2ee8f03 --- /dev/null +++ b/backend/src/users/users.module.ts @@ -0,0 +1,26 @@ +import {Module, ValidationPipe} from '@nestjs/common'; +import { UsersService } from './users.service'; +import {TypeOrmModule} from "@nestjs/typeorm"; +import {Song} from "@/song/song.entity"; +import {AlbumModule} from "@/album/album.module"; +import {User} from "@/users/user.entity"; +import {APP_PIPE} from "@nestjs/core"; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User]) + ], + providers: [ + UsersService, + { + provide: APP_PIPE, + useValue: new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + }, + ], + exports: [UsersService, TypeOrmModule], +}) +export class UsersModule {} diff --git a/backend/src/users/users.service.spec.ts b/backend/src/users/users.service.spec.ts new file mode 100644 index 0000000..62815ba --- /dev/null +++ b/backend/src/users/users.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersService } from './users.service'; + +describe('UsersService', () => { + let service: UsersService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UsersService], + }).compile(); + + service = module.get(UsersService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts new file mode 100644 index 0000000..aed37b4 --- /dev/null +++ b/backend/src/users/users.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import {InjectRepository} from "@nestjs/typeorm"; +import {Repository} from "typeorm"; +import {User} from "@/users/user.entity"; + +@Injectable() +export class UsersService { + constructor( + @InjectRepository(User) + private userRepository: Repository, + ) {} + + async findOneByUsername(username: string): Promise { + return this.userRepository.findOneBy({ + username: username, + }); + } + + async findOneByEmail(email: string): Promise { + return this.userRepository.findOneBy({ + email: email, + }); + } + + async create(user: User): Promise { + return this.userRepository.save(user); + } +} diff --git a/backend/target/sqlite-dev-db.sql b/backend/target/sqlite-dev-db.sql index 16b4a1e..e3616fe 100644 Binary files a/backend/target/sqlite-dev-db.sql and b/backend/target/sqlite-dev-db.sql differ diff --git a/frontend/src/app/actions.tsx b/frontend/src/app/actions.tsx index 295c06a..15d1aa0 100644 --- a/frontend/src/app/actions.tsx +++ b/frontend/src/app/actions.tsx @@ -56,17 +56,15 @@ export async function getAlbum(id: number) { }); } -export async function createAlbum(formData: FormData) { - const title = formData.get('title'); - const artist = formData.get('artist'); - const genre = formData.get('genre'); +export async function createAlbum(formData: { id: string; title: string; artist: string; genre: string }) { + console.log("POSTING Album"); return fetch(`${backendUrl}/album/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - title: title, - artist: artist, - genre: genre, + title: formData.title, + artist: formData.artist, + genre: formData.genre, }) }).then(response => { if (response.ok) { @@ -78,7 +76,7 @@ export async function createAlbum(formData: FormData) { }); } -export async function updateAlbum(formData: FormData) { +export async function updateAlbum(formData: { id: string; title: string; artist: string; genre: string }) { const id = Number(formData.get('id')); const title = formData.get('title'); const artist = formData.get('artist'); diff --git a/frontend/src/app/album/page.tsx b/frontend/src/app/album/page.tsx index c9a0b60..16822f2 100644 --- a/frontend/src/app/album/page.tsx +++ b/frontend/src/app/album/page.tsx @@ -8,6 +8,7 @@ import Button from 'react-bootstrap/Button'; import Modal from 'react-bootstrap/Modal'; import { Alert, Snackbar, IconButton } from '@mui/material'; import { AddCircleOutline, Delete, Edit } from '@mui/icons-material'; +import AlbumForm from '@/app/components/forms/album.form'; export default function Page() { const [albums, setAlbums] = useState([]); @@ -47,7 +48,7 @@ export default function Page() { try { if (formData.get('id') == "") { await createAlbum(formData) - .then((response) => { + .then((response) => { if (response.messages) { handleSnackbar(`Failed to create Album with ID ${formData.get("id")}: ${JSON.stringify(response)}`, false); } else { @@ -57,7 +58,7 @@ export default function Page() { .catch(error => {handleSnackbar(`Failed to create Album with ID ${formData.get("id")}: ${JSON.stringify(error)}`, false);}); } else { await updateAlbum(formData) - .then(() => { + .then(() => { handleSnackbar(`Successfully updated Album with ID ${formData.get("id")}`, true); }) .catch(error => {handleSnackbar(`Failed to update Album with ID ${formData.get("id")}: ${JSON.stringify(error)}`, false);}); @@ -181,23 +182,7 @@ export default function Page() {
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
+
diff --git a/frontend/src/app/components/forms/album.form.tsx b/frontend/src/app/components/forms/album.form.tsx new file mode 100644 index 0000000..0521a1f --- /dev/null +++ b/frontend/src/app/components/forms/album.form.tsx @@ -0,0 +1,76 @@ +import Form from 'next/form'; +import Button from "react-bootstrap/Button"; +import React, {FormEvent, useState} from "react"; +import {createAlbum, getAlbums, revalidateAlbums, updateAlbum} from "@/app/actions"; + +export default function AlbumForm() { + const [formData, setFormData] = useState({ + id: '', + title: '', + artist: '', + genre: '', + }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prevData) => ({ + ...prevData, + [name]: value, + })); + }; + + const handleSubmit = async (event: React) => { + event.preventDefault(); + try { + if (formData.id == "") { + await createAlbum(formData) + .then((response) => { + if (response.messages) { + // handleSnackbar(`Failed to create Album with ID ${formData.id}: ${JSON.stringify(response)}`, false); + } else { + // handleSnackbar(`Successfully created Album with ID ${formData.id}`, true); + } + }) + .catch(error => { + // handleSnackbar(`Failed to create Album with ID ${formData.id}: ${JSON.stringify(error)}`, false); + }); + } else { + await updateAlbum(formData) + .then(() => { + // handleSnackbar(`Successfully updated Album with ID ${formData.id}`, true); + }) + .catch(error => { + // handleSnackbar(`Failed to update Album with ID ${formData.id}: ${JSON.stringify(error)}`, false); + }); + } + revalidateAlbums(); + const data = await getAlbums(); + // setAlbums(data); + } catch (error) { + // handleSnackbar(`Error creating Album: ${JSON.stringify(error)}`, false); + } + } + + return ( +
+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+ ) +} \ No newline at end of file