TiL#25. 쿠키, JWT, 세션

깡통·2024년 1월 29일
0
  • category
  • app.js
import express from "express";
import cookieParser from "cookie-parser";
import UsersRouter from "./routes/users.router.js";
import PostsRouter from "./routes/posts.router.js";
import CommentsRouter from "./routes/posts.router.js";
import LogMiddleware from "./middlewares/log.middleware.js";
import ErrorHandlerMiddleware from "./middlewares/error-handler.middleware.js";
import expressSession from "express-session";
import expressMySQLSession from "express-mysql-session";
import dotenv from "dotenv";

dotenv.config();

const app = express();
const PORT = 4501;

const MySQLStore = expressMySQLSession(expressSession);
//외부 세션 스토어 정의 및 기본 사항 정의
const sessionStore = new MySQLStore({
  user: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
  host: process.env.DATABASE_URL,
  port: process.env.DATABASE_PORT,
  database: process.env.DATABASE_NAME,
  //유효기간 지정
  expiration: (1000 * 60 * 60) & 24, //1일
  //해당하는 데이터베이스가 없을 시 신설할 지 여부
  createDatabaseTable: true,
});

app.use(LogMiddleware); // 얘는 가장 앞에 띄워야 함
app.use(express.json());
app.use(cookieParser());
app.use(
  expressSession({
    secret: process.env.SESSION_SECRET_KEY, // 세션을 암호화하는 비밀 키를 설정
    resave: false, // 클라이언트의 요청이 올 때마다 세션을 새롭게 저장할 지 설정, 변경사항이 없어도 다시 저장
    saveUninitialized: false, // 세션이 초기화되지 않았을 때 세션을 저장할 지 설정
    cookie: {
      // 세션 쿠키 설정
      maxAge: 1000 * 60 * 60 * 24, // 쿠키의 만료 기간을 1일로 설정합니다.(1000ms * 60s * 60m * 24h)
    },
    //expressSession에 인메모리 대신 외부 저장소에 세션 정보를 저장하겠다고 통보
    store: sessionStore,
  })
);
app.get("/", (req, res) => {
  return res.status(201).json({ message: "국밥이 배달되었습니다." });
});

app.use("/api", [UsersRouter, PostsRouter, CommentsRouter]);

//에러 처리 미들웨어는 app.js의 가장 아랫 부분에 있어야 함(에러를 최종 처리하는 미들웨어니까)

app.use(ErrorHandlerMiddleware);

app.listen(PORT, () => {
  console.log(PORT, "포트로 실행되었습니다.");
});
  • users.router.js
import express from "express";
import { prisma } from "../utils/prisma/index.js";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import authMiddleware from "../middlewares/auth.middleware.js";
import { Prisma } from "@prisma/client";

const app = express();
app.use(express());
const router = express.Router();

//회원가입
router.post("/sign-up", async (req, res, next) => {
  try {
    const { email, password, name, age, gender, profileImage } = req.body;

    //중복 검사
    const isExistUser = await prisma.findFirst({
      where: { email },
    });

    if (isExistUser) {
      return res.status(409).json({ message: "이미 존재하는 회원정보입니다." });
    }

    //interactive transaction
    const [user, userInfo] = await prisma.$transaction(
      async (tx) => {
        //bcrypt를 통한 암호화
        const hashedPassword = await bcrypt.hash(password, 10);
        const user = await tx.users.create({
          data: {
            email,
            password: hashedPassword,
          },
        });

        const userInfo = await tx.userInfos.create({
          data: {
            userId: user.userId,
            name,
            age,
            gender,
            profileImage,
          },
        });

        return [user, userInfo];
      },

      //격리수준 설정(커밋된 데이터만 읽기만 할 수 있음)
      {
        isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted,
      }
    );
    return res.status(201).json({ message: "회원가입이 완료되었습니다." });
  } catch (err) {
    next(err);
  }
});

//로그인;
router.post("/sign-in", async (req, res, next) => {
  const { email, password } = req.body;
  const user = await prisma.users.findFirst({
    where: {
      email,
    },
  });

  if (!user)
    return res.status(401).json({
      message: "해당 사용자를 찾을 수 없습니다.",
    });

  //bcrypt를 통해 전달 받은 비밀번호와 db에 저장된 비밀번호 검증
  if (!(await bcrypt.compare(express, user.password)))
    return res.status(401).json({ message: "비밀번호가 일치하지 않습니다." });

  // + 리팩토링 전-jwt(jwt 할당 부분 - 이렇게 하면 클라이언트한테 jwt 로그인 정보가 그대로 전달됨) +
  // const token = jwt.sign({ userId: user.userId }, "custom-secret-key");
  // res.cookie("authorization", `Bearer ${token}`);

  //+ 리팩토링 후-세션 +
  //이렇게 하면 서버가 자체적으로 쿠키 정보를 가지고 비교하여 인증을 절차를 처리함, 클라이언트에게는 이전과 같은 jwt 쿠키 정보가 가지 않고, 내부적으로 처리를 완료한 후 사용자의 세션 정보에 userId를 할당하게 됨
  req.session.userId = user.userId;
  return res.status(200).json({ message: "로그인에 성공하였습니다." });
});

//사용자 조회 API
//라우터 경로로 들어왔다가, 사용자 인증 미들웨어를 통과해서, 비즈니스 로직을 수행함
router.get("/users", authMiddleware, async (req, res, next) => {
  const { userId } = req.user;

  const user = await prisma.users.findFirst({
    where: {
      userId: +userId,
    },
    select: {
      userId: true,
      email: true,
      createdAt: true,
      updatedAt: true,
      //schema.prisma에서 관계를 설정해 놨기 때문에 가능했던 것, mySQL의 JOIN 문법과 비슷
      userInfos: {
        select: {
          name: true,
          age: true,
          gender: true,
          profileImage: true,
        },
      },
    },
  });
});

router.patch("/users", authMiddleware, async (req, res, next) => {
  // 이번엔 객체 구조분해 할당이 아닌 다른 방식으로 감(스프레드로 뿌리기)
  const updatedData = req.body;
  // const { name, age, gender, profileImage } = req.body;
  //authMiddleware를 통과하면 req.user에 userId가 저장됨, 그 유저 아이디를 이 api에서 사용할 수 있도록 변수에 할당
  const { userId } = req.user;

  const userInfo = await prisma.userInfos.findFirst({
    where: { userId: +userId },
  });
  if (!userInfo) {
    return res.status(404).json({
      message: "사용자 정보가 존재하지 않습니다.",
    });
  }

  await prisma.$transaction(
    async (tx) => {
      await tx.userInfos.update({
        data: {
          ...updatedData,
        },
        where: {
          userId: +userId,
        },
      });

      //이 key가 뭐지
      for (let key in updatedData) {
        if (userInfo[key] !== updatedData[key]) {
          await tx.userHistories.create({
            data: {
              userId: +userId,
              changedField: key,
              oldValue: String(userInfo[key]),
              newValue: String(updatedData[key]),
            },
          });
        }
      }
    },
    {
      isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted,
    }
  );

  return res
    .status(200)
    .json({ message: "사용자 정보 변경에 성공하였습니다." });
});

export default router;
  • posts.router.js
import express from "express";
import { prisma } from "../utils/prisma/index.js"; //프리즈마 클라이언트를 통째로 들고 오는게 아니라, 프리즈마 모델을 생성해 놓은 하위폴더에서 필요한 부분들을 하나씩 들고 오도록 설정
import authMiddleware from "../middlewares/auth.middleware.js";

const router = express.Router();

//게시글 생성 API
router.post("/posts", async (req, res, next) => {
  const { title, content } = req.body;

  //authMiddleware에서 인증된 사용자 정보를 가져와 할당함
  //sign-in API를 미리 호출해서 사용자 정보를 저장해두지 않으면,
  //인증에 사용할 쿠키가 없어서 authMiddleware의 사용자 인증 로직을 통과하지 못해 에러가 남
  const { userId } = req.user;

  const post = await prisma.posts.create({
    data: {
      userId: +userId,
      title: title,
      content: content,
    },
  });

  return res.status(201).json({ data: post });
});

router.get("/posts", async (req, res, next) => {
  const posts = await prisma.posts.findMany({
    select: {
      postId: true,
      userId: true,
      title: true,
      updatedAt: true,
      updatedAt: true,
    },
    //정렬 메서드
    //생성날짜를 기준으로 내림차순 하겠다
    orderBy: {
      createdAt: "desc",
    },
  });

  return res.status(200).json({ data: posts });
});

router.get("/posts/:postId", async (req, res, next) => {
  const { postId } = req.params;

  const post = await prisma.posts.findFist({
    where: {
      postId: +postId,
    },
    select: {
      postId: true,
      userId: true,
      title: true,
      content: true,
      createdAt: true,
      updatedAt: true,
    },
  });
  return res.status(200).json({ data: post });
});

export default router;
  • comment.router.js
import express from "express";
import { prisma } from "../utils/prisma/index.js"; //프리즈마 클라이언트 모듈 전체가 아닌, 필요한 부분만 가져옴
import authMiddleware from "../middlewares/auth.middleware.js"; //사용자 인증 미들웨어

const router = express.Router();

//특정 게시글의 코멘트(authMiddleware = 해당 라우터 실행 시 authMiddleware가 실행되어 사용자 인증을 먼저 하고, 비즈니스 로직으로 넘어감 )
router.post(
  "/posts/:postId/comments",
  authMiddleware,
  async (req, res, next) => {
    const { postId } = req.params;
    const { content } = req.body;
    //authMiddleware를 통과하면서 req에 req.user로 저장된 userId를 가져옴
    const { userId } = req.user;

    const post = await prisma.posts.findFirst({
      where: { postId: +postId },
    });
    if (!post)
      return res.status(404).json({
        message: "게시글이 존재하지 않습니다.",
      });

    const comment = await prisma.comments.create({
      data: {
        postId: +postId,
        userId: +userId,
        content: content,
      },
    });
    return res.status(201).json({
      data: comment,
    });
  }
);

router.get("/post/:postId/comments", async (req, res, next) => {
  const { postId } = req.params;

  const comments = await prisma.comments.findMany({
    where: {
      postId: +postId,
    },
    //생성날짜 기준 내림차순 정렬
    orderBy: {
      createdAt: "desc",
    },
  });

  return res.status(201).json({
    data: comments,
  });
});

export default router;
  • log.middleware.js

  • auth.middleware.js
  1. JWT

  2. 세션

  3. 전문

import jwt from "jsonwebtoken";
import { prisma } from "../utils/prisma/index.js";

//사용자 인증 미들웨어는 express에 의존성이 존재하지 않아서 단순하게 함수명으로 만들면 된다?
export default async function (req, res, next) {
  try {
    // + jwt를 통해 사용자 인증을 처리하는 프로세스 +
    // const { authorization } = req.cookies;

    // if (!authorization)
    //   throw new Error("요청한 사용자의 토큰이 존재하지 않습니다.");

    // //authorization cookie가 "Bearer esdsfadsfdsf"<<이런식으로 구성 됨 그래서 이걸 ''를 기준으로 배열 구조분해 할당함
    // const [tokenType, token] = authorization.split("");

    // if (tokenType !== "Bearer")
    //   throw new Error("토큰타입이 Bearer 형식이 아닙니다.");

    // //token과 custom-secret-key가 일치하면(verify를 통과하면) 왼쪽의 decodedToken에 할당되고, 아니면 error 뜸
    // const decodedToken = jwt.verify(token, "custom-secret-key");
    // //users.router.js에 있는 토큰에서 userId 추출해 올 수 있음
    // const userId = decodedToken.userId;

    // + 세션을 통해 사용자 인증을 처리하는 프로세스 +

    const { userId } = req.session;

    if (!userId) throw new Error("로그인을 해주세요");

    const user = await prisma.users.findFirst({
      where: {
        userId: +userId,
      },
    });
    if (!user) {
      throw new Error("토큰 사용자가 존재하지 않습니다.");
    }
    //다음 미들웨어부터는 req.user에 실제 저장된 user 정보를 할당해서 사용 가능
    req.user = user;
    //throw 로 생성된 에러를 catch로 던져서 거기서 처리함
    next();
  } catch (error) {
    return res.status(400).json({
      message: error.message,
    });
  }
}
profile
코딩하러 온 사람입니다.

0개의 댓글