728x90
반응형

이 포스트는 전 포스트에서 이어집니다.

 

 

 

https://docs.nestjs.kr/middleware

 

네스트JS 한국어 매뉴얼 사이트

네스트JS 한국, 네스트JS Korea 한국어 매뉴얼

docs.nestjs.kr


 

Nest 미들웨어는 기본적으로 express 미들웨어와 동일하다. 

 

  • 모든 코드를 실행하십시오.
  • 요청 및 응답 객체를 변경합니다.
  • 요청-응답주기를 종료합니다.
  • 스택의 next 미들웨어 함수를 호출합니다.
  • 현재 미들웨어 함수가 요청-응답주기를 종료하지 않으면 next()를 호출하여 next 미들웨어 기능에 제어를 전달합니다. 그렇지 않으면 요청이 중단됩니다.

 


Request가 들어올 때 로그로 확인하기 위한 미들웨어를 작성해보자.

 

/src/common/middleware/logger.middleware.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}

 

 

app.module에서 전 포스트에서 작성한 UserModule을 import하고 다음과 같이 수정하자.

app.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { LoggerMiddleware } from './common/middleware/logger.middleware';

@Module({
  imports: [UserModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('user');
  }
}

 

이제 Swagger에서 요청을 보내보자.

 

 

콘솔을 확인해보면 미들웨어에서 작성한 내용이 잘 뜨는 것을 확인할 수 있다.

 

미들웨어에서 한 번 들어온 데이터를 검증하고 next()로 넘길 수 있다. 

( 토큰이나 쿠키를 확인하는 것 등)

 

 

logger.middleware.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(req.body)
    next();
  }
}

 

swagger로 요청을 보내보면

 

 

잘 확인되는 것을 볼 수 있다.

 

참고로 이렇게 예외 상황도 만들 수 있다.

consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'cats', method: RequestMethod.GET },
    { path: 'cats', method: RequestMethod.POST },
    'cats/(.*)',
  )
  .forRoutes(CatsController);
728x90
반응형

'Back-End > Nest.js' 카테고리의 다른 글

Nest.js | Swagger  (0) 2021.10.28
Nest.js | MongoDB | PATCH and DELETE  (0) 2021.10.06
Nest.js | MongoDB | Create  (0) 2021.10.06
Nest.js | MongoDB | Find  (0) 2021.10.06
Nest.js | MongoDB | Model,Schema,Controller  (0) 2021.10.06
728x90
반응형

swagger에 관한 자세한 사항은 아래 링크

https://docs.nestjs.com/openapi/introduction#bootstrap

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com


 

NestJS에서 swagger를 사용하면 아래와 같이 API를 테스트 할 수 있는 GUI 환경을 제공해준다.

 

 

 

일단 NestJS와 Swagger를 설치한다.

$ npm i -g @nestjs/cli
$ nest new teepo
$ npm install --save @nestjs/swagger swagger-ui-express

 

그다음 main.ts에 swagger를 추가해준다.

main.ts

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('User example')
    .setDescription('The user API description')
    .setVersion('1.0')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('swagger', app, document);

  await app.listen(3000);
}
bootstrap();

SwaggerModule.setup() 메소드 안에서 Swagger UI를 마운트하는 경로를 설정할 수 있다.

 

 

이제 npm run start:dev를 하고 화면(:3000/swagger)을 확인해보자.

$ npm run start:dev

 

 

정상적으로 출력되었다. 이제 api에 문서화를 진행해보자.

 

임의로 user라는 resource를 만들어보겠다.

$ nest g res user

 

 

간단하게 dto 파일을 작성한다.

create-user.dto.ts

export class CreateUserDto {
    name : string;
    age : number;
}

 

 

userController 파일을 다음과 같이 수정해보자.

user.controller.ts

import { Controller, Response, Get, Post, Body, Patch, Param, Delete, Res, HttpStatus } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiCreatedResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { User } from './entities/user.entity';

@Controller('user')
@ApiTags('유저 API')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  @ApiOperation({summary : '유저 생성 API', description: '유저를 생성한다.' })
  @ApiCreatedResponse({description: '유저를 생성한다', type: User})
  async create(@Body() createUserDto: CreateUserDto) {
    const user: User = await this.userService.createUser(createUserDto);
    return user;
  }
}

 

userController에서 쓰일 userService를 정의한다.

user.service.ts

import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UserService {
  createUser(createUserDto: CreateUserDto) {
    return {
      name : 'teepo',
      age : 28
    };
  }

}

 

 

이제 실행 화면을 보면

 

 

이렇게 우리가 추가한 '유저 API'가 잘 생성된 것을 볼 수 있다. 오른쪽 Try it out 버튼을 클릭하면 테스트 화면이 나온다.

일단 아무것도 입력하지 않고 바로 보내보자.

 

 

결과화면 : 

 

 

Service에 작성했던 정보가 잘 넘어온 것을 확인할 수 있다. 

 

dto와 클래스 또한 Swagger를 사용해 정의할 수 있다. 타입도 지정해주고 확인해주자.

class-validator와 class-transformer를 설치한다.

$ npm i class-validator class-transformer

 

 

dto 파일에 타입과 swagger 데코레이터를 추가한다.

create-user.dto

import {IsString, IsNumber} from 'class-validator'
import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
    @ApiProperty({ description: '이름' })
    @IsString()
    name : string;

    @ApiProperty({ description: '나이' })
    @IsNumber()
    age : number;
}

 

 

main.ts에 파이프를 설정해준다.

main.ts

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('User example')
    .setDescription('The user API description')
    .setVersion('1.0')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('swagger', app, document);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist : true, 
      forbidNonWhitelisted : true,
      transform : true
    })
  )

  await app.listen(3000);
}
bootstrap();

 

Service부분에 내가 입력한 값 그대로를 반환하게끔 수정해준다.

user.service.ts

import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UserService {
  createUser(createUserDto: CreateUserDto) {
    return createUserDto
  }

}

 

 

이제 다시 화면을 보면 DTO에 설정해두었던 타입들이 잘 들어가는 것을 확인할 수 있다.

 

 

 

맨 아래쪽에 스키마 부분도 설명이 잘 되어있다.

 

 

 

이번엔 타입과 다른 값을 보내보자. name에 string대신 number를 보내줄 것이다.

 

 

 

결과를 확인해보면

 

 

"name must be a string" 이라는 문구를 확인할 수 있다.

728x90
반응형

'Back-End > Nest.js' 카테고리의 다른 글

Nest.js | Middleware  (0) 2021.10.28
Nest.js | MongoDB | PATCH and DELETE  (0) 2021.10.06
Nest.js | MongoDB | Create  (0) 2021.10.06
Nest.js | MongoDB | Find  (0) 2021.10.06
Nest.js | MongoDB | Model,Schema,Controller  (0) 2021.10.06
728x90
반응형

윈도우에서 도커에 우분투를 설치했을 때 sudo 명령어가 안먹히면 아래 방법을 써보자.

 

1. 도커 컨테이너 ID 확인

# docker ps -a

 

2. 확인한 ID로 컨테이너 접속

# docker exec -u root -t -i 컨테이너ID /bin/bash

 

3. 아래 명령어 입력

apt-get update && \
      apt-get -y install sudo

 

728x90
반응형
728x90
반응형

전 포스트에서 올바른 ID, PW가 전달됐을 경우 토큰을 생성하고 반환하는 것까지 해보았다.

 

이번 포스트에서는 쿠키에있는 JWT를 복호화하는 과정을 구현해볼 것이다.

 

NextJS JWT

먼저 쿠키를 읽어오기 위해 next-cookies를 설치한다. (NextJS)

$ npm i next-cookies

 

복호화 하기위해 아래도 설치한다.

$ npm i jwt-decode
$ npm i @types/jwt-decode --save-dev

 

dashboard/index 에 import 해주고 getServerSideProps 쪽에 확인하는 코드를 작성해준다.

src/dashboard/index.tsx

import axios from 'axios'
import type { GetServerSideProps, NextPage } from 'next'
import Link from 'next/link'
import cookies from 'next-cookies'
import jwtDecode, { JwtPayload } from 'jwt-decode'


interface Props {
    data : any
}


const Dashboard:NextPage<Props> = ({data}) => {
    return (
        <div>
            <div className='dashboard'>
                Dashboard
            </div>
        </div>
        )
}


export default Dashboard;

export const getServerSideProps:GetServerSideProps = async (context) => { // SSR
    const token = cookies(context).token;
    const decodedToken: JwtPayload = jwtDecode<JwtPayload>(token ? token : '');
    console.log(decodedToken)
    
    return {
        props: {
        }
    }
}

 

 

로그인에 성공해서 쿠키에 담긴 토큰을 decode해서 콘솔에 찍어 확인해보자.

 

 

로그인 성공 :

 

NextJS 서버 콘솔 ( 서버사이드라 브라우저에서 뜨지 않습니다. )

 

추가로 시간이 지나거나 토큰이 읽히지 않는경우 다시 첫 페이지로 가게끔 코딩한다.

src/dashboard/index.tsx

export const getServerSideProps:GetServerSideProps = async (context) => { // SSR
    try {
        const token = cookies(context).token;
        const decodedToken: JwtPayload = jwtDecode<JwtPayload>(token ? token : '');
        
        if(decodedToken) {
            return {
                props: {
                }
            }
        }
        else {
            return {
                redirect: {
                destination: '/',
                permanent: false,
                },
            }
        }
    }
    catch(e) {
        console.log(e)
        return {
            redirect: {
            destination: '/',
            permanent: false,
            },
        }
    }
}
728x90
반응형
728x90
반응형

로그인 시 인증관련을 위해 Passport와 JWT에 대해 다루어보겠다.


passport에 관한 내용

https://velog.io/@kdo0129/Passport%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

 

Passport로 로그인 구현하기

passport는 Node 용 인증 미들웨어다. 요청 인증이라는 단일 목적을 제공하기 위해 설계되었다.이번 surf 프로젝트에서는 다양한 인증 방식을 기획했다. (통합해서 로그인이라고 부르겠다.)종류를 살

velog.io

Passport는 커뮤니티에 잘 알려져 있고 성공적으로 많은 app 제품에 사용된 nodeJS의 가장 인기있는 인증관련 라이브러리이다. passport는 @nestjs/passport 모듈에 통합되었습니다.

JWT(Json Web Token)에 관한 내용은 아래 링크를 확인하자. 구글링해서도 금방 찾을 수 있다.

http://www.opennaru.com/opennaru-blog/jwt-json-web-token/

 

JWT (JSON Web Token) 이해하기와 활용 방안 - Opennaru, Inc.

JWT 는 JSON Web Token의 약자로 전자 서명 된 URL-safe (URL로 이용할 수있는 문자 만 구성된)의 JSON입니다.JWT는 속성 정보 (Claim)를 JSON 데이터 구조로 표현한 토큰으로 RFC7519 표준 입니다.

www.opennaru.com


Nest Authentication 공식문서

https://docs.nestjs.com/security/authentication

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com


Local Strategy Passport

 

이제 우린 NestJS에서 지원해주는 Passport, JWT 모듈을 사용할 것이기 때문에
이전에 작성해두었던 user.controller.ts의 login과 user.service.ts의 login 함수를 삭제해도 된다. 

 

 

필요한 패키지를 설치한다.

$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local

$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt

 

1. 인증관련 모듈을 만들기 위해 auth 모듈, 서비스를 생성한다.

$ nest g module auth
$ nest g service auth

 

 

2. auth.service.ts를 다음과 같이 수정한다.

/auth/auth.service.ts

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { CreateUserDto } from 'src/user/dto/create-user.dto';
import { User, UserDocument } from 'src/user/schemas/user.schema';
//bcrypt
import * as bcrypt from 'bcrypt'

@Injectable()
export class AuthService {
    constructor(@InjectModel(User.name) private userModel: Model<UserDocument>,
    private jwtService: JwtService) {}

  async validateUser(username: string, password : string): Promise<any> {
    const user = await this.userModel.findOne({id : username}); // ID가 존재할 경우
    if (user) {
      const result = await bcrypt.compare(password,user.pw); 
      if(result) { // ID와 비밀번호가 일치할 경우
        return {result : "success",user : user};
      }
      else { // ID는 맞지만 비밀번호가 틀릴 경우
        return {result : "Pwfailed"}; 
      }
    }
    else { // ID 자체가 없는 경우
      return {result : "Idfailed"};
    }
  }

  async login(user : any, res : any) {
    const payload = { id: user.id };
    const token = await this.jwtService.sign(payload);
    res.cookie('token', token, {path: '/', expires: new Date(Date.now()+10000)}); // 쿠키생성
    return {
      result : "success",
      access_token: token
    };
  }
}

- 나중에 local.strategy.ts 파일에서 쓰일 validateUser 함수를 구현했다.

- 처음 api를 타기 전 클라이언트에서 보낸 usernamepasswordDB에 존재하는지 확인한다.

- 로그인 할 때 쓰일 login 함수를 선언했다. (user.controller에서 쓰임)

- 성공 시 쿠키를 생성하고 토큰과 결과값을 반환한다.

 

 

3. authModule에 User 모델을 가져온다.

/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { MongooseModule } from '@nestjs/mongoose';
import { PassportModule } from '@nestjs/passport';
import { User, UserSchema } from 'src/user/schemas/user.schema';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { jwtConstants } from './constants';

@Module({
  imports : [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
  providers: [AuthService],
})
export class AuthModule {}

 

 

4. local.strategy.ts 파일과 jwt.strategy.ts 파일을 작성한다.

/auth/local.strategy.ts

import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({
      username : "",
      password : ""
    });
  }

  async validate(username: string, password: string): Promise<any> {
    const result = await this.authService.validateUser(username,password); // ID,PW 검증
    // if (result.result !== "success") { // 검증 안될 경우 에러메세지 띄어줌
    //   throw new UnauthorizedException();
    // }
    return result;
  }
}

- 들어올 데이터인 username, password를 정의했다 ( 나중에 속성을 바꾸고 싶으면 super() 부분을 수정한다. )

- 그대로 authServicevalidateUser 함수로 넘긴다.

 

 

/auth/jwt.strategy.ts

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: any) {
    return {  username: payload.username };
  }
}

- 차후에 validate 부분을 바꿀 것이다. 

 

 

5. jwt 비밀키로 쓰일 contants파일을 생성하고 authModule를 수정해준다.

/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { MongooseModule } from '@nestjs/mongoose';
import { PassportModule } from '@nestjs/passport';
import { User, UserSchema } from 'src/user/schemas/user.schema';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { jwtConstants } from './constants';

@Module({
  imports : [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
  PassportModule,
  JwtModule.register({
    secret: jwtConstants.secret,
    signOptions: { expiresIn: '60s' },
  }),
],
  providers: [AuthService,LocalStrategy,JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

- PassportModuleJwt 모듈을 가져왔고, 비밀키와 시간을 설정해주었다.

- 작성한 Strategy파일들을 providers에 담아줬다.

- 나중에 user Controller에서 Service를 쓰기위해 export 해주었다.

 

 

6. local-auth.guard.tsjwt-auth.guard.ts 파일들을 작성한다. (user controller 에서 쓰인다.)

/auth/local-auth.guard.ts

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

 

/auth/jwt-auth.guard.ts

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

 

 

7. userModule에서 AuthModuleimport 한다.

user.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { User, UserSchema } from './schemas/user.schema';
import { PassportModule } from '@nestjs/passport';
import { AuthModule } from 'src/auth/auth.module';


@Module({
  imports: [
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
    AuthModule
],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService]
})
export class UserModule {}

 

 

8. userController에서 api를 다시 지정해준다. 하는김에 토큰 확인하는 profile도 만들었다.

user.controller.ts

import { Controller, Request, Response, Get, Post, Body, Patch, Param, Delete, Res, Req, UseGuards } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

//auth
import { LocalAuthGuard } from '../auth/local-auth.guard';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { AuthService } from '../auth/auth.service';

@Controller('user')
export class UserController {
  constructor(
    private readonly userService: UserService,
    private authService: AuthService
    ) {}

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.userService.findOne(id);
  }

  @Post('signup')
  async signUp(@Res({ passthrough: true}) res : any, @Req() req : any) {
    return this.userService.signUp(req.body.userInfo);
  }

  @UseGuards(LocalAuthGuard)
  @Post('login')
  async login(@Request() req, @Res({ passthrough: true }) res : any) {
    if(req.user.result === "success") { // ID,PW 둘다 확인 됐을 경우
      return this.authService.login(req.user.user, res);
    }
    else {
      return {result : req.user.result}; // 둘 중 하나 이상이 틀릴 경우
    }
  }

  @UseGuards(JwtAuthGuard)
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }
}

 

  1. api 요청이 들어온다
  2. UseGuards('local')에서 유저의 정보가 잘 들어왔는지 local-strategy 파일의 validate까지 확인한다.
  3. authServicevalidateUser에서 DB를 조회하고, 상황에 맞는 값을 반환한다.
  4. local-strategy 파일에서 다시 Controller로 값을 req.user에 담아서 반환한다.
  5. ID, PW 중 한 개 이상이 틀릴 경우 validateUser 함수에서 반환했던 실패 반환값을 그대로 반환한다.
  6. 성공했을 경우 authServicelogin함수로 들어가 쿠키와 토큰을 생성하고, 토큰을 반환한다.

 

이제 원래 작성했었던 LoginComponent와 user_action 을 수정하자.

 

LoginComponent.tsx

    const LoginHandler = (event : any) => {
        event.preventDefault();
        dispatch(userLogin(userState.userInfo.id,userState.userInfo.pw)).then((req : any) => {
            if(req.payload.result === "success") { // 로그인 성공 시 
                alert('로그인 성공입니다.');
                router.push(`/dashboard`)
            }
            else if(req.payload.result === "IdFailed") { // 아이디가 없을 경우
                alert('존재하지 않는 아이디입니다.');
            }else if(req.payload.result === "PwFailed"){ // 비밀번호가 틀릴경우
                alert('비밀번호가 틀렸습니다.');
            }else { // 그 외의 경우
                alert('로그인 실패입니다.');
            }
        })
    }

dispatch의 userLogin 함수에 전달값을 수정했다.

 

 

user_action.ts

export async function userLogin (id : string, pw : string) {
    const request = Axios.post('/api/user/login',{"username": id, "password": pw})
    .then(response => response.data)

    return {
        type : actionTypesUser.USER_LOGIN,
        payload: request
    }
}

전달받는 값과 보내는 값을 백엔드에 보내는 요청에 맞게 수정했다.

 

이제 화면에서 다시 로그인을 해보자. ( 헤더 부분은 신경 안쓰셔도 됩니다. )

 

 

로그인이 성공했을 때 토큰이 쿠키에 담긴 것을 확인할 수 있다.

 

 

 

728x90
반응형
728x90
반응형

회원가입 완료 후 회원가입된 아이디 비밀번호로 로그인이 성공하면 대쉬보드 페이지로 넘어가는 서비스를 구현해보자.

 

먼저 SignupComponent와 LoginComponent에 useEffect로 해당 컴포넌트에 처음 들어갈 때 userState를 초기화하는 구문을 넣어준다. 이 구문을 넣지 않으면 회원가입에서 진행했던 내용들이 그대로 로그인 화면에도 쓰이게 된다.

 

    useEffect(()=> {
        dispatch({type: actionTypesUser.USER_INIT})
    },[])

 

[cate].tsx에서 SignupComponent에 전달했던 값과 똑같은 값을 LoginComponent에 넘겨준다.

 

[cate].tsx

            <LoginComponent
                userChangeHandler={userChangeHandler} // 로그인에 props로 Handler함수를 보낸다.
                userState={userState} /> : ""}

 

로그인 화면을 다음과같이 구성해준다.

 

LoginComponent.tsx

import type { NextPage } from 'next'
import { actionTypesUser, UserState } from "../../store/interfaces/";
import { useEffect } from 'react'
import { useDispatch } from 'react-redux';
import Link from 'next/link'

interface Props {
    userChangeHandler : (event : any) => void;
    userState : UserState;
}

const Login:NextPage<Props> = ({userChangeHandler,userState}) => {
    const dispatch = useDispatch();

    useEffect(()=> {
        dispatch({type: actionTypesUser.USER_INIT});
    },[])

    const LoginHandler = (event : any) => {
        event.preventDefault();
    }
    
    return (
        <div className='login'>
            <form onSubmit={LoginHandler}>
                <div>
                    <input 
                        name="id"
                        type="text"
                        placeholder="Enter ID"
                        onChange={userChangeHandler} />
                </div>
                <div>
                    <input 
                        name="pw"
                        type="password"
                        placeholder="Enter PW"
                        onChange={userChangeHandler} />
                </div>
                <div>
                    <button
                        type="submit">
                    Login
                    </button>
                </div>
                <Link href={`/auth/signup`}>
                    <a> 회원가입 화면으로 가기 </a>
                </Link>
            </form>
        </div>
        )
}


export default Login

아이디 입력창과 비밀번호 입력창, 로그인 버튼과 Link를 사용해 화면을 구성했다. 회원가입에서 이미 다뤘던 내용들이라 자세한 설명은 하지 않겠다. 

 


next/link에 관한 자세한 내용은 다음 링크에서 확인할 수 있다.

https://nextjs.org/docs/api-reference/next/link

 

next/link | Next.js

Enable client-side transitions between routes with the built-in Link component.

nextjs.org


화면을 확인해보면 

 

 

이제 로그인 버튼을 눌렀을 경우 DB를 조회하고 해당 아이디 비밀번호가 올바르면 로그인 성공 메세지가 뜨게 할 것이다.

 

먼저 로그인에 필요한 액트, 타입, 리듀서, 액션을 만들어주자.

 

userAct.interfaces.ts

(...)

USER_LOGIN = "USER_LOGIN", // 로그인

(...)
    
export type ActionsUser = UserInit | UserState | UserInfo | UserIdDuplicate
                         | UserEmailDuplicate | UserEmailSend | UserEmailCert
                         | UserSignup | UserLogin
                         
(...)
                         
export interface UserLogin {
    type : actionTypesUser.USER_LOGIN;
    payload : any;
}

 

user_reducer.ts

        case actionTypesUser.USER_LOGIN:
                return {
                    ...state, data : action.payload
                }

 

user_action.ts

export async function userLogin (userInfo : UserInfo) {
    const request = Axios.post('/api/user/login',{userInfo})
    .then(response => response.data)

    return {
        type : actionTypesUser.USER_LOGIN,
        payload: request
    }
}

 

 

그다음 LoginHandler 함수를 다음과 같이 조건식을 세워서 만든다.

 

LoginComponent.tsx

    const LoginHandler = (event : any) => {
        event.preventdefault();
        dispatch(userLogin(userState.userInfo)).then((req : any) => {
            if(req.payload.result === "success") { // 로그인 성공 시 
                alert('로그인 성공입니다.');
            }
            else if(req.payload.result === "IdFailed") { // 아이디가 없을 경우
                alert('존재하지 않는 아이디입니다.');
            }else if(req.payload.result === "PwFailed"){ // 비밀번호가 틀릴경우
                alert('비밀번호가 틀렸습니다.');
            }else { // 그 외의 경우
                alert('로그인 실패입니다.');
            }
        })
    }

 

이제 프론트에서 할 일은 끝났다. 백엔드로 넘어가보자. 먼저 컨트롤러 부분에 login Post데코레이터를 만들어준다.

 

user.controller.ts

  @Post('login')
  async logIn(@Res({ passthrough: true}) res : any, @Req() req : any) {
    return await this.userService.logIn(req.body.userInfo,res);
  }

 

user.service.ts

  async logIn(userInfo : CreateUserDto, res : any) {
    try {
      const userOne = await this.userModel.findOne({id : userInfo.id});
      if(userOne) { // ID가 존재할 경우
        const result = await bcrypt.compare(userInfo.pw,userOne.pw); 
        if(result) { // 비밀번호가 일치할 경우
          res.cookie('isLogined',userInfo.id, {path: '/', expires: new Date(Date.now()+86400000)});
          return { result : "success"};
        }
        else { // 비밀번호가 틀릴 경우
          return { result : "PwFailed"};
        }
      }else { // ID가 없을 경우
        return { result : 'IdFailed'};
      }
    }catch(e) {
      return false;
    }
  }

 

이제 화면에서 로그인이 성공했을 때 쿠키가 제대로 생기는지 확인해보자.

 

- 존재하지 않는 ID일 경우

 

- 비밀번호가 틀릴 경우

 

- 로그인이 성공할 경우

 

쿠키까지 확인 : 

 

로그인 성공 시 쿠키가 잘 생성되는것을 확인했다.

 

마지막으로 로그인이 완료되면 넘어갈 페이지인 대시보드 페이지를 만들어보자.

 

/pages/dashboard.tsx

import type { NextPage } from 'next'
import Link from 'next/link'


const Dashboard:NextPage = () => {
    return (
        <div className='dashboard'>
            Dashboard
        </div>
        )
}


export default Dashboard

 

LoginComponent.tsx

    const LoginHandler = (event : any) => {
        event.preventDefault();
        dispatch(userLogin(userState.userInfo)).then((req : any) => {
            if(req.payload.result === "success") { // 로그인 성공 시 
                alert('로그인 성공입니다.');
                router.push(`/dashboard`)
            }
            else if(req.payload.result === "IdFailed") { // 아이디가 없을 경우
                alert('존재하지 않는 아이디입니다.');
            }else if(req.payload.result === "PwFailed"){ // 비밀번호가 틀릴경우
                alert('비밀번호가 틀렸습니다.');
            }else { // 그 외의 경우
                alert('로그인 실패입니다.');
            }
        })
    }

 

router.push를 이용해 성공시 대쉬보드 페이지로 넘어가게끔 했다.

728x90
반응형
728x90
반응형

지금까지 구현했던 회원가입창에 모든 데이터를 넣고 회원가입은 완료시켜보자.

 

회원가입을 눌렀을 경우 실행되는 함수는 submitHandler이다. 다음과 같이 수정해주자.

    const submitHandler = (event : any) => {
        event.preventDefault();
        if(!userState.idDuple) {
            alert('아이디 중복여부를 체크해주세요.');
        }else {
            if(!userState.emailDuple) {
                alert('이메일 중복여부를 확인해주세요.');
            }else {
                if(!userState.emailAuth) {
                    alert('이메일 인증을 확인해주세요.');
                }else {
                    if(userState.userInfo.pw.length < 4) {
                        alert('비밀번호를 4자이상 입력해주세요.')
                    } else {
                        if(userState.userInfo.pw !== pwdCertText) {
                            alert('비밀번호와 비밀번호 확인 문자가 다릅니다.')
                        }
                        else {
                            // signup dispatch
                        }
                    }
                }
            }
        }
    }

 

지금까지 실행했던 것들로 인증을 하고 만약 다를경우 alert창을 띄워줄 것이다

 

이제부터 액션을 만들고 주석처리된 부분에 dispatch를 실행할 것이다. 액트와 타입을 지정해준다.

 

userAct.interfaces.ts

(...)

USER_SIGNUP = "USER_SIGNUP", // 회원가입

(...)

export type ActionsUser = UserInit | UserState | UserInfo | UserIdDuplicate
                         | UserEmailDuplicate | UserEmailSend | UserEmailCert
                         | UserSignup

(...)

export interface UserSignup {
    type : actionTypesUser.USER_SIGNUP;
    payload : any;
}

 

이번엔 리듀서 부분을 수정해준다.

 

user_reducer.ts

        case actionTypesUser.USER_SIGNUP:
                return {
                    ...state, data : action.payload
                }

 

 

클라이언트에서 사용할 dispatch 액션을 정의해준다. post메소드로 데이터를 전송할 것이다.

interfaces에서 userInfo 클래스를 가져오고 다음 함수를 만들어주자.

 

user_action.ts

export async function userSignup (userInfo : UserInfo) {
    const request = Axios.post('/api/user/signup',{userInfo})
    .then(response => response.data)

    return {
        type : actionTypesUser.USER_SIGNUP,
        payload: request
    }
}

 

다시 SignupComponent로 넘어와서 NextJS의 라우터를 사용해 회원가입 성공 시 로그인 화면으로 넘어가게끔 하자.

 

SignupComponent.tsx

import { useRouter } from 'next/router'

(...)

const Signup:NextPage<Props> = ({userChangeHandler,userState}) => {

(...)

    // router
    const router = useRouter()
    
(...)

    const submitHandler = (event : any) => {
        event.preventDefault();
        if(!userState.idDuple) {
            alert('아이디 중복여부를 체크해주세요.');
        }else {
            if(!userState.emailDuple) {
                alert('이메일 중복여부를 확인해주세요.');
            }else {
                if(!userState.emailAuth) {
                    alert('이메일 인증을 확인해주세요.');
                }else {
                    if(userState.userInfo.pw.length < 4) {
                        alert('비밀번호를 4자이상 입력해주세요.');
                    } else {
                        if(userState.userInfo.pw !== pwdCertText) {
                            alert('비밀번호와 비밀번호 확인 문자가 다릅니다.');
                        }
                        else {
                            // add address detail
                            dispatch({type : actionTypesUser.USER_INFO, data: ['address',userState.userInfo.address + addressDetail]})
                            // signup dispatch
                            dispatch(userSignup(userState.userInfo)).then((req: any) => {
                                if(req.payload.result) {
                                    alert('회원가입 성공입니다.')
                                    router.push('/auth/login');
                                }
                                else {
                                    alert('회원가입 오류입니다.');
                                }
                            })
                        }
                    }
                }
            }
        }
    }

 

이제 프론트에서 할 일은 끝났다. 백엔드에서 값이 잘 들어왔는지 확인하고 올바른 결과값을 반환해주자.

 

먼저 필요한 것들을 설치하고 dto를 설정해준다.

$ npm i class-validator class-transformer

 

create-user.dto.ts

import {IsString, IsNumber} from 'class-validator'

export class CreateUserDto {
    @IsString()
    id : string;

    @IsString()
    pw : string;

    @IsString()
    email : string;

    @IsString()
    address : string;
}

 

main.ts 파일에서 ValidationPipe를 사용한다.

maints

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(cookieParser());

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist : true, 
      forbidNonWhitelisted : true,
      transform : true
    })
  )

  await app.listen(3001);
}
bootstrap();

 

 

 

컨트롤러에 signup 함수를 지정해준다.

 

user.controller.ts

import { Controller, Get, Post, Body, Patch, Param, Delete, Res, Req } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.userService.findOne(id);
  }

  @Post('signup')
  async signUp(@Res({ passthrough: true}) res : any, @Req() req : any) {
    return this.userService.signUp(req.body.userInfo);
  }

}

 

서비스 부분에서 비밀번호를 암호화하고 DB에 저장하는 함수를 선언한다.

 

user.service.ts

import { Injectable } from '@nestjs/common';
//mongoose
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from './schemas/user.schema';
//dto
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
//bcrypt
import * as bcrypt from 'bcrypt'

@Injectable()
export class UserService {
  constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}

  async findOne(id: string) {
    const userOne = await this.userModel.findOne({id});
    if(userOne)
      return {result : false, user : userOne};
    else
      return {result : true};
  }

  async signUp(userInfo : CreateUserDto) {
    try{
      const authPW = await bcrypt.hash(userInfo.pw,parseInt(process.env.saltOrRounds));
      const createUser = await this.userModel.create({
        id : userInfo.id,
        pw : authPW,
        email : userInfo.email,
        address : userInfo.address
      })
      return {result : true};
    }catch(e) {
      return {result : false};
    }
  }
}

 

 

이제 실행을 해본다!

 

 

회원가입 버튼 클릭 후 :

 

alert 창의 확인버튼 누른 후 : 

 

정상적으로 로그인 페이지로 넘어오는것을 확인할 수 있다.

참고로 robo 3t라는 프로그램으로 들어온 mongoDB 데이터를 확인할 수 있다.

 

728x90
반응형
728x90
반응형

 

회원가입에 필요한 주소검색을 구현해보자. 먼저 다음 명령어를 실행한다.

$ npm i react-daum-postcode

 

그다음 상단에서 import 하고 

import DaumPostcode from 'react-daum-postcode';

 

다음과 같이 컴포넌트를 사용할 것이다.

<DaumPostcode onComplete={AddressComplete} />

 

 

일단 필요한 State 먼저 선언한다.

    //address
    const [addressOpen,setAddressOpen] = useState<boolean>(false); // 주소검색 오픈
    const [addressDetail,setAddressDetail] = useState<string>(''); // 자세한 주소 입력칸

 

그리고 HTML부분을 다음과같이 바꾸고 화면을 보자.

<div>
    <input 
        name="address"
        type="text"
        placeholder="Enter Address"
        value={userState.userInfo.address}
        disabled />
    <button
    	type="button"
        onClick={()=> { setAddressOpen(true) }}>
    주소검색    
    </button>
</div>
{ addressOpen ?
<DaumPostcode onComplete={AddressComplete} />
: ""}
<div>
    <input 
            name="address"
            type="text"
            placeholder="Enter Address2"
            value={addressDetail}
            onChange={(event)=> {
                setAddressDetail(event.target.value)
            }} />
</div>

 

그다음 주소검색 버튼을 누르고 원하는 주소를 클릭하면 실행되는 AddressComplete 함수를 구현한다.

const AddressComplete = (data : any) => {
        let fullAddress = data.address;
        let extraAddress = '';
        if (data.addressType === 'R') {
            if (data.bname !== '') {
                extraAddress += data.bname;
            }
            if (data.buildingName !== '') {
                extraAddress += (extraAddress !== '' ? `, ${data.buildingName}` : data.buildingName);
            }
            fullAddress += (extraAddress !== '' ? ` (${extraAddress})` : '');
        }
        //fullAddress -> 전체 주소반환
        dispatch({type : actionTypesUser.USER_INFO, data : ['address',fullAddress]})
        setAddressOpen(false);
    }

 

  • 기본적으로 검색한 주소가 들어가는 칸과 주소검색 버튼, 상세 주소 입력 칸이 있다.
  • 검색한 주소는 검색한 내용이 들어가야되므로 disabled 처리한다. value는 userInfo의 address 이다.
  • 주소검색 버튼을 누르면 하단에 주소검색칸이 생긴다.
  • 원하는 주소를 클릭하면 setAddressOpen 함수가 실행된다.
  • 상세주소까지 입력하면 끝!

 

화면을 확인해보자

 

1. 첫 화면

 

 

2. 주소검색을 클릭할 경우

 

 

3. 원하는 주소 클릭 후 상세주소까지 입력

 

 

현재 주소검색 결과값과 상세주소가 합쳐져있지 않기 때문에 회원가입 버튼을 눌렀을 경우에

합쳐줄 것이다.

728x90
반응형
728x90
반응형

react-useuseInterval 을 사용하여 남은 시간을 구현하겠다.

 

$ npm i react-use

 

SignupComponent.tsx 파일 상단에 import를 추가한다.

import { useInterval } from 'react-use';

 

useInterval을 사용한다.

    useInterval(()=> {
        setEmailCount(emailCount -1);
    },1000)

 

 

emailSend 부분에 emailCount에 숫자를 채워준다.

추가로 email중복확인이 완료되었으므로 userState도 업데이트해준다.

    const emailSend = () => {
        if(userState.userInfo.email !=='' && userState.userInfo.email.indexOf('@') !== -1) { // 입력된 값이 존재하고 email 형식에 맞을경우
            dispatch(userEmailDuplicate(userState.userInfo.email)).then((req : any) => {
                if(!req.payload.result) { // 이메일이 이미 존재할 때
                    alert('해당 이메일이 이미 존재합니다.');
                }else {
                        dispatch(userEmailSend(userState.userInfo.email)).then((req : any) => {
                            if(req.payload.result) { // 이메일 보내기 성공했을 때
                                setEmailSendOK(true);
                                setEmailCount(300);
                                dispatch({type : actionTypesUser.USER_STATE, data: ['emailDuple',true]});
                            }else {
                                alert('이메일 전송이 실패됐습니다.')
                            }
                        });
                }
            })
        }
    }

 

전송을 하고 웹을 확인해보면

 

이렇게 잘 뜨는것을 확인할 수 있다.

 

useEffect를 이용해서 만약 남은 시간이 0이 되면 emailSendOK state가 false가 되면서 재전송버튼은 다시 보내기버튼이 되고 남은시간과 인증번호 입력칸, 확인버튼이 사라지게 한다.

    useEffect(()=> {
        if(emailCount === 0)
            setEmailSendOK(false)
    },[emailCount])

 

이제 성공적으로 보냈고, 남은시간과 보여줘야 할 요소들의 정의가 끝이 났으니 인증번호를 확인하는 작업을 해보자.

 

  1. 이메일로 확인한 인증번호를 입력한다.
  2. 확인 버튼을 누른다. 백엔드에 쿠키에 저장된 authNum값과 입력한 enterNum 두 값이 전송된다.
  3. 백엔드에서 쿠키를 확인하고 두 값이 일치하면 result true를 반환한다.
  4. 만약 이메일 인증이 성공하면 userState.emailAuth 값이 true가 되고 남은시간과 인증번호 입력창, 확인 버튼이 사라질 것이며 보내기 버튼이 비활성화된다.
  5. 만약 인증에 실패하면 alert창을 띄어준다.

 

먼저 인증번호 입력칸 state를 작성해준다.

const [emailAuthText,setEmailAuthText] = useState<string>(''); // email auth text

 

이제 확인버튼을 누르면 입력한 인증번호를 bcrypt한 값이랑 쿠키에 담겨있던 암호화된 인증번호를 비교해서

두개가 일치하면 userState.emailAuth 값을 true로 바꿀 것이다.

 

emailCert 함수를 만들고, 입력칸과 확인버튼 쪽 코드를 수정해준다.

(...)

    const emailCert = () => {
        if(emailAuthText !== '') { // 인증번호 입력란이 공백이 아닐 경우에 실행
            

        }
    }
    
(...)

  <div>
      <p style={{color : 'red', fontSize: 5}}>남은 시간 : {Math.floor(emailCount / 60)} 분 {Math.floor(emailCount % 60)} 초</p>
      <input 
          name="emailAuthText"
          type="text"
          placeholder="Enter number"
          onChange={(event) => { setEmailAuthText(event.target.value)}}/>
      <button
          type="button"
          onClick={emailCert}>
      확인  
      </button>
  </div>
  
(...)

 

dispatch 사용을 위해 필요한 것들을 작성해준다.

 

userAct.interfaces.ts

export enum actionTypesUser {
    USER_INIT = "USER_INIT", // state 초기화
    USER_STATE = "USER_STATE", // userState 변경
    USER_INFO = "USER_INFO",  // userInfo 변경 
    USER_ID_DUPLICATE = "USER_ID_DUPLICATE", // ID 중복 검사
    USER_EMAIL_DUPLICATE = "USER_EMAIL_DUPLICATE", // Email 중복검사
    USER_EMAIL_SEND = "USER_EMAIL_SEND", // Email 보내기
    USER_EMAIL_CERT = "USER_EMAIL_CERT", // Email 인증번호 확인
}

export type ActionsUser = UserInit | UserState | UserInfo | UserIdDuplicate
                         | UserEmailDuplicate | UserEmailSend | UserEmailCert

export interface UserInit {
    type : actionTypesUser.USER_INIT;
    data : any;
}

export interface UserState {
    type : actionTypesUser.USER_STATE;
    data : any;
}

export interface UserInfo {
    type : actionTypesUser.USER_INFO;
    data : any;
}

export interface UserIdDuplicate {
    type : actionTypesUser.USER_ID_DUPLICATE;
    payload : any;
}

export interface UserEmailDuplicate {
    type : actionTypesUser.USER_EMAIL_DUPLICATE;
    payload : any;
}

export interface UserEmailSend {
    type : actionTypesUser.USER_EMAIL_SEND;
    payload : any;
}

export interface UserEmailCert {
    type : actionTypesUser.USER_EMAIL_CERT;
    payload : any;
}

 

user_reducer.ts

        case actionTypesUser.USER_EMAIL_CERT:
            return {
                ...state, data : action.payload
            }

 

user_action.ts

export async function userEmailCert (authNum : string) {
    const request = Axios.post('/api/email/cert',{authNum : authNum})
    .then(response => response.data)

    return {
        type : actionTypesUser.USER_EMAIL_CERT,
        payload: request
    }
}

 

프론트에서 로직을 먼저 구현한다.

 

signComponent.tsx

    const emailCert = () => {
        if(emailAuthText !== '') { // 인증번호 입력란이 공백이 아닐 경우에 실행
            dispatch(userEmailCert(emailAuthText)).then((req : any) => {
                switch(req.payload.result) {
                    case "expiry" :
                        alert('쿠키가 존재하지 않습니다.')
                        break;
                    case "false" :
                        alert('인증번호가 틀립니다.')
                        break;
                    case "success" :
                    	alert('인증번호가 일치합니다.');
                        setEmailSendOK(false);
                        setEmailCount(0);
                        dispatch({type : actionTypesUser.USER_STATE, data : ['emailAuth',true]});
                    default :
                        break;
                }
            })
        }
    }
  • authNum 쿠키가 존재하지 않을 경우 : '쿠키가 존재하지 않습니다' 문구를 띄운다.
  • 인증번호가 틀릴경우 : '인증번호가 틀립니다' 문구를 띄운다.
  • 성공할 경우 : '인증번호가 일치합니다' 문구를 띄우고 emailSendOK를 false로 바꿔서 인증번호 입력칸과 전송버튼을 사라지게하고 남은 시간을 다시 0으로 바꾼다. userState의 emailAuth를 true로 바꿔서 보내기 버튼을 비활성화 시킨다.

 

이제 프론트쪽에서의 구현은 끝났으니 백엔드(NestJS)로 넘어가보자.

 

email.controller.ts 

  @Post('cert')
  async emailCert(@Res({ passthrough: true}) res : any, @Req() req : any) {
    return await this.emailService.emailCert(req);
  }

service의 emailCert 함수 결과값을 리턴한다.

 

email.service.ts

  async emailCert(req : any) {
    if(req.cookies.authNum) {
      const result = await bcrypt.compare(req.body.authNum,req.cookies.authNum); 
      if(result) {
        return {result : "success"}
      }
      else {
        return {result : "failed"}
      }
    }
    else{ 
      return {result : "expiry"}
    }
  }

쿠키가 존재하지 않으면 expiry를 반환한다.

bcrypt.compare 메소드를 사용해서 암호가 일치하면 success, 다르면 failed를 반환한다.

 

이제 직접 화면에서 실행해보자.

 

- 이메일을 보냈을 때

 

 

- 인증번호 입력해서 확인버튼 눌렀을 때

 

- alert 창의 확인버튼을 눌렀을 때

 

이렇게 이메일 인증번호를 보내고 인증번호를 확인하는것 까지 완료했다.

728x90
반응형
728x90
반응형

 

전 포스터에서 이메일 중복여부를 확인하는 로직까지 완성했다.

이번엔 백엔드에서 클라이언트가 원하는 데이터를 전송해보도록 하겠다.

 

이메일 리소스를 만든다.

$ nest g res email

 

이메일 중복 여부와 이메일 보내기 기능을 같이 구현해보겠다.

먼저 EmailService에서 UserModel을 써야하기 때문에 EmailModule부분을 다음과 같이 바꿔준다.

email.module.ts

import { Module } from '@nestjs/common';
import { EmailService } from './email.service';
import { EmailController } from './email.controller';
//mongose
import { MongooseModule } from '@nestjs/mongoose';
//userModel
import { User, UserSchema } from 'src/user/schemas/user.schema';


@Module({
  imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
  controllers: [EmailController],
  providers: [EmailService]
})
export class EmailModule {}

 

또한 nest에서 지원해주는 email모듈을 써야하기 때문에 다음 모듈을 설치하고 import 해준다.

 

 $ npm install @nestjs-modules/mailer nodemailer handlebars --save

 

그다음 emailModule을 다음과 같이 바꿔준다. 네이버 아이디와 비밀번호는 개인용으로 쓰면 된다.

email.module.ts

import { Module } from '@nestjs/common';
import { EmailService } from './email.service';
import { EmailController } from './email.controller';
//mongose
import { MongooseModule } from '@nestjs/mongoose';
//userModel
import { User, UserSchema } from 'src/user/schemas/user.schema';
//Email
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter} from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';

@Module({
  imports: [
  MailerModule.forRoot({
    transport: {
      service: 'Naver',
      host: 'smtp.naver.com',
      port: 587, 
      auth: {
        user: process.env.NAVER_ID, // 네이버 아이디
        pass: process.env.NAVER_PW, // 네이버 비밀번호
      },
    },
    template: {
      dir: process.cwd() + '/template/',
      adapter: new HandlebarsAdapter(), // or new PugAdapter()
      options: {
        strict: true,
      },
    },
  }),
  MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
  controllers: [EmailController],
  providers: [EmailService]
})
export class EmailModule {}

 


Nest Configuration에 관한 내용은 아래 링크에서 확인하면 된다.

 

https://docs.nestjs.kr/techniques/configuration

 

네스트JS 한국어 매뉴얼 사이트

네스트JS 한국, 네스트JS Korea 한국어 매뉴얼

docs.nestjs.kr


 

Naver smtp 서비스를 이용하려면 Naver 메일로 가서 설정을 해주어야한다.

 

 

자세한 내용은 아래 링크

https://velog.io/@jiwon/-Nodemailer%EB%A1%9C-%EC%9D%B8%EC%A6%9D-%EA%B4%80%EB%A0%A8-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%B3%B4%EB%82%B4%EA%B8%B0-d4k4pqoot4

 

Nodemailer로 인증 관련 이메일 보내기

사용자가 회원가입 시, 입력한 이메일이 유효한지 검증하는 상황 혹은 사용자에게 임시 비밀번호를 전달하는 경우 등등 서버 측에서 사용자에게 이메일을 보내야하는 경우가 존재합니다! 이때,

velog.io

 

 

EmailService 부분에 Usermodel을 가져오고 다음과같이 수정해준다.

email.service.ts

import { Injectable } from '@nestjs/common';
//mongoose
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from '../user/schemas/user.schema';
//dtd
import { CreateEmailDto } from './dto/create-email.dto';
import { UpdateEmailDto } from './dto/update-email.dto';

@Injectable()
export class EmailService {
  constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}
  
  create(createEmailDto: CreateEmailDto) { 
    return 'This action adds a new email';
  }

  findAll() {
    return `This action returns all email`;
  }

  async findOne(email: string) {
    const userOne = await this.userModel.findOne({email});
    if(userOne)
      return {result : false, user : userOne};
    else
      return {result : true};
  }

  update(id: number, updateEmailDto: UpdateEmailDto) {
    return `This action updates a #${id} email`;
  }

  remove(id: number) {
    return `This action removes a #${id} email`;
  }
}

 

 

이제 이메일을 보내는 함수를 작성해보자.

email.service.ts

import { Injectable } from '@nestjs/common';
//mongoose
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from '../user/schemas/user.schema';
//dto
import { CreateEmailDto } from './dto/create-email.dto';
import { UpdateEmailDto } from './dto/update-email.dto';
//mailer
import { MailerService } from '@nestjs-modules/mailer';

@Injectable()
export class EmailService {
  constructor(
    @InjectModel(User.name) private userModel: Model<UserDocument>,
    private mailerService: MailerService
    ) {}

  async findOne(email: string) {
    const userOne = await this.userModel.findOne({email});
    if(userOne)
      return {result : false, user : userOne};
    else
      return {result : true};
  }

  async emailSend(email : string) {
    try {
      const number: number = Math.floor(100000 + Math.random() * 900000);
      // 메일보내기
      await this.mailerService.sendMail({
        to: email, // list of receivers
        from: 'user@email', // sender address
        subject: '이메일 인증 요청 메일입니다.', // Subject line
        html: '6자리 인증 코드 : ' + `<b> ${number}</b>`, // HTML body content
      });
      return {result : true, authNum : number}
    } catch (err) {
      return {result : false}
    }
  }
}

 

async emailSend 함수에서 파라미터로 email을 받아 메일을 받을 사람을 정하고, 보낼 사람과 내용까지 정의해서

메일을 보내게끔 한다.

 

반환값으로 인증번호를 그대로 보내기엔 보안위험이 있을 수 있으니 bcrypt로 암호화 해서 보내자.

 

$ npm i bcrypt
$ npm i @types/bcrypt -D

 

email.service.ts

import { Injectable } from '@nestjs/common';
//mongoose
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from '../user/schemas/user.schema';
//dto
import { CreateEmailDto } from './dto/create-email.dto';
import { UpdateEmailDto } from './dto/update-email.dto';
//mailer
import { MailerService } from '@nestjs-modules/mailer';
//bcrypt
import * as bcrypt from 'bcrypt'

@Injectable()
export class EmailService {
  constructor(
    @InjectModel(User.name) private userModel: Model<UserDocument>,
    private mailerService: MailerService,
    ) {}

  async findOne(email: string) {
    const userOne = await this.userModel.findOne({email});
    if(userOne)
      return {result : false, user : userOne};
    else
      return {result : true};
  }

  async emailSend(email : string) {
    try {
      const number: string = Math.floor(100000 + Math.random() * 900000).toString();
      console.log(number)
      // 메일보내기
      await this.mailerService.sendMail({
        to: email, // list of receivers
        from: 'user@email.com', // sender address
        subject: '이메일 인증 요청 메일입니다.', // Subject line
        html: '6자리 인증 코드 : ' + `<b> ${number}</b>`, // HTML body content
      });
      const authNum = await bcrypt.hash(number,parseInt(process.env.saltOrRounds));
      return {result : true, authNum : authNum};
    } catch (err) {
      return {result : false, authNum : ''};
      console.log(err)
    }
  }
}

bcryptimport하고 class 내부에 salt 문자열을 선언했으며 bcrypt로 number를 암호화해서 반환했다.

 

이제 생성한 authNum을 쿠키에 넣어주기 위해 res를 추가하고 수정하자.

 

email.service.ts

  async emailSend(email : string, res : any) {
    try {
      const number: string = Math.floor(100000 + Math.random() * 900000).toString();
      console.log(number)
      // 메일보내기
      await this.mailerService.sendMail({
        to: email, // list of receivers
        from: process.env.EMAIL, // sender address
        subject: '이메일 인증 요청 메일입니다.', // Subject line
        html: '6자리 인증 코드 : ' + `<b> ${number}</b>`, // HTML body content
      });
      const authNum = await bcrypt.hash(number,parseInt(process.env.saltOrRounds));
      
      res.cookie('authNum', authNum, {path: '/', expires: new Date(Date.now()+300000)}); // 쿠키생성
    
      return {result : true, authNum : authNum};
    } catch (err) {
      console.log(err)
      return {result : false, authNum : ''};
      
    }
  }

 


쿠키를 사용하기 위해 먼저 다음 명령어를 실행한다.

$ npm i cookie-parser
$ npm i -D @types/cookie-parser

 

그다음 main.ts 파일을 다음과 같이 수정한다.

 

main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(cookieParser());
  await app.listen(3001);
}
bootstrap();

 


 

 

 

이메일을 성공적으로 보냈으면 cookie에 authNum을 시간을 설정해서 담아주고 result를 true로 보냈다.

이제 다시 프론트로 넘어가 result가 true로 반환되었을 때 로직을 구현해보자.

 

남은시간과 이메일 확인 입력칸 state를 만들어준다.

    const [emailCount,setEmailCount] = useState<number>(0); // email counter
    const [emailAuthText,setEmailAuthText] = useState<string>('emailAuthText'); // email auth text

 

 

emailSendOK state를 이용해 재전송으로 바꾸고 하단에 인증번호 입력칸과 확인버튼, 남은시간을 보여주는 곳을 만든다. 

 

<div>
    <input 
        name="email"
        type="email"
        placeholder="Enter Email"
        onChange={userChangeHandler}
        disabled={userState.emailAuth}/>
    <button
    	type="button"
        onClick={emailSend}
        disabled={userState.emailAuth}>
    {emailsendOK ? '재전송' : '보내기'}    
    </button>
</div>
{ emailsendOK ?
<div>
    <p style={{color : 'red', fontSize: 5}}>남은 시간 : {Math.floor(emailCount / 60)} 분 {Math.floor(emailCount % 60)} 초</p>
    <input 
        name="emailAuthText"
        type="text"
        placeholder="Enter Email"
        onChange={(event) => { setEmailAuthText(event.target.value)}}/>
    <button
    	type="button"
        onClick={emailSend}>
    확인  
    </button>
</div>
: ''}

 

보내기 버튼을 눌러보자. 이메일이 정상적으로 보내졌고 하단에 추가되었다.

 

 

 

 

 

 

728x90
반응형

+ Recent posts