본문 바로가기

Side Project

[PROJECT] Trello Clone 프로젝트 API - (4) User 관련 기능

Trello Clone 프로젝트 API - (4) User 관련 기능

서론

이번 포스팅 부터는 기능을 주로 구현해보려고 한다. 우선 User 관련 기능을 구현해 보자.

User 관련 API

일단 구현해야 하는 기능들을 나열해보자.

  • 로그인 - POST /user/join
  • 회원가입 - POST /user/login
  • 회원 정보 가져오기 - GET /user/info
  • 회원 정보 수정 - PUT /user/info
  • reset code 검증 - GET /user/reset
  • reset code 발급 - POST /user/reset
  • reset code 파기 - DELETE /user/reset
  • 비밀번호 변경 - PUT /user/reset
  • 이메일로 유저 검색 - GET /user/email/:email

주요 기능 구현 로직

우선 라우팅 정보는

/routes/user/index.js

const router = require('express').Router()
const auth = require('../../middlewares/auth')
const controller = require('./user.controller')

/* 회원가입 */
router.post('/join', controller.join)

/* 로그인 */
router.post('/login', controller.login)

/* 회원 정보 가져오기*/
router.get('/info', auth, controller.getInfo)

/* 회원 정보 업데이트 */
router.put('/info', auth, controller.updateInfo)

/* reset code 검증 */
router.get('/reset', controller.verifyResetCode)

/* reset code 발급 */
router.post('/reset', controller.issueResetCode)

/* reset code 삭제 */
router.delete('/reset', controller.deleteResetCode)

/* 비밀번호 변경 */
router.put('/reset', controller.updatePassword)

/* 이메일로 유저 리스트 검색 */
router.get('/email/:email', auth, controller.getUserList)

module.exports = router

컨트롤러는 대부분 Promise 기반 체이닝을 이용해 개발해 보있다. (용어가 맞는지 모르겠다.)

/** 회원가입 */
exports.join = (req, res) => {
    let t
    const {
        username,
        password,
        email
    } = req.body

    const create = (user) => {
        if (user) {
            throw new Error('EXIST')
        } else {
            return User.create({
                username,
                password,
                email,
                photo: username.substr(0, 1)
            }, {
                transaction: t
            })
        }
    }

    const respond = () => {
        res.json({
            message: "회원 가입에 성공하였습니다.",
            result: true
        })
    }

    const onError = (error) => {
        console.error(error)
        res.status(400).json(ErrorHandler(error.message))
    }

    models.sequelize.transaction(transaction => {
            t = transaction
            return User.findOne({
                    transaction: t,
                    where: {
                        username
                    }
                })
                .then(create)
        })
        .then(respond)
        .catch(onError)
}

예시는 회원가입 로직이다. sequelizetransaction을 사용하였고 promise를 반환하여 함수들을 체이닝하였다. error 발생시 throw new Error('EXIST')처럼 error를 발생시켜 onError 로 넘어가게 하였다.

주요 기능

대부분의 코드는 위에 로직으로 구현되었고 그 중 개발에 있어 주요했던 기능은 reset code 관련 기능과 reset 링크 이메일 발급이 있다.

  • 이메일 코드 발급 로직
1. 이메일 주소를 기반으로 존재하는 유저인지 판별
2. 존재 할시 reset code 와 reset code expiredate 를 각각 생성 ( reset code expiredate는 현재 시간 + 24시)
3. 이메일을 기반으로 데이터베이스에서 유저 정보를 가져온 다음 node mailer 를 이용해서 해당 이메일로 리셋 링크 보내기

리셋 링크를 보낼때 html 형태로 보내는데 이때 주의할 점은 flex 를 이용해서 짜면 특정 메일에서 전부 깨져서 보인다는 것이다. 그래서 table 기반의 구성을 작성했다.

exports.issueResetCode = (req, res) => {
    let t
    const {
        email
    } = req.body

    const generateCode = (user) => {
        if (!user) {
            throw new Error("NOAUTH")
        } else {
            let date = new Date()
            let reset_code = uuidv4()
            date.setDate(date.getDate() + 1)
            return User.update({
                reset_code,
                reset_code_expiredate: date
            }, {
                where: {
                    uid: user.uid
                },
                transaction: t
            })
        }
    }

    const getUserData = () => {
        return User.findOne({
            transaction: t,
            attributes: ["uid", "username", "reset_code"],
            where: {
                email
            }
        })
    }

    const sendMail = (user) => {
        let {
            uid,
            username,
            reset_code
        } = user
        let reset_url = `http://localhost:8080/reset?uid=${uid}&resetCode=${reset_code}&reset=true`
        let dont_reset_url = `http://localhost:8080/reset?uid=${uid}&resetCode=${reset_code}&reset=false`
        let mailOption = {
            from: mailConfig.user,
            to: email,
            subject: 'Trello Password Reset',
            html: `<div style="font: 15px 'Helvetica Neue',Arial,Helvetica;background-color: #F0F0F0; height: 420px; color: #333;">
            <table style="color: #333;padding: 0;margin: 0;width: 100%;font: 15px 'Helvetica Neue',Arial,Helvetica;">
                <tbody>
                    <tr width="100%">
                        <td>
                            <table style="border: none;padding: 0px 18px;margin: 50px auto;width: 500px;">
                                <tbody>
                                    <tr width="100%" height="57">
                                        <td style="background-color: #0079bf; border-top-left-radius: 4px;border-top-right-radius: 4px;text-align: center;padding: 12px 18px;"><img
                                                width="120px" src="https://trello.com/images/email-header-logo-white-v2.png" alt=""></td>
                                    </tr>
                                    <tr width="100%">
                                        <td style="background: #fff; padding: 18px; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px;">
                                            <div style="font-weight: bold;font-size: 20px;color: #333;margin: 0;">Hello ${username},</div>
                                            <p style="font-size: 15px; color: #333;">We heard you need a password reset. Click the link below
                                                and you'll be redirected to a secure site from which you can set a new password.</p>
                                            <p style="text-align: center; color: #333; font-size: 15px;"><a href="${reset_url}" target="_blank" style="background-color: #3aa54c;border-radius: 3px;text-decoration: none;color: #fff;line-height: 1.25em;font-size: 16px;font-weight: 700;padding: 10px 18px;margin: 24px auto 24px;display: block;width: 180px;">Reset Password</a></p>
                                            <p style="color: #939393">If you didn't try to reset your password, <a href="${dont_reset_url}" style="color: #365FC9">click here</a> and we'll forget this ever happened.</p>
                                        </td>
                                    </tr>
                                </tbody>
                            </table>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>`
        }
        smtpTransport.sendMail(mailOption, function (err, info) {
            if (err) {
                console.error('Send Mail error : ', error)
                throw new Error('MAILFAIL')
            } else {
                console.log(info)
                return;
            }
        })
    }

    const respond = () => {
        res.json({
            result: true,
            message: "코드를 발급했습니다. 이메일을 확인해주세요."
        })
    }

    const onError = (error) => {
        console.error(error)
        res.status(400).json(ErrorHandler(error.message))
    }
    models.sequelize.transaction(transaction => {
            t = transaction
            return User.findOne({
                    transaction: t,
                    where: {
                        email
                    },
                    attributes: ['uid', 'username', 'email']
                }).then(generateCode)
                .then(getUserData)
                .then(sendMail)
        })
        .then(respond)
        .catch(onError)
}
  • reset 코드
1. user 테이블 안에 reset_code, reset_code_expiredate 칼럼을 두어 유저가 비밀번호 변경 요청을 했을때 코드와 코드의 유효 기간을 담아 둔다.
2. 유저가 변경 링크를 통해 변경을 시도할때 해당 코드가 유효 한지 검사한다.
3. 만약 유효한 코드라면 변경을 진행할 수 있도록 하고 아니라면 바꾸지 못하게 한다.

끝내며

더 많은 코드들이 있지만 로직이 비슷하거나 쉽게 구현할 수있는 코드여서 내용을 담지는 않았다.
user 기능 구현 관련 소스