iipbs_cs #2

Merged
chanii8 merged 4 commits from iipbs_cs into main 2025-03-11 16:21:49 +09:00
18 changed files with 372 additions and 49 deletions
Showing only changes of commit 58a0c5f1de - Show all commits
+81
View File
@@ -0,0 +1,81 @@
import {
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
REFRESH_TOKEN,
SERVER_API_URL,
SITE_ID,
} from "@/shared/config/env";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function POST(
request: Request,
props: { params: Promise<{ url: string[] }> },
) {
const params = await props.params;
const headers: Record<string, string> = {
"Content-Type": "application/json",
"X-Site-Id": SITE_ID ?? "",
};
const cookieStore = await cookies();
// silent refresh
if (params.url.includes("refresh")) {
const refreshToken = cookieStore.get(REFRESH_TOKEN);
headers[CLAIM_NAME] = refreshToken?.value ?? "";
if (!headers[CLAIM_NAME]) {
return NextResponse.json({ message: "Can't refresh" }, { status: 401 });
}
}
try {
const body = await request.json();
const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, {
method: "POST",
headers,
body: JSON.stringify(body),
});
if (!result.ok) {
return NextResponse.json(
{ message: "Invalid Credentials" },
{ status: 401 },
);
}
const accessToken = result.headers.get(ACCESS_TOKEN);
const refreshToken = result.headers.get(REFRESH_TOKEN);
const userId = result.headers.get(AUTH_USER_ID);
if (accessToken && userId) {
const response = NextResponse.json({
[ACCESS_TOKEN]: accessToken,
[AUTH_USER_ID]: userId,
});
if (refreshToken) {
cookieStore.set(REFRESH_TOKEN, refreshToken, {
httpOnly: true,
sameSite: "lax",
path: "/",
});
}
return response;
}
return NextResponse.json(
{ message: "Invalid Credentials" },
{ status: 401 },
);
} catch (error) {
return NextResponse.json(
{ message: "Server Error", error },
{ status: 500 },
);
}
}
+31 -1
View File
@@ -1,19 +1,49 @@
import type { NextConfig } from "next";
import webpack from "webpack";
const serverApiUrl =
process.env.NEXT_PUBLIC_SERVER_API_URL || "http://localhost:3000";
const nextConfig: NextConfig = {
webpack: (config) => {
config.plugins.push(
new webpack.DefinePlugin({
"process.env.SERVER_API_URL": JSON.stringify(
"import.meta.env.VITE_PORT": JSON.stringify(
process.env.NEXT_PUBLIC_PORT,
),
"import.meta.env.VITE_PROXY_HOST": JSON.stringify(
process.env.NEXT_PUBLIC_PROXY_HOST,
),
"import.meta.env.VITE_SERVER_API_URL": JSON.stringify(
process.env.NEXT_PUBLIC_SERVER_API_URL,
),
"import.meta.env.VITE_SITE_ID": JSON.stringify(process.env.SITE_ID),
"import.meta.env.VITE_CLAIM_NAME": JSON.stringify(
process.env.NEXT_PUBLIC_CLAIM_NAME,
),
"import.meta.env.VITE_ACCESS_TOKEN": JSON.stringify(
process.env.NEXT_PUBLIC_ACCESS_TOKEN,
),
"import.meta.env.VITE_REFRESH_TOKEN": JSON.stringify(
process.env.NEXT_PUBLIC_REFRESH_TOKEN,
),
"import.meta.env.VITE_AUTH_USER_ID": JSON.stringify(
process.env.NEXT_PUBLIC_AUTH_USER_ID,
),
"process.env.SITE_ID": JSON.stringify(process.env.NEXT_PUBLIC_SITE_ID),
}),
);
return config;
},
async rewrites() {
return [
{
source: "/server/:path*",
destination: `${serverApiUrl}/:path*`,
},
];
},
};
export default nextConfig;
+1
View File
@@ -11,6 +11,7 @@
"dependencies": {
"@common/assets": "workspace:*",
"@common/components": "workspace:*",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
+6 -1
View File
@@ -1,10 +1,15 @@
import "./styles/base.css";
import "./styles/components.css";
import { CookieProvider, QueryProvider } from "@/shared/providers";
const App = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
<html lang="ko">
<body>{children}</body>
<body>
<QueryProvider>
<CookieProvider>{children}</CookieProvider>
</QueryProvider>
</body>
</html>
);
};
@@ -0,0 +1,22 @@
import { client } from "@/shared/api";
import { getToken } from "@/shared/lib/cookie";
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
import type { IUser, IUserInfo, UserService } from "../model/user";
export class UsersApiService implements UserService {
public async userInfo(payload: IUserInfo): Promise<IUser> {
const response = await client.get<IUser>(
`${ENDPOINT.USER_SERVICE.USERS}/${payload.userUniqId}`,
{
headers: {
[CLAIM_NAME]: `${getToken(CLAIM_NAME)}`,
[AUTH_USER_ID]: `${getToken(AUTH_USER_ID)}`,
},
},
);
return response.data;
}
}
export const UserApi = new UsersApiService();
+1
View File
@@ -0,0 +1 @@
export { useUser } from "./lib/use-user";
+19
View File
@@ -0,0 +1,19 @@
import { useQuery } from "@tanstack/react-query";
import { UserApi } from "../api/user-service";
import { useCookie } from "@/shared/providers";
import { AUTH_USER_ID, ENDPOINT } from "@/shared/config";
const useUser = () => {
const { getToken } = useCookie();
const { data } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS],
queryFn: () =>
UserApi.userInfo({
userUniqId: getToken(AUTH_USER_ID) ?? "",
}),
});
return { userInfo: data };
};
export { useUser };
+41
View File
@@ -0,0 +1,41 @@
export interface IUserInfo {
userUniqId: string;
}
export interface IUser {
regDt: string;
mdfcnDt: string;
rgtr: string;
mdfr: string;
userUnqId: string;
userId: string;
userName: string;
email: string;
encryptedPassword: string;
refreshToken: string;
userStateCode: string;
lastLoginDate: string;
loginFailCount: number;
pswdHnt: string;
pswdAns: string;
mphn: string;
telno: string;
zip: string;
addr: string;
daddr: string;
laeSeCd: string;
ogdpSeCd: string;
aprvYn: string;
autzrId: string;
expryYmd: string;
lastPswdChgDt: string;
googleId: string;
kakaoId: string;
naverId: string;
isSocialUser: boolean;
hasPassword: boolean;
}
export interface UserService {
userInfo: (payload: IUserInfo) => Promise<IUser>;
}
+23 -25
View File
@@ -1,12 +1,10 @@
import { AxiosError } from "axios";
import { client } from "@/shared/api";
import {
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
ENDPOINT,
JWT_EXPIRATION_TIME,
REFRESH_TOKEN,
TOKEN_ID,
} from "@/shared/config";
import { tokenManager } from "../lib/token-manager";
import type { LoginService, ILogin } from "../model/login";
@@ -25,22 +23,24 @@ export class LoginApiService implements LoginService {
public async login(payload: ILogin): Promise<void> {
try {
const response = await client.post(ENDPOINT.USER_SERVICE.LOGIN, {
userId: payload.userId,
password: payload.password,
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
method: "POST",
body: JSON.stringify({
userId: payload.userId,
password: payload.password,
}),
});
const accessToken = response.headers[ACCESS_TOKEN];
const refreshToken = response.headers[REFRESH_TOKEN];
const tokenId = response.headers[TOKEN_ID];
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!refreshToken || !accessToken || !tokenId) {
if (!accessToken || !tokenId) {
throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(ACCESS_TOKEN, accessToken);
tokenManager.setToken(TOKEN_ID, tokenId);
tokenManager.setRefreshToken(refreshToken);
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
tokenManager.setupRefreshTimer(
() => this.silentRefresh(),
JWT_EXPIRATION_TIME,
@@ -57,25 +57,23 @@ export class LoginApiService implements LoginService {
public async silentRefresh(): Promise<void> {
try {
const refreshToken = tokenManager.getRefreshToken();
if (!refreshToken) {
throw new Error("No refresh token available");
}
const response = await fetch(
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
{ method: "PUT" },
);
tokenManager.setToken(CLAIM_NAME, refreshToken);
const response = await client.put(ENDPOINT.USER_SERVICE.SILENT_REFRESH);
const accessToken = response.headers[ACCESS_TOKEN];
const tokenId = response.headers[TOKEN_ID];
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(ACCESS_TOKEN, accessToken);
tokenManager.setToken(TOKEN_ID, tokenId);
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
} catch (error) {
console.warn("Token has expired, logging out.");
if (error instanceof AxiosError) {
throw new Error(
`Silent refresh failed: ${error.response?.data?.message || error.message}`,
@@ -1,6 +1,5 @@
import axios from "axios";
import Cookies from "js-cookie";
import { CLAIM_NAME, REFRESH_TOKEN, TOKEN_ID } from "@/shared/config";
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
import { setToken, removeToken } from "@/shared/lib/cookie";
export class TokenManager {
private static instance: TokenManager;
@@ -16,21 +15,12 @@ export class TokenManager {
}
public setToken(key: string, value: string): void {
axios.defaults.headers.common[key] = value;
}
public setRefreshToken(token: string): void {
Cookies.set(REFRESH_TOKEN, token, {
httpOnly: true,
secure: true,
setToken(key, value, {
secure: window.location.protocol === "https:",
sameSite: "lax",
});
}
public getRefreshToken(): string | undefined {
return Cookies.get(REFRESH_TOKEN);
}
public setupRefreshTimer(
callback: () => Promise<void>,
expirationTime: number,
@@ -52,9 +42,9 @@ export class TokenManager {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
Cookies.remove(REFRESH_TOKEN);
delete axios.defaults.headers.common[CLAIM_NAME];
delete axios.defaults.headers.common[TOKEN_ID];
removeToken(CLAIM_NAME);
removeToken(AUTH_USER_ID);
}
}
+3 -3
View File
@@ -1,7 +1,7 @@
import { Client } from "@common/network";
import { SERVER_API_URL, SITE_ID } from "../config";
import { BASE_URL, SITE_ID } from "../config";
export const client = Client.getInstance({
baseURL: SERVER_API_URL as string,
siteId: SITE_ID as string,
baseURL: BASE_URL,
siteId: SITE_ID,
});
+5
View File
@@ -1,6 +1,11 @@
import { PROXY_HOST } from "./env";
export const BASE_URL = `${PROXY_HOST}/server`;
export const ENDPOINT = {
USER_SERVICE: {
LOGIN: "/user-service/login",
USERS: "/user-service/api/v1/users",
SILENT_REFRESH: "/user-service/api/v1/users/token/refresh",
},
};
+6 -2
View File
@@ -1,9 +1,13 @@
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
export const PROXY_HOST =
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID;
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS";
export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
export const ACCESS_TOKEN =
process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
export const REFRESH_TOKEN =
process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
export const TOKEN_ID = process.env.NEXT_PUBLIC_TOKEN_ID || "token-id";
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
+50
View File
@@ -0,0 +1,50 @@
"use client";
import Cookies from "js-cookie";
export function getCookie(name: string): string | undefined {
if (typeof window === "undefined") {
return undefined;
}
return Cookies.get(name);
}
export function setCookie(
name: string,
value: string,
options: Cookies.CookieAttributes,
): void {
if (typeof window === "undefined") {
return;
}
Cookies.set(name, value, options);
}
export function removeCookie(
name: string,
options: Cookies.CookieAttributes,
): void {
if (typeof window === "undefined") {
return;
}
Cookies.remove(name, options);
}
export function getToken(key: string): string | undefined {
return getCookie(key);
}
export function setToken(
key: string,
token: string,
options: Cookies.CookieAttributes,
): void {
setCookie(key, token, {
...options,
path: "/",
});
}
export function removeToken(key: string): void {
removeCookie(key, { path: "/" });
}
@@ -0,0 +1,56 @@
"use client";
import {
getCookie,
setCookie,
removeCookie,
getToken,
setToken,
removeToken,
} from "../../lib/cookie";
import { createContext, useContext } from "react";
export interface CookieContextType {
getToken: (key: string) => string | undefined;
setToken: (
key: string,
token: string,
options: Cookies.CookieAttributes,
) => void;
removeToken: (key: string) => void;
getCookie: (name: string) => string | undefined;
setCookie: (
name: string,
value: string,
options: Cookies.CookieAttributes,
) => void;
removeCookie: (name: string, options: Cookies.CookieAttributes) => void;
}
export const CookieContext = createContext<CookieContextType | null>(null);
export function CookieProvider({ children }: { children: React.ReactNode }) {
const cookieContextValue: CookieContextType = {
getToken,
setToken,
removeToken,
getCookie,
setCookie,
removeCookie,
};
return (
<CookieContext.Provider value={cookieContextValue}>
{children}
</CookieContext.Provider>
);
}
export function useCookie(): CookieContextType {
const context = useContext(CookieContext);
if (!context) {
throw new Error("useCookie must be used within a CookieProvider");
}
return context;
}
+6
View File
@@ -0,0 +1,6 @@
export {
CookieContext,
CookieProvider,
useCookie,
} from "./cookie/cookie-provider";
export { QueryProvider } from "./query/query-client";
@@ -0,0 +1,11 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
+3
View File
@@ -42,6 +42,9 @@ importers:
'@common/components':
specifier: workspace:*
version: link:../../packages/common/components
'@tanstack/react-query':
specifier: ^5.67.1
version: 5.67.1(react@18.3.1)
'@tanstack/react-table':
specifier: ^8.21.2
version: 8.21.2(react-dom@18.3.1)(react@18.3.1)