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