router.post(
'/register',
[
body('email').notEmpty().isEmail().withMessage('email 형태로 입력해주세요'),
body('password').notEmpty().isString().withMessage('password를 입력해주세요'),
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const {email, password} = req.body;
const sql = `INSERT INTO users (email, password) VALUES (?,?)`;
const values = [email, password];
db.query(sql, values,
(err, results) => {
// 입력값 검증했기 때문에 상태코드 500으로 반환
if (err) return res.status(400).json({ msg: err.message });
return res.status(201).json(results)
});
})
이 코드에서 가장 아쉬운 건 상태코드의 하드 코딩이에요.
NestJS에서는 throw new NotFoundException(e.message); 등으로 처리하던 것을 생각하고 express를 구현하니 res 을 직접 사용하는 게 어색했어요.

NestJS처럼 이미 상수화된 에러 코드를 해당 라이브러리를 import해서 사용할 수 있어요.
db.query(sql, values,
(err, results) => {
if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ msg: err.message });
return res.status(StatusCodes.CREATED).json(results)
});
라이브러리를 적용하니, 하드코딩을 없앨 수 있었어요.

DB도 잘 연결되어있고, 쿼리도 잘 날아간 것을 볼 수 있어요.
라우터와 컨트롤러를 분리해야할 필요를 느꼈어요.
라우터는 url에 따른 요청만 컨트롤러에게 넘겨주는 역할이 맞다고 생각했어요.
컨트롤러를 따로 파일을 만들어, 라우터의 콜백함수를 옮겨놨어요.
import {validationResult} from "express-validator";
import db from "../db.js";
import {StatusCodes} from "http-status-codes";
export const register = (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({errors: errors.array()});
}
const {email, password} = req.body;
const sql = `INSERT INTO users (email, password)
VALUES (?, ?)`;
const values = [email, password];
db.query(sql, values,
(err, results) => {
// 입력값 검증했기 때문에 상태코드 500으로 반환
if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({msg: err.message});
return res.status(StatusCodes.CREATED).json(results)
});
};
이렇게 UsersController.js 를 분리하고, 라우터에는 export 한 컨트롤러만 붙였어요.
import { register } from '../controllers/UsersController.js';
router.post(
'/register',
[
body('email').notEmpty().isEmail().withMessage('email 형태로 입력해주세요'),
body('password').notEmpty().isString().withMessage('password를 입력해주세요'),
],
register,
);
이렇게 보니 validator도 미들웨어를 따로 분리하자고 생각했어요.
validators/userValidator.js 를 따로 만들었어요.
import { body, validationResult } from 'express-validator';
export const validateRegister = [
body('email').isEmail().withMessage('이메일 형식을 확인해주세요.'),
body('password').notEmpty().withMessage('비밀번호를 입력해주세요.'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next(); // 에러가 없으면 다음 단계(컨트롤러)로 이동
}
];
router.post(
'/register',
validaeRegister,
register,
);
훨씬 깔끔하게 바뀌었어요.
export const login = (req, res) => {
const {email, password} = req.body;
const sql = 'SELECT * FROM users WHERE email = ?';
const values = [email];
db.query(sql, values, (err, results) => {
if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({msg: err.message});
const user = results[0];
if (user && user.password === password) {
const token = jwt.sign({
id: user.id
}, process.env.JWT_SECRET,
{
expiresIn: '1d',
issuer: 'bookstore-api',
});
}
res.cookie("access_token", token, {
httpOnly: true,
});
return res.status(StatusCodes.OK).json(results)
});
비밀번호가 틀렸을 때 401 UnAuthorized를 반환하도록 했어요.
export const passwordResetRequest = (req, res) => {
const { email } = req.body;
const query = 'SELECT * FROM users WHERE email = ?';
const values = [email];
db.query(query, values, (err, results) => {
if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({message: err.message});
const user = results[0];
if (user) {
return res.status(StatusCodes.OK).json({
email: user.email
});
} else {
return res.status(StatusCodes.NOT_FOUND).end();
}
});
};
이 곳도 이메일로 유저를 찾아내면 200, 아니면 404를 내게 했어요.
비밀번호 초기화 form에 이메일을 넣지 않기 때문에 클라이언트가 기억할 수 있게 이메일을 응답에 넣어줬어요.
export const passwordReset = (req, res) => {
const { email, password } = req.body;
const query = 'UPDATE users SET password = ? WHERE email = ?';
const values = [password, email];
db.query(query, values, (err, results) => {
if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({message: err.message});
if (results.affectedRows === 0) return res.status(StatusCodes.NOT_FOUND).end();
return res.status(StatusCodes.OK).json(results);
});
};
UPDATE 문을 사용해 비밀번호를 변경하도록 했어요.
이때 affectedRow가 0이면, 해당 이메일로 유저를 못 찾은 것이니 404를 반환했어요.
요청 형식의 문제는 아니니 절대 인강에서 작성하는 400은 아니라고 생각했어요.
드디어 평문으로 된 비밀번호를 암호화하기로 했어요.
노드는 bcrypt 를 사용하여 할 수 있어요.
하지만 인강을 따라 우선 crypto 자체 모듈을 사용해볼게요.
const salt = crypto.randomBytes(64).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString();
이 암호화는 salt가 매번 바뀌니까 salt 또한 db에 저장해야해요.
export const register = (req, res) => {
const {email, password} = req.body;
const salt = crypto.randomBytes(10).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({errors: errors.array()});
}
const sql = `INSERT INTO users (email, password, salt)
VALUES (?, ?, ?)`;
const values = [email, hashPassword, salt];
db.query(sql, values,
(err, results) => {
if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({msg: err.message});
return res.status(StatusCodes.CREATED).json(results)
});
};

password 와 salt 가 무사히 저장된 것을 볼 수 있어요.
export const login = (req, res) => {
const {email, password} = req.body;
const sql = 'SELECT * FROM users WHERE email = ?';
const values = [email];
db.query(sql, values, (err, results) => {
if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({message: err.message});
const user = results[0];
// salt를 꺼내 들어온 비밀번호를 암호화하고 db의 비밀번호와 비교
const hashPassword = crypto.pbkdf2Sync(password, user.salt, 10000, 10, 'sha512').toString('base64');
if (user && user.password === hashPassword) {
const token = jwt.sign({
id: user.id
}, process.env.JWT_SECRET,
{
expiresIn: '1d',
issuer: 'bookstore-api',
});
res.cookie("access_token", token, {
httpOnly: true,
});
} else {
return res.status(StatusCodes.UNAUTHORIZED).end();
}
return res.status(StatusCodes.OK).json(results)
});
};
같은 salt를 친 비밀번호와 db 속 비밀번호를 비교하게 했어요.

무사히 로그인이 잘 된 것을 볼 수 있어요.
export const passwordReset = (req, res) => {
const { email, password } = req.body;
const salt = crypto.randomBytes(10).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
const query = 'UPDATE users SET password = ?, salt = ? WHERE email = ?';
const values = [hashPassword, salt, email];
db.query(query, values, (err, results) => {
if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({message: err.message});
if (results.affectedRows === 0) return res.status(StatusCodes.NOT_FOUND).end();
return res.status(StatusCodes.OK).json(results);
});
};
비밀번호 변경 api도 salt와 해싱을 추가했어요.
salt와 변경된 password 모두 db에 새로 넣어주는 것으로 마무리했어요.