Skip to content

Commit

Permalink
Merge pull request #45 from Brints/google_auth
Browse files Browse the repository at this point in the history
auth: Add Google OAuth
  • Loading branch information
aniebietafia authored Oct 13, 2024
2 parents 5dfb1de + 920385a commit 207f0db
Show file tree
Hide file tree
Showing 27 changed files with 3,546 additions and 330 deletions.
3 changes: 3 additions & 0 deletions brints-estate-api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,6 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# documenation
documentation/
9 changes: 6 additions & 3 deletions brints-estate-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"name": "brints-estate-api",
"version": "0.0.1",
"description": "A Real Estate API",
"author": "",
"author": "Aniebiet Afia",
"private": true,
"license": "UNLICENSED",
"license": "MIT",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
Expand All @@ -18,13 +18,15 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"api:doc": "npx @compodoc/compodoc -p tsconfig.json -s --port 3002 --watch -d ./documentation"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.658.1",
"@aws-sdk/client-s3": "^3.658.1",
"@aws-sdk/client-ses": "^3.658.1",
"@aws-sdk/client-sns": "^3.658.1",
"@compodoc/compodoc": "^1.1.25",
"@hapi/joi": "^17.1.1",
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/common": "^10.4.1",
Expand All @@ -40,6 +42,7 @@
"class-validator": "^0.14.1",
"cloudinary": "^2.4.0",
"ejs": "^3.1.10",
"google-auth-library": "^9.14.2",
"joi": "^17.13.3",
"multer": "^1.4.5-lts.1",
"multer-storage-cloudinary": "^4.0.0",
Expand Down
5 changes: 4 additions & 1 deletion brints-estate-api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import { SendOtpProvider } from 'src/services/email-service/mailgun-service/prov
import { SendPasswordResetTokenProvider } from 'src/services/email-service/mailgun-service/providers/send-password-reset-token.provider';
import { SendResetPasswordConfirmationProvider } from 'src/services/email-service/mailgun-service/providers/send-reset-password-confirmation.provider';
import { SendPasswordChangedEmailProvider } from 'src/services/email-service/mailgun-service/providers/send-password-changed-email.provider';
import { GoogleAuthenticationController } from './socials/google-authentication.controller';
import { GoogleAuthenticationService } from './socials/providers/google-authentication.service';

@Module({
controllers: [AuthController],
controllers: [AuthController, GoogleAuthenticationController],
providers: [
{
provide: HashingProvider,
Expand All @@ -57,6 +59,7 @@ import { SendPasswordChangedEmailProvider } from 'src/services/email-service/mai
SendPasswordResetTokenProvider,
SendResetPasswordConfirmationProvider,
SendPasswordChangedEmailProvider,
GoogleAuthenticationService,
],
imports: [
forwardRef(() => UsersModule),
Expand Down
2 changes: 2 additions & 0 deletions brints-estate-api/src/auth/config/jwt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ export default registerAs('jwt', () => {
audience: process.env.JWT_TOKEN_AUDIENCE,
issuer: process.env.JWT_TOKEN_ISSUER,
refresh_token_expires: Number(process.env.JWT_REFRESH_TOKEN_TTL ?? '86400'),
google_client_id: process.env.GOOGLE_OAUTH_CLIENT_ID,
google_client_secret: process.env.GOOGLE_OAUTH_CLIENT_SECRET,
};
});
1 change: 1 addition & 0 deletions brints-estate-api/src/auth/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class CreateUserDto {
phone_number: string;

@ApiProperty({
enum: UserGender,
description: 'The gender of the user.',
examples: ['female', 'male'],
type: String,
Expand Down
23 changes: 23 additions & 0 deletions brints-estate-api/src/auth/interfaces/auth-service.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { User } from 'src/users/entities/user.entity';

/**
* @description LoginUser Interface
* @interface ILoginUser
*/
export interface ILoginUser {
user: User;
tokens: { access_token: string; refresh_token: string };
}

/**
* @description RefreshToken Interface
* @interface IRefreshToken
*/
export interface IRefreshToken {
refresh_token: string;
}

export interface IAccessTokens {
access_token: string;
refresh_token: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface IGooglePayload {
email: string;
sub: string;
given_name: string;
family_name: string;
}
44 changes: 41 additions & 3 deletions brints-estate-api/src/auth/providers/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,45 @@ import { LoginUserProvider } from './login-user.provider';
import { RefreshTokenDto } from '../dto/refresh-token.dto';
import { RefreshTokensProvider } from './refresh-tokens.provider';
import { CreateLoginAttemptDto } from '../../login-attempts/dto/create-login-attempt.dto';
import { User } from 'src/users/entities/user.entity';
import {
ILoginUser,
IRefreshToken,
} from '../interfaces/auth-service.interface';

/**
* AuthService Class handles the business logic of the authentication module.
* @class AuthService
* @exports AuthService
* @constructor createUserProvider
* @constructor loginUserProvider
* @constructor refreshTokensProvider
*/
@Injectable()
export class AuthService {
/**
* Constructor AuthService
*/
constructor(
private readonly createUserProvider: CreateUserProvider,
private readonly loginUserProvider: LoginUserProvider,
private readonly refreshTokensProvider: RefreshTokensProvider,
) {}

/**
* createUser method handles the create user logic.
* @param createUserDto
* @param createUserAuthDto
* @param createLoginAttemptDto
* @param file
* @returns {Promise<User>}
*/
public async createUser(
createUserDto: CreateUserDto,
createUserAuthDto: CreateUserAuthDto,
createLoginAttemptDto: CreateLoginAttemptDto,
file: Express.Multer.File,
) {
): Promise<User> {
return this.createUserProvider.createUser(
createUserDto,
createUserAuthDto,
Expand All @@ -31,11 +55,25 @@ export class AuthService {
);
}

public async loginUser(loginUserDto: LoginUserDto) {
/**
* loginUser method handles the login logic.
* @param loginUserDto
* @returns {Promise<ILoginUser | undefined>}
*/
public async loginUser(
loginUserDto: LoginUserDto,
): Promise<ILoginUser | undefined> {
return this.loginUserProvider.loginUser(loginUserDto);
}

public async refreshTokens(refreshTokenDto: RefreshTokenDto) {
/**
* refreshTokens method handles the refresh token logic.
* @param refreshTokenDto
* @returns {Promise<IRefreshToken>}
*/
public async refreshTokens(
refreshTokenDto: RefreshTokenDto,
): Promise<IRefreshToken> {
return this.refreshTokensProvider.refreshTokens(refreshTokenDto);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class LoginUserProvider {

const passwordMatch: boolean = await this.hashingProvider.comparePassword(
loginUserDto.password,
user.password,
user.password as string,
);

if (!passwordMatch) {
Expand Down
7 changes: 7 additions & 0 deletions brints-estate-api/src/auth/socials/dto/google-token.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';

export class GoogleTokenDto {
@IsNotEmpty()
@IsString()
token: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
Body,
ClassSerializerInterceptor,
Controller,
HttpStatus,
Post,
UseFilters,
UseInterceptors,
} from '@nestjs/common';
import { GoogleAuthenticationService } from './providers/google-authentication.service';
import { AuthType } from '../enum/auth-type.enum';
import { Auth } from '../decorators/auth.decorator';
import { HttpExceptionFilter } from 'src/exceptions/http-exception.filter';
import { GoogleTokenDto } from './dto/google-token.dto';
import { ApiTags } from '@nestjs/swagger';

@Controller('auth')
@ApiTags('Authentication')
export class GoogleAuthenticationController {
constructor(
private readonly googleAuthenticationService: GoogleAuthenticationService,
) {}

@Post('google-auth')
@Auth(AuthType.None)
@UseInterceptors(ClassSerializerInterceptor)
@UseFilters(HttpExceptionFilter)
public async authenticate(@Body() googleTokenDto: GoogleTokenDto) {
const payload =
await this.googleAuthenticationService.authenticate(googleTokenDto);

return {
message: 'Google Login Successful',
status_code: HttpStatus.OK,
payload,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
forwardRef,
HttpStatus,
Inject,
Injectable,
OnModuleInit,
} from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import { OAuth2Client } from 'google-auth-library';
import jwtConfig from 'src/auth/config/jwt.config';
import { GoogleTokenDto } from '../dto/google-token.dto';
import { UsersService } from 'src/users/providers/users.service';
import { CustomException } from 'src/exceptions/custom.exception';
import { GenerateTokensProvider } from 'src/auth/providers/generate-tokens.provider';
import { IGooglePayload } from 'src/auth/interfaces/google-token-payload.interface';
import { IAccessTokens } from 'src/auth/interfaces/auth-service.interface';

/**
* GoogleAuthenticationService Class handles the business logic of the google authentication module.
* @class GoogleAuthenticationService
* @exports GoogleAuthenticationService
*/
@Injectable()
export class GoogleAuthenticationService implements OnModuleInit {
private oAuthClient: OAuth2Client;

/**
* Constructor GoogleAuthenticationService
*/
constructor(
@Inject(forwardRef(() => UsersService))
private readonly usersService: UsersService,

@Inject(jwtConfig.KEY)
private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,

private readonly generateTokensProvider: GenerateTokensProvider,
) {}

/**
* onModuleInit method initializes the google authentication module.
*/
onModuleInit(): void {
this.oAuthClient = new OAuth2Client({
clientId: this.jwtConfiguration.google_client_id,
clientSecret: this.jwtConfiguration.google_client_secret,
});
}

/**
* getOAuthClient method returns the google oAuth client.
* @returns {OAuth2Client}
*/
public async authenticate(
googleTokenDto: GoogleTokenDto,
): Promise<IAccessTokens> {
const loginTicket = await this.oAuthClient.verifyIdToken({
idToken: googleTokenDto.token,
});

if (!loginTicket)
throw new CustomException(
HttpStatus.UNAUTHORIZED,
'Google login denied.',
);

const {
email,
sub: google_id,
given_name: first_name,
family_name: last_name,
} = loginTicket.getPayload() as IGooglePayload;

const user = await this.usersService.findOneByGoogleId(google_id);

if (user) {
return this.generateTokensProvider.generateTokens(user);
}

const newUser = await this.usersService.createGoogleUser({
email,
first_name,
last_name,
google_id,
});

return this.generateTokensProvider.generateTokens(newUser);
}
}
1 change: 1 addition & 0 deletions brints-estate-api/src/enums/gender.enum.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum UserGender {
MALE = 'male',
FEMALE = 'female',
BINARY = 'binary',
}
31 changes: 11 additions & 20 deletions brints-estate-api/src/users/dto/update-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from '../../auth/dto/create-user.dto';
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { PartialType, PickType } from '@nestjs/swagger';

export class UpdateUserDto extends PartialType(CreateUserDto) {
@ApiProperty({
description: 'The id of the user',
example: '60b7f3a8d8e9a7e4d8f9b1a7',
})
@IsNotEmpty()
@IsString()
id: string;

@ApiProperty({
description: "The user's country code",
example: '+234',
})
@IsOptional()
@IsString()
country_code?: string;
}
export class UpdateUserDto extends PartialType(
PickType(CreateUserDto, [
'first_name',
'last_name',
'country_code',
'phone_number',
'gender',
'marketing',
]),
) {}
4 changes: 2 additions & 2 deletions brints-estate-api/src/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export class User extends AbstractBaseEntity {
@Column({ type: 'varchar', length: 255, unique: true })
email: string;

@Column({ type: 'varchar', length: 255 })
@Column({ type: 'varchar', length: 255, nullable: true })
@Exclude()
password: string;
password?: string;

@Column({ type: 'varchar', length: 50, unique: true })
phone_number: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface GoogleUser {
email: string;
first_name: string;
last_name: string;
google_id: string;
}
Loading

0 comments on commit 207f0db

Please sign in to comment.