Skip to content

Commit

Permalink
Merge pull request #255 from boostcampwm2023/feat/be/mailer
Browse files Browse the repository at this point in the history
์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ ์ธ์ฆ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ถ”๊ฐ€
  • Loading branch information
sickbirdd authored Feb 26, 2024
2 parents 8f569c1 + a402f47 commit 17c0e5a
Show file tree
Hide file tree
Showing 22 changed files with 2,569 additions and 150 deletions.
2,217 changes: 2,086 additions & 131 deletions backend/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs-modules/mailer": "^1.10.3",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
Expand All @@ -45,6 +46,7 @@
"mongoose": "^8.0.1",
"mysql2": "^3.6.3",
"nest-winston": "^1.9.4",
"nodemailer": "^6.9.9",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
Expand Down
3 changes: 3 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { MONGODB_URL } from './constants';
import { ScheduleModule } from '@nestjs/schedule';
import { RedisModule } from '@songkeys/nestjs-redis';
import { RedisConfig } from './configs/redis.config';
import { MailerModule } from '@nestjs-modules/mailer';
import { MailerConfig } from './configs/mailer.config';

@Module({
imports: [
Expand All @@ -25,6 +27,7 @@ import { RedisConfig } from './configs/redis.config';
MongooseModule.forRoot(MONGODB_URL),
ScheduleModule.forRoot(),
RedisModule.forRoot(RedisConfig),
MailerModule.forRoot(MailerConfig),
],
controllers: [AppController],
providers: [AppService],
Expand Down
28 changes: 26 additions & 2 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import {
ApiBadRequestResponse,
ApiBody,
ApiGoneResponse,
ApiHeader,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { Controller, Get, HttpStatus, Req, UseFilters, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, HttpStatus, Post, Req, UseFilters, UseGuards } from '@nestjs/common';
import { HttpExceptionFilter } from 'src/exceptions/http.exception.filter';
import { AuthSuccess, ExpiredTokenError, InvalidTokenError, RefreshJWTSuccess } from 'src/dto/auth.swagger.dto';
import {
AuthSuccess,
ExpiredTokenError,
InvalidTokenError,
RefreshJWTSuccess,
VerifyEmailSuccess,
} from 'src/dto/auth.swagger.dto';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { Request } from 'express';
import { User } from 'src/entities/user.entity';
import { VerifyEmailDto } from 'src/dto/auth.verify.email.dto';
import { RequestError } from 'src/dto/product.swagger.dto';
import { EmailNotFound } from 'src/dto/user.swagger.dto';

@ApiTags('auth api')
@Controller('auth')
Expand Down Expand Up @@ -48,4 +60,16 @@ export class AuthController {
const { accessToken, refreshToken } = await this.authService.refreshJWT(req.user.id);
return { statusCode: HttpStatus.OK, message: 'ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์„ฑ๊ณต', accessToken, refreshToken };
}

@ApiOperation({ summary: '์ด๋ฉ”์ผ ์ธ์ฆ ์ฝ”๋“œ ๊ฒ€์ฆ', description: '์ด๋ฉ”์ผ ์ธ์ฆ ์ฝ”๋“œ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.' })
@ApiOkResponse({ type: VerifyEmailSuccess, description: '์ด๋ฉ”์ผ ์ธ์ฆ ์„ฑ๊ณต' })
@ApiBadRequestResponse({ type: RequestError, description: '์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.' })
@ApiNotFoundResponse({ type: EmailNotFound, description: 'ํ•ด๋‹น ์ด๋ฉ”์ผ๋กœ ๋ฐœ๊ธ‰๋ฐ›์€ ์ฝ”๋“œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋จ' })
@ApiBody({ type: VerifyEmailDto })
@Post('/verify/email')
async verifyEmail(@Body() verifyEmailDto: VerifyEmailDto) {
await this.authService.verifyEmail(verifyEmailDto.email, verifyEmailDto.verificationCode);
const verifyToken = await this.authService.getVerifyToken(verifyEmailDto.email);
return { statusCode: HttpStatus.OK, message: '์ด๋ฉ”์ผ ์ธ์ฆ ์„ฑ๊ณต', verifyToken };
}
}
27 changes: 24 additions & 3 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import * as bcrypt from 'bcrypt';
import { User } from 'src/entities/user.entity';
import { ValidationException } from 'src/exceptions/validation.exception';
import { JwtService } from '@nestjs/jwt';
import { ACCESS_TOKEN_SECRETS, REFRESH_TOKEN_SECRETS, TWO_MONTHS_TO_SEC, TWO_WEEKS_TO_SEC } from 'src/constants';
import {
ACCESS_TOKEN_SECRETS,
REFRESH_TOKEN_SECRETS,
VERIFY_TOKEN_SECRETS,
TWO_MONTHS_TO_SEC,
TWO_WEEKS_TO_SEC,
} from 'src/constants';
import { InjectRedis } from '@songkeys/nestjs-redis';
import Redis from 'ioredis';

Expand All @@ -30,7 +36,7 @@ export class AuthService {
}

async validateUser(email: string, password: string): Promise<Record<string, string>> {
const user = await this.usersService.findOne(email);
const user = await this.usersService.findUserByEmail(email);
if (!user || !bcrypt.compareSync(password, user.password)) {
throw new ValidationException('๋กœ๊ทธ์ธ ์‹คํŒจ');
}
Expand All @@ -41,7 +47,7 @@ export class AuthService {
}

async refreshJWT(userId: string): Promise<Record<string, string>> {
const user = await this.usersService.getUserById(userId);
const user = await this.usersService.findUserById(userId);
if (!user) {
throw new HttpException('์œ ํšจํ•˜์ง€ ์•Š์€ refreshToken', HttpStatus.BAD_REQUEST);
}
Expand All @@ -59,4 +65,19 @@ export class AuthService {
throw new HttpException('Firebase ํ† ํฐ ๋“ฑ๋ก ์‹คํŒจ', HttpStatus.INTERNAL_SERVER_ERROR);
}
}

async verifyEmail(email: string, code: string) {
const verficationCode = await this.redis.get(`verficationCode:${email}`);
if (!verficationCode) {
throw new HttpException('ํ•ด๋‹น ์ด๋ฉ”์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.', HttpStatus.NOT_FOUND);
}
if (verficationCode !== code) {
throw new HttpException('์œ ํšจํ•˜์ง€ ์•Š์€ ์ธ์ฆ ์ฝ”๋“œ', HttpStatus.UNAUTHORIZED);
}
}

async getVerifyToken(email: string) {
const verifyToken = this.jwtService.sign({ email }, { secret: VERIFY_TOKEN_SECRETS, expiresIn: '5m' });
return verifyToken;
}
}
20 changes: 19 additions & 1 deletion backend/src/auth/jwt/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { ACCESS_TOKEN_SECRETS, REFRESH_TOKEN_SECRETS } from 'src/constants';
import { ACCESS_TOKEN_SECRETS, REFRESH_TOKEN_SECRETS, VERIFY_TOKEN_SECRETS } from 'src/constants';
import { JWTService } from './jwt.service';

@Injectable()
Expand Down Expand Up @@ -40,3 +40,21 @@ export class RefreshJwtStrategy extends PassportStrategy(Strategy, 'refresh') {
return { id: payload.id };
}
}

@Injectable()
export class VerifyJwtStrategy extends PassportStrategy(Strategy, 'verify') {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: true,
secretOrKey: VERIFY_TOKEN_SECRETS,
});
}

async validate(payload: any) {
if (Date.now() >= payload.exp * 1000) {
throw new HttpException('ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', HttpStatus.GONE);
}
return { email: payload.email };
}
}
17 changes: 17 additions & 0 deletions backend/src/configs/mailer.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MailerOptions } from '@nestjs-modules/mailer';
import { MAILER_PW, MAILER_USER } from 'src/constants';

export const MailerConfig: MailerOptions = {
transport: {
host: 'smtp.gmail.com',
prot: 587,
secure: false,
auth: {
user: MAILER_USER,
pass: MAILER_PW,
},
},
defaults: {
from: '"PriceGuard" <[email protected]>',
},
};
7 changes: 7 additions & 0 deletions backend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const DB_NAME = process.env.DB_NAME;
export const NODE_ENV = process.env.NODE_ENV;
export const ACCESS_TOKEN_SECRETS = process.env.ACCESS_TOKEN_SECRETS;
export const REFRESH_TOKEN_SECRETS = process.env.REFRESH_TOKEN_SECRETS;
export const VERIFY_TOKEN_SECRETS = process.env.VERIFY_TOKEN_SECRETS;
export const OPEN_API_KEY_11ST = process.env.OPEN_API_KEY_11ST as string;
export const BASE_URL_11ST = process.env.BASE_URL_11ST as string;
export const MAX_TRACKING_RANK = parseInt(process.env.MAX_TRACKING_RANK || '50');
Expand Down Expand Up @@ -54,3 +55,9 @@ type AddProductLimit = {
export const ADD_PRODUCT_LIMIT: AddProductLimit = {
tier1: 3,
};
export const MAILER_USER = process.env.MAILER_USER;
export const MAILER_PW = process.env.MAILER_PW;
export const MIN_VERFICATION_CODE = 100000;
export const MAX_VERFICATION_CODE = 999999;
export const MAX_SENDING_EMAIL = 5;
export const THREE_MIN_TO_SEC = 3 * 60;
18 changes: 18 additions & 0 deletions backend/src/dto/auth.swagger.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,21 @@ export class RefreshJWTSuccess {
})
refreshToken: string;
}

export class VerifyEmailSuccess {
@ApiProperty({
example: HttpStatus.OK,
description: 'Http ์ƒํƒœ ์ฝ”๋“œ',
})
statusCode: number;
@ApiProperty({
example: '์ด๋ฉ”์ผ ์ธ์ฆ ์„ฑ๊ณต',
description: '๋ฉ”์‹œ์ง€',
})
message: string;
@ApiProperty({
example: 'verify token example',
description: 'Verify Token์˜ ๋งŒ๋ฃŒ ๊ธฐ๊ฐ„์€ 3๋ถ„์ด๋‹ค.',
})
verifyToken: string;
}
21 changes: 21 additions & 0 deletions backend/src/dto/auth.verify.email.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class VerifyEmailDto {
@ApiProperty({
example: '[email protected]',
description: '์ธ์ฆ์„ ์œ„ํ•œ ์ด๋ฉ”์ผ',
required: true,
})
@IsString()
@IsNotEmpty()
email: string;

@ApiProperty({
example: '345123',
description: '์ธ์ฆ์ฝ”๋“œ',
required: true,
})
@IsString()
verificationCode: string;
}
2 changes: 1 addition & 1 deletion backend/src/dto/login.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IsEmail, IsString } from 'class-validator';

export class LoginDto {
@ApiProperty({
example: 'bichoi0715@naver.com',
example: 'example123@naver.com',
description: '๋กœ๊ทธ์ธ ์ด๋ฉ”์ผ',
required: true,
})
Expand Down
2 changes: 1 addition & 1 deletion backend/src/dto/user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IsEmail, IsString } from 'class-validator';

export class UserDto {
@ApiProperty({
example: 'bichoi0715@naver.com',
example: 'example123@naver.com',
description: '์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ',
required: true,
})
Expand Down
13 changes: 13 additions & 0 deletions backend/src/dto/user.email.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class UserEmailDto {
@ApiProperty({
example: '[email protected]',
description: '์ธ์ฆ์„ ์œ„ํ•œ ์ด๋ฉ”์ผ',
required: true,
})
@IsString()
@IsNotEmpty()
email: string;
}
13 changes: 13 additions & 0 deletions backend/src/dto/user.password.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class UserPasswordDto {
@ApiProperty({
example: '1q2w3e4r',
description: '์‚ฌ์šฉ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ',
required: true,
})
@IsString()
@IsNotEmpty()
password: string;
}
36 changes: 36 additions & 0 deletions backend/src/dto/user.register.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsString } from 'class-validator';

export class UserRegisterDto {
@ApiProperty({
example: '[email protected]',
description: '์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ',
required: true,
})
@IsEmail()
email: string;

@ApiProperty({
example: '์ตœ๋ณ‘์ต',
description: '์‚ฌ์šฉ์ž ์ด๋ฆ„',
required: true,
})
@IsString()
userName: string;

@ApiProperty({
example: '345123',
description: '์ธ์ฆ์ฝ”๋“œ',
required: true,
})
@IsString()
verificationCode: string;

@ApiProperty({
example: '1q2w3e4r',
description: '์‚ฌ์šฉ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ',
required: true,
})
@IsString()
password: string;
}
Loading

0 comments on commit 17c0e5a

Please sign in to comment.