Skip to content

Commit

Permalink
CRYP-56 Add feature login with email link
Browse files Browse the repository at this point in the history
  • Loading branch information
thongnguyen5 authored and thongnguyendev committed May 6, 2024
1 parent 1370f8d commit 3488bd6
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 9 deletions.
32 changes: 27 additions & 5 deletions app/apps/onebox/src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { User } from '../users/schemas/user.schema';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { LoginResponseDto } from './dto/login.reponse.dto';
import { LoginEmailDto, LoginWithTokenDto } from './dto/login.dto';
import {
LoginEmailResponseDto,
LoginResponseDto,
} from './dto/login.reponse.dto';
import { LocalAuthGuard } from './guards/local-auth.guard';

@ApiTags('Auth')
Expand All @@ -24,10 +27,29 @@ export class AuthController {
@Post('login')
@HttpCode(200)
@ApiOkResponse({ type: LoginResponseDto })
async login(
async login(@Req() req: Request): Promise<LoginResponseDto> {
return this.authService.login(req.user as User);
}

@ApiOperation({ summary: 'Login Using Email only' })
@Post('login-email')
@HttpCode(200)
@ApiOkResponse({ type: LoginEmailResponseDto })
async loginWithEmail(
@Req() req: Request,
@Body() body: LoginDto,
@Body() body: LoginEmailDto,
): Promise<LoginEmailResponseDto> {
return this.authService.loginWithEmail(body);
}

@ApiOperation({ summary: 'Login Using Token' })
@Post('login-token')
@HttpCode(200)
@ApiOkResponse({ type: LoginResponseDto })
async loginWithToken(
@Req() req: Request,
@Body() body: LoginWithTokenDto,
): Promise<LoginResponseDto> {
return this.authService.login(req.user as User);
return this.authService.loginWithToken(body);
}
}
5 changes: 4 additions & 1 deletion app/apps/onebox/src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
import { DatabaseModule } from '@app/database';
import { UsersProviders } from '../users/users.providers';

@Module({
imports: [
Expand All @@ -18,9 +20,10 @@ import { LocalStrategy } from './strategies/local.strategy';
}),
UsersModule,
PassportModule,
DatabaseModule,
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
providers: [AuthService, LocalStrategy, JwtStrategy, ...UsersProviders],
exports: [AuthService],
})
export class AuthModule {}
68 changes: 66 additions & 2 deletions app/apps/onebox/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { ErrorCode } from '@app/global/global.error';
import { comparePassword } from '@app/utils/bcrypt.util';
import { Injectable } from '@nestjs/common';
import { sendEmail } from '@app/utils/email.sender';
import { renderTemplate } from '@app/utils/file-template';
import { generateUUID } from '@app/utils/uuidUtils';
import { Inject, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Builder } from 'builder-pattern';
import { Model } from 'mongoose';
import { User } from '../users/schemas/user.schema';
import { UsersService } from '../users/users.service';
import { LoginResponseDto } from './dto/login.reponse.dto';
import { LoginEmailDto, LoginWithTokenDto } from './dto/login.dto';
import {
LoginEmailResponseDto,
LoginResponseDto,
} from './dto/login.reponse.dto';

@Injectable()
export class AuthService {
constructor(
@Inject('USER_MODEL') private readonly userModel: Model<User>,
private usersService: UsersService,
private jwtService: JwtService,
) {}
Expand All @@ -29,4 +39,58 @@ export class AuthService {
};
return new LoginResponseDto(this.jwtService.sign(payload));
}

async loginWithEmail(request: LoginEmailDto): Promise<LoginEmailResponseDto> {
const user = await this.usersService.findOne(request.email.toLowerCase());
if (!user) {
throw ErrorCode.ACCOUNT_NOT_FOUND.asException();
}

const loginToken = generateUUID();
const tokenExpire = Date.now() + 5 * 60 * 1000; // 5 minutes
await this.userModel.updateOne(
{ userId: user.userId },
{
emailLogin: {
token: loginToken,
expire: tokenExpire,
},
},
);
const encodedToken = Buffer.from(`${user.email}:${loginToken}`).toString(
'base64',
);
const linkEmailLogin = `https://${process.env.WEB_DOMAIN}/token-login?token=${encodedToken}`;
const emailBody = await renderTemplate(
'resources/email_template/token_login.html',
{
linkEmailLogin,
expire: new Date(tokenExpire).toUTCString(),
},
);
sendEmail(user.email, 'Login Confirmation', emailBody);
return Builder<LoginEmailResponseDto>().success(true).build();
}

async loginWithToken(request: LoginWithTokenDto): Promise<LoginResponseDto> {
const decodedToken = Buffer.from(request.token, 'base64').toString();
const [email, loginToken] = decodedToken.split(':');
const user = await this.usersService.findOne(email);
if (!user || !user.emailLogin) {
throw ErrorCode.WRONG_EMAIL_OR_TOKEN.asException();
}
if (user.emailLogin.token !== loginToken) {
throw ErrorCode.WRONG_EMAIL_OR_TOKEN.asException();
}
if (user.emailLogin.expire < Date.now()) {
throw ErrorCode.WRONG_EMAIL_OR_TOKEN.asException();
}
await this.userModel.updateOne(
{ userId: user.userId },
{
$unset: { emailLogin: '' },
},
);
return this.login(user);
}
}
17 changes: 16 additions & 1 deletion app/apps/onebox/src/modules/auth/dto/login.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsEmail } from 'class-validator';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class LoginDto {
@ApiProperty()
Expand All @@ -13,3 +13,18 @@ export class LoginDto {
@IsNotEmpty()
password: string;
}

export class LoginEmailDto {
@ApiProperty()
@IsString()
@IsEmail()
@IsNotEmpty()
email: string;
}

export class LoginWithTokenDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
token: string;
}
5 changes: 5 additions & 0 deletions app/apps/onebox/src/modules/auth/dto/login.reponse.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export class LoginResponseDto {
this.accessToken = accessToken;
}
}

export class LoginEmailResponseDto {
@ApiResponseProperty()
success: boolean;
}
6 changes: 6 additions & 0 deletions app/apps/onebox/src/modules/users/schemas/user.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export class User {
token: string;
expire: number;
};

@Prop({ type: Object })
emailLogin: {
token: string;
expire: number;
};
}

export const UserSchema = SchemaFactory.createForClass(User);
1 change: 1 addition & 0 deletions app/libs/global/src/global.error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class ErrorCode {
401002,
'Wrong email or password',
);
static WRONG_EMAIL_OR_TOKEN = new ErrorCode(401003, 'Wrong email or token');

// Forbidden - Client is authorized but doesn't have permission to perform action
static FORBIDDEN = new ErrorCode(403001, 'Forbidden');
Expand Down
109 changes: 109 additions & 0 deletions app/resources/email_template/token_login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Document</title>
</head>
<body>
<table align="center">
<tbody>
<tr>
<td>
<table
align="center"
style="
width: 100%;
margin: 0;
padding: 0;
background-color: #ffffff;
width: 700px;
border-radius: 5px;
border: solid 1px #ebebeb;
margin-left: auto;
margin-right: auto;
"
>
<tbody>
<tr style="height: 131px; text-align: center">
<td style="padding: 0; border-bottom: solid 1px #ebebeb">
<a href="https://crypitor.com/" target="_blank">
<img
src="https://crypitor-asset.s3.ap-southeast-1.amazonaws.com/Artboard+2+copy.png"
width="300"/>

</a>
</td>
</tr>
<tr>
<td style="padding: 24px 67px 46px">
<div style="font-size: 17px; font-weight: 500">
Hello,
</div>
<div
style="
margin-top: 24px;
margin-bottom: 24px;
font-size: 17px;
line-height: 30px;
"
>
You have requested to login to your account.
<br>
Please click on the button below to complete your login.
<br>
Your request is valid for 5 minutes and will be expired at {{ expire }}.
</div>
<div style="text-align: center">
<a
href="{{ linkEmailLogin }}"
target="_blank"
style="
font-size: 18px;
font-weight: 600;
display: inline-block;
background-color: #0052FF;
text-align: center;
color: white;
text-decoration: none;
border-radius: 5px;
padding: 16px 24px;
"
>
Login now
</a>
</div>
<div
style="
margin-top: 24px;
margin-bottom: 24px;
font-size: 17px;
line-height: 30px;
"
>
If you did not perform this action, please contact us immediately at <a href="mailto:[email protected]">[email protected]</a>
</div>
</td>
</tr>
<tr>
<td
style="
text-align: center;
color: #878787;
font-size: 11px;
padding-bottom: 29px;
line-height: 20px;
"
>
© 2024 All rights reserved
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>

0 comments on commit 3488bd6

Please sign in to comment.