Skip to content

Commit

Permalink
Merge pull request #3 from jjikky/feature/auth
Browse files Browse the repository at this point in the history
Feature/auth : 로컬 로그인 / 카카오 로그인
  • Loading branch information
jikky authored Jun 21, 2024
2 parents 5628861 + 6700946 commit 7245ec6
Show file tree
Hide file tree
Showing 18 changed files with 827 additions and 29 deletions.
420 changes: 420 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,23 @@
"homepage": "https://github.com/Murakano/murakano-be#readme",
"dependencies": {
"ajv": "^8.16.0",
"ajv-formats": "^3.0.1",
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-session": "^1.18.0",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"moment": "^2.30.1",
"mongoose": "^8.4.3",
"morgan": "^1.10.0",
"nodemon": "^3.1.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pm2": "^5.3.1"
},
Expand Down
6 changes: 6 additions & 0 deletions src/common/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ const conf = {
port: process.env.PORT,
corsWhiteList: process.env.CORS_WHITELIST,
mongoURL: process.env.MONGO_URL,
jwtSecret: process.env.JWT_SECRET,
cookieSecret: process.env.COOKIE_SECRET,

// social login
kakaoRestApiKey: process.env.KAKAO_REST_API_KEY,
kakaoCallback: process.env.KAKAO_CALLBACK,
};

module.exports = conf;
2 changes: 2 additions & 0 deletions src/common/constants/error-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const ErrorMessage = Object.freeze({
EMAIL_CHECK_ERROR: '이메일 중복검사중 오류가 발생하였습니다.',
EXIST_NICKNAME: '이미 존재하는 닉네임 입니다.',
EXIST_EMAIL: '이미 존재하는 이메일 입니다.',
LOGIN_ERROR: '로그인중 오류가 발생하였습니다.',
KAKAO_LOGIN_ERROR: '카카오 로그인중 오류가 발생하였습니다.',
});

module.exports = ErrorMessage;
7 changes: 6 additions & 1 deletion src/common/constants/success-message.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
const SucesssMessage = Object.freeze({
// USER
// USER - 회원가입
REGISTER_SUCCESSS: '회원가입 성공',
AVAILABLE_NICKNAME: '사용 가능한 닉네임입니다.',
AVAILABLE_EMAIL: '사용 가능한 이메일입니다.',

// USER - 로그인
LOGIN_SUCCESSS: '로그인 성공',

GET_PROFILE_SUCCESS: '유저 정보 조회 성공',
});

module.exports = SucesssMessage;
30 changes: 28 additions & 2 deletions src/common/modules/express/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
const express = require('express');
const morgan = require('morgan');
const passport = require('passport');
const session = require('express-session');
const helmet = require('helmet');
const cors = require('cors');
const crypto = require('crypto');
const cookieParser = require('cookie-parser');

const conf = require('../../config/index');
const conf = require('../../config');
const passportConfig = require('../../passport');
const router = require('../../../routes/index');

module.exports = expressLoader = (app) => {
passportConfig();

app.use(morgan('dev'));
app.use(helmet());

Expand All @@ -23,14 +28,35 @@ module.exports = expressLoader = (app) => {
cors({
credentials: true,
origin: (origin, callback) => {
if (origin !== null || conf.corsWhiteList?.indexOf(origin) !== -1) {
if (origin === undefined || (origin && conf.corsWhiteList?.indexOf(origin) !== -1)) {
return callback(null, true);
}
callback(new Error('CORS ERROR'));
},
})(req, res, next);
});

app.use(
session({
name: 'user',
resave: false,
saveUninitialized: false,
secret: conf.cookieSecret,
cookie: {
// cleint 쿠키 접근 불가
httpOnly: true,
// TODO : ssl 적용하면 true로 변경
secure: false,
// 24h
maxAge: 86400000,
},
})
);

// Passport 세팅
app.use(passport.initialize());
app.use(passport.session());

// Body Parser 세팅
app.use(
express.json({
Expand Down
23 changes: 23 additions & 0 deletions src/common/passport/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const passport = require('passport');
const localStrategy = require('./localStrategy');
const jwtStrategy = require('./jwtStrategy');
const User = require('../../routes/user/user.model');

module.exports = () => {
passport.serializeUser((user, done) => {
done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});

// 초기화
localStrategy();
jwtStrategy();
};
27 changes: 27 additions & 0 deletions src/common/passport/jwtStrategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const passport = require('passport');
const User = require('../../routes/user/user.model');
const config = require('../config'); // 비밀 키를 저장한 파일

const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.jwtSecret,
};

module.exports = () => {
passport.use(
new JwtStrategy(opts, async (jwtPayload, done) => {
try {
const user = await User.findById(jwtPayload.id);
if (user) {
return done(null, user);
} else {
return done(null, false);
}
} catch (error) {
console.error(error);
return done(error, false);
}
})
);
};
30 changes: 30 additions & 0 deletions src/common/passport/localStrategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('../../routes/user/user.model');

module.exports = () => {
passport.use(
new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
},
async (email, password, done) => {
try {
const user = await User.findOne({ email });
if (!user) {
return done(null, false, { message: '가입되지 않은 회원입니다.' });
}
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return done(null, false, { message: '비밀번호가 일치하지 않습니다.' });
}
return done(null, user);
} catch (error) {
console.error(error);
return done(error);
}
}
)
);
};
57 changes: 57 additions & 0 deletions src/common/utils/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const jwt = require('jsonwebtoken');
const passport = require('passport');
const config = require('../config');

exports.generateToken = (user) => {
return jwt.sign(
{
id: user._id,
email: user.email,
nickname: user.nickname,
},
config.jwtSecret,
{ expiresIn: '24h' }
);
};

exports.verifyToken = (token) => {
return jwt.verify(token, config.jwtSecret);
};

exports.isAuthenticated = (req, res, next) => {
passport.authenticate('jwt', { session: false }, (err, user, info) => {
if (err) {
return res.status(500).json({ message: '서버 오류' });
}
if (!user) {
return res.status(401).json({ message: '인증되지 않은 사용자' });
}
req.user = user;
next();
})(req, res, next);
};

exports.isLoggedIn = (req, res, next) => {
passport.authenticate('jwt', { session: false }, (err, user, info) => {
if (err) {
return res.status(500).json({ message: '서버 오류' });
}
if (!user) {
return res.status(401).json({ message: '로그인이 필요합니다.' });
}
req.user = user;
next();
})(req, res, next);
};

exports.isNotLoggedIn = (req, res, next) => {
passport.authenticate('jwt', { session: false }, (err, user, info) => {
if (err) {
return res.status(500).json({ message: '서버 오류' });
}
if (user) {
return res.status(403).json({ message: '이미 로그인된 상태입니다.' });
}
next();
})(req, res, next);
};
37 changes: 37 additions & 0 deletions src/common/utils/kakao.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const axios = require('axios');
const conf = require('../../common/config');

const header = {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
Authorization: 'Bearer ',
};
exports.getKakaoToken = async (code) => {
const data = {
grant_type: 'authorization_code',
client_id: conf.kakaoRestApiKey,
code,
};

const queryString = Object.keys(data)
.map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(data[k]))
.join('&');

const token = await axios.post('https://kauth.kakao.com/oauth/token', queryString, { headers: header });
return { accessToken: token.data.access_token };
};
exports.getUserInfo = async (accessToken) => {
// Authorization: 'Bearer access_token'
header.Authorization += accessToken;

// 카카오 사용자 정보 조회
const get = await axios.get('https://kapi.kakao.com/v2/user/me', { headers: header });
const result = get.data;

return {
snsId: result.id,
email: result.kakao_account.email ? result.kakao_account.email : `${result.id}@no.agreement`,
// NOTE: 닉네임 10글자 제한 때문에, 임시 처리
// kakao 닉네임 규정은 20글자. result.id는 10글자로 추정
nickname: result.id,
};
};
2 changes: 2 additions & 0 deletions src/common/utils/request.validator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const ErrorMessage = require('../../common/constants/error-message');
const Ajv = require('ajv');
const addFormats = require('ajv-formats');
const ajv = new Ajv();
addFormats(ajv);

exports.validateRequest = (schema, req) => {
const validate = ajv.compile(schema);
Expand Down
Loading

0 comments on commit 7245ec6

Please sign in to comment.