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, "포트로 실행되었습니다.");
});
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;
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;
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;
JWT
세션
전문
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,
});
}
}