React-Native FE, NLP, DA, 기획 파트의 팀원들을 새롭게 맞이하고
2024년도 상반기 제품 상용화, 하반기 수익화를 위해 프로젝트를 재구성하였다.
추후 위 이미지 구조에 프로젝트를 진행하며 내용을 추가할 예정이다.
Github, Notion, Jira, Slack 등을 통해 협업을 진행할 예정이다.
현재 기획된 MVP 모델에서의 예상 API Chart이다.
OAuth, 일반 Auth를 구분하여 API를 구성하고 강의 데이터 구조가 확정되면 /lecture
라우트 하위에서 해당 데이터에 대한 처리를 진행할 예정이다.
또한 실시간 처리를 위한 Socket.io
의 서버 기준 on
, emit
처리는 위와 같다.
User
모델의 요구사항 명세서를 위와 같이 작성하였다.
강의 데이터를 저장할 Lecture
모델은 추후 작성할 예정이다.
/HEARUS-BACKEND
/controllers
로직을 구현하고 response를 보냄
/middlewares
HTTP req와 res 사이에서 단계별 동작을 수행
/models
Table Column들의 표현
/routers
특정 주소를 구현된 controller로 라우팅
app.js
API BE 서버를 구현하기 위해 프로젝트를 위와 같이 구성한다.
위 이미지와 같이 Hearus-Cluster를 구성하였다.
npm install mongodb bcrypt express-session mongoose
이후 MongoDB를 사용하기 위해 mongodb
, bcrypt
, mongoose
패키지를 설치한다.
const mongoose = require('mongoose');
// Connect MongoDB with mongoose
const uri = 'mongodb+srv://' +
process.env.MONGO_USERNAME + ':' +
process.env.MONGO_PASSWORD + '@' +
process.env.MONGO_HOST + '/' +
'test?retryWrites=true&w=majority';
mongoose.connect(uri, {
serverSelectionTimeoutMS: 5000
}).catch(err => console.log(err.reason))
.then(console.log("MongoDB Connected"));
위와 같이 환경변수로 USERNAME, PASSWORD, HOST를 connect시에 입력한다.
const mongoose = require('mongoose');
const userSchema = mongoose.Schema({
name: {
type: String,
maxlength: 50,
required: true,
},
email: {
// 중복 허용x, 사용자 로그인시 사용
type: String,
trim: true,
unique: 1,
required: true,
},
password: {
type: String,
minlength: 5,
required: true,
},
isOAuth: {
type: Boolean,
required: true,
},
OAuthType: {
type: String,
enum: ['kakao', 'google', 'naver'],
},
school: {
type: String,
required: true,
},
major: {
type: String,
required: true,
},
grade: {
// 1학년, 2학년, 3학년, 4학년, 휴학생, 졸업생, 대학원생
type: String,
enum: ['freshman', 'sophomore', 'junior', 'senior', 'leaveAbsense', 'graduate', 'postgraduate'],
required: true,
},
savedLectures: {
Array: {
type: String,
},
},
usePurpose: {
type: String,
enum: ['offline', 'online', 'conversation', 'else'],
required: true,
},
});
const User = mongoose.model("User", userSchema);
module.export = { User };
또한 이전 DB 요구사항 명세서를 위와 같이 mongoose를 활용하여 구현한다.
// controller/auth.js
const bycrpt = require('bcrypt');
const User = require('../models/user');
exports.signup = async (req, res, next) => {
// destructure req.body
const {
name, email, password, isOAuth, OAuthType,
school, major, grade, savedLectures, usePurpose,
} = req.body;
try {
const exUser = await User.findOne({ email: email });
if (exUser)
return res.status(409).json({ status: "fail", message: "User Already Exists" });
// hash password
const salt = await bycrpt.genSalt(10);
const hashedPW = await bycrpt.hash(password, salt);
// save newUser
const newUser = new User({
name, email, password: hashedPW, isOAuth, OAuthType,
school, major, grade, savedLectures, usePurpose,
});
await newUser.save();
return res.status(201).json({ status: "success", message: "Signup success" });
} catch (error) {
console.error(error);
return next(error);
}
}
auth
의 controller
에서 signup 함수를 위와 같이 구현하여 export한다.
// routes/auth.js
const express = require('express');
const { signup } = require('../controllers/auth');
const router = express.Router();
router.post('/signup', signup);
module.exports = router;
// app.js
const authRouter = require('./routes/auth');
...
app.use('/auth', authRouter);
이후 위와 같이 Router를 추가하여 준다.
위 이미지와 같이 JSON 형태로 Signup을 요청하면
MongoDB에 데이터가 정상적으로 들어가며 signup
기능이 구현된 것을 볼 수 있다.
보안을 강화하기 위해 위와 같이 AccessToken
과 RefreshToken
을 로그인시 발급한다.
이때 AccessToken
이 1h이후 만료되면 만료시간이 더 긴 RefreshToken
으로 재발급할 수 있도록 한다.
npm install jsonwebtoken
Token 방식의 Auth를 구현하기 위해 jsonwebtoken
패키지를 설치한다.
// controllers/auth.js
exports.login = async (req, res, next) => {
// Destructure req.body
const {
email, password, isOAuth, OAuthType,
} = req.body;
// OAuth
if (isOAuth)
return res.status(405).json({ status: "fail", message: "OAuth not Implemented" });
try {
// Find User
const exUser = await User.findOne({ email: email });
if (!exUser)
return res.status(401).json({ status: "fail", message: "Unknown User" });
// Match password
const matchPW = await bycrpt.compare(password, exUser.password);
if (!matchPW)
return res.status(401).json({ status: "fail", message: "Wrong Password" });
// Certify Tokens
const accessToken = jwt.sign({ userID: exUser._id }, process.env.JWT_ACCESS_SECRET, {
expiresIn: '1h',
});
const refreshToken = jwt.sign({ userID: exUser._id }, process.env.JWT_REFRESH_SECRET, {
expiresIn: '24h',
});
return res.status(201).json({
status: "success",
message: "Login Success",
accessToken: accessToken,
refreshToken: refreshToken,
});
} catch (error) {
console.error(error);
return next(error);
}
}
bycrpt
를 통해 암호화된 비밀번호를 검증하고 이후 JWT
를 발급하여 res로 전달한다.
// middlewares/jwtToken.js
exports.verifyAccessToken = (req, res, next) => {
const token = req.header('Authorization');
if (!token)
return res.status(401).json({ error: 'Access Denied' });
try {
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
req.userID = decoded.userID;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid Token' });
}
};
exports.verifyRefreshToken = (req, res, next) => {
const token = req.header('Authorization');
if (!token)
return res.status(401).json({ error: 'Access Denied' })
try {
const decoded = jwt.verify(token, process.env.JWT_REFRESH_SECRET);
req.userID = decoded.userID;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid Token' });
}
}
AccessToken
과 RefreshToken
를 서로 다른 SecretKey
로 verify하는 미들웨어를 작성한다.
// controllers/auth.js
exports.renewAccessToken = async (req, res, next) => {
try {
// Renew Access Token
const accessToken = jwt.sign({ userID: req.userID }, process.env.JWT_ACCESS_SECRET, {
expiresIn: '1h',
});
return res.status(201).json({
status: "success",
message: "Renew Access Token",
accessToken: accessToken,
});
} catch (error) {
console.error(error);
return next(error);
}
}
또한 만료된 AccessToken
를 재발급할 수 있는 로직을 작성한다.
// routes/auth.js
const express = require('express');
const { verifyToken } = require('../middlewares/jwtToken');
const { signup, login, renewAccessToken } = require('../controllers/auth');
const router = express.Router();
router.post('/signup', signup);
router.post('/login', login);
router.post('/renewToken', verifyToken, renewAccessToken);
module.exports = router;
최종적으로 controller를 router를 통해 붙여주면 JWT를 이용한 Token Auth 로직 구현이 완료된다.
Auth와 Token을 테스트하기 위해 추후 구현될 lecture
구조를 구현하였다.
// routes/lecture.js
var express = require('express');
const { verifyAccessToken } = require('../middlewares/jwtToken');
const { renderLecture } = require('../controllers/lecture');
var router = express.Router();
router.get('/', verifyAccessToken, renderLecture);
module.exports = router;
// controllers/lecture.js
exports.renderLecture = (req, res) => {
res.locals.userID = req.userID;
res.render('lecture');
};
lecture controller는 token에서 decoded된 userID
를 받아와서 view로 넘겨준다.
<!-- views/lecture.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
background-color: #121212;
margin: 0;
}
.background {
height: 100vh;
}
.card-container {
border-radius: 20px;
width: 30%;
height: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #1e1e1e;
}
.card-item {
margin: 15px;
}
.logo-img {
border-radius: 100px;
width: 150px;
}
.btn {
font-size: 20px;
border-radius: 10px;
background-color: #337ea9;
color: white;
}
.text {
font-size: 15px;
color: white;
}
</style>
</head>
<body>
<div class="background d-flex justify-content-center align-items-center vh-100">
<div class="card-container text-center">
<img src="/images/logo-red.png" alt="Logo" class="card-item logo-img mb-3" />
<p class="text">Protected Lecture API</p>
<p class="text">{{userID}}</p>
</div>
</div>
</body>
</html>
헤더에 토큰정보 없이 /lecture
에 요청한 결과이다.
토큰과 함께 요청하면 userID
를 뷰에서 받아오는 것을 볼 수 있다.
마지막으로 refreshToken
을 통해 요청하면 요청이 거부되는 것을 볼 수 있다.
accessToken
으로 요청하면 요청이 거부되며
refreshToken
으로 요청하면 새로운 accessToken
이 발급되는 것을 볼 수 있다.