Skip to content

Latest commit

 

History

History
565 lines (464 loc) · 16.5 KB

File metadata and controls

565 lines (464 loc) · 16.5 KB

Understanding Validation

Forms, User Input & Validation

contents

  1. Why Validate?
  2. How to Validate
  3. 프로젝트에 적용하기

1. Why Validate?

Validate

다양한 유저가 우리 웹사이트에 들어올 수 있다. 이때 Input을 바로 서버에 있는 Database 나 File로 저장하는 것이 아닌 Controller나 Middleware 에서 검증하는 단계를 거치자 즉, 잘못된 Input을 한번 거르는 단계를 추가해보자

2. How to Validate

  • Client 단에서 실행하는 JavaScript 코드는 실행되지 않거나 변조 될 수 있다.
  • 따라서 선택적으로 사용하고 대신 Server (Node App) 에서 검증하는 방법을 사용해야 한다.

3. 프로젝트에 적용하기

로그인 및 가입 페이지에 유효한 Email 검사하기

  1. $ npm i --save express-validator 공식 문서 : https://express-validator.github.io/docs/

  2. routes/auth.js에 위 패키지 가져오고 적용시키기

    // JavaScript에서 제공하는 Destructuring 을 사용하여 check 함수를 가져오자
    const { check } = require('express-validator/check');
    
    
    // 라우터에는 넣고싶은 만큼 라우터를 추가할 수 있다. 따라서 check() 를 넣어주고 인자로 form 을 넣어주자
    router.post('/signup', check('email').isEmail(), authController.postSignup);
  3. controllers/auth.js 수정하기

    • express-validator 가져오기

      // routes/auth.js에서 미리 설정했던 isEmail 함수에서 받아 오류 생성
      const { validationResult } = require('express-validator/check');
    • postSignup 에서 오류 설정하기

      exports.postSignup = (req, res, next) => {
        const email = req.body.email;
        const password = req.body.password;
        const confirmPassword = req.body.confirmPassword;
        // client 사이드에서 오류가 발생하면 req로 전달
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
          console.log(errors.array());
          return res.status(422).render('auth/signup', {
            path: '/signup',
            pageTitle: 'Signup',
            errorMessage: errors.array()[0].msg
          });
        }
      
          ...
          
  4. Error 메세지 수정하기 위해 routes/auth.jspostSignup 수정하기

    router.post(
      '/signup',
      check('email')
        .isEmail()
        .withMessage('Please Enter a Valid Email'),
      authController.postSignup
    );
  5. express-validator 커스터마이징 하기 아래 처럼 custom 함수를 활용 할 수 있다.

    router.post(
      '/signup',
      check('email')
        .isEmail()
        .withMessage('Please Enter a Valid Email')
        .custom((value, { req }) => {
          if (value === 'test@test.com') {
            throw new Error('This Email Address if forbidden.');
          }
        }),
      authController.postSignup
    );

비밀번호 검증하기

  1. controllers/admin.js에 있는 express-validator 에 body 추가하기

    const { check, body } = require('express-validator/check');
  2. controllers/admin.js에 있는 postSignup 라우터에 비밀번호 체크 기능 넣기

    router.post(
      '/signup',
    
      [
        check('email')
          .isEmail()
          .withMessage('Please Enter a Valid Email')
          .custom((value, { req }) => {
            if (value === 'test@test.com') {
              throw new Error('This Email Address if forbidden.');
            }
            // default case
            return true;
          }),
        //body(인자1, 인자2) => req.body로 날라오는 값을 검증
        // 인자1 : 검증할 값, 인자2: default 에러 메세지
        body(
          'password',
          'Please Enter a Password wiht only numbers and text and at least 5 characters.'
        )
          .isLength({ min: 5 })
          // 알파벳만 가능
          .isAlphanumeric()
      ],
      authController.postSignup
    );
  3. 패스워드 재입력 기능 만들기 위해 다시 postSignup 수정

    router.post(
      '/signup',
    
      [
        check('email')
          .isEmail()
          .withMessage('Please Enter a Valid Email')
          .custom((value, { req }) => {
            if (value === 'test@test.com') {
              throw new Error('This Email Address if forbidden.');
            }
            // default case
            return true;
          }),
        body(
          'password',
          'Please Enter a Password wiht only numbers and text and at least 5 characters.'
        )
          .isLength({ min: 5 })
          .isAlphanumeric(),
        body('confirmPassword').custom(value => {
          if (!value === req.body.password) {
            throw new Error('Passwords have to match!');
          }
          return true;
        })
      ],
      authController.postSignup
    );

Email 체크 기능 router에서 express-validator 로 하기

  1. routes/auth.jsmodels/user.js 불러오기

    const User = require('../models/user');
  2. routes/auth.jspostSign에 Email 검증 로직 추가하기

  3. 기존 controllers/auth.jspostSignup에서 Email 검증 기능 빼기

    exports.postSignup = (req, res, next) => {
      const email = req.body.email;
      const password = req.body.password;
      // client 사이드에서 오류가 발생하면 req로 전달
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        console.log(errors.array());
        return res.status(422).render('auth/signup', {
          path: '/signup',
          pageTitle: 'Signup',
          errorMessage: errors.array()[0].msg
        });
      }
      return bcrypt
        .hash(password, 12)
        .then(hashedPassword => {
          const user = new User({
            email: email,
            password: hashedPassword,
            cart: { items: [] }
          });
          return user.save();
        })
        .then(result => {
          res.redirect('/login');
          return sgMail.send({
            to: email,
            from: 'nodeshop@shop.com',
            subject: 'Signup suceeded!',
            html: '<h1>Hi! 회원가입을 축하합니다.</h1>'
          });
        })
        .catch(err => {
          console.log(err);
        });
    };

UX 개선하기1 -로그인 / 회원가입 할때 잘못 입력해도 입력했던 form 유지하기

  1. controllers/auth.js 사용자 Input을 유지하기 위해 render할 때 사용자 Input 다시 보내주기

    exports.getSignup = (req, res, next) => {
      let message = req.flash('error');
      if (message.length > 0) {
        message = message[0];
      } else {
        message = null;
      }
      res.render('auth/signup', {
        path: '/signup',
        pageTitle: 'Signup',
        errorMessage: message,
       // 처음엔 빈 값 보내주기
          oldInput: {
          email: '',
          password: '',
          confirmPassword: ''
        }
      });
    };
    
    
    exports.postSignup = (req, res, next) => {
      const email = req.body.email;
      const password = req.body.password;
      // client 사이드에서 오류가 발생하면 req로 전달
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        console.log(errors.array());
        return res.status(422).render('auth/signup', {
          path: '/signup',
          pageTitle: 'Signup',
          errorMessage: errors.array()[0].msg,
          // 이전 Input 다시 보내줘서 form value로 넣기
          oldInput: { email: email, password: password }
        });
      }
  2. views/auth/signup.ejs form value 로 값 전달하기

    <form class="login-form" action="/signup" method="POST" novalidate>
        <div class="form-control">
            <label for="email">E-Mail</label>
            <input type="email" name="email" id="email" value="<%= oldInput.email %>">
        </div>
        <div class="form-control">
            <label for="password">Password</label>
            <input type="password" name="password" id="password" value="<%= oldInput.password %>">
        </div>
        <div class="form-control">
            <label for="confirmPassword">Confirm Password</label>
            <input type="password" name="confirmPassword" id="confirmPassword" value="<%= oldInput.confirmPassword %>">
        </div>
        <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
        <button class="btn" type="submit">SignUP!</button>
    </form>

UX 개선하기2 - CSS Style

잘못 된 Input을 줬을 때 해당 box 빨간색 테두리 넣기

  1. controllers/auth.js postSignup errors 배열 전체 보내주기

    exports.getSignup = (req, res, next) => {
      let message = req.flash('error');
      if (message.length > 0) {
        message = message[0];
      } else {
        message = null;
      }
      res.render('auth/signup', {
        path: '/signup',
        pageTitle: 'Signup',
        errorMessage: message,
        oldInput: {
          email: '',
          password: '',
          confirmPassword: ''
        },
        validationErrors: []
      });
    };
    
    exports.postSignup = (req, res, next) => {
      const email = req.body.email;
      const password = req.body.password;
      // client 사이드에서 오류가 발생하면 req로 전달
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        return res.status(422).render('auth/signup', {
          path: '/signup',
          pageTitle: 'Signup',
          errorMessage: errors.array()[0].msg,
          // 이전 Input 다시 보내줘서 form value로 넣기
          oldInput: {
            email: email,
            password: password,
            confirmPassword: req.body.confirmPassword
          },
          // 오류 생겼을 때 errors 보내주기
          validationErrors: errors.array()
        });
      }
  2. views/auth/signup.ejsinputcss class 추가하기

    <input 
           class="<%= validationErrors.find(e => e.param === 'email') ? 'invalid' : '' %>"
           type="email" 
           name="email" 
           id="email" 
           value="<%= oldInput.email %>"
    >
  3. public/css/form.css 에 추가하기

    .form-control input.invalid {
      border-color: red;
    }

로그인 할때 정규식 추가로 사용하기

사용자들은 회원가입 할 때 Email에 대문자나 공백을 넣을 수 있다. 이를 방지하기 위해 router 단에서 미리 사용자 Input을 검증하자

  1. routes/auth.js

    • postLogin 에 정규식 추가하기

      router.post(
        '/login',
        [
          body('email')
            .isEmail()
            .withMessage('Email형식을 입력해주세요')
            .normalizeEmail(), // 이메일 형식 만들기
          body('password', '패스워드를 5글자 이상 또는 알파벳만 넣어주세요.')
            .isLength({ min: 5 })
            .isAlphanumeric()
            .trim() // 공백 지우기
        ],
        authController.postLogin
      );
    • postSignup에도 적용하기

      router.post(
        '/signup',
      
        [
          body('email', 'E-mail exists already!')
            .isEmail()
            .withMessage('Please Enter a Valid Email')
            .custom((value, { req }) => {
              // 유저가 입력한 Eamil 존재하는지 DB에서 찾기
              return User.findOne({ email: value }).then(userDoc => {
                if (userDoc) {
                  // Error 던지기
                  return Promise.reject('E-mail exists already!!');
                }
              });
            })
            .normalizeEmail(),
          //body(인자1, 인자2) => req.body로 날라오는 값을 검증
          // 인자1 : 검증할 값, 인자2: default 에러 메세지
          body(
            'password',
            'Please Enter a Password wiht only numbers and text and at least 5 characters.'
          )
            .isLength({ min: 5 })
            // 알파벳만 가능
            .isAlphanumeric()
            .trim(),
          body('confirmPassword')
            .trim()
            .custom((value, { req }) => {
              if (value !== req.body.password) {
                throw new Error('Passwords have to match!');
              }
              return true;
            })
        ],
        authController.postSignup
      );

    Add Product 검증하기

    1.routes/admin.js

    • express-validator 불러오기

      const { body } = require('express-validator/check');
    • postAddProduct 에 검증 로직 추가하기

      router.post(
        '/add-product',
        [
          body('title', 'Title에 최소한 5글자 이상 넣어주세요')
            .isLength({ min: 3 })
            .isAlphanumeric()
            .trim(),
          body('imageUrl', 'URL 형식을 넣어주세요').isURL(),
          body('price').isFloat(),
          body('description')
            .isLength({ min: 5, max: 400 })
            .trim()
        ],
        isAuth,
        adminController.postAddProduct
      );

    2.controllers/admin.js

    • express-validator 불러오기

      const { body } = require('express-validator/check');
    • postAddProduct 검증 로직 추가하기

    3.views/admin/edit-product.ejs

    • editing 상태 구별하고 erorrMessage 넣기

      <main>
          <% if (errorMessage) { %>
          <div class="user-message user-message--error"><%= errorMessage %></div>
          <% } %>
          <form class="product-form" action="/admin/<% if (editing) { %>edit-product<% } else { %>add-product<% } %>" method="POST">
              <div class="form-control">
                  <label for="title">Title</label>
                  <input type="text" name="title" id="title" value="<% if (editing || hasError) { %><%= product.title %><% } %>">
              </div>
              <div class="form-control">
                  <label for="imageUrl">Image URL</label>
                  <input type="text" name="imageUrl" id="imageUrl" value="<% if (editing || hasError) { %><%= product.imageUrl %><% } %>">
              </div>
              <div class="form-control">
                  <label for="price">Price</label>
                  <input type="number" name="price" id="price" step="0.01" value="<% if (editing || hasError) { %><%= product.price %><% } %>">
              </div>
              <div class="form-control">
                  <label for="description">Description</label>
                  <textarea name="description" id="description" rows="5"><% if (editing  || hasError) { %><%= product.description %><% } %></textarea>
              </div>
              <% if (editing) { %>
              <input type="hidden" value="<%= product._id %>" name="productId">
              <% } %>
              <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
              <button class="btn" type="submit"><% if (editing) { %>Update Product<% } else { %>Add Product<% } %></button>
          </form>
      </main>

Edit Product 수정

  1. controllers/admin.js

    • postEditProduct

      exports.postEditProduct = (req, res, next) => {
        const prodId = req.body.productId;
        const updatedTitle = req.body.title;
        const updatedPrice = req.body.price;
        const updatedImageUrl = req.body.imageUrl;
        const updatedDesc = req.body.description;
        const errors = validationResult(req);
      
        if (!errors.isEmpty()) {
          return res.status(422).render('admin/edit-product', {
            pageTitle: 'Edit Product',
            path: '/admin/edit-product',
            editing: true,
            hasError: true,
            product: {
              title: updatedTitle,
              imageUrl: updatedImageUrl,
              price: updatedPrice,
              description: updatedDesc,
              _id: prodId // 꼭 추가해주기, getEditProduct 에는 이 값이 있다.
            },
            errorMessage: errors.array()[0].msg
          });
        }
    • 참고: Edit 버튼을 보면 URI 인자로 product._id와 쿼리로 edit=true를 줄 수 있는 것을 볼 수 있다. 이렇게 포스트 요청을 하면 각각 req.paramsreq.query로 받을 수 있다.

      <a href="/admin/edit-product/<%= product._id %>?edit=true" class="btn">Edit</a>