Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"check": "biome check"
},
"dependencies": {
"@solid-connect/api-client": "workspace:*",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
Expand Down
16 changes: 1 addition & 15 deletions apps/admin/src/lib/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1 @@
import type { AxiosResponse } from "axios";
import { publicAxiosInstance } from "@/lib/api/client";
import type { AdminSignInResponse, ReissueAccessTokenResponse } from "@/types/auth";

export const adminSignInApi = (email: string, password: string): Promise<AxiosResponse<AdminSignInResponse>> =>
publicAxiosInstance.post("/auth/email/sign-in", { email, password });

export const reissueAccessTokenApi = (refreshToken: string): Promise<AxiosResponse<ReissueAccessTokenResponse>> =>
publicAxiosInstance.post(
"/admin/auth/reissue",
{},
{
headers: { Authorization: `Bearer ${refreshToken}` },
},
);
export { adminSignInApi, reissueAccessTokenApi } from "@solid-connect/api-client/generated/admin";
97 changes: 22 additions & 75 deletions apps/admin/src/lib/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import axios, { type AxiosInstance } from "axios";
import {
axiosInstance,
configureApiClientRuntime,
publicAxiosInstance,
type TokenStorageAdapter,
} from "@solid-connect/api-client/runtime";
import { reissueAccessTokenApi } from "@/lib/api/auth";
import { isTokenExpired } from "@/lib/utils/jwtUtils";
import {
Expand All @@ -9,86 +14,28 @@ import {
saveAccessToken,
} from "@/lib/utils/localStorage";

const convertToBearer = (token: string) => `Bearer ${token}`;

const API_SERVER_URL = import.meta.env.VITE_API_SERVER_URL?.trim();

if (!API_SERVER_URL) {
throw new Error("[admin] VITE_API_SERVER_URL is required. Configure it in your environment.");
}

export const axiosInstance: AxiosInstance = axios.create({
baseURL: API_SERVER_URL,
withCredentials: true,
});

axiosInstance.interceptors.request.use(
async (config) => {
const newConfig = { ...config };
let accessToken: string | null = loadAccessToken();

if (accessToken === null || isTokenExpired(accessToken)) {
const refreshToken = loadRefreshToken();
if (refreshToken === null || isTokenExpired(refreshToken)) {
removeAccessToken();
removeRefreshToken();
return config;
}

await reissueAccessTokenApi(refreshToken)
.then((res) => {
accessToken = res.data.accessToken;
saveAccessToken(accessToken);
})
.catch((err) => {
removeAccessToken();
removeRefreshToken();
console.error("인증 토큰 갱신중 오류가 발생했습니다", err);
});
}

if (accessToken !== null) {
newConfig.headers.Authorization = convertToBearer(accessToken);
}
return newConfig;
},
(error) => Promise.reject(error),
);

axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const newError = { ...error };
if (error.response?.status === 401 || error.response?.status === 403) {
const refreshToken = loadRefreshToken();

if (refreshToken === null || isTokenExpired(refreshToken)) {
removeAccessToken();
removeRefreshToken();
throw newError;
}

try {
const newAccessToken = await reissueAccessTokenApi(refreshToken).then((res) => res.data.accessToken);
saveAccessToken(newAccessToken);

if (error?.config.headers === undefined) {
newError.config.headers = {};
}
newError.config.headers.Authorization = convertToBearer(newAccessToken);

return await axios.request(newError.config);
} catch (_err) {
removeAccessToken();
removeRefreshToken();
throw Error("로그인이 필요합니다");
}
} else {
throw newError;
}
},
);
const tokenStorage: TokenStorageAdapter = {
loadAccessToken,
loadRefreshToken,
saveAccessToken,
removeAccessToken,
removeRefreshToken,
};

export const publicAxiosInstance: AxiosInstance = axios.create({
configureApiClientRuntime({
baseURL: API_SERVER_URL,
tokenStorage,
isTokenExpired,
reissueAccessToken: async (refreshToken: string) => {
const response = await reissueAccessTokenApi(refreshToken);
return response.data.accessToken;
},
});

export { axiosInstance, publicAxiosInstance };
46 changes: 1 addition & 45 deletions apps/admin/src/lib/api/scores.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1 @@
import { axiosInstance } from "@/lib/api/client";
import type {
GpaScoreUpdateRequest,
GpaScoreWithUser,
LanguageScoreWithUser,
LanguageTestScoreUpdateRequest,
LanguageTestType,
PageResponse,
ScoreSearchCondition,
VerifyStatus,
} from "@/types/scores";

export const scoreApi = {
// GPA 성적 조회
getGpaScores: (condition: ScoreSearchCondition, page: number): Promise<PageResponse<GpaScoreWithUser>> =>
axiosInstance.get("/admin/scores/gpas", { params: { ...condition, page } }).then((res) => res.data),

// GPA 성적 수정
updateGpaScore: (id: number, status: VerifyStatus, reason?: string, score?: GpaScoreWithUser) => {
if (!score) throw new Error("Score data is required");
const request: GpaScoreUpdateRequest = {
gpa: score.gpaScoreStatusResponse.gpaResponse.gpa,
gpaCriteria: score.gpaScoreStatusResponse.gpaResponse.gpaCriteria,
verifyStatus: status,
rejectedReason: reason,
};
return axiosInstance.put(`/admin/scores/gpas/${id}`, request);
},

// 어학성적 조회
getLanguageScores: (condition: ScoreSearchCondition, page: number): Promise<PageResponse<LanguageScoreWithUser>> =>
axiosInstance.get("/admin/scores/language-tests", { params: { ...condition, page } }).then((res) => res.data),

// 어학성적 수정
updateLanguageScore: (id: number, status: VerifyStatus, reason?: string, score?: LanguageScoreWithUser) => {
if (!score) throw new Error("Score data is required");
const request: LanguageTestScoreUpdateRequest = {
languageTestType: score.languageTestScoreStatusResponse.languageTestResponse.languageTestType as LanguageTestType,
languageTestScore: score.languageTestScoreStatusResponse.languageTestResponse.languageTestScore,
verifyStatus: status,
rejectedReason: reason,
};
return axiosInstance.put(`/admin/scores/language-tests/${id}`, request);
},
};
export { scoreApi } from "@solid-connect/api-client/generated/admin";
24 changes: 23 additions & 1 deletion apps/admin/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,34 @@
import { TanStackDevtools } from "@tanstack/react-devtools";
import { createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
import { createRootRoute, HeadContent, redirect, Scripts } from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { Toaster } from "sonner";
import { QueryProvider } from "@/components/providers/QueryProvider";
import { isTokenExpired } from "@/lib/utils/jwtUtils";
import { loadAccessToken } from "@/lib/utils/localStorage";

import appCss from "../styles.css?url";

const PUBLIC_PATHS = new Set(["/auth/login", "/login"]);

export const Route = createRootRoute({
beforeLoad: ({ location }) => {
if (typeof window === "undefined") {
return;
}

const pathname = location.pathname;
const isPublicPath = PUBLIC_PATHS.has(pathname);
const accessToken = loadAccessToken();
const isAuthenticated = accessToken !== null && !isTokenExpired(accessToken);

if (!isAuthenticated && !isPublicPath) {
throw redirect({ to: "/auth/login" });
}

if (isAuthenticated && isPublicPath) {
throw redirect({ to: "/scores" });
}
},
head: () => ({
meta: [
{
Expand Down
Loading