Merge pull request #2 from HamzaSha1/feat/user-registration

feat: registration journrey for parents
This commit is contained in:
abdalhamid99
2024-12-05 11:34:58 +03:00
committed by GitHub
98 changed files with 2270 additions and 18 deletions

View File

@ -8,6 +8,12 @@ DB_NAME=
MIGRATIONS_RUN=true
SWAGGER_API_DOCS_PATH="/api-docs"
JWT_ACCESS_TOKEN_SECRET=your_access_token_secret
JWT_ACCESS_TOKEN_EXPIRY=1d
JWT_REFRESH_TOKEN_SECRET=your_refresh_token_secret
JWT_REFRESH_TOKEN_EXPIRY=1d
USE_MOCK=true
OCI_TENANCY_ID=
OCI_USER_ID=

580
package-lock.json generated
View File

@ -17,7 +17,9 @@
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0",
"@nestjs/event-emitter": "^2.1.1",
"@nestjs/jwt": "^10.2.0",
"@nestjs/microservices": "^10.4.7",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.8",
"@nestjs/swagger": "^8.0.5",
"@nestjs/terminus": "^10.2.3",
@ -25,15 +27,21 @@
"@nestjs/typeorm": "^10.0.2",
"amqp-connection-manager": "^4.1.14",
"amqplib": "^0.10.4",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"google-libphonenumber": "^3.2.39",
"handlebars": "^4.7.8",
"ioredis": "^5.4.1",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"nestjs-i18n": "^10.4.9",
"nestjs-pino": "^4.1.0",
"nodemailer": "^6.9.16",
"oci-common": "^2.99.0",
"oci-sdk": "^2.99.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.13.1",
"pino-http": "^10.3.0",
"pino-pretty": "^13.0.0",
@ -46,11 +54,15 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/express": "^5.0.0",
"@types/google-libphonenumber": "^7.4.30",
"@types/jest": "^29.5.2",
"@types/lodash": "^4.17.13",
"@types/multer": "^1.4.12",
"@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
@ -1877,6 +1889,71 @@
"node": ">=8"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"license": "ISC",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@microsoft/tsdoc": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz",
@ -2110,6 +2187,50 @@
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0"
}
},
"node_modules/@nestjs/jwt": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz",
"integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==",
"license": "MIT",
"dependencies": {
"@types/jsonwebtoken": "9.0.5",
"jsonwebtoken": "9.0.2"
},
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0"
}
},
"node_modules/@nestjs/jwt/node_modules/@types/jsonwebtoken": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
"integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@nestjs/jwt/node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/@nestjs/mapped-types": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz",
@ -2188,6 +2309,16 @@
}
}
},
"node_modules/@nestjs/passport": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz",
"integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
"passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0"
}
},
"node_modules/@nestjs/platform-express": {
"version": "10.4.8",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.8.tgz",
@ -2591,6 +2722,16 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/bcrypt": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
"integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@ -2681,6 +2822,13 @@
"@types/send": "*"
}
},
"node_modules/@types/google-libphonenumber": {
"version": "7.4.30",
"resolved": "https://registry.npmjs.org/@types/google-libphonenumber/-/google-libphonenumber-7.4.30.tgz",
"integrity": "sha512-Td1X1ayRxePEm6/jPHUBs2tT6TzW1lrVB6ZX7ViPGellyzO/0xMNi+wx5nH6jEitjznq276VGIqjK5qAju0XVw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@ -2764,6 +2912,13 @@
"integrity": "sha512-oBnY3csYnXfqZXDRBJwP1nDDJCW/+VMJ88UHT4DCy0deSXpJIQvMCwYlnmdW4M+u7PiSfQc44LmiFcUbJ8hLEw==",
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
"integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/methods": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@ -2833,6 +2988,38 @@
"@types/node": "*"
}
},
"node_modules/@types/passport": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/passport-jwt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
"integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/jsonwebtoken": "*",
"@types/passport-strategy": "*"
}
},
"node_modules/@types/passport-strategy": {
"version": "0.2.38",
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
"integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/passport": "*"
}
},
"node_modules/@types/pug": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@ -3414,6 +3601,18 @@
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
@ -3658,6 +3857,40 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"license": "ISC"
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/are-we-there-yet/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@ -3931,6 +4164,20 @@
],
"license": "MIT"
},
"node_modules/bcrypt": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@ -4430,6 +4677,15 @@
"fsevents": "~2.3.2"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/chrome-trace-event": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
@ -4800,6 +5056,15 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
@ -4859,7 +5124,6 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/concat-stream": {
@ -4894,6 +5158,12 @@
"integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==",
"license": "MIT"
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/constantinople": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
@ -5199,6 +5469,12 @@
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -5237,6 +5513,15 @@
"node": ">=8"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@ -6550,6 +6835,36 @@
"node": ">=12"
}
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs-minipass/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/fs-monkey": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz",
@ -6561,7 +6876,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
@ -6587,6 +6901,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/gauge/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -6749,6 +7090,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/google-libphonenumber": {
"version": "3.2.39",
"resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.39.tgz",
"integrity": "sha512-dpCbkY6ZxHXIHEFDwSir/gPBWkn22e2EixBv47guVs/NE8+qd35f1yu+fxQ8awRnHEXC60uhcPM9mbqmrD6nmw==",
"license": "(MIT AND Apache-2.0)",
"engines": {
"node": ">=0.10"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@ -6876,6 +7226,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -7026,6 +7382,19 @@
"node": ">=0.10"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@ -7130,7 +7499,6 @@
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
@ -9087,12 +9455,48 @@
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -9107,6 +9511,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@ -9632,6 +10042,37 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/mjml": {
"version": "4.15.3",
"resolved": "https://registry.npmjs.org/mjml/-/mjml-4.15.3.tgz",
@ -10102,6 +10543,15 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -10243,6 +10693,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
"node_modules/node-emoji": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
@ -10334,6 +10790,19 @@
"node": ">=8"
}
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@ -12339,6 +12808,42 @@
"node": ">= 0.8"
}
},
"node_modules/passport": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
"license": "MIT",
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-jwt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
"integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
"passport-strategy": "^1.0.0"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -12353,7 +12858,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -12413,6 +12917,11 @@
"node": ">=8"
}
},
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/peberminta": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
@ -13451,7 +13960,6 @@
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
@ -13467,7 +13975,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@ -13479,7 +13986,6 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@ -13500,7 +14006,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@ -13899,6 +14404,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@ -14426,6 +14937,50 @@
"node": ">=6"
}
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tar/node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"license": "ISC",
"engines": {
"node": ">=8"
}
},
"node_modules/tar/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tar/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/terser": {
"version": "5.36.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz",
@ -15619,6 +16174,15 @@
"node": ">= 8"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/widest-line": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",

View File

@ -34,7 +34,9 @@
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0",
"@nestjs/event-emitter": "^2.1.1",
"@nestjs/jwt": "^10.2.0",
"@nestjs/microservices": "^10.4.7",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.8",
"@nestjs/swagger": "^8.0.5",
"@nestjs/terminus": "^10.2.3",
@ -42,15 +44,21 @@
"@nestjs/typeorm": "^10.0.2",
"amqp-connection-manager": "^4.1.14",
"amqplib": "^0.10.4",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"google-libphonenumber": "^3.2.39",
"handlebars": "^4.7.8",
"ioredis": "^5.4.1",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"nestjs-i18n": "^10.4.9",
"nestjs-pino": "^4.1.0",
"nodemailer": "^6.9.16",
"oci-common": "^2.99.0",
"oci-sdk": "^2.99.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.13.1",
"pino-http": "^10.3.0",
"pino-pretty": "^13.0.0",
@ -63,11 +71,15 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/express": "^5.0.0",
"@types/google-libphonenumber": "^7.4.30",
"@types/jest": "^29.5.2",
"@types/lodash": "^4.17.13",
"@types/multer": "^1.4.12",
"@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",

View File

@ -4,10 +4,13 @@ import { APP_FILTER, APP_PIPE } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
import { LoggerModule } from 'nestjs-pino';
import { AuthModule } from './auth/auth.module';
import { OtpModule } from './common/modules/otp/otp.module';
import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/filters';
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
import { buildI18nOptions } from './core/module-options/i18n-options';
import { buildValidationPipe } from './core/pipes';
import { CustomerModule } from './customer/customer.module';
import { migrations } from './db';
import { DocumentModule } from './document/document.module';
import { HealthModule } from './health/health.module';
@ -25,10 +28,12 @@ import { HealthModule } from './health/health.module';
inject: [ConfigService],
}),
I18nModule.forRoot(buildI18nOptions()),
HealthModule,
// Application Modules
// App modules
AuthModule,
CustomerModule,
DocumentModule,
HealthModule,
OtpModule,
],
providers: [
// Global Pipes

17
src/auth/auth.module.ts Normal file
View File

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './controllers';
import { Device, User, UserNotificationSettings } from './entities';
import { DeviceRepository, UserRepository } from './repositories';
import { AuthService, DeviceService } from './services';
import { UserService } from './services/user.service';
import { AccessTokenStrategy } from './strategies';
@Module({
imports: [TypeOrmModule.forFeature([User, UserNotificationSettings, Device]), JwtModule.register({})],
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
controllers: [AuthController],
exports: [UserService],
})
export class AuthModule {}

View File

@ -0,0 +1 @@
export const COUNTRY_CODE_REGEX = /^\+\d{1,3}$/;

View File

@ -0,0 +1 @@
export * from './country-code-regex.constant.';

View File

@ -0,0 +1,71 @@
import { Body, Controller, Headers, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { DEVICE_ID_HEADER } from '~/common/constants';
import { AuthenticatedUser } from '~/common/decorators';
import { AccessTokenGuard } from '~/common/guards';
import { ResponseFactory } from '~/core/utils';
import {
CreateUnverifiedUserRequestDto,
DisableBiometricRequestDto,
EnableBiometricRequestDto,
LoginRequestDto,
SetEmailRequestDto,
SetPasscodeRequestDto,
VerifyUserRequestDto,
} from '../dtos/request';
import { SendRegisterOtpResponseDto } from '../dtos/response';
import { LoginResponseDto } from '../dtos/response/login.response.dto';
import { IJwtPayload } from '../interfaces';
import { AuthService } from '../services';
@Controller('auth')
@ApiTags('Auth')
@ApiBearerAuth()
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register/otp')
async register(@Body() createUnverifiedUserDto: CreateUnverifiedUserRequestDto) {
const phoneNumber = await this.authService.sendRegisterOtp(createUnverifiedUserDto);
return ResponseFactory.data(new SendRegisterOtpResponseDto(phoneNumber));
}
@Post('register/verify')
async verifyUser(@Body() verifyUserDto: VerifyUserRequestDto) {
const [res, user] = await this.authService.verifyUser(verifyUserDto);
return ResponseFactory.data(new LoginResponseDto(res, user));
}
@Post('register/set-email')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
async setEmail(@AuthenticatedUser() { sub }: IJwtPayload, @Body() setEmailDto: SetEmailRequestDto) {
await this.authService.setEmail(sub, setEmailDto);
}
@Post('register/set-passcode')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
async setPasscode(@AuthenticatedUser() { sub }: IJwtPayload, @Body() { passcode }: SetPasscodeRequestDto) {
await this.authService.setPasscode(sub, passcode);
}
@Post('biometric/enable')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
enableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() enableBiometricDto: EnableBiometricRequestDto) {
return this.authService.enableBiometric(sub, enableBiometricDto);
}
@Post('biometric/disable')
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard)
disableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() disableBiometricDto: DisableBiometricRequestDto) {
return this.authService.disableBiometric(sub, disableBiometricDto);
}
@Post('login')
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
const [res, user] = await this.authService.login(loginDto, deviceId);
return ResponseFactory.data(new LoginResponseDto(res, user));
}
}

View File

@ -0,0 +1 @@
export * from './auth.controller';

View File

@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { Matches } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
import { IsValidPhoneNumber } from '~/core/decorators/validations';
export class CreateUnverifiedUserRequestDto {
@ApiProperty({ example: '+962' })
@Matches(COUNTRY_CODE_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
})
countryCode: string = '+966';
@ApiProperty({ example: '787259134' })
@IsValidPhoneNumber({
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
}

View File

@ -0,0 +1,4 @@
import { PickType } from '@nestjs/swagger';
import { EnableBiometricRequestDto } from './enable-biometric.request.dto';
export class DisableBiometricRequestDto extends PickType(EnableBiometricRequestDto, ['deviceId']) {}

View File

@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
export class EnableBiometricRequestDto {
@ApiProperty({ example: 'device-id' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.deviceId' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.deviceId' }) })
deviceId!: string;
@ApiProperty({ example: 'publicKey' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.publicKey' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.publicKey' }) })
publicKey!: string;
}

View File

@ -0,0 +1,7 @@
export * from './create-unverified-user.request.dto';
export * from './disable-biometric.request.dto';
export * from './enable-biometric.request.dto';
export * from './login.request.dto';
export * from './set-email.request.dto';
export * from './set-passcode.request.dto';
export * from './verify-user.request.dto';

View File

@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsEnum, IsString, ValidateIf } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { GrantType } from '~/auth/enums';
export class LoginRequestDto {
@ApiProperty({ example: GrantType.PASSWORD })
@IsEnum(GrantType, { message: i18n('validation.IsEnum', { path: 'general', property: 'auth.grantType' }) })
grantType!: GrantType;
@ApiProperty({ example: 'test@test.com' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.email' }) })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
email!: string;
@ApiProperty({ example: '123456' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) })
@ValidateIf((o) => o.grantType === GrantType.PASSWORD)
password!: string;
@ApiProperty({ example: 'device-token' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.deviceToken' }) })
@ValidateIf((o) => o.grantType === GrantType.BIOMETRIC)
deviceToken!: string;
}

View File

@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
export class SetEmailRequestDto {
@ApiProperty({ example: 'test@test.com' })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
email!: string;
}

View File

@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumberString, MaxLength, MinLength } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
const PASSCODE_LENGTH = 6;
export class SetPasscodeRequestDto {
@ApiProperty({ example: '123456' })
@IsNumberString(
{ no_symbols: true },
{ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.passcode' }) },
)
@MinLength(PASSCODE_LENGTH, { message: i18n('validation.MinLength', { path: 'general', property: 'auth.passcode' }) })
@MaxLength(PASSCODE_LENGTH, { message: i18n('validation.MaxLength', { path: 'general', property: 'auth.passcode' }) })
passcode!: string;
}

View File

@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumberString, MaxLength, MinLength } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
import { CreateUnverifiedUserRequestDto } from './create-unverified-user.request.dto';
export class VerifyUserRequestDto extends CreateUnverifiedUserRequestDto {
@ApiProperty({ example: '111111' })
@IsNumberString(
{ no_symbols: true },
{ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.otp' }) },
)
@MaxLength(DEFAULT_OTP_LENGTH, {
message: i18n('validation.MaxLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
})
@MinLength(DEFAULT_OTP_LENGTH, {
message: i18n('validation.MinLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
})
otp!: string;
}

View File

@ -0,0 +1,3 @@
export * from './send-register-otp.response.dto';
export * from './user.response.dto';
export * from './verify-user.response.dto';

View File

@ -0,0 +1,30 @@
import { ApiProperty } from '@nestjs/swagger';
import { User } from '~/auth/entities';
import { ILoginResponse } from '~/auth/interfaces';
import { CustomerResponseDto } from '~/customer/dtos/response';
import { UserResponseDto } from './user.response.dto';
export class LoginResponseDto {
@ApiProperty()
accessToken!: string;
@ApiProperty()
refreshToken!: string;
@ApiProperty()
expiresAt!: Date;
@ApiProperty({ example: UserResponseDto })
user!: UserResponseDto;
@ApiProperty({ example: CustomerResponseDto })
customer!: CustomerResponseDto;
constructor(IVerifyUserResponse: ILoginResponse, user: User) {
this.user = new UserResponseDto(user);
this.customer = new CustomerResponseDto(user.customer);
this.accessToken = IVerifyUserResponse.accessToken;
this.refreshToken = IVerifyUserResponse.refreshToken;
this.expiresAt = IVerifyUserResponse.expiresAt;
}
}

View File

@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
export class SendRegisterOtpResponseDto {
@ApiProperty()
phoneNumber!: string;
constructor(phoneNumber: string) {
this.phoneNumber = phoneNumber;
}
}

View File

@ -0,0 +1,36 @@
import { ApiProperty } from '@nestjs/swagger';
import { User } from '~/auth/entities';
import { Roles } from '~/auth/enums';
export class UserResponseDto {
@ApiProperty()
id!: string;
@ApiProperty()
email!: string;
@ApiProperty()
phoneNumber!: string;
@ApiProperty()
countryCode!: string;
@ApiProperty()
isPasswordSet!: boolean;
@ApiProperty()
isProfileCompleted!: boolean;
@ApiProperty()
roles!: Roles[];
constructor(user: User) {
this.id = user.id;
this.email = user.email;
this.phoneNumber = user.phoneNumber;
this.countryCode = user.countryCode;
this.isPasswordSet = user.isPasswordSet;
this.isProfileCompleted = user.isProfileCompleted;
this.roles = user.roles;
}
}

View File

@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { User } from '~/auth/entities';
import { ILoginResponse } from '~/auth/interfaces';
export class VerifyUserResponseDto {
@ApiProperty()
accessToken!: string;
@ApiProperty()
refreshToken!: string;
@ApiProperty()
expiresAt!: Date;
@ApiProperty()
user!: User;
constructor(data: ILoginResponse, user: User) {
this.accessToken = data.accessToken;
this.refreshToken = data.refreshToken;
this.expiresAt = data.expiresAt;
this.user = user;
}
}

View File

@ -0,0 +1,30 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('devices')
export class Device {
@PrimaryColumn('varchar', { length: 255 })
deviceId!: string;
@Column('varchar', { name: 'user_id' })
userId!: string;
@Column('varchar', { name: 'device_name', nullable: true })
deviceName?: string | null;
@Column('varchar', { name: 'public_key', nullable: true })
publicKey?: string | null;
@Column('timestamp with time zone', { name: 'last_access_on', default: () => 'CURRENT_TIMESTAMP' })
lastAccessOn!: Date;
@ManyToOne(() => User, (user) => user.devices, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user!: User;
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
updatedAt!: Date;
}

View File

@ -0,0 +1,3 @@
export * from './device.entity';
export * from './user-notification-settings.entity';
export * from './user.entity';

View File

@ -0,0 +1,36 @@
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from './user.entity';
@Entity('user_notification_settings')
export class UserNotificationSettings extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ name: 'is_email_enabled', default: false })
isEmailEnabled!: boolean;
@Column({ name: 'is_push_enabled', default: false })
isPushEnabled!: boolean;
@Column({ name: 'is_sms_enabled', default: false })
isSmsEnabled!: boolean;
@OneToOne(() => User, (user) => user.notificationSettings, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user!: User;
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone' })
updatedAt!: Date;
}

View File

@ -0,0 +1,82 @@
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Otp } from '~/common/modules/otp/entities';
import { Customer } from '~/customer/entities/customer.entity';
import { Document } from '~/document/entities';
import { Roles } from '../enums';
import { Device } from './device.entity';
import { UserNotificationSettings } from './user-notification-settings.entity';
@Entity('users')
export class User extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column('varchar', { length: 255, nullable: true, name: 'email' })
email!: string;
@Column('varchar', { length: 255, name: 'phone_number' })
phoneNumber!: string;
@Column('varchar', { length: 10, name: 'country_code' })
countryCode!: string;
@Column('varchar', { length: 255, name: 'password', nullable: true })
password!: string;
@Column('varchar', { length: 255, name: 'salt', nullable: true })
salt!: string;
@Column('varchar', { length: 255, nullable: true, name: 'google_id' })
googleId!: string;
@Column('varchar', { length: 255, nullable: true, name: 'apple_id' })
appleId!: string;
@Column('boolean', { default: false, name: 'is_profile_completed' })
isProfileCompleted!: boolean;
@Column('text', { nullable: true, array: true, name: 'roles' })
roles!: Roles[];
@Column('varchar', { name: 'profile_picture_id', nullable: true })
profilePictureId!: string;
@OneToOne(() => Document, (document) => document.user, { cascade: true, nullable: true })
@JoinColumn({ name: 'profile_picture_id' })
profilePicture!: Document;
@OneToMany(() => Otp, (otp) => otp.user)
otp!: Otp[];
@OneToOne(() => UserNotificationSettings, (notificationSettings) => notificationSettings.user, {
cascade: true,
eager: true,
})
notificationSettings!: UserNotificationSettings;
@OneToOne(() => Customer, (customer) => customer.user, { cascade: true, eager: true })
customer!: Customer;
@OneToMany(() => Device, (device) => device.user)
devices!: Device[];
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone' })
updatedAt!: Date;
get isPasswordSet(): boolean {
return !!this.password;
}
}

View File

@ -0,0 +1,4 @@
export enum GrantType {
PASSWORD = 'PASSWORD',
BIOMETRIC = 'BIOMETRIC',
}

2
src/auth/enums/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './grant-type.enum';
export * from './roles.enum';

View File

@ -0,0 +1,4 @@
export enum Roles {
JUNIOR = 'JUNIOR',
GUARDIAN = 'GUARDIAN',
}

View File

@ -0,0 +1,2 @@
export * from './jwt-payload.interface';
export * from './login-response.interface';

View File

@ -0,0 +1,4 @@
export interface IJwtPayload {
sub: string;
roles: string[];
}

View File

@ -0,0 +1,5 @@
export interface ILoginResponse {
accessToken: string;
refreshToken: string;
expiresAt: Date;
}

View File

@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Device } from '../entities';
@Injectable()
export class DeviceRepository {
constructor(@InjectRepository(Device) private readonly deviceRepository: Repository<Device>) {}
findUserDeviceById(deviceId: string, userId: string) {
return this.deviceRepository.findOne({ where: { deviceId, userId } });
}
createDevice(data: Partial<Device>) {
return this.deviceRepository.save(data);
}
updateDevice(deviceId: string, data: Partial<Device>) {
return this.deviceRepository.update({ deviceId }, data);
}
}

View File

@ -0,0 +1,2 @@
export * from './device.repository';
export * from './user.repository';

View File

@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FindOptionsWhere, Repository } from 'typeorm';
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request';
import { Customer } from '~/customer/entities';
import { User, UserNotificationSettings } from '../entities';
@Injectable()
export class UserRepository {
constructor(@InjectRepository(User) private readonly userRepository: Repository<User>) {}
createUnverifiedUser(data: Partial<User>) {
return this.userRepository.save(
this.userRepository.create({
phoneNumber: data.phoneNumber,
countryCode: data.countryCode,
roles: data.roles,
notificationSettings: UserNotificationSettings.create(),
}),
);
}
findOne(where: FindOptionsWhere<User>) {
return this.userRepository.findOne({ where });
}
updateNotificationSettings(user: User, body: UpdateNotificationsSettingsRequestDto) {
user.notificationSettings = UserNotificationSettings.create({ ...user.notificationSettings, ...body });
return this.userRepository.save(user);
}
verifyUserAndCreateCustomer(user: User) {
user.customer = Customer.create({ ...user.customer, id: user.id, isGuardian: true });
return this.userRepository.save(user);
}
update(userId: string, data: Partial<User>) {
return this.userRepository.update(userId, data);
}
}

View File

@ -0,0 +1,202 @@
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
import { OtpService } from '~/common/modules/otp/services';
import {
CreateUnverifiedUserRequestDto,
DisableBiometricRequestDto,
EnableBiometricRequestDto,
LoginRequestDto,
SetEmailRequestDto,
} from '../dtos/request';
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
import { User } from '../entities';
import { GrantType } from '../enums';
import { ILoginResponse } from '../interfaces';
import { removePadding, verifySignature } from '../utils';
import { DeviceService } from './device.service';
import { UserService } from './user.service';
const ONE_THOUSAND = 1000;
const SALT_ROUNDS = 10;
@Injectable()
export class AuthService {
constructor(
private readonly otpService: OtpService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly userService: UserService,
private readonly deviceService: DeviceService,
) {}
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
return this.otpService.generateAndSendOtp({
userId: user.id,
phoneNumber: user.phoneNumber,
scope: OtpScope.VERIFY_PHONE,
otpType: OtpType.SMS,
});
}
async verifyUser(verifyUserDto: VerifyUserRequestDto): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUserOrThrow({ phoneNumber: verifyUserDto.phoneNumber });
if (user.isPasswordSet) {
throw new BadRequestException('USERS.PHONE_ALREADY_VERIFIED');
}
const isOtpValid = await this.otpService.verifyOtp({
userId: user.id,
scope: OtpScope.VERIFY_PHONE,
otpType: OtpType.SMS,
value: verifyUserDto.otp,
});
if (!isOtpValid) {
throw new BadRequestException('USERS.INVALID_OTP');
}
const updatedUser = await this.userService.verifyUserAndCreateCustomer(user);
const tokens = await this.generateAuthToken(updatedUser);
return [tokens, updatedUser];
}
async setEmail(userId: string, { email }: SetEmailRequestDto) {
const user = await this.userService.findUserOrThrow({ id: userId });
if (user.email) {
throw new BadRequestException('USERS.EMAIL_ALREADY_SET');
}
return this.userService.setEmail(userId, email);
}
async setPasscode(userId: string, passcode: string) {
const user = await this.userService.findUserOrThrow({ id: userId });
if (user.password) {
throw new BadRequestException('USERS.PASSCODE_ALREADY_SET');
}
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
const hashedPasscode = bcrypt.hashSync(passcode, salt);
await this.userService.setPasscode(userId, hashedPasscode, salt);
}
async enableBiometric(userId: string, { deviceId, publicKey }: EnableBiometricRequestDto) {
const device = await this.deviceService.findUserDeviceById(deviceId, userId);
if (!device) {
return this.deviceService.createDevice({
deviceId,
userId,
publicKey,
});
}
if (device.publicKey) {
throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_ENABLED');
}
return this.deviceService.updateDevice(deviceId, { publicKey });
}
async disableBiometric(userId: string, { deviceId }: DisableBiometricRequestDto) {
const device = await this.deviceService.findUserDeviceById(deviceId, userId);
if (!device) {
throw new BadRequestException('AUTH.DEVICE_NOT_FOUND');
}
if (!device.publicKey) {
throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_DISABLED');
}
return this.deviceService.updateDevice(deviceId, { publicKey: null });
}
async login(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUser({ email: loginDto.email });
let tokens;
if (!user) {
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
}
if (loginDto.grantType === GrantType.PASSWORD) {
tokens = await this.loginWithPassword(loginDto, user);
} else {
tokens = await this.loginWithBiometric(loginDto, user, deviceId);
}
this.deviceService.updateDevice(deviceId, { lastAccessOn: new Date() });
return [tokens, user];
}
private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise<ILoginResponse> {
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
}
const tokens = await this.generateAuthToken(user);
return tokens;
}
private async loginWithBiometric(loginDto: LoginRequestDto, user: User, deviceId: string): Promise<ILoginResponse> {
const device = await this.deviceService.findUserDeviceById(deviceId, user.id);
if (!device) {
throw new UnauthorizedException('AUTH.DEVICE_NOT_FOUND');
}
if (!device.publicKey) {
throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED');
}
const cleanToken = removePadding(loginDto.deviceToken);
const isValidToken = await verifySignature(
device.publicKey,
cleanToken,
`${user.email} - ${device.deviceId}`,
'SHA1',
);
if (!isValidToken) {
throw new UnauthorizedException('AUTH.INVALID_BIOMETRIC');
}
const tokens = await this.generateAuthToken(user);
return tokens;
}
private async generateAuthToken(user: User) {
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.sign(
{ sub: user.id, roles: user.roles },
{
expiresIn: this.configService.getOrThrow('JWT_ACCESS_TOKEN_EXPIRY'),
secret: this.configService.getOrThrow('JWT_ACCESS_TOKEN_SECRET'),
},
),
this.jwtService.sign(
{ sub: user.id, roles: user.roles },
{
expiresIn: this.configService.getOrThrow('JWT_REFRESH_TOKEN_EXPIRY'),
secret: this.configService.getOrThrow('JWT_REFRESH_TOKEN_SECRET'),
},
),
]);
return { accessToken, refreshToken, expiresAt: new Date(this.jwtService.decode(accessToken).exp * ONE_THOUSAND) };
}
}

View File

@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { Device } from '../entities';
import { DeviceRepository } from '../repositories';
@Injectable()
export class DeviceService {
constructor(private readonly deviceRepository: DeviceRepository) {}
findUserDeviceById(deviceId: string, userId: string) {
return this.deviceRepository.findUserDeviceById(deviceId, userId);
}
createDevice(data: Partial<Device>) {
return this.deviceRepository.createDevice(data);
}
updateDevice(deviceId: string, data: Partial<Device>) {
return this.deviceRepository.updateDevice(deviceId, data);
}
}

View File

@ -0,0 +1,3 @@
export * from './auth.service';
export * from './device.service';
export * from './user.service';

View File

@ -0,0 +1,64 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { FindOptionsWhere } from 'typeorm';
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request';
import { CreateUnverifiedUserRequestDto } from '../dtos/request';
import { User } from '../entities';
import { Roles } from '../enums';
import { UserRepository } from '../repositories';
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async updateNotificationSettings(userId: string, body: UpdateNotificationsSettingsRequestDto) {
const user = await this.findUserOrThrow({ id: userId });
const notificationSettings = (await this.userRepository.updateNotificationSettings(user, body))
.notificationSettings;
return notificationSettings;
}
findUser(where: FindOptionsWhere<User>) {
return this.userRepository.findOne(where);
}
async findUserOrThrow(where: FindOptionsWhere<User>) {
const user = await this.findUser(where);
if (!user) {
throw new BadRequestException('USERS.NOT_FOUND');
}
return user;
}
async findOrCreateUser({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
const user = await this.userRepository.findOne({ phoneNumber });
if (!user) {
return this.userRepository.createUnverifiedUser({ phoneNumber, countryCode, roles: [Roles.GUARDIAN] });
}
if (user && user.roles.includes(Roles.GUARDIAN) && user.isPasswordSet) {
throw new BadRequestException('USERS.PHONE_NUMBER_ALREADY_EXISTS');
}
if (user && user.roles.includes(Roles.JUNIOR)) {
//TODO add role Guardian to the existing user and send OTP
}
return user;
}
setEmail(userId: string, email: string) {
return this.userRepository.update(userId, { email });
}
setPasscode(userId: string, passcode: string, salt: string) {
return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true });
}
verifyUserAndCreateCustomer(user: User) {
return this.userRepository.verifyUserAndCreateCustomer(user);
}
}

View File

@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { IJwtPayload } from '../interfaces';
@Injectable()
export class AccessTokenStrategy extends PassportStrategy(Strategy, 'access-token') {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_ACCESS_TOKEN_SECRET'),
});
}
validate(payload: IJwtPayload) {
return payload;
}
}

View File

@ -0,0 +1 @@
export * from './access-token.strategy';

26
src/auth/utils/crypt.ts Normal file
View File

@ -0,0 +1,26 @@
import * as crypto from 'crypto';
export function verifySignature(
publicKeyBase64: string,
signatureBase64: string,
message: string,
algorithm: 'SHA1' | 'SHA384',
) {
const signatureBuffer = Buffer.from(signatureBase64, 'base64');
const publicKeyPEM = '-----BEGIN PUBLIC KEY-----\n' + publicKeyBase64 + '\n-----END PUBLIC KEY-----';
const verifier = crypto.createVerify(algorithm);
verifier.update(message, 'utf8');
return verifier.verify(
{
key: publicKeyPEM,
padding: crypto.constants.RSA_PKCS1_PADDING,
},
signatureBuffer,
);
}
export function removePadding(originalSignature: string) {
const buffer = Buffer.from(originalSignature, 'base64');
return buffer.toString('base64');
}

1
src/auth/utils/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './crypt';

View File

@ -0,0 +1 @@
export const DEVICE_ID_HEADER = 'x-client-id';

View File

@ -0,0 +1 @@
export * from './global.constant';

View File

@ -0,0 +1,2 @@
export * from './public.decorator';
export * from './user.decorator';

View File

@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View File

@ -0,0 +1,6 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const AuthenticatedUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest();
return req.user;
});

View File

@ -0,0 +1,23 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
import { IS_PUBLIC_KEY } from '../decorators';
@Injectable()
export class AccessTokenGuard extends AuthGuard('access-token') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}

View File

@ -0,0 +1 @@
export * from './access-token.guard';

View File

@ -0,0 +1 @@
export * from './otp-default.constant';

View File

@ -0,0 +1,2 @@
export const DEFAULT_OTP_LENGTH = 6;
export const DEFAULT_OTP_DIGIT = '1';

View File

@ -0,0 +1,20 @@
import { IsNotEmpty } from 'class-validator';
import { UserLocale } from '~/core/enums';
import { OtpScope, OtpType } from '../../enums';
export class GenerateOtpRequestDto {
@IsNotEmpty()
userId!: string;
@IsNotEmpty()
phoneNumber!: string;
@IsNotEmpty()
scope!: OtpScope;
@IsNotEmpty()
language?: UserLocale = UserLocale.ENGLISH;
@IsNotEmpty()
otpType!: OtpType;
}

View File

@ -0,0 +1,2 @@
export * from './generate-otp-request.request.dto';
export * from './verify-otp-request.dto';

View File

@ -0,0 +1,16 @@
import { IsNotEmpty } from 'class-validator';
import { OtpScope, OtpType } from '../../enums';
export class VerifyOtpRequestDto {
@IsNotEmpty()
userId!: string;
@IsNotEmpty()
scope!: OtpScope;
@IsNotEmpty()
otpType!: OtpType;
@IsNotEmpty()
value!: string;
}

View File

@ -0,0 +1 @@
export * from './otp.entity';

View File

@ -0,0 +1,33 @@
import { Column, CreateDateColumn, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { User } from '~/auth/entities';
import { OtpScope, OtpType } from '../enums';
@Entity('otp')
export class Otp {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column('varchar', { length: 255, name: 'value' })
value!: string;
@Index()
@Column('varchar', { length: 255, name: 'scope' })
scope!: OtpScope;
@Column('varchar', { length: 255, name: 'otp_type' })
otpType!: OtpType;
@Column('timestamp with time zone', { name: 'expires_at' })
expiresAt!: Date;
@Index()
@Column('varchar', { name: 'user_id' })
userId!: string;
@ManyToOne(() => User, (user) => user.otp, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user!: User;
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
createdAt!: Date;
}

View File

@ -0,0 +1,2 @@
export * from './otp-scope.enum';
export * from './otp-type.enum';

View File

@ -0,0 +1,3 @@
export enum OtpScope {
VERIFY_PHONE = 'VERIFY_PHONE',
}

View File

@ -0,0 +1,4 @@
export enum OtpType {
SMS = 'SMS',
EMAIL = 'EMAIL',
}

View File

@ -0,0 +1,13 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Otp } from './entities';
import { OtpRepository } from './repositories';
import { OtpService } from './services/otp.service';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([Otp])],
providers: [OtpService, OtpRepository],
exports: [OtpService],
})
export class OtpModule {}

View File

@ -0,0 +1 @@
export * from './otp.repository';

View File

@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MoreThan, Repository } from 'typeorm';
import { VerifyOtpRequestDto } from '../dtos/request';
import { Otp } from '../entities';
const FIVE = 5;
const SIXTY = 60;
const ONE_THOUSAND = 1000;
const FIVE_MINUTES_IN_MILLISECONDS = FIVE * SIXTY * ONE_THOUSAND;
@Injectable()
export class OtpRepository {
constructor(@InjectRepository(Otp) private readonly otpRepository: Repository<Otp>) {}
createOtp(otp: Partial<Otp>) {
return this.otpRepository.save(
this.otpRepository.create({
userId: otp.userId,
value: otp.value,
scope: otp.scope,
otpType: otp.otpType,
expiresAt: new Date(Date.now() + FIVE_MINUTES_IN_MILLISECONDS),
}),
);
}
findOtp(otp: VerifyOtpRequestDto) {
return this.otpRepository.findOne({
where: {
userId: otp.userId,
scope: otp.scope,
value: otp.value,
otpType: otp.otpType,
expiresAt: MoreThan(new Date()),
},
order: {
createdAt: 'DESC',
},
});
}
}

View File

@ -0,0 +1 @@
export * from './otp.service';

View File

@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DEFAULT_OTP_DIGIT, DEFAULT_OTP_LENGTH } from '../constants';
import { GenerateOtpRequestDto, VerifyOtpRequestDto } from '../dtos/request';
import { OtpRepository } from '../repositories';
import { generateRandomOtp } from '../utils';
@Injectable()
export class OtpService {
constructor(private readonly configService: ConfigService, private readonly otpRepository: OtpRepository) {}
private useMock = this.configService.get<boolean>('USE_MOCK', false);
async generateAndSendOtp(sendotpRequest: GenerateOtpRequestDto) {
const otp = this.useMock ? DEFAULT_OTP_DIGIT.repeat(DEFAULT_OTP_LENGTH) : generateRandomOtp(DEFAULT_OTP_LENGTH);
await this.otpRepository.createOtp({ ...sendotpRequest, value: otp });
this.sendOtp(sendotpRequest, otp);
return sendotpRequest.phoneNumber.replace(/.(?=.{4})/g, '*');
}
async verifyOtp(verifyOtpRequest: VerifyOtpRequestDto) {
const otp = await this.otpRepository.findOtp(verifyOtpRequest);
return !!otp;
}
private sendOtp(sendotpRequest: GenerateOtpRequestDto, otp: string) {
// TODO: send OTP to the user
return;
}
}

View File

@ -0,0 +1 @@
export * from './otp-generator.util';

View File

@ -0,0 +1,9 @@
import { getRandomValues } from 'crypto';
import { shuffle } from 'lodash';
const ZERO = 0;
const ONE = 1;
export function generateRandomOtp(length: number): string {
const u32 = getRandomValues(new Uint32Array(ONE))[ZERO];
const randomOtpDigits = u32.toString().substring(ZERO, length).padEnd(length, '0');
return shuffle(randomOtpDigits).join('');
}

View File

@ -1 +1,3 @@
// placeholder
export * from './is-above-18';
export * from './is-valid-phone-number';

View File

@ -0,0 +1,42 @@
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import moment from 'moment';
const EIGHTTEEN = 18;
export function IsAbove18(validationOptions?: ValidationOptions) {
return (object: any, propertyName: string) => {
registerDecorator({
name: 'IsAbove18',
target: object.constructor,
propertyName,
options: {
message: `${propertyName} must be above 18 years old`,
...validationOptions,
},
constraints: [],
validator: IsAbove18Constraint,
});
};
}
@ValidatorConstraint({ name: 'IsAbove18' })
export class IsAbove18Constraint implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
if (!value) return true;
const dateOfBirth = moment(value);
if (!dateOfBirth.isValid()) {
return false;
}
const today = moment();
const age = today.diff(dateOfBirth, 'years');
return age >= EIGHTTEEN;
}
}

View File

@ -0,0 +1,40 @@
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import * as libphonenumber from 'google-libphonenumber';
const phoneUtil = libphonenumber.PhoneNumberUtil.getInstance();
export function IsValidPhoneNumber(validationOptions?: ValidationOptions) {
return (object: any, propertyName: string) => {
registerDecorator({
name: 'IsValidPhoneNumber',
target: object.constructor,
propertyName,
options: {
message: `${propertyName} must be valid mobile number`,
...validationOptions,
},
constraints: [],
validator: IsValidPhoneNumberConstraint,
});
};
}
@ValidatorConstraint({ name: 'IsValidPhoneNumber' })
export class IsValidPhoneNumberConstraint implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
const countryCode = (args.object as any).countryCode; // +962;
const isoCountryCode = phoneUtil.getRegionCodeForCountryCode(+countryCode); // JO
try {
const parsedNumber = phoneUtil.parse(value, isoCountryCode);
return phoneUtil.isValidNumberForRegion(parsedNumber, isoCountryCode);
} catch (e) {
return false;
}
}
}

View File

@ -34,7 +34,7 @@ describe('ValidationPipe', () => {
transform: true,
validateCustomDecorators: true,
stopAtFirstError: true,
forbidNonWhitelisted: false,
forbidNonWhitelisted: true,
dismissDefaultMessages: true,
enableDebugMessages: true,
exceptionFactory: i18nValidationErrorFactory,

View File

@ -9,7 +9,7 @@ export function buildValidationPipe(config: ConfigService): ValidationPipe {
transform: true,
validateCustomDecorators: true,
stopAtFirstError: true,
forbidNonWhitelisted: false,
forbidNonWhitelisted: true,
dismissDefaultMessages: true,
enableDebugMessages: config.getOrThrow('NODE_ENV') === Environment.DEV,
exceptionFactory: i18nValidationErrorFactory,

View File

@ -0,0 +1,34 @@
import { Body, Controller, Patch, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { IJwtPayload } from '~/auth/interfaces';
import { AuthenticatedUser } from '~/common/decorators';
import { AccessTokenGuard } from '~/common/guards';
import { ResponseFactory } from '~/core/utils';
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
import { CustomerResponseDto, NotificationSettingsResponseDto } from '../dtos/response';
import { CustomerService } from '../services';
@Controller('customers')
@ApiTags('Customers')
@ApiBearerAuth()
@UseGuards(AccessTokenGuard)
export class CustomerController {
constructor(private readonly customerService: CustomerService) {}
@Patch('')
async updateCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateCustomerRequestDto) {
const customer = await this.customerService.updateCustomer(sub, body);
return ResponseFactory.data(new CustomerResponseDto(customer));
}
@Patch('settings/notifications')
async updateNotificationSettings(
@AuthenticatedUser() { sub }: IJwtPayload,
@Body() body: UpdateNotificationsSettingsRequestDto,
) {
const notificationSettings = await this.customerService.updateNotificationSettings(sub, body);
return ResponseFactory.data(new NotificationSettingsResponseDto(notificationSettings));
}
}

View File

@ -0,0 +1 @@
export * from './customer.controller';

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from '~/auth/auth.module';
import { CustomerController } from './controllers';
import { Customer } from './entities';
import { CustomerRepository } from './repositories/customer.repository';
import { CustomerService } from './services';
@Module({
imports: [TypeOrmModule.forFeature([Customer]), AuthModule],
controllers: [CustomerController],
providers: [CustomerService, CustomerRepository],
})
export class CustomerModule {}

View File

@ -0,0 +1,2 @@
export * from './update-customer.request.dto';
export * from './update-notifications-settings.request.dto';

View File

@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDateString, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { IsAbove18 } from '~/core/decorators/validations';
export class UpdateCustomerRequestDto {
@ApiProperty({ example: 'John' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.firstName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) })
@IsOptional()
firstName!: string;
@ApiProperty({ example: 'Doe' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.lastName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.lastName' }) })
@IsOptional()
lastName!: string;
@ApiProperty({ example: 'JO' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.countryOfResidence' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.countryOfResidence' }) })
@IsOptional()
countryOfResidence!: string;
@ApiProperty({ example: '2021-01-01' })
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
@IsOptional()
dateOfBirth!: Date;
}

View File

@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsOptional } from 'class-validator';
export class UpdateNotificationsSettingsRequestDto {
@ApiProperty()
@IsBoolean()
@IsOptional()
isEmailEnabled!: boolean;
@ApiProperty()
@IsBoolean()
@IsOptional()
isPushEnabled!: boolean;
@ApiProperty()
@IsBoolean()
@IsOptional()
isSmsEnabled!: boolean;
}

View File

@ -0,0 +1,71 @@
import { ApiProperty } from '@nestjs/swagger';
import { Customer } from '~/customer/entities';
export class CustomerResponseDto {
@ApiProperty()
id!: string;
@ApiProperty()
customerStatus!: string;
@ApiProperty()
rejectionReason!: string;
@ApiProperty()
firstName!: string;
@ApiProperty()
lastName!: string;
@ApiProperty()
dateOfBirth!: Date;
@ApiProperty()
nationalId!: string;
@ApiProperty()
nationaIdExpiry!: Date;
@ApiProperty()
countryOfResidence!: string;
@ApiProperty()
sourceOfIncome!: string;
@ApiProperty()
profession!: string;
@ApiProperty()
professionType!: string;
@ApiProperty()
isPep!: boolean;
@ApiProperty()
gender!: string;
@ApiProperty()
isJunior!: boolean;
@ApiProperty()
isGuardian!: boolean;
constructor(customer: Customer) {
this.id = customer.id;
this.customerStatus = customer.customerStatus;
this.rejectionReason = customer.rejectionReason;
this.firstName = customer.firstName;
this.lastName = customer.lastName;
this.dateOfBirth = customer.dateOfBirth;
this.nationalId = customer.nationalId;
this.nationaIdExpiry = customer.nationaIdExpiry;
this.countryOfResidence = customer.countryOfResidence;
this.sourceOfIncome = customer.sourceOfIncome;
this.profession = customer.profession;
this.professionType = customer.professionType;
this.isPep = customer.isPep;
this.gender = customer.gender;
this.isJunior = customer.isJunior;
this.isGuardian = customer.isGuardian;
}
}

View File

@ -0,0 +1,2 @@
export * from './customer-response.dto';
export * from './notification-settings.response.dto';

View File

@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { UserNotificationSettings } from '~/auth/entities';
export class NotificationSettingsResponseDto {
@ApiProperty()
isEmailEnabled!: boolean;
@ApiProperty()
isPushEnabled!: boolean;
@ApiProperty()
isSmsEnabled!: boolean;
constructor(notificationSettings: UserNotificationSettings) {
this.isEmailEnabled = notificationSettings.isEmailEnabled;
this.isPushEnabled = notificationSettings.isPushEnabled;
this.isSmsEnabled = notificationSettings.isSmsEnabled;
}
}

View File

@ -0,0 +1,75 @@
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToOne,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from '~/auth/entities';
@Entity()
export class Customer extends BaseEntity {
@PrimaryColumn('uuid')
id!: string;
@Column('varchar', { length: 255, default: 'PENDING', name: 'customer_status' })
customerStatus!: string;
@Column('text', { nullable: true, name: 'rejection_reason' })
rejectionReason!: string;
@Column('varchar', { length: 255, nullable: true, name: 'first_name' })
firstName!: string;
@Column('varchar', { length: 255, nullable: true, name: 'last_name' })
lastName!: string;
@Column('date', { nullable: true, name: 'date_of_birth' })
dateOfBirth!: Date;
@Column('varchar', { length: 255, nullable: true, name: 'national_id' })
nationalId!: string;
@Column('date', { nullable: true, name: 'national_id_expiry' })
nationaIdExpiry!: Date;
@Column('varchar', { length: 255, nullable: true, name: 'country_of_residence' })
countryOfResidence!: string;
@Column('varchar', { length: 255, nullable: true, name: 'source_of_income' })
sourceOfIncome!: string;
@Column('varchar', { length: 255, nullable: true, name: 'profession' })
profession!: string;
@Column('varchar', { length: 255, nullable: true, name: 'profession_type' })
professionType!: string;
@Column('boolean', { default: false, name: 'is_pep' })
isPep!: boolean;
@Column('varchar', { length: 255, nullable: true, name: 'gender' })
gender!: string;
@Column('boolean', { default: false, name: 'is_junior' })
isJunior!: boolean;
@Column('boolean', { default: false, name: 'is_guardian' })
isGuardian!: boolean;
@Column('varchar', { name: 'user_id' })
userId!: string;
@OneToOne(() => User, (user) => user.customer, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user!: User;
@CreateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date;
@UpdateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
updatedAt!: Date;
}

View File

@ -0,0 +1 @@
export * from './customer.entity';

View File

@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FindOptionsWhere, Repository } from 'typeorm';
import { UpdateCustomerRequestDto } from '../dtos/request';
import { Customer } from '../entities';
@Injectable()
export class CustomerRepository {
constructor(@InjectRepository(Customer) private readonly customerRepository: Repository<Customer>) {}
updateCustomer(id: string, data: UpdateCustomerRequestDto) {
return this.customerRepository.update(id, data);
}
findOne(where: FindOptionsWhere<Customer>) {
return this.customerRepository.findOne({ where });
}
}

View File

View File

@ -0,0 +1,26 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { UserService } from '~/auth/services/user.service';
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
import { Customer } from '../entities';
import { CustomerRepository } from '../repositories/customer.repository';
@Injectable()
export class CustomerService {
constructor(private readonly userService: UserService, private readonly customerRepository: CustomerRepository) {}
updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto) {
return this.userService.updateNotificationSettings(userId, data);
}
async updateCustomer(userId: string, data: UpdateCustomerRequestDto): Promise<Customer> {
await this.customerRepository.updateCustomer(userId, data);
return this.findCustomerById(userId);
}
async findCustomerById(id: string) {
const customer = await this.customerRepository.findOne({ id });
if (!customer) {
throw new BadRequestException('CUSTOMER.NOT_FOUND');
}
return customer;
}
}

View File

@ -0,0 +1 @@
export * from './customer.service';

View File

@ -0,0 +1,35 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateUserEntity1733206728721 implements MigrationInterface {
name = 'CreateUserEntity1733206728721';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "users" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"email" character varying(255),
"phone_number" character varying(255) NOT NULL,
"country_code" character varying(10) NOT NULL,
"password" character varying(255),
"salt" character varying(255),
"google_id" character varying(255),
"apple_id" character varying(255),
"is_profile_completed" boolean NOT NULL DEFAULT false,
"roles" text array,
"profile_picture_id" uuid,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "REL_02ec15de199e79a0c46869895f" UNIQUE ("profile_picture_id"),
CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "users" ADD CONSTRAINT "FK_02ec15de199e79a0c46869895f4" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "FK_02ec15de199e79a0c46869895f4"`);
await queryRunner.query(`DROP TABLE "users"`);
}
}

View File

@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateOtpEntity1733209041336 implements MigrationInterface {
name = 'CreateOtpEntity1733209041336';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "otp"
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"value" character varying(255) NOT NULL,
"scope" character varying(255) NOT NULL,
"otp_type" character varying(255) NOT NULL,
"expires_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"user_id" uuid NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_32556d9d7b22031d7d0e1fd6723" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_6427c192ef35355ebac18fb683" ON "otp" ("scope") `);
await queryRunner.query(`CREATE INDEX "IDX_258d028d322ea3b856bf9f12f2" ON "otp" ("user_id") `);
await queryRunner.query(
`ALTER TABLE "otp" ADD CONSTRAINT "FK_258d028d322ea3b856bf9f12f25" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "otp" DROP CONSTRAINT "FK_258d028d322ea3b856bf9f12f25"`);
await queryRunner.query(`DROP INDEX "public"."IDX_258d028d322ea3b856bf9f12f2"`);
await queryRunner.query(`DROP INDEX "public"."IDX_6427c192ef35355ebac18fb683"`);
await queryRunner.query(`DROP TABLE "otp"`);
}
}

View File

@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateNotificationSettingsTable1733231692252 implements MigrationInterface {
name = 'CreateNotificationSettingsTable1733231692252';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "user_notification_settings"
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"is_email_enabled" boolean NOT NULL DEFAULT false,
"is_push_enabled" boolean NOT NULL DEFAULT false,
"is_sms_enabled" boolean NOT NULL DEFAULT false,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"user_id" uuid, CONSTRAINT "REL_52182ffd0f785e8256f8fcb4fd" UNIQUE ("user_id"),
CONSTRAINT "PK_a195de67d093e096152f387afbd" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "user_notification_settings" ADD CONSTRAINT "FK_52182ffd0f785e8256f8fcb4fd6" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_notification_settings" DROP CONSTRAINT "FK_52182ffd0f785e8256f8fcb4fd6"`,
);
await queryRunner.query(`DROP TABLE "user_notification_settings"`);
}
}

View File

@ -0,0 +1,40 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateCustomerEntity1733298524771 implements MigrationInterface {
name = 'CreateCustomerEntity1733298524771';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "customer"
("id" uuid NOT NULL,
"customer_status" character varying(255) NOT NULL DEFAULT 'PENDING',
"rejection_reason" text,
"first_name" character varying(255),
"last_name" character varying(255),
"date_of_birth" date,
"national_id" character varying(255),
"national_id_expiry" date,
"country_of_residence" character varying(255),
"source_of_income" character varying(255),
"profession" character varying(255),
"profession_type" character varying(255),
"is_pep" boolean NOT NULL DEFAULT false,
"gender" character varying(255),
"is_junior" boolean NOT NULL DEFAULT false,
"is_guardian" boolean NOT NULL DEFAULT false,
"user_id" uuid NOT NULL,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "REL_5d1f609371a285123294fddcf3" UNIQUE ("user_id"),
CONSTRAINT "PK_a7a13f4cacb744524e44dfdad32" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "customer" ADD CONSTRAINT "FK_5d1f609371a285123294fddcf3a" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "customer" DROP CONSTRAINT "FK_5d1f609371a285123294fddcf3a"`);
await queryRunner.query(`DROP TABLE "customer"`);
}
}

View File

@ -0,0 +1,27 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateDeviceEntity1733314952318 implements MigrationInterface {
name = 'CreateDeviceEntity1733314952318';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "devices"
("deviceId" character varying(255) NOT NULL,
"user_id" uuid NOT NULL,
"device_name" character varying,
"public_key" character varying,
"last_access_on" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_666c9b59efda8ca85b29157152c" PRIMARY KEY ("deviceId"))`,
);
await queryRunner.query(
`ALTER TABLE "devices" ADD CONSTRAINT "FK_5e9bee993b4ce35c3606cda194c" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "devices" DROP CONSTRAINT "FK_5e9bee993b4ce35c3606cda194c"`);
await queryRunner.query(`DROP TABLE "devices"`);
}
}

View File

@ -1 +1,6 @@
export * from './1732434281561-create-document-entity';
export * from './1733206728721-create-user-entity';
export * from './1733209041336-create-otp-entity';
export * from './1733231692252-create-notification-settings-table';
export * from './1733298524771-create-customer-entity';
export * from './1733314952318-create-device-entity';

View File

@ -8,7 +8,7 @@ import { DocumentMetaResponseDto } from '../dtos/response';
import { DocumentType } from '../enums';
import { DocumentService } from '../services';
@Controller('document')
@ApiTags('document')
@ApiTags('Document')
export class DocumentController {
constructor(private readonly documentService: DocumentService) {}

View File

@ -1,4 +1,5 @@
import { Column, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { Column, Entity, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { User } from '~/auth/entities';
import { DocumentType } from '../enums';
@Entity('documents')
@ -15,6 +16,9 @@ export class Document {
@Column({ type: 'varchar', length: 255 })
documentType!: DocumentType;
@OneToOne(() => User, (user) => user.profilePicture, { onDelete: 'CASCADE' })
user!: User;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt!: Date;

View File

@ -22,5 +22,7 @@
"IsLengthIn": "$t({path}.PROPERTY_MAPPINGS.{property}) يجب ان يكون {constraints.0} حروف/ارقام",
"Matches": "$t({path}.PROPERTY_MAPPINGS.{property}) يجب ان يتطابق مع النمط",
"IsValidExpiryDate": "$t({path}.PROPERTY_MAPPINGS.{property}) يجب ان يكون تاريخ انتهاء بطاقه صحيح",
"IsEnglishOnly": "$t({path}.PROPERTY_MAPPINGS.{property}) يجب أن يكون باللغة الانجليزية"
"IsEnglishOnly": "$t({path}.PROPERTY_MAPPINGS.{property}) يجب أن يكون باللغة الانجليزية",
"IsAbove18": "$t({path}.PROPERTY_MAPPINGS.{property}) يجب ان يكون فوق 18 سنة",
"IsValidPhoneNumber": "$t({path}.PROPERTY_MAPPINGS.{property}) يجب ان يكون رقم هاتف صحيح"
}

View File

@ -22,5 +22,7 @@
"IsLengthIn": "$t({path}.PROPERTY_MAPPINGS.{property}) must be {constraints.0} characters/digits",
"Matches": "$t({path}.PROPERTY_MAPPINGS.{property}) invalid format",
"IsValidExpiryDate": "$t({path}.PROPERTY_MAPPINGS.{property}) must be a valid expiry date",
"IsEnglishOnly": "$t({path}.PROPERTY_MAPPINGS.{property}) must be in English"
"IsEnglishOnly": "$t({path}.PROPERTY_MAPPINGS.{property}) must be in English",
"IsAbove18": "$t({path}.PROPERTY_MAPPINGS.{property}) must be above 18 years",
"IsValidPhoneNumber": "$t({path}.PROPERTY_MAPPINGS.{property}) must be a valid phone number"
}

View File

@ -15,7 +15,6 @@ async function bootstrap() {
if (config.getOrThrow<string>('NODE_ENV') === 'dev') {
SwaggerModule.setup(config.getOrThrow<string>('SWAGGER_API_DOCS_PATH'), app, swaggerDocument, {
swaggerOptions: {
tagsSorter: 'alpha',
docExpansion: 'none',
},
});