-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] access token 재갱신 #142
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
22 commits
Select commit
Hold shift + click to select a range
39c8b8b
chore: 개발환경에서 https 설정
aken-you 44ba882
refactor: redirection url에서 로딩 ui 제거
aken-you 6c161c5
style: 주석 제거
aken-you 0b9066e
refactor: logout mutation 수정
aken-you 769a12f
feat: ApiError 클래스 생성
aken-you fad8026
refactor: 에러 메세지 제공
aken-you ed16614
refactor: useLogoutMutation onSuccess에서 socialImageURL 쿠키 삭제
aken-you 1c2d10d
refactor: access token 재갱신
aken-you f6f92e3
style: prettier 적용
aken-you 48a56f9
refactor: https 설정 삭제
aken-you 235aaf7
refactor: API_BASE_URL 수정
aken-you 7c0a496
refactor: middleware를 통해 비회원 페이지 접근 제한
aken-you d1be94f
refactor: access token 재갱신 로직 개선 및 대기열 처리 추가
aken-you af34ca6
refactor: sign-up 페이지에서 access token 체크 로직 추가
aken-you 9aad95f
feat: access token 재갱신
aken-you 3653afe
refactor: 서버 instance에서 refresh token 재갱신 실패하면 login 페이지로 이동
aken-you 1af891e
feat: 이미 이름을 등록한 유저가 sign-up에 접근할 경우, 메인 페이지로 이동
aken-you 5eacf17
refactor: middleware에서 access token 갱신
aken-you 9d06134
refactor: 회원가입 페이지가 아닌 경우 memberId check
aken-you 1cb8bff
style: 주석 추가
aken-you 5542102
refactor: gitignore에 certificates 삭제
aken-you 9d56392
refactor: verifyAccessToken에서 axios를 fetch로 변경
aken-you 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
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
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
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
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
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,114 @@ | ||
| import { NextResponse } from 'next/server'; | ||
| import type { NextRequest } from 'next/server'; | ||
| import { getServerCookie } from '@/shared/lib/server-cookie'; | ||
| import { isNumeric } from '@/shared/lib/validation'; | ||
| import { isApiError } from '@/shared/tanstack-query/api-error'; | ||
|
|
||
| const verifyAccessToken = async (accessToken: string) => { | ||
| try { | ||
| // Access token로 memberId만 반환하는 api | ||
| const response = await fetch( | ||
| `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/auth/me`, | ||
| { | ||
| method: 'GET', | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| const errorData = await response.json(); | ||
|
|
||
| if (isApiError(errorData) && errorData.errorCode === 'AUTH001') { | ||
| return { state: 'invalid' }; | ||
| } | ||
|
|
||
| return { state: 'unknownError' }; | ||
| } | ||
|
|
||
| const data: { content: number } = await response.json(); | ||
|
|
||
| return { state: 'valid', memberId: data.content }; | ||
| } catch (error) { | ||
| return { state: 'unknownError' }; | ||
| } | ||
| }; | ||
|
|
||
| const refreshAccessToken = async () => { | ||
| try { | ||
| const refreshToken = await getServerCookie('refresh_token'); | ||
|
|
||
| const response = await fetch( | ||
| `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/auth/access-token/refresh`, | ||
| { | ||
| method: 'GET', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| cookie: `refresh_token=${refreshToken}`, // 쿠키를 수동으로 전송 | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| const data: { content: { accessToken: string } } = await response.json(); | ||
|
|
||
| return data.content.accessToken; | ||
| } catch (error) { | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| export async function middleware(request: NextRequest) { | ||
| const accessToken = request.cookies.get('accessToken')?.value; | ||
| const memberId = request.cookies.get('memberId')?.value; | ||
|
|
||
| const hasAccessToken = request.cookies.has('accessToken'); | ||
| const hasMemberId = request.cookies.has('memberId') && isNumeric(memberId); | ||
|
|
||
| if ( | ||
| !hasAccessToken || | ||
| (request.nextUrl.pathname !== '/sign-up' && !hasMemberId) // 회원가입 페이지가 아닌 경우 memberId 체크 (회원가입 하지 않을 경우, memberId는 null) | ||
| ) { | ||
| const loginUrl = new URL('/login', request.url); | ||
|
|
||
| return NextResponse.redirect(loginUrl); | ||
| } | ||
|
|
||
| // access token 갱신 필요 여부 확인 | ||
| const verifyResponse = await verifyAccessToken(accessToken); | ||
|
|
||
| const response = NextResponse.next(); | ||
|
|
||
| // access token이 유효하지 않을 경우 -> 갱신 | ||
| if (verifyResponse.state === 'invalid') { | ||
| const newAccessToken = await refreshAccessToken(); | ||
|
|
||
| if (newAccessToken) { | ||
| // 갱신 성공 | ||
| response.cookies.set('accessToken', newAccessToken, { | ||
| secure: true, | ||
| sameSite: 'strict', | ||
| path: '/', | ||
| }); | ||
| } else { | ||
| // 갱신 실패 | ||
| const loginUrl = new URL('/login', request.url); | ||
|
|
||
| return NextResponse.redirect(loginUrl); | ||
| } | ||
| } | ||
|
|
||
| // 이미 회원가입 완료 했는데, sign-up 페이지에 진입할 경우 메인 페이지로 리다이렉트 | ||
| if (request.nextUrl.pathname === '/sign-up' && hasMemberId) { | ||
| const mainUrl = new URL('/', request.url); | ||
|
|
||
| return NextResponse.redirect(mainUrl); | ||
| } | ||
|
|
||
| return response; | ||
| } | ||
|
|
||
| // middleware가 적용될 경로 설정 | ||
| export const config = { | ||
| matcher: ['/', '/my-page', '/my-study', '/my-study-review', '/sign-up'], | ||
| }; |
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,10 @@ | ||
| import { axiosServerInstance } from '@/shared/tanstack-query/axios.server'; | ||
| import { GetUserProfileResponse } from './types'; | ||
|
|
||
| export const getUserProfileInServer = async ( | ||
| memberId: number, | ||
| ): Promise<GetUserProfileResponse> => { | ||
| const res = await axiosServerInstance.get(`/members/${memberId}/profile`); | ||
|
|
||
| return res.data.content; | ||
| }; |
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
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
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
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
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
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,3 @@ | ||
| export function isNumeric(str: string) { | ||
| return /^\d+$/.test(str); | ||
| } |
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,40 @@ | ||
| class ApiError extends Error { | ||
| public name = 'ApiError'; | ||
| public statusCode: number; | ||
| public errorCode: string; | ||
| public errorName: string; | ||
| public message: string; | ||
|
|
||
| constructor({ | ||
| statusCode, | ||
| errorCode, | ||
| errorName, | ||
| message, | ||
| }: { | ||
| statusCode: number; | ||
| errorCode: string; | ||
| errorName: string; | ||
| message: string; | ||
| }) { | ||
| super(message); | ||
| this.statusCode = statusCode; | ||
| this.errorCode = errorCode; | ||
| this.errorName = errorName; | ||
| this.message = message; | ||
| } | ||
| } | ||
|
|
||
| // API 에러인지 확인하는 함수 | ||
| const isApiError = (error: unknown): error is ApiError => { | ||
| return ( | ||
| error instanceof ApiError || | ||
| (typeof error === 'object' && | ||
| error !== null && | ||
| 'statusCode' in error && | ||
| 'errorCode' in error && | ||
| 'errorName' in error && | ||
| 'message' in error) | ||
| ); | ||
| }; | ||
|
|
||
| export { ApiError, isApiError }; | ||
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
혹시 여기 name과 errorName이 다른 역할을 하는 건가요?
name은 어떤 곳에 쓰이는 건지 궁금합니당
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
name은 저희쪽에서 만든 Error 객체임을 표현하고 싶었고,
errorName은 서버에서 응답값으로 내려줍니다.