diff --git a/package-lock.json b/package-lock.json index 0b01f0c..618b880 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "*", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.2.10", "@nestjs/swagger": "^7.1.16", "@nestjs/terminus": "^10.2.0", @@ -30,9 +31,13 @@ "dayjs": "^1.11.10", "dotenv": "^16.3.1", "express": "^4.18.2", + "generate-password": "^1.7.1", + "google-auth-library": "^9.4.2", "lint-staged": "^15.1.0", "nest-winston": "^1.9.4", "nestjs-i18n": "^10.4.0", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "stripe": "^14.9.0", @@ -52,6 +57,7 @@ "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-google-oauth20": "^2.0.14", "@types/supertest": "^2.0.12", "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.0.0", @@ -2018,6 +2024,15 @@ } } }, + "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==", + "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.2.10", "license": "MIT", @@ -2581,6 +2596,46 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/oauth": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.4.tgz", + "integrity": "sha512-qk9orhti499fq5XxKCCEbd0OzdPZuancneyse3KtR+vgMiHRbh+mn8M4G6t64ob/Fg+GZGpa565MF/2dKWY32A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/passport": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", + "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-google-oauth20": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.14.tgz", + "integrity": "sha512-ZaZpRUAeMl3vy298ulKO1wGLn9SQtj/CyIfZL/Px5xU9pybMiQU3mhXDCBiWSbg0EK9uXT4ZoWC3ktuWY+5fwQ==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-oauth2": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.15.tgz", + "integrity": "sha512-9cUTP/HStNSZmhxXGuRrBJfEWzIEJRub2eyJu3CvkA+8HAMc9W3aKdFhVq+Qz1hi42qn+GvSAnz3zwacDSYWpw==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, "node_modules/@types/pug": { "version": "2.0.6", "license": "MIT", @@ -3410,7 +3465,8 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "dev": true, + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -3450,6 +3506,14 @@ "node": ">=0.6" } }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "license": "MIT", @@ -5696,6 +5760,11 @@ "node": ">= 0.8" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/extend-object": { "version": "1.0.0", "license": "MIT" @@ -6161,6 +6230,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz", + "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/generate-password": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/generate-password/-/generate-password-1.7.1.tgz", + "integrity": "sha512-9bVYY+16m7W7GczRBDqXE+VVuCX+bWNrfYKC/2p2JkZukFb2sKxT6E3zZ3mJGz7GMe5iRK0A/WawSL3jQfJuNQ==" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "dev": true, @@ -6443,6 +6566,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.4.2.tgz", + "integrity": "sha512-rTLO4gjhqqo3WvYKL5IdtlCvRqeQ4hxUx/p4lObobY2xotFW3bCQC+Qf1N51CYOfiqfMecdMwW9RIo7dFWYjqw==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/gopd": { "version": "1.0.1", "license": "MIT", @@ -6462,6 +6620,37 @@ "dev": true, "license": "MIT" }, + "node_modules/gtoken": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.0.1.tgz", + "integrity": "sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/handlebars": { "version": "4.7.8", "license": "MIT", @@ -7873,6 +8062,14 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "dev": true, @@ -9603,6 +9800,11 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "node_modules/object-assign": { "version": "4.1.1", "license": "MIT", @@ -9923,6 +10125,61 @@ "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==", + "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-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", + "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "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", "dev": true, @@ -9982,6 +10239,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", "license": "MIT", @@ -12152,6 +12414,11 @@ "node": ">=8" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "node_modules/undici-types": { "version": "5.26.5", "license": "MIT" diff --git a/package.json b/package.json index 5796e0c..faf429c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "*", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.2.10", "@nestjs/swagger": "^7.1.16", "@nestjs/terminus": "^10.2.0", @@ -54,9 +55,13 @@ "dayjs": "^1.11.10", "dotenv": "^16.3.1", "express": "^4.18.2", + "generate-password": "^1.7.1", + "google-auth-library": "^9.4.2", "lint-staged": "^15.1.0", "nest-winston": "^1.9.4", "nestjs-i18n": "^10.4.0", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "stripe": "^14.9.0", @@ -76,6 +81,7 @@ "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-google-oauth20": "^2.0.14", "@types/supertest": "^2.0.12", "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.0.0", diff --git a/prisma/migrations/20240119135401_add_auth_type_and_sign/migration.sql b/prisma/migrations/20240119135401_add_auth_type_and_sign/migration.sql new file mode 100644 index 0000000..5fdd4b5 --- /dev/null +++ b/prisma/migrations/20240119135401_add_auth_type_and_sign/migration.sql @@ -0,0 +1,6 @@ +-- CreateEnum +CREATE TYPE "AuthType" AS ENUM ('EMAIL', 'GOOGLE'); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "authSocialSign" TEXT, +ADD COLUMN "authType" "AuthType" NOT NULL DEFAULT 'EMAIL'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cee4924..d377979 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,6 +8,8 @@ model User { isEmailVerified Boolean @default(false) isVerified Boolean @default(false) isDeleted Boolean @default(false) + authType AuthType @default(EMAIL) + authSocialSign String? createdAt DateTime @default(now()) hashedRefreshToken String? activationLink String? @@ -176,6 +178,11 @@ enum Status { COMPLETED } +enum AuthType { + EMAIL + GOOGLE +} + generator client { provider = "prisma-client-js" } diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 3fc7daf..76146dc 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -38,6 +38,7 @@ import { RegisterDto, } from './dto'; import { PasswordUpdateDto } from './dto/update-password.dto'; +import { GoogleService } from './google/google.service'; import { VerificationSerivce } from './verification.service'; @ApiTags('auth') @@ -45,7 +46,8 @@ import { VerificationSerivce } from './verification.service'; export class AuthController { constructor( private readonly authService: AuthService, - private readonly verificationService: VerificationSerivce + private readonly verificationService: VerificationSerivce, + private readonly googleService: GoogleService ) {} @ApiOperation({ summary: 'Sign up user' }) @@ -185,4 +187,9 @@ export class AuthController { forgotPasswordReset(@Body() body: ForgotPasswordResetDto) { return this.authService.resetForgotPassword(body); } + + @Post('google/login') + async googleAuth(@Body('token') token: string, @Res({ passthrough: true }) res: Response) { + return await this.googleService.googleLogin(token, res); + } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 860d748..4496572 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,15 +1,17 @@ import { Module } from '@nestjs/common'; -import { AuthService } from './auth.service'; -import { AuthController } from './auth.controller'; -import { PrismaModule } from '../prisma/prisma.module'; import { JwtModule } from '@nestjs/jwt'; -import { UserGuard } from '../common/guards/user.guard'; import { MailerModule } from 'src/mailer/mailer.module'; +import { UserGuard } from '../common/guards/user.guard'; +import { PrismaModule } from '../prisma/prisma.module'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { GoogleService } from './google/google.service'; +import { PasswordService } from './google/password.service'; import { VerificationSerivce } from './verification.service'; @Module({ imports: [PrismaModule, JwtModule.register({}), MailerModule], controllers: [AuthController], - providers: [AuthService, UserGuard, VerificationSerivce], + providers: [AuthService, UserGuard, VerificationSerivce, GoogleService, PasswordService], }) export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index fd5db90..7310882 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -360,8 +360,8 @@ export class AuthService { return token; } - /** SET REFRESH TOKEN COKKIE PRIVATE FUNCTION */ - private setRefreshTokenCookie(refresh_token: string, res: Response) { + /** SET REFRESH TOKEN COKKIE FUNCTION */ + setRefreshTokenCookie(refresh_token: string, res: Response) { const maxAge = parseInt(MAX_REFRESH_TOKEN_AGE || '0', 10); res.cookie('refresh_token', refresh_token, { maxAge, diff --git a/src/auth/google/google.service.ts b/src/auth/google/google.service.ts new file mode 100644 index 0000000..2e9d025 --- /dev/null +++ b/src/auth/google/google.service.ts @@ -0,0 +1,99 @@ +import { HttpException, Injectable, UnauthorizedException } from '@nestjs/common'; +import * as bcrypt from 'bcryptjs'; +import { Response } from 'express'; +import { OAuth2Client } from 'google-auth-library'; +import ErrorsTypes from 'src/errors/errors.enum'; +import { GlobalException } from 'src/exceptions/global.exception'; +import { MailerService } from 'src/mailer/mailer.service'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { AuthService } from '../auth.service'; +import { PasswordService } from './password.service'; + +const { SALT_LENGTH, GOOGLE_CLIENT_ID, GOOGLE_SECRET } = process.env; + +const SaltLength = parseInt(SALT_LENGTH!); + +const client = new OAuth2Client(GOOGLE_CLIENT_ID, GOOGLE_SECRET); + +@Injectable() +export class GoogleService { + constructor( + private readonly prismaService: PrismaService, + private readonly passwordService: PasswordService, + private readonly mailerService: MailerService, + private readonly authService: AuthService + ) {} + async googleLogin(token: string, res: Response) { + try { + const ticket = await client.verifyIdToken({ + idToken: token, + audience: process.env.GOOGLE_CLIENT_ID, + }); + + const user = ticket.getPayload(); + + if (!user) { + throw new UnauthorizedException(ErrorsTypes.AUTH_GOOGLE_FAILED_TO_LOGIN); + } + const { + email, + sub, + given_name: firstName, + family_name: lastName, + email_verified: isEmailVerified, + } = user; + + if (!email) { + throw new UnauthorizedException(ErrorsTypes.AUTH_GOOGLE_FAILED_GET_USER_DATA); + } + let userFromDb = await this.prismaService.user.findUnique({ + where: { email }, + }); + const password = this.passwordService.generatePassword(); + + if (!userFromDb) { + const hashedPassword: string = await bcrypt.hash(password, SaltLength); + + userFromDb = await this.prismaService.user.create({ + data: { + email, + password: hashedPassword, + firstName, + lastName, + isEmailVerified, + authSocialSign: sub, + authType: 'GOOGLE', + }, + }); + + await this.mailerService.sendHtmlEmail( + email, + 'Your credentials', + this.generateEmailBody(email, password) + ); + } + + const tokens = await this.authService.getTokens(userFromDb.id, email, userFromDb.role); + this.authService.setRefreshTokenCookie(tokens.refresh_token, res); + + return { tokens, id: userFromDb.id }; + } catch (error) { + if (error instanceof HttpException) throw error; + throw new GlobalException(ErrorsTypes.AUTH_FAILED_TO_LOGIN, error.message); + } + } + + private generateEmailBody(email: string, password: string): string { + return ` +
+
+ Your credentials to log in without using Google service (you can also log in with Google) +
+
Email:
+
${email}
+
Password:
+
${password}
+
+`; + } +} diff --git a/src/auth/google/password.service.ts b/src/auth/google/password.service.ts new file mode 100644 index 0000000..12d13c4 --- /dev/null +++ b/src/auth/google/password.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import * as generator from 'generate-password'; + +@Injectable() +export class PasswordService { + generatePassword(): string { + const passwordOptions = { + length: 12, + numbers: true, + symbols: true, + uppercase: true, + excludeSimilarCharacters: true, + strict: true, + }; + + return generator.generate(passwordOptions); + } +} diff --git a/src/errors/errors.enum.ts b/src/errors/errors.enum.ts index 4beb295..5cb2c63 100644 --- a/src/errors/errors.enum.ts +++ b/src/errors/errors.enum.ts @@ -50,6 +50,8 @@ enum ErrorsTypes { AUTH_FAILED_TOKEN_VERIFY = 'AUTH_FAILED_TOKEN_VERIFY', AUTH_FAILED_TO_UPDATE_PASSWORD = 'AUTH_FAILED_TO_UPDATE_PASSWORD', AUTH_FORGOT_PASSWORD_FAILED_TO_SEND_EMAIL = 'AUTH_FORGOT_PASSWORD_FAILED_TO_SEND_EMAIL', + AUTH_GOOGLE_FAILED_TO_LOGIN = 'AUTH_GOOGLE_FAILED_TO_LOGIN', + AUTH_GOOGLE_FAILED_GET_USER_DATA = 'AUTH_GOOGLE_FAILED_GET_USER_DATA', // Amenities module CONFLICT_AMENITIES_ALREADY_EXIST = 'CONFLICT_AMENITIES_ALREADY_EXIST', diff --git a/src/i18n/de/errors.json b/src/i18n/de/errors.json index 11dab9c..2bc96b9 100644 --- a/src/i18n/de/errors.json +++ b/src/i18n/de/errors.json @@ -58,6 +58,8 @@ "AUTH_FAILED_SEND_VERIFICATION_EMAIL": "Bestätigungs-E-Mail konnte nicht gesendet werden", "AUTH_FAILED_TOKEN_VERIFY": "Benutzertoken konnte nicht überprüft werden", "AUTH_FORGOT_PASSWORD_FAILED_TO_SEND_EMAIL": "Das Senden einer E-Mail zum Festlegen eines neuen Passworts ist fehlgeschlagen", + "AUTH_GOOGLE_FAILED_TO_LOGIN": "Google-Authentifizierung fehlgeschlagen", + "AUTH_GOOGLE_FAILED_GET_USER_DATA": "Fehler beim Abrufen von Benutzerinformationen von Google", "FORBIDDEN_FORGOT_PASSWORD_INVALID_TOKEN": "Ungültiges Reset-Token", "USER_FAILED_TO_GET_LIST": "Benutzerliste konnte nicht abgerufen werden", "USER_FAILED_TO_GET": "Benutzer konnte nicht abgerufen werden", diff --git a/src/i18n/en/errors.json b/src/i18n/en/errors.json index 33cd73f..7408fb8 100644 --- a/src/i18n/en/errors.json +++ b/src/i18n/en/errors.json @@ -58,6 +58,8 @@ "AUTH_FAILED_SEND_VERIFICATION_EMAIL": "Failed to send verification email", "AUTH_FAILED_TOKEN_VERIFY": "Failed to verify user token", "AUTH_FORGOT_PASSWORD_FAILED_TO_SEND_EMAIL": "Failed to send email to set a new password", + "AUTH_GOOGLE_FAILED_TO_LOGIN": "Google authentication failed", + "AUTH_GOOGLE_FAILED_GET_USER_DATA": "Failed to fetch user information from Google", "FORBIDDEN_FORGOT_PASSWORD_INVALID_TOKEN": "Invalid reset token", "USER_FAILED_TO_GET_LIST": "Failed to get users list", "USER_FAILED_TO_GET": "Failed to get user", diff --git a/src/i18n/kz/errors.json b/src/i18n/kz/errors.json index 9e55622..873d7f8 100644 --- a/src/i18n/kz/errors.json +++ b/src/i18n/kz/errors.json @@ -58,6 +58,8 @@ "AUTH_FAILED_SEND_VERIFICATION_EMAIL": "Растау электрондық поштасын жіберу мүмкін болмады", "AUTH_FAILED_TOKEN_VERIFY": "Пайдаланушы таңбалауышы расталмады", "AUTH_FORGOT_PASSWORD_FAILED_TO_SEND_EMAIL": "Жаңа құпия сөз орнату үшін электрондық поштаны жіберу мүмкін болмады", + "AUTH_GOOGLE_FAILED_TO_LOGIN": "Google авторизациясы сәтсіз аяқталды", + "AUTH_GOOGLE_FAILED_GET_USER_DATA": "Google-дан пайдаланушы ақпаратты алу сәтсіз аяқталды", "FORBIDDEN_FORGOT_PASSWORD_INVALID_TOKEN": "Жарамсыз қалпына келтіру белгісі", "USER_FAILED_TO_GET_LIST": "Пайдаланушылар тізімін алу мүмкін болмады", "USER_FAILED_TO_GET": "Пайдаланушыны алу мүмкін болмады", diff --git a/src/i18n/ru/errors.json b/src/i18n/ru/errors.json index 7d61441..89e032d 100644 --- a/src/i18n/ru/errors.json +++ b/src/i18n/ru/errors.json @@ -58,6 +58,8 @@ "AUTH_FAILED_SEND_VERIFICATION_EMAIL": "Не удалось отправить письмо с подтверждением", "AUTH_FAILED_TOKEN_VERIFY": "Не удалось проверить токен пользователя", "AUTH_FORGOT_PASSWORD_FAILED_TO_SEND_EMAIL": "Не удалось отправить электронное письмо для установки нового пароля.", + "AUTH_GOOGLE_FAILED_TO_LOGIN": "Авторизация через Google не удалась", + "AUTH_GOOGLE_FAILED_GET_USER_DATA": "Не удалось получить информацию о пользователе от Google", "FORBIDDEN_FORGOT_PASSWORD_INVALID_TOKEN": "Неверный токен сброса", "USER_FAILED_TO_GET_LIST": "Не удалось получить список пользователей", "USER_FAILED_TO_GET": "Не удалось получить пользователя", diff --git a/src/i18n/uz/errors.json b/src/i18n/uz/errors.json index 08a6717..408a829 100644 --- a/src/i18n/uz/errors.json +++ b/src/i18n/uz/errors.json @@ -58,6 +58,8 @@ "AUTH_FAILED_SEND_VERIFICATION_EMAIL": "Tasdiqlash xati yuborilmadi", "AUTH_FAILED_TOKEN_VERIFY": "Tokenni tasdiqlashda xatolik", "AUTH_FORGOT_PASSWORD_FAILED_TO_SEND_EMAIL": "Yangi parol oʻrnatish uchun xat yuborishda xatolik", + "AUTH_GOOGLE_FAILED_TO_LOGIN": "Google autentifikatsiyasi muvaffaqiyatsiz tugadi", + "AUTH_GOOGLE_FAILED_GET_USER_DATA": "Google dan foydalanuvchi ma'lumotlari olish muvaffaqiyatsiz tugadi", "FORBIDDEN_FORGOT_PASSWORD_INVALID_TOKEN": "Noto'g'ri tiklash tokeni", "USER_FAILED_TO_GET_LIST": "Foydalanuvchilar ro'yxatini olib bo'lmadi", "USER_FAILED_TO_GET": "Foydalanuvchini topib bo'lmadi", diff --git a/src/prisma/seeds/users.json b/src/prisma/seeds/users.json index d7a017a..f02a135 100644 --- a/src/prisma/seeds/users.json +++ b/src/prisma/seeds/users.json @@ -9,6 +9,7 @@ "isEmailVerified": true, "isVerified": true, "isDeleted": false, + "authType": "EMAIL", "profile": { "create": { "id": "1", @@ -32,6 +33,7 @@ "isEmailVerified": true, "isVerified": true, "isDeleted": false, + "authType": "EMAIL", "profile": { "create": { "id": "2", @@ -55,6 +57,7 @@ "isEmailVerified": true, "isVerified": true, "isDeleted": false, + "authType": "EMAIL", "profile": { "create": { "id": "3", @@ -78,6 +81,7 @@ "isEmailVerified": true, "isVerified": true, "isDeleted": false, + "authType": "EMAIL", "profile": { "create": { "id": "4", @@ -101,6 +105,7 @@ "isEmailVerified": true, "isVerified": true, "isDeleted": false, + "authType": "EMAIL", "profile": { "create": { "id": "5", @@ -124,6 +129,7 @@ "isEmailVerified": true, "isVerified": true, "isDeleted": false, + "authType": "EMAIL", "profile": { "create": { "id": "6", @@ -147,6 +153,7 @@ "isEmailVerified": true, "isVerified": true, "isDeleted": false, + "authType": "EMAIL", "profile": { "create": { "id": "7", @@ -170,6 +177,7 @@ "isEmailVerified": true, "isVerified": true, "isDeleted": false, + "authType": "EMAIL", "profile": { "create": { "id": "8", @@ -193,6 +201,7 @@ "isEmailVerified": true, "isVerified": true, "isDeleted": false, + "authType": "EMAIL", "profile": { "create": { "id": "9", @@ -216,6 +225,7 @@ "isEmailVerified": true, "isVerified": true, "isDeleted": false, + "authType": "EMAIL", "profile": { "create": { "id": "10",