From 5e8999d50468b65b30472b9daff79b7d03f495ff Mon Sep 17 00:00:00 2001 From: Harsh Makwana Date: Wed, 1 Nov 2023 01:40:44 +0530 Subject: [PATCH] feat: auth service improvement changes --- .env.local | 30 +- .husky/pre-commit | 2 +- .prettierrc | 16 - Dockerfile.dev | 2 +- junit.xml | 33 -- package-lock.json | 366 ++++++++++-------- package.json | 34 +- .../migration.sql | 8 + .../migration.sql | 2 + .../migration.sql | 2 + prisma/schema.prisma | 3 +- src/app.controller.ts | 21 - src/app/app.controller.ts | 32 ++ src/{ => app}/app.module.ts | 29 +- src/common/auth/auth.module.ts | 39 ++ .../auth/controllers/auth.controller.ts | 30 ++ src/common/auth/dtos/login.dto.ts | 14 + src/common/auth/dtos/signup.dto.ts | 24 ++ src/common/auth/guards/jwt-access.guard.ts | 12 + .../interfaces/auth.response.interface.ts | 6 + .../auth/interfaces/auth.service.interface.ts | 10 + src/common/auth/services/auth.service.ts | 103 +++++ src/common/auth/services/helper.service.ts | 18 + .../auth/strategies/jwt-access.strategy.ts | 23 ++ src/common/common.module.ts | 23 ++ .../congnito-auth/cognito-auth.module.ts} | 39 +- .../controllers/cognito-auth.controller.ts | 47 +++ .../congnito-auth/dtos/cognito-login.dto.ts} | 0 .../congnito-auth/dtos/cognito-signup.dto.ts} | 0 .../congnito-auth/dtos/cognito-verify.dto.ts} | 0 .../guards/cognito-auth.guard.ts} | 2 +- .../services/cognito-auth.service.ts} | 39 +- .../services/cognito-jwt.service.ts} | 23 +- .../services/cognito.service.ts | 14 +- src/{ => common}/services/prisma.service.ts | 0 src/config/app.config.ts | 22 ++ src/config/auth.config.ts | 21 + src/config/config.module.ts | 9 - src/config/config.service.ts | 35 -- src/config/doc.config.ts | 11 + src/config/index.ts | 6 + src/config/rmq.config.ts | 10 + src/core/core.module.ts | 25 ++ ...er.decorator.ts => auth-user.decorator.ts} | 2 +- src/core/decorators/index.ts | 3 - ...allow.decorator.ts => public.decorator.ts} | 0 src/core/dtos/index.ts | 3 - src/core/guards/index.ts | 2 - src/core/index.ts | 4 - src/core/interceptor/index.ts | 2 - .../exception.interceptor.ts | 0 .../response.interceptor.ts | 0 src/i18n/en/translation.json | 3 +- src/main.ts | 73 ++-- .../user/controllers/auth.controller.ts | 8 + .../user/controllers/user.controller.ts | 8 + src/modules/user/services/user.service.ts | 8 + src/modules/user/user.module.ts | 11 + src/modules/v1/auth.controller.ts | 60 --- src/services/firebase.service.ts | 40 -- src/services/index.ts | 4 - src/swagger.ts | 44 +++ src/types/index.ts | 39 -- test/app.e2e-spec.ts | 4 +- tsconfig.json | 18 +- 65 files changed, 938 insertions(+), 583 deletions(-) delete mode 100644 .prettierrc delete mode 100644 junit.xml create mode 100644 prisma/migrations/20231031195314_password_added/migration.sql create mode 100644 prisma/migrations/20231031200020_cognito_sub_optional/migration.sql create mode 100644 prisma/migrations/20231031200454_password_optional/migration.sql delete mode 100644 src/app.controller.ts create mode 100644 src/app/app.controller.ts rename src/{ => app}/app.module.ts (56%) create mode 100644 src/common/auth/auth.module.ts create mode 100644 src/common/auth/controllers/auth.controller.ts create mode 100644 src/common/auth/dtos/login.dto.ts create mode 100644 src/common/auth/dtos/signup.dto.ts create mode 100644 src/common/auth/guards/jwt-access.guard.ts create mode 100644 src/common/auth/interfaces/auth.response.interface.ts create mode 100644 src/common/auth/interfaces/auth.service.interface.ts create mode 100644 src/common/auth/services/auth.service.ts create mode 100644 src/common/auth/services/helper.service.ts create mode 100644 src/common/auth/strategies/jwt-access.strategy.ts create mode 100644 src/common/common.module.ts rename src/{modules/v1/auth.module.ts => common/congnito-auth/cognito-auth.module.ts} (55%) create mode 100644 src/common/congnito-auth/controllers/cognito-auth.controller.ts rename src/{core/dtos/login.dto.ts => common/congnito-auth/dtos/cognito-login.dto.ts} (100%) rename src/{core/dtos/signup.dto.ts => common/congnito-auth/dtos/cognito-signup.dto.ts} (100%) rename src/{core/dtos/verify.dto.ts => common/congnito-auth/dtos/cognito-verify.dto.ts} (100%) rename src/{core/guards/auth.guard.ts => common/congnito-auth/guards/cognito-auth.guard.ts} (89%) rename src/{modules/v1/auth.service.ts => common/congnito-auth/services/cognito-auth.service.ts} (68%) rename src/{services/jwt.service.ts => common/congnito-auth/services/cognito-jwt.service.ts} (72%) rename src/{ => common/congnito-auth}/services/cognito.service.ts (85%) rename src/{ => common}/services/prisma.service.ts (100%) create mode 100644 src/config/app.config.ts create mode 100644 src/config/auth.config.ts delete mode 100644 src/config/config.module.ts delete mode 100644 src/config/config.service.ts create mode 100644 src/config/doc.config.ts create mode 100644 src/config/index.ts create mode 100644 src/config/rmq.config.ts create mode 100644 src/core/core.module.ts rename src/core/decorators/{user.decorator.ts => auth-user.decorator.ts} (80%) delete mode 100644 src/core/decorators/index.ts rename src/core/decorators/{allow.decorator.ts => public.decorator.ts} (100%) delete mode 100644 src/core/dtos/index.ts delete mode 100644 src/core/guards/index.ts delete mode 100644 src/core/index.ts delete mode 100644 src/core/interceptor/index.ts rename src/core/{interceptor => interceptors}/exception.interceptor.ts (100%) rename src/core/{interceptor => interceptors}/response.interceptor.ts (100%) create mode 100644 src/modules/user/controllers/auth.controller.ts create mode 100644 src/modules/user/controllers/user.controller.ts create mode 100644 src/modules/user/services/user.service.ts create mode 100644 src/modules/user/user.module.ts delete mode 100644 src/modules/v1/auth.controller.ts delete mode 100644 src/services/firebase.service.ts delete mode 100644 src/services/index.ts create mode 100644 src/swagger.ts delete mode 100644 src/types/index.ts diff --git a/.env.local b/.env.local index c9149bd..7d6a5c3 100644 --- a/.env.local +++ b/.env.local @@ -1,9 +1,21 @@ -NODE_ENV=development -PORT=9001 -RABBITMQ_URL=amqp://admin:master123@rabbitmq:5672 -RABBITMQ_FILES_QUEUE=files_queue -RABBITMQ_AUTH_QUEUE=auth_queue -DATABASE_URL="postgresql://admin:master123@postgres:5432/user-db?schema=public" -AWS_COGNITO_USER_POOL_ID=ap-south-1_gR5KggzuW -AWS_COGNITO_CLIENT_ID=5vqpp7t39j2vl5lr9vjmcmurjq -AWS_COGNITO_AUTHORITY=https://cognito-idp.ap-south-1.amazonaws.com \ No newline at end of file +APP_NAME="auth" +APP_ENV="development" + +HTTP_ENABLE=true +HTTP_HOST="0.0.0.0" +HTTP_PORT=9001 +HTTP_VERSIONING_ENABLE=true +HTTP_VERSION=1 + +ACCESS_TOKEN_SECRET_KEY=1234567890 +ACCESS_TOKEN_EXPIRED="14d" + +RABBITMQ_URL="amqp://admin:master123@localhost:5672" +RABBITMQ_FILES_QUEUE="files_queue" +RABBITMQ_AUTH_QUEUE="auth_queue" + +DATABASE_URL="postgresql://admin:master123@localhost:5432/user-db?schema=public" + +AWS_COGNITO_USER_POOL_ID="ap-south-1_gR5KggzuW" +AWS_COGNITO_CLIENT_ID="5vqpp7t39j2vl5lr9vjmcmurjq" +AWS_COGNITO_AUTHORITY="https://cognito-idp.ap-south-1.amazonaws.com" diff --git a/.husky/pre-commit b/.husky/pre-commit index a1c8f7f..36af219 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npm run pre-commit \ No newline at end of file +npx lint-staged diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 460e6f6..0000000 --- a/.prettierrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "singleQuote": true, - "trailingComma": "all", - "useTabs": false, - "tabWidth": 2, - "overrides": [ - { - "files": "*.yml", - "options": { - "tadWidth": 2, - "printWidth": 40, - "singleQuote": true - } - } - ] -} \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev index 1aca948..898541a 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM node:14 AS builder +FROM node:16 AS builder WORKDIR /app diff --git a/junit.xml b/junit.xml deleted file mode 100644 index 3e97577..0000000 --- a/junit.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - TypeError: Cannot read property 'findUnique' of undefined - at AppService.signup (/home/harsh-simform/nestjs-microservices/auth/src/app.service.ts:192:48) - at Object.<anonymous> (/home/harsh-simform/nestjs-microservices/auth/src/app.service.spec.ts:60:36) - at Promise.then.completed (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/utils.js:333:28) - at new Promise (<anonymous>) - at callAsyncCircusFn (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/utils.js:259:10) - at _callCircusTest (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/run.js:277:40) - at processTicksAndRejections (internal/process/task_queues.js:95:5) - at _runTest (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/run.js:209:3) - at _runTestsForDescribeBlock (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/run.js:97:9) - at _runTestsForDescribeBlock (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/run.js:91:9) - - - TypeError: Cannot read property 'findUnique' of undefined - at AppService.login (/home/harsh-simform/nestjs-microservices/auth/src/app.service.ts:151:43) - at Object.<anonymous> (/home/harsh-simform/nestjs-microservices/auth/src/app.service.spec.ts:66:36) - at Promise.then.completed (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/utils.js:333:28) - at new Promise (<anonymous>) - at callAsyncCircusFn (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/utils.js:259:10) - at _callCircusTest (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/run.js:277:40) - at processTicksAndRejections (internal/process/task_queues.js:95:5) - at _runTest (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/run.js:209:3) - at _runTestsForDescribeBlock (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/run.js:97:9) - at _runTestsForDescribeBlock (/home/harsh-simform/nestjs-microservices/auth/node_modules/jest-circus/build/run.js:91:9) - - - \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9b1980b..b233b54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { - "name": "auth", - "version": "0.0.1", + "name": "@backendworks/auth", + "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "auth", - "version": "0.0.1", + "name": "@backendworks/auth", + "version": "1.0.0", "hasInstallScript": true, - "license": "UNLICENSED", "dependencies": { "@aws-sdk/client-cognito-identity-provider": "^3.281.0", "@nestjs/common": "^9.0.0", + "@nestjs/config": "^3.1.1", "@nestjs/core": "^9.0.0", + "@nestjs/jwt": "^10.1.1", "@nestjs/microservices": "^8.0.6", "@nestjs/passport": "^9.0.3", "@nestjs/platform-express": "^9.0.0", @@ -134,6 +135,22 @@ } } }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@angular-devkit/core/node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -2002,22 +2019,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@eslint/eslintrc/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2035,12 +2036,6 @@ } } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/@eslint/eslintrc/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3605,6 +3600,40 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, + "node_modules/@nestjs/config": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.1.1.tgz", + "integrity": "sha512-qu5QlNiJdqQtOsnB6lx4JCXPQ96jkKUsOGd+JXfXwqJqZcOSAq6heNFg0opW4pq4J/VZoNwoo87TNnx9wthnqQ==", + "dependencies": { + "dotenv": "16.3.1", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21", + "uuid": "9.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/@nestjs/config/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nestjs/core": { "version": "9.3.9", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.3.9.tgz", @@ -3647,6 +3676,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, + "node_modules/@nestjs/jwt": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.1.1.tgz", + "integrity": "sha512-sISYylg8y1Mb7saxPx5Zh11i7v9JOh70CEC/rN6g43MrbFlJ57c1eYFrffxip1YAx3DmV4K67yXob3syKZMOew==", + "dependencies": { + "@types/jsonwebtoken": "9.0.2", + "jsonwebtoken": "9.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.1.tgz", @@ -4062,7 +4103,7 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.10.1.tgz", "integrity": "sha512-B3tcTxjx196nuAu1GOTKO9cGPUgTFHYRdkPkTS4m5ptb2cejyBlH9X7GOfSt3xlI7p4zAJDshJP4JJivCg9ouA==", - "dev": true, + "devOptional": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { @@ -4412,9 +4453,9 @@ "dev": true }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", - "integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==", "dependencies": { "@types/node": "*" } @@ -5124,14 +5165,14 @@ } }, "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" }, "funding": { @@ -5156,6 +5197,22 @@ } } }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -5165,6 +5222,12 @@ "ajv": "^6.9.1" } }, + "node_modules/ajv/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/amazon-cognito-identity-js": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.1.2.tgz", @@ -6579,7 +6642,6 @@ "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", - "dev": true, "engines": { "node": ">=12" } @@ -6955,22 +7017,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/eslint/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -7034,12 +7080,6 @@ "node": ">=10.13.0" } }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/eslint/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -11042,7 +11082,7 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.10.1.tgz", "integrity": "sha512-0jDxgg+DruB1kHVNlcspXQB9au62IFfVg9drkhzXudszHNUAQn0lVuu+T8np0uC2z1nKD5S3qPeCyR8u5YFLnA==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "dependencies": { "@prisma/engines": "4.10.1" @@ -11638,28 +11678,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -13136,6 +13154,18 @@ "source-map": "0.7.4" }, "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, "rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -14698,18 +14728,6 @@ "strip-json-comments": "^3.1.1" }, "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -14719,12 +14737,6 @@ "ms": "2.1.2" } }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -14906,7 +14918,8 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", - "dev": true + "dev": true, + "requires": {} }, "@firebase/component": { "version": "0.6.4", @@ -15089,7 +15102,8 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", - "dev": true + "dev": true, + "requires": {} }, "@firebase/functions": { "version": "0.10.0", @@ -15173,7 +15187,8 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", - "dev": true + "dev": true, + "requires": {} }, "@firebase/logger": { "version": "0.4.0", @@ -15329,7 +15344,8 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", - "dev": true + "dev": true, + "requires": {} }, "@firebase/util": { "version": "1.9.3", @@ -15951,6 +15967,29 @@ } } }, + "@nestjs/config": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.1.1.tgz", + "integrity": "sha512-qu5QlNiJdqQtOsnB6lx4JCXPQ96jkKUsOGd+JXfXwqJqZcOSAq6heNFg0opW4pq4J/VZoNwoo87TNnx9wthnqQ==", + "requires": { + "dotenv": "16.3.1", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21", + "uuid": "9.0.0" + }, + "dependencies": { + "dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + } + } + }, "@nestjs/core": { "version": "9.3.9", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.3.9.tgz", @@ -15971,10 +16010,20 @@ } } }, + "@nestjs/jwt": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.1.1.tgz", + "integrity": "sha512-sISYylg8y1Mb7saxPx5Zh11i7v9JOh70CEC/rN6g43MrbFlJ57c1eYFrffxip1YAx3DmV4K67yXob3syKZMOew==", + "requires": { + "@types/jsonwebtoken": "9.0.2", + "jsonwebtoken": "9.0.0" + } + }, "@nestjs/mapped-types": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.1.tgz", - "integrity": "sha512-NFvofzSinp00j5rzUd4tf+xi9od6383iY0JP7o0Bnu1fuItAUkWBgc4EKuIQ3D+c2QI3i9pG1kDWAeY27EMGtg==" + "integrity": "sha512-NFvofzSinp00j5rzUd4tf+xi9od6383iY0JP7o0Bnu1fuItAUkWBgc4EKuIQ3D+c2QI3i9pG1kDWAeY27EMGtg==", + "requires": {} }, "@nestjs/microservices": { "version": "8.4.7", @@ -15988,7 +16037,8 @@ "@nestjs/passport": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.3.tgz", - "integrity": "sha512-HplSJaimEAz1IOZEu+pdJHHJhQyBOPAYWXYHfAPQvRqWtw4FJF1VXl1Qtk9dcXQX1eKytDtH+qBzNQc19GWNEg==" + "integrity": "sha512-HplSJaimEAz1IOZEu+pdJHHJhQyBOPAYWXYHfAPQvRqWtw4FJF1VXl1Qtk9dcXQX1eKytDtH+qBzNQc19GWNEg==", + "requires": {} }, "@nestjs/platform-express": { "version": "9.3.9", @@ -16170,7 +16220,7 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.10.1.tgz", "integrity": "sha512-B3tcTxjx196nuAu1GOTKO9cGPUgTFHYRdkPkTS4m5ptb2cejyBlH9X7GOfSt3xlI7p4zAJDshJP4JJivCg9ouA==", - "dev": true + "devOptional": true }, "@prisma/engines-version": { "version": "4.10.1-2.aead147aa326ccb985dcfed5b065b4fdabd44b19", @@ -16515,9 +16565,9 @@ "dev": true }, "@types/jsonwebtoken": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", - "integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==", "requires": { "@types/node": "*" } @@ -17042,13 +17092,15 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true + "dev": true, + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "devOptional": true + "devOptional": true, + "requires": {} }, "agent-base": { "version": "6.0.2", @@ -17084,15 +17136,23 @@ } }, "ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "dependencies": { + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } } }, "ajv-formats": { @@ -17102,13 +17162,28 @@ "dev": true, "requires": { "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + } } }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "amazon-cognito-identity-js": { "version": "6.1.2", @@ -18155,8 +18230,7 @@ "dotenv-expand": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", - "dev": true + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==" }, "duplexify": { "version": "4.1.2", @@ -18394,18 +18468,6 @@ "text-table": "^0.2.0" }, "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -18446,12 +18508,6 @@ "is-glob": "^4.0.3" } }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -18464,7 +18520,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz", "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==", - "dev": true + "dev": true, + "requires": {} }, "eslint-plugin-prettier": { "version": "4.2.1", @@ -20003,7 +20060,8 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "28.0.2", @@ -20817,7 +20875,8 @@ "version": "8.6.7", "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", - "optional": true + "optional": true, + "requires": {} }, "marked": { "version": "4.3.0", @@ -21337,7 +21396,8 @@ "pg-pool": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz", - "integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==" + "integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==", + "requires": {} }, "pg-protocol": { "version": "1.6.0", @@ -21509,7 +21569,7 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.10.1.tgz", "integrity": "sha512-0jDxgg+DruB1kHVNlcspXQB9au62IFfVg9drkhzXudszHNUAQn0lVuu+T8np0uC2z1nKD5S3qPeCyR8u5YFLnA==", - "dev": true, + "devOptional": true, "requires": { "@prisma/engines": "4.10.1" } @@ -21937,26 +21997,6 @@ "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - } } }, "semver": { diff --git a/package.json b/package.json index b88b85d..067e4a2 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,31 @@ { - "name": "auth", - "version": "0.0.1", + "name": "@backendworks/auth", + "version": "1.0.0", "private": true, - "license": "UNLICENSED", "scripts": { "prebuild": "rimraf dist", - "postinstall": "npm run gen", + "postinstall": "npm run generate && npx typesync", "build": "nest build", "dev": "dotenv -e .env.local -- nest start --watch", "debug": "nest start --debug --watch", - "gen": "prisma generate", + "generate": "prisma generate", "start": "node dist/main", - "db:migrate": "dotenv -e .env.local -- prisma migrate dev --preview-feature", - "db:migrate:prod": "prisma migrate deploy --preview-feature", + "migrate": "dotenv -e .env.local -- prisma migrate dev", + "migrate:prod": "prisma migrate deploy", "lint": "eslint .", "lint:fix": "eslint --fix .", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:e2e": "jest --config ./test/jest-e2e.json --forceExit", - "prepare": "husky install", - "pre-commit": "lint-staged" + "prepare": "husky install" }, "dependencies": { "@aws-sdk/client-cognito-identity-provider": "^3.281.0", "@nestjs/common": "^9.0.0", + "@nestjs/config": "^3.1.1", "@nestjs/core": "^9.0.0", + "@nestjs/jwt": "^10.1.1", "@nestjs/microservices": "^8.0.6", "@nestjs/passport": "^9.0.3", "@nestjs/platform-express": "^9.0.0", @@ -94,6 +94,22 @@ "tsconfig-paths": "^4.0.0", "typescript": "^4.7.4" }, + "prettier": { + "singleQuote": true, + "trailingComma": "all", + "useTabs": false, + "tabWidth": 2, + "overrides": [ + { + "files": "*.yml", + "options": { + "tadWidth": 2, + "printWidth": 40, + "singleQuote": true + } + } + ] + }, "jest": { "moduleFileExtensions": [ "js", diff --git a/prisma/migrations/20231031195314_password_added/migration.sql b/prisma/migrations/20231031195314_password_added/migration.sql new file mode 100644 index 0000000..05edc77 --- /dev/null +++ b/prisma/migrations/20231031195314_password_added/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "password" TEXT NOT NULL; diff --git a/prisma/migrations/20231031200020_cognito_sub_optional/migration.sql b/prisma/migrations/20231031200020_cognito_sub_optional/migration.sql new file mode 100644 index 0000000..05c5880 --- /dev/null +++ b/prisma/migrations/20231031200020_cognito_sub_optional/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "cognito_sub" DROP NOT NULL; diff --git a/prisma/migrations/20231031200454_password_optional/migration.sql b/prisma/migrations/20231031200454_password_optional/migration.sql new file mode 100644 index 0000000..0b600a7 --- /dev/null +++ b/prisma/migrations/20231031200454_password_optional/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6f8bcb5..9207ebc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,7 +13,8 @@ datasource db { model User { id Int @id @default(autoincrement()) email String @unique - cognito_sub String @unique + password String? + cognito_sub String? @unique first_name String last_name String is_verified Boolean @default(false) diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index 34b623d..0000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { Public } from './core'; -import { PrismaService } from './services'; -import { HealthCheck, HealthCheckService } from '@nestjs/terminus'; - -@Controller() -export class AppController { - constructor( - private healthCheckService: HealthCheckService, - private prismaService: PrismaService, - ) {} - - @Get('/health') - @HealthCheck() - @Public() - public async getHealth() { - return this.healthCheckService.check([ - () => this.prismaService.isHealthy(), - ]); - } -} diff --git a/src/app/app.controller.ts b/src/app/app.controller.ts new file mode 100644 index 0000000..06787f2 --- /dev/null +++ b/src/app/app.controller.ts @@ -0,0 +1,32 @@ +import { Controller, Get } from '@nestjs/common'; +import { HealthCheck, HealthCheckService } from '@nestjs/terminus'; +import { PrismaService } from 'src/common/services/prisma.service'; +import { Public } from 'src/core/decorators/public.decorator'; + +@Controller() +export class AppController { + constructor( + private readonly healthCheckService: HealthCheckService, + private readonly prismaService: PrismaService, + ) {} + + // @MessagePattern('getUserById') + // public async getUserById(@Payload() data: string): Promise { + // const payload = JSON.parse(data); + // return this.authService.getUserById(payload.id); + // } + + // @MessagePattern('validateToken') + // public async getUserByAccessToken(@Payload() token: string) { + // return this.jwtService.validateToken(token); + // } + + @Get('/health') + @HealthCheck() + @Public() + public async getHealth() { + return this.healthCheckService.check([ + () => this.prismaService.isHealthy(), + ]); + } +} diff --git a/src/app.module.ts b/src/app/app.module.ts similarity index 56% rename from src/app.module.ts rename to src/app/app.module.ts index 547aedb..86ad507 100644 --- a/src/app.module.ts +++ b/src/app/app.module.ts @@ -1,23 +1,24 @@ import { Module } from '@nestjs/common'; import { join } from 'path'; -import { APP_GUARD } from '@nestjs/core'; import { TerminusModule } from '@nestjs/terminus'; import { AcceptLanguageResolver, I18nModule, QueryResolver } from 'nestjs-i18n'; -import { JwtService, PrismaService } from './services'; -import { JwtAuthGuard, RolesGuard } from './core'; import { PassportModule } from '@nestjs/passport'; import { AppController } from './app.controller'; -import { AuthModule } from './modules/v1/auth.module'; -import { ConfigService } from 'src/config/config.service'; +import { CoreModule } from 'src/core/core.module'; +import { CommonModule } from 'src/common/common.module'; +import { PrismaService } from 'src/common/services/prisma.service'; +import { UserModule } from 'src/modules/user/user.module'; @Module({ imports: [ - AuthModule, + CoreModule, + CommonModule, + UserModule, PassportModule.register({ defaultStrategy: 'jwt' }), I18nModule.forRoot({ fallbackLanguage: 'en', loaderOptions: { - path: join(__dirname, '/i18n/'), + path: join(__dirname, '../i18n/'), watch: true, }, resolvers: [ @@ -28,18 +29,6 @@ import { ConfigService } from 'src/config/config.service'; TerminusModule, ], controllers: [AppController], - providers: [ - JwtService, - ConfigService, - PrismaService, - { - provide: APP_GUARD, - useClass: JwtAuthGuard, - }, - { - provide: APP_GUARD, - useClass: RolesGuard, - }, - ], + providers: [PrismaService], }) export class AppModule {} diff --git a/src/common/auth/auth.module.ts b/src/common/auth/auth.module.ts new file mode 100644 index 0000000..3a155fe --- /dev/null +++ b/src/common/auth/auth.module.ts @@ -0,0 +1,39 @@ +import { Module } from '@nestjs/common'; +import { AuthJwtAccessStrategy } from './strategies/jwt-access.strategy'; +import { AuthService } from './services/auth.service'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { APP_GUARD } from '@nestjs/core'; +import { AuthJwtAccessGuard } from './guards/jwt-access.guard'; +import { PrismaService } from '../services/prisma.service'; +import { HelperService } from './services/helper.service'; +import { AuthController } from './controllers/auth.controller'; + +@Module({ + imports: [ + JwtModule.registerAsync({ + inject: [ConfigService], + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('auth.accessToken.secret'), + signOptions: { + expiresIn: configService.get( + 'auth.accessToken.expirationTime', + ), + }, + }), + }), + ], + controllers: [AuthController], + providers: [ + AuthJwtAccessStrategy, + AuthService, + PrismaService, + HelperService, + { + provide: APP_GUARD, + useClass: AuthJwtAccessGuard, + }, + ], +}) +export class AuthModule {} diff --git a/src/common/auth/controllers/auth.controller.ts b/src/common/auth/controllers/auth.controller.ts new file mode 100644 index 0000000..ecd10ac --- /dev/null +++ b/src/common/auth/controllers/auth.controller.ts @@ -0,0 +1,30 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { AuthUser } from 'src/core/decorators/auth-user.decorator'; +import { Public } from 'src/core/decorators/public.decorator'; +import { AuthService } from '../services/auth.service'; +import { UserCreateDto } from '../dtos/signup.dto'; +import { UserLoginDto } from '../dtos/login.dto'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) { + // + } + + @Public() + @Post('login') + public login(@Body() payload: UserLoginDto) { + return this.authService.login(payload); + } + + @Public() + @Post('signup') + public signup(@Body() payload: UserCreateDto) { + return this.authService.signup(payload); + } + + @Get('me') + public me(@AuthUser() userId: number) { + return this.authService.me(userId); + } +} diff --git a/src/common/auth/dtos/login.dto.ts b/src/common/auth/dtos/login.dto.ts new file mode 100644 index 0000000..159a129 --- /dev/null +++ b/src/common/auth/dtos/login.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UserLoginDto { + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'email not provided' }) + public email: string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'password not provided' }) + public password: string; +} diff --git a/src/common/auth/dtos/signup.dto.ts b/src/common/auth/dtos/signup.dto.ts new file mode 100644 index 0000000..3634f51 --- /dev/null +++ b/src/common/auth/dtos/signup.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UserCreateDto { + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'email not provided' }) + public email: string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'password not provided' }) + public password: string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'firstname not provided' }) + public firstName: string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'lastname not provided' }) + public lastName: string; +} diff --git a/src/common/auth/guards/jwt-access.guard.ts b/src/common/auth/guards/jwt-access.guard.ts new file mode 100644 index 0000000..d122d55 --- /dev/null +++ b/src/common/auth/guards/jwt-access.guard.ts @@ -0,0 +1,12 @@ +import { AuthGuard } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; + +@Injectable() +export class AuthJwtAccessGuard extends AuthGuard('jwt') { + handleRequest(err: Error, user: TUser, _info: Error): TUser { + if (err || !user) { + throw new UnauthorizedException('accessTokenUnauthorized'); + } + return user; + } +} diff --git a/src/common/auth/interfaces/auth.response.interface.ts b/src/common/auth/interfaces/auth.response.interface.ts new file mode 100644 index 0000000..6cc5662 --- /dev/null +++ b/src/common/auth/interfaces/auth.response.interface.ts @@ -0,0 +1,6 @@ +import { User } from '@prisma/client'; + +export interface AuthResponse { + accessToken: string; + user: User; +} diff --git a/src/common/auth/interfaces/auth.service.interface.ts b/src/common/auth/interfaces/auth.service.interface.ts new file mode 100644 index 0000000..87ef5cf --- /dev/null +++ b/src/common/auth/interfaces/auth.service.interface.ts @@ -0,0 +1,10 @@ +import { User } from '@prisma/client'; +import { AuthResponse } from './auth.response.interface'; +import { UserLoginDto } from '../dtos/login.dto'; +import { UserCreateDto } from '../dtos/signup.dto'; + +export interface IAuthService { + login(data: UserLoginDto): Promise; + signup(data: UserCreateDto): Promise; + me(id: number): Promise; +} diff --git a/src/common/auth/services/auth.service.ts b/src/common/auth/services/auth.service.ts new file mode 100644 index 0000000..b11e8b9 --- /dev/null +++ b/src/common/auth/services/auth.service.ts @@ -0,0 +1,103 @@ +import { + HttpException, + HttpStatus, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { IAuthService } from '../interfaces/auth.service.interface'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { UserLoginDto } from '../dtos/login.dto'; +import { PrismaService } from 'src/common/services/prisma.service'; +import { HelperService } from './helper.service'; +import { UserCreateDto } from '../dtos/signup.dto'; +import { Role } from '@prisma/client'; + +@Injectable() +export class AuthService implements IAuthService { + private readonly accessTokenSecret: string; + + constructor( + private readonly configService: ConfigService, + private readonly jwtService: JwtService, + private readonly prismaService: PrismaService, + private readonly helperService: HelperService, + ) { + this.accessTokenSecret = this.configService.get( + 'auth.accessToken.secret', + ); + } + + public async login(data: UserLoginDto) { + try { + const { email, password } = data; + const user = await this.prismaService.user.findUnique({ + where: { email }, + }); + if (!user) { + throw new NotFoundException('userNotFound'); + } + const match = this.helperService.match(user.password, password); + if (!match) { + throw new NotFoundException('invalidPassword'); + } + const accessToken = this.jwtService.sign( + { + user: user.id, + }, + { + secret: this.accessTokenSecret, + }, + ); + return { + accessToken, + user, + }; + } catch (e) { + throw new Error(e); + } + } + + public async signup(data: UserCreateDto) { + try { + const { email, firstName, lastName, password } = data; + const user = await this.prismaService.user.findUnique({ + where: { email }, + }); + if (user) { + throw new HttpException('userExists', HttpStatus.CONFLICT); + } + const createdUser = await this.prismaService.user.create({ + data: { + email, + password: this.helperService.createHash(password), + first_name: firstName.trim(), + last_name: lastName.trim(), + role: Role.USER, + }, + }); + const accessToken = this.jwtService.sign( + { + user: user.id, + }, + { + secret: this.accessTokenSecret, + }, + ); + return { + accessToken, + user: createdUser, + }; + } catch (e) { + throw new Error(e); + } + } + + public async me(id: number) { + return this.prismaService.user.findUnique({ + where: { + id, + }, + }); + } +} diff --git a/src/common/auth/services/helper.service.ts b/src/common/auth/services/helper.service.ts new file mode 100644 index 0000000..506954e --- /dev/null +++ b/src/common/auth/services/helper.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import * as bcrypt from 'bcrypt'; + +@Injectable() +export class HelperService { + public salt: string; + constructor() { + this.salt = bcrypt.genSaltSync(); + } + + public createHash(password: string): string { + return bcrypt.hashSync(password, this.salt); + } + + public match(hash: string, password: string): boolean { + return bcrypt.compareSync(password, hash); + } +} diff --git a/src/common/auth/strategies/jwt-access.strategy.ts b/src/common/auth/strategies/jwt-access.strategy.ts new file mode 100644 index 0000000..88ad412 --- /dev/null +++ b/src/common/auth/strategies/jwt-access.strategy.ts @@ -0,0 +1,23 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { AuthService } from 'src/common/auth/services/auth.service'; + +@Injectable() +export class AuthJwtAccessStrategy extends PassportStrategy(Strategy, 'jwt') { + constructor( + private readonly configService: ConfigService, + private readonly authService: AuthService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('auth.accessToken.secret'), + }); + } + + async validate({ data }: Record): Promise> { + return data; + } +} diff --git a/src/common/common.module.ts b/src/common/common.module.ts new file mode 100644 index 0000000..04d8e16 --- /dev/null +++ b/src/common/common.module.ts @@ -0,0 +1,23 @@ +import configs from '../config'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { AuthModule } from './auth/auth.module'; +import { PrismaService } from './services/prisma.service'; +import { CognitoAuthModule } from './congnito-auth/cognito-auth.module'; + +@Module({ + controllers: [], + imports: [ + AuthModule, + CognitoAuthModule, + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + ], + providers: [PrismaService], +}) +export class CommonModule {} diff --git a/src/modules/v1/auth.module.ts b/src/common/congnito-auth/cognito-auth.module.ts similarity index 55% rename from src/modules/v1/auth.module.ts rename to src/common/congnito-auth/cognito-auth.module.ts index 7870cc3..2f0a8f2 100644 --- a/src/modules/v1/auth.module.ts +++ b/src/common/congnito-auth/cognito-auth.module.ts @@ -1,14 +1,15 @@ import { Module } from '@nestjs/common'; import { ClientsModule, Transport } from '@nestjs/microservices'; -import { ConfigService } from '../../config/config.service'; -import { ConfigModule } from '../../config/config.module'; -import { AuthService } from './auth.service'; -import { AuthController } from './auth.controller'; -import { CognitoService, JwtService, PrismaService } from '../../services'; +import { CongnitoAuthService } from './services/cognito-auth.service'; +import { CongitoAuthController } from './controllers/cognito-auth.controller'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; +import { JwtAuthGuard } from './guards/cognito-auth.guard'; +import { PrismaService } from '../services/prisma.service'; +import { CognitoService } from './services/cognito.service'; @Module({ imports: [ - ConfigModule, ClientsModule.registerAsync([ { name: 'MAIL_SERVICE', @@ -16,8 +17,8 @@ import { CognitoService, JwtService, PrismaService } from '../../services'; useFactory: (configService: ConfigService) => ({ transport: Transport.RMQ, options: { - urls: [`${configService.get('rb_url')}`], - queue: `${configService.get('mailer_queue')}`, + urls: [`${configService.get('rmq.uri')}`], + queue: `${configService.get('rmq.mailer')}`, queueOptions: { durable: false, }, @@ -33,8 +34,8 @@ import { CognitoService, JwtService, PrismaService } from '../../services'; useFactory: (configService: ConfigService) => ({ transport: Transport.RMQ, options: { - urls: [`${configService.get('rb_url')}`], - queue: `${configService.get('files_queue')}`, + urls: [`${configService.get('rmq.uri')}`], + queue: `${configService.get('rmq.files')}`, queueOptions: { durable: false, }, @@ -50,8 +51,8 @@ import { CognitoService, JwtService, PrismaService } from '../../services'; useFactory: (configService: ConfigService) => ({ transport: Transport.RMQ, options: { - urls: [`${configService.get('rb_url')}`], - queue: `${configService.get('notification_queue')}`, + urls: [`${configService.get('rmq.uri')}`], + queue: `${configService.get('rmq.notification')}`, queueOptions: { durable: false, }, @@ -61,7 +62,15 @@ import { CognitoService, JwtService, PrismaService } from '../../services'; }, ]), ], - controllers: [AuthController], - providers: [AuthService, PrismaService, CognitoService, JwtService], + controllers: [CongitoAuthController], + providers: [ + CongnitoAuthService, + PrismaService, + CognitoService, + { + provide: APP_GUARD, + useClass: JwtAuthGuard, + }, + ], }) -export class AuthModule {} +export class CognitoAuthModule {} diff --git a/src/common/congnito-auth/controllers/cognito-auth.controller.ts b/src/common/congnito-auth/controllers/cognito-auth.controller.ts new file mode 100644 index 0000000..725cec3 --- /dev/null +++ b/src/common/congnito-auth/controllers/cognito-auth.controller.ts @@ -0,0 +1,47 @@ +import { Body, Controller, Get, HttpCode, Post } from '@nestjs/common'; +import { User } from '@prisma/client'; +import { CongnitoAuthService } from '../services/cognito-auth.service'; +import { Public } from 'src/core/decorators/public.decorator'; +import { AuthUser } from 'src/core/decorators/auth-user.decorator'; +import { LoginDto } from '../dtos/cognito-login.dto'; +import { CreateUserDto } from '../dtos/cognito-signup.dto'; +import { GetOtpDto, VerifyDto } from '../dtos/cognito-verify.dto'; + +@Controller({ + version: '1', + path: '/cognito/auth', +}) +export class CongitoAuthController { + constructor(private cognitoAuthService: CongnitoAuthService) {} + + @Public() + @Post('/login') + login(@Body() data: LoginDto) { + return this.cognitoAuthService.login(data); + } + + @Public() + @Post('/signup') + signup(@Body() data: CreateUserDto) { + return this.cognitoAuthService.signup(data); + } + + @Public() + @Post('/verify') + @HttpCode(200) + verifyUser(@Body() data: VerifyDto) { + return this.cognitoAuthService.verifySignup(data); + } + + @Public() + @Post('/otp') + @HttpCode(201) + getOtp(@Body() data: GetOtpDto) { + return this.cognitoAuthService.getOtp(data); + } + + @Get('/me') + me(@AuthUser() user: User) { + return this.cognitoAuthService.getUserByEmail(user.email); + } +} diff --git a/src/core/dtos/login.dto.ts b/src/common/congnito-auth/dtos/cognito-login.dto.ts similarity index 100% rename from src/core/dtos/login.dto.ts rename to src/common/congnito-auth/dtos/cognito-login.dto.ts diff --git a/src/core/dtos/signup.dto.ts b/src/common/congnito-auth/dtos/cognito-signup.dto.ts similarity index 100% rename from src/core/dtos/signup.dto.ts rename to src/common/congnito-auth/dtos/cognito-signup.dto.ts diff --git a/src/core/dtos/verify.dto.ts b/src/common/congnito-auth/dtos/cognito-verify.dto.ts similarity index 100% rename from src/core/dtos/verify.dto.ts rename to src/common/congnito-auth/dtos/cognito-verify.dto.ts diff --git a/src/core/guards/auth.guard.ts b/src/common/congnito-auth/guards/cognito-auth.guard.ts similarity index 89% rename from src/core/guards/auth.guard.ts rename to src/common/congnito-auth/guards/cognito-auth.guard.ts index db3f8bc..98270ce 100644 --- a/src/core/guards/auth.guard.ts +++ b/src/common/congnito-auth/guards/cognito-auth.guard.ts @@ -1,7 +1,7 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; -import { IS_PUBLIC_KEY } from '../decorators'; import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; +import { IS_PUBLIC_KEY } from 'src/core/decorators/public.decorator'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { diff --git a/src/modules/v1/auth.service.ts b/src/common/congnito-auth/services/cognito-auth.service.ts similarity index 68% rename from src/modules/v1/auth.service.ts rename to src/common/congnito-auth/services/cognito-auth.service.ts index 409727c..6a843d1 100644 --- a/src/modules/v1/auth.service.ts +++ b/src/common/congnito-auth/services/cognito-auth.service.ts @@ -7,24 +7,35 @@ import { } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { Prisma } from '@prisma/client'; -import { CognitoService, PrismaService } from '../../services'; -import { VerifyDto, GetOtpDto, LoginDto, CreateUserDto } from '../../core'; +import { PrismaService } from 'src/common/services/prisma.service'; +import { CognitoService } from './cognito.service'; +import { GetOtpDto, VerifyDto } from '../dtos/cognito-verify.dto'; +import { LoginDto } from '../dtos/cognito-login.dto'; +import { CreateUserDto } from '../dtos/cognito-signup.dto'; @Injectable() -export class AuthService { +export class CongnitoAuthService { constructor( @Inject('FILES_SERVICE') private readonly fileClient: ClientProxy, - private prisma: PrismaService, - private cognitoService: CognitoService, + private readonly prismaService: PrismaService, + private readonly cognitoService: CognitoService, ) { this.fileClient.connect(); } + public async getUserByEmail(email) { + return this.prismaService.user.findUnique({ where: { email } }); + } + + public async getUserById(id: number) { + return this.prismaService.user.findUnique({ where: { id } }); + } + public async verifySignup(data: VerifyDto) { try { const { email, otp } = data; const response = await this.cognitoService.verify(email, otp); - await this.prisma.user.update({ + await this.prismaService.user.update({ where: { email }, data: { is_verified: true, @@ -52,7 +63,9 @@ export class AuthService { email, password, }); - const user = await this.prisma.user.findUnique({ where: { email } }); + const user = await this.prismaService.user.findUnique({ + where: { email }, + }); return { ...authResponse, user, @@ -69,7 +82,7 @@ export class AuthService { email, password, }); - const user = await this.prisma.user.create({ + const user = await this.prismaService.user.create({ data: { email, first_name: firstName?.trim(), @@ -83,19 +96,11 @@ export class AuthService { } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError) { if (e.code === 'P2002') { - throw new HttpException('user_exists', HttpStatus.CONFLICT); + throw new HttpException('userExists', HttpStatus.CONFLICT); } } else { throw new BadRequestException(e.message); } } } - - public me(email) { - return this.prisma.user.findUnique({ where: { email } }); - } - - public getUserById(id: number) { - return this.prisma.user.findUnique({ where: { id } }); - } } diff --git a/src/services/jwt.service.ts b/src/common/congnito-auth/services/cognito-jwt.service.ts similarity index 72% rename from src/services/jwt.service.ts rename to src/common/congnito-auth/services/cognito-jwt.service.ts index d2d1cdf..d247340 100644 --- a/src/services/jwt.service.ts +++ b/src/common/congnito-auth/services/cognito-jwt.service.ts @@ -3,11 +3,11 @@ import { PassportStrategy } from '@nestjs/passport'; import { passportJwtSecret } from 'jwks-rsa'; import { CognitoJwtVerifier } from 'aws-jwt-verify'; import { ExtractJwt, Strategy } from 'passport-jwt'; -import { ConfigService } from 'src/config/config.service'; -import { PrismaService } from './prisma.service'; +import { PrismaService } from '../../services/prisma.service'; +import { ConfigService } from '@nestjs/config'; @Injectable() -export class JwtService extends PassportStrategy(Strategy) { +export class CognitoJwtService extends PassportStrategy(Strategy) { constructor( private configService: ConfigService, private prisma: PrismaService, @@ -17,21 +17,20 @@ export class JwtService extends PassportStrategy(Strategy) { cache: true, rateLimit: true, jwksRequestsPerMinute: 3, - jwksUri: `${configService.get('authority')}/${configService.get( - 'cognitoUserPoolId', - )}/.well-known/jwks.json`, + jwksUri: `${configService.get( + 'auth.cognito.authority', + )}/${configService.get('auth.cognito.poolId')}/.well-known/jwks.json`, }), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - audience: configService.get('cognitoClientId'), - issuer: `${configService.get('authority')}/${configService.get( - 'cognitoUserPoolId', - )}`, + audience: configService.get('auth.cognito.clientId'), + issuer: `${configService.get( + 'auth.cognito.authority', + )}/${configService.get('auth.cognito.poolId')}`, algorithms: ['RS256'], }); } public async validate(payload: any) { - // console.debug('JWT VALIDATION', payload); const user = await this.prisma.user.findUnique({ where: { cognito_sub: payload?.sub }, }); @@ -41,7 +40,7 @@ export class JwtService extends PassportStrategy(Strategy) { return user; } - async validateToken(accessToken: string) { + public async validateToken(accessToken: string) { try { const verifier = CognitoJwtVerifier.create({ userPoolId: this.configService.get('cognitoUserPoolId'), diff --git a/src/services/cognito.service.ts b/src/common/congnito-auth/services/cognito.service.ts similarity index 85% rename from src/services/cognito.service.ts rename to src/common/congnito-auth/services/cognito.service.ts index 5fd2b4d..24ed201 100644 --- a/src/services/cognito.service.ts +++ b/src/common/congnito-auth/services/cognito.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { AuthenticationDetails, CognitoUser, CognitoUserPool, ISignUpResult, } from 'amazon-cognito-identity-js'; -import { ConfigService } from '../config/config.service'; @Injectable() export class CognitoService { @@ -13,12 +13,12 @@ export class CognitoService { constructor(private readonly configService: ConfigService) { this.userPool = new CognitoUserPool({ - UserPoolId: this.configService.get('cognitoUserPoolId'), - ClientId: this.configService.get('cognitoClientId'), + UserPoolId: this.configService.get('auth.cognito.poolId'), + ClientId: this.configService.get('auth.cognito.clientId'), }); } - registerUser(registerRequest: { + public async registerUser(registerRequest: { email: string; password: string; }): Promise { @@ -31,7 +31,7 @@ export class CognitoService { }); } - verify(email: string, otp: string) { + public async verify(email: string, otp: string) { return new Promise((resolve, reject) => { const userData = { Username: email, @@ -45,7 +45,7 @@ export class CognitoService { }); } - sendOtpRequest(email: string) { + public async sendOtpRequest(email: string) { return new Promise((resolve, reject) => { const userData = { Username: email, @@ -59,7 +59,7 @@ export class CognitoService { }); } - authenticateUser(data: { + public async authenticateUser(data: { email: string; password: string; }): Promise<{ accessToken: string; refreshToken: string }> { diff --git a/src/services/prisma.service.ts b/src/common/services/prisma.service.ts similarity index 100% rename from src/services/prisma.service.ts rename to src/common/services/prisma.service.ts diff --git a/src/config/app.config.ts b/src/config/app.config.ts new file mode 100644 index 0000000..3316799 --- /dev/null +++ b/src/config/app.config.ts @@ -0,0 +1,22 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'app', + (): Record => ({ + name: process.env.APP_NAME ?? 'auth', + env: process.env.APP_ENV ?? 'development', + versioning: { + enable: process.env.HTTP_VERSIONING_ENABLE === 'true' ?? false, + prefix: 'v', + version: process.env.HTTP_VERSION ?? '1', + }, + globalPrefix: '/api', + http: { + enable: process.env.HTTP_ENABLE === 'true' ?? false, + host: process.env.HTTP_HOST ?? '0.0.0.0', + port: process.env.HTTP_PORT + ? Number.parseInt(process.env.HTTP_PORT) + : 9001, + }, + }), +); diff --git a/src/config/auth.config.ts b/src/config/auth.config.ts new file mode 100644 index 0000000..b569195 --- /dev/null +++ b/src/config/auth.config.ts @@ -0,0 +1,21 @@ +import { registerAs } from '@nestjs/config'; +import ms from 'ms'; + +function seconds(msValue: string): number { + return ms(msValue) / 1000; +} + +export default registerAs( + 'auth', + (): Record => ({ + accessToken: { + secret: process.env.ACCESS_TOKEN_SECRET_KEY, + expirationTime: seconds(process.env.ACCESS_TOKEN_EXPIRED ?? '1h'), + }, + cognito: { + poolId: process.env.AWS_COGNITO_USER_POOL_ID, + clientId: process.env.AWS_COGNITO_CLIENT_ID, + authority: process.env.AWS_COGNITO_AUTHORITY, + }, + }), +); diff --git a/src/config/config.module.ts b/src/config/config.module.ts deleted file mode 100644 index 1bc2456..0000000 --- a/src/config/config.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigService } from './config.service'; - -@Module({ - imports: [], - providers: [ConfigService], - exports: [ConfigService], -}) -export class ConfigModule {} diff --git a/src/config/config.service.ts b/src/config/config.service.ts deleted file mode 100644 index bb79cac..0000000 --- a/src/config/config.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { config } from 'dotenv'; -config(); - -interface Config { - env: string; - servicePort: string; - rb_url: string; - auth_queue: string; - mailer_queue: string; - files_queue: string; - notification_queue: string; - cognitoUserPoolId: string; - cognitoClientId: string; - authority: string; -} - -@Injectable() -export class ConfigService { - private config = {} as Config; - constructor() { - this.config.env = process.env.NODE_ENV; - this.config.servicePort = process.env.PORT; - this.config.rb_url = process.env.RABBITMQ_URL; - this.config.auth_queue = process.env.RABBITMQ_AUTH_QUEUE; - this.config.files_queue = process.env.RABBITMQ_FILES_QUEUE; - this.config.cognitoClientId = process.env.AWS_COGNITO_CLIENT_ID; - this.config.cognitoUserPoolId = process.env.AWS_COGNITO_USER_POOL_ID; - this.config.authority = process.env.AWS_COGNITO_AUTHORITY; - } - - public get(key: keyof Config): any { - return this.config[key]; - } -} diff --git a/src/config/doc.config.ts b/src/config/doc.config.ts new file mode 100644 index 0000000..62e97b9 --- /dev/null +++ b/src/config/doc.config.ts @@ -0,0 +1,11 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'doc', + (): Record => ({ + name: `${process.env.APP_NAME} APIs Specification`, + description: 'Auth APIs description', + version: '1.0', + prefix: '/docs', + }), +); diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..57ae122 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,6 @@ +import AppConfig from './app.config'; +import AuthConfig from './auth.config'; +import DocConfig from './doc.config'; +import RmqConfig from './rmq.config'; + +export default [AppConfig, AuthConfig, DocConfig, RmqConfig]; diff --git a/src/config/rmq.config.ts b/src/config/rmq.config.ts new file mode 100644 index 0000000..cf05908 --- /dev/null +++ b/src/config/rmq.config.ts @@ -0,0 +1,10 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'rmq', + (): Record => ({ + uri: process.env.RABBITMQ_URL, + files: process.env.RABBITMQ_FILES_QUEUE, + auth: process.env.RABBITMQ_AUTH_QUEUE, + }), +); diff --git a/src/core/core.module.ts b/src/core/core.module.ts new file mode 100644 index 0000000..c01b70a --- /dev/null +++ b/src/core/core.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; +import { RolesGuard } from './guards/roles.guard'; +import { ResponseInterceptor } from './interceptors/response.interceptor'; +import { HttpExceptionFilter } from './interceptors/exception.interceptor'; + +@Module({ + controllers: [], + imports: [], + providers: [ + { + provide: APP_GUARD, + useClass: RolesGuard, + }, + { + provide: APP_INTERCEPTOR, + useClass: ResponseInterceptor, + }, + { + provide: APP_INTERCEPTOR, + useClass: HttpExceptionFilter, + }, + ], +}) +export class CoreModule {} diff --git a/src/core/decorators/user.decorator.ts b/src/core/decorators/auth-user.decorator.ts similarity index 80% rename from src/core/decorators/user.decorator.ts rename to src/core/decorators/auth-user.decorator.ts index 7919497..2f6e6fc 100644 --- a/src/core/decorators/user.decorator.ts +++ b/src/core/decorators/auth-user.decorator.ts @@ -1,6 +1,6 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'; -export const CurrentUser = createParamDecorator( +export const AuthUser = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.user; diff --git a/src/core/decorators/index.ts b/src/core/decorators/index.ts deleted file mode 100644 index fe8a871..0000000 --- a/src/core/decorators/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './allow.decorator'; -export * from './role.decorator'; -export * from './user.decorator'; diff --git a/src/core/decorators/allow.decorator.ts b/src/core/decorators/public.decorator.ts similarity index 100% rename from src/core/decorators/allow.decorator.ts rename to src/core/decorators/public.decorator.ts diff --git a/src/core/dtos/index.ts b/src/core/dtos/index.ts deleted file mode 100644 index 5d4672d..0000000 --- a/src/core/dtos/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './login.dto'; -export * from './signup.dto'; -export * from './verify.dto'; diff --git a/src/core/guards/index.ts b/src/core/guards/index.ts deleted file mode 100644 index 4ae679c..0000000 --- a/src/core/guards/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './auth.guard'; -export * from './roles.guard'; diff --git a/src/core/index.ts b/src/core/index.ts deleted file mode 100644 index ee8e8ed..0000000 --- a/src/core/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './decorators'; -export * from './dtos'; -export * from './guards'; -export * from './interceptor'; diff --git a/src/core/interceptor/index.ts b/src/core/interceptor/index.ts deleted file mode 100644 index 3133e8c..0000000 --- a/src/core/interceptor/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './exception.interceptor'; -export * from './response.interceptor'; diff --git a/src/core/interceptor/exception.interceptor.ts b/src/core/interceptors/exception.interceptor.ts similarity index 100% rename from src/core/interceptor/exception.interceptor.ts rename to src/core/interceptors/exception.interceptor.ts diff --git a/src/core/interceptor/response.interceptor.ts b/src/core/interceptors/response.interceptor.ts similarity index 100% rename from src/core/interceptor/response.interceptor.ts rename to src/core/interceptors/response.interceptor.ts diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json index 8b6c126..fd4846a 100644 --- a/src/i18n/en/translation.json +++ b/src/i18n/en/translation.json @@ -1,3 +1,4 @@ { - "user_exists": "An account with email is already exists." + "userExists": "An account with email is already exists.", + "accessTokenUnauthorized": "Token is unauthorized" } diff --git a/src/main.ts b/src/main.ts index 3f562c6..7f8267d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,70 +1,55 @@ -import { - ClassSerializerInterceptor, - Logger, - ValidationPipe, -} from '@nestjs/common'; -import { NestFactory, Reflector } from '@nestjs/core'; +import { Logger, ValidationPipe, VersioningType } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { ConfigService } from '@nestjs/config'; import { Transport } from '@nestjs/microservices'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { AppModule } from './app.module'; -import { HttpExceptionFilter, ResponseInterceptor } from './core'; -import { ConfigService } from './config/config.service'; import { ExpressAdapter } from '@nestjs/platform-express'; -import * as express from 'express'; +import { AppModule } from './app/app.module'; +import { setupSwagger } from './swagger'; +import express from 'express'; import helmet from 'helmet'; -function configureSwagger(app): void { - const config = new DocumentBuilder() - .setTitle('auth-service') - .setVersion('1.0') - .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('/api/docs', app, document); -} - async function bootstrap() { const logger = new Logger(); const app = await NestFactory.create( AppModule, new ExpressAdapter(express()), { - bufferLogs: true, cors: true, }, ); - app.setGlobalPrefix('/api'); - app.use(helmet()); const configService = app.get(ConfigService); - const moduleRef = app.select(AppModule); - const reflector = moduleRef.get(Reflector); - app.useGlobalInterceptors( - new ResponseInterceptor(reflector), - new ClassSerializerInterceptor(reflector), + const port: number = configService.get('app.http.port'); + const host: string = configService.get('app.http.host'); + const globalPrefix: string = configService.get('app.globalPrefix'); + const versioningPrefix: string = configService.get( + 'app.versioning.prefix', ); - app.useGlobalFilters(new HttpExceptionFilter()); - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - }), + const version: string = configService.get('app.versioning.version'); + const versionEnable: string = configService.get( + 'app.versioning.enable', ); - configureSwagger(app); + app.use(helmet()); + app.useGlobalPipes(new ValidationPipe()); + app.setGlobalPrefix(globalPrefix); + if (versionEnable) { + app.enableVersioning({ + type: VersioningType.URI, + defaultVersion: version, + prefix: versioningPrefix, + }); + } + setupSwagger(app); app.connectMicroservice({ transport: Transport.RMQ, options: { - urls: [`${configService.get('rb_url')}`], - queue: `${configService.get('auth_queue')}`, + urls: [`${configService.get('rmq.uri')}`], + queue: `${configService.get('rmq.auth')}`, queueOptions: { durable: false }, prefetchCount: 1, }, }); await app.startAllMicroservices(); - await app.listen(configService.get('servicePort')); - logger.log( - `🚀 Auth service started successfully on port ${configService.get( - 'servicePort', - )}`, - ); + await app.listen(port, host); + logger.log(`🚀 Auth service started successfully on port ${port}`); } bootstrap(); diff --git a/src/modules/user/controllers/auth.controller.ts b/src/modules/user/controllers/auth.controller.ts new file mode 100644 index 0000000..2f892fb --- /dev/null +++ b/src/modules/user/controllers/auth.controller.ts @@ -0,0 +1,8 @@ +import { Controller } from '@nestjs/common'; + +@Controller() +export class AuthController { + constructor() { + // + } +} diff --git a/src/modules/user/controllers/user.controller.ts b/src/modules/user/controllers/user.controller.ts new file mode 100644 index 0000000..512af07 --- /dev/null +++ b/src/modules/user/controllers/user.controller.ts @@ -0,0 +1,8 @@ +import { Controller } from '@nestjs/common'; + +@Controller() +export class UserController { + constructor() { + // + } +} diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts new file mode 100644 index 0000000..2d2b594 --- /dev/null +++ b/src/modules/user/services/user.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class UserService { + constructor() { + // + } +} diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts new file mode 100644 index 0000000..cb53831 --- /dev/null +++ b/src/modules/user/user.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AuthController } from './controllers/auth.controller'; +import { UserController } from './controllers/user.controller'; +import { UserService } from './services/user.service'; + +@Module({ + controllers: [AuthController, UserController], + imports: [], + providers: [UserService], +}) +export class UserModule {} diff --git a/src/modules/v1/auth.controller.ts b/src/modules/v1/auth.controller.ts deleted file mode 100644 index 5418f72..0000000 --- a/src/modules/v1/auth.controller.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Body, Controller, Get, HttpCode, Post } from '@nestjs/common'; -import { MessagePattern, Payload } from '@nestjs/microservices'; -import { User } from '@prisma/client'; -import { AuthService } from './auth.service'; -import { JwtService } from '../../services'; -import { - CreateUserDto, - CurrentUser, - GetOtpDto, - LoginDto, - Public, - VerifyDto, -} from '../../core'; - -@Controller('v1') -export class AuthController { - constructor(private authService: AuthService, private jwt: JwtService) {} - - @Public() - @Post('/login') - login(@Body() data: LoginDto) { - return this.authService.login(data); - } - - @Public() - @Post('/signup') - signup(@Body() data: CreateUserDto) { - return this.authService.signup(data); - } - - @Public() - @Post('/verify') - @HttpCode(200) - verifyUser(@Body() data: VerifyDto) { - return this.authService.verifySignup(data); - } - - @Public() - @Post('/otp') - @HttpCode(201) - getOtp(@Body() data: GetOtpDto) { - return this.authService.getOtp(data); - } - - @Get('/me') - me(@CurrentUser() user: User) { - return this.authService.me(user.email); - } - - @MessagePattern('get_user_by_id') - public async getUserById(@Payload() data: string): Promise { - const payload = JSON.parse(data); - return this.authService.getUserById(payload.id); - } - - @MessagePattern('validate_token') - public async getUserByAccessToken(@Payload() token: string) { - return this.jwt.validateToken(token); - } -} diff --git a/src/services/firebase.service.ts b/src/services/firebase.service.ts deleted file mode 100644 index a59a4de..0000000 --- a/src/services/firebase.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import * as firebase from 'firebase-admin'; -// import * as firebaseConfig from '../../firebase.config.json'; - -@Injectable() -export class FirebaseService { - private firebaseAdmin: firebase.app.App; - constructor() { - // const params = { - // type: firebaseConfig.type, - // projectId: firebaseConfig.project_id, - // privateKeyId: firebaseConfig.private_key_id, - // privateKey: firebaseConfig.private_key, - // clientEmail: firebaseConfig.client_email, - // clientId: firebaseConfig.client_id, - // authUri: firebaseConfig.auth_uri, - // tokenUri: firebaseConfig.token_uri, - // authProviderX509CertUrl: firebaseConfig.auth_provider_x509_cert_url, - // clientC509CertUrl: firebaseConfig.client_x509_cert_url, - // }; - // this.firebaseAdmin = firebase.initializeApp({ - // credential: firebase.credential.cert(params), - // }); - } - - signUp(data: { - firstName: string; - lastName: string; - email: string; - password: string; - }) { - const { firstName, lastName, email, password } = data; - return this.firebaseAdmin.auth().createUser({ - displayName: `${firstName} ${lastName}`, - email, - emailVerified: false, - password, - }); - } -} diff --git a/src/services/index.ts b/src/services/index.ts deleted file mode 100644 index 1d01585..0000000 --- a/src/services/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './prisma.service'; -export * from './cognito.service'; -export * from './jwt.service'; -export * from './firebase.service'; diff --git a/src/swagger.ts b/src/swagger.ts new file mode 100644 index 0000000..ed9eb93 --- /dev/null +++ b/src/swagger.ts @@ -0,0 +1,44 @@ +import { Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { INestApplication } from '@nestjs/common'; +import { + DocumentBuilder, + SwaggerCustomOptions, + SwaggerModule, +} from '@nestjs/swagger'; + +export const setupSwagger = async (app: INestApplication) => { + const configService = app.get(ConfigService); + const logger = new Logger(); + + const docName: string = configService.get('doc.name'); + const docDesc: string = configService.get('doc.description'); + const docVersion: string = configService.get('doc.version'); + const docPrefix: string = configService.get('doc.prefix'); + + const documentBuild = new DocumentBuilder() + .setTitle(docName) + .setDescription(docDesc) + .setVersion(docVersion) + .addTag("API's") + .addBearerAuth( + { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + 'accessToken', + ) + .build(); + + const document = SwaggerModule.createDocument(app, documentBuild, { + deepScanRoutes: true, + }); + const customOptions: SwaggerCustomOptions = { + swaggerOptions: { + persistAuthorization: true, + }, + }; + SwaggerModule.setup(docPrefix, app, document, { + explorer: true, + customSiteTitle: docName, + ...customOptions, + }); + logger.log(`Docs will serve on ${docPrefix}`, 'NestApplication'); +}; diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index ad4dc96..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { User } from '@prisma/client'; - -interface PaylaodType { - emails: string[]; - subject: string; - data: any; -} - -export interface IDecodeTokenPayload { - token: string; -} - -export interface ICreateTokenPayload { - userId: number; -} - -export interface IMailPayload { - template: keyof typeof EmailTemplates; - payload: PaylaodType; -} - -export interface IResponse { - data: T[]; -} - -export interface IAuthResponse { - accessToken: string; - refreshToken: string; - // user: User; -} - -export interface IAuthPayload { - userId: number; - role: string; -} - -enum EmailTemplates { - FORGOT_PASSWORD = 'forgot-password', -} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index f3aa9c5..43aeb95 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,9 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { HttpStatus, INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { AppModule } from '../src/app.module'; +import { AppModule } from '../src/app/app.module'; import { faker } from '@faker-js/faker'; -import { PrismaService } from '../src/services'; +import { PrismaService } from '../src/common/services'; describe('AppController (e2e)', () => { let app: INestApplication; diff --git a/tsconfig.json b/tsconfig.json index 7a64609..e31a108 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,17 +6,19 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "target": "es2017", + "useDefineForClassFields": false, + "target": "ESNext", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", "incremental": true, "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false, - "resolveJsonModule": true - } + "allowJs": false, + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["src", "test"], + "exclude": ["node_modules", "dist", "*coverage"] }