-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] 인증 인가 관련 토큰 로직 생성 #128
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
7 commits
Select commit
Hold shift + click to select a range
2ae24ca
fix: 변경된 api 설계에 따라 refreshToken 변경
yoorli dd08514
feat: 토큰 처리 로직 생성
yoorli 729c547
feat: 토큰 재발급 msw 추가
yoorli e3139ce
Merge remote-tracking branch 'origin/main' into yoolri-feat/token-logic
yoorli b04c503
fix: 리뷰 사항 참고 수정
yoorli c468074
Merge branch 'main' into yoolri-feat/token-logic
yoorli f7c573a
fix: 추가된 공통 응답 처리 함수에 맞춰 로그인, 회원가입 코드 수정
yoorli 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,33 +1,37 @@ | ||
| import type { AxiosError } from 'axios'; | ||
|
|
||
| import { baseAPI } from '@/api/core'; | ||
| import { LoginRequest, LoginResponse, SignupRequest, SignupResponse } from '@/types/service/auth'; | ||
| import { CommonErrorResponse } from '@/types/service/common'; | ||
|
|
||
| export const isProblemDetailError = (error: unknown): error is AxiosError<CommonErrorResponse> => { | ||
| return ( | ||
| typeof error === 'object' && | ||
| error !== null && | ||
| 'isAxiosError' in error && | ||
| (error as AxiosError).isAxiosError === true | ||
| ); | ||
| }; | ||
| import { api } from '@/api/core'; | ||
| import { clearAccessToken, setAccessToken } from '@/lib/auth/token'; | ||
| import { | ||
| LoginRequest, | ||
| LoginResponse, | ||
| RefreshResponse, | ||
| SignupRequest, | ||
| SignupResponse, | ||
| } from '@/types/service/auth'; | ||
|
|
||
| export const authServiceRemote = () => ({ | ||
| // 로그인 | ||
| login: async (payload: LoginRequest): Promise<LoginResponse> => { | ||
| const { data } = await baseAPI.post<LoginResponse>('/api/v1/auth/login', payload); | ||
| const data = await api.post<LoginResponse>('/auth/login', payload); | ||
|
|
||
| setAccessToken(data.accessToken, data.expiresIn); | ||
| return data; | ||
| }, | ||
|
|
||
| // 회원가입 | ||
| signup: async (payload: SignupRequest): Promise<SignupResponse> => { | ||
| const { data } = await baseAPI.post<SignupResponse>('/api/v1/auth/signup', payload); | ||
| return data; | ||
| }, | ||
| signup: async (payload: SignupRequest): Promise<SignupResponse> => | ||
| api.post<SignupResponse>(`/auth/signup`, payload), | ||
|
|
||
| // 로그아웃 | ||
| logout: async (): Promise<void> => { | ||
| await baseAPI.post('/api/v1/auth/logout'); | ||
| await api.post<void>('/auth/logout'); | ||
| clearAccessToken(); | ||
| }, | ||
|
|
||
| // 액세스 토큰 재발급 | ||
| refresh: async (): Promise<RefreshResponse> => { | ||
| const data = await api.post<RefreshResponse>('/auth/refresh'); | ||
|
|
||
| setAccessToken(data.accessToken, data.expiresIn); | ||
| return data; | ||
| }, | ||
| }); |
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,19 @@ | ||
| const ACCESS_TOKEN_KEY = 'accessToken'; | ||
|
|
||
| export const setAccessToken = (token: string, maxAgeSeconds?: number) => { | ||
| if (typeof document === 'undefined') return; | ||
|
|
||
| const parts = [`${ACCESS_TOKEN_KEY}=${encodeURIComponent(token)}`, 'path=/']; | ||
|
|
||
| if (typeof maxAgeSeconds === 'number' && maxAgeSeconds > 0) { | ||
| parts.push(`Max-Age=${maxAgeSeconds}`); | ||
| } | ||
|
|
||
| document.cookie = parts.join('; '); | ||
| }; | ||
|
|
||
| export const clearAccessToken = () => { | ||
| if (typeof document === 'undefined') return; | ||
|
|
||
| document.cookie = `${ACCESS_TOKEN_KEY}=; Max-Age=0; path=/`; | ||
| }; |
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 |
|---|---|---|
| @@ -1,66 +1,104 @@ | ||
| import { http, HttpResponse } from 'msw'; | ||
|
|
||
| import { LoginRequest, LoginResponse, SignupRequest } from '@/types/service/auth'; | ||
| import { CommonErrorResponse } from '@/types/service/common'; | ||
| import { | ||
| LoginRequest, | ||
| LoginResponse, | ||
| RefreshResponse, | ||
| SignupRequest, | ||
| SignupResponse, | ||
| } from '@/types/service/auth'; | ||
|
|
||
| import { createMockErrorResponse, createMockSuccessResponse } from '../common/common-mock'; | ||
| import { | ||
| createLoginResponse, | ||
| createSignupResponse, | ||
| isEmailTaken, | ||
| isNicknameTaken, | ||
| } from './auth-utils'; | ||
|
|
||
| // 회원가입 | ||
| const signupMock = http.post('*/api/v1/auth/signup', async ({ request }) => { | ||
| const body = (await request.json()) as SignupRequest; | ||
|
|
||
| if (isEmailTaken(body.email)) { | ||
| const errorBody: CommonErrorResponse = { | ||
| type: 'https://example.com/errors/email-duplicate', | ||
| title: 'EMAIL_DUPLICATE', | ||
| status: 400, | ||
| detail: '이미 존재하는 이메일입니다.', | ||
| instance: '/api/v1/auth/signup', | ||
| errorCode: 'A002', | ||
| }; | ||
| return HttpResponse.json<CommonErrorResponse>(errorBody, { status: 400 }); | ||
| return HttpResponse.json( | ||
| createMockErrorResponse({ | ||
| status: 400, | ||
| detail: '이미 존재하는 이메일입니다.', | ||
| errorCode: 'A002', | ||
| }), | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| if (isNicknameTaken(body.nickName)) { | ||
| const errorBody: CommonErrorResponse = { | ||
| type: 'https://example.com/errors/nickname-duplicate', | ||
| title: 'NICKNAME_DUPLICATE', | ||
| status: 400, | ||
| detail: '이미 존재하는 닉네임입니다.', | ||
| instance: '/api/v1/auth/signup', | ||
| errorCode: 'A003', | ||
| }; | ||
| return HttpResponse.json<CommonErrorResponse>(errorBody, { status: 400 }); | ||
| return HttpResponse.json( | ||
| createMockErrorResponse({ | ||
| status: 400, | ||
| detail: '이미 존재하는 닉네임입니다.', | ||
| errorCode: 'A003', | ||
| }), | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| const response = createSignupResponse(body.email, body.nickName, body.password); | ||
| return HttpResponse.json(response, { status: 201 }); | ||
|
|
||
| return HttpResponse.json(createMockSuccessResponse<SignupResponse>(response)); | ||
| }); | ||
|
|
||
| // 로그인 | ||
| const loginMock = http.post('*/api/v1/auth/login', async ({ request }) => { | ||
| const body = (await request.json()) as LoginRequest; | ||
|
|
||
| try { | ||
| const response = createLoginResponse(body.email, body.password); | ||
| return HttpResponse.json<LoginResponse>(response, { status: 200 }); | ||
|
|
||
| return HttpResponse.json(createMockSuccessResponse<LoginResponse>(response), { | ||
| headers: { | ||
| 'Set-Cookie': 'refreshToken=mock-refresh-token; Path=/; HttpOnly; SameSite=Strict; Secure', | ||
| }, | ||
| }); | ||
| } catch { | ||
| const errorBody: CommonErrorResponse = { | ||
| type: 'https://example.com/errors/invalid-credentials', | ||
| title: 'INVALID_CREDENTIALS', | ||
| status: 400, | ||
| detail: '이메일 또는 비밀번호가 올바르지 않습니다.', | ||
| instance: '/api/v1/auth/login', | ||
| errorCode: 'A001', | ||
| }; | ||
| return HttpResponse.json(errorBody, { status: 400 }); | ||
| return HttpResponse.json( | ||
| createMockErrorResponse({ | ||
| status: 400, | ||
| detail: '이메일 또는 비밀번호가 올바르지 않습니다.', | ||
| errorCode: 'A001', | ||
| }), | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| const logoutMock = http.post('*/api/v1/auth/logout', async ({}) => { | ||
| return new HttpResponse(null, { status: 204 }); | ||
| // 로그아웃 | ||
| const logoutMock = http.post('*/api/v1/auth/logout', async () => { | ||
| return HttpResponse.json(createMockSuccessResponse<void>(undefined)); | ||
| }); | ||
|
|
||
| // 액세스 토큰 재발급 | ||
| const refreshMock = http.post('*/api/v1/auth/refresh', async ({ cookies }) => { | ||
| const refreshToken = cookies['refreshToken']; | ||
|
|
||
| if (!refreshToken) { | ||
| return HttpResponse.json( | ||
| createMockErrorResponse({ | ||
| status: 401, | ||
| detail: '리프레시 토큰이 없습니다.', | ||
| errorCode: 'A004', | ||
| }), | ||
| { status: 401 }, | ||
| ); | ||
| } | ||
|
|
||
| const response: RefreshResponse = { | ||
| accessToken: 'refreshed-mock-access-token', | ||
| tokenType: 'Bearer', | ||
| expiresIn: 3600, | ||
| expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), | ||
| }; | ||
|
|
||
| return HttpResponse.json(createMockSuccessResponse<RefreshResponse>(response)); | ||
| }); | ||
|
|
||
| export const authHandlers = [signupMock, loginMock, logoutMock]; | ||
| export const authHandlers = [signupMock, loginMock, logoutMock, refreshMock]; |
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
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.
getAccessToken과 clearAccessToken은 클라이언트에서만 사용되어야 하니까 서버 환경일 때는 error를 던져주면 디버깅에 용이할 것 같네요!