utils/hash-password.js
const crypto = require('crypto');
module.exports = (password) => {
const hash = crypto.createHash('sha1');
hash.update(password);
return hash.digest("hex");
}
암호화 라이브러리 crypto를 사용했다.
sh1 방식으로.
routes/index.js
const { Router } = require('express');
const asyncHandler = require('../utils/async-handler');
const { User } = require('../models');
const getHash = require('../utils/hash-password');
const router = Router();
router.post('/join', asyncHandler(async (req, res) => {
const { email, name, password } = req.body;
// 비밀번호 해쉬값 만들기
const hashedPassword = getHash(password);
const user = await User.create({
email, name, password:hashedPassword,
});
console.log('신규 회원', user);
res.redirect('/');
}));
hash-password.js 파일에서 만든 함수로 암호화를 해주고 있는 모습이다.
passport는 strategy를 설정해줘야 한다.
passport/strategies/local.js
const LocalStrategy = require('passport-local').Strategy;
const { User } = require('../../models');
const hashPassword = require('../../utils/hash-password');
const config = {
usernameField:'email', // 'email' 필드 사용하도록 설정
passwordField:'password', // 'password' 필드 사용하도록 설정
};
const local = new LocalStrategy(config, async (email, password, done) => {
try {
const user = await User.findOne({ email });
if (!user) {
throw new Error('회원을 찾을 수 없습니다.');
}
// 검색 한 유저의 비밀번호와 요청된 비밀번호의 해쉬값이 일치하는지 확인
if (user.password !== hashPassword(password)) {
throw new Error('비밀번호가 일치하지 않습니다.');
}
done (null, {
shortId: user.shortId,
email: user.email,
name: user.name,
});
} catch (err) {
done(err, null);
}
});
module.exports = local;
passport에서 done은 라우터의 next와 같은 역할을 한다.
passport/index.js
const passport = require('passport');
const local = require('./strategies/local');
module.exports = () => {
// local strategy 사용
passport.use(local);
passport.serializeUser((user, callback) => {
callback(null, user);
});
passport.deserializeUser((obj, callback) => {
callback(null, obj);
});
};
local strategy를 사용하겠다고 선언을 해주고, serializerUser와 deserializeUser를 선언해준다.
routes/auth.js
const { Router } = require('express');
const passport = require('passport');
const router = Router();
// passport local 로 authenticate 하기
router.post('/', passport.authenticate('local'), (req, res, next) => {
//req.user
res.redirect('/');
});
module.exports = router;
라우터에서 미들웨어로 passport를 추가해주는데, local strategy를 사용하게 authenticate도 같이써준다.
이렇게하면 req.user에 유저정보가 저장된다.
유저정보를 세션으로 저장하기 위해서는 mongoDB를 사용하는게 좋다.
그냥 쓰면 서버를 종료했을때 세션정보가 다 삭제되는데, mongoDB에 저장해두면 서버가 종료되어도 정보가 남아있기 때문이다.
app.js
const MongoStore = require('connect-mongo');
app.use(session({
secret: 'elice',
resave: false,
saveUninitialized: true,
// 세션 스토어 사용하기
store: MongoStore.create({
mongoUrl: 'mongoDB접속주소',
}),
}));
session설정에다가 추가로 적어줘야한다.
이게 좀 어렵다
User와 Post 모델을 연결하는 상황이다.
정확하게는, Post의 author에 User 정보를 join하는 것이다.
const { Schema } = require('mongoose');
const shortId = require('./types/short-id');
const PostSchema = new Schema({
shortId,
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
// index 추가하기
index: true,
},
}, {
timestamps: true,
});
module.exports = PostSchema;
router.post('/', asyncHandler(async (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
throw new Error('제목과 내용을 입력 해 주세요');
}
// 로그인 된 사용자의 shortId 로 사용자를 찾아 게시글 생성시 작성자로 추가
const author = await User.findOne({shortId: req.user.shortId});
if (!author) {
throw new Error('No Author');
}
const post = await Post.create({ title, content, author });
res.redirect(`/posts/${post.shortId}`);
}));
router.get('/:shortId', asyncHandler(async (req, res) => {
const { shortId } = req.params;
const post = await Post.findOne({ shortId }).populate('author');
if (req.query.edit) {
res.render('post/edit', { post });
return;
}
res.render('post/view', { post });
}));
게시글의 shortId를 이용해 Post 모델에서 게시글을 찾고, author 속성을 populate하여 post에 할당한다.
router.post('/:shortId', asyncHandler(async (req, res) => {
const { shortId } = req.params;
const { title, content } = req.body;
if (!title || !content) {
throw new Error('제목과 내용을 입력 해 주세요');
}
const post = await Post.findOne({ shortId }).populate('author'); // 작성자 populate
// 작성자와 로그인된 사용자의 shortId 가 다를경우 오류 발생
if (post.author.shortId !== req.user.shortId) {
throw new Error('작성자가 아닙니다.');
}
await Post.updateOne({ shortId }, { title, content });
res.redirect(`/posts/${shortId}`);
}));
이렇게 하면 post.author가 post의 author에 연결된 user모델이 되어 post.author.shortId !== req.user.shortId
처럼 req.user
와 비교가 가능하다.
const posts = Post
.find({author})
.sort({ createdAt : -1 })
.skip(perPage * (page - 1))
.limit(perPage)
.populate('author')
const { Schema } = require('mongoose');
const CommentSchema = new Schema({
// content, String, required,
content: {
type: String,
required:true,
},
// author, User, required
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required:true,
}
}, {
timestamps: true,
});
module.exports = CommentSchema;
댓글스키마의 author를 User 모델과 연결시킨다.
const { Schema } = require('mongoose');
const shortId = require('./types/short-id');
const CommentSchema = require('./comment');
const PostSchema = new Schema({
shortId,
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true,
},
// comments 필드 선언
comments: [CommentSchema],
}, {
timestamps: true,
});
module.exports = PostSchema;
게시글의 댓글과 댓글스키마를 연결하는데, 댓글모델이아니고 댓글스키마라는걸 주의하자.
왜냐면 위의 Post, User 연결시키는것과는 다르게 이건 댓글을 서브스키마를 사용하여 배열로 선언하는것이기 때문이다.
const { Router } = require('express');
const asyncHandler = require('../utils/async-handler');
const { Post, User } = require('../models');
const router = Router();
router.get('/posts/:shortId/comments', asyncHandler(async (req, res) => {
const { shortId } = req.params;
const post = await Post.findOne({ shortId });
// post.comments 의 작성자 populate 하기
await User.populate(post.comments, {path: 'author'})
// json 으로 응답 보내기
res.json(post.comments);
}));
module.exports = router;
게시글의 shortId로 Post모델에 검색해서 게시글을 post에 할당한다.
post의 댓글을 서브쿼리로 등록했으므로 populate하는방법이 좀 다르다.
const { Router } = require('express');
const asyncHandler = require('../utils/async-handler');
const { Post, User } = require('../models');
const router = Router();
router.post('/posts/:shortId/comments', asyncHandler(async (req, res) => {
const { shortId } = req.params;
const { content } = req.body;
const author = await User.findOne({ shortId: req.user.shortId });
// $push operator 사용하여 댓글 추가하기
await Post.updateOne({
shortId,
}, {
$push: {comments: {content,author}},
})
res.json({ result: 'success' });
}));
module.exports = router;
4번과는 다르게 req.user.shortId로 해당되는 user를 User모델에서 찾아 author에 할당한다.
그러고 Post모델을 업데이트해주는데, 게시글의 shortId로 찾아서 $push: {comments:content, author}
처럼 $push를 이용하여 댓글내용과 작성자를 저장한다.
그러고 성공했다는 결과를 보낸다.
특정 URL에 들어가려면 로그인을 해야하고 그것을 request handler 전에 미들웨어로 끼워넣음
1. 토큰 검증
import jwt from "jsonwebtoken";
function login_required(req, res, next) {
// request 헤더로부터 authorization bearer 토큰을 받음.
const userToken = req.headers["authorization"]?.split(" ")[1] ?? "null";
// 이 토큰은 jwt 토큰 문자열이거나, 혹은 "null" 문자열임.
// 토큰이 "null" 일 경우, login_required 가 필요한 서비스 사용을 제한함.
if (userToken === "null") {
console.log("서비스 사용 요청이 있습니다.하지만, Authorization 토큰: 없음");
res.status(400).send("로그인한 유저만 사용할 수 있는 서비스입니다.");
return;
}
// 해당 token 이 정상적인 token인지 확인 -> 토큰에 담긴 user_id 정보 추출
try {
const secretKey = process.env.JWT_SECRET_KEY || "jwt-secret-key";
// jwt.verify 함수를 이용하여 정상적인 jwt인지 확인
const jwtDecoded = jwt.verify(userToken, secretKey);
// verify 함수로부터 반환된 결과에서 user_id 추출
const user_id = jwtDecoded.user_id;
// req 객체에 currentUserId 프로퍼티를 추가하고, 값으로는 user_id를 할당
req.currentUserId = user_id;
// next 함수를 호출하여 본래 요청이 갔었던 라우터로 진행
next();
} catch (error) {
res.status(400).send("정상적인 토큰이 아닙니다. 다시 한 번 확인해 주세요.");
return;
}
}
export { login_required };
import { User } from "../db"; // from을 폴더(db) 로 설정 시, 디폴트로 index.js 로부터 import함.
import bcrypt from "bcrypt";
import { v4 as uuidv4 } from "uuid";
import jwt from "jsonwebtoken";
class userAuthService {
.
.
.
// 로그인 POST
static async getUser({ email, password }) {
// 이메일 db에 존재 여부 확인
const user = await User.findByEmail({ email });
if (!user) {
const errorMessage =
"해당 이메일은 가입 내역이 없습니다. 다시 한 번 확인해 주세요.";
return { errorMessage };
}
// 비밀번호 일치 여부 확인
const correctPasswordHash = user.password;
const isPasswordCorrect = await bcrypt.compare(
password,
correctPasswordHash
);
if (!isPasswordCorrect) {
const errorMessage =
"비밀번호가 일치하지 않습니다. 다시 한 번 확인해 주세요.";
return { errorMessage };
}
const secretKey = process.env.JWT_SECRET_KEY || "jwt-secret-key";
// jwt 의 sign 함수를 이용하여 토큰 생성, 이 때 위의 secretKey 사용
const token = jwt.sign({ user_id: user.id }, secretKey);
// 반환할 loginuser 객체를 위한 변수 설정
const id = user.id;
const name = user.name;
const description = user.description;
const loginUser = {
token,
id,
email,
name,
description,
errorMessage: null,
};
return loginUser;
}
// 회원가입 POST
static async addUser({ name, email, password }) {
// 이메일 중복 확인
const user = await User.findByEmail({ email });
if (user) {
const errorMessage =
"이 이메일은 현재 사용중입니다. 다른 이메일을 입력해 주세요.";
return { errorMessage };
}
// 비밀번호 해쉬화
const hashedPassword = await bcrypt.hash(password, 10);
// id 는 유니크 값 부여
const id = uuidv4();
const newUser = { id, name, email, password: hashedPassword };
// db에 저장
const createdNewUser = await User.create({ newUser });
createdNewUser.errorMessage = null; // 문제 없이 db 저장 완료되었으므로 에러가 없음.
return createdNewUser;
}
}
export { userAuthService };