포스트

2024.06.04 프로그래머스-북끼오 (bookkio) API에 auth 뭍히기

Bookkio 프로젝트 10일차

  • 인증이 필요한 API 에 auth 추가 하기

도서 조회 API 수정

  • auth 를 적용시켜, 사용자 로그인 상태(token이 존재) 일 때는 조회하는 도서의 좋아요 수를 표시하고, 아닐 때는 좋아요 수를 제외한 정보만을 표출하도록 수정하였다.
  • jwt인증을 수행하는 auth.js 파일에서 /books API 의 경우, 인증없이도 이용은 가능해야 하기 때문에 해당 baseURL 정보에 따른 처리를 추가하였다.

  • auth.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    
      const {
        verify,
        TokenExpiredError,
        JsonWebTokenError,
      } = require("jsonwebtoken");
      const { StatusCodes } = require("http-status-codes");
        
      var dotenv = require("dotenv");
      dotenv.config();
        
      const KEY = process.env.PRIVATE_KEY;
        
      function validateJSONToken(token) {
        return verify(token, KEY);
      }
        
      /**
       *
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       * @param {import("express").NextFunction} next
       */
      function checkAuth(req, res, next) {
        if (req.method === "OPTIONS") {
          return next();
        }
        
        if (!req.headers.authorization) {
          console.log("NOT AUTH");
          return res.status(400).json({ message: "Not Auth" });
        } else if (!req.headers.authorization && req.baseUrl.includes("/books")) {
      	  // /books 이면서 jwt token이 없을 때.
          return next();
        }
        console.log(req.headers.authorization);
        const authFragment = req.headers.authorization.split(" ");
        console.log(authFragment);
        const authToken = authFragment[0];
        console.log(authToken);
        
        try {
          const validateToken = validateJSONToken(authToken);
          console.log(validateToken);
          req.token = validateToken;
        } catch (err) {
          if (err instanceof TokenExpiredError) {
            return res
              .status(StatusCodes.UNAUTHORIZED)
              .json({ message: "로그인 세션이 만료 되었으니 다시 로그인해 주세요" })
              .end();
          } else if (err instanceof JsonWebTokenError) {
            return res
              .status(StatusCodes.BAD_REQUEST)
              .json({ message: "올바르지 않은 토큰 정보 입니다." })
              .end();
          }
          return res
            .status(StatusCodes.BAD_REQUEST)
            .json({ message: "사용자 인증에 문제가 발생했습니다." });
        }
        next();
      }
        
      // token에서 id 값 빼내기
      function getIdFromToken(req) {
        const { token } = req;
        const { id } = token;
        return +id;
      }
        
      exports.checkAuth = checkAuth;
      exports.extractId = getIdFromToken;
        
    
  • book.router.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
      const express = require("express");
      const {
        searchBooks,
        searchOneBook,
        getNewBooks,
      } = require("../controller/books.controller");
      const { checkAuth } = require("../utils/auth");
        
      const router = express.Router();
      router.use(checkAuth);
      router.get("/", searchBooks);
      router.get("/new", getNewBooks);
      router.get("/:bookId", searchOneBook);
        
      module.exports = router;
        
    
  • books.controller.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    
      const dbConnection = require("../model/mysql.js");
      const { StatusCodes } = require("http-status-codes");
        
      /**
       * 전체 도서 조회 Or 카테고리별 도서 조회
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       * @param {import("express").NextFunction} next
       */
      const searchBooks = (req, res, next) => {
        let { category_id, limit, currentpage } = req.query;
        const token = req.token;
        
        if (!limit || !currentpage) {
          limit = !limit ? 5 : limit;
          currentpage = !currentpage ? 1 : currentpage;
        }
        
        const offset = +limit * (+currentpage - 1);
        
        //카테고리 검색
        if (category_id) {
          let sqlQuery = token
            ? `
          SELECT *, (SELECT COUNT(*) from likes WHERE book_id = books.id) AS likes FROM books LEFT 
          JOIN category ON books.category_id = category.id
          WHERE books.category_id = ?
          LIMIT ? OFFSET ?;
          `
            : `
          SELECT * FROM books LEFT 
          JOIN category ON books.category_id = category.id
          WHERE books.category_id = ?
          LIMIT ? OFFSET ?;
          `;
        
          dbConnection.query(
            sqlQuery,
            [+category_id, +limit, offset],
            (err, results) => {
              if (err) {
                console.log(err);
                return res.status(StatusCodes.BAD_REQUEST);
              }
        
              if (results.length > 0) {
                return res.status(StatusCodes.OK).json(results);
              } else {
                return res.status(StatusCodes.NOT_FOUND).end();
              }
            }
          );
        } else {
          // 전체 도서 조회
        
          console.log(token);
          let sqlQuery = token
            ? `
          SELECT *, (SELECT COUNT(*) from likes WHERE book_id = books.id) AS likes FROM books
          LIMIT ? OFFSET ?
          `
            : `
          SELECT * FROM books
          LIMIT ? OFFSET ?
          `;
          dbConnection.query(sqlQuery, [+limit, offset], (err, results) => {
            if (err) {
              console.log(err);
              return res.status(StatusCodes.BAD_REQUEST).end();
            }
        
            if (results[0]) {
              const books = results.map((book) => {
                const resultBook = token
                  ? {
                      id: book.id,
                      title: book.title,
                      summary: book.summary,
                      author: book.author,
                      price: book.price,
                      pub_date: book.pub_date,
                      likes: book.likes,
                    }
                  : {
                      id: book.id,
                      title: book.title,
                      summary: book.summary,
                      author: book.author,
                      price: book.price,
                      pub_date: book.pub_date,
                    };
                return resultBook;
              });
              return res.status(StatusCodes.OK).json(books);
            }
          });
        }
      };
        
      /**
       * 개별 도서 조회 로직
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       * @param {import("express").NextFunction} next
       */
      const searchOneBook = (req, res, next) => {
        const { bookId } = req.params;
        const token = req.token;
        const userId = token?.id;
        let sqlQuery = token
          ? `
        SELECT *,
        (SELECT COUNT(*) FROM likes WHERE book_id = books.id) AS likes,
        (SELECT EXISTS (SELECT * FROM likes WHERE user_id=? AND book_id=?)) AS liked
        FROM books
        LEFT JOIN category
        ON books.category_id = category.category_id
        WHERE books.id= ?;
        `
          : `
        SELECT *,
        (SELECT COUNT(*) FROM likes WHERE book_id = books.id) AS likes
        FROM books
        LEFT JOIN category
        ON books.category_id = category.category_id
        WHERE books.id= ?;
        `;
        
        let queryArg = token ? [+userId, +bookId, +bookId] : [+bookId, +bookId];
        dbConnection.query(sqlQuery, queryArg, (err, results) => {
          if (err) {
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
          }
        
          const book = results[0];
          if (book) {
            return res.status(StatusCodes.OK).json(book);
          } else {
            return res.status(StatusCodes.NOT_FOUND).end();
          }
        });
      };
        
      /**
       * 1달 이내 출간된 신간 도서 조회
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       * @param {import("express").NextFunction} next
       */
      const getNewBooks = (req, res, next) => {
        const { limit, currentpage } = req.query;
        const offset = +limit * (+currentpage - 1);
        const token = req.token;
        
        let sqlQuery = token
          ? `
        SELECT *, (SELECT COUNT(*) from likes WHERE book_id = books.id) AS likes FROM bookkio.books
        WHERE pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()
        LIMIT ? OFFSET ?
        `
          : `
        SELECT * FROM bookkio.books
        WHERE pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()
        LIMIT ? OFFSET ?
        `;
        
        dbConnection.query(sqlQuery, [+limit, offset], (err, results) => {
          if (err) {
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
          }
        
          if (results.length > 0) {
            return res.status(StatusCodes.OK).json(results);
          } else {
            return res.status(StatusCodes.NOT_FOUND).end();
          }
        });
      };
        
      module.exports = {
        searchBooks,
        searchOneBook,
        getNewBooks,
      };
        
    

    books.controller.js의 함수들에는 대체적으로 사용자 인증여부에 따른 정보 전달의 제한은 두지만 사용은 가능하도록 수정하였다.

주문하기 API 사용자 인증 추가

주문과 관련된 API는 사실 사용자 인증을 받지 않고는 접근을 허용하면 안되는 API 중 하나이다.

비회원 주문과 같은 기능들이 있는 사이트가 존재 하지만, 현재 프로젝트에서 비회원 관리 까지는 어렵기 때문에,

회원일 경우에만 주문을 할 수 있도록 구조를 개선 하였다.

앞서 사용하던 checkAuth 함수 (JWT 검증) 를 통해 orders.route.js에서 무조건 검증을 걸치게 하면 생각보다 더 쉽게

검증과정을 진행 시킬 수 있다.

  • orders.route.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
      const express = require("express");
      const router = express.Router();
        
      const {
        getOrderDetail,
        getOrderList,
        orderItems,
      } = require("../controller/orders.controller.js");
      const { checkAuth } = require("../utils/auth.js");
        
      // 사용자 인증
      router.use(checkAuth);
        
      // 주문하기 API
      router.post("/", orderItems);
        
      // 주문 목록조회  API
      router.get("/", getOrderList);
      router.get("/:orderId", getOrderDetail);
        
      module.exports = router;
    

위의 사용자 인증 과정이 추가 됨에 따라, 정상적인 사용자 인증 성공일 경우, Request의 token에 값이 추가 되어있을 것이 기 때문에, userId와 같은 값들은 token 에서 추출하도록 orders.contoller.js 코드를 수정하였다.

  • orders.controller.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    
      const dbConnection = require("../model/mysql.js");
      const { StatusCodes } = require("http-status-codes");
      const { extractId } = require("../utils/auth.js");
        
      // const createQueryFormat = (query, queryArgument) => {
      //   let format;
      //   format = dbConnection.format(query, queryArgument);
      //   return format;
      // };
        
      /**
       * 주문하기 API
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       */
      const orderItems = (req, res) => {
        const { items, delivery, totalPrice, totalQty } = req.body;
        const userId = extractId(req);
        console.log({
          items,
          delivery,
          totalPrice,
          totalQty,
          userId,
        });
        
        // delivery INSERT query
        let sqlQuery1 = `
        INSERT INTO delivery (address, receiver, contact)
        VALUES (?,?,?);
        `;
        const queryArg1 = [delivery.address, delivery.receiver, delivery.contact];
        const format1 = dbConnection.format(sqlQuery1, queryArg1); // => String (쿼리문)
        
        console.log("포맷 1");
        console.log(format1);
        
        // oreders INSERT query
        let sqlQuery2 = `
        INSERT INTO orders (book_title, total_price, total_qty, user_id, delivery_id)
        VALUES((SELECT title FROM books WHERE id = ?),?,?,?,(SELECT MAX(id) FROM delivery));
        `;
        const queryArg2 = [items[0].bookId, totalPrice, totalQty, userId];
        
        const format2 = dbConnection.format(sqlQuery2, queryArg2);
        console.log("포맷 2");
        console.log(format2);
        
        // orderedBook INSERT query
        let sqlQuery3 = `
          INSERT INTO orderedbook (order_id, book_id, qty)
          VALUES((SELECT MAX(id) FROM orders), ?, ?);
        `;
        let format3 = "";
        
        items.forEach((item) => {
          format3 += dbConnection.format(sqlQuery3, [item.bookId, item.qty]);
        });
        console.log("포맷 3");
        console.log(format3);
        
        // cartItem DELETE query
        let sqlQuery4 = `
          DELETE FROM cartitems
          WHERE id=?;
        `;
        
        let format4 = "";
        
        items.forEach((item) => {
          format4 += dbConnection.format(sqlQuery4, item.cartItemId);
        });
        
        console.log("포맷 4");
        console.log(format4);
        
        dbConnection.query(format1 + format2 + format3 + format4, (err, result) => {
          if (err) {
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
          }
        
          if (
            result[0].affectedRows > 0 &&
            result[1].affectedRows > 0 &&
            result[2].affectedRows > 0 &&
            result[3].affectedRows > 0
          ) {
            return res.status(StatusCodes.OK).json({
              deliveryResult: result[0],
              ordersResult: result[1],
              orderedBookResult: result[2],
              deleteCartItemResult: result[3],
            });
          } else {
            console.log(err);
            return res
              .status(StatusCodes.INTERNAL_SERVER_ERROR)
              .json({ message: "오류가 발생하였습니다." });
          }
        });
      };
        
      /**
       * 전체 주문 목록 조회 API
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       * @returns
       */
      const getOrderList = (req, res) => {
        const userId = extractId(req);
        
        let sqlQuery = `
        SELECT orders.id AS order_id,
        orders.book_title AS order_book_title,
        orders.total_qty AS order_total_qty,
        orders.total_price AS order_total_price,
        orders.created_at,
        orders.user_id,
        orders.delivery_id,
        delivery.address AS delivery_address,
        delivery.receiver AS delivery_receiver,
        delivery.contact AS delivery_contact
        FROM orders LEFT 
        JOIN delivery ON delivery.id = orders.delivery_id
        WHERE orders.user_id = ?;
        `;
        let queryArg = [+userId];
        
        dbConnection.query(sqlQuery, queryArg, (err, result) => {
          if (err) {
            console.log(err);
            return res
              .status(StatusCodes.BAD_REQUEST)
              .json({ message: "잘못된 주문 목록 조회 요청입니다.", error: err });
          }
        
          const userOrders = result[0];
          console.log(userOrders);
          if (userOrders) {
            const resultJson = {
              orderId: userOrders.order_id,
              created_at: userOrders.created_at,
              delivery: {
                address: userOrders.delivery_address,
                receiver: userOrders.delivery_receiver,
                contact: userOrders.delivery_contact,
              },
              totalPrice: userOrders.order_total_price,
              bookTitle: userOrders.order_book_title,
              totalQty: userOrders.order_total_price,
            };
            return res.status(StatusCodes.OK).json(resultJson);
          } else {
            return res
              .status(StatusCodes.NOT_FOUND)
              .json({ message: "주문한 목록이 없습니다." });
          }
        });
      };
        
      /**
       * 주문 상품 상세 조회 API
       * @param {import("express").Request} req
       * @param {import("express").Response} res
       * @returns
       */
      const getOrderDetail = (req, res) => {
        const { orderId } = req.params;
        let sqlQuery = `
        SELECT books.id AS book_id,
        books.price AS book_price,
        books.title AS book_title,
        books.author AS book_author,
        orderedbook.qty AS order_qty
        FROM orderedBook LEFT 
        JOIN books ON books.id = orderedbook.book_id
        WHERE orderedbook.order_id = ?;
        `;
        let queryArg = [+orderId];
        
        const format = dbConnection.format(sqlQuery, queryArg);
        
        dbConnection.query(format, (err, results) => {
          if (err) {
            console.log(err);
            return res.status(StatusCodes.BAD_REQUEST).end();
          }
        
          const orderDetails = [...results];
          console.log(orderDetails);
        
          if (orderDetails.length > 0) {
            const resultJson = orderDetails.map((item) => {
              return {
                bookId: item.book_id,
                bookTitle: item.book_title,
                author: item.book_author,
                price: item.book_price,
                qty: item.order_qty,
              };
            });
            return res.status(StatusCodes.OK).json(resultJson);
          } else {
            return res
              .status(StatusCodes.NOT_FOUND)
              .json({ message: "해당하는 주문을 찾지 못했습니다." });
          }
        });
      };
        
      module.exports = {
        orderItems,
        getOrderList,
        getOrderDetail,
      };
        
    

오늘은 위와 같이 사용자 인증을 받은 사용자만 사용할 수 있는 기능을 구분하여 기존에 작성하였던 API를 수정 하였다.

실제 router.use를 통해서 생각보다 쉽게 진행 할 수 있었고, 작성된 routing의 순서가 왜 중요한지를 새삼 깨닫게 된 것 같다.

실무에서는 token에 또 다른 어떠한 값을 담아서 사용하는지 아직은 모르겠지만, 위와 같은 방식으로는 나도 이제 백엔드로 간단한 인증절차는 개발 할 수 있을 것 같다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.