From ce2857bd1bd1ef2ce4e2e6384c1aa7c5ba24f3e3 Mon Sep 17 00:00:00 2001 From: choihb Date: Thu, 3 Aug 2023 10:26:02 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BackEnd/src/controllers/user.js | 38 +++++++++++++++++++++++++++++++++ BackEnd/src/routes/user.js | 3 +++ FrontEnd/views/user/forgot.ejs | 0 3 files changed, 41 insertions(+) create mode 100644 FrontEnd/views/user/forgot.ejs diff --git a/BackEnd/src/controllers/user.js b/BackEnd/src/controllers/user.js index d7db2c6..811dc24 100644 --- a/BackEnd/src/controllers/user.js +++ b/BackEnd/src/controllers/user.js @@ -222,6 +222,35 @@ const editPassword = async (req, res) => { } }; +/** + * email을 받아 확인후 새로운 비밀번호 만들어 메일전송 + * @param {string} email 사용자가 입력한 기존 비밀번호 + */ +const forgotPassword = async (req, res) => { + let { email } = req.body; + try { + let user = await user.findUser('email', email, 0); + if (!user) { + throw new Error('Services error.'); // service에서 email 찾았지만 controller로 받아오지 못함 + } else { + //난수생성 + //비밀번호 변경 + //email 보내기 + } + } catch (err) { + let code; + switch (err.message) { + case 'Can not find profile.': + code = 404; + break; + + default: + code = 500; + break; + } + } +}; + /** * 로그인 페이지를 렌더링한다. */ @@ -263,6 +292,13 @@ const viewChangePassword = (req, res) => { res.render('user/password'); }; +/** + * 비밀번호 찾기페이지를 렌더링한다. + */ +const viewForgotPassword = (req, res) => { + res.render('user/forgot'); +}; + module.exports = { postLogin, postRegister, @@ -271,9 +307,11 @@ module.exports = { postAttendance, getAttendance, editPassword, + forgotPassword, viewLogin, viewRegister, viewProfile, viewAttend, viewChangePassword, + viewForgotPassword, }; diff --git a/BackEnd/src/routes/user.js b/BackEnd/src/routes/user.js index 09cd558..0c083fc 100644 --- a/BackEnd/src/routes/user.js +++ b/BackEnd/src/routes/user.js @@ -72,6 +72,8 @@ router.patch( ], ctrl.editPassword); +router.post('/profile/forgot', /*[],*/ ctrl.forgotPassword); + // token refresh router.get('/token/refresh', issuanceToken); @@ -81,5 +83,6 @@ router.get('/register', ctrl.viewRegister); router.get('/profile/output/', ctrl.viewProfile); router.get('/attendance/output', ctrl.viewAttend); router.get('/profile/password', ctrl.viewChangePassword); +router.get('/profile/forgot', ctrl.viewForgotPassword); module.exports = router; diff --git a/FrontEnd/views/user/forgot.ejs b/FrontEnd/views/user/forgot.ejs new file mode 100644 index 0000000..e69de29 From f755d7639b498c7aaa3b9442f977e3c83676c783 Mon Sep 17 00:00:00 2001 From: choihb Date: Sat, 5 Aug 2023 15:36:25 +0900 Subject: [PATCH 2/6] =?UTF-8?q?Feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B0=BE=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BackEnd/package-lock.json | 14 ++ BackEnd/package.json | 1 + BackEnd/src/controllers/user.js | 379 +++++++++++++++-------------- BackEnd/src/functions/nodemail.js | 29 +++ BackEnd/src/routes/user.js | 4 +- BackEnd/src/services/user.js | 25 ++ FrontEnd/public/js/user/resetpw.js | 26 ++ FrontEnd/views/user/forgot.ejs | 0 FrontEnd/views/user/login.ejs | 1 + FrontEnd/views/user/resetpw.ejs | 26 ++ 10 files changed, 322 insertions(+), 183 deletions(-) create mode 100644 BackEnd/src/functions/nodemail.js create mode 100644 FrontEnd/public/js/user/resetpw.js delete mode 100644 FrontEnd/views/user/forgot.ejs create mode 100644 FrontEnd/views/user/resetpw.ejs diff --git a/BackEnd/package-lock.json b/BackEnd/package-lock.json index 35cbca2..34e5b8b 100644 --- a/BackEnd/package-lock.json +++ b/BackEnd/package-lock.json @@ -26,6 +26,7 @@ "multer": "^1.4.5-lts.1", "multer-s3": "^2.10.0", "mysql2": "^2.3.3", + "nodemailer": "^6.9.4", "sequelize": "^6.32.1", "sequelize-cli": "^6.4.1", "winston": "^3.8.2", @@ -6368,6 +6369,14 @@ "integrity": "sha512-+M0PwXeU80kRohZ3aT4J/OnR+l9/KD2nVLNNoRgFtnf+umQVFdGBAO2N8+nCnEi0xlh/Wk3zOGC+vNNx+uM79Q==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz", + "integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -13459,6 +13468,11 @@ "integrity": "sha512-+M0PwXeU80kRohZ3aT4J/OnR+l9/KD2nVLNNoRgFtnf+umQVFdGBAO2N8+nCnEi0xlh/Wk3zOGC+vNNx+uM79Q==", "dev": true }, + "nodemailer": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz", + "integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==" + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", diff --git a/BackEnd/package.json b/BackEnd/package.json index 191328d..42aa669 100644 --- a/BackEnd/package.json +++ b/BackEnd/package.json @@ -29,6 +29,7 @@ "multer": "^1.4.5-lts.1", "multer-s3": "^2.10.0", "mysql2": "^2.3.3", + "nodemailer": "^6.9.4", "sequelize": "^6.32.1", "sequelize-cli": "^6.4.1", "winston": "^3.8.2", diff --git a/BackEnd/src/controllers/user.js b/BackEnd/src/controllers/user.js index 811dc24..9a17492 100644 --- a/BackEnd/src/controllers/user.js +++ b/BackEnd/src/controllers/user.js @@ -5,6 +5,7 @@ const user = require('../services/user'); const { success, fail } = require('../functions/responseStatus'); const { startDate, endDate, todayDate, firstDay } = require('../functions/common'); +const { passowrdMail, passwordMail } = require('../functions/nodemail'); /** * 제공된 이메일과 비밀번호로 로그인을 시도하고, 성공하면 토큰을 발급한다. @@ -15,28 +16,28 @@ const { startDate, endDate, todayDate, firstDay } = require('../functions/common * @returns {object} { code: number, message: string, access_token: string, refresh_token: string } */ const postLogin = async (req, res) => { - let { email, password } = req.body; - try { - await user.verifyLogin(email, password).then((data) => { - let token = { - access_token: data.access_token, - refresh_token: data.refresh_token, - }; - return success(res, 200, 'Authorize success.', token); - }); - } catch (err) { - let code; - switch (err.message) { - case 'Unauthorized email.': - case 'Incorrect password.': - code = 401; - break; - default: - code = 500; - break; + let { email, password } = req.body; + try { + await user.verifyLogin(email, password).then((data) => { + let token = { + access_token: data.access_token, + refresh_token: data.refresh_token, + }; + return success(res, 200, 'Authorize success.', token); + }); + } catch (err) { + let code; + switch (err.message) { + case 'Unauthorized email.': + case 'Incorrect password.': + code = 401; + break; + default: + code = 500; + break; + } + return fail(res, code, err.message); } - return fail(res, code, err.message); - } }; /** @@ -51,32 +52,32 @@ const postLogin = async (req, res) => { * @returns {object} { code: number, message: string } */ const postRegister = async (req, res) => { - let { email, password, user_name } = req.body; + let { email, password, user_name } = req.body; - try { - let result = await user.verifyRegister(email, password, user_name); - if (result) { - user.createUser(email, password, user_name); - return success(res, 201, 'Register success.'); - } - } catch (err) { - let code; - switch (err.message) { - case 'Exist username.': - case 'Exist email.': - code = 409; - break; - case 'Please input username.': - case 'Please input id.': - case 'Please input password.': - code = 400; - break; - default: - code = 500; - break; + try { + let result = await user.verifyRegister(email, password, user_name); + if (result) { + user.createUser(email, password, user_name); + return success(res, 201, 'Register success.'); + } + } catch (err) { + let code; + switch (err.message) { + case 'Exist username.': + case 'Exist email.': + code = 409; + break; + case 'Please input username.': + case 'Please input id.': + case 'Please input password.': + code = 400; + break; + default: + code = 500; + break; + } + return fail(res, code, err.message); } - return fail(res, code, err.message); - } }; /** @@ -85,21 +86,21 @@ const postRegister = async (req, res) => { * @returns {object} { code: number, data: data } */ const getProfile = async (req, res) => { - try { - const data = await user.findUser('id', req.decoded.id, 0); - return success(res, 200, 'No message', data); - } catch (err) { - let code; - switch (err.message) { - case 'Can not find profile.': - code = 404; - break; - default: - code = 500; - break; + try { + const data = await user.findUser('id', req.decoded.id, 0); + return success(res, 200, 'No message', data); + } catch (err) { + let code; + switch (err.message) { + case 'Can not find profile.': + code = 404; + break; + default: + code = 500; + break; + } + return fail(res, code, err.message); } - return fail(res, code, err.message); - } }; /** @@ -112,33 +113,33 @@ const getProfile = async (req, res) => { * - username, email 변동없을 시 편집 정상 수행 */ const updateProfile = async (req, res) => { - let { user_name, email } = req.body; - let user_id = req.decoded.id; - let data; - try { - let result = await user.updateUser(user_id, email, user_name, req.file); - if (result.message === 'Profile no change.') { - data = result.user; - } else if (result.message === 'Profile edit success.') { - data = result.data; - } - return success(res, 200, result.message, data); - } catch (err) { - let code; - switch (err.message) { - case 'Profile type must be only image.': - code = 400; - break; - case 'The username is already in use.': - case 'The email is already in use.': - code = 409; - break; - default: - code = 500; - break; + let { user_name, email } = req.body; + let user_id = req.decoded.id; + let data; + try { + let result = await user.updateUser(user_id, email, user_name, req.file); + if (result.message === 'Profile no change.') { + data = result.user; + } else if (result.message === 'Profile edit success.') { + data = result.data; + } + return success(res, 200, result.message, data); + } catch (err) { + let code; + switch (err.message) { + case 'Profile type must be only image.': + code = 400; + break; + case 'The username is already in use.': + case 'The email is already in use.': + code = 409; + break; + default: + code = 500; + break; + } + return fail(res, code, err.message); } - return fail(res, code, err.message); - } }; /** @@ -149,21 +150,21 @@ const updateProfile = async (req, res) => { * 출석하지 않았다면 출석 체크를 하고 200반환 */ const postAttendance = async (req, res) => { - let user_id = req.decoded.id; - const today_date = todayDate(); + let user_id = req.decoded.id; + const today_date = todayDate(); - try { - const attendance = await user.findAttendance(user_id, today_date); + try { + const attendance = await user.findAttendance(user_id, today_date); - if (attendance) { - return fail(res, 409, 'Already checked attendance today.'); - } + if (attendance) { + return fail(res, 409, 'Already checked attendance today.'); + } - await user.createAttendance(user_id, today_date); - return success(res, 201, 'Attendance check success.'); - } catch (err) { - return fail(res, 500, err.message); - } + await user.createAttendance(user_id, today_date); + return success(res, 201, 'Attendance check success.'); + } catch (err) { + return fail(res, 500, err.message); + } }; /** @@ -171,22 +172,22 @@ const postAttendance = async (req, res) => { * @returns {object} { code: number, message: string, data: array } */ const getAttendance = async (req, res) => { - try { - const user_id = req.decoded.id; - const start_date = startDate(); - const end_date = endDate(); + try { + const user_id = req.decoded.id; + const start_date = startDate(); + const end_date = endDate(); - const attendance_dates = await user.findAttendanceDate(user_id, start_date, end_date); + const attendance_dates = await user.findAttendanceDate(user_id, start_date, end_date); - const data = attendance_dates.map((attendance) => { - const date = new Date(attendance.attendance_date); - return date.getDate(); - }); + const data = attendance_dates.map((attendance) => { + const date = new Date(attendance.attendance_date); + return date.getDate(); + }); - return success(res, 200, 'No message.', data); - } catch (err) { - return fail(res, 500, err.message); - } + return success(res, 200, 'No message.', data); + } catch (err) { + return fail(res, 500, err.message); + } }; /** @@ -195,123 +196,139 @@ const getAttendance = async (req, res) => { * @param {string} new_password 새 비밀번호 */ const editPassword = async (req, res) => { - let { confirm_password, new_password } = req.body; - let user_id = req.decoded.id; - try { - let result = await user.updatePassword(user_id, confirm_password, new_password); - if (result.message === 'Password changed.') { - let data = result.user; - return success(res, 200, result.message, data); - } else { - throw new Error('Services error.'); - } - } catch (err) { - let code; - switch (err.message) { - case 'Can not find profile.': - code = 404; - break; - case 'Incorrect password.': - code = 401; - break; - default: - code = 500; - break; + let { confirm_password, new_password } = req.body; + let user_id = req.decoded.id; + try { + let result = await user.updatePassword(user_id, confirm_password, new_password); + if (result.message === 'Password changed.') { + let data = result.user; + return success(res, 200, result.message, data); + } else { + throw new Error('Services error.'); + } + } catch (err) { + let code; + switch (err.message) { + case 'Can not find profile.': + code = 404; + break; + case 'Incorrect password.': + code = 401; + break; + default: + code = 500; + break; + } + return fail(res, code, err.message); } - return fail(res, code, err.message); - } }; /** * email을 받아 확인후 새로운 비밀번호 만들어 메일전송 * @param {string} email 사용자가 입력한 기존 비밀번호 */ -const forgotPassword = async (req, res) => { - let { email } = req.body; - try { - let user = await user.findUser('email', email, 0); - if (!user) { - throw new Error('Services error.'); // service에서 email 찾았지만 controller로 받아오지 못함 - } else { - //난수생성 - //비밀번호 변경 - //email 보내기 - } - } catch (err) { - let code; - switch (err.message) { - case 'Can not find profile.': - code = 404; - break; +const resetPassword = async (req, res) => { + let { email } = req.body; + let password; + try { - default: - code = 500; - break; + let user_info = await user.findUser('email', email, 0); + if (!user_info) { + throw new Error('Services error.'); // service에서 email 찾았지만 controller로 받아오지 못함 + } else { + //임시 비밀번호 생성 후 변경 + password = await user.tempPassword(user_info.id); + //email 보내기 + let result = await passwordMail(email, password); + if (result === 'Mail send success.') { + return success(res, 200, result); + } else { throw new Error(result); } + } + } catch (err) { + let code; + switch (err.message) { + case 'Can not find profile.': + code = 404; + break; + case 'Mail send error.': + code = 999; // 에러코드 확인하기 + break; + case 'Password changed failed.' || 'Password changed failePassword rollback failed.': + code = 999; // 에러코드 확인하기 + break; + case 'Mail send fail.': + code = 999; // 에러코드 확인하기 + // 비밀번호 롤백이 필요한지 + break; + default: + code = 500; + break; + } + return fail(res, code, err.message); } - } }; /** * 로그인 페이지를 렌더링한다. */ const viewLogin = (req, res) => { - res.render('user/login'); + res.render('user/login'); }; /** * 회원가입 페이지를 렌더링한다. */ const viewRegister = (req, res) => { - res.render('user/register'); + res.render('user/register'); }; /** * 프로필 페이지를 렌더링한다. */ const viewProfile = (req, res) => { - res.render('user/profile'); + res.render('user/profile'); }; /** * 출석 페이지를 렌더링한다. */ const viewAttend = (req, res) => { - const first_day = firstDay(); - const end_date = endDate(); + const first_day = firstDay(); + const end_date = endDate(); - res.render('user/attendance', { - first_day: first_day, - end_date: end_date, - }); + res.render('user/attendance', { + first_day: first_day, + end_date: end_date, + }); }; /** * 비밀번호 변경페이지를 렌더링한다. */ const viewChangePassword = (req, res) => { - res.render('user/password'); + res.render('user/password'); }; /** * 비밀번호 찾기페이지를 렌더링한다. */ -const viewForgotPassword = (req, res) => { - res.render('user/forgot'); +const viewResetPassword = (req, res) => { + res.render('user/resetpw'); }; module.exports = { - postLogin, - postRegister, - getProfile, - updateProfile, - postAttendance, - getAttendance, - editPassword, - forgotPassword, - viewLogin, - viewRegister, - viewProfile, - viewAttend, - viewChangePassword, - viewForgotPassword, + postLogin, + postRegister, + getProfile, + updateProfile, + postAttendance, + getAttendance, + editPassword, + resetPassword, + viewLogin, + viewRegister, + viewProfile, + viewAttend, + viewChangePassword, + viewResetPassword, }; diff --git a/BackEnd/src/functions/nodemail.js b/BackEnd/src/functions/nodemail.js new file mode 100644 index 0000000..f9ac68d --- /dev/null +++ b/BackEnd/src/functions/nodemail.js @@ -0,0 +1,29 @@ +'use strict'; + +const nodemailer = require('nodemailer'); +const sender_info = require('../../config/senderInfo.json'); + +const transporter = nodemailer.createTransport({ + service: 'gmail', + port: 587, // 보안없는경우 587, 있는경우 465로 설정. 기본은 587 + auth: { + user: sender_info.user, + pass: sender_info.pass + } +}); + +exports.passwordMail = async (email, password) => { + console.log('mailer'); + let mailOptions = { + from: sender_info, + to: email, + subject: 'Temporary Password by CSW_BOARD', + text: `Your temporary password is ${password}.` + };; + let send = await transporter.sendMail(mailOptions); + if (send) { + return 'Mail send success.' + } else { + throw new Error('Mail send fail.'); + } +}; \ No newline at end of file diff --git a/BackEnd/src/routes/user.js b/BackEnd/src/routes/user.js index 0c083fc..2ec9ce1 100644 --- a/BackEnd/src/routes/user.js +++ b/BackEnd/src/routes/user.js @@ -72,7 +72,7 @@ router.patch( ], ctrl.editPassword); -router.post('/profile/forgot', /*[],*/ ctrl.forgotPassword); +router.post('/resetPW', /*[],*/ ctrl.resetPassword); // token refresh router.get('/token/refresh', issuanceToken); @@ -83,6 +83,6 @@ router.get('/register', ctrl.viewRegister); router.get('/profile/output/', ctrl.viewProfile); router.get('/attendance/output', ctrl.viewAttend); router.get('/profile/password', ctrl.viewChangePassword); -router.get('/profile/forgot', ctrl.viewForgotPassword); +router.get('/resetPW', ctrl.viewResetPassword); module.exports = router; diff --git a/BackEnd/src/services/user.js b/BackEnd/src/services/user.js index b639d56..a1b099d 100644 --- a/BackEnd/src/services/user.js +++ b/BackEnd/src/services/user.js @@ -5,6 +5,7 @@ const { Op } = require('sequelize'); const { accessToken, refreshToken } = require('../functions/signJWT'); const bcrypt = require('bcrypt'); +const random = require('crypto'); /** * 사용자 검색 후 return @@ -238,6 +239,29 @@ const updatePassword = async (user_id, confirm_password, new_password) => { } }; +/** + * id 입력받아 임시비밀번호 생성 후 변경 + * @param {number} user_id + * + * @returns {Object} { message: string, data : DBdata } + */ +const tempPassword = async (user_id) => { + //난수 생성 + let temp_pw = parseInt(random.randomBytes(6).toString('hex'), 16).toString(36); + const encrypted_pw = await bcrypt.hash(temp_pw, 10); + const update = await User.update( + { password: encrypted_pw }, + { + where: { id: user_id }, + }, + ); + if (update) { + return temp_pw; + } else { + throw new Error('Password changed failed.'); + } +}; + module.exports = { findUser, createUser, @@ -248,4 +272,5 @@ module.exports = { createAttendance, findAttendanceDate, updatePassword, + tempPassword, }; diff --git a/FrontEnd/public/js/user/resetpw.js b/FrontEnd/public/js/user/resetpw.js new file mode 100644 index 0000000..3c7daeb --- /dev/null +++ b/FrontEnd/public/js/user/resetpw.js @@ -0,0 +1,26 @@ +const email = document.querySelector("#email"); +const button = document.querySelector("#button"); + +button.addEventListener("click", Resetpw); + +function Resetpw() { + if (!email.value) return alert("Please input email."); + + const req = { + email: email.value, + } + + fetch("/user/resetpw", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(req) + }) + .then((res) => res.json()) + .then((res) => { + if (res.code === 200) { + location.href = "/"; + } else return alert(res.message); + }) +} \ No newline at end of file diff --git a/FrontEnd/views/user/forgot.ejs b/FrontEnd/views/user/forgot.ejs deleted file mode 100644 index e69de29..0000000 diff --git a/FrontEnd/views/user/login.ejs b/FrontEnd/views/user/login.ejs index 9375ffc..e22c02b 100644 --- a/FrontEnd/views/user/login.ejs +++ b/FrontEnd/views/user/login.ejs @@ -21,6 +21,7 @@ + forgot your password? diff --git a/FrontEnd/views/user/resetpw.ejs b/FrontEnd/views/user/resetpw.ejs new file mode 100644 index 0000000..9e1c1d4 --- /dev/null +++ b/FrontEnd/views/user/resetpw.ejs @@ -0,0 +1,26 @@ + + + + + <%- include('../partials/head') %> + + + + <%- include('../partials/nav') %> + +
+

Reset Password

+ +
+
+ + +
+ +
+
+ + + + + \ No newline at end of file From 429bb4179204dfac7828085c684f9552d2b22138 Mon Sep 17 00:00:00 2001 From: choihb Date: Sat, 5 Aug 2023 19:31:17 +0900 Subject: [PATCH 3/6] Test: add TDD --- BackEnd/src/controllers/user.js | 5 +-- BackEnd/src/functions/nodemail.js | 1 - BackEnd/src/test/user.test.js | 53 +++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/BackEnd/src/controllers/user.js b/BackEnd/src/controllers/user.js index 9a17492..309a17e 100644 --- a/BackEnd/src/controllers/user.js +++ b/BackEnd/src/controllers/user.js @@ -250,10 +250,7 @@ const resetPassword = async (req, res) => { case 'Can not find profile.': code = 404; break; - case 'Mail send error.': - code = 999; // 에러코드 확인하기 - break; - case 'Password changed failed.' || 'Password changed failePassword rollback failed.': + case 'Password changed failed.': code = 999; // 에러코드 확인하기 break; case 'Mail send fail.': diff --git a/BackEnd/src/functions/nodemail.js b/BackEnd/src/functions/nodemail.js index f9ac68d..1d892cc 100644 --- a/BackEnd/src/functions/nodemail.js +++ b/BackEnd/src/functions/nodemail.js @@ -13,7 +13,6 @@ const transporter = nodemailer.createTransport({ }); exports.passwordMail = async (email, password) => { - console.log('mailer'); let mailOptions = { from: sender_info, to: email, diff --git a/BackEnd/src/test/user.test.js b/BackEnd/src/test/user.test.js index 9015c19..19c3b05 100644 --- a/BackEnd/src/test/user.test.js +++ b/BackEnd/src/test/user.test.js @@ -9,6 +9,7 @@ const user = require('../controllers/user'); const { path, config, chalk } = require('../../loaders/module'); const bcrypt = require('bcrypt'); +//const { describe } = require('../models/user'); /** * * 로그인 테스트 @@ -538,3 +539,55 @@ describe('passwordChange', () => { }); }); }); + +/** + * *비밀번호 찾기 테스트 + * 1. 임시 비밀번호 전송 선공 + * 2. 프로필 조회 실패 + * 3. 비밀번호 변경 실패 + * 4. 메일 전송 실패 + */ +describe('resetPassword', () => { + let req, res; + + beforeEach(() => { + req = { + body: { + email: 'test_user@example.com', + }, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + }); + afterEach(async () => { + let encrypted_pw = await bcrypt.hash('password', 10); + await User.update({ password: encrypted_pw }, { where: { id: '1' } }); + jest.clearAllMocks(); + }); + + //임시 비밀번호 전송 성공 + test(`should return ${chalk.green(200)} if ${chalk.blue(`password changed`)}`, async () => { + await user.resetPassword(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + code: 200, + message: 'Mail send success.', + data: 'No data.', + }); + }); + + //프로필 조회 실패 + test(`should return ${chalk.yellow(404)} if ${chalk.blue(`Can not find profile.`)}`, async () => { + req.body.email = 'different_user@example.com'; + await user.resetPassword(req, res); + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + message: 'Can not find profile.', + detail: 'No detail.', + }); + }); + //비밀번호 변경 실패 (에러 케이스 확인 필요) + //메일전송 실패 (에러 케이스 확인 필요) 1. 메일비밀번호 오류등, API 관련 에러 +}); \ No newline at end of file From 9e1996230f89492f7d85e1a166809c218bd61463 Mon Sep 17 00:00:00 2001 From: choihb Date: Sat, 5 Aug 2023 19:33:16 +0900 Subject: [PATCH 4/6] Refactor: add validator --- BackEnd/src/routes/user.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/BackEnd/src/routes/user.js b/BackEnd/src/routes/user.js index 2ec9ce1..543e0f5 100644 --- a/BackEnd/src/routes/user.js +++ b/BackEnd/src/routes/user.js @@ -72,7 +72,14 @@ router.patch( ], ctrl.editPassword); -router.post('/resetPW', /*[],*/ ctrl.resetPassword); +router.post('/resetPW', [ + check('email') + .isEmail() + .withMessage('Email must be in the correct format.') + .isLength({ max: 30 }) + .withMessage('Email must be shorter than 31 characters.'), + validator, +], ctrl.resetPassword); // token refresh router.get('/token/refresh', issuanceToken); From 7e378a3945ceb892e43872e49f2c7de2571a21bb Mon Sep 17 00:00:00 2001 From: choihb Date: Thu, 31 Aug 2023 00:02:51 +0900 Subject: [PATCH 5/6] CHANGE: change PWchange, forgotPW. --- BackEnd/package-lock.json | 11 + BackEnd/package.json | 1 + BackEnd/src/controllers/user.js | 134 +++++++--- BackEnd/src/functions/nodemail.js | 9 +- BackEnd/src/functions/signJWT.js | 8 + BackEnd/src/routes/user.js | 38 ++- BackEnd/src/services/user.js | 93 +++++-- BackEnd/src/test/user.test.js | 243 +++++++++--------- .../js/user/{password.js => newPassword.js} | 12 +- FrontEnd/public/js/user/resetpw.js | 26 -- FrontEnd/public/js/user/verifyEmail.js | 93 +++++++ FrontEnd/public/js/user/verifyPassword.js | 39 +++ FrontEnd/views/user/login.ejs | 2 +- .../user/{password.ejs => newPassword.ejs} | 10 +- FrontEnd/views/user/profile.ejs | 2 +- FrontEnd/views/user/resetpw.ejs | 26 -- FrontEnd/views/user/verifyEmail.ejs | 37 +++ FrontEnd/views/user/verifyPassword.ejs | 31 +++ 18 files changed, 553 insertions(+), 262 deletions(-) rename FrontEnd/public/js/user/{password.js => newPassword.js} (73%) delete mode 100644 FrontEnd/public/js/user/resetpw.js create mode 100644 FrontEnd/public/js/user/verifyEmail.js create mode 100644 FrontEnd/public/js/user/verifyPassword.js rename FrontEnd/views/user/{password.ejs => newPassword.ejs} (71%) delete mode 100644 FrontEnd/views/user/resetpw.ejs create mode 100644 FrontEnd/views/user/verifyEmail.ejs create mode 100644 FrontEnd/views/user/verifyPassword.ejs diff --git a/BackEnd/package-lock.json b/BackEnd/package-lock.json index e1425e4..ed4e7e9 100644 --- a/BackEnd/package-lock.json +++ b/BackEnd/package-lock.json @@ -21,6 +21,7 @@ "express-validator": "^7.0.1", "helmet": "^7.0.0", "jsonwebtoken": "^9.0.1", + "memory-cache": "^0.2.0", "method-override": "^3.0.0", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", @@ -5775,6 +5776,11 @@ "timers-ext": "^0.1.7" } }, + "node_modules/memory-cache": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", + "integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==" + }, "node_modules/meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", @@ -12998,6 +13004,11 @@ "timers-ext": "^0.1.7" } }, + "memory-cache": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", + "integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==" + }, "meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", diff --git a/BackEnd/package.json b/BackEnd/package.json index c4e2012..4bfe775 100644 --- a/BackEnd/package.json +++ b/BackEnd/package.json @@ -24,6 +24,7 @@ "express-validator": "^7.0.1", "helmet": "^7.0.0", "jsonwebtoken": "^9.0.1", + "memory-cache": "^0.2.0", "method-override": "^3.0.0", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", diff --git a/BackEnd/src/controllers/user.js b/BackEnd/src/controllers/user.js index 309a17e..3f7f092 100644 --- a/BackEnd/src/controllers/user.js +++ b/BackEnd/src/controllers/user.js @@ -5,7 +5,7 @@ const user = require('../services/user'); const { success, fail } = require('../functions/responseStatus'); const { startDate, endDate, todayDate, firstDay } = require('../functions/common'); -const { passowrdMail, passwordMail } = require('../functions/nodemail'); +const { verifycodeMail } = require('../functions/nodemail'); /** * 제공된 이메일과 비밀번호로 로그인을 시도하고, 성공하면 토큰을 발급한다. @@ -191,15 +191,47 @@ const getAttendance = async (req, res) => { }; /** - * 현재 비밀번호, 새 비밀번호, 비밀번호 확인 입력받아 비밀번호 변경 - * @param {string} confirm_password 사용자가 입력한 기존 비밀번호 + * 비밀번호 재확인 + * @param {string} confirm_password 기존 비밀번호 + * + */ +const checkPassword = async (req, res) => { + let { confirm_password } = req.body; + let user_id = req.decoded.id; + try { + //비밀번호 확인 + let result = await user.comparePassword(confirm_password, user_id); + if (result) { + //일회성 토큰 발급 + await user.issueOneTimeToken(result).then((data) => { + let token = data; + return success(res, 200, 'Authorize success.', token); + }); + } + } catch (err) { + let code; + switch (err.message) { + case 'Incorrect password.': + code = 401; + break; + default: + code = 500; + break; + } + return fail(res, code, err.message); + } +}; + +/** + * 새 비밀번호 입력받아 비밀번호 변경 * @param {string} new_password 새 비밀번호 */ const editPassword = async (req, res) => { - let { confirm_password, new_password } = req.body; + let { new_password } = req.body; let user_id = req.decoded.id; try { - let result = await user.updatePassword(user_id, confirm_password, new_password); + if (req.decoded.type !== 'OneTimeJWT') { throw new Error('token is invalid.'); } + let result = await user.updatePassword(user_id, new_password); if (result.message === 'Password changed.') { let data = result.user; return success(res, 200, result.message, data); @@ -209,12 +241,12 @@ const editPassword = async (req, res) => { } catch (err) { let code; switch (err.message) { + case 'token is invalid.': + code = 403; + break; case 'Can not find profile.': code = 404; break; - case 'Incorrect password.': - code = 401; - break; default: code = 500; break; @@ -224,25 +256,22 @@ const editPassword = async (req, res) => { }; /** - * email을 받아 확인후 새로운 비밀번호 만들어 메일전송 + * email을 받아 확인후 인증번호 메일전송 * @param {string} email 사용자가 입력한 기존 비밀번호 */ -const resetPassword = async (req, res) => { - let { email } = req.body; - let password; +const sendVerifyEmail = async (req, res) => { + let { email } = req.body try { - - let user_info = await user.findUser('email', email, 0); - if (!user_info) { - throw new Error('Services error.'); // service에서 email 찾았지만 controller로 받아오지 못함 + let result = await user.findUser('email', email, 0); + if (result) { + // 인증번호, 캐시저장 + let verifycode = await user.verifycode(email); + console.log(verifycode); // 임시 + //인증번호 전송 메일 (비동기) + verifycodeMail(email, verifycode); + return success(res, 200); } else { - //임시 비밀번호 생성 후 변경 - password = await user.tempPassword(user_info.id); - //email 보내기 - let result = await passwordMail(email, password); - if (result === 'Mail send success.') { - return success(res, 200, result); - } else { throw new Error(result); } + throw new Error('Services error.'); } } catch (err) { let code; @@ -250,12 +279,39 @@ const resetPassword = async (req, res) => { case 'Can not find profile.': code = 404; break; - case 'Password changed failed.': - code = 999; // 에러코드 확인하기 + default: + code = 500; break; - case 'Mail send fail.': - code = 999; // 에러코드 확인하기 - // 비밀번호 롤백이 필요한지 + } + return fail(res, code, err.message); + } +}; + + +/** + * 이메일, 인증번호 입력받아 확인 후 새 비밀번호 페이지로 넘겨줌 + * @param {string} email 사용자가 입력한 기존 비밀번호 + * @param {number} verifycode 입력한 인증번호 + */ +const checkVerifyCode = async (req, res) => { + let { email, verifycode } = req.body + try { + //인증번호 확인 + let result = user.checkCode(email, verifycode); + //새비밀번호 페이지로 넘기기 + if (result) { + //일회성 토큰 발급 + await user.issueOneTimeToken(email).then((data) => { + let token = data; + return success(res, 200, 'Authorize success.', token); + }); + } + } catch (err) { + let code; + switch (err.message) { + case 'Code dosesn\'t match.': //401이나 403? + case 'Verifycode expired.': + code = 409; break; default: code = 500; @@ -299,18 +355,25 @@ const viewAttend = (req, res) => { }); }; +/** + * 비밀번호 확인페이지를 렌더링한다. + */ +const viewVerifyPassword = (req, res) => { + res.render('user/verifyPassword'); +}; + /** * 비밀번호 변경페이지를 렌더링한다. */ const viewChangePassword = (req, res) => { - res.render('user/password'); + res.render('user/newPassword'); }; /** - * 비밀번호 찾기페이지를 렌더링한다. + * 메일 인증페이지를 렌더링한다. */ -const viewResetPassword = (req, res) => { - res.render('user/resetpw'); +const viewverifyEmail = (req, res) => { + res.render('user/verifyEmail'); }; module.exports = { @@ -320,12 +383,15 @@ module.exports = { updateProfile, postAttendance, getAttendance, + checkPassword, editPassword, - resetPassword, + sendVerifyEmail, + checkVerifyCode, viewLogin, viewRegister, viewProfile, viewAttend, + viewVerifyPassword, viewChangePassword, - viewResetPassword, + viewverifyEmail, }; diff --git a/BackEnd/src/functions/nodemail.js b/BackEnd/src/functions/nodemail.js index 1d892cc..5601584 100644 --- a/BackEnd/src/functions/nodemail.js +++ b/BackEnd/src/functions/nodemail.js @@ -12,15 +12,16 @@ const transporter = nodemailer.createTransport({ } }); -exports.passwordMail = async (email, password) => { +exports.verifycodeMail = async (email, code) => { let mailOptions = { from: sender_info, to: email, - subject: 'Temporary Password by CSW_BOARD', - text: `Your temporary password is ${password}.` - };; + subject: 'Verifying Code by CSW_BOARD', + text: `Your Verifycode is ${code}.` + }; let send = await transporter.sendMail(mailOptions); if (send) { + console.log('send OK'); return 'Mail send success.' } else { throw new Error('Mail send fail.'); diff --git a/BackEnd/src/functions/signJWT.js b/BackEnd/src/functions/signJWT.js index ce184fd..d76ef5e 100644 --- a/BackEnd/src/functions/signJWT.js +++ b/BackEnd/src/functions/signJWT.js @@ -21,6 +21,13 @@ const refreshToken = (payload) => { }); }; +const oneTimeToken = (payload) => { + return jwt.sign(payload, access_secret_key, { + expiresIn: '5m', + issuer: config.get('JWT.issuer'), + }); +}; + const issuanceToken = (req, res) => { return jwt.verify(req.headers.authorization, refresh_secret_key, (err, decoded) => { if (err) { @@ -41,5 +48,6 @@ const issuanceToken = (req, res) => { module.exports = { accessToken, refreshToken, + oneTimeToken, issuanceToken, }; diff --git a/BackEnd/src/routes/user.js b/BackEnd/src/routes/user.js index 543e0f5..794b8bd 100644 --- a/BackEnd/src/routes/user.js +++ b/BackEnd/src/routes/user.js @@ -62,24 +62,42 @@ router.patch( router.post('/attendance', auth, ctrl.postAttendance); router.get('/attendance', auth, ctrl.getAttendance); -router.patch( - '/profile/password', +router.post( + '/verifyPassword', auth, [ - check('confirm_password', 'Please input password.').notEmpty(), - check('new_password', 'Password must be longer than 2 characters & shorter than 101 characters.').isLength({ min: 3, max: 100 }), + check('confirm_password', 'Password must be shorter than 101 characters.').isLength({ max: 100 }), validator, ], - ctrl.editPassword); + ctrl.checkPassword); + +router.post('/sendEmail', [ + check('email') + .isEmail() + .withMessage('Email must be in the correct format.') + .isLength({ max: 30 }) + .withMessage('Email must be shorter than 31 characters.'), + validator, +], ctrl.sendVerifyEmail); -router.post('/resetPW', [ +router.post('/verifyEmail', [ check('email') .isEmail() .withMessage('Email must be in the correct format.') .isLength({ max: 30 }) .withMessage('Email must be shorter than 31 characters.'), + check('verifycode', 'Please input code.').notEmpty(), validator, -], ctrl.resetPassword); +], ctrl.checkVerifyCode); + +router.patch( + '/newPassword', + auth, + [ + check('new_password', 'Password must be longer than 2 characters & shorter than 101 characters.').isLength({ min: 3, max: 100 }), + validator, + ], + ctrl.editPassword); // token refresh router.get('/token/refresh', issuanceToken); @@ -89,7 +107,9 @@ router.get('/login', ctrl.viewLogin); router.get('/register', ctrl.viewRegister); router.get('/profile/output/', ctrl.viewProfile); router.get('/attendance/output', ctrl.viewAttend); -router.get('/profile/password', ctrl.viewChangePassword); -router.get('/resetPW', ctrl.viewResetPassword); +router.get('/verifyPassword', ctrl.viewVerifyPassword); // 비밀번호 확인 페이지 +router.get('/verifyEmail', ctrl.viewverifyEmail); // 인증번호 보내고 체크하는 페이지 +router.get('/newPassword', ctrl.viewChangePassword); // 비밀번호 변경 페이지 + module.exports = router; diff --git a/BackEnd/src/services/user.js b/BackEnd/src/services/user.js index a1b099d..ad0d29d 100644 --- a/BackEnd/src/services/user.js +++ b/BackEnd/src/services/user.js @@ -3,10 +3,13 @@ const { User, Attendance } = require('../utils/connect'); const { Op } = require('sequelize'); -const { accessToken, refreshToken } = require('../functions/signJWT'); +const { accessToken, refreshToken, oneTimeToken } = require('../functions/signJWT'); const bcrypt = require('bcrypt'); const random = require('crypto'); +const cache = require('memory-cache'); + + /** * 사용자 검색 후 return * @param {string} field 검색할 필드명 ('email' 또는 'id') @@ -207,15 +210,13 @@ const findAttendanceDate = async (user_id, start_date, end_date) => { }; /** - * 비밀번호 입력받아 확인 후, 비밀번호 변경 + * 비밀번호 확인 + * @param {string} confirm_password 비밀번호 * @param {number} user_id - * @param {string} confirm_password 사용자가 입력한 기존 비밀번호 - * @param {string} new_password 새 비밀번호 * - * @returns {Object} { message: string, data : DBdata } + * @returns {string} email */ -const updatePassword = async (user_id, confirm_password, new_password) => { - let message = ''; +const comparePassword = async (confirm_password, user_id) => { const user = await User.findByPk(user_id); if (user === null) { throw new Error('Can not find profile.'); @@ -223,6 +224,23 @@ const updatePassword = async (user_id, confirm_password, new_password) => { const match = await bcrypt.compare(confirm_password, user.password); if (!match) { throw new Error('Incorrect password.'); + } else { + return user.email; + } +}; + +/** + * 비밀번호 변경 + * @param {number} user_id + * @param {string} new_password 새 비밀번호 + * + * @returns {Object} { message: string, data : DBdata } + */ +const updatePassword = async (user_id, new_password) => { + let message = ''; + const user = await User.findByPk(user_id); + if (user === null) { + throw new Error('Can not find profile.'); } const encrypted_pw = await bcrypt.hash(new_password, 10); const data = await User.update( @@ -240,28 +258,52 @@ const updatePassword = async (user_id, confirm_password, new_password) => { }; /** - * id 입력받아 임시비밀번호 생성 후 변경 - * @param {number} user_id + * email을 입력 받아 인증번호 생성 후 캐시메모리에 저장 + * @param {string} email * - * @returns {Object} { message: string, data : DBdata } + * @returns {string} code */ -const tempPassword = async (user_id) => { - //난수 생성 - let temp_pw = parseInt(random.randomBytes(6).toString('hex'), 16).toString(36); - const encrypted_pw = await bcrypt.hash(temp_pw, 10); - const update = await User.update( - { password: encrypted_pw }, - { - where: { id: user_id }, - }, - ); - if (update) { - return temp_pw; +const verifycode = (email) => { + let code = parseInt(random.randomBytes(2).toString('hex'), 16).toString(10); + //캐시메모리에 저장 (캐시 메모리 너무 많이 쌓이는 경우?) + cache.put(email, code, 300000, (key, value) => { + console.log('key: ' + key + ' value: ' + value + ' timeout'); // 로그로 변경필요 + }); // key: email, value: code, 300000ms (5min) 후 삭제 + return code; +}; + +/** + * 인증번호 체크 + * @param {string} email + * @param {number} verifycode + * + * @returns {boolean} + */ +const checkCode = (email, verifycode) => { + let cachecode = cache.get(email); + if (cachecode) { + if (parseInt(verifycode) !== parseInt(cachecode)) { + throw new Error('Code dosesn\'t match.'); + } else { + return true; + } } else { - throw new Error('Password changed failed.'); + throw new Error('Verifycode expired.'); } }; +/** + * 일회용 토큰 발급 + * @param {string} email + * + * @returns {object} data + */ +const issueOneTimeToken = async (email) => { + let user = await findUser('email', email, 1); + let one_time_access_token = await oneTimeToken({ type: 'OneTimeJWT', id: user.id }); + return one_time_access_token; +} + module.exports = { findUser, createUser, @@ -271,6 +313,9 @@ module.exports = { findAttendance, createAttendance, findAttendanceDate, + comparePassword, updatePassword, - tempPassword, + verifycode, + checkCode, + issueOneTimeToken, }; diff --git a/BackEnd/src/test/user.test.js b/BackEnd/src/test/user.test.js index 19c3b05..12c6511 100644 --- a/BackEnd/src/test/user.test.js +++ b/BackEnd/src/test/user.test.js @@ -9,7 +9,6 @@ const user = require('../controllers/user'); const { path, config, chalk } = require('../../loaders/module'); const bcrypt = require('bcrypt'); -//const { describe } = require('../models/user'); /** * * 로그인 테스트 @@ -470,124 +469,124 @@ describe('getAttendance', () => { }); }); -/** - * *비밀번호 변경 테스트 - * 1. 비밀번호 변경 성공 - * 2. 비밀번호 변경 실패 (비밀번호 에러) - * 3. 프로필 조회 실패 - */ -describe('passwordChange', () => { - let req, res; - - beforeEach(() => { - req = { - body: { - confirm_password: 'password', - new_password: 'newpassword', - }, - decoded: { id: '1' }, - }; - res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - }; - }); - afterEach(async () => { - let encrypted_pw = await bcrypt.hash('password', 10); - await User.update({ password: encrypted_pw }, { where: { id: '1' } }); - jest.clearAllMocks(); - }); - - //비밀번호 변경 성공 - test(`should return ${chalk.green(200)} if ${chalk.blue(`password changed`)}`, async () => { - await user.editPassword(req, res); - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith( - expect.objectContaining({ - code: 200, - message: 'Password changed.', - data: expect.objectContaining({ - id: 1, - user_name: 'test_user', - email: 'test_user@example.com', - }), - }), - ); - }); - - //비밀번호 변경 실패 (비밀번호 오류) - test(`should return ${chalk.yellow(401)} if ${chalk.blue(`incorrect password`)}`, async () => { - req.body.confirm_password = 'differentpassword'; - await user.editPassword(req, res); - - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ - detail: 'No detail.', - message: 'Incorrect password.', - }); - }); - - //비밀번호 변경 실패 (프로필 조회 실패) - test(`should return ${chalk.yellow(404)} if ${chalk.blue(`can not find profile`)}`, async () => { - req.decoded.id = 0; - await user.editPassword(req, res); - - expect(res.status).toHaveBeenCalledWith(404); - expect(res.json).toHaveBeenCalledWith({ - detail: 'No detail.', - message: 'Can not find profile.', - }); - }); -}); - -/** - * *비밀번호 찾기 테스트 - * 1. 임시 비밀번호 전송 선공 - * 2. 프로필 조회 실패 - * 3. 비밀번호 변경 실패 - * 4. 메일 전송 실패 - */ -describe('resetPassword', () => { - let req, res; - - beforeEach(() => { - req = { - body: { - email: 'test_user@example.com', - }, - }; - res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - }; - }); - afterEach(async () => { - let encrypted_pw = await bcrypt.hash('password', 10); - await User.update({ password: encrypted_pw }, { where: { id: '1' } }); - jest.clearAllMocks(); - }); - - //임시 비밀번호 전송 성공 - test(`should return ${chalk.green(200)} if ${chalk.blue(`password changed`)}`, async () => { - await user.resetPassword(req, res); - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith({ - code: 200, - message: 'Mail send success.', - data: 'No data.', - }); - }); - - //프로필 조회 실패 - test(`should return ${chalk.yellow(404)} if ${chalk.blue(`Can not find profile.`)}`, async () => { - req.body.email = 'different_user@example.com'; - await user.resetPassword(req, res); - expect(res.status).toHaveBeenCalledWith(404); - expect(res.json).toHaveBeenCalledWith({ - message: 'Can not find profile.', - detail: 'No detail.', - }); - }); - //비밀번호 변경 실패 (에러 케이스 확인 필요) - //메일전송 실패 (에러 케이스 확인 필요) 1. 메일비밀번호 오류등, API 관련 에러 -}); \ No newline at end of file +// /** +// * *비밀번호 변경 테스트 +// * 1. 비밀번호 변경 성공 +// * 2. 비밀번호 변경 실패 (비밀번호 에러) +// * 3. 프로필 조회 실패 +// */ +// describe('passwordChange', () => { +// let req, res; + +// beforeEach(() => { +// req = { +// body: { +// confirm_password: 'password', +// new_password: 'newpassword', +// }, +// decoded: { id: '1' }, +// }; +// res = { +// status: jest.fn().mockReturnThis(), +// json: jest.fn(), +// }; +// }); +// afterEach(async () => { +// let encrypted_pw = await bcrypt.hash('password', 10); +// await User.update({ password: encrypted_pw }, { where: { id: '1' } }); +// jest.clearAllMocks(); +// }); + +// //비밀번호 변경 성공 +// test(`should return ${chalk.green(200)} if ${chalk.blue(`password changed`)}`, async () => { +// await user.editPassword(req, res); +// expect(res.status).toHaveBeenCalledWith(200); +// expect(res.json).toHaveBeenCalledWith( +// expect.objectContaining({ +// code: 200, +// message: 'Password changed.', +// data: expect.objectContaining({ +// id: 1, +// user_name: 'test_user', +// email: 'test_user@example.com', +// }), +// }), +// ); +// }); + +// //비밀번호 변경 실패 (비밀번호 오류) +// test(`should return ${chalk.yellow(401)} if ${chalk.blue(`incorrect password`)}`, async () => { +// req.body.confirm_password = 'differentpassword'; +// await user.editPassword(req, res); + +// expect(res.status).toHaveBeenCalledWith(401); +// expect(res.json).toHaveBeenCalledWith({ +// detail: 'No detail.', +// message: 'Incorrect password.', +// }); +// }); + +// //비밀번호 변경 실패 (프로필 조회 실패) +// test(`should return ${chalk.yellow(404)} if ${chalk.blue(`can not find profile`)}`, async () => { +// req.decoded.id = 0; +// await user.editPassword(req, res); + +// expect(res.status).toHaveBeenCalledWith(404); +// expect(res.json).toHaveBeenCalledWith({ +// detail: 'No detail.', +// message: 'Can not find profile.', +// }); +// }); +// }); + +// /** +// * *비밀번호 찾기 테스트 +// * 1. 임시 비밀번호 전송 선공 +// * 2. 프로필 조회 실패 +// * 3. 비밀번호 변경 실패 +// * 4. 메일 전송 실패 +// */ +// describe('resetPassword', () => { +// let req, res; + +// beforeEach(() => { +// req = { +// body: { +// email: 'test_user@example.com', +// }, +// }; +// res = { +// status: jest.fn().mockReturnThis(), +// json: jest.fn(), +// }; +// }); +// afterEach(async () => { +// let encrypted_pw = await bcrypt.hash('password', 10); +// await User.update({ password: encrypted_pw }, { where: { id: '1' } }); +// jest.clearAllMocks(); +// }); + +// //임시 비밀번호 전송 성공 +// test(`should return ${chalk.green(200)} if ${chalk.blue(`password changed`)}`, async () => { +// await user.resetPassword(req, res); +// expect(res.status).toHaveBeenCalledWith(200); +// expect(res.json).toHaveBeenCalledWith({ +// code: 200, +// message: 'Mail send success.', +// data: 'No data.', +// }); +// }); + +// //프로필 조회 실패 +// test(`should return ${chalk.yellow(404)} if ${chalk.blue(`Can not find profile.`)}`, async () => { +// req.body.email = 'different_user@example.com'; +// await user.resetPassword(req, res); +// expect(res.status).toHaveBeenCalledWith(404); +// expect(res.json).toHaveBeenCalledWith({ +// message: 'Can not find profile.', +// detail: 'No detail.', +// }); +// }); +// //비밀번호 변경 실패 (에러 케이스 확인 필요) +// //메일전송 실패 (에러 케이스 확인 필요) 1. 메일비밀번호 오류등, API 관련 에러 +// }); \ No newline at end of file diff --git a/FrontEnd/public/js/user/password.js b/FrontEnd/public/js/user/newPassword.js similarity index 73% rename from FrontEnd/public/js/user/password.js rename to FrontEnd/public/js/user/newPassword.js index c4b84dd..184b4d9 100644 --- a/FrontEnd/public/js/user/password.js +++ b/FrontEnd/public/js/user/newPassword.js @@ -2,14 +2,13 @@ const pw_change_btn = document.querySelector("#pw_change_btn"); -pw_change_btn.addEventListener("click", passwordPatch); +pw_change_btn.addEventListener("click", newPassword); -function passwordPatch() { - const confirm_password = document.getElementsByName("old_password")[0].value; +function newPassword() { const new_password = document.getElementsByName("new_password")[0].value; const new_password_confirm = document.getElementsByName("new_password_confirm")[0].value; - if (!(confirm_password && new_password && new_password_confirm)) { + if (!(new_password && new_password_confirm)) { return alert("Please input password."); } else if (new_password !== new_password_confirm) { return alert("confirm password does not match."); @@ -18,15 +17,14 @@ function passwordPatch() { } const req = { - confirm_password: confirm_password, new_password: new_password, }; - fetch("/user/profile/password", { + fetch("/user/newPassword", { method: 'PATCH', headers: { 'content-type': 'application/json', - authorization: localStorage.getItem('access_token') + authorization: localStorage.getItem('one_time_access_token') }, body: JSON.stringify(req) }).then((res) => res.json()) diff --git a/FrontEnd/public/js/user/resetpw.js b/FrontEnd/public/js/user/resetpw.js deleted file mode 100644 index 3c7daeb..0000000 --- a/FrontEnd/public/js/user/resetpw.js +++ /dev/null @@ -1,26 +0,0 @@ -const email = document.querySelector("#email"); -const button = document.querySelector("#button"); - -button.addEventListener("click", Resetpw); - -function Resetpw() { - if (!email.value) return alert("Please input email."); - - const req = { - email: email.value, - } - - fetch("/user/resetpw", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(req) - }) - .then((res) => res.json()) - .then((res) => { - if (res.code === 200) { - location.href = "/"; - } else return alert(res.message); - }) -} \ No newline at end of file diff --git a/FrontEnd/public/js/user/verifyEmail.js b/FrontEnd/public/js/user/verifyEmail.js new file mode 100644 index 0000000..e0ec953 --- /dev/null +++ b/FrontEnd/public/js/user/verifyEmail.js @@ -0,0 +1,93 @@ +const email = document.querySelector("#email"); +const email_button = document.querySelector("#email_button"); +const verifycode = document.querySelector("#verifycode"); +const submit_button = document.querySelector("#submit_button"); + +email_button.addEventListener("click", sendEmail); +submit_button.addEventListener("click", checkCode); + +const Timer = document.getElementById("timer"); //타이머 +let time = 300000; +let min = 5; +let sec = 60; + +Timer.value = 'left time: ' + min + ':' + '00'; + +function timer() { + playtime = setInterval(() => { + time = time - 1000; + min = time / (60 * 1000); + if (sec > 0) { + sec = sec - 1; + Timer.value = 'left time: ' + Math.floor(min) + ':' + sec; + } + if (sec === 0) { + sec = 60; + Timer.value = 'left time: ' + Math.floor(min) + ':' + '00'; + } + }, 1000); +}; + + +function sendEmail() { + if (!email.value) return alert("Please input email."); + + const req = { + email: email.value, + } + + if (Timer.value !== '') { + Timer.value = ''; + time = 300000; + min = 5; + sec = 60; + } + + fetch("/user/sendEmail", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(req) + }) + .then((res) => res.json()) + .then((res) => { + if (res.code === 200) { + console.log('res good'); + email_button.innerText = "resend"; + document.getElementById("verify").style.display = "block"; + timer(); + setTimeout(() => { + clearInterval(playtime); + Timer.value = "code expired. please resend" + }, 300000); + } else return alert(res.message); + }) +}; + +function checkCode() { + if (!verifycode.value) return alert("Please enter code."); + + const req = { + email: email.value, + verifycode: verifycode.value, + } + console.log(req); + + fetch("/user/verifyEmail", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(req) + }) + .then((res) => res.json()) + .then((res) => { + if (res.code === 200) { + //일회성 토큰저장 + localStorage.setItem("one_time_access_token", res.data); + //리다이렉트 + location.href = "/user/newPassword"; + } else return alert(res.message); + }) +}; diff --git a/FrontEnd/public/js/user/verifyPassword.js b/FrontEnd/public/js/user/verifyPassword.js new file mode 100644 index 0000000..bea2cec --- /dev/null +++ b/FrontEnd/public/js/user/verifyPassword.js @@ -0,0 +1,39 @@ +'use strict'; + +const pw_change_btn = document.querySelector("#pw_btn"); + +pw_change_btn.addEventListener("click", checkPassword); + +function checkPassword() { + const confirm_password = document.getElementsByName("old_password")[0].value; + + if (!confirm_password) { + return alert("Please input password."); + } else if (confirm_password.length > 100) { + return alert("Password must be shorter than 101 characters."); + } + + const req = { + confirm_password: confirm_password, + }; + + fetch("/user/verifyPassword", { + method: 'POST', + headers: { + 'content-type': 'application/json', + authorization: localStorage.getItem('access_token') + }, + body: JSON.stringify(req) + }).then((res) => res.json()) + .then((res) => { + if (res.code === 200) { + //일회성 토큰저장 + localStorage.setItem("one_time_access_token", res.data); + location.href = "/user/newPassword" + } else { + alert(res.message); + location.reload(); + } + }) + +} \ No newline at end of file diff --git a/FrontEnd/views/user/login.ejs b/FrontEnd/views/user/login.ejs index e22c02b..7fbba00 100644 --- a/FrontEnd/views/user/login.ejs +++ b/FrontEnd/views/user/login.ejs @@ -21,7 +21,7 @@ - forgot your password? + forgot your password? diff --git a/FrontEnd/views/user/password.ejs b/FrontEnd/views/user/newPassword.ejs similarity index 71% rename from FrontEnd/views/user/password.ejs rename to FrontEnd/views/user/newPassword.ejs index 37c83c8..5168671 100644 --- a/FrontEnd/views/user/password.ejs +++ b/FrontEnd/views/user/newPassword.ejs @@ -9,16 +9,10 @@ <%- include('../partials/nav') %>
-

Password Change

+

New Password

-
- -
- -
-
@@ -37,7 +31,7 @@
- + \ No newline at end of file diff --git a/FrontEnd/views/user/profile.ejs b/FrontEnd/views/user/profile.ejs index 3c0c895..f348b83 100644 --- a/FrontEnd/views/user/profile.ejs +++ b/FrontEnd/views/user/profile.ejs @@ -58,7 +58,7 @@ if (!localStorage.getItem('access_token')) { alert("Please login first."); location.href = '/user/login'; - } else { location.href = '/user/profile/password'; } + } else { location.href = '/user/verifyPassword'; } } diff --git a/FrontEnd/views/user/resetpw.ejs b/FrontEnd/views/user/resetpw.ejs deleted file mode 100644 index 9e1c1d4..0000000 --- a/FrontEnd/views/user/resetpw.ejs +++ /dev/null @@ -1,26 +0,0 @@ - - - - - <%- include('../partials/head') %> - - - - <%- include('../partials/nav') %> - -
-

Reset Password

- -
-
- - -
- -
-
- - - - - \ No newline at end of file diff --git a/FrontEnd/views/user/verifyEmail.ejs b/FrontEnd/views/user/verifyEmail.ejs new file mode 100644 index 0000000..0e75388 --- /dev/null +++ b/FrontEnd/views/user/verifyEmail.ejs @@ -0,0 +1,37 @@ + + + + + <%- include('../partials/head') %> + + + + <%- include('../partials/nav') %> + +
+

Reset Password

+ +
+
+ + +
+ +
+
+ + + + + + \ No newline at end of file diff --git a/FrontEnd/views/user/verifyPassword.ejs b/FrontEnd/views/user/verifyPassword.ejs new file mode 100644 index 0000000..da7f005 --- /dev/null +++ b/FrontEnd/views/user/verifyPassword.ejs @@ -0,0 +1,31 @@ + + + + + <%- include('../partials/head') %> + + + + <%- include('../partials/nav') %> + +
+

Password Change

+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+ + + + \ No newline at end of file From 475b2e60013166c0f9cfd00c3eb72982fd5dcfba Mon Sep 17 00:00:00 2001 From: choihb Date: Sat, 4 Nov 2023 23:04:24 +0900 Subject: [PATCH 6/6] CHANGE: change find password --- BackEnd/src/controllers/user.js | 485 +++++++++++++------------ BackEnd/src/functions/nodemail.js | 44 +-- BackEnd/src/services/user.js | 17 +- BackEnd/src/test/user.test.js | 389 +++++++++++++------- FrontEnd/public/js/user/newPassword.js | 6 +- 5 files changed, 549 insertions(+), 392 deletions(-) diff --git a/BackEnd/src/controllers/user.js b/BackEnd/src/controllers/user.js index 3f7f092..921c39f 100644 --- a/BackEnd/src/controllers/user.js +++ b/BackEnd/src/controllers/user.js @@ -16,28 +16,28 @@ const { verifycodeMail } = require('../functions/nodemail'); * @returns {object} { code: number, message: string, access_token: string, refresh_token: string } */ const postLogin = async (req, res) => { - let { email, password } = req.body; - try { - await user.verifyLogin(email, password).then((data) => { - let token = { - access_token: data.access_token, - refresh_token: data.refresh_token, - }; - return success(res, 200, 'Authorize success.', token); - }); - } catch (err) { - let code; - switch (err.message) { - case 'Unauthorized email.': - case 'Incorrect password.': - code = 401; - break; - default: - code = 500; - break; - } - return fail(res, code, err.message); + let { email, password } = req.body; + try { + await user.verifyLogin(email, password).then((data) => { + let token = { + access_token: data.access_token, + refresh_token: data.refresh_token, + }; + return success(res, 200, 'Authorize success.', token); + }); + } catch (err) { + let code; + switch (err.message) { + case 'Unauthorized email.': + case 'Incorrect password.': + code = 401; + break; + default: + code = 500; + break; } + return fail(res, code, err.message); + } }; /** @@ -52,32 +52,32 @@ const postLogin = async (req, res) => { * @returns {object} { code: number, message: string } */ const postRegister = async (req, res) => { - let { email, password, user_name } = req.body; + let { email, password, user_name } = req.body; - try { - let result = await user.verifyRegister(email, password, user_name); - if (result) { - user.createUser(email, password, user_name); - return success(res, 201, 'Register success.'); - } - } catch (err) { - let code; - switch (err.message) { - case 'Exist username.': - case 'Exist email.': - code = 409; - break; - case 'Please input username.': - case 'Please input id.': - case 'Please input password.': - code = 400; - break; - default: - code = 500; - break; - } - return fail(res, code, err.message); + try { + let result = await user.verifyRegister(email, password, user_name); + if (result) { + user.createUser(email, password, user_name); + return success(res, 201, 'Register success.'); + } + } catch (err) { + let code; + switch (err.message) { + case 'Exist username.': + case 'Exist email.': + code = 409; + break; + case 'Please input username.': + case 'Please input id.': + case 'Please input password.': + code = 400; + break; + default: + code = 500; + break; } + return fail(res, code, err.message); + } }; /** @@ -86,21 +86,21 @@ const postRegister = async (req, res) => { * @returns {object} { code: number, data: data } */ const getProfile = async (req, res) => { - try { - const data = await user.findUser('id', req.decoded.id, 0); - return success(res, 200, 'No message', data); - } catch (err) { - let code; - switch (err.message) { - case 'Can not find profile.': - code = 404; - break; - default: - code = 500; - break; - } - return fail(res, code, err.message); + try { + const data = await user.findUser('id', req.decoded.id, 0); + return success(res, 200, 'No message', data); + } catch (err) { + let code; + switch (err.message) { + case 'Can not find profile.': + code = 404; + break; + default: + code = 500; + break; } + return fail(res, code, err.message); + } }; /** @@ -113,58 +113,58 @@ const getProfile = async (req, res) => { * - username, email 변동없을 시 편집 정상 수행 */ const updateProfile = async (req, res) => { - let { user_name, email } = req.body; - let user_id = req.decoded.id; - let data; - try { - let result = await user.updateUser(user_id, email, user_name, req.file); - if (result.message === 'Profile no change.') { - data = result.user; - } else if (result.message === 'Profile edit success.') { - data = result.data; - } - return success(res, 200, result.message, data); - } catch (err) { - let code; - switch (err.message) { - case 'Profile type must be only image.': - code = 400; - break; - case 'The username is already in use.': - case 'The email is already in use.': - code = 409; - break; - default: - code = 500; - break; - } - return fail(res, code, err.message); + let { user_name, email } = req.body; + let user_id = req.decoded.id; + let data; + try { + let result = await user.updateUser(user_id, email, user_name, req.file); + if (result.message === 'Profile no change.') { + data = result.user; + } else if (result.message === 'Profile edit success.') { + data = result.data; + } + return success(res, 200, result.message, data); + } catch (err) { + let code; + switch (err.message) { + case 'Profile type must be only image.': + code = 400; + break; + case 'The username is already in use.': + case 'The email is already in use.': + code = 409; + break; + default: + code = 500; + break; } + return fail(res, code, err.message); + } }; /** * 사용자의 id로 오늘 출석 했는지를 조회합니다. - * @param {number} id + * @param {number} id * @returns {object} { code: number, message: string } * 출석했다면 409을 반환 * 출석하지 않았다면 출석 체크를 하고 200반환 */ const postAttendance = async (req, res) => { - let user_id = req.decoded.id; - const today_date = todayDate(); - - try { - const attendance = await user.findAttendance(user_id, today_date); + let user_id = req.decoded.id; + const today_date = todayDate(); - if (attendance) { - return fail(res, 409, 'Already checked attendance today.'); - } + try { + const attendance = await user.findAttendance(user_id, today_date); - await user.createAttendance(user_id, today_date); - return success(res, 201, 'Attendance check success.'); - } catch (err) { - return fail(res, 500, err.message); + if (attendance) { + return fail(res, 409, 'Already checked attendance today.'); } + + await user.createAttendance(user_id, today_date); + return success(res, 201, 'Attendance check success.'); + } catch (err) { + return fail(res, 500, err.message); + } }; /** @@ -172,54 +172,57 @@ const postAttendance = async (req, res) => { * @returns {object} { code: number, message: string, data: array } */ const getAttendance = async (req, res) => { - try { - const user_id = req.decoded.id; - const start_date = startDate(); - const end_date = endDate(); + try { + const user_id = req.decoded.id; + const start_date = startDate(); + const end_date = endDate(); - const attendance_dates = await user.findAttendanceDate(user_id, start_date, end_date); + const attendance_dates = await user.findAttendanceDate(user_id, start_date, end_date); - const data = attendance_dates.map((attendance) => { - const date = new Date(attendance.attendance_date); - return date.getDate(); - }); + const data = attendance_dates.map((attendance) => { + const date = new Date(attendance.attendance_date); + return date.getDate(); + }); - return success(res, 200, 'No message.', data); - } catch (err) { - return fail(res, 500, err.message); - } + return success(res, 200, 'No message.', data); + } catch (err) { + return fail(res, 500, err.message); + } }; /** * 비밀번호 재확인 * @param {string} confirm_password 기존 비밀번호 - * + * */ const checkPassword = async (req, res) => { - let { confirm_password } = req.body; - let user_id = req.decoded.id; - try { - //비밀번호 확인 - let result = await user.comparePassword(confirm_password, user_id); - if (result) { - //일회성 토큰 발급 - await user.issueOneTimeToken(result).then((data) => { - let token = data; - return success(res, 200, 'Authorize success.', token); - }); - } - } catch (err) { - let code; - switch (err.message) { - case 'Incorrect password.': - code = 401; - break; - default: - code = 500; - break; - } - return fail(res, code, err.message); + let { confirm_password } = req.body; + let user_id = req.decoded.id; + try { + //비밀번호 확인 + let result = await user.comparePassword(confirm_password, user_id); + if (result) { + //일회성 토큰 발급 + await user.issueOneTimeToken(result).then((data) => { + let token = data; + return success(res, 200, 'Authorize success.', token); + }); } + } catch (err) { + let code; + switch (err.message) { + case 'Incorrect password.': + code = 401; + break; + case 'Can not find profile.': + code = 404; + break; + default: + code = 500; + break; + } + return fail(res, code, err.message); + } }; /** @@ -227,32 +230,35 @@ const checkPassword = async (req, res) => { * @param {string} new_password 새 비밀번호 */ const editPassword = async (req, res) => { - let { new_password } = req.body; - let user_id = req.decoded.id; - try { - if (req.decoded.type !== 'OneTimeJWT') { throw new Error('token is invalid.'); } - let result = await user.updatePassword(user_id, new_password); - if (result.message === 'Password changed.') { - let data = result.user; - return success(res, 200, result.message, data); - } else { - throw new Error('Services error.'); - } - } catch (err) { - let code; - switch (err.message) { - case 'token is invalid.': - code = 403; - break; - case 'Can not find profile.': - code = 404; - break; - default: - code = 500; - break; - } - return fail(res, code, err.message); + let { new_password } = req.body; + let user_id = req.decoded.id; + //console.log(req.headers.authorization); + try { + if (req.decoded.type !== 'OneTimeJWT') { + throw new Error('token is invalid.'); + } + let result = await user.updatePassword(user_id, new_password); + if (result.message === 'Password changed.') { + let data = result.user; + return success(res, 200, result.message, data); + } else { + throw new Error('Services error.'); + } + } catch (err) { + let code; + switch (err.message) { + case 'token is invalid.': + code = 403; // 403일 경우 위치 변경 + break; + case 'Can not find profile.': + code = 404; + break; + default: + code = 500; + break; } + return fail(res, code, err.message); + } }; /** @@ -260,138 +266,137 @@ const editPassword = async (req, res) => { * @param {string} email 사용자가 입력한 기존 비밀번호 */ const sendVerifyEmail = async (req, res) => { - let { email } = req.body - try { - let result = await user.findUser('email', email, 0); - if (result) { - // 인증번호, 캐시저장 - let verifycode = await user.verifycode(email); - console.log(verifycode); // 임시 - //인증번호 전송 메일 (비동기) - verifycodeMail(email, verifycode); - return success(res, 200); - } else { - throw new Error('Services error.'); - } - } catch (err) { - let code; - switch (err.message) { - case 'Can not find profile.': - code = 404; - break; - default: - code = 500; - break; - } - return fail(res, code, err.message); + let { email } = req.body; + try { + let result = await user.findUser('email', email, 0); + if (result) { + // 인증번호, 캐시저장 + let verifycode = await user.verifycode(email); + console.log(verifycode); // 임시 + //인증번호 전송 메일 (비동기) + verifycodeMail(email, verifycode); + return success(res, 200); + } else { + throw new Error('Services error.'); } + } catch (err) { + let code; + switch (err.message) { + case 'Can not find profile.': + code = 404; + break; + default: + code = 500; + break; + } + return fail(res, code, err.message); + } }; - /** - * 이메일, 인증번호 입력받아 확인 후 새 비밀번호 페이지로 넘겨줌 - * @param {string} email 사용자가 입력한 기존 비밀번호 + * 이메일, 인증번호 입력받아 확인 + * @param {string} email 사용자가 받는데 사용한 이메일 * @param {number} verifycode 입력한 인증번호 */ const checkVerifyCode = async (req, res) => { - let { email, verifycode } = req.body - try { - //인증번호 확인 - let result = user.checkCode(email, verifycode); - //새비밀번호 페이지로 넘기기 - if (result) { - //일회성 토큰 발급 - await user.issueOneTimeToken(email).then((data) => { - let token = data; - return success(res, 200, 'Authorize success.', token); - }); - } - } catch (err) { - let code; - switch (err.message) { - case 'Code dosesn\'t match.': //401이나 403? - case 'Verifycode expired.': - code = 409; - break; - default: - code = 500; - break; - } - return fail(res, code, err.message); + let { email, verifycode } = req.body; + try { + //인증번호 확인 + let result = user.checkCode(email, verifycode); + //새비밀번호 페이지로 넘기기 + if (result) { + //일회성 토큰 발급 + await user.issueOneTimeToken(email).then((data) => { + let token = data; + return success(res, 200, 'Authorize success.', token); + }); + } + } catch (err) { + let code; + switch (err.message) { + case "Code dosesn't match.": //401이나 403? + case 'Verifycode expired.': + code = 409; + break; + default: + code = 500; + break; } + return fail(res, code, err.message); + } }; /** * 로그인 페이지를 렌더링한다. */ const viewLogin = (req, res) => { - res.render('user/login'); + res.render('user/login'); }; /** * 회원가입 페이지를 렌더링한다. */ const viewRegister = (req, res) => { - res.render('user/register'); + res.render('user/register'); }; /** * 프로필 페이지를 렌더링한다. */ const viewProfile = (req, res) => { - res.render('user/profile'); + res.render('user/profile'); }; /** * 출석 페이지를 렌더링한다. */ const viewAttend = (req, res) => { - const first_day = firstDay(); - const end_date = endDate(); + const first_day = firstDay(); + const end_date = endDate(); - res.render('user/attendance', { - first_day: first_day, - end_date: end_date, - }); + res.render('user/attendance', { + first_day: first_day, + end_date: end_date, + }); }; /** * 비밀번호 확인페이지를 렌더링한다. */ const viewVerifyPassword = (req, res) => { - res.render('user/verifyPassword'); + res.render('user/verifyPassword'); }; /** * 비밀번호 변경페이지를 렌더링한다. */ const viewChangePassword = (req, res) => { - res.render('user/newPassword'); + res.render('user/newPassword'); }; /** * 메일 인증페이지를 렌더링한다. */ const viewverifyEmail = (req, res) => { - res.render('user/verifyEmail'); + res.render('user/verifyEmail'); }; module.exports = { - postLogin, - postRegister, - getProfile, - updateProfile, - postAttendance, - getAttendance, - checkPassword, - editPassword, - sendVerifyEmail, - checkVerifyCode, - viewLogin, - viewRegister, - viewProfile, - viewAttend, - viewVerifyPassword, - viewChangePassword, - viewverifyEmail, + postLogin, + postRegister, + getProfile, + updateProfile, + postAttendance, + getAttendance, + checkPassword, + editPassword, + sendVerifyEmail, + checkVerifyCode, + viewLogin, + viewRegister, + viewProfile, + viewAttend, + viewVerifyPassword, + viewChangePassword, + viewverifyEmail, }; diff --git a/BackEnd/src/functions/nodemail.js b/BackEnd/src/functions/nodemail.js index 5601584..6cd0bb9 100644 --- a/BackEnd/src/functions/nodemail.js +++ b/BackEnd/src/functions/nodemail.js @@ -1,29 +1,31 @@ 'use strict'; const nodemailer = require('nodemailer'); -const sender_info = require('../../config/senderInfo.json'); +const config = require('../../config/default.json'); +const logger = require('./winston'); const transporter = nodemailer.createTransport({ - service: 'gmail', - port: 587, // 보안없는경우 587, 있는경우 465로 설정. 기본은 587 - auth: { - user: sender_info.user, - pass: sender_info.pass - } + service: 'gmail', + port: 587, // 보안없는경우 587, 있는경우 465로 설정. 기본은 587 + auth: { + user: config.mail_info.user, + pass: config.mail_info.pass, + }, }); exports.verifycodeMail = async (email, code) => { - let mailOptions = { - from: sender_info, - to: email, - subject: 'Verifying Code by CSW_BOARD', - text: `Your Verifycode is ${code}.` - }; - let send = await transporter.sendMail(mailOptions); - if (send) { - console.log('send OK'); - return 'Mail send success.' - } else { - throw new Error('Mail send fail.'); - } -}; \ No newline at end of file + let mailOptions = { + from: config.mail_info.user, + to: email, + subject: 'Verifying Code by CSW_BOARD', + text: `Your Verifycode is ${code}.`, + }; + let send = await transporter.sendMail(mailOptions); + if (send) { + logger.info(`'send verifycode to ${email}'`); + //console.log('send OK'); + return 'Mail send success.'; + } else { + throw new Error('Mail send fail.'); + } +}; diff --git a/BackEnd/src/services/user.js b/BackEnd/src/services/user.js index ad0d29d..b0001c0 100644 --- a/BackEnd/src/services/user.js +++ b/BackEnd/src/services/user.js @@ -8,7 +8,7 @@ const bcrypt = require('bcrypt'); const random = require('crypto'); const cache = require('memory-cache'); - +const logger = require('../functions/winston'); /** * 사용자 검색 후 return @@ -214,7 +214,7 @@ const findAttendanceDate = async (user_id, start_date, end_date) => { * @param {string} confirm_password 비밀번호 * @param {number} user_id * - * @returns {string} email + * @returns {string} email */ const comparePassword = async (confirm_password, user_id) => { const user = await User.findByPk(user_id); @@ -267,7 +267,8 @@ const verifycode = (email) => { let code = parseInt(random.randomBytes(2).toString('hex'), 16).toString(10); //캐시메모리에 저장 (캐시 메모리 너무 많이 쌓이는 경우?) cache.put(email, code, 300000, (key, value) => { - console.log('key: ' + key + ' value: ' + value + ' timeout'); // 로그로 변경필요 + logger.info(`'verifycode is timeout. key: ${key} - value: ${value}'`); + //console.log('key: ' + key + ' value: ' + value + ' timeout'); // 로그로 변경필요 }); // key: email, value: code, 300000ms (5min) 후 삭제 return code; }; @@ -276,14 +277,14 @@ const verifycode = (email) => { * 인증번호 체크 * @param {string} email * @param {number} verifycode - * - * @returns {boolean} + * + * @returns {boolean} */ const checkCode = (email, verifycode) => { let cachecode = cache.get(email); if (cachecode) { if (parseInt(verifycode) !== parseInt(cachecode)) { - throw new Error('Code dosesn\'t match.'); + throw new Error("Code dosesn't match."); } else { return true; } @@ -295,14 +296,14 @@ const checkCode = (email, verifycode) => { /** * 일회용 토큰 발급 * @param {string} email - * + * * @returns {object} data */ const issueOneTimeToken = async (email) => { let user = await findUser('email', email, 1); let one_time_access_token = await oneTimeToken({ type: 'OneTimeJWT', id: user.id }); return one_time_access_token; -} +}; module.exports = { findUser, diff --git a/BackEnd/src/test/user.test.js b/BackEnd/src/test/user.test.js index 12c6511..9a68280 100644 --- a/BackEnd/src/test/user.test.js +++ b/BackEnd/src/test/user.test.js @@ -8,7 +8,7 @@ const user = require('../controllers/user'); const { path, config, chalk } = require('../../loaders/module'); -const bcrypt = require('bcrypt'); +const cache = require('memory-cache'); /** * * 로그인 테스트 @@ -469,124 +469,269 @@ describe('getAttendance', () => { }); }); -// /** -// * *비밀번호 변경 테스트 -// * 1. 비밀번호 변경 성공 -// * 2. 비밀번호 변경 실패 (비밀번호 에러) -// * 3. 프로필 조회 실패 -// */ -// describe('passwordChange', () => { -// let req, res; - -// beforeEach(() => { -// req = { -// body: { -// confirm_password: 'password', -// new_password: 'newpassword', -// }, -// decoded: { id: '1' }, -// }; -// res = { -// status: jest.fn().mockReturnThis(), -// json: jest.fn(), -// }; -// }); -// afterEach(async () => { -// let encrypted_pw = await bcrypt.hash('password', 10); -// await User.update({ password: encrypted_pw }, { where: { id: '1' } }); -// jest.clearAllMocks(); -// }); - -// //비밀번호 변경 성공 -// test(`should return ${chalk.green(200)} if ${chalk.blue(`password changed`)}`, async () => { -// await user.editPassword(req, res); -// expect(res.status).toHaveBeenCalledWith(200); -// expect(res.json).toHaveBeenCalledWith( -// expect.objectContaining({ -// code: 200, -// message: 'Password changed.', -// data: expect.objectContaining({ -// id: 1, -// user_name: 'test_user', -// email: 'test_user@example.com', -// }), -// }), -// ); -// }); - -// //비밀번호 변경 실패 (비밀번호 오류) -// test(`should return ${chalk.yellow(401)} if ${chalk.blue(`incorrect password`)}`, async () => { -// req.body.confirm_password = 'differentpassword'; -// await user.editPassword(req, res); - -// expect(res.status).toHaveBeenCalledWith(401); -// expect(res.json).toHaveBeenCalledWith({ -// detail: 'No detail.', -// message: 'Incorrect password.', -// }); -// }); - -// //비밀번호 변경 실패 (프로필 조회 실패) -// test(`should return ${chalk.yellow(404)} if ${chalk.blue(`can not find profile`)}`, async () => { -// req.decoded.id = 0; -// await user.editPassword(req, res); - -// expect(res.status).toHaveBeenCalledWith(404); -// expect(res.json).toHaveBeenCalledWith({ -// detail: 'No detail.', -// message: 'Can not find profile.', -// }); -// }); -// }); - -// /** -// * *비밀번호 찾기 테스트 -// * 1. 임시 비밀번호 전송 선공 -// * 2. 프로필 조회 실패 -// * 3. 비밀번호 변경 실패 -// * 4. 메일 전송 실패 -// */ -// describe('resetPassword', () => { -// let req, res; - -// beforeEach(() => { -// req = { -// body: { -// email: 'test_user@example.com', -// }, -// }; -// res = { -// status: jest.fn().mockReturnThis(), -// json: jest.fn(), -// }; -// }); -// afterEach(async () => { -// let encrypted_pw = await bcrypt.hash('password', 10); -// await User.update({ password: encrypted_pw }, { where: { id: '1' } }); -// jest.clearAllMocks(); -// }); - -// //임시 비밀번호 전송 성공 -// test(`should return ${chalk.green(200)} if ${chalk.blue(`password changed`)}`, async () => { -// await user.resetPassword(req, res); -// expect(res.status).toHaveBeenCalledWith(200); -// expect(res.json).toHaveBeenCalledWith({ -// code: 200, -// message: 'Mail send success.', -// data: 'No data.', -// }); -// }); - -// //프로필 조회 실패 -// test(`should return ${chalk.yellow(404)} if ${chalk.blue(`Can not find profile.`)}`, async () => { -// req.body.email = 'different_user@example.com'; -// await user.resetPassword(req, res); -// expect(res.status).toHaveBeenCalledWith(404); -// expect(res.json).toHaveBeenCalledWith({ -// message: 'Can not find profile.', -// detail: 'No detail.', -// }); -// }); -// //비밀번호 변경 실패 (에러 케이스 확인 필요) -// //메일전송 실패 (에러 케이스 확인 필요) 1. 메일비밀번호 오류등, API 관련 에러 -// }); \ No newline at end of file +// 비밀번호 변경 테스트 +/** + * 비밀번호 인증 테스트 + * 1. 비밀번호 인증 완료 + * 2. 비밀번호 에러 + * 3. 프로필 조회 실패 + */ +describe('checkPassword', () => { + let req, res, server; + + beforeAll(() => { + server = app.listen(config.get('server.port')); + }); + + afterAll((done) => { + server.close(done); + }); + + beforeEach(() => { + req = { + decoded: { id: 1 }, + body: { + confirm_password: 'password', + }, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + }); + + // 비밀번호 인증 완료 + test(`should return ${chalk.green(200)} if ${chalk.blue(`password authorize success`)}`, async () => { + await user.checkPassword(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + code: 200, + message: 'Authorize success.', + data: expect.stringMatching(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_.+/=]+$/), + }), + ); + }); + + // 비밀번호 에러 + test(`should return ${chalk.yellow(401)} if ${chalk.blue(`password is incorrect`)}`, async () => { + req.body.confirm_password = 'notpassword'; + await user.checkPassword(req, res); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + message: 'Incorrect password.', + detail: 'No detail.', + }); + }); + + // 프로필 조회 실패 + test(`should return ${chalk.yellow(404)} if ${chalk.blue(`Can not find profile`)}`, async () => { + req.decoded.id = 6974; + await user.checkPassword(req, res); + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + message: 'Can not find profile.', + detail: 'No detail.', + }); + }); +}); + +/** + * 이메일 인증 테스트 + * 인증번호 발송 테스트 + * 1. 인증번호 발송 완료 + * 2. 프로필 조회 실패 + */ +jest.mock('../functions/nodemail'); +describe('sendVerifyEmail', () => { + let req, res; + + beforeEach(() => { + req = { + body: { + email: 'test_user@example.com', + }, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + }); + + // 인증번호 발송 완료 + test(`should return ${chalk.green(200)} if ${chalk.blue(`email send success`)}`, async () => { + await user.sendVerifyEmail(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + code: 200, + message: 'No message.', + data: 'No data.', + }), + ); + }); + + // 프로필 조회 실패 + test(`should return ${chalk.yellow(404)} if ${chalk.blue(`Can not find profile`)}`, async () => { + req.body.email = 'notmail@example.com'; + await user.sendVerifyEmail(req, res); + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + message: 'Can not find profile.', + detail: 'No detail.', + }); + }); +}); + +/** + * 이메일 인증 테스트 + * 인증번호 확인 테스트 + * 1. 인증번호 확인 완료 + * 2. 인증번호 에러 + * 3. 인증번호 만료 + */ +describe('checkVerifyCode', () => { + let req, res; + + beforeEach(() => { + req = { + body: { + email: 'test_user@example.com', + verifycode: '12345', + }, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + cache.put('test_user@example.com', 12345); + }); + + afterEach(() => { + cache.clear(); + }); + + // 인증번호 확인 완료 + test(`should return ${chalk.green(200)} if ${chalk.blue(`email authorize success`)}`, async () => { + await user.checkVerifyCode(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + code: 200, + message: 'Authorize success.', + data: expect.stringMatching(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_.+/=]+$/), + }), + ); + }); + + // 인증번호 에러 + test(`should return ${chalk.yellow(409)} if ${chalk.blue(`code is incorrect`)}`, async () => { + req.body.verifycode = 6974; + await user.checkVerifyCode(req, res); + expect(res.status).toHaveBeenCalledWith(409); + expect(res.json).toHaveBeenCalledWith({ + message: "Code dosesn't match.", + detail: 'No detail.', + }); + }); + + // 인증번호 만료 + test(`should return ${chalk.yellow(409)} if ${chalk.blue(`verifycode is expired`)}`, async () => { + cache.del('test_user@example.com'); + await user.checkVerifyCode(req, res); + expect(res.status).toHaveBeenCalledWith(409); + expect(res.json).toHaveBeenCalledWith({ + message: 'Verifycode expired.', + detail: 'No detail.', + }); + }); +}); + +/** + * 비밀번호 변경 테스트 + * 1. 비밀번호 변경 성공 + * 2. 프로필 조회 실패 + * 3. 비밀번호 변경 실패 (시간 만료) + */ +describe('newPassword', () => { + let req, res, token, server; + + beforeAll(async () => { + server = app.listen(config.get('server.port')); + + //캐시생성 + cache.put('test_user@example.com', 12345); + + // 인증하여 토큰발급 + const res = await request(app) + .post('/user/verifyEamil') + .send({ email: 'test_user@example.com', verifycode: 12345 }); + token = res.body.data; + }); + + afterAll((done) => { + cache.clear(); + server.close(done); + }); + + beforeEach(() => { + req = { + decoded: { + id: 1, + type: 'OneTimeJWT', + }, + body: { + new_password: '0000', + }, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + }); + + afterEach(async () => { + // await request(app) + // .post('/user/newPassword') + // .set('authorization', `${token}`) + // .send({ new_password: 'password' }); + req.body.new_password = 'password'; + await user.editPassword(req, res); + }); + + //비밀번호 변경성공 + test(`should return ${chalk.green(200)} if ${chalk.blue(`password change success`)}`, async () => { + await user.editPassword(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + code: 200, + message: 'Password changed.', + }), + ); + }); + + // 프로필 조회 실패 + test(`should return ${chalk.yellow(404)} if ${chalk.blue(`can not find profile`)}`, async () => { + req.decoded.id = 6974; + await user.editPassword(req, res); + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + message: 'Can not find profile.', + detail: 'No detail.', + }); + }); + + // 토큰 만료 (시간이 걸리므로 TDD 어려움) + // test(`should return ${chalk.yellow(403)} if ${chalk.blue(`token is invalid`)}`, async () => { + // token = ''; + // await user.editPassword(req, res); + // expect(res.status).toHaveBeenCalledWith(403); + // expect(res.json).toHaveBeenCalledWith( + // { + // message: "Verifycode expired.", + // detail: 'No detail.', + // } + // ); + // }); +}); diff --git a/FrontEnd/public/js/user/newPassword.js b/FrontEnd/public/js/user/newPassword.js index 184b4d9..6a3d4d6 100644 --- a/FrontEnd/public/js/user/newPassword.js +++ b/FrontEnd/public/js/user/newPassword.js @@ -31,9 +31,13 @@ function newPassword() { .then((res) => { if (res.code === 200) { location.href = "/" - } else { + } else if (res.code === 500) { alert(res.message); location.reload(); + } else { //403 or 404 + alert(res.message); + alert("please try again."); + location.href = "/" } })