728x90
반응형

Nextjs 화면을 반응형으로 만들기 위해 우리는 bootstrap5를 이용할 것이다.

 

리액트에서의 bootstrap5는 아래 링크를 확인하면 된다.

https://designmodo.com/bootstrap-react-sass/#bootstrap-5-with-react-js

 

Getting Started with Bootstrap 5, React, and Sass

Get React JS and Bootstrap 5 install and integrate together using npm. Setup Sass for different screen resolutions of your app.

designmodo.com


SCSS에 관한 내용은 아래 링크를 참조하자.

https://typo.tistory.com/entry/Nextjs-%EA%B8%B0%EB%B3%B8%EA%B8%B0%EB%8A%A5-Built-In-CSS-SupportSCSS?category=895504 

 

Next.js | 기본기능 | Built-In CSS Support(SCSS)

리액트에서의 스타일 적용법은 다음 링크로 확인할 수 있다. https://typo.tistory.com/entry/Reactjs-%EC%9E%85%EB%AC%B8-CSS-SASS-SCSS?category=891266 React.js | 입문 | CSS, SASS, SCSS ReactJS에 CSS를 추..

typo.tistory.com


 

먼저 bootstrap을 설치한다.

$ npm i bootstrap@5.0.0-alpha3

 

_app.tsx 파일에 추가해준다.

_app.tsx

import { AppProps } from "next/app";
import { NextPage } from "next";
//store
import wrapper from "../store";
//css
import "bootstrap/dist/css/bootstrap.min.css";

const MyApp: NextPage<AppProps> = ({ Component, pageProps }: AppProps) => {
	return (
		<>
			<Component {...pageProps} />
		</>
	);
};

export default wrapper.withRedux(MyApp);

 

 

 

기존에 만들어 두었던 dashboard 페이지를 다시 확인하자.

 

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}) => {
    const ClickHandler = () => {

    }
    return (
        <div>
            <div className='dashboard'>
                Dashboard
            </div>
        </div>
        )
}


export default Dashboard;

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,
            },
        }
    }
}

 

버튼을 눌렀을 경우 position이 "absolute"인 NavBar를 만들자.

 

dashboard/index.tsx

const Dashboard:NextPage<Props> = ({data}) => {
    const [sidebarOpen,setSidebarOpen] = useState(false);
    const SidebarOpen = () => {
        if(sidebarOpen)
            setSidebarOpen(false);
        else
            setSidebarOpen(true);
    }
    return (
        <div>
            {/* header */}
            <div className={"w-100 d-flex align-items-center mx-auto border border-bottom-1"}
                style={{
                    height: "50px"
                }}>
                <div style={{width: "5%"}}>
                    <button
                        style={{color:"blue"}}
                        onClick={SidebarOpen}>
                    o
                    </button>
                </div>
                <div 
                    style={{ width: "75%"}}>
                    로고
                </div>
                <div style={{width: "20%"}}>
                    프로필
                </div>
            </div>
            {/* nav */}
            {sidebarOpen ? 
                <div
                    style={{
                    backgroundColor: "lightblue",
                    position: "absolute",
                    width: "200px",
                    height: "100%",
                    opacity: 1
                    }}>
                        we
                </div>
                : ""
            }
            {/* body */}
            <div className='dashboard'>
                Dashboard
            </div>
            {/* footer */}
        </div>
        )
}

 

화면을 확인해보자.

 

 

버튼을 누르면 Navbar가 뜨고, 다시 누르면 사라진다. 너무 딱딱하니까 효과를 좀 넣어주자.

SCSS와 styled-components를 사용하기 위해 다음 모듈을 설치해준다.

 

$ npm install sass styled-components @types/styled-components

 

next.config.js 파일에 path를 import 하고 sassOptions를 설정해준다.

 

next.config.js

/** @type {import('next').NextConfig} */
const path = require('path');

module.exports = {
  sassOptions: {
    includePaths: [path.join(__dirname, 'styles'), path.join(__dirname, 'src')],
  },
  reactStrictMode: true,
  async rewrites() {
    console.log(process.env.NODE_ENV)
    if (process.env.NODE_ENV !== 'production') {
      return [
        {
          source: process.env.SOURCE_PATH,
          destination: process.env.DESTINATION_URL,
        },
      ];
    }
    else {
      return [
        {
          source: process.env.SOURCE_PATH,
          destination: process.env.DESTINATION_URL,
        },
      ];
    }
  },
}

 

dashboard 페이지에 SCSS와 styled-components를 활용해준다.

 

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'
import { useState } from 'react'
import styled, {keyframes} from 'styled-components'


interface Props {
    data : any
}

// 투명도 설정
const fadein = keyframes`
 from {
     opacity : 0
 }
 to {
     opacity: 1
 }
`

// NavDiv에 효과 설정
const NavDiv = styled.div`
  animation-duration: 0.25s;
  animation-timing-function: ease-out;
  animation-name: ${fadein};
  animation-fill-mode: forwards
`


const Dashboard:NextPage<Props> = ({data}) => {
    const [sidebarOpen,setSidebarOpen] = useState(false);
    const SidebarOpen = () => {
        if(sidebarOpen)
            setSidebarOpen(false);
        else
            setSidebarOpen(true);
    }
    return (
        <div>
            {/* header */}
            <div className={"w-100 d-flex align-items-center mx-auto border border-bottom-1"}
                style={{
                    height: "50px"
                }}>
                <div style={{width: "5%"}}>
                    <button
                        style={{color:"blue"}}
                        onClick={SidebarOpen}>
                    o
                    </button>
                </div>
                <div 
                    style={{ width: "75%"}}>
                    로고
                </div>
                <div style={{width: "20%"}}>
                    프로필
                </div>
            </div>
            {/* nav */}
            {sidebarOpen ? 
                <NavDiv
                    style={{
                    backgroundColor: "lightblue",
                    position: "absolute",
                    width: "200px",
                    height: "100%",
                    opacity: 1
                    }}>
                        we
                </NavDiv>
                : ""
            }
            {/* body */}
            <div className='dashboard'>
                Dashboard
            </div>
            {/* footer */}
        </div>
        )
}


export default Dashboard;

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,
            },
        }
    }
}

 

inline-css가 좋지는 못하다고 하나 나중에 한꺼번에 정리하려고 일단 기능이 되는대로 넣어놨다.

 

다시 버튼을 눌러보면 효과가 적용되어 나타나는 것을 확인할 수 있다.

 

 

728x90
반응형
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


 

먼저 Swagger를 설치해주자.

$ npm install --save @nestjs/swagger swagger-ui-express

 

 

main.ts 파일에 swagger를 추가해준다.

main.ts

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

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

  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(3001);
  }
bootstrap();

 

그 다음 UserController와 EmailController를 바꿔준다.

 

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';

//swagger
import { ApiCreatedResponse, ApiOperation, ApiTags } from '@nestjs/swagger';

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

  @Get(':id')
  @ApiOperation({summary : '유저 확인 API', description: '유저를 찾는다.' })
  @ApiCreatedResponse({description: '유저를 찾는다'})
  findOne(@Param('id') id: string) {
    return this.userService.findOne(id);
  }

  @Post('signup')
  @ApiOperation({summary : '회원가입 API', description: '회원가입' })
  @ApiCreatedResponse({description: '회원가입을 한다'})
  async signUp(@Res({ passthrough: true}) res : any, @Req() req : any) {
    return this.userService.signUp(req.body.userInfo);
  }

  @UseGuards(LocalAuthGuard)
  @Post('login')
  @ApiOperation({summary : '로그인 API', description: '로그인' })
  @ApiCreatedResponse({description: '로그인을 한다'})
  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')
  @ApiOperation({summary : '토큰확인 API', description: '토큰확인' })
  @ApiCreatedResponse({description: '토큰확인을 한다'})
  getProfile(@Request() req) {
    return req.user;
  }
}

 

email.controller.ts

import { Controller, Get, Post, Body, Patch, Param, Delete, Req, Res } from '@nestjs/common';
import { EmailService } from './email.service';
import { CreateEmailDto } from './dto/create-email.dto';
import { UpdateEmailDto } from './dto/update-email.dto';

//swagger
import { ApiCreatedResponse, ApiOperation, ApiTags } from '@nestjs/swagger';

@Controller('email')
@ApiTags('이메일 API')
export class EmailController {
  constructor(private readonly emailService: EmailService) {}

  @Get(':id')
  @ApiOperation({summary : '이메일 확인 API', description: '이메일로 유저를 찾는다.' })
  @ApiCreatedResponse({description: '이메일로 유저를 찾는다'})
  findOne(@Param('id') id: string) {
    return this.emailService.findOne(id);
  }

  @Post('send')
  @ApiOperation({summary : '이메일 전송 API', description: '이메일을 전송한다.' })
  @ApiCreatedResponse({description: '이메일을 전송한다.'})
  async emailsend(@Res({ passthrough: true }) res : any, @Req() req : any) {
    return await this.emailService.emailSend(req.body.email,res);
  }

  @Post('cert')
  @ApiOperation({summary : '이메일 인증 API', description: '인증번호를 확인한다.' })
  @ApiCreatedResponse({description: '인증번호를 확인한다.'})
  async emailCert(@Res({ passthrough: true}) res : any, @Req() req : any) {
    return await this.emailService.emailCert(req);
  }
}

 

swagger 화면(localhost:3001/swagger)으로 접속해본다.

 

 

정상적으로 뜨는 것을 확인했다.

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
반응형
728x90
반응형

Password

비밀번호와 비밀번호 확인부터 해보자.

 

비밀번호 확인란 텍스트 state를 만들어준다.

 

    const [pwdCertText,setPwdCertText] = useState<string>('');

 

비밀번호 확인에 관한 state는 이 페이지 안에서만 사용될 것이다.

 

그다음 다음과 같이 코드를 작성해준다.

 

(...)
  <div>
      <input 
          style={{ borderColor : (userState.userInfo.pw.length < 4) ? 'red' : 'lightgreen' }}
          name="pw"
          type="password"
          placeholder="Enter PW"
          onChange={userChangeHandler} />
      <p style={{color : 'red', fontSize: 5}}>
          {(userState.userInfo.pw.length < 4) ? '4글자 이상 입력해주세요' : ''}
      </p>
  </div>
  <div>
      <input 
          style={{ borderColor : (userState.userInfo.pw !== pwdCertText) ? 'red' : 'lightgreen' }}
          name="pw2"
          type="password"
          placeholder="Verify password"
          value={pwdCertText}
          onChange={(event) => { setPwdCertText(event.target.value)}}/>
      <p style={{color : 'red', fontSize: 5}}>
          {(userState.userInfo.pw !== pwdCertText) ? '비밀번호가 다릅니다' : ''}
      </p>
  </div>
(...)

 

비밀번호 입력칸은 userState.userInfo.pw 안에 들어갈 것이고 비밀번호 확인 입력칸은 pwdCertText 안에 들어갈 것이다.

  • 각각 input의 border를 검증이 완료되지 않으면 빨간색으로 지정한다.
  • userState.userInfo.pw의 값이 4보다 작을 땐 '4글자 이상 입력해주세요' 문구를 나타내준다.
  • pwdCertText의 값이 userState.userInfo.pw 의 값과 다르면 '비밀번호가 다릅니다' 문구를 나타내준다.
  • 모든 조건이 성립하면 border의 색깔을 lightgreen으로 바꾼다.

화면구성을 보자.

 

 

4글자 입력은 했지만 비밀번호 확인 텍스트와 다를 경우 :

 

 

두가지 모두 성립할 경우 :

 

 

이 밖에도 숫자, 영문, 특수문자 조합 등 다양한 조건으로 validation 을 설정할 수 있다.

 

Email

이메일 전송 방식은 

  1. 이메일을 입력하고 보내기 버튼을 누른다.
  2. 만약 이메일이 중복되면 아무것도 실행되지 않는다.
  3. NestJS에서 입력한 주소로 이메일을 보내준다.
  4. 보내기 버튼이 재전송 버튼으로 바뀌게되고 남은 시간이 보여진다.
  5. 재전송 버튼을 눌렀을 경우 남은 시간이 reset 된다.
  6. 인증번호 입력칸과 확인버튼이 생성된다.
  7. 확인 버튼을 눌러서 인증번호가 확인되면 확인버튼도 비활성화된다.
  8. 남은 시간이 만료되면 인증번호 입력칸과 확인버튼이 사라지고 보내기버튼이 활성화된다.

 

이메일 전송에 필요한 state를 선언해준다.

    const [emailsendOK,setEmailSendOK] = useState<boolean>(false); // email send ( 인증번호 입력칸과  확인버튼 활성화 )

 

 

이메일 보내기 버튼에 onClick을 추가하고 input과 button의 disabled 옵션에 useState.emailAuth를 넣어서 인증이 완료되면 비활성화 되게 한다.

 

<div>
    <input 
        name="email"
        type="email"
        placeholder="Enter Email"
        onChange={userChangeHandler}
        disabled={userState.emailAuth}/>
        
    <button
    	type="button"
    	onClick={emailSend}
        disabled={userState.emailAuth}>
    보내기    
    </button>
</div>

 

emailSend 함수를 만들어준다.

const emailSend = () => {
}

아이디 중복할 때와 비슷하게 이메일 중복확인, 이메일 보내기를 act, type, action, reducer를 만들고 실행해줄 것이다.

 

act와 type을 만들어준다.

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 보내기
}

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

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;
}

 

리듀서에 추가해준다.

user_reducer.ts

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

 

action을 만들어준다.

user_action.ts

export async function userEmailDuplicate (email : string) {
    const request = Axios.get('/api/email/'+email)
    .then(response => response.data)
    
    return {
        type : actionTypesUser.USER_ID_DUPLICATE,
        payload: request
    }
}

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

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

axios post 메소드로 백엔드에 데이터를 보냈다.

 

다시  SignupComponent.tsx 파일로 돌아와 emailSend 함수를 수정한다.

 

SignupComponent.tsx

    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)
                                // 성공했을 경우 로직
                            }else {
                                alert('이메일 전송이 실패됐습니다.')
                            }
                        });
                }
            })
        }
    }

이메일이 존재하는지 확인하고, 중복되지 않을 경우에 

  1. 인증번호에 대한 쿠키를 시간을 설정해서 bcrypt로 암호화한 후 생성해준다.
  2. 보내기 버튼을 재전송 버튼으로 바꾼다.
  3. 재전송 버튼을 눌렀을 때 남은시간은 리셋된다.
  4. 하단에 인증번호 입력칸과 확인버튼을 만든다.
  5. 남은시간이 0이되면 인증번호 입력칸과 확인버튼이 사라지고 재전송이 보내기로 바뀐다.

 

이메일 중복 여부부터 다음 포스터에서 다루겠다.

 

 

 

 

 

 

728x90
반응형

+ Recent posts