diff --git a/config/db.config.js b/config/db.config.js index 9ac000e..b7b248c 100644 --- a/config/db.config.js +++ b/config/db.config.js @@ -7,7 +7,7 @@ export const pool = mysql.createPool({ user: process.env.DB_USER || 'root', // user 이름 port: process.env.DB_PORT || '3306', // 포트 번호 database: process.env.DB_TABLE || 'myplace_dev', // 데이터베이스 이름 - password: process.env.DB_PASSWORD || '111111', // 비밀번호 + password: process.env.DB_PASSWORD || '1111', // 비밀번호 waitForConnections: true, // Pool에 획득할 수 있는 connection이 없을 때, // true면 요청을 queue에 넣고 connection을 사용할 수 있게 되면 요청을 실행하며, false이면 즉시 오류를 내보내고 다시 요청 diff --git a/config/jwt.config.js b/config/jwt.config.js index 4bcf032..5f6994e 100644 --- a/config/jwt.config.js +++ b/config/jwt.config.js @@ -2,9 +2,16 @@ import dotenv from 'dotenv' dotenv.config() export const jwtConfig = { - secretKey: process.env.TOKKEN_SECRET, + secretKey: process.env.TOKEN_SECRET, options: { - algorithm: process.env.TOKKEN_ALGORITHM, - expiresIn: process.env.TOKKEN_EXPIRE, + algorithm: process.env.TOKEN_ALGORITHM, + expiresIn: process.env.TOKEN_EXPIRE, + }, +} +export const jwtRefreshConfig = { + secretKey: process.env.TOKEN_SECRET, + options: { + algorithm: process.env.TOKEN_ALGORITHM, + expiresIn: process.env.TOKEN_REFRESH_EXPIRE, }, } diff --git a/config/response.status.js b/config/response.status.js index 775d075..3595e70 100644 --- a/config/response.status.js +++ b/config/response.status.js @@ -29,6 +29,12 @@ export const status = { code: 'AUTH2001', message: '구글 로그인 완료!', }, + TOKEN_LOGIN_SUCCESS: { + status: 200, + isSuccess: true, + code: 'AUTH2001', + message: '토큰 로그인 완료!', + }, REGISTER_SUCCESS: { status: 200, isSuccess: true, @@ -103,7 +109,7 @@ export const status = { status: 400, isSuccess: false, code: 'TOEKN4002', - message: '유효하지 않은 토큰', + message: '유효하지 않은 토큰. 재로그인 필요', }, TOKEN_SIGNITURE: { status: 400, diff --git a/ecosystem.config.js b/ecosystem.config.js index c1550b8..62b10e1 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -11,8 +11,10 @@ module.exports = { error: 'logs/pm2/myplace.error.log', //merge_logs: true, autorestart: true, + ignore_watch: ['node_modules'], watch: true, // max_memory_restart: "512M", + time: true, }, ], } diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 3e0feda..2d6ff83 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -1,6 +1,6 @@ import { response } from '../../config/response.js' import { status } from '../../config/response.status.js' -import { tokenVerify } from '../middlewares/jwt.middleware.js' +import { tokenVerify, tokenCheck } from '../middlewares/jwt.middleware.js' import { kakaoLogin, googleLogin } from '../services/auth.service.js' @@ -8,37 +8,74 @@ import Axios from 'axios' //임시 import dotenv from 'dotenv' +import { + newRefreshTokenLogin, + newTokenLogin, + tokenLogin, +} from '../providers/auth.provider.js' dotenv.config() export const authLogin = async (req, res) => { try { - const provider = req.body.provider - if (provider == '0') { - console.log('카카오 로그인 요청!') + //토큰 확인 + const tokenResult = await tokenCheck(req, res) + + //Access토큰이 유효한 경우 + if (tokenResult.status == 1) { + console.log('Token로그인 요청: ', tokenResult) return res.send( - response( - status.KAKAO_LOGIN_SUCCESS, - await kakaoLogin(req.headers, req.body), - ), + response(status.TOKEN_LOGIN_SUCCESS, await tokenLogin(tokenResult)), + ) + } + //Access토큰은 만료, Refresh토큰이 유효한 경우 + else if (tokenResult.status == 2) { + console.log('newToken로그인 요청: ', tokenResult) + return res.send( + response(status.TOKEN_LOGIN_SUCCESS, await newTokenLogin(tokenResult)), ) - } else if (provider == '1') { - console.log('구글 로그인 요청!') + } + //Access토큰은 유효, Refresh토큰이 만료된 경우 + else if (tokenResult.status == 3) { + console.log('newRefreshToken로그인 요청: ', tokenResult) return res.send( response( - status.GOOGLE_LOGIN_SUCCESS, - await googleLogin(req.headers, req.body), + status.TOKEN_LOGIN_SUCCESS, + await newRefreshTokenLogin(tokenResult), ), ) - } //else if (provider == 2) { - // console.log('애플 로그인 요청!') - // res.send( - // response( - // status.REGISTER_SUCCESS, - // await appleLogin(req.headers, req.body), - // ), - // ) - // } - return res.send(response(status.INVAILD_PROVIDER, null)) + } + //Access, Refresh 토큰이 없거나, 만료,이상인 경우 + else if (tokenResult.status == -1) { + //return res.send(response(status.TOKEN_INVAILD, tokenResult.message)) + const provider = req.body.provider + if (provider == '0') { + console.log('카카오 로그인 요청!') + return res.send( + response( + status.KAKAO_LOGIN_SUCCESS, + await kakaoLogin(req.headers, req.body), + ), + ) + } else if (provider == '1') { + console.log('구글 로그인 요청!') + return res.send( + response( + status.GOOGLE_LOGIN_SUCCESS, + await googleLogin(req.headers, req.body), + ), + ) + } //else if (provider == 2) { + // console.log('애플 로그인 요청!') + // res.send( + // response( + // status.REGISTER_SUCCESS, + // await appleLogin(req.headers, req.body), + // ), + // ) + // } + return res.send(response(status.INVAILD_PROVIDER, null)) + } + return res.send(response(status.TOKEN_ERROR, null)) } catch (err) { console.log('LoginController Err: ', err) return res.send(response(status.CONTROL_ERROR, null)) @@ -66,6 +103,7 @@ export const authGoogleRedirectTest = async (req, res) => { //임시 export const authJWT = async (req, res) => { try { + console.log(req.headers['token']) return res.send( response(status.SUCCESS, await tokenVerify(req.headers['token'])), ) diff --git a/src/dtos/auth.dto.js b/src/dtos/auth.dto.js index 636d08e..015356e 100644 --- a/src/dtos/auth.dto.js +++ b/src/dtos/auth.dto.js @@ -1,5 +1,5 @@ // login response DTO -export const loginResponseDTO = (user, access_token) => { +export const loginResponseDTO = (user, accessToken, refreshToken) => { user = user[0] console.log(user) return { @@ -7,6 +7,7 @@ export const loginResponseDTO = (user, access_token) => { email: user.email, username: user.username, profile_img: user.profile_img, - access_Token: access_token, + accessToken: accessToken, + refreshToken: refreshToken, } } diff --git a/src/middlewares/jwt.middleware.js b/src/middlewares/jwt.middleware.js index ba7e29b..450690f 100644 --- a/src/middlewares/jwt.middleware.js +++ b/src/middlewares/jwt.middleware.js @@ -1,5 +1,6 @@ +import { pool } from '../../config/db.config' import jwt from 'jsonwebtoken' -import { jwtConfig } from '../../config/jwt.config' +import { jwtConfig, jwtRefreshConfig } from '../../config/jwt.config' import { BaseError } from '../../config/error' import { status } from '../../config/response.status' @@ -11,11 +12,16 @@ export const tokenSign = async (user) => { username: user.username, email: user.email, } - console.log(user) const result = jwt.sign(payload, jwtConfig.secretKey, jwtConfig.options) return result } +export const tokenRefreshSign = async () => { + //현재는 username와 email을 payload로 넣었지만 필요한 값을 넣으면 됨! + const result = jwt.sign({}, jwtConfig.secretKey, jwtRefreshConfig.options) + return result +} + //토큰 인증 export const tokenVerify = async (token) => { let decoded @@ -25,13 +31,16 @@ export const tokenVerify = async (token) => { } catch (err) { if (err.message === 'jwt expired') { console.log('TOKEN_EXPIRED') - throw new BaseError(status.TOKEN_EXPIRED) + return 'TOKEN_EXPIRED' } else if (err.message === 'invalid token') { console.log('TOKEN_INVALID') - throw new BaseError(status.TOKEN_INVAILD) + return 'TOKEN_INVALID' } else if (err.message === 'invalid signature') { console.log('TOKEN_SIGNITURE') - throw new BaseError(status.TOKEN_SIGNITURE) + return 'TOKEN_INVAILD_SIGNITURE' + } else if (err.message === 'jwt malformed') { + console.log('TOKEN_INVALID') + return 'TOKEN_INVALID' } else { console.log(err.message) throw new BaseError(status.TOKEN_ERROR) @@ -39,3 +48,118 @@ export const tokenVerify = async (token) => { } return decoded } + +//토큰 인증 +export const tokenResponseDTO = ( + status = -1, + result = null, + refresh = null, + access = null, + userId = null, +) => { + return { + status: status, //-1 재로그인, 1 토큰 로그인 & refresh 재발급 후 로그인, 2 access 재발급 후 로그인 + result: result, + refresh: refresh, //로그인 시 반환할 refresh 토큰. 만료시 자동 재발급 + access: access, //로그인 시 반환할 refresh 토큰. 만료시 자동 재발급 + userId: userId, //access 만료 시 refresh를 통해 userId 제공 -> 로그인 진행 + } +} + +export const tokenCheck = async (req, res) => { + if (req.headers['access'] === undefined) + throw new BaseError(status.TOKEN_ERROR) + const accessToken = await tokenVerify(req.headers['access']) + const refreshToken = await tokenVerify(req.headers['refresh']) // *실제로는 DB 조회 + console.log(accessToken, refreshToken) + if ( + accessToken === 'TOKEN_INVALID' || + accessToken === 'TOKEN_INVAILD_SIGNITURE' || + refreshToken === 'TOKEN_INVAILD_SIGNITURE' + ) { + console.log('case 0') + return tokenResponseDTO(-1, '유효하지 않은 토큰. 재로그인 필요.') + } + if (accessToken === 'TOKEN_EXPIRED') { + if (refreshToken === 'TOKEN_EXPIRED') { + console.log('CASE 1-1') + // case1: access token과 refresh token 모두가 만료된 경우 + return tokenResponseDTO(-1, '토큰 만료. 재로그인 필요.') + } else if (refreshToken === 'TOKEN_INVALID') { + console.log('CASE 1-2') + // case1: access token과 refresh token 모두가 만료된 경우 + return tokenResponseDTO(-1, '토큰 만료. 재로그인 필요.') + } else { + console.log('CASE 2') + // case2: access token은 만료됐지만, refresh token은 유효한 경우 + /** + * DB를 조회해서 payload에 담을 값들을 가져오는 로직 + */ + const conn = await pool.getConnection() + const [user] = await conn.query( + `SELECT user_id FROM oauthid WHERE access_token = '${req.headers['refresh']}';`, + ) + console.log(user[0]) + // res.cookie('access', newAccessToken) + // req.cookies.access = newAccessToken + const newAccessToken = await tokenSign(user[0]) + res.set('access', newAccessToken) + return tokenResponseDTO( + 2, + accessToken, + req.headers['refresh'], + newAccessToken, + user[0].user_id, + ) + } + } else { + if (refreshToken === 'TOKEN_INVALID' || refreshToken === 'TOKEN_EXPIRED') { + console.log('CASE 3') + // case3: access token은 유효하지만, refresh token은 만료된 경우 + let conn + try { + const newRefreshToken = await tokenRefreshSign() + conn = await pool.getConnection() + await conn.beginTransaction() + console.log(accessToken) + const [user] = await conn.query( + 'SELECT * FROM USER WHERE id = ?;', + accessToken.id, + ) + console.log(user[0]) + await conn.query( + `UPDATE oauthid + SET access_token = '${newRefreshToken}' + WHERE user_id = ${user[0].id};`, + ) + await conn.commit() + conn.release() + //res.cookie('refresh', newRefreshToken) + //req.cookies.refresh = newRefreshToken + res.set('access', newRefreshToken) + return tokenResponseDTO(1, accessToken, newRefreshToken, user[0].id) + } catch (err) { + console.log('JWT: ', err) + + try { + if (conn) { + await conn.rollback() + } + } catch (rollbackError) { + console.error('Error in rollback:', rollbackError) + } + + throw new BaseError(status.PARAMETER_IS_WRONG) + } + } else { + // case4: accesss token과 refresh token 모두가 유효한 경우 + console.log('Case4') + return tokenResponseDTO( + 1, + accessToken, + req.headers['refresh'], + req.headers['access'], + ) + } + } +} diff --git a/src/models/auth.dao.js b/src/models/auth.dao.js index 276d431..07a3b15 100644 --- a/src/models/auth.dao.js +++ b/src/models/auth.dao.js @@ -49,6 +49,7 @@ export const addUser = async ( profileImage, provider, oauthId, + refresh, ) => { let conn try { @@ -60,7 +61,12 @@ export const addUser = async ( email, profileImage, ]) - await conn.query(insertOauthSql, [result[0].insertId, oauthId, provider]) + await conn.query(insertOauthSql, [ + result[0].insertId, + oauthId, + provider, + refresh, + ]) await conn.commit() conn.release() return result[0].insertId diff --git a/src/models/auth.sql.js b/src/models/auth.sql.js index d6cee0d..c26d41d 100644 --- a/src/models/auth.sql.js +++ b/src/models/auth.sql.js @@ -11,8 +11,10 @@ export const insertUserSql = //유저 oauth정보 저장 export const insertOauthSql = - 'INSERT INTO oauthid(user_id, access_token, provider) VALUES(?,?,? );' + 'INSERT INTO oauthid(user_id, id, provider,access_token) VALUES(?,?,?,? );' -export const selectUser = ` - SELECT * FROM user WHERE id = ?` +export const insertOauthRefreshSql = + 'UPDATE oauthid SET access_token = ? WHERE user_id = ?;' +export const selectUser = ` + SELECT * FROM user WHERE id = ?;` diff --git a/src/providers/auth.provider.js b/src/providers/auth.provider.js new file mode 100644 index 0000000..0024c47 --- /dev/null +++ b/src/providers/auth.provider.js @@ -0,0 +1,41 @@ +import { pool } from '../../config/db.config' +import { BaseError } from '../../config/error' +import { status } from '../../config/response.status' +import { loginResponseDTO } from '../dtos/auth.dto' +import { tokenSign } from '../middlewares/jwt.middleware' +import { selectUser } from '../models/auth.sql' +export const tokenLogin = async (token) => { + try { + const conn = await pool.getConnection() + console.log('Login process: ', token) + const user = await conn.query(selectUser, token.result.id) + console.log('Token Login Done!') + return loginResponseDTO(user[0], token.access, token.refresh) + } catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG, err) + } +} + +export const newTokenLogin = async (token) => { + try { + const conn = await pool.getConnection() + console.log('newTokenLogin process: ', token) + const user = await conn.query(selectUser, token.userId) + console.log('newTokenLogin 완료') + return loginResponseDTO(user[0], await tokenSign(user[0][0]), token.refresh) + } catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG, err) + } +} + +export const newRefreshTokenLogin = async (token) => { + try { + const conn = await pool.getConnection() + console.log('newTokenLogin process: ', token) + const user = await conn.query(selectUser, token.userId) + console.log('newTokenLogin 완료') + return loginResponseDTO(user[0], await tokenSign(user[0][0]), token.refresh) + } catch (err) { + throw new BaseError(status.PARAMETER_IS_WRONG, err) + } +} diff --git a/src/routes/auth.route.js b/src/routes/auth.route.js index 81f6582..cdc38ff 100644 --- a/src/routes/auth.route.js +++ b/src/routes/auth.route.js @@ -1,5 +1,5 @@ import express from 'express' -import { authLogin, authJWT } from '../controllers/auth.controller' +import { authLogin, authJWT, authGoogleRedirectTest } from '../controllers/auth.controller' import { imageUploader, profileUpload, @@ -8,7 +8,7 @@ import { export const authRouter = express.Router({ mergeParams: true }) -//authRouter.get('/', authGoogleRedirectTest) +authRouter.get('/', authGoogleRedirectTest)//임시 authRouter.post('/login', authLogin) authRouter.get('/login', authJWT) //임시 diff --git a/src/services/auth.service.js b/src/services/auth.service.js index a22ed8a..fe813b5 100644 --- a/src/services/auth.service.js +++ b/src/services/auth.service.js @@ -8,7 +8,7 @@ import { addOauth, } from '../models/auth.dao' import Axios from 'axios' -import { tokenSign } from '../middlewares/jwt.middleware' +import { tokenSign, tokenRefreshSign } from '../middlewares/jwt.middleware' //카카오 소셜로그인 export const kakaoLogin = async (headers, body) => { @@ -41,7 +41,8 @@ export const kakaoLogin = async (headers, body) => { const user = await getUserByEmail(email) if (user == -1) { - await addUser(username, email, profileImage, provider, kakaoId) + const refresh = await tokenRefreshSign() + await addUser(username, email, profileImage, provider, kakaoId, refresh) const new_user = await getUserByEmail(email) return loginResponseDTO(new_user, await tokenSign(new_user[0])) } else { @@ -49,7 +50,11 @@ export const kakaoLogin = async (headers, body) => { if (oauth == -1) { await addOauth(user[0].id, kakaoId, provider) } - return loginResponseDTO(user, await tokenSign(user[0])) + return loginResponseDTO( + user, + await tokenSign(user[0]), + oauth[0].access_token, + ) } } diff --git a/yarn.lock b/yarn.lock index d15347f..1e0fef9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2320,7 +2320,6 @@ husky@^8.0.3: version "8.0.3" resolved "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz" integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== - iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"