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