따꿍의 프로젝트

[2025.08.03] auth 이메일 인증, 이메일찾기, 아이디 찾기, 임시 비밀번호 전송, 비밀번호 재설정 기능 PR 확인 본문

웹프로젝트/졸작

[2025.08.03] auth 이메일 인증, 이메일찾기, 아이디 찾기, 임시 비밀번호 전송, 비밀번호 재설정 기능 PR 확인

공장 주인 따꿍 2025. 8. 3. 12:22

이메일 찾기

export const findEmailById = async (
    req: Request,
    res: Response,
    next: NextFunction
) => {
    try {
        const { userId } = req.query; //쿼리에서 userId 뽑아내기
        if (!userId || typeof userId !== 'string') { //입력 안했거나 입력값의 타입이 틀렸을때
            return res.status(400).json({
                success: false,
                message: '아이디를 올바르게 입력해주세요.',
            });
        }

        const user = await User.findOne({ where: { userId } });
        if (!user) { //유저가 DB에 존재하지 않는 경우
            return res.status(404).json({
                success: false,
                message: '해당 아이디로 가입된 계정이 없습니다.',
            });
        }

        return res.status(200).json({ //유저를 DB에서 찾은 경우 email을 보냄
            success: true,
            email: user?.email,
        });
    } catch (err) {
        console.error(err);
        return next(err);
    }
};

 

❗ 문제점: I'm not sure if making the endpoint like this is common practice.
Isn't it considered not safe because the user info is in the url?

Putting sensitive information (like an email or username) directly in the query string of a GET request is generally not recommended for several reasons:

Security Risks

  • Logged in server logs & browser history:
    URLs (including query parameters) are often logged in:
    • Web server logs
    • Browser history
    • Proxy servers
      Meaning the user's email can be exposed unintentionally.
  • Easier to be leaked:
    If someone takes a screenshot, shares a link, or a referrer header is sent to a third-party site, the email address becomes visible.

Common Practice

For retrieving user information, especially when it involves personally identifiable information (PII), most APIs:

  • Use POST requests with parameters in the request body:

아이디 찾기

export const findIdByEmail = async (
    req: Request,
    res: Response,
    next: NextFunction
) => {
    try {
        const { email } = req.query;
        if (!email || typeof email !== 'string') { //입력값이 없거나 타입이 잘못되었을떄
            return res.status(400).json({
                success: false,
                message: '이메일을 올바르게 입력해주세요.',
            });
        }

        const user = await User.findOne({ where: { email } });
        if (!user) { //입력값이 DB에 존재하지 않을때
            return res.status(404).json({
                success: false,
                message: '해당 이메일로 가입된 계정이 없습니다.',
            });
        }

        return res.status(200).json({ //입력값이 DB에 존재해서 찾았을때 useId 리턴하기
            success: true,
            userId: user.userId,
        });
    } catch (err) {
        console.error(err);
        return next(err);
    }
};

임시 비밀번호 전송

export const sendTempPassword = async (
    req: Request,
    res: Response,
    next: NextFunction
) => {
    const { email } = req.body;
    try {
        const user = await User.findOne({ where: { email } });
        if (!user) {
            return res.status(404).json({
                success: false,
                message: '해당 이메일로 가입된 유저가 없습니다.',
            });
        }

        const tempPassword = generateCode();
        const hashed = await bcrypt.hash(tempPassword, 12);
        await user.update({ password: hashed });

        await sendEmail(email, tempPassword, true);
        return res.status(200).json({
            success: true,
            message: '임시 비밀번호가 전송되었습니다.',
        });
    } catch (err) {
        console.error(err);
        return next(err);
    }
};

비밀번호 변경

import User from '../models/user';
export const resetPassword = async (
    req: Request,
    res: Response,
    next: NextFunction
) => {
    try {
        const userPk = (req.user as User).user_id; //req.user의 User 타입으로 정의
        const { currentPassword, newPassword } = req.body;

        if (!currentPassword || !newPassword) { //currentPassword와 newPassword 입력값이 없을때
            return res.status(400).json({
                message: '현재 비밀번호와 새 비밀번호를 모두 입력해주세요.',
            });
        }

        const user = await User.findByPk(userPk);
        if (!user) { //req.user(지금 로그인되어있는 애)가 존재하지 않는 애일경우 - 굳이 확인할 필요 없는것 같지만 음..
            return res.status(404).json({
                message: '유저를 찾을 수 없습니다.',
            });
        }
        if (!user.password) { //req.user 비밀번호 정보가 없을 경우 - 얘도 굳이긴 한데 확인하면 좋긴 하지
            return res.status(400).json({
                message: '비밀번호 정보가 없습니다.',
            });
        }
        const isMatch = await bcrypt.compare(currentPassword, user.password);
        if (!isMatch) { //DB의 비밀번호 정보와 currentPassword 값이 일치하지 않을 경우
            return res.status(401).json({
                message: '현재 비밀번호가 일치하지 않습니다.',
            });
        }

        if (newPassword.length < 6) { //새 비밀번호가 너무 짧을 경우
            return res.status(400).json({
                message: '비밀번호는 최소 6자 이상입니다.',
            });
        }

        if (user.provider !== 'local') { //소셜로그인 유저일 경우
            return res.status(403).json({
                message: '소셜 로그인 계정은 비밀번호를 변경할 수 없습니다.',
            });
        }

		//문제가 없을시 비밀번호 변경
        const hashed = await bcrypt.hash(newPassword, 12);
        await user.update({ password: hashed });

        return res.status(200).json({
            message: '비밀번호가 성공적으로 변경되었습니다.',
        });
    } catch (err) {
        next(err);
    }
};

❗ Res에 success:true/false 란이 없음


이메일 인증 코드 발송

services>auth.ts

type VerificationInfo = {
    code: string;
    expiresAt: number;
};

const verificationStore = new Map<string, VerificationInfo>();

export const generateCode = (): string => {
    return Math.floor(100000 + Math.random() * 900000).toString(); // 6자리 숫자
};

export const saveCodeToStore = (email: string, code: string) => {
    const expiresAt = Date.now() + 5 * 60 * 1000; // 5분 뒤 만료
    verificationStore.set(email, { code, expiresAt });
};

export const checkCodeFromStore = (
    email: string,
    inputCode: string
): boolean => {
    const record = verificationStore.get(email);
    if (!record) return false;

    const { code, expiresAt } = record;

    if (Date.now() > expiresAt) {
        verificationStore.delete(email);
        return false;
    }

    return code === inputCode;
};

 

controllers>auth.ts

export const sendVerificationCode = async (
    req: Request,
    res: Response,
    next: NextFunction
) => {
    try {
        const { email } = req.body;
        const code = generateCode();
        saveCodeToStore(email, code); //map에 저장
        await sendEmail(email, code); //이메일 전송
        return res.status(200).json({
            message: '인증 코드 전송이 완료되었습니다.',
        });
    } catch (err) {
        return next(err);
    }
};

이메일 인증 코드 확인

export const verifyCode = (req: Request, res: Response, next: NextFunction) => {
    try {
        const { email, code } = req.body;
        const isVaild = checkCodeFromStore(email, code);
        if (!isVaild) {
            return res.status(400).json({
                message: '코드가 유효하지 않거나 만료되었습니다.',
            });
        }
        return res.status(200).json({
            message: '인증 성공되었습니다.',
        });
    } catch (err) {
        return next(err);
    }
};

❗ 유효하지 않는 경우와 만료된 경우를 서로 분리해야지 않을까?