View
import express from 'express';
import { body } from 'express-validator';
import { validate } from '../middleware/validator.js';
import * as authController from '../controller/auth.js'
import { isAuth } from '../middleware/auth.js';
const router = express.Router();
const validateCredential = [
body('username')
.trim()
.notEmpty()
.isLength({ min: 5 })
.withMessage('usename should be at least 5 characters'),
body('password')
.trim()
.isLength({ min: 5 })
.withMessage('password should be at least 5 characters'),
validate
];
const validateSingup = [
...validateCredential,
body('name')
.notEmpty()
.withMessage('name is missing'),
body('email')
.isEmail()
.normalizeEmail()
.withMessage('invalid email'),
body('url')
.isURL()
.withMessage('invalid URL')
.optional({ nullable: true, checkFalsy: true }),
// nullable: null 허용, checkFalsy: 빈 문자열 허용
validate
];
// POST : /auth/signup
router.post('/signup', validateSingup, authController.signup);
// POST : /auth/login
router.post('/login', validateCredential, authController.login);
// POST : /auth/logout
router.post('/logout', authController.logout);
// GET : /auth/me
router.get('/me', isAuth, authController.me);
// GET : /auth/csrf-token
router.get('/csrf-token', authController.csrfToken);
export default router;
Controller
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import * as userRepository from '../data/auth.js';
import { config } from '../config.js';
// 회원가입
export async function signup(req, res, next) {
const { username, password, name, email, url } = req.body;
const found = await userRepository.findByUsername(username);
if (found) {
return res.status(409).json({ message: `${username} already exists` });
}
const hashed = await bcrypt.hash(password, config.bcrypt.saltRounds);
const userId = await userRepository.createUser({
username,
password: hashed,
name,
email,
url,
});
// [ 토큰 데이터 생성 조건 ]
// Database 에서 유저 확인 후 존재하면 -> Token 데이터 생성
// Database 에서 유저 확인 후 존재하지 않으면 -> 에러 메세지
const token = await createJwtToken(userId);
setToken(res, token); // 토큰 데이터 쿠키에 저장
res.status(201).json({ token, username });
};
// 로그인
export async function login(req, res, next) {
const { username, password } = req.body;
const user = await userRepository.findByUsername(username);
if (!user) {
return res.status(401).json({ message: `Invalid user or password` });
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ message: `invalid user or password` });
}
// [ 토큰 데이터 생성 조건 ]
// Database 에서 유저 확인 후 존재하면 -> Token 데이터 생성
// Database 에서 유저 확인 후 존재하지 않으면 -> 에러 메세지
const token = await createJwtToken(user.id);
setToken(res, token); // 토큰 데이터 쿠키에 저장
res.status(200).json({ token, username });
};
// 로그아웃
export async function logout(req, res, next) {
setToken(res, '');
res.status(200).json({ message: 'User has been logged out' });
}
// 토큰 생성
async function createJwtToken(id) {
return jwt.sign(
{
id,
},
config.jwt.secretKey,
{ expiresIn: config.jwt.expiresInSec }
);
}
// Set Cookie (httpOnly)
function setToken(res, token) {
const options = {
maxAge: config.jwt.expiresInSec * 1000, // - 쿠키의 유효기간을 설정한다 (jwt 시간과 동일하게 해주면 좋다 / ms로 설정)
httpOnly: true, // - httpOnly 로 지정 (브라우저 자체적으로 쿠키를 보관, JavaScript 로 접근 불가)
sameSite: 'none', // - CORS 와 비슷하게 클라이언트와 서버가 다른 도메인, 즉 다른 IP 이더라도 서로 동작할 수 있게끔 설정
secure: true, // - sameSite 가 지정되면 secure 를 true 로 지정해 주어야 한다. (https 가 아닌 http 에서는 미적용 해주어도 상관없다.)
}
res.cookie('token', token, options); // - options를 통해 그냥 cookie 가 아닌 httpOnly 쿠키를 지정한다.
}
// 브라우저 클라이언트에서 사용자에 대한 민감한 데이터를 localStorage 나 그냥 cookie 에 저장해 두는 것은 좋지 않습니다.
// 왜냐하면 XSS attack 의 script injection 로 인한 보안 이슈로 누군가 악의적으로 저장된 정보에 접근할 수 있기 때문입니다.
// 그래서 httpOnly 를 포함한 cookie 를 사용해야 보다 안전하게 데이터를 저장할 수 있습니다.
// 그치만 httpOnly 도 CSRF attack 으로 인한 보안 이슈가 있기 땨문에 완전히 안전한 방법이라 할 수 없다.
// ※ CSRF (Cross-Site Request Forgery) : 사용자가 특정한 액션을 하도록 만드는 공격.
// 로그인된 토큰 유저 확인
export async function me(req, res, next) {
const user = await userRepository.findById(req.userId);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.status(200).json({ token: req.token, username: user.username });
}
Model (MySQL)
import { db } from '../db/database.js';
// 회원 조회
export async function findByUsername(username) {
return db
.execute('SELECT * FROM dwitter.users WHERE USERNAME = ?', [username])
.then((result) => result[0][0]); // users 단건 데이터 출력
};
// 회원 조회
export async function findById(id) {
return db
.execute('SELECT * FROM dwitter.users WHERE ID = ?', [id])
.then((result) => result[0][0]); // users 단건 데이터 출력
}
// 회원 생성
export async function createUser(user) {
const { username, password, name, email, url } = user;
return db
.execute(
'INSERT INTO dwitter.users (username, password, name, email, url) VALUES (?, ?, ?, ?, ?) ',
[username, password, name, email, url]
)
.then((result) => result[0].insertId); // userId 데이터 출력
};
Model (MongoDB)
import MongoDB from 'mongodb';
import { getUsers } from '../db/mongo.js';
// ---------------------------------
// [ MVC ( Model ) ]
// ---------------------------------
// MongoDB 조회
// 1. 해당 Collection을 가져온다.
// 2. findOne() 을 사용해 데이터를 가져온다.
export async function findByUsername(username) {
return getUsers()
.findOne({ username })
.then(mapOptionalUser);
};
export async function findById(id) {
return getUsers()
.findOne({ _id: new MongoDB.ObjectId(id) })
.then(mapOptionalUser);
}
export async function createUser(user) {
return getUsers()
.insertOne(user)
.then((data) => data.insertedId.toString());
};
// insert 결과 출력 -> data
// {
// acknowledged: true,
// insertedId: new ObjectId("624e71e8e3cc970768008bac")
// }
// -> 생성된 id 를 반환
function mapOptionalUser(user) {
return user ? { ...user, id: user._id.toString() } : user;
}