본문 바로가기

웹개발

게시판-이메일 인증, 아이디 찾기

<이메일 인증>

이메일 인증을 하려면 회원가입 도중에 인증코드를 받아서 코드가 맞는지 확인해야 한다.

그런데 인증이 되기 전에 회원 정보와 코드를 같이 users 테이블에 만들어버리면 인증을 실패하거나 했을 때 남은 데이터를 처리하기가 곤란해질거 같아서 verificationCodes라는 새로운 테이블을 만들었다.

module.exports = function(sequelize, DataTypes) { 
    return sequelize.define('verificationCodes', {
      idx: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true,
        allowNull: false
      },
      email: {
        type: DataTypes.STRING(255),
        allowNull: false
      },
      verificationCode: {
        type: DataTypes.STRING(6),
        allowNull: true
      },
      expiresAt: {
        type: DataTypes.DATE,
        allowNull: true
      }
    });
}

이렇게 만들고 db.js에 연결도 시켜줬다.

이메일을 발송하기 위해서 nodemailer라는 것을 사용하였다. npm install로 설치를 해주고

라우터 auth.js 파일에 

const nodemailer = require('nodemailer');

let transporter = nodemailer.createTransport({
  service: 'gmail',
  auth: {
    user: 'codehacker0207@gmail.com',
    pass: 'tqbvncmcrjxhwjwz'
  }
});

이렇게 해줬다. 여기서 pass는 구글의 앱 비밀번호인데 2차인증 이상된 구글 계정에서 앱 비밀번호를 만들면 된다.

인증번호를 전송하는 과정부터 살펴보면,

router.post('/sendcode', async (req, res) => {
    const verificationCode = Math.floor(Math.random() * 1000000);
    const {email} = req.body;
    let mailOptions = {
        from: 'codehakcer0207@gmail.com',
        to: email,
        subject: 'Please verify your email address',
        text: `Your verification code is: ${verificationCode}`
        };
    
    await transporter.sendMail(mailOptions);
    await db.verificationCodes.create({
        email: email,
        verificationCode: verificationCode,
        expiresAt: new Date(Date.now() + 3 * 60 * 1000)
    });

    res.json({
        success: true,
        message: 'Verification code sent successfully.'
    });

});

이렇게 구성이 되어있다. 무작위 6자리 인증번호를 만들고 메일을 발송한다. 

그리고 verificationCodes에 데이터를 넣어준다. expiresAt은 비밀번호의 만료시간으로, 3분으로 설정하였다.

 

인증번호가 일치하는지 확인하는 부분은

router.post('/verify', async (req, res) => {
    try {
        const {email, code} = req.body;

        const latest = await db.verificationCodes.findOne({
            where: { email: email },
            order: [ ['expiresAt', 'DESC'] ]
        });
        
        if (!latest) {
            return res.status(400).send('User not found.');
        }

        if (new Date() > latest.expiresAt) {
            return res.status(400).send('Verification code has expired.');
        }
        

        if (latest.verificationCode == code) {
            return res.json({success: true});
        } else {
            return res.status(400).send('Incorrect verification code.');
        }
    } catch (error) {
        return res.status(500).send('Server error.');
    }
});

이렇데 되어있다. latest를 설정한 이유는 인증번호가 여러번 발송되었을 때 가장 나중의 번호로 인증을 받기 위해서이다.

 

view의 signup.ejs의 구조가 좀 바뀌었는데,

<body>
    <h1>회원가입</h1>
    <form action="/auth/signup" method="post" id="signupForm">
        <div>
            <label for="username">사용자 이름:</label>
            <input type="text" id="username" name="username" required>
        </div>
        <div>
            <label for="email">이메일:</label>
            <input type="email" id="email" name="email" required>
        </div>
        <div>
            <button type="button" id="sendCodeBtn">인증 코드 발송</button>
        </div>
        <div>
            <button type="button" id="verifyCodeBtn">인증 코드 확인</button>
            <input type="text" id="verificationCode" name="verificationCode" placeholder="인증 코드" required>
        </div>
        <div>
            <label for="password">비밀번호:</label>
            <input type="password" id="password" name="password" required>
        </div>
        <div>
            <button type="submit" id="signupBtn" disabled>회원가입</button>
        </div>
    </form>
    
    <a href="/auth/login">이미 계정이 있으신가요? 로그인</a>

    <script src = "../../public/verify.js" defer></script>
    

</body>

우선, 인증코드 발송 버튼과 인증 코드를 입력하고 맞는지 확인하는 버튼을 만들었다.

회원가입 버튼에는 disabled를 추가하였는데, 인증코드 인증이 완료되었을 때만 가입을 할 수 있도록 만들기 위해서다.

그리고 아래에 <script>로 새로운 파일을 불러오도록 했는데, 이렇게 된 이유는 좀 복잡하다.

인증코드를 발송하느라 새로운 post 요청을 보내면서 페이지가 새로고침이 되면 안되기 때문에

jquery와 ajax라는 것을 사용하려고 했는데, jquery를 사용할 수 있는 <script> 태그를 head 안에 넣고 실행을 했더니

csp라는 xss공격 방지 기법 때문에 사용할 수 없다는 에러가 발생했다.

그래서 jquery 대신에 그냥 자바스크립트의 fetch라는 것을 사용해서 구성을 했는데 또 에러가 발생했다.

해당 에러에 대해 알아보니 js코드를 다른 파일에 저장해야 한다고 해서 그냥 views 폴더 안에 저장을 했더니 또 에러가 생겨서 정적 파일을 보관하는 폴더 안에 파일을 만들어야 한다길래 예전에 만들어서 css파일을 담았던 public 폴더 안에 verify.js라는 파일을 만들게 되었다.

 

아무튼 verify.js의 동작을 살펴보면,

let isVerified = false;
document.getElementById('sendCodeBtn').addEventListener('click', function () {
    const email = document.getElementById('email').value;
    if (email) {
        fetch('/auth/sendcode', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ email })
        })
        
            .then(response => {
                if (!response.ok) throw new Error('Network response was not ok');
                return response.json();
            })
            .then(data => {
                alert('인증 코드가 발송되었습니다.');
            })
            .catch(err => {
                alert('오류가 발생했습니다. 다시 시도해주세요.');
            });
    } else {
        alert('이메일 주소를 입력하세요.');
    }
});

먼저 인증번호 발송 버튼을 눌렀을 때이다. 라우터 파일에서 설정했던 링크로 post 요청을 보내고 인증 코드가 발송되었다는 알림이 뜨게 했다.

document.getElementById('verifyCodeBtn').addEventListener('click', function () {
    const email = document.getElementById('email').value;
    const code = document.getElementById('verificationCode').value;
    if (email && code) {
        fetch('/auth/verify', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ email, code})
        })
        .then(response => {
            if (!response.ok) throw new Error('Network response was not ok');
            return response.json();
        })
            .then(data => {
                if (data.success) {
                    isVerified = true;
                    document.getElementById('signupBtn').disabled = false;
                    alert('인증 성공');
                } else {
                    isVerified = false;
                    alert('인증 실패');
                }
            })
            .catch(err => {
                console.error(err);
                alert('인증 실패');
            });
    } else {
        alert('이메일과 인증 코드를 입력하세요.');
    }
});

그 다음은 인증 코드가 맞는지 확인하는 부분이다. 아까 라우터 파일에서 인증코드가 맞으면 json으로 success가 true이도록 response를 보냈기 때문에 이를 받아서 success이면 인증이 성공했다는 알림이 뜨게 하고 isVerified를 true로 바꿔주었다. 그리고 인증을 성공했을 때 회원가입 버튼의 disabled를 false로 바꿔서 누를 수 있게 했다.

 

회원가입할 때 이미 가입된 이메일로 두 번 가입되는 일을 방지하기 위해

라우터에서 signup에

const user = await db.users.findOne({ where: { email } });
        if(user)
        {
            return res.redirect('/auth/failed');
        }

이런 코드를 추가하였다. if문 안에 원래는 에러를 발생시키는 코드를 넣었는데 왠지 모르게 에러 페이지로 넘어가지 않길래 redirect로 직접 만든 에러 페이지로 넘어가게 했다.

이미 있는 이메일인 경우

이 페이지로 이동된다.

 

 

<아이디 찾기>

router.post('/findid', async (req, res) => {
    try {
      const { email } = req.body;
      const user = await db.users.findOne({ where: { email } });
  
      if (!user) {
        return res.status(400);
      }
  
      const mailOptions = {
        from: 'codehacker0207@gmail.com',
        to: email,
        subject: '아이디 찾기',
        text: `귀하의 아이디는 ${user.username} 입니다.`
      };
  
      transporter.sendMail(mailOptions, (error, info) => {
        if (error) {
          return res.status(500);
        }
        res.json({ success: true, message: '아이디가 이메일로 발송되었습니다.' });
      });
    } catch (error) {
      res.status(500).json({ message: '아이디 찾기 실패', error: error.message });
    }
});

이렇게 구성하였다. 

이메일을 입력하면 해당 이메일의 아이디를 찾아서 이메일로 발송한다.

/findin링크의 view파일은

<form id="findIdForm" action="/auth/findid" method="post">
    <label for="email">이메일:</label>
    <input type="email" id="email" name="email" required>
    <button type="submit">아이디 찾기</button>
</form>

이렇다. findid로 가는 링크는 로그인 페이지에 넣어두었다.

 

'웹개발' 카테고리의 다른 글

게시판-댓글  (0) 2023.10.26
게시판-자기 글 수정, 삭제, 비밀글  (0) 2023.10.25
게시판-회원가입, 로그인  (0) 2023.10.24
게시판-CRUD  (0) 2023.10.24
flask - mySQL 연동  (0) 2023.10.22