Skip to content

Commit

Permalink
Merge pull request #3 from GihoKo/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
GihoKo authored Jul 15, 2024
2 parents 3497b75 + 71c5888 commit 0be3b98
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 58 deletions.
66 changes: 48 additions & 18 deletions src/apis/instances/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import axios, { AxiosError } from 'axios';
import { handleAxiosError } from '../../utils/handleAxiosError';
import { handleUnexpectedError } from '../../utils/handleUnexpectedError';
import getAccessToken from '../../utils/getAccessToken';
import { renewTokens } from '../services/auth';
import { useApplicationAuthTokenStore } from '@/store/useAuthStore';

const baseURL = process.env.API_URL;

Expand All @@ -13,6 +13,7 @@ export const axiosInstance = axios.create({
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});

export const axiosInstanceWithToken = axios.create({
Expand All @@ -26,41 +27,70 @@ export const axiosInstanceWithToken = axios.create({

// request interceptor의 경우 token을 넣을 때 자주 사용한다.
axiosInstanceWithToken.interceptors.request.use(
(config) => {
// 토큰을 가져온다. useApplicationAuthTokenStore()는
// hook을 사용하는 것이기 때문에 .getState()를 사용한다.
const accessToken = getAccessToken();
console.log('엑세스 토큰', accessToken);
(AxiosRequestConfig) => {
const { accessToken } = useApplicationAuthTokenStore.getState();

// 만약 토큰이 존재하는 경우 헤더에 넣어준다.
if (accessToken) {
config.headers['authorization'] = `Bearer ${accessToken}`;
AxiosRequestConfig.headers['authorization'] = `Bearer ${accessToken}`;
}
return config;
return AxiosRequestConfig;
},
(error) => {
console.error(error);
return Promise.reject(error);
},
);

axiosInstanceWithToken.interceptors.response.use(
(response) => {
return response;
},
(response) => response,
async (error: AxiosError) => {
if (!axios.isAxiosError(error)) {
handleUnexpectedError(error);
return Promise.reject(error);
}

// accessToken 만료시 재발급
if (error.response?.status === 401) {
// 토큰 관리
try {
const { setAccessToken } = useApplicationAuthTokenStore.getState();
const originalRequest = error.config;
await renewTokens();
const accessToken = getAccessToken();
if (accessToken && originalRequest) {
originalRequest.headers['authorization'] = `Bearer ${accessToken}`;
return axiosInstanceWithToken(originalRequest);

// 1. accessToken이 없거나 만료된 경우
if (error.response?.status === 401) {
// accessToken 갱신
const newAccessToken = await renewTokens();
if (typeof newAccessToken === 'string') {
// 상태 업데이트 후 새로운 accessToken 값 가져오기
return new Promise((resolve, reject) => {
// Promise로 subscribe를 사용해 accessToken이 업데이트 되었을 때까지 기다림
const unsubscribe = useApplicationAuthTokenStore.subscribe((state) => {
// 비동기적으로 accessToken이 업데이트 되면 조건문을 만족하게 됨
if (state.accessToken === newAccessToken) {
// 일단 unsubscribe
unsubscribe();
if (originalRequest) {
// 새로운 accessToken으로 요청 재시도
originalRequest.headers['authorization'] = `Bearer ${state.accessToken}`;
resolve(axiosInstanceWithToken(originalRequest));
} else {
reject(new Error('Original request is missing'));
}
}
});
// 비동기적으로 accessToken 업데이트
setAccessToken(newAccessToken);
});
}
}
} catch (error: unknown) {
if (!axios.isAxiosError(error)) {
handleUnexpectedError(error);
return Promise.reject(error);
}
// 2. accessToken을 새로 요청했을 때 refreshToken이 만료되거나 잘못된 경우 로그아웃 처리
if (error.response?.status === 401) {
window.location.href = '/signIn';
return null;
}
}

Expand Down
21 changes: 9 additions & 12 deletions src/apis/services/auth.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import axios from 'axios';
import { axiosInstanceWithToken } from '../instances';
import { axiosInstance } from '../instances';

interface RenewTokenResponse {
applicationAccessToken: string;
}

// accessToken 만료시 token들을 재발급
export const renewTokens = async () => {
try {
const response = await axiosInstanceWithToken.post('/auth/renewTokens');
return response.data;
const response = await axiosInstance.post<RenewTokenResponse>('/auth/renewTokens');
return response.data.applicationAccessToken;
} catch (error) {
if (!axios.isAxiosError(error)) {
console.error('에러가 발생했습니다.');
return;
}
if (error.response?.status === 401) {
console.error('리프레시 토큰이 만료됐습니다. 다시 로그인해주세요.');
window.location.href = '/signIn';
}
console.error(error);
return error;
}
};
1 change: 0 additions & 1 deletion src/apis/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export const getMyUser = async (userId: string) => {
export const getMyOwnChannels = async (userId: string) => {
try {
const response = await axiosInstanceWithToken.get(`/users/myOwnChannels/${userId}`);
console.log(response.data);
return response.data;
} catch (e) {
console.error(e);
Expand Down
14 changes: 14 additions & 0 deletions src/pages/Signin/SignInPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import useSignInPage from './SignInPage.hook';
// components
import { GoogleOAuthProvider } from '@react-oauth/google';
import GoogleLoginButton from './_components/GoogleLoginButton';
import { useApplicationAuthTokenStore, useGoogleOAuthTokenStore } from '@/store/useAuthStore';
import { useUserStore } from '@/store/useUserStore';

export default function SignInPage() {
// logics
const { CLIENT_ID } = useSignInPage();
const accessToken = useApplicationAuthTokenStore.getState();
const googleOAuthToken = useGoogleOAuthTokenStore.getState();
const user = useUserStore.getState();

// view
if (!CLIENT_ID) {
Expand All @@ -21,6 +26,15 @@ export default function SignInPage() {
<BackGround>
<Container>
<Header>간단하게 로그인 또는 회원가입하세요</Header>
<button
onClick={() => {
console.log(accessToken);
console.log(googleOAuthToken);
console.log(user);
}}
>
accessToken 확인
</button>
<GoogleOAuthProvider clientId={CLIENT_ID}>
<GoogleLoginButton />
</GoogleOAuthProvider>
Expand Down
31 changes: 15 additions & 16 deletions src/pages/Signin/_components/GoogleLoginButton.hook.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,27 @@
// libraries
import { axiosInstanceWithToken } from '../../../apis/instances';
import { axiosInstance } from '../../../apis/instances';

// hooks
import { useNavigate } from 'react-router-dom';
import { useGoogleLogin } from '@react-oauth/google';
import { useApplicationAuthTokenStore, useGoogleOAuthTokenStore } from '../../../store/useAuthStore';
import { useUserStore } from '../../../store/useUserStore';
import { useEffect } from 'react';

export default function useGoogleLoginButton() {
const navigate = useNavigate();
const { setAccessToken } = useApplicationAuthTokenStore();
const { setGoogleOAuthToken } = useGoogleOAuthTokenStore();
const { setUser } = useUserStore();
const { accessToken, setAccessToken } = useApplicationAuthTokenStore.getState();
const { googleOAuthToken, setGoogleOAuthToken } = useGoogleOAuthTokenStore.getState();
const { user, setUser } = useUserStore();
const login = useGoogleLogin({
scope: 'email profile',
onSuccess: async ({ code }) => {
try {
await axiosInstanceWithToken.post('/auth/google/callback', { code }).then((response) => {
setAccessToken(response.data.applicationToken.accessToken);
setGoogleOAuthToken(response.data.googleToken.googleAccessToken);
await axiosInstance.post('/auth/google/callback', { code }).then((response) => {
console.log(response.data);
setAccessToken(response.data.applicationAccessToken);
setGoogleOAuthToken(response.data.googleAccessToken);
setUser(response.data.user);

// refresh token의 경우 백엔드에서 cookie로 보내주기 때문에 따로 저장할 필요가 없다.
if (
response.data.user &&
response.data.applicationToken.accessToken &&
response.data.googleToken.googleAccessToken
) {
navigate('/main');
}
});
} catch (error) {
console.error(error);
Expand All @@ -40,5 +33,11 @@ export default function useGoogleLoginButton() {
flow: 'auth-code',
});

useEffect(() => {
if (accessToken && googleOAuthToken && user) {
navigate('/main');
}
}, [accessToken, googleOAuthToken, user]);

return { login };
}
39 changes: 28 additions & 11 deletions src/store/useAuthStore.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface ApplicationAuthTokenStore {
accessToken: string | null;
setAccessToken: (accessToken: string) => void;
setAccessToken: (accessToken: string | undefined) => void;
removeAccessToken: () => void;
}

Expand All @@ -12,14 +13,30 @@ interface GoogleOAuthTokenStore {
removeGoogleOAuthToken: () => void;
}

export const useApplicationAuthTokenStore = create<ApplicationAuthTokenStore>((set) => ({
accessToken: null,
setAccessToken: (accessToken: string) => set({ accessToken }),
removeAccessToken: () => set({ accessToken: null }),
}));
export const useApplicationAuthTokenStore = create(
persist<ApplicationAuthTokenStore>(
(set) => ({
accessToken: null,
setAccessToken: (accessToken: string | undefined) => set({ accessToken }),
removeAccessToken: () => set({ accessToken: null }),
}),
{
name: 'applicationAuthTokenStore',
getStorage: () => localStorage,
},
),
);

export const useGoogleOAuthTokenStore = create<GoogleOAuthTokenStore>((set) => ({
googleOAuthToken: null,
setGoogleOAuthToken: (googleOAuthToken: string) => set({ googleOAuthToken }),
removeGoogleOAuthToken: () => set({ googleOAuthToken: null }),
}));
export const useGoogleOAuthTokenStore = create(
persist<GoogleOAuthTokenStore>(
(set) => ({
googleOAuthToken: null,
setGoogleOAuthToken: (googleOAuthToken: string) => set({ googleOAuthToken }),
removeGoogleOAuthToken: () => set({ googleOAuthToken: null }),
}),
{
name: 'googleOAuthTokenStore',
getStorage: () => localStorage,
},
),
);
1 change: 1 addition & 0 deletions src/utils/getAccessToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useApplicationAuthTokenStore } from '../store/useAuthStore';

const getAccessToken = () => {
const { accessToken } = useApplicationAuthTokenStore.getState();
console.log(accessToken);
return accessToken;
};

Expand Down

0 comments on commit 0be3b98

Please sign in to comment.