[2024.05.28 TIL] 내일배움캠프 30일차 (개인 과제 기능 구현, 배포)

My_Code·2024년 5월 28일
0

TIL

목록 보기
40/112
post-thumbnail

본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.


💻 TIL(Today I Learned)

📌 Today I Done

✏️ 역할 인가 Middleware

  • 사용자 정보는 인증 Middleware(req.user)를 통해서 전달 받음

  • 허용 역할은 Middleware 사용 시 배열로 전달 받음

// src/middlewares/role.middleware.js

// 미들웨어는 req, res, next를 필요로 하는 함수
// 그렇기에 매개변수를 사용할 수 있는 미들웨어를 만들기 위해 미들웨어를 리턴하는 함수를 만듦
export const requiredRoles = (roles) => {
    return async (req, res, next) => {
        // 현재 사용자의 역할을 가져옴
        const { role } = req.user;

        // 배열로 받아온 roles에 현재 사용자의 역할이 포함되는지 확인
        if (roles.includes(role)) {
            // 역할이 포함되면 다음으로 진행
            return next();
        }
        return res.status(401).json({ status: 401, message: '접근 권한이 없습니다.' });
    };
};

✏️ 이력서 지원 상태 변경 API

  • 사용자 정보는 인증 Middleware(req.user)를 통해서 전달 받음

  • 이력서 ID를 Path Parameters(req.params)로 전달 받음

  • 지원 상태, 사유Request Body(req.body)로 전달 받음

  • 이력서 정보 수정과 이력서 로그 생성을 Transaction으로 묶어서 실행

// src/routers/resumes.router.js

// 이력서 지원 상태 변경 API
router.patch('/resumes/:resumeId/state', authMiddleware, requiredRoles(Object.values(USER_ROLE)), async (req, res, next) => {
    try {
        // 사용자 정보 가져옴
        const { userId } = req.user;
        // 이력서 ID 가져옴
        const { resumeId } = req.params;
        //지원 상태, 사유 가져옴
        const validation = await resumeStateSchema.validateAsync(req.body);
        const { state, reason } = validation;

        // 이력서가 존재하는지 조회
        const resume = await prisma.resumes.findFirst({ where: { resumeId: +resumeId } });
        if (!resume) {
            return res.status(401).json({ status: 401, message: '이력서가 존재하지 않습니다.' });
        }

        let resumeLog; // 이력서 변경 로그

        // 트랜젝션을 통해서 작업의 일관성 유지
        await prisma.$transaction(
            async (tx) => {
                // 이력서 수정
                const updatedResume = await tx.resumes.update({ where: { resumeId: +resumeId }, data: { state } });

                // 이력서 변경 로그 생성
                resumeLog = await tx.resumeHistories.create({
                    data: {
                        RecruiterId: +userId,
                        ResumeId: +resumeId,
                        oldState: resume.state,
                        newState: updatedResume.state,
                        reason,
                    },
                });
            },
            {
                isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted,
            },
        );

        return res.status(201).json({ status: 201, message: '지원 상태 변경에 성공했습니다.', data: { resumeLog } });
    } catch (err) {
        next(err);
    }
});

✏️ 이력서 로그 목록 조회 API

  • 이력서 ID를 Path Parameters(req.params)로 전달 받음

  • 생성일시 기준 최신순으로 조회

  • 채용 담당자 이름을 반환하기 위해 스키마에 정의 한 Relation을 활용해 조회

// src/routers/resumes.router.js

// 이력서 로그 목록 조회 API
router.get('/resumes/:resumeId/log', authMiddleware, requiredRoles(Object.values(USER_ROLE)), async (req, res, next) => {
    // 이력서 ID 가져옴
    const { resumeId } = req.params;

    // 이력서 로그 조회
    const resumeLogs = await prisma.resumeHistories.findMany({
        where: { ResumeId: +resumeId },
        select: {
            resumeLogId: true,
            Resume: {
                select: {
                    User: {
                        select: {
                            name: true,
                        },
                    },
                },
            },
            ResumeId: true,
            oldState: true,
            newState: true,
            reason: true,
            createdAt: true,
        },
        orderBy: { createdAt: 'desc' },
    });

    return res.status(200).json({ status: 200, message: '이력서 로그 목록 조회에 성공했습니다.', data: { resumeLogs } });
});

✏️ RefreshToken 인증 Middleware

  • AccessToken 인증 미들웨어와 거의 동일함

  • RefreshTokenRequest Header의 Authorization 값(req.headers.authorization)으로 전달 받음

  • Payload에 담긴 사용자 ID를 이용하여 사용자 정보를 조회

  • 이 때, RefreshToken은 DB에 보관하기 때문에 DB에 접근해서 조회

  • Payload에 담긴 사용자 ID와 일치하는 사용자가 없는 경우에는 폐기 된 인증 정보입니다 라고 출력

// src/middlewares/auth.refresh.token.middleware.js

// RefreshToken 인증 미들웨어
export default async (req, res, next) => {
    try {
        // 헤더에서 Refresh 토큰 가져옴
        const authorization = req.headers['authorization'];
        if (!authorization) throw new Error('인증 정보가 없습니다.');

        // Refresh 토큰이 Bearer 형식인지 확인
        const [tokenType, token] = authorization.split(' ');
        if (tokenType !== 'Bearer') throw new Error('지원하지 않는 인증 방식입니다.');

        // 서버에서 발급한 JWT가 맞는지 검증
        const decodedToken = jwt.verify(token, process.env.REFRESH_TOKEN_SECRET_KEY);
        const userId = decodedToken.userId;

        // JWT에서 꺼낸 userId로 실제 사용자가 있는지 확인
        const user = await prisma.users.findFirst({ where: { userId: +userId } });
        if (!user) {
            return res.status(401).json({ status: 401, message: '인증 정보와 일치하는 사용자가 없습니다.' });
        }

        // DB에 저장된 RefreshToken를 조회
        const refreshToken = await prisma.refreshTokens.findFirst({ where: { UserId: user.userId } });
        // DB에 저장 된 RefreshToken이 없거나 전달 받은 값과 일치하지 않는 경우
        if (!refreshToken || refreshToken.token !== token) {
            return res.status(401).json({ status: 401, message: '폐기 된 인증 정보입니다.' });
        }

        // 조회된 사용자 정보를 req.user에 넣음
        req.user = user;
        // 다음 동작 진행
        next();
    } catch (err) {
        switch (err.name) {
            case 'TokenExpiredError':
                return res.status(401).json({ status: 401, message: '인증 정보가 만료되었습니다.' });
            case 'JsonWebTokenError':
                return res.status(401).json({ status: 401, message: '인증 정보가 유효하지 않습니다.' });
            default:
                return res.status(401).json({ status: 401, message: err.message ?? '비정상적인 요청입니다.' });
        }
    }
};

✏️ 토큰 재발급 API

  • AccessToken 만료 시 RefreshToken을 활용해 재발급

  • RefreshToken(JWT)을 Request Header의 Authorization 값(req.headers.authorization)으로 전달 받음

  • 사용자 정보는 인증 Middleware(req.user)를 통해서 전달 받음

  • AccessToken(Payload사용자 ID를 포함하고, 유효기한12시간)을 재발급

  • RefreshToken (Payload: 사용자 ID 포함, 유효기한: 7일)을 재발급

  • RefreshToken은 DB에서 보관하기 때문에 DB의 데이터를 갱신

// src/routers/auth.router.js

// 토큰 재발급 API
router.post('/auth/refresh', authRefreshTokenMiddleware, async (req, res, next) => {
    try {
        // 사용자 정보 가져옴
        const user = req.user;

        // Access Token 재발급 (12시간)
        const AccessToken = jwt.sign({ userId: user.userId }, process.env.ACCESS_TOKEN_SECRET_KEY, { expiresIn: '12h' });

        // Refresh Token 재발급 (7일)
        const RefreshToken = jwt.sign({ userId: user.userId }, process.env.REFRESH_TOKEN_SECRET_KEY, { expiresIn: '7d' });
        await prisma.refreshTokens.update({
            where: { UserId: user.userId },
            data: {
                token: RefreshToken,
                ip: req.ip,
                userAgent: req.headers['user-agent'],
                createdAt: new Date(Date.now()),
            },
        });

        return res.status(201).json({ status: 201, message: '토큰 재발급에 성공했습니다.', data: { AccessToken, RefreshToken } });
    } catch (err) {
        next(err);
    }
});

✏️ 로그아웃 API

  • RefreshToken(JWT)을 Request Header의 Authorization 값(req.headers.authorization)으로 전달 받음

  • 사용자 정보는 인증 Middleware(req.user)를 통해서 전달 받음

  • RefreshToken은 DB에서 보관하기 때문에 DB의 데이터를 삭제

  • 실제로는 AccessToken이 만료되기 전까지는 AccessToken이 필요한 API는 사용 가능함

// src/routers/auth.router.js

// 로그아웃 API
router.post('/auth/sign-out', authRefreshTokenMiddleware, async (req, res, next) => {
    try {
        // 사용자 정보 가져옴
        const user = req.user;

        // DB에서 Refresh Token 삭제
        const deletedUserId = await prisma.refreshTokens.delete({
            where: { UserId: user.userId },
            select: { UserId: true },
        });

        return res.status(201).json({ status: 201, message: '로그아웃 되었습니다.', data: { deletedUserId } });
    } catch (err) {
        next(err);
    }
});


📌 Tomorrow's Goal

✏️ 리드미 작성

  • TIL에 작성한 내용을 기반으로 리드미 작성하기

✏️ 개인과제 마무리

  • 코드 구현도 끝났으니 과제 발제 문서를 확인하면서 체크하기

  • 배포한 서버 다시 확인하기

  • 과제 질문 작성해서 도메인 주소와 함께 제출하기



📌 Today's Goal I Done

✔️ 개인과제 기능 구현

  • 그래도 어제 대부분의 기능을 구현해서 오늘은 구현이 수월했음

  • 다만 구현한 내용을 다듬고 Insomnia로 체크하는 시간이 길었음

  • 예비군 때문에 일정이 빠듯했지만 무사히 마무리 될 것 같음

  • 배포는 지난번 과제에서 배포한 EC2 인스턴스를 그대로 사용함

  • 인바운드 규칙이나 대상 그룹 설정은 따로 필요했음



⚠️ 구현 시 발생한 문제

✔️ 로그아웃을 해도 기능들을 사용할 수 있음

  • 처음에는 로그아웃을 통해 RefreshToken을 삭제하면 진짜 로그아웃 기능처럼 다른 기능들을 사용할 수 없는 줄 알았음

  • 하지만 정상적으로 로그아웃 기능을 구현 후 로그아웃을 진행해도 다른 API들을 사용할 수 있었음

  • 너무 단순하게 생각했음

  • 이번 과제에서 구현한 방식은 AccessToken과 RefreshToken을 사용하는 인증 방식임

  • AccessToken에는 만료 시간을 짧게 하고, RefreshToken에는 만료 시간을 비교적 길게 만듦

  • 이를 통해 AccessToken이 만료되면 다시 DB에 있는 RefreshToken를 통해 토큰들을 재발급 받는 구조임

  • 여기서 AccessToken은 백엔드에서 처리할 수 있는 토큰이 아님

  • 로그인을 하면 클라이언트에게 AccessToken을 넘겨줌

  • 이후 AccessToken은 클라이언트에서 다뤄지는 데이터임

  • 그렇기에 서버측에서는 AccessToken을 삭제하는 것이 불가능 함

profile
조금씩 정리하자!!!

0개의 댓글