-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 인증 및 api 클라이언트 구현 #50 #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
fd7e6dc
feat(50): ky 설치
Seojunhwan 7ea37cc
chore(50): iron-session 설치
Seojunhwan e1c5c9b
feat(50): 환경 확인 유틸 추가
Seojunhwan 280d68c
refactor(50): function -> const
Seojunhwan c1512dd
feat(50): custom exception 추가
Seojunhwan 9f213ce
feat(50): API 클라이언트 및 에러 처리 로직 추가
Seojunhwan f3b86cd
feat(50): 클라이언트 및 서버 세션 관리 기능 추가
Seojunhwan dafb9b6
feat(50): 시간 관련 상수 및 토큰 유효 기간 정의 추가
Seojunhwan 0dbe27c
feat(50): 로그인, 리프레시 토큰 발급 및 세션 조회 API 추가
Seojunhwan 821fa04
feat(50): 인증 관련 API 및 쿼리 훅 추가 (로그인, 토큰 재발급, 세션 관리)
Seojunhwan 2c67bfa
fix(50): ApiError의 errorCode 필드를 선택적(optional)로 변경
Seojunhwan 88b49f4
refactor(50): 인증 API 경로 수정 및 주석 개선
Seojunhwan 9fc1e18
feat(50): 인증 콜백 페이지 추가 및 로그인 처리 로직 구현
Seojunhwan f3952b0
feat(50): 미들웨어 추가하여 인증 및 토큰 갱신 로직 구현
Seojunhwan 5a7b795
refactor(50): 세션 관련 모듈 경로 통합 및 코드 정리
Seojunhwan bad6fca
refactor(50): OAuth 로그인 URL 요청 함수 이름 변경 및 주석 개선
Seojunhwan 5f7e73c
fix(50): 로그인 요청 시 origin 헤더 추가 및 OAuth 리다이렉션 URL 수정
Seojunhwan 5a993b2
fix(50): 로그인 실패 시 UnauthorizedException 처리 및 에러 메시지 개선
Seojunhwan 4feb0e6
feat(50): 인증 API 및 쿼리 훅 구조 개선, 세션 관리 기능 추가
Seojunhwan c4b2183
fix(50): 로그인 실패 시 리다이렉션 추가 및 에러 메시지 처리 개선
Seojunhwan 4269b15
fix(50): 공개 경로에 루트 경로 추가
Seojunhwan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { http, nextHttp } from "@/lib/api/client"; | ||
|
|
||
| import type { | ||
| LoginRequest, | ||
| LoginResponse, | ||
| ReissueRequest, | ||
| ReissueResponse, | ||
| } from "./auth.types"; | ||
|
|
||
| /** | ||
| * 백엔드의 /api/auth/login 엔드포인트에 로그인 요청을 보냅니다. | ||
| * | ||
| * @param {LoginRequest} params - 카카오 인가 코드 | ||
| * @returns {Promise<LoginResponse>} 로그인 응답 데이터 | ||
| */ | ||
| export const postLogin = async (params: LoginRequest) => { | ||
| return await http | ||
| .post("api/auth/login", { json: params }) | ||
| .json<LoginResponse>(); | ||
| }; | ||
|
|
||
| /** | ||
| * 백엔드의 /api/auth/reissue 엔드포인트에 토큰 재발급 요청을 보냅니다. | ||
| * | ||
| * @param {ReissueRequest} params - 리프레시 토큰 | ||
| * @returns {Promise<ReissueResponse>} 재발급된 토큰 데이터 | ||
| */ | ||
| export const postReissue = async (params: ReissueRequest) => { | ||
| return await http | ||
| .post("api/auth/reissue", { json: params }) | ||
| .json<ReissueResponse>(); | ||
| }; | ||
|
|
||
| /** | ||
| * OAuth 제공자의 인증 페이지로 브라우저를 리다이렉트시킵니다. | ||
| * | ||
| * @description | ||
| * 이 함수를 호출하면, 서버로부터 302 리다이렉트 응답을 받아 | ||
| * OAuth 제공자의 인증 페이지로 즉시 이동합니다. | ||
| * 반환 값은 없으며, 호출 즉시 페이지가 전환됩니다. | ||
| */ | ||
| export const redirectToKakaoOAuthLoginPage = async () => { | ||
| window.location.href = `${process.env.NEXT_PUBLIC_API_URL}/api/auth/login/oauth`; | ||
| }; | ||
|
|
||
| type Information = Omit<LoginResponse, "token">["information"]; | ||
|
|
||
| /** | ||
| * Next.js API Route(/api/auth/login)를 통해 로그인 요청을 보냅니다. | ||
| * | ||
| * @param {LoginRequest} params - 카카오 인가 코드 | ||
| * @returns {Promise<Information>} 회원 정보 | ||
| */ | ||
| export const postClientLogin = async (params: Omit<LoginRequest, "origin">) => { | ||
| return await nextHttp | ||
| .post("api/auth/login", { json: params }) | ||
| .json<Information>(); | ||
| }; | ||
|
|
||
| /** | ||
| * Next.js API Route(/api/auth/reissue)를 통해 토큰 재발급 요청을 보냅니다. | ||
| * | ||
| * @returns {Promise<ReissueResponse>} 재발급된 세션 정보 | ||
| */ | ||
| export const postClientReissue = async () => { | ||
| return await nextHttp.post("api/auth/reissue").json<ReissueResponse>(); | ||
| }; | ||
|
|
||
| /** | ||
| * Next.js API Route(/api/auth/logout)를 통해 세션 삭제(로그아웃) 요청을 보냅니다. | ||
| * | ||
| * @returns {Promise<void>} 로그아웃 결과 | ||
| */ | ||
| export const deleteClientSession = async () => { | ||
| return await nextHttp.delete("api/auth/logout").json(); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { useMutation } from "@tanstack/react-query"; | ||
|
|
||
| import { | ||
| deleteClientSession, | ||
| postClientLogin, | ||
| postClientReissue, | ||
| } from "./auth.api"; | ||
|
|
||
| export const useLoginMutation = () => { | ||
| return useMutation({ | ||
| mutationFn: postClientLogin, | ||
| }); | ||
| }; | ||
|
|
||
| export const useReissueMutation = () => { | ||
| return useMutation({ | ||
| mutationFn: postClientReissue, | ||
| }); | ||
| }; | ||
|
|
||
| export const useDeleteSessionMutation = () => { | ||
| return useMutation({ | ||
| mutationFn: deleteClientSession, | ||
| }); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| export type LoginRequest = { | ||
| code: string; | ||
| origin: string; | ||
| }; | ||
|
|
||
| export type LoginResponse = { | ||
| token: { | ||
| accessToken: string; | ||
| refreshToken: string; | ||
| }; | ||
| information: { | ||
| id: number; | ||
| isSignUp: boolean; | ||
| }; | ||
| }; | ||
|
|
||
| export type ReissueRequest = { | ||
| refreshToken: string; | ||
| }; | ||
|
|
||
| export type ReissueResponse = { | ||
| accessToken: string; | ||
| refreshToken: string; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { nextHttp } from "@/lib/api/client"; | ||
|
|
||
| import type { SessionData } from "./session.types"; | ||
|
|
||
| /** | ||
| * Next.js API Route(/api/session)를 통해 세션 정보를 요청합니다. | ||
| * | ||
| * @returns {Promise<SessionData>} 세션 정보 | ||
| */ | ||
| export const getSession = async () => { | ||
| return await nextHttp.get<SessionData>("api/session").json(); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { queryOptions } from "@tanstack/react-query"; | ||
|
|
||
| import { getSession } from "./session.api"; | ||
|
|
||
| const sessionQueryKeys = { | ||
| session: ["session"], | ||
| }; | ||
|
|
||
| export const sessionQueries = { | ||
| session: queryOptions({ | ||
| queryKey: sessionQueryKeys.session, | ||
| queryFn: getSession, | ||
| }), | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { type SessionData } from "@/lib/session"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| "use client"; | ||
|
|
||
| import { useRouter, useSearchParams } from "next/navigation"; | ||
| import { useEffect } from "react"; | ||
|
|
||
| import { useLoginMutation } from "@/app/(auth)/_api/auth/auth.queries"; | ||
| import { clearClientSessionCache } from "@/lib/session"; | ||
|
|
||
| export default function AuthCallbackPage() { | ||
| const router = useRouter(); | ||
| const searchParams = useSearchParams(); | ||
| const code = searchParams.get("code"); | ||
| const next = searchParams.get("next"); | ||
|
|
||
| const { mutate: login } = useLoginMutation(); | ||
|
|
||
| useEffect(() => { | ||
| if (code) { | ||
| login( | ||
| { code }, | ||
| { | ||
| onSuccess: () => { | ||
| clearClientSessionCache(); | ||
|
|
||
| const redirectUrl = next || "/"; | ||
|
|
||
| router.replace(redirectUrl); | ||
| }, | ||
| onError: error => { | ||
| console.error("로그인에 실패했습니다:", error); | ||
| alert("로그인에 실패했습니다. 다시 시도해주세요."); | ||
| router.replace("/login"); | ||
| }, | ||
| } | ||
| ); | ||
| } else { | ||
| alert("비정상적인 접근입니다."); | ||
| router.replace("/"); | ||
| } | ||
| }, [code, login, router, next]); | ||
|
|
||
| return <div>로그인 중입니다...</div>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import { type NextRequest, NextResponse } from "next/server"; | ||
|
|
||
| import { postLogin } from "@/app/(auth)/_api/auth/auth.api"; | ||
| import { TOKEN_TIMES } from "@/constants/time.constants"; | ||
| import { type ApiError } from "@/lib/api"; | ||
| import { UnauthorizedException } from "@/lib/exceptions"; | ||
| import { getSessionFromServer } from "@/lib/session"; | ||
|
|
||
| /** | ||
| * 로그인 요청 | ||
| * @param req - 요청 객체 | ||
| * @returns 응답 객체 | ||
| */ | ||
| export const POST = async (req: NextRequest) => { | ||
| const { code } = await req.json(); | ||
| const origin = req.headers.get("origin"); | ||
|
|
||
| if (!code) { | ||
| return NextResponse.json<ApiError>( | ||
| { errorMessage: "인가 코드가 필요합니다." }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| if (!origin) { | ||
| return NextResponse.json<ApiError>( | ||
| { errorMessage: "올바르지 않은 요청입니다." }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| const data = await postLogin({ | ||
| code, | ||
| origin, | ||
| }); | ||
| const session = await getSessionFromServer(); | ||
|
|
||
| session.isLoggedIn = true; | ||
| session.accessToken = data.token.accessToken; | ||
| session.refreshToken = data.token.refreshToken; | ||
| session.userId = String(data.information.id); | ||
| // TODO: 백엔드로부터 토큰 만료 시간 받아오면 변경하기 (1시간) | ||
| session.accessTokenExpiresAt = | ||
| Date.now() + TOKEN_TIMES.ACCESS_TOKEN_LIFESPAN; | ||
|
|
||
| await session.save(); | ||
|
|
||
| return NextResponse.json(data.information); | ||
| } catch (error) { | ||
| console.error("Login failed:", error); | ||
|
|
||
| return NextResponse.json<ApiError>( | ||
| { errorMessage: "로그인에 실패했습니다." }, | ||
| { status: error instanceof UnauthorizedException ? 401 : 400 } | ||
| ); | ||
| } | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| import { postReissue } from "@/app/(auth)/_api/auth/auth.api"; | ||
| import { type ApiError } from "@/lib/api"; | ||
| import { getSessionFromServer } from "@/lib/session"; | ||
|
|
||
| export const POST = async () => { | ||
| const session = await getSessionFromServer(); | ||
| const { refreshToken } = session; | ||
|
|
||
| if (!refreshToken) { | ||
| return NextResponse.json<ApiError>( | ||
| { errorMessage: "리프레시 토큰이 없습니다." }, | ||
| { status: 401 } | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| const data = await postReissue({ refreshToken }); | ||
|
|
||
| session.accessToken = data.accessToken; | ||
| session.refreshToken = data.refreshToken; | ||
| await session.save(); | ||
|
Seojunhwan marked this conversation as resolved.
|
||
|
|
||
| return NextResponse.json(session); | ||
| } catch (error) { | ||
| console.error("Reissue failed:", error); | ||
| // 토큰 재발급 실패 시 세션 초기화 | ||
| session.destroy(); | ||
|
Seojunhwan marked this conversation as resolved.
|
||
| return NextResponse.json<ApiError>( | ||
| { errorMessage: "토큰 재발급에 실패했습니다." }, | ||
| { status: 401 } | ||
| ); | ||
| } | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| import { getSessionFromServer } from "@/lib/session"; | ||
|
|
||
| export const GET = async () => { | ||
| const session = await getSessionFromServer(); | ||
| return NextResponse.json(session); | ||
| }; | ||
|
Seojunhwan marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./time.constants"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| // 기본 시간 단위 (밀리초) | ||
| const SECOND = 1000; | ||
| const MINUTE = 60 * SECOND; | ||
| const HOUR = 60 * MINUTE; | ||
| const DAY = 24 * HOUR; | ||
|
|
||
| export const TIME = { | ||
| SECOND, | ||
| MINUTE, | ||
| HOUR, | ||
| DAY, | ||
| }; | ||
|
|
||
| /** | ||
| * 인증 및 토큰과 관련된 시간 상수(밀리초 단위)를 정의합니다. | ||
| */ | ||
| export const TOKEN_TIMES = { | ||
| // /** Access Token의 유효 시간 (1시간) */ | ||
| ACCESS_TOKEN_LIFESPAN: 1 * HOUR, | ||
|
|
||
| // /** 클라이언트 세션 캐시 유지 시간 (59분) */ | ||
| CLIENT_SESSION_CACHE_DURATION: 59 * MINUTE, | ||
|
|
||
| // /** 주기적 토큰 재발급 간격 (59분) */ | ||
| PERIODIC_TOKEN_REFRESH_INTERVAL: 59 * MINUTE, | ||
|
|
||
| // /** 미들웨어에서 토큰 만료 임박으로 간주하고 재발급을 시도하는 시간 (5분) */ | ||
| MIDDLEWARE_TOKEN_REFRESH_THRESHOLD: 5 * MINUTE, | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.