Skip to content
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

[FE] Access 토큰 쿠키로 변경(#808) #809

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions frontend/src/@types/apiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

export interface QueueItem {
resolve: (value: string | PromiseLike<string>) => void;
reject: (reason?: Error) => void;
}
57 changes: 25 additions & 32 deletions frontend/src/apis/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
import { API_ENDPOINTS } from "./endpoints";
import { Method, QueueItem } from "@/@types/apiClient";
import { serverUrl } from "@/config/serverUrl";
import MESSAGES from "@/constants/message";
import { AuthorizationError, HTTPError } from "@/utils/Errors";

type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

interface ApiProps {
export interface ApiProps {
endpoint: string;
headers?: Record<string, string>;
body?: object | null;
errorMessage?: string;
}

interface RequestProps extends ApiProps {
export interface RequestProps extends ApiProps {
method: Method;
}

interface QueueItem {
resolve: (value: string | PromiseLike<string>) => void;
reject: (reason?: Error) => void;
}

let isRefreshing = false;
let failedQueue: QueueItem[] = [];

Expand All @@ -37,14 +31,12 @@ const processQueue = (error: Error | null = null, token: string | null = null) =
};

const refreshAccessToken = async (): Promise<string | undefined> => {
const refreshToken = localStorage.getItem("refreshToken");

const response = await fetch(`${serverUrl}${API_ENDPOINTS.REFRESH}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ refreshToken }),
credentials: "include",
});

const text = await response.text();
Expand All @@ -53,7 +45,7 @@ const refreshAccessToken = async (): Promise<string | undefined> => {
const newAccessToken = response.headers.get("Authorization");

if (!response.ok) {
if (response.status === 401) {
if (response.status === 401 && data.exceptionType === "TOKEN_EXPIRED") {
const error = new AuthorizationError(data.message || MESSAGES.ERROR.POST_REFRESH);
processQueue(error, null);
isRefreshing = false;
Expand All @@ -72,24 +64,6 @@ const refreshAccessToken = async (): Promise<string | undefined> => {
}
};

const createRequestInit = (
method: Method,
headers: Record<string, string>,
body: object | null,
): RequestInit => {
const token = localStorage.getItem("accessToken");

return {
method,
headers: {
...headers,
Authorization: token ? `Bearer ${token}` : "",
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : null,
};
};

const fetchWithToken = async (
endpoint: string,
requestInit: RequestInit,
Expand All @@ -103,7 +77,7 @@ const fetchWithToken = async (
let text = await response.text();
let data = text ? JSON.parse(text) : null;

if (response.status === 401 && data.message === "토큰이 만료되었습니다.") {
if (response.status === 401 && data.exceptionType === "TOKEN_EXPIRED") {
if (isRefreshing) {
new Promise<string>((resolve, reject) => {
failedQueue.push({ resolve, reject });
Expand Down Expand Up @@ -145,6 +119,25 @@ const fetchWithToken = async (
return text ? data : response;
};

const createRequestInit = (
method: Method,
headers: Record<string, string>,
body: object | null,
): RequestInit => {
const token = localStorage.getItem("accessToken");

return {
method,
headers: {
...headers,
Authorization: token ? `Bearer ${token}` : "",
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : null,
credentials: "include",
};
};

const apiClient = {
get: ({ endpoint, headers = {}, errorMessage = "" }: ApiProps) =>
apiClient.request({ method: "GET", endpoint, headers, errorMessage }),
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/apis/auth.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const postLogin = async (code: string): Promise<UserInfoResponse> => {
"Content-Type": "application/json",
},
body: JSON.stringify({ code }),
credentials: "include",
});

if (!response.ok) {
Expand All @@ -23,15 +24,14 @@ export const postLogin = async (code: string): Promise<UserInfoResponse> => {
const accessToken = response.headers.get("Authorization");

const authBody = text ? JSON.parse(text) : response;
const refreshToken = authBody.refreshToken;
const userInfo = authBody.userInfo as UserInfo;
const memberRole = authBody.memberRole as string;

if (!accessToken) {
throw new Error(MESSAGES.ERROR.POST_LOGIN);
}

return { accessToken, refreshToken, userInfo, memberRole };
return { accessToken, userInfo, memberRole };
};

export const postLogout = async (): Promise<void> => {
Expand Down
Binary file added frontend/src/assets/etc/blank-profile.webp
Binary file not shown.
1 change: 1 addition & 0 deletions frontend/src/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { default as happyCharacter } from "@/assets/character/happy-character.we
export { default as thinkingCharacter } from "@/assets/character/thinking-character.webp";

export { default as spinner } from "@/assets/etc/spinner.svg";
export { default as blankProfile } from "@/assets/etc/blank-profile.webp";

export { default as github_pr } from "@/assets/guide/GitHub PR 화면.webp";
export { default as github_suggestion } from "@/assets/guide/GitHub Suggestion 기능.webp";
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/components/common/img/ImageWithFallback.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from "react";
import * as S from "@/components/common/img/ImageWithFallback.style";
import { errorCharacter } from "@/assets";

Expand All @@ -9,18 +8,17 @@ interface ImageWithFallbackProps {
}

const ImageWithFallback = ({
src,
src = "",
alt,
fallbackSrc = errorCharacter,
...props
}: ImageWithFallbackProps) => {
const [isFallback, setIsFallback] = useState(!src);
const isFallback = !src;

const handleError = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {
const img = e.target as HTMLImageElement;
img.onerror = null;
img.src = fallbackSrc;
setIsFallback(true);
};

return (
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/common/profile/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import ImageWithFallback from "../img/ImageWithFallback";
import { ButtonHTMLAttributes } from "react";
import * as S from "@/components/common/profile/Profile.style";
import { blankProfile } from "@/assets";

interface ProfileProps extends ButtonHTMLAttributes<HTMLButtonElement> {
imgSrc: string;
Expand All @@ -9,7 +11,13 @@ interface ProfileProps extends ButtonHTMLAttributes<HTMLButtonElement> {
const Profile = ({ imgSrc, size, ...rest }: ProfileProps) => {
return (
<S.ProfileContainer $size={size} {...rest}>
<S.ProfileImg src={imgSrc} $size={size} alt="유저 프로필 이미지" />
<S.ProfileImg
as={ImageWithFallback}
src={imgSrc}
$size={size}
alt="유저 프로필 이미지"
fallbackSrc={blankProfile}
/>
</S.ProfileContainer>
);
};
Expand Down
21 changes: 11 additions & 10 deletions frontend/src/components/shared/roomCard/RoomCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import RoomCard from "./RoomCard";
import type { Meta, StoryObj } from "@storybook/react";
import { RoomInfo } from "@/@types/roomInfo";
import {
Classification,
MemberRole,
ParticipationStatus,
RoomInfo,
RoomStatus,
} from "@/@types/roomInfo";
import roomInfo from "@/mocks/mockResponse/roomInfo.json";

const sampleRoomList = {
...roomInfo,
roomStatus: roomInfo.roomStatus as "OPEN" | "CLOSE" | "PROGRESS" | "FAIL",
participationStatus: roomInfo.participationStatus as
| "NOT_PARTICIPATED"
| "PARTICIPATED"
| "MANAGER"
| "PULL_REQUEST_NOT_SUBMITTED",
memberRole: roomInfo.memberRole as "BOTH" | "REVIEWER" | "REVIEWEE" | "NONE",
classification: roomInfo.classification as "ALL" | "FRONTEND" | "BACKEND" | "ANDROID",
isPublic: false,
roomStatus: roomInfo.roomStatus as RoomStatus,
participationStatus: roomInfo.participationStatus as ParticipationStatus,
memberRole: roomInfo.memberRole as MemberRole,
classification: roomInfo.classification as Classification,
} satisfies RoomInfo;

const meta = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { Meta, StoryObj } from "@storybook/react";
import RoomCardModal from "@/components/shared/roomCardModal/RoomCardModal";
import { RoomInfo } from "@/@types/roomInfo";
import {
Classification,
MemberRole,
ParticipationStatus,
RoomInfo,
RoomStatus,
} from "@/@types/roomInfo";
import roomInfo from "@/mocks/mockResponse/roomInfo.json";

const sampleRoomList = {
...roomInfo,
roomStatus: roomInfo.roomStatus as "OPEN" | "CLOSE" | "PROGRESS" | "FAIL",
participationStatus: roomInfo.participationStatus as
| "NOT_PARTICIPATED"
| "PARTICIPATED"
| "MANAGER"
| "PULL_REQUEST_NOT_SUBMITTED",
memberRole: roomInfo.memberRole as "BOTH" | "REVIEWER" | "REVIEWEE" | "NONE",
classification: roomInfo.classification as "ALL" | "FRONTEND" | "BACKEND" | "ANDROID",
isPublic: false,
roomStatus: roomInfo.roomStatus as RoomStatus,
participationStatus: roomInfo.participationStatus as ParticipationStatus,
memberRole: roomInfo.memberRole as MemberRole,
classification: roomInfo.classification as Classification,
} satisfies RoomInfo;

const meta: Meta<typeof RoomCardModal> = {
Expand Down
22 changes: 11 additions & 11 deletions frontend/src/components/shared/roomList/RoomList.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import RoomList from "./RoomList";
import type { Meta, StoryObj } from "@storybook/react";
import { RoomInfo } from "@/@types/roomInfo";
import {
Classification,
MemberRole,
ParticipationStatus,
RoomInfo,
RoomStatus,
} from "@/@types/roomInfo";
import roomInfos from "@/mocks/mockResponse/roomInfos.json";

const sampleRoomList = roomInfos.rooms.map((roomInfo) => ({
...roomInfo,
roomStatus: roomInfo.roomStatus as "OPEN" | "CLOSE" | "PROGRESS" | "FAIL",
participationStatus: roomInfo.participationStatus as
| "NOT_PARTICIPATED"
| "PARTICIPATED"
| "MANAGER"
| "PULL_REQUEST_NOT_SUBMITTED",

memberRole: roomInfo.memberRole as "BOTH" | "REVIEWER" | "REVIEWEE" | "NONE",
classification: roomInfo.classification as "ALL" | "FRONTEND" | "BACKEND" | "ANDROID",
isPublic: false,
roomStatus: roomInfo.roomStatus as RoomStatus,
participationStatus: roomInfo.participationStatus as ParticipationStatus,
memberRole: roomInfo.memberRole as MemberRole,
classification: roomInfo.classification as Classification,
})) satisfies RoomInfo[];

const meta = {
Expand Down
12 changes: 2 additions & 10 deletions frontend/src/hooks/mutations/useMutateAuth.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import useMutateHandlers from "./useMutateHandlers";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useMutation } from "@tanstack/react-query";
import { UserInfo } from "@/@types/userInfo";
import { postLogin, postLogout } from "@/apis/auth.api";
import QUERY_KEYS from "@/apis/queryKeys";

export interface UserInfoResponse {
accessToken: string;
refreshToken: string;
userInfo: UserInfo;
memberRole: string;
}

const useMutateAuth = () => {
const { handleMutateError } = useMutateHandlers();
const queryClient = useQueryClient();

const postLoginMutation = useMutation({
mutationFn: (code: string) => postLogin(code),
onSuccess: ({ accessToken, refreshToken, userInfo, memberRole }: UserInfoResponse) => {
onSuccess: ({ accessToken, userInfo, memberRole }: UserInfoResponse) => {
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
localStorage.setItem("userInfo", JSON.stringify(userInfo));
localStorage.setItem("memberRole", memberRole);
},
Expand All @@ -32,10 +28,6 @@ const useMutateAuth = () => {
const postLogoutMutation = useMutation({
mutationFn: () => postLogout(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.PARTICIPATED_ROOM_LIST] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.PROGRESS_ROOM_LIST] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.OPENED_ROOM_LIST] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.CLOSED_ROOM_LIST] });
localStorage.clear();
window.location.replace("/");
},
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/mocks/mockResponse/roomInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
"isClosed": false,
"memberRole": "BOTH",
"classification": "BACKEND",
"message": "방 실패했습니다."
"message": "방 실패했습니다.",
"isPublic": false
}
15 changes: 10 additions & 5 deletions frontend/src/mocks/mockResponse/roomInfos.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"participationStatus": "PARTICIPATED",
"memberRole": "BOTH",
"classification": "ALL",
"message": "방 실패했습니다."
"message": "방 실패했습니다.",
"isPublic": false
},
{
"id": 2,
Expand All @@ -38,7 +39,8 @@
"participationStatus": "PARTICIPATED",
"memberRole": "BOTH",
"classification": "ANDROID",
"message": "방 실패했습니다."
"message": "방 실패했습니다.",
"isPublic": false
},
{
"id": 2,
Expand All @@ -58,7 +60,8 @@
"participationStatus": "PARTICIPATED",
"memberRole": "REVIEWER",
"classification": "FRONTEND",
"message": "방 실패했습니다."
"message": "방 실패했습니다.",
"isPublic": false
},
{
"id": 1,
Expand All @@ -78,7 +81,8 @@
"participationStatus": "NOT_PARTICIPATED",
"memberRole": "BOTH",
"classification": "BACKEND",
"message": "방 실패했습니다."
"message": "방 실패했습니다.",
"isPublic": false
},
{
"id": 2,
Expand All @@ -98,7 +102,8 @@
"participationStatus": "PARTICIPATED",
"memberRole": "BOTH",
"classification": "BACKEND",
"message": "방 실패했습니다."
"message": "방 실패했습니다.",
"isPublic": false
}
],
"isLastPage": false,
Expand Down
Loading