API 명세서
과제를 시작하기에 어떤 API를 만들껀지 어떤 request와 response를 주고 받을건지 등등
하나 만들때마다 생각하고 고민하고 하는건 시간 효율도 떨어지고 과제제출 시기도 임박했기에
API명세서를 먼저 작성하고 시작했다.
처음 만들어보다보니 이게 맞나..싶은 느낌이 들었지만 일단 써보기라도 하자! 라는 느낌으로 작성했다.
막상 명세서를 짜고 코드를 작성하는데도 명세서 작성시에는 예상못했던 부분들도있고 명세에는 작성했지만 구현하지못한 API들도 많긴하지만 어느정도 뭐가 필요했지? 싶은 부분에서 바로바로 명세서만 보면되서 시간효율은 좋았던것 같다.
작성은 Notion을 사용했다.API명세서링크회원가입 로그인
아이템 시뮬레이터 과제정리
① 회원가입 API
회원가입 API Code
import express from "express"; import { prisma } from "../utils/prisma/index.js"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; const env = process.env; const router = express.Router(); /* 사용자 회원가입 API */ router.post("/sign-up", async (req, res, next) => { const { userId, userPassword, checkPassword, userName } = req.body; try { //비밀번호 일치여부 확인 if ( !userPassword || !checkPassword || userPassword !== checkPassword || userPassword.length < 6 ) { return res.status(400).json({ message: "비밀번호가 일치하지 않습니다." }); } //사용자 존재여부 확인 const isExistUser = await prisma.account.findUnique({ where: { userId, }, }); if (isExistUser) { return res.status(409).json({ message: "이미 존재하는 아이디입니다." }); } const isExistUserName = await prisma.account.findUnique({ where: { userName }, }); if (isExistUserName) { return res.status(409).json({ message: "이미 존재하는 이름입니다." }); } //비밀번호 해시화 const hashedPassword = await bcrypt.hash(userPassword, 10); // Users 테이블에 사용자를 추가합니다. const user = await prisma.account.create({ data: { userId, userPassword: hashedPassword, userName, }, }); return res.status(201).json({ message: "회원가입이 완료되었습니다.", accountId: user.accountId, userId: user.userId, userName: user.userName, }); } catch (error) { return res.status(500).json({ message: "서버 오류가 발생하였습니다." }); } });
■ 코드 분석
ⓘ.라우터 정의
router.post("/sign-up", async (req, res, next) => { }
ⓙ . 클라이언트에서 데이터 받아오기
회원가입시 필요한 데이터를 클라이언트에서 입력한 값(req.body)을 받아온다.const { userId, userPassword, checkPassword, userName } = req.body;
ⓚ . 비밀번호 검사
userPassword와 chackPassword가 일치하는지 6글자 보다 짧은지 혹은 입력하지 않았는지를 체크하고
|| 연산자를 이용해 그중 하나라도 충족하지 못한다면 상태코드400을 넣어서 실패메시지를 보내준다.//비밀번호 일치여부 확인 if ( !userPassword || !checkPassword || userPassword !== checkPassword || userPassword.length < 6 ) { return res.status(400).json({ message: "비밀번호가 일치하지 않습니다." }); }
ⓛ . 이미 존재하는지 존재여부확인
사용자의 아이디와 이름이 이미 데이터베이스 상에 존재한다면 중복코드(409)와 함께 실패메시지를 보내준다.
그를 위해 account테이블에 userId값과 일치하는지 찾아보는 코드를 isExistUser 에 할당시킨다.//사용자 ID 존재 여부 검사 const isExistUser = await prisma.account(검사하려는 테이블명).findUnique(특정 조건의 고유한값을조회)({ where: { userId, }, });
db에 userId가 일치하는지 값을 조회하는 코드
이를 통해 얻은값을 조건문을 사용해 이미 있다면 실패메시지를 보내준다.if (isExistUser) {//db에 isExistUser 가 있다면 return res.status(409).json({ message: "이미 존재하는 아이디입니다." }); }
사용자의 이름도 똑같은 방식으로 검사한다.//사용자 Name 존재 여부 검사 const isExistUserName = await prisma.account.findUnique({ where: { userName }, });
있을경우 실패if (isExistUserName) { return res.status(409).json({ message: "이미 존재하는 이름입니다." }); }
ⓜ . 비밀번호 해시화
비밀번호는 보안을위해? 단방향 암호화를 통해 해시화시켜 db에 저장시키기로했다.
bcrypt 라이브러리를 사용해 간단하게 구현했다.
입력받은 userPassword를 bcrypt.hash메서드를 사용해 10번 볶아주고 hashedPassword에 할당한다.//입력받은 비밀번호 해시화 const hashedPassword = await bcrypt.hash(userPassword, 10);
ⓝ . 테이블에 사용자 정보 저장
이전의 모든 절차를 거쳐서 살아남은 데이터를create메서드를 사용해 Account 테이블에 저장한다.//사용자 정보 db에 추가! const user = await prisma.account.create({ data: { userId, userPassword: hashedPassword, //비밀번호는 해시화한값을 저장! userName, }, });
저장에 성공한다면 생성코드(201)과 함께 회원가입성공 메시지와 유저의 정보를 반환해준다.
막약 문제가 생긴다면 catch를 통해 서버오류코드(500) 과 함께 실패메시지를 보내준다.return res.status(201).json({ message: "회원가입이 완료되었습니다.", accountId: user.accountId, userId: user.userId, userName: user.userName, }); } catch (error) { return res.status(500).json({ message: "서버 오류가 발생하였습니다." }); }
② . 로그인 API
로그인 API Code
/*사용자 로그인 페이지*/ router.post("/login", async (req, res, next) => { const { userId, userPassword } = req.body; try { //사용자 아이디 확인 const user = await prisma.account.findUnique({ where: { userId: userId, }, }); //사용자 존재 여부 확인 if (!user) { return res .status(401) .json({ message: "아이디 또는 비밀번호가 일치하지 않습니다." }); } //비밀번호 확인 const passwordValid = await bcrypt.compare(userPassword, user.userPassword); if (!passwordValid) { return res .status(401) .json({ message: "아이디 또는 비밀번호가 일치하지 않습니다." }); } //jwt 토큰 생성 const JWT_ACCESS_TOKEN = jwt.sign( { userId: user.userId }, env.JWT_TOKEN_SECRETKEY, { expiresIn: "1h" }, ); res.header("authorization", `Bearer ${JWT_ACCESS_TOKEN}`); return res.status(200).json({ message: "로그인에 성공했습니다.", JWT_ACCESS_TOKEN: JWT_ACCESS_TOKEN, }); } catch (error) { return res.status(500).json({ message: "서버 오류가 발생하였습니다." }); } });
■ 코드 분석
ⓐ . 라우터 정의
router.post("/login", async (req, res, next) => { }
ⓑ . 클라이언트에서 데이터 받아오기
로그인에 필요한 userId와 userPassword의 입력값을 받아온다.const { userId, userPassword } = req.body;
ⓒ . 사용자 아이디 확인
db에있는 고유한 userId를 조회한다.const user = await prisma.account.findUnique({ where: { userId: userId, }, });
조회한 userId를 기반으로 db에 userId가 없다면 인증실패코드(401)과함께 실패메시지를 보내준다.
if (!user) { return res .status(401) .json({ message: "아이디 또는 비밀번호가 일치하지 않습니다." }); }
ⓓ . 비밀번호 확인
클라이언트에서 입력한 userPassword와 해시화하여 저장한 userPassword를 비교한다.
두 값이 일치하지 않는다면 인증실패코드(401)과함께 실패메시지를 보내준다.//비밀번호 확인 const passwordValid = await bcrypt.compare(userPassword, user.userPassword); if (!passwordValid) { return res .status(401) .json({ message: "아이디 또는 비밀번호가 일치하지 않습니다." }); }
★ 해시화한 비밀번호 비교방법
사용자가 로그인을 시도할 때 입력한 비밀번호를 해시화한다.
그 결과로 나온 해시 값을 데이터베이스에 저장된 해시 값과 비교한다.
두 해시 값이 일치하면 비밀번호가 일치하는 것으로 판단한다.
ⓔ . JWT 토큰 발행
모든 과정을 넘긴다면 jwtwebtoken 라이브러리의 메서드 jwt.sign을 이용해 JWT토큰을 발행한다.
토큰의 payload값에 userId를 넣어주고 보안을 위해 1시간후 만료되도록 설정한다.const JWT_ACCESS_TOKEN = jwt.sign( { userId: user.userId }, env.JWT_TOKEN_SECRETKEY, { expiresIn: "1h" }, );
★userId:user.userId = 가장 처음에 조회한 account의 userId값을담은 user안에서 찾기때문이다..아마도..
JWT_TOKEN_SECRETKEY는 dotenv 라이브러리를 이용하여 .env파일에 있는값을 가져와서 사용했다.
이렇게 생성한 JWT 토큰을 베어러(Bearer) 형식으로 HTTP 헤더에 보내준다.res.header("authorization", `Bearer ${JWT_ACCESS_TOKEN}`);
ⓕ 로그인 성공!
토큰 발급이 끝난다면 OK코드(200)과함께 성공메시지와 토큰을 보내준다. (헤더로 토큰을 보내서 딱히 토큰은 안보내줘도 됬을꺼같다.)return res.status(200).json({ message: "로그인에 성공했습니다.", JWT_ACCESS_TOKEN: JWT_ACCESS_TOKEN, });
위 과정에서 서버에 문제가 생긴다면 catch를 사용해 서버오류코드(500)과 에러메시지catch (error) { return res.status(500).json({ message: "서버 오류가 발생하였습니다." }); }
③ . JWT 토큰 검증 미들웨어
검증 미들웨어 Code
import jwt from "jsonwebtoken"; import { prisma } from "../utils/prisma/index.js"; const env = process.env; export default async (req, res, next) => { try { const authHeader = req.headers["authorization"]; if (!authHeader) { throw new Error("요청한 사용자의 토큰이 존재하지 않습니다."); } const [tokenType, JWT_ACCESS_TOKEN] = authHeader.split(" "); if (tokenType !== "Bearer") throw new Error("토큰 타입이 Bearer가 아닙니다."); const decodedToken = jwt.verify(JWT_ACCESS_TOKEN, env.JWT_TOKEN_SECRETKEY); const userId = decodedToken.userId; const user = await prisma.account.findUnique({ where: { userId: userId }, }); if (!user) { throw new Error("토큰 사용자가 존재하지 않습니다."); } req.user = user; next(); } catch (error) { return res.status(401).json({ message: error.message }); } };
■ 코드 분석
① . 헤더로 토큰 받아오기
req.headers에서 Bearer토큰을 받아와야하므로 HTTP 헤더 중 하나인["authorization"]를 사용한다.
const authHeader = req.headers["authorization"];
여기서 문제가생겨서 한참 끙끙거렸는데 로그인을 성공할 경우 토큰이 헤더로 전달되긴하는데 이 토큰의 값이 authHeader값으로 전달하는 방법을 몰라서 값이자꾸 undefined가 출력되는게 문제였다.
이 문제는 어이없게도 VSCode가 아니라 insomnia에 Auth 부분에 직접 입력하는 거였다..저렇게 넣어주니 잘만 들어간다.. 토큰 입력창 console.log(authHeater)출력값
만약 이 과정에서 토큰이없다면(로그인 안한경우) throw 에러를 출력하여 토큰이없음을 알려준다.
if (!authHeader) { throw new Error("요청한 사용자의 토큰이 존재하지 않습니다."); }
이 경우
ⓑ . 토큰 검증
authHeater 값을 spilt메서드를 이용해 공백을 기준으로 나누어준다.
그 값을 구조분해할당을통해 토큰타입은 tokenType에 토큰의값은 JWT_ACCESS_TOKEN 에 할당한다.
[tokenTypq:Bearer , JWT_ACCESS_TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.] 이런식으로 할당된다.
const [tokenType, JWT_ACCESS_TOKEN] = authHeader.split(" ");
여기서 만약 tokenType이 Bearer 가 아니라면 throw에러메시지를 보내준다.if (tokenType !== "Bearer") throw new Error("토큰 타입이 Bearer가 아닙니다.");
Bearer 타입이 맞다면 jwt.verify메서드를 통해 토큰을 디코딩 한다.
디코딩에 성공한다면 디코딩된 토큰에 정보를 포함하는 객체를 반환하는데 이 객체는 payload에 해당하는 정보에 접근할수 있게해준다.
디코딩된 토큰 객체에서 userId속성을 추출하여 변수에 저장한다. 이 값은 JWT를 생성할 때 포함된 userId를 나타낸다.
const decodedToken = jwt.verify(JWT_ACCESS_TOKEN, env.JWT_TOKEN_SECRETKEY); const userId = decodedToken.userId;
이제 다시 db에 이 userId에 해당하는 정보가 있는지 조회한후 값이 없다면 there 에러메시지
값이 있다면 req.user에 요청객체(req)에 user속성을 추가하여, 이후의 미들웨어나 라우터에서 쉽게 접근할 수 있도록 한다. 이 속성에는 db에서 조회한 userId가 저장된다.
const user = await prisma.account.findUnique({ where: { userId: userId }, }); if (!user) { throw new Error("토큰 사용자가 존재하지 않습니다."); } req.user = user; next(); } catch (error) { return res.status(401).json({ message: error.message }); }
이 과정에서 토큰이 있다면 next()를 사용해 다음 미들웨어나 라우터로 넘어가고
문제가 있다면 catch를 통해 에러메시지를 출력한다
④ . 캐릭터 생성
캐릭터 생성 API Code
/*캐릭터 생성 API*/ router.post("/characters", authMiddleware, async (req, res, next) => { const { characterName } = req.body; const accountId = req.user.accountId; try { //캐릭터 이름 중복 체크 const existsCharacter = await prisma.characters.findUnique({ where: { characterName: characterName, }, }); if (existsCharacter) { return res .status(400) .json({ message: "이미 존재하는 캐릭터명 입니다." }); } //새로운 캐릭터 생성 const newCharacter = await prisma.characters.create({ data: { characterName: characterName, accountId, }, }); return res.status(200).json({ message: "캐릭터 생성이 완료되었습니다.", character: newCharacter, }); } catch (error) { return res.status(500).json({ message: "서버 오류가 발생하였습니다." }); } });
■ 코드 분석
ⓐ . 라우터 정의
router.post("/characters", authMiddleware, async (req, res, next) => { }
ⓑ . 클라이언트에서 데이터 받아오기
만들고자 하는 캐릭터의 이름을 입력한다.
const { characterName } = req.body;
캐릭터를 만드려고 하는 유저의 아이디를 받아온다.
이는 jwt미들웨어에서 정의한 req.user를 통해 받아온다.const accountId = req.user.accountId;
ⓒ . 중복 검사
캐릭터의 이름이 이미 데이터 베이스안에 존재한다면 중복코드(409) 를 보내준다.
//캐릭터 이름 중복 체크 const existsCharacter = await prisma.characters.findUnique({ where: { characterName: characterName, }, }); if (existsCharacter) { return res .status(400) .json({ message: "이미 존재하는 캐릭터명 입니다." }); }
ⓓ . 새로운 캐릭터 생성
위의 과정을 거쳐 문제가 없다면 OK코드(200)과 함께 새로운 캐릭터를 생성한다.
하지만 문제가 발생했다면 서버오류코드(500)을 보내준다.const newCharacter = await prisma.characters.create({ data: { characterName: characterName, accountId, }, }); return res.status(200).json({ message: "캐릭터 생성이 완료되었습니다.", character: newCharacter, }); } catch (error) { return res.status(500).json({ message: "서버 오류가 발생하였습니다." }); }
⑤ . 캐릭터 삭제
캐릭터 삭제 API Code
/*캐릭터 삭제 API*/ router.delete( "/characters/:characterId", authMiddleware, async (req, res, next) => { const { characterId } = req.params; const { accountId } = req.user; try { //캐릭터 조회 const character = await prisma.characters.findFirst({ where: { characterId: +characterId }, }); //캐릭터가 존재하지 않을시 if (!character) { return res.status(404).json({ message: "캐릭터가 존재하지 않습니다." }); } //캐릭터의 계정ID와 사용자의 계정ID가 일치하는지 확인 if (character.accountId !== accountId) { return res.status(403).json({ message: "본인의 캐릭터가 아닙니다." }); } //캐릭터 삭제 await prisma.characters.delete({ where: { characterId: +characterId }, }); return res.status(200).json({ message: "캐릭터가 삭제되었습니다." }); } catch (error) { throw new Error("서버 오류가 발생했습니다."); } }, );
■ 코드 분석
ⓐ . 라우터 정의router.delete("/characters/:characterId",authMiddleware,async (req, res, next) => { }
ⓑ . URL을 통해 데이터 받아오기
삭제하고자 하는 캐릭터의 id를 url 을 통해 params로 받아온다.
const { characterId } = req.params;
미들웨어에서 정의한 req.user에서 캐릭을 지우려하는 유저또한 받아온다.
const { accountId } = req.user;
ⓒ . 캐릭터 조회
삭제할 캐릭터가 존재하는지 파악해야하기 때문에 데이터 베이스에서 조회한다.const character = await prisma.characters.findFirst({ where: { characterId: +characterId }, });
ⓓ . 캐릭터삭제시 에러//캐릭터가 존재하지 않을시 if (!character) { return res.status(404).json({ message: "캐릭터가 존재하지 않습니다." }); }
//캐릭터의 계정ID와 사용자의 계정ID가 일치하지 않을시 if (character.accountId !== accountId) { return res.status(403).json({ message: "본인의 캐릭터가 아닙니다." }); }
ⓔ . 캐릭터 삭제
위 과정에 아무런 문제가 없을시 캐릭터를 삭제한다.//캐릭터 삭제 await prisma.characters.delete({ where: { characterId: +characterId }, }); return res.status(200).json({ message: "캐릭터가 삭제되었습니다." }); } catch (error) { throw new Error("서버 오류가 발생했습니다."); } },
'부트캠프' 카테고리의 다른 글
Nalgangdoo_Project 2일차 (0) | 2024.09.19 |
---|---|
Nalgangdoo_project 1일차 (1) | 2024.09.13 |
EC2접속 , 로컬과 AWS EC2 간 파일 올리기, 내려받기 (1) | 2024.09.12 |
OSI 3계층 , 전송 계층 (1) | 2024.09.11 |
OSI 3계층 , 네트워크 계층 (0) | 2024.09.09 |