Merge pull request #88 from SyncrowIOT/feature/google-login

added google login
This commit is contained in:
yousef-khriasat-uba
2024-10-07 09:13:00 +03:00
committed by GitHub
7 changed files with 240 additions and 26 deletions

View File

@ -90,3 +90,6 @@ FIREBASE_DATABASE_URL=
OTP_LIMITER= OTP_LIMITER=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

View File

@ -1,4 +1,8 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import {
BadRequestException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import * as argon2 from 'argon2'; import * as argon2 from 'argon2';
import { HelperHashService } from '../../helper/services'; import { HelperHashService } from '../../helper/services';
@ -6,16 +10,21 @@ import { UserRepository } from '../../../../common/src/modules/user/repositories
import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository'; import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository';
import { UserSessionEntity } from '../../../../common/src/modules/session/entities'; import { UserSessionEntity } from '../../../../common/src/modules/session/entities';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import { OAuth2Client } from 'google-auth-library';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
private client: OAuth2Client;
constructor( constructor(
private jwtService: JwtService, private jwtService: JwtService,
private readonly userRepository: UserRepository, private readonly userRepository: UserRepository,
private readonly sessionRepository: UserSessionRepository, private readonly sessionRepository: UserSessionRepository,
private readonly helperHashService: HelperHashService, private readonly helperHashService: HelperHashService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
) {} ) {
this.client = new OAuth2Client(this.configService.get('GOOGLE_CLIENT_ID'));
}
async validateUser( async validateUser(
email: string, email: string,
@ -80,8 +89,17 @@ export class AuthService {
type: user.type, type: user.type,
sessionId: user.sessionId, sessionId: user.sessionId,
roles: user?.roles, roles: user?.roles,
googleCode: user.googleCode,
}; };
if (payload.googleCode) {
const profile = await this.getProfile(payload.googleCode);
user = await this.userRepository.findOne({
where: { email: profile.email },
});
if (!user) {
return { profile };
}
}
const tokens = await this.getTokens(payload); const tokens = await this.getTokens(payload);
await this.updateRefreshToken(user.uuid, tokens.refreshToken); await this.updateRefreshToken(user.uuid, tokens.refreshToken);
return tokens; return tokens;
@ -100,4 +118,19 @@ export class AuthService {
hashData(data: string) { hashData(data: string) {
return argon2.hash(data); return argon2.hash(data);
} }
async getProfile(googleCode: string) {
try {
const ticket = await this.client.verifyIdToken({
idToken: googleCode,
audience: this.configService.get('GOOGLE_CLIENT_ID'),
});
const payload = ticket.getPayload();
return {
...payload,
};
} catch (error) {
throw new UnauthorizedException('Google login failed');
}
}
} }

164
package-lock.json generated
View File

@ -20,13 +20,14 @@
"@nestjs/websockets": "^10.3.8", "@nestjs/websockets": "^10.3.8",
"@tuya/tuya-connector-nodejs": "^2.1.2", "@tuya/tuya-connector-nodejs": "^2.1.2",
"argon2": "^0.40.1", "argon2": "^0.40.1",
"axios": "^1.6.7", "axios": "^1.7.7",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"firebase": "^10.12.5", "firebase": "^10.12.5",
"google-auth-library": "^9.14.1",
"helmet": "^7.1.0", "helmet": "^7.1.0",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"morgan": "^1.10.0", "morgan": "^1.10.0",
@ -3564,6 +3565,18 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/agent-base": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "8.12.0", "version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
@ -3784,11 +3797,12 @@
"integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==" "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g=="
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.6.7", "version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.4", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
@ -3962,6 +3976,15 @@
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
}, },
"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==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -5887,15 +5910,16 @@
"dev": true "dev": true
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.5", "version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh" "url": "https://github.com/sponsors/RubenVerborgh"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": ">=4.0" "node": ">=4.0"
}, },
@ -6056,6 +6080,35 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"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==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^6.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -6194,6 +6247,44 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/google-auth-library": {
"version": "9.14.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.1.tgz",
"integrity": "sha512-Rj+PMjoNFGFTmtItH7gHfbHpGVSb3vmnGK3nwNBqxQF9NoBpttSZI/rc0WiM63ma2uGDQtYEkMHkK9U6937NiA==",
"license": "Apache-2.0",
"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==",
"license": "MIT",
"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==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@ -6217,6 +6308,40 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true "dev": true
}, },
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"license": "MIT",
"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==",
"license": "MIT",
"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==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/har-schema": { "node_modules/har-schema": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@ -6384,6 +6509,19 @@
"npm": ">=1.3.7" "npm": ">=1.3.7"
} }
}, },
"node_modules/https-proxy-agent": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@ -6689,7 +6827,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
}, },
@ -7533,6 +7670,15 @@
"node": ">=4" "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==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json-buffer": { "node_modules/json-buffer": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",

View File

@ -31,13 +31,14 @@
"@nestjs/websockets": "^10.3.8", "@nestjs/websockets": "^10.3.8",
"@tuya/tuya-connector-nodejs": "^2.1.2", "@tuya/tuya-connector-nodejs": "^2.1.2",
"argon2": "^0.40.1", "argon2": "^0.40.1",
"axios": "^1.6.7", "axios": "^1.7.7",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"firebase": "^10.12.5", "firebase": "^10.12.5",
"google-auth-library": "^9.14.1",
"helmet": "^7.1.0", "helmet": "^7.1.0",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"morgan": "^1.10.0", "morgan": "^1.10.0",

View File

@ -41,5 +41,5 @@ export class UserSignUpDto {
@IsString() @IsString()
@IsOptional() @IsOptional()
public regionUuid: string; public regionUuid?: string;
} }

View File

@ -1,19 +1,23 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsEmail, IsOptional, IsString } from 'class-validator';
export class UserLoginDto { export class UserLoginDto {
@ApiProperty() @ApiProperty()
@IsEmail() @IsEmail()
@IsNotEmpty() @IsOptional()
email: string; email?: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
@IsOptional() @IsOptional()
password: string; password?: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
@IsOptional() @IsOptional()
regionUuid?: string; regionUuid?: string;
@IsOptional()
@IsString()
googleCode?: string;
} }

View File

@ -19,6 +19,7 @@ import * as argon2 from 'argon2';
import { differenceInSeconds } from '@app/common/helper/differenceInSeconds'; import { differenceInSeconds } from '@app/common/helper/differenceInSeconds';
import { LessThan, MoreThan } from 'typeorm'; import { LessThan, MoreThan } from 'typeorm';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { UUID } from 'typeorm/driver/mongodb/bson.typings';
@Injectable() @Injectable()
export class UserAuthService { export class UserAuthService {
@ -89,13 +90,38 @@ export class UserAuthService {
async userLogin(data: UserLoginDto) { async userLogin(data: UserLoginDto) {
try { try {
const user = await this.authService.validateUser( let user;
data.email, if (data.googleCode) {
data.password, const googleUserData = await this.authService.login({
data.regionUuid, googleCode: data.googleCode,
); });
if (!user) { const userExists = await this.userRepository.exists({
throw new UnauthorizedException('Invalid login credentials.'); where: {
email: googleUserData['email'],
},
});
user = await this.userRepository.findOne({
where: {
email: googleUserData['email'],
},
});
if (!userExists) {
await this.signUp({
email: googleUserData['email'],
firstName: googleUserData['given_name'],
lastName: googleUserData['family_name'],
password: googleUserData['email'],
});
}
data.email = googleUserData['email'];
data.password = googleUserData['password'];
}
if (!data.googleCode) {
user = await this.authService.validateUser(
data.email,
data.password,
data.regionUuid,
);
} }
const session = await Promise.all([ const session = await Promise.all([
await this.sessionRepository.update( await this.sessionRepository.update(
@ -110,7 +136,7 @@ export class UserAuthService {
isLoggedOut: false, isLoggedOut: false,
}), }),
]); ]);
return await this.authService.login({ const res = await this.authService.login({
email: user.email, email: user.email,
userId: user.uuid, userId: user.uuid,
uuid: user.uuid, uuid: user.uuid,
@ -119,6 +145,7 @@ export class UserAuthService {
}), }),
sessionId: session[1].uuid, sessionId: session[1].uuid,
}); });
return res;
} catch (error) { } catch (error) {
throw new BadRequestException('Invalid credentials'); throw new BadRequestException('Invalid credentials');
} }