diff --git a/package.json b/package.json index 7613eaa..dca6287 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", - "start:dev": "nest start --watch", + "dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", @@ -21,15 +21,29 @@ }, "dependencies": { "@nestjs/common": "9.4.1", + "@nestjs/config": "^2.3.3", "@nestjs/core": "9.4.1", + "@nestjs/cqrs": "^9.0.4", "@nestjs/mapped-types": "*", + "@nestjs/passport": "^9.0.3", "@nestjs/platform-express": "^9.4.1", "@nestjs/platform-fastify": "^9.4.1", - "@prisma/client": "^4.14.1", + "@nestjs/schedule": "^2.2.3", + "@nestjs/swagger": "^6.3.0", + "@prisma/client": "^4.15.0", + "bcrypt": "^5.1.0", "class-transformer": "0.3.1", "class-validator": "^0.14.0", + "cookie-parser": "^1.4.6", + "date-fns": "^2.30.0", + "dotenv": "^16.1.4", + "jsonwebtoken": "^9.0.0", + "nodemailer": "^6.9.3", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "reflect-metadata": "^0.1.13", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "uuid": "^9.0.0" }, "devDependencies": { "@nestjs/cli": "^9.5.0", @@ -46,7 +60,7 @@ "eslint-plugin-prettier": "^4.2.1", "jest": "29.5.0", "prettier": "^2.8.8", - "prisma": "^4.14.1", + "prisma": "^4.15.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "29.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5dcdde..cb25e5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,36 +1,82 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false dependencies: '@nestjs/common': specifier: 9.4.1 version: 9.4.1(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/config': + specifier: ^2.3.3 + version: 2.3.3(@nestjs/common@9.4.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': specifier: 9.4.1 version: 9.4.1(@nestjs/common@9.4.1)(@nestjs/platform-express@9.4.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/cqrs': + specifier: ^9.0.4 + version: 9.0.4(@nestjs/common@9.4.1)(@nestjs/core@9.4.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/mapped-types': specifier: '*' version: 0.0.1(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) + '@nestjs/passport': + specifier: ^9.0.3 + version: 9.0.3(@nestjs/common@9.4.1)(passport@0.6.0) '@nestjs/platform-express': specifier: ^9.4.1 version: 9.4.1(@nestjs/common@9.4.1)(@nestjs/core@9.4.1) '@nestjs/platform-fastify': specifier: ^9.4.1 version: 9.4.1(@nestjs/common@9.4.1)(@nestjs/core@9.4.1) + '@nestjs/schedule': + specifier: ^2.2.3 + version: 2.2.3(@nestjs/common@9.4.1)(@nestjs/core@9.4.1)(reflect-metadata@0.1.13) + '@nestjs/swagger': + specifier: ^6.3.0 + version: 6.3.0(@nestjs/common@9.4.1)(@nestjs/core@9.4.1)(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) '@prisma/client': - specifier: ^4.14.1 - version: 4.14.1(prisma@4.14.1) + specifier: ^4.15.0 + version: 4.15.0(prisma@4.15.0) + bcrypt: + specifier: ^5.1.0 + version: 5.1.0 class-transformer: specifier: 0.3.1 version: 0.3.1 class-validator: specifier: ^0.14.0 version: 0.14.0 + cookie-parser: + specifier: ^1.4.6 + version: 1.4.6 + date-fns: + specifier: ^2.30.0 + version: 2.30.0 + dotenv: + specifier: ^16.1.4 + version: 16.1.4 + jsonwebtoken: + specifier: ^9.0.0 + version: 9.0.0 + nodemailer: + specifier: ^6.9.3 + version: 6.9.3 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 reflect-metadata: specifier: ^0.1.13 version: 0.1.13 rxjs: specifier: ^7.8.1 version: 7.8.1 + uuid: + specifier: ^9.0.0 + version: 9.0.0 devDependencies: '@nestjs/cli': @@ -38,7 +84,7 @@ devDependencies: version: 9.5.0 '@nestjs/schematics': specifier: ^9.2.0 - version: 9.2.0(chokidar@3.5.3)(typescript@4.9.5) + version: 9.2.0(typescript@5.0.4) '@nestjs/testing': specifier: ^9.4.1 version: 9.4.1(@nestjs/common@9.4.1)(@nestjs/core@9.4.1)(@nestjs/platform-express@9.4.1) @@ -76,8 +122,8 @@ devDependencies: specifier: ^2.8.8 version: 2.8.8 prisma: - specifier: ^4.14.1 - version: 4.14.1 + specifier: ^4.15.0 + version: 4.15.0 source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -448,6 +494,13 @@ packages: '@babel/helper-plugin-utils': 7.21.5 dev: true + /@babel/runtime@7.22.5: + resolution: {integrity: sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + dev: false + /@babel/template@7.20.7: resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} engines: {node: '>=6.9.0'} @@ -885,6 +938,24 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + /@mapbox/node-pre-gyp@1.0.10: + resolution: {integrity: sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==} + hasBin: true + dependencies: + detect-libc: 2.0.1 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.6.11 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.5.1 + tar: 6.1.15 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /@nestjs/cli@9.5.0: resolution: {integrity: sha512-Z7q+3vNsQSG2d2r2Hl/OOj5EpfjVx3OfnJ9+KuAsOdw1sKLm7+Zc6KbhMFTd/eIvfx82ww3Nk72xdmfPYCulWA==} engines: {node: '>= 12.9.0'} @@ -943,6 +1014,22 @@ packages: tslib: 2.5.0 uid: 2.0.2 + /@nestjs/config@2.3.3(@nestjs/common@9.4.1)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-WcBA+0sv8euzKoYpxsCAdMzADxZEeUq8ulD+T+7QBNF0Yha6KC9edkXzk5xTJcanrKL9qnP0kboAXONscHb/Kw==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 + reflect-metadata: ^0.1.13 + rxjs: ^6.0.0 || ^7.2.0 + dependencies: + '@nestjs/common': 9.4.1(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + dotenv: 16.1.4 + dotenv-expand: 10.0.0 + lodash: 4.17.21 + reflect-metadata: 0.1.13 + rxjs: 7.8.1 + uuid: 9.0.0 + dev: false + /@nestjs/core@9.4.1(@nestjs/common@9.4.1)(@nestjs/platform-express@9.4.1)(reflect-metadata@0.1.13)(rxjs@7.8.1): resolution: {integrity: sha512-KbG0L5UVaI9kjZv3QkSyCf8Cz5lj11hy60n+NPoO0GZmJbhWkfsjletwKpwlpMeGbi7jLGTsU+HPDgprANSNEA==} requiresBuild: true @@ -974,6 +1061,21 @@ packages: transitivePeerDependencies: - encoding + /@nestjs/cqrs@9.0.4(@nestjs/common@9.4.1)(@nestjs/core@9.4.1)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-nWDF+xs4jqs6OjxFg/wVSd0NiIV9+EFCJrJNTo4VRWe78CcAaitbp56CBspUh4gKyfkci95i+EhHdEqRXKFptg==} + peerDependencies: + '@nestjs/common': ^9.0.0 + '@nestjs/core': ^9.0.0 + reflect-metadata: 0.1.13 + rxjs: ^7.2.0 + dependencies: + '@nestjs/common': 9.4.1(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 9.4.1(@nestjs/common@9.4.1)(@nestjs/platform-express@9.4.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) + reflect-metadata: 0.1.13 + rxjs: 7.8.1 + uuid: 9.0.0 + dev: false + /@nestjs/mapped-types@0.0.1(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13): resolution: {integrity: sha512-4G4Ui7Sj0UqXiZsUFk/6cPD3K7uZEFSElzkOftaJ3/lXW+HUi1/vfWXabF53qrzO1enTRQDxt1plDbP6SsqXEg==} peerDependencies: @@ -986,6 +1088,35 @@ packages: reflect-metadata: 0.1.13 dev: false + /@nestjs/mapped-types@1.2.2(@nestjs/common@9.4.1)(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-3dHxLXs3M0GPiriAcCFFJQHoDFUuzTD5w6JDhE7TyfT89YKpe6tcCCIqOZWdXmt9AZjjK30RkHRSFF+QEnWFQg==} + peerDependencies: + '@nestjs/common': ^7.0.8 || ^8.0.0 || ^9.0.0 + class-transformer: ^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 + class-validator: ^0.11.1 || ^0.12.0 || ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + '@nestjs/common': 9.4.1(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + class-transformer: 0.3.1 + class-validator: 0.14.0 + reflect-metadata: 0.1.13 + dev: false + + /@nestjs/passport@9.0.3(@nestjs/common@9.4.1)(passport@0.6.0): + resolution: {integrity: sha512-HplSJaimEAz1IOZEu+pdJHHJhQyBOPAYWXYHfAPQvRqWtw4FJF1VXl1Qtk9dcXQX1eKytDtH+qBzNQc19GWNEg==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 + passport: ^0.4.0 || ^0.5.0 || ^0.6.0 + dependencies: + '@nestjs/common': 9.4.1(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + passport: 0.6.0 + dev: false + /@nestjs/platform-express@9.4.1(@nestjs/common@9.4.1)(@nestjs/core@9.4.1): resolution: {integrity: sha512-lz6GowtaU9pbycmvqDVDiLp0Vib78ama02dRg9QvvDgq61CLg4BSLAj+7duR2/LsWfQQ+4jqdQRzhlEKsM5Oiw==} peerDependencies: @@ -1028,6 +1159,20 @@ packages: - supports-color dev: false + /@nestjs/schedule@2.2.3(@nestjs/common@9.4.1)(@nestjs/core@9.4.1)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-PxoGdoBwZQ6SzGfFcERTk7mDxrmesNt2cfqKgtLsFpjYNpV6ZYlKw9Ku8C0ZIjdhy0tBbysj+Fsi3sYua6o6Eg==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 + reflect-metadata: ^0.1.12 + dependencies: + '@nestjs/common': 9.4.1(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 9.4.1(@nestjs/common@9.4.1)(@nestjs/platform-express@9.4.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) + cron: 2.3.1 + reflect-metadata: 0.1.13 + uuid: 9.0.0 + dev: false + /@nestjs/schematics@9.2.0(chokidar@3.5.3)(typescript@4.9.5): resolution: {integrity: sha512-wHpNJDPzM6XtZUOB3gW0J6mkFCSJilzCM3XrHI1o0C8vZmFE1snbmkIXNyoi1eV0Nxh1BMymcgz5vIMJgQtTqw==} peerDependencies: @@ -1042,6 +1187,49 @@ packages: - chokidar dev: true + /@nestjs/schematics@9.2.0(typescript@5.0.4): + resolution: {integrity: sha512-wHpNJDPzM6XtZUOB3gW0J6mkFCSJilzCM3XrHI1o0C8vZmFE1snbmkIXNyoi1eV0Nxh1BMymcgz5vIMJgQtTqw==} + peerDependencies: + typescript: '>=4.3.5' + dependencies: + '@angular-devkit/core': 16.0.1(chokidar@3.5.3) + '@angular-devkit/schematics': 16.0.1(chokidar@3.5.3) + jsonc-parser: 3.2.0 + pluralize: 8.0.0 + typescript: 5.0.4 + transitivePeerDependencies: + - chokidar + dev: true + + /@nestjs/swagger@6.3.0(@nestjs/common@9.4.1)(@nestjs/core@9.4.1)(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-Gnig189oa1tD+h0BYIfUwhp/wvvmTn6iO3csR2E4rQrDTgCxSxZDlNdfZo3AC+Rmf8u0KX4ZAX1RZN1qXTtC7A==} + peerDependencies: + '@fastify/static': ^6.0.0 + '@nestjs/common': ^9.0.0 + '@nestjs/core': ^9.0.0 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + '@nestjs/common': 9.4.1(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 9.4.1(@nestjs/common@9.4.1)(@nestjs/platform-express@9.4.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/mapped-types': 1.2.2(@nestjs/common@9.4.1)(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) + class-transformer: 0.3.1 + class-validator: 0.14.0 + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.2.0 + reflect-metadata: 0.1.13 + swagger-ui-dist: 4.18.2 + dev: false + /@nestjs/testing@9.4.1(@nestjs/common@9.4.1)(@nestjs/core@9.4.1)(@nestjs/platform-express@9.4.1): resolution: {integrity: sha512-DErwymDxsY80dH2v3eZDVOhjDv6oY6YsaI3GnUEU+gUrYa8qxxKPHfOofAsFF7SExfeJo5c8RQRfazW3bQupJA==} peerDependencies: @@ -1093,8 +1281,8 @@ packages: transitivePeerDependencies: - encoding - /@prisma/client@4.14.1(prisma@4.14.1): - resolution: {integrity: sha512-TZIswkeX1ccsHG/eN2kICzg/csXll0osK3EHu1QKd8VJ3XLcXozbNELKkCNfsCUvKJAwPdDtFCzF+O+raIVldw==} + /@prisma/client@4.15.0(prisma@4.15.0): + resolution: {integrity: sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw==} engines: {node: '>=14.17'} requiresBuild: true peerDependencies: @@ -1103,16 +1291,16 @@ packages: prisma: optional: true dependencies: - '@prisma/engines-version': 4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c - prisma: 4.14.1 + '@prisma/engines-version': 4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944 + prisma: 4.15.0 dev: false - /@prisma/engines-version@4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c: - resolution: {integrity: sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw==} + /@prisma/engines-version@4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944: + resolution: {integrity: sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg==} dev: false - /@prisma/engines@4.14.1: - resolution: {integrity: sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA==} + /@prisma/engines@4.15.0: + resolution: {integrity: sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA==} requiresBuild: true /@sinclair/typebox@0.25.24: @@ -1582,6 +1770,10 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} dev: true + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: false + /abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -1627,6 +1819,15 @@ packages: hasBin: true dev: true + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /ajv-formats@2.1.1(ajv@8.11.0): resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1699,7 +1900,6 @@ packages: /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - dev: true /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} @@ -1730,10 +1930,22 @@ packages: /append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + /aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + dev: false + /archy@1.0.0: resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} dev: false + /are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + dev: false + /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true @@ -1746,7 +1958,6 @@ packages: /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true /array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -1853,11 +2064,22 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + /bcrypt@5.1.0: + resolution: {integrity: sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==} + engines: {node: '>= 10.0.0'} + requiresBuild: true + dependencies: + '@mapbox/node-pre-gyp': 1.0.10 + node-addon-api: 5.1.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -1914,7 +2136,6 @@ packages: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true /brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -1953,6 +2174,10 @@ packages: node-int64: 0.4.0 dev: true + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2045,6 +2270,11 @@ packages: fsevents: 2.3.2 dev: true + /chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: false + /chrome-trace-event@1.0.3: resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} engines: {node: '>=6.0'} @@ -2137,6 +2367,11 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + /color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + dev: false + /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2159,7 +2394,6 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true /concat-stream@1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} @@ -2173,6 +2407,10 @@ packages: /consola@2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + /console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + dev: false + /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -2191,9 +2429,22 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true + /cookie-parser@1.4.6: + resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.4.1 + cookie-signature: 1.0.6 + dev: false + /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + /cookie@0.4.1: + resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} + engines: {node: '>= 0.6'} + dev: false + /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -2227,6 +2478,12 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true + /cron@2.3.1: + resolution: {integrity: sha512-1eRRlIT0UfIqauwbG9pkg3J6CX9A6My2ytJWqAXoK0T9oJnUZTzGBNPxao0zjodIbPgf8UQWjE62BMb9eVllSQ==} + dependencies: + luxon: 3.3.0 + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -2236,6 +2493,13 @@ packages: which: 2.0.2 dev: true + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.22.5 + dev: false + /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -2281,6 +2545,10 @@ packages: engines: {node: '>=0.4.0'} dev: true + /delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dev: false + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -2289,6 +2557,11 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + /detect-libc@2.0.1: + resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==} + engines: {node: '>=8'} + dev: false + /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -2325,6 +2598,22 @@ packages: esutils: 2.0.3 dev: true + /dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + dev: false + + /dotenv@16.1.4: + resolution: {integrity: sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw==} + engines: {node: '>=12'} + dev: false + + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -2339,7 +2628,6 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true /encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} @@ -2868,13 +3156,19 @@ packages: universalify: 2.0.0 dev: true + /fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: false + /fs-monkey@1.0.3: resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==} dev: true /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true /fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} @@ -2887,6 +3181,21 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: false + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2949,7 +3258,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} @@ -3014,6 +3322,10 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + /has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + dev: false + /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -3039,6 +3351,16 @@ packages: statuses: 2.0.1 toidentifier: 1.0.1 + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} @@ -3090,7 +3412,6 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -3171,7 +3492,6 @@ packages: /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - dev: true /is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} @@ -3703,7 +4023,6 @@ packages: hasBin: true dependencies: argparse: 2.0.1 - dev: true /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} @@ -3744,6 +4063,31 @@ packages: graceful-fs: 4.2.11 dev: true + /jsonwebtoken@9.0.0: + resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash: 4.17.21 + ms: 2.1.3 + semver: 7.5.1 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -3806,7 +4150,6 @@ packages: /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true /log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} @@ -3833,6 +4176,11 @@ packages: engines: {node: 14 || >=16.14} dev: true + /luxon@3.3.0: + resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==} + engines: {node: '>=12'} + dev: false + /macos-release@2.5.1: resolution: {integrity: sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==} engines: {node: '>=6'} @@ -3850,7 +4198,6 @@ packages: engines: {node: '>=8'} dependencies: semver: 6.3.0 - dev: true /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -3927,7 +4274,6 @@ packages: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 - dev: true /minimatch@8.0.4: resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} @@ -3939,22 +4285,48 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + /minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: false + /minipass@4.2.8: resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} engines: {node: '>=8'} dev: true + /minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + dev: false + /minipass@6.0.2: resolution: {integrity: sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==} engines: {node: '>=16 || 14 >=14.17'} dev: true + /minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + dev: false + /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true dependencies: minimist: 1.2.8 + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: false + /mnemonist@0.39.5: resolution: {integrity: sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==} dependencies: @@ -4006,6 +4378,10 @@ packages: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} dev: true + /node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + dev: false + /node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} dependencies: @@ -4031,6 +4407,19 @@ packages: resolution: {integrity: sha512-+M0PwXeU80kRohZ3aT4J/OnR+l9/KD2nVLNNoRgFtnf+umQVFdGBAO2N8+nCnEi0xlh/Wk3zOGC+vNNx+uM79Q==} dev: true + /nodemailer@6.9.3: + resolution: {integrity: sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==} + engines: {node: '>=6.0.0'} + dev: false + + /nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: false + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -4043,6 +4432,15 @@ packages: path-key: 3.1.1 dev: true + /npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4068,7 +4466,6 @@ packages: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 - dev: true /onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} @@ -4171,6 +4568,34 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + /passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + dependencies: + jsonwebtoken: 9.0.0 + passport-strategy: 1.0.0 + dev: false + + /passport-local@1.0.0: + resolution: {integrity: sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==} + engines: {node: '>= 0.4.0'} + dependencies: + passport-strategy: 1.0.0 + dev: false + + /passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + dev: false + + /passport@0.6.0: + resolution: {integrity: sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==} + engines: {node: '>= 0.4.0'} + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + dev: false + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4179,7 +4604,6 @@ packages: /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - dev: true /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} @@ -4213,6 +4637,10 @@ packages: engines: {node: '>=8'} dev: true + /pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + dev: false + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true @@ -4294,13 +4722,13 @@ packages: react-is: 18.2.0 dev: true - /prisma@4.14.1: - resolution: {integrity: sha512-z6hxzTMYqT9SIKlzD08dhzsLUpxjFKKsLpp5/kBDnSqiOjtUyyl/dC5tzxLcOa3jkEHQ8+RpB/fE3w8bgNP51g==} + /prisma@4.15.0: + resolution: {integrity: sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA==} engines: {node: '>=14.17'} hasBin: true requiresBuild: true dependencies: - '@prisma/engines': 4.14.1 + '@prisma/engines': 4.15.0 /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -4415,7 +4843,6 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: true /readable-stream@4.4.0: resolution: {integrity: sha512-kDMOq0qLtxV9f/SQv522h8cxZBqNZXuXNyjyezmfAAuribMyVXziljpQ/uQhfE1XLg2/TLTW2DsnoE4VAi/krg==} @@ -4449,6 +4876,10 @@ packages: /reflect-metadata@0.1.13: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: false + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4515,7 +4946,6 @@ packages: hasBin: true dependencies: glob: 7.2.3 - dev: true /rimraf@4.4.1: resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} @@ -4577,7 +5007,6 @@ packages: /semver@6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} hasBin: true - dev: true /semver@7.5.1: resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} @@ -4623,6 +5052,10 @@ packages: transitivePeerDependencies: - supports-color + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: false + /set-cookie-parser@2.6.0: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} dev: false @@ -4661,7 +5094,6 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -4741,7 +5173,6 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true /string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -4752,14 +5183,12 @@ packages: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 - dev: true /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 - dev: true /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} @@ -4834,6 +5263,10 @@ packages: engines: {node: '>= 0.4'} dev: true + /swagger-ui-dist@4.18.2: + resolution: {integrity: sha512-oVBoBl9Dg+VJw8uRWDxlyUyHoNEDC0c1ysT6+Boy6CTgr2rUcLcfPon4RvxgS2/taNW6O0+US+Z/dlAsWFjOAQ==} + dev: false + /symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -4844,6 +5277,18 @@ packages: engines: {node: '>=6'} dev: true + /tar@6.1.15: + resolution: {integrity: sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==} + engines: {node: '>=10'} + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + /terser-webpack-plugin@5.3.9(webpack@5.82.1): resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} engines: {node: '>= 10.13.0'} @@ -5166,6 +5611,11 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + /uuid@9.0.0: + resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + hasBin: true + dev: false + /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} dev: true @@ -5314,6 +5764,12 @@ packages: isexe: 2.0.0 dev: true + /wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + dependencies: + string-width: 4.2.3 + dev: false + /windows-release@4.0.0: resolution: {integrity: sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==} engines: {node: '>=10'} @@ -5337,7 +5793,6 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true /write-file-atomic@4.0.2: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9cf333d..4c8beff 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -5,7 +5,20 @@ datasource db { } generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" + previewFeatures = ["fullTextSearch", "fullTextIndex"] +} + +model Verification { + id String @id @default(cuid()) + userId String @unique + isEmailVerified Boolean @default(false) + verificationToken String? @unique @default(uuid()) + verificationTokenExpiry DateTime? + verificationEmailsSent Int @default(0) + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) } model User { @@ -14,17 +27,41 @@ model User { password String isAdmin Boolean @default(false) name String @db.VarChar(40) - isVerified Boolean @default(false) avatar String? deckCount Int @default(0) isDeleted Boolean? @default(false) deleteTime Int? created DateTime @default(now()) updated DateTime @updatedAt - cards Card[] // One-to-many relation - decks Deck[] // One-to-many relation - grades Grade[] // One-to-many relation - generalChatMessages GeneralChatMessage[] // One-to-many relation + cards Card[] + decks Deck[] + grades Grade[] + generalChatMessages GeneralChatMessage[] + verification Verification? + AccessToken AccessToken[] + RefreshToken RefreshToken[] +} + +model AccessToken { + id String @id @default(cuid()) + userId String + token String @unique + expiresAt DateTime + isRevoked Boolean @default(false) + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) +} + +model RefreshToken { + id String @id @default(cuid()) + userId String + token String @unique + expiresAt DateTime + isRevoked Boolean @default(false) + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) } model Card { diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index c234cc7..132bee2 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,12 +1,20 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; -import { UsersModule } from './users/users.module'; +import { JwtStrategy } from './modules/auth/strategies/jwt.strategy'; +import { JwtPayloadExtractorStrategy } from './guards/common/jwt-payload-extractor.strategy'; +import { JwtPayloadExtractorGuard } from './guards/common/jwt-payload-extractor.guard'; +import { ConfigModule } from './settings/config.module'; +import { AuthModule } from './modules/auth/auth.module'; +import { UsersModule } from './modules/users/users.module'; import { PrismaModule } from './prisma.module'; @Module({ - imports: [UsersModule, PrismaModule], - controllers: [AppController], - providers: [AppService], + imports: [ConfigModule, AuthModule, UsersModule, PrismaModule], + controllers: [], + providers: [ + JwtStrategy, + JwtPayloadExtractorStrategy, + JwtPayloadExtractorGuard, + ], + exports: [], }) export class AppModule {} diff --git a/src/exception.filter.ts b/src/exception.filter.ts new file mode 100644 index 0000000..a0b895e --- /dev/null +++ b/src/exception.filter.ts @@ -0,0 +1,37 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, +} from '@nestjs/common'; +import { Request, Response } from 'express'; + +@Catch(HttpException) +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = exception.getStatus(); + if (status === 400) { + const errorsResponse = { + errorsMessages: [], + }; + const responseBody: any = exception.getResponse(); + if (typeof responseBody.message === 'object') { + responseBody.message.forEach((e) => + errorsResponse.errorsMessages.push(e), + ); + } else { + errorsResponse.errorsMessages.push(responseBody.message); + } + response.status(status).json(errorsResponse); + } else { + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + }); + } + } +} diff --git a/src/guards/auth/check-user-existing.guard.ts b/src/guards/auth/check-user-existing.guard.ts new file mode 100644 index 0000000..b5e747a --- /dev/null +++ b/src/guards/auth/check-user-existing.guard.ts @@ -0,0 +1,24 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, +} from '@nestjs/common'; +import { UsersRepository } from '../../modules/users/infrastructure/users.repository'; + +export class LimitsControlGuard implements CanActivate { + constructor(private usersRepository: UsersRepository) {} + + async canActivate(context: ExecutionContext): Promise | null { + const request = context.switchToHttp().getRequest(); + const email = request.body.email; + const userWithExistingEmail = await this.usersRepository.findUserByEmail( + email, + ); + if (userWithExistingEmail) + throw new BadRequestException({ + message: 'email already exist', + field: 'email', + }); + return true; + } +} diff --git a/src/guards/common/jwt-payload-extractor.guard.ts b/src/guards/common/jwt-payload-extractor.guard.ts new file mode 100644 index 0000000..7ece315 --- /dev/null +++ b/src/guards/common/jwt-payload-extractor.guard.ts @@ -0,0 +1,23 @@ +import { + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtPayloadExtractorGuard extends AuthGuard('payloadExtractor') { + canActivate(context: ExecutionContext) { + // Add your custom authentication logic here + // for example, call super.logIn(request) to establish a session. + return super.canActivate(context); + } + + handleRequest(err, user, info) { + // You can throw an exception based on either "info" or "err" arguments + if (err) { + throw err || new UnauthorizedException(); + } + return user; + } +} diff --git a/src/guards/common/jwt-payload-extractor.strategy.ts b/src/guards/common/jwt-payload-extractor.strategy.ts new file mode 100644 index 0000000..676d819 --- /dev/null +++ b/src/guards/common/jwt-payload-extractor.strategy.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { AppSettings } from '../../settings/app-settings'; + +@Injectable() +export class JwtPayloadExtractorStrategy extends PassportStrategy( + Strategy, + 'payloadExtractor', +) { + constructor( + @Inject(AppSettings.name) private readonly appSettings: AppSettings, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY, + }); + } + + async validate(payload: any) { + const userId = payload.userId; + const login = payload.login; + if (payload) return { userId, login }; + return null; + } +} diff --git a/src/infrastructure/common/pagination.service.ts b/src/infrastructure/common/pagination.service.ts new file mode 100644 index 0000000..1202611 --- /dev/null +++ b/src/infrastructure/common/pagination.service.ts @@ -0,0 +1,9 @@ +export class Pagination { + static getPaginationData(query) { + const page = typeof query.PageNumber === 'string' ? +query.PageNumber : 1; + const pageSize = typeof query.PageSize === 'string' ? +query.PageSize : 10; + const searchNameTerm = + typeof query.SearchNameTerm === 'string' ? query.SearchNameTerm : ''; + return { page, pageSize, searchNameTerm }; + } +} diff --git a/src/main.ts b/src/main.ts index 23274b7..bf6fc30 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,18 +1,38 @@ import { NestFactory } from '@nestjs/core'; -import { - FastifyAdapter, - NestFastifyApplication, -} from '@nestjs/platform-fastify'; import { AppModule } from './app.module'; -import { ValidationPipe } from '@nestjs/common'; - +import { BadRequestException, ValidationPipe } from '@nestjs/common'; +import { HttpExceptionFilter } from './exception.filter'; +import * as cookieParser from 'cookie-parser'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; async function bootstrap() { - const app = await NestFactory.create( - AppModule, - new FastifyAdapter({ logger: true }), + const app = await NestFactory.create(AppModule); + const config = new DocumentBuilder() + .setTitle('Flashcards') + .setDescription('The config API description') + .setVersion('1.0') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('docs', app, document); + app.useGlobalPipes( + new ValidationPipe({ + stopAtFirstError: false, + exceptionFactory: (errors) => { + const customErrors = errors.map((e) => { + const firstError = JSON.stringify(e.constraints); + return { field: e.property, message: firstError }; + }); + throw new BadRequestException(customErrors); + }, + }), ); - app.useGlobalPipes(new ValidationPipe({})); - await app.listen(3000, '0.0.0.0'); + app.useGlobalFilters(new HttpExceptionFilter()); + app.use(cookieParser()); + await app.listen(process.env.PORT || 3000); +} +try { + bootstrap(); +} catch (e) { + console.log('BOOTSTRAP CALL FAILED'); + console.log('ERROR: '); + console.log(e); } - -void bootstrap(); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..667e9ef --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,103 @@ +import { + Controller, + Get, + Post, + Body, + UseGuards, + Request, + Response, + NotFoundException, + UnauthorizedException, + BadRequestException, + Res, + HttpCode, +} from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { RegistrationDto } from './dto/registration.dto'; +import { LocalAuthGuard } from './guards/local-auth.guard'; +import { UsersService } from '../users/services/users.service'; +import { JwtAuthGuard } from './guards/jwt-auth.guard'; + +@Controller('auth') +export class AuthController { + constructor( + private readonly authService: AuthService, + private readonly usersService: UsersService, + ) {} + + @UseGuards(JwtAuthGuard) + @Get('me') + async getUserData(@Request() req) { + const userId = req.user.userId; + const user = await this.usersService.getUserById(userId); + + if (!user) throw new UnauthorizedException(); + + return { + email: user.email, + name: user.name, + is: user.id, + }; + } + @HttpCode(200) + @UseGuards(LocalAuthGuard) + @Post('login') + async login(@Request() req, @Res({ passthrough: true }) res) { + const userData = req.user.data; + res.cookie('refreshToken', userData.refreshToken, { + httpOnly: true, + secure: true, + }); + return { accessToken: req.user.data.accessToken }; + } + @HttpCode(201) + @Post('registration') + async registration(@Body() registrationData: RegistrationDto) { + return await this.usersService.createUser( + registrationData.name, + registrationData.password, + registrationData.email, + ); + } + + @Post('registration-confirmation') + async confirmRegistration(@Body('code') confirmationCode) { + const result = await this.authService.confirmEmail(confirmationCode); + if (!result) { + throw new NotFoundException(); + } + return null; + } + + @Post('registration-email-resending') + async resendRegistrationEmail(@Body('email') email: string) { + const isResented = await this.authService.resendCode(email); + if (!isResented) + throw new BadRequestException({ + message: 'email already confirmed or such email not found', + field: 'email', + }); + return null; + } + + @UseGuards(JwtAuthGuard) + @Post('logout') + async logout(@Request() req) { + if (!req.cookie?.refreshToken) throw new UnauthorizedException(); + await this.usersService.addRevokedToken(req.cookie.refreshToken); + return null; + } + + @UseGuards(JwtAuthGuard) + @Post('refresh-token') + async refreshToken(@Request() req, @Response() res) { + if (!req.cookie?.refreshToken) throw new UnauthorizedException(); + const userId = req.user.id; + const newTokens = this.authService.createJwtTokensPair(userId, null); + res.cookie('refreshToken', newTokens.refreshToken, { + httpOnly: true, + secure: true, + }); + return { accessToken: newTokens.accessToken }; + } +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..d8eb331 --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { UsersModule } from '../users/users.module'; +import { LocalStrategy } from './strategies/local.strategy'; + +@Module({ + imports: [UsersModule], + controllers: [AuthController], + providers: [AuthService, LocalStrategy], +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..2a2bac5 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,88 @@ +import { Injectable } from '@nestjs/common'; +import { isAfter } from 'date-fns'; +import * as jwt from 'jsonwebtoken'; +import * as bcrypt from 'bcrypt'; +import { UsersRepository } from '../users/infrastructure/users.repository'; +import * as process from 'process'; + +@Injectable() +export class AuthService { + constructor(private usersRepository: UsersRepository) {} + + createJwtTokensPair(userId: string, email: string | null) { + const accessSecretKey = process.env.ACCESS_JWT_SECRET_KEY; + const refreshSecretKey = process.env.REFRESH_JWT_SECRET_KEY; + const payload: { userId: string; date: Date; email: string | null } = { + userId, + date: new Date(), + email, + }; + const accessToken = jwt.sign(payload, accessSecretKey, { expiresIn: '1d' }); + const refreshToken = jwt.sign(payload, refreshSecretKey, { + expiresIn: '30d', + }); + return { + accessToken, + refreshToken, + }; + } + + async checkCredentials(email: string, password: string) { + const user = await this.usersRepository.findUserByEmail(email); + if (!user /*|| !user.emailConfirmation.isConfirmed*/) + return { + resultCode: 1, + data: { + accessToken: null, + refreshToken: null, + }, + }; + const isPasswordValid = await this.isPasswordCorrect( + password, + user.password, + ); + if (!isPasswordValid) { + return { + resultCode: 1, + data: { + token: { + accessToken: null, + refreshToken: null, + }, + }, + }; + } + const tokensPair = this.createJwtTokensPair(user.id, user.email); + return { + resultCode: 0, + data: tokensPair, + }; + } + + private async isPasswordCorrect(password: string, hash: string) { + return bcrypt.compare(password, hash); + } + + async confirmEmail(token: string): Promise { + const user = await this.usersRepository.findUserByVerificationToken(token); + if (!user || user.isEmailVerified) return false; + const dbToken = user.verificationToken; + const isTokenExpired = isAfter(user.verificationTokenExpiry, new Date()); + if (dbToken !== token || isTokenExpired) { + return false; + } + + return await this.usersRepository.updateConfirmation(user.id); + } + + async resendCode(email: string) { + const user = await this.usersRepository.findUserByEmail(email); + if (!user || user?.verification.isEmailVerified) return null; + const updatedUser = await this.usersRepository.updateVerificationToken( + user.id, + ); + if (!updatedUser) return null; + + return true; + } +} diff --git a/src/modules/auth/dto/registration.dto.ts b/src/modules/auth/dto/registration.dto.ts new file mode 100644 index 0000000..6f55a32 --- /dev/null +++ b/src/modules/auth/dto/registration.dto.ts @@ -0,0 +1,10 @@ +import { IsEmail, Length } from 'class-validator'; + +export class RegistrationDto { + @Length(3, 30) + name: string; + @Length(3, 30) + password: string; + @IsEmail() + email: string; +} diff --git a/src/modules/auth/dto/update-auth.dto.ts b/src/modules/auth/dto/update-auth.dto.ts new file mode 100644 index 0000000..d4602e9 --- /dev/null +++ b/src/modules/auth/dto/update-auth.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { RegistrationDto } from './registration.dto'; + +export class UpdateAuthDto extends PartialType(RegistrationDto) {} diff --git a/src/modules/auth/entities/auth.entity.ts b/src/modules/auth/entities/auth.entity.ts new file mode 100644 index 0000000..15f15a8 --- /dev/null +++ b/src/modules/auth/entities/auth.entity.ts @@ -0,0 +1 @@ +export class Auth {} diff --git a/src/modules/auth/guards/auth.guard.ts b/src/modules/auth/guards/auth.guard.ts new file mode 100644 index 0000000..e35f0b6 --- /dev/null +++ b/src/modules/auth/guards/auth.guard.ts @@ -0,0 +1,52 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; +import * as jwt from 'jsonwebtoken'; +import { UsersRepository } from '../../users/infrastructure/users.repository'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private readonly usersRepository: UsersRepository) {} + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + if (!request.headers || !request.headers.authorization) { + throw new BadRequestException([{ message: 'No any auth headers' }]); + } + const authorizationData = request.headers.authorization.split(' '); + const token = authorizationData[1]; + const tokenName = authorizationData[0]; + if (tokenName != 'Bearer') { + throw new UnauthorizedException([ + { + message: 'login or password invalid', + }, + ]); + } + try { + const secretKey = process.env.JWT_SECRET_KEY; + const decoded: any = jwt.verify(token, secretKey!); + const user = await this.usersRepository.findUserById(decoded.userId); + if (!user) { + throw new NotFoundException([ + { + field: 'token', + message: 'user not found', + }, + ]); + } + } catch (e) { + console.log(e); + throw new UnauthorizedException([ + { + message: 'login or password invalid', + }, + ]); + } + return true; + } +} diff --git a/src/modules/auth/guards/base-auth.guard.ts b/src/modules/auth/guards/base-auth.guard.ts new file mode 100644 index 0000000..3c4ce4f --- /dev/null +++ b/src/modules/auth/guards/base-auth.guard.ts @@ -0,0 +1,29 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class BaseAuthGuard implements CanActivate { + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + const exceptedAuthInput = 'Basic YWRtaW46cXdlcnR5'; + if (!request.headers || !request.headers.authorization) { + throw new UnauthorizedException([{ message: 'No any auth headers' }]); + } else { + if (request.headers.authorization != exceptedAuthInput) { + throw new UnauthorizedException([ + { + message: 'login or password invalid', + }, + ]); + } + } + return true; + } +} diff --git a/src/modules/auth/guards/jwt-auth.guard.ts b/src/modules/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..f67ef53 --- /dev/null +++ b/src/modules/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,25 @@ +import { + ExecutionContext, + Injectable, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor() { + super(); + } + @UsePipes(new ValidationPipe()) + validateLoginDto(): void {} + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + const res: boolean = await (super.canActivate(context) as Promise); + if (!res) return false; + + // check DTO + return res; + } +} diff --git a/src/modules/auth/guards/local-auth.guard.ts b/src/modules/auth/guards/local-auth.guard.ts new file mode 100644 index 0000000..ccf962b --- /dev/null +++ b/src/modules/auth/guards/local-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class LocalAuthGuard extends AuthGuard('local') {} diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..751f97d --- /dev/null +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -0,0 +1,21 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { AppSettings } from '../../../settings/app-settings'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + @Inject(AppSettings.name) private readonly appSettings: AppSettings, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: true, + secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY, + }); + } + + async validate(payload: any) { + return { userId: payload.userId }; + } +} diff --git a/src/modules/auth/strategies/local.strategy.ts b/src/modules/auth/strategies/local.strategy.ts new file mode 100644 index 0000000..e21a355 --- /dev/null +++ b/src/modules/auth/strategies/local.strategy.ts @@ -0,0 +1,21 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-local'; +import { AuthService } from '../auth.service'; + +@Injectable() +export class LocalStrategy extends PassportStrategy(Strategy) { + constructor(private readonly authService: AuthService) { + super({ + usernameField: 'login', + }); + } + + async validate(login: string, password: string): Promise { + const user = await this.authService.checkCredentials(login, password); + if (user.resultCode === 1) { + throw new UnauthorizedException(); + } + return user; + } +} diff --git a/src/modules/core/validation/notification.ts b/src/modules/core/validation/notification.ts new file mode 100644 index 0000000..a4f5e2c --- /dev/null +++ b/src/modules/core/validation/notification.ts @@ -0,0 +1,69 @@ +import { IEvent } from '@nestjs/cqrs'; + +export class ResultNotification { + constructor(data: T | null = null) { + this.data = data; + } + + extensions: NotificationExtension[] = []; + code = 0; + data: T | null = null; + + hasError() { + return this.code !== 0; + } + + addError( + message: string, + key: string | null = null, + code: number | null = null, + ) { + this.code = code ?? 1; + this.extensions.push(new NotificationExtension(message, key)); + } + + addData(data: T) { + this.data = data; + } +} + +export class NotificationExtension { + constructor(public message: string, public key: string | null) {} +} + +export class DomainResultNotification< + TData = null, +> extends ResultNotification { + public events: IEvent[] = []; + addEvents(...events: IEvent[]) { + this.events = [...this.events, ...events]; + } + + static create( + mainNotification: DomainResultNotification, + ...otherNotifications: DomainResultNotification[] + ) { + const domainResultNotification = new DomainResultNotification(); + + if (!!mainNotification.data) { + domainResultNotification.addData(mainNotification.data); + } + domainResultNotification.events = mainNotification.events; + + mainNotification.extensions.forEach((e) => { + domainResultNotification.addError(e.message, e.key); + }); + + otherNotifications.forEach((n) => { + domainResultNotification.events = [ + ...domainResultNotification.events, + ...n.events, + ]; + n.extensions.forEach((e) => { + domainResultNotification.addError(e.message, e.key); + }); + }); + + return domainResultNotification; + } +} diff --git a/src/modules/core/validation/validation.utils.ts b/src/modules/core/validation/validation.utils.ts new file mode 100644 index 0000000..14ead9a --- /dev/null +++ b/src/modules/core/validation/validation.utils.ts @@ -0,0 +1,58 @@ +import { DomainResultNotification, ResultNotification } from './notification'; +import { validateOrReject } from 'class-validator'; +import { IEvent } from '@nestjs/cqrs'; +import { + validationErrorsMapper, + ValidationPipeErrorType, +} from '../../../settings/pipes-setup'; + +export class DomainError extends Error { + constructor(message: string, public resultNotification: ResultNotification) { + super(message); + } +} + +export const validateEntityOrThrow = async (entity: any) => { + try { + await validateOrReject(entity); + } catch (errors) { + const resultNotification: ResultNotification = mapErrorsToNotification( + validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray( + errors, + ), + ); + + throw new DomainError('domain entity validation error', resultNotification); + } +}; + +export const validateEntity = async ( + entity: T, + events: IEvent[], +): Promise> => { + try { + await validateOrReject(entity); + } catch (errors) { + const resultNotification: DomainResultNotification = + mapErrorsToNotification( + validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray( + errors, + ), + ); + resultNotification.addData(entity); + resultNotification.addEvents(...events); + return resultNotification; + } + const domainResultNotification = new DomainResultNotification(entity); + domainResultNotification.addEvents(...events); + + return domainResultNotification; +}; + +export function mapErrorsToNotification(errors: ValidationPipeErrorType[]) { + const resultNotification = new DomainResultNotification(); + errors.forEach((item: ValidationPipeErrorType) => + resultNotification.addError(item.message, item.field, 1), + ); + return resultNotification; +} diff --git a/src/modules/users/api/users.controller.ts b/src/modules/users/api/users.controller.ts new file mode 100644 index 0000000..6fa20c3 --- /dev/null +++ b/src/modules/users/api/users.controller.ts @@ -0,0 +1,47 @@ +import { + Body, + Controller, + Delete, + Get, + NotFoundException, + Param, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import { UsersService } from '../services/users.service'; +import { CreateUserDto } from '../dto/create-user.dto'; +import { Pagination } from '../../../infrastructure/common/pagination.service'; +import { BaseAuthGuard } from '../../auth/guards/base-auth.guard'; + +@Controller('users') +export class UsersController { + constructor(private usersService: UsersService) {} + + @Get() + async findAll(@Query() query) { + const { page, pageSize, searchNameTerm } = + Pagination.getPaginationData(query); + const users = await this.usersService.getUsers( + page, + pageSize, + searchNameTerm, + ); + if (!users) throw new NotFoundException('Users not found'); + return users; + } + //@UseGuards(BaseAuthGuard) + @Post() + async create(@Body() createUserDto: CreateUserDto) { + return await this.usersService.createUser( + createUserDto.login, + createUserDto.password, + createUserDto.email, + ); + } + @UseGuards(BaseAuthGuard) + @Delete(':id') + async remove(@Param('id') id: string) { + return await this.usersService.deleteUserById(id); + } +} diff --git a/src/modules/users/dto/create-user.dto.ts b/src/modules/users/dto/create-user.dto.ts new file mode 100644 index 0000000..eb57c78 --- /dev/null +++ b/src/modules/users/dto/create-user.dto.ts @@ -0,0 +1,10 @@ +import { Length, Matches } from 'class-validator'; + +export class CreateUserDto { + @Length(3, 10) + login: string; + @Length(6, 20) + password: string; + @Matches(/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/) + email: string; +} diff --git a/src/users/dto/update-user.dto.ts b/src/modules/users/dto/update-user.dto.ts similarity index 100% rename from src/users/dto/update-user.dto.ts rename to src/modules/users/dto/update-user.dto.ts diff --git a/src/modules/users/infrastructure/users.repository.ts b/src/modules/users/infrastructure/users.repository.ts new file mode 100644 index 0000000..58122a6 --- /dev/null +++ b/src/modules/users/infrastructure/users.repository.ts @@ -0,0 +1,164 @@ +import { + CreateUserInput, + EntityWithPaginationType, + User, + UserViewType, + VerificationWithUser, +} from '../../../types/types'; +import { Injectable } from '@nestjs/common'; +import { addHours } from 'date-fns'; +import { IUsersRepository } from '../services/users.service'; +import { v4 as uuidv4 } from 'uuid'; +import { PrismaService } from '../../../prisma.service'; + +@Injectable() +export class UsersRepository implements IUsersRepository { + constructor(private prisma: PrismaService) {} + + async getUsers( + currentPage: number, + itemsPerPage: number, + searchNameTerm: string, + ): Promise> { + const where = { + name: { + search: searchNameTerm, + }, + }; + const [totalItems, users] = await this.prisma.$transaction([ + this.prisma.user.count({ where }), + this.prisma.user.findMany({ + where, + skip: (currentPage - 1) * itemsPerPage, + take: itemsPerPage, + }), + ]); + + console.log(users, 'usersFromBase'); + const totalPages = Math.ceil(totalItems / itemsPerPage); + const usersView = users.map((u) => ({ + id: u.id, + name: u.name, + email: u.email, + })); + console.log(usersView, 'users---'); + return { + totalPages, + currentPage, + itemsPerPage, + totalItems, + items: usersView, + }; + } + + async createUser(newUser: CreateUserInput): Promise { + return await this.prisma.user.create({ + data: { + email: newUser.email, + password: newUser.password, + name: newUser.name, + verification: { + create: { + verificationToken: newUser.verificationToken, + verificationTokenExpiry: newUser.verificationTokenExpiry, + }, + }, + }, + include: { + verification: true, + }, + }); + } + + async deleteUserById(id: string): Promise { + const result = await this.prisma.user.delete({ + where: { + id, + }, + }); + return result.isDeleted; + } + + async findUserById(id: string): Promise { + const user = await this.prisma.user.findUnique({ where: { id } }); + if (!user) { + return null; + } + + return user; + } + + async findUserByEmail(email: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { email }, + include: { verification: true }, + }); + + if (!user) { + return null; + } + return user; + } + + async findUserByVerificationToken( + token: string, + ): Promise { + const verification = await this.prisma.verification.findUnique({ + where: { + verificationToken: token, + }, + include: { + user: true, + }, + }); + if (!verification) { + return null; + } + return verification; + } + + async updateConfirmation(id: string) { + const result = await this.prisma.verification.update({ + where: { + userId: id, + }, + data: { + isEmailVerified: true, + }, + }); + return result.isEmailVerified; + } + + async updateVerificationToken(id: string) { + return await this.prisma.verification.update({ + where: { + userId: id, + }, + data: { + verificationToken: uuidv4(), + verificationTokenExpiry: addHours(new Date(), 24), + }, + include: { + user: true, + }, + }); + } + + async revokeToken(id: string, token: string): Promise { + const revokedToken = await this.prisma.accessToken.update({ + where: { + token: token, + }, + data: { + isRevoked: true, + }, + include: { + user: true, + }, + }); + if (!revokedToken.user) { + return null; + } + return revokedToken.user; + } +} diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts new file mode 100644 index 0000000..3d61b67 --- /dev/null +++ b/src/modules/users/services/users.service.ts @@ -0,0 +1,87 @@ +import { v4 as uuidv4 } from 'uuid'; +import { + CreateUserInput, + EntityWithPaginationType, + User, + UserViewType, +} from '../../../types/types'; +import { addHours } from 'date-fns'; +import { Injectable } from '@nestjs/common'; +import jwt from 'jsonwebtoken'; +import { UsersRepository } from '../infrastructure/users.repository'; +import * as bcrypt from 'bcrypt'; + +@Injectable() +export class UsersService { + constructor(private usersRepository: UsersRepository) {} + + async getUsers(page: number, pageSize: number, searchNameTerm: string) { + return await this.usersRepository.getUsers(page, pageSize, searchNameTerm); + } + + async getUserById(id: string) { + return await this.usersRepository.findUserById(id); + } + + async createUser( + name: string, + password: string, + email: string, + ): Promise { + const passwordHash = await this._generateHash(password); + const newUser: CreateUserInput = { + name: name || email.split('@')[0], + email: email, + password: passwordHash, + verificationToken: uuidv4(), + verificationTokenExpiry: addHours(new Date(), 24), + isEmailVerified: false, + }; + const createdUser = await this.usersRepository.createUser(newUser); + if (!createdUser) { + return null; + } + + return { + id: createdUser.id, + name: createdUser.name, + email: createdUser.email, + }; + } + + async deleteUserById(id: string): Promise { + return await this.usersRepository.deleteUserById(id); + } + + async addRevokedToken(token: string) { + const secretKey = process.env.JWT_SECRET_KEY; + if (!secretKey) throw new Error('JWT_SECRET_KEY is not defined'); + + try { + const decoded: any = jwt.verify(token, secretKey); + return this.usersRepository.revokeToken(decoded.userId, token); + } catch (e) { + console.log('Decoding error: e'); + return null; + } + } + private async _generateHash(password: string) { + return await bcrypt.hash(password, 10); + } +} + +export interface IUsersRepository { + getUsers( + page: number, + pageSize: number, + searchNameTerm: string, + ): Promise>; + + createUser(newUser: CreateUserInput): Promise; + + deleteUserById(id: string): Promise; + + findUserById(id: string): Promise; + + revokeToken(id: string, token: string): Promise; +} diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts new file mode 100644 index 0000000..9fe8f01 --- /dev/null +++ b/src/modules/users/users.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { UsersService } from './services/users.service'; +import { UsersController } from './api/users.controller'; +import { UsersRepository } from './infrastructure/users.repository'; + +@Module({ + controllers: [UsersController], + providers: [UsersService, UsersRepository], + exports: [UsersRepository, UsersService], +}) +export class UsersModule {} diff --git a/src/settings/app-settings.ts b/src/settings/app-settings.ts new file mode 100644 index 0000000..4477cde --- /dev/null +++ b/src/settings/app-settings.ts @@ -0,0 +1,54 @@ +//базовые настройки env переменных +//по умолчанию переменные беруться сначала из ENV илм смотрят всегда на staging +//для подстановки локальных значений переменных использовать исключительно локальные env файлы env.development.local +//при необзодимости добавляем сюда нужные приложению переменные +import * as dotenv from 'dotenv'; +dotenv.config(); + +export type EnvironmentVariable = { [key: string]: string | undefined }; +export type EnvironmentsTypes = + | 'DEVELOPMENT' + | 'STAGING' + | 'PRODUCTION' + | 'TEST'; +export class EnvironmentSettings { + constructor(private env: EnvironmentsTypes) {} + getEnv() { + return this.env; + } + isProduction() { + return this.env === 'PRODUCTION'; + } + isStaging() { + return this.env === 'STAGING'; + } + isDevelopment() { + return this.env === 'DEVELOPMENT'; + } + isTesting() { + return this.env === 'TEST'; + } +} + +class AuthSettings { + public readonly BASE_AUTH_HEADER: string; + public readonly ACCESS_JWT_SECRET_KEY: string; + public readonly REFRESH_JWT_SECRET_KEY: string; + constructor(private envVariables: EnvironmentVariable) { + this.BASE_AUTH_HEADER = + envVariables.BASE_AUTH_HEADER || 'Basic YWRtaW46cXdlcnR5'; + this.ACCESS_JWT_SECRET_KEY = + envVariables.ACCESS_JWT_SECRET_KEY || 'accessJwtSecret'; + this.REFRESH_JWT_SECRET_KEY = + envVariables.REFRESH_JWT_SECRET_KEY || 'refreshJwtSecret'; + } +} + +export class AppSettings { + constructor(public env: EnvironmentSettings, public auth: AuthSettings) {} +} +const env = new EnvironmentSettings( + (process.env.NODE_ENV || 'DEVELOPMENT') as EnvironmentsTypes, +); +const auth = new AuthSettings(process.env); +export const appSettings = new AppSettings(env, auth); diff --git a/src/settings/config.module.ts b/src/settings/config.module.ts new file mode 100644 index 0000000..fa434ee --- /dev/null +++ b/src/settings/config.module.ts @@ -0,0 +1,16 @@ +import { Global, Module } from '@nestjs/common'; +import { appSettings, AppSettings } from './app-settings'; + +//главный config модуль для управления env переменными импортируется в app.module.ts глобально +//поскольку он глобальный то импортировать в каждый модуль его не надо +@Global() +@Module({ + providers: [ + { + provide: AppSettings.name, + useFactory: () => appSettings, + }, + ], + exports: [AppSettings.name], +}) +export class ConfigModule {} diff --git a/src/settings/pipes-setup.ts b/src/settings/pipes-setup.ts new file mode 100644 index 0000000..10e0b49 --- /dev/null +++ b/src/settings/pipes-setup.ts @@ -0,0 +1,43 @@ +import { + BadRequestException, + INestApplication, + ValidationPipe, +} from '@nestjs/common'; +import { ValidationError } from 'class-validator'; + +export const validationErrorsMapper = { + mapValidationErrorArrayToValidationPipeErrorTypeArray( + errors: ValidationError[], + ): ValidationPipeErrorType[] { + return errors.flatMap((error) => { + const constraints = error.constraints ?? []; + return Object.entries(constraints).map(([_, value]) => ({ + field: error.property, + message: value, + })); + }); + }, +}; + +export function pipesSetup(app: INestApplication) { + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + + stopAtFirstError: true, + exceptionFactory: (errors: ValidationError[]) => { + const err = + validationErrorsMapper.mapValidationErrorArrayToValidationPipeErrorTypeArray( + errors, + ); + throw new BadRequestException(err); + }, + }), + ); +} + +export type ValidationPipeErrorType = { + field: string; + message: string; +}; diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 0000000..8d871e9 --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1,145 @@ +import { Prisma } from '@prisma/client'; +export type NewestLikesType = { + id: string; + login: string; + addedAt: Date; +}; +export type ExtendedLikesInfoType = { + dislikesCount: number; + likesCount: number; + myStatus: string; + newestLikes: Array; +}; +export type PostType = { + addedAt: Date; + id?: string; + title: string | null; + shortDescription: string | null; + content: string | null; + blogId: string; + blogName?: string | null; + extendedLikesInfo: ExtendedLikesInfoType; +}; +export type BlogType = { + id: string; + name: string | null; + youtubeUrl: string | null; +}; + +export type LikeType = { + userId: string; + login: string; + action: string; + addedAt: Date; +}; + +export type CommentType = { + id: string; + content: string; //20 = { + totalPages: number; + currentPage: number; + itemsPerPage: number; + totalItems: number; + items: T[]; +}; + +export type QueryDataType = { + page: number; + pageSize: number; + searchNameTerm: string; +}; +export type ErrorMessageType = { + message: string; + field: string; +}; + +const userInclude: Prisma.UserInclude = { + verification: true, +}; + +export type VerificationWithUser = Prisma.VerificationGetPayload<{ + include: { user: true }; +}>; + +export type User = Prisma.UserGetPayload<{ + include: typeof userInclude; +}>; + +export type CreateUserInput = Omit< + Prisma.UserCreateInput & Prisma.VerificationCreateInput, + 'user' +>; + +export type UserType = { + accountData: Prisma.UserCreateInput; + emailConfirmation: EmailConfirmationType; +}; + +export type UserViewType = { + id: string; + name: string; + email: string; +}; + +export type UserAccountType = { + id: string; + email: string; + login: string; + passwordHash: string; + createdAt: Date; + revokedTokens?: string[] | null; +}; +export type SentConfirmationEmailType = { + sentDate: Date; +}; + +export type LoginAttemptType = { + attemptDate: Date; + ip: string; +}; + +export type EmailConfirmationType = { + isConfirmed: boolean; + confirmationCode: string; + expirationDate: Date; + sentEmails?: SentConfirmationEmailType[]; +}; + +export type LimitsControlType = { + userIp: string; + url: string; + time: Date; +}; +export type CheckLimitsType = { + login: string | null; + userIp: string; + url: string; + time: Date; +}; + +export type EmailConfirmationMessageType = { + email: string; + message: string; + subject: string; + isSent: boolean; + createdAt: Date; +}; + +export enum LikeAction { + Like = 'Like', + Dislike = 'Dislike', + None = 'None', +} +export type LikeActionType = 'Like' | 'Dislike' | 'None'; diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts deleted file mode 100644 index 5afa753..0000000 --- a/src/users/dto/create-user.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IsEmail, IsString, Length } from 'class-validator'; - -export class CreateUserDto { - @IsEmail() - email: string; - - @IsString() - password: string; - - @IsString() - @Length(2, 40) - name: string; -} diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts deleted file mode 100644 index 4f82c14..0000000 --- a/src/users/entities/user.entity.ts +++ /dev/null @@ -1 +0,0 @@ -export class User {} diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts deleted file mode 100644 index a76d310..0000000 --- a/src/users/users.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { UsersController } from './users.controller'; -import { UsersService } from './users.service'; - -describe('UsersController', () => { - let controller: UsersController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [UsersController], - providers: [UsersService], - }).compile(); - - controller = module.get(UsersController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts deleted file mode 100644 index b21a836..0000000 --- a/src/users/users.controller.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, -} from '@nestjs/common'; -import { UsersService } from './users.service'; -import { CreateUserDto } from './dto/create-user.dto'; -import { UpdateUserDto } from './dto/update-user.dto'; - -@Controller('users') -export class UsersController { - constructor(private readonly usersService: UsersService) {} - - @Post() - create(@Body() createUserDto: CreateUserDto) { - return this.usersService.create(createUserDto); - } - - @Get() - findAll() { - return this.usersService.findAll(); - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.usersService.findOne(id); - } - - @Patch(':id') - update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { - return this.usersService.update(id, updateUserDto); - } - - @Delete(':id') - remove(@Param('id') id: string) { - return this.usersService.remove(id); - } -} diff --git a/src/users/users.module.ts b/src/users/users.module.ts deleted file mode 100644 index ecca17a..0000000 --- a/src/users/users.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UsersService } from './users.service'; -import { UsersController } from './users.controller'; - -@Module({ - controllers: [UsersController], - providers: [UsersService], -}) -export class UsersModule {} diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts deleted file mode 100644 index 62815ba..0000000 --- a/src/users/users.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -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/src/users/users.service.ts b/src/users/users.service.ts deleted file mode 100644 index 00f12ee..0000000 --- a/src/users/users.service.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { CreateUserDto } from './dto/create-user.dto'; -import { UpdateUserDto } from './dto/update-user.dto'; -import { PrismaService } from '../prisma.service'; - -@Injectable() -export class UsersService { - constructor(private prisma: PrismaService) {} - - create(createUserDto: CreateUserDto) { - return this.prisma.user.create({ - data: createUserDto, - }); - } - - findAll() { - return this.prisma.user.findMany(); - } - - findOne(id: string) { - return this.prisma.user.findUnique({ - where: { id }, - }); - } - - update(id: string, updateUserDto: UpdateUserDto) { - return this.prisma.user.update({ - where: { id }, - data: updateUserDto, - }); - } - - remove(id: string) { - return this.prisma.user.delete({ - where: { id }, - }); - } -}