feat: 클라이언트 IP 가드 로직 작성
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const ipAddress =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0] || "unknown";
|
||||
|
||||
const ipv4Regex = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/;
|
||||
const match = ipAddress.match(ipv4Regex);
|
||||
|
||||
const normalizedIp = match ? match[1] : ipAddress;
|
||||
|
||||
return NextResponse.json({
|
||||
originalIp: ipAddress,
|
||||
normalizedIp: normalizedIp,
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
||||
import { ClientIpGuard } from "@/entities/ip";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
import { PromptProvider, StoreProvider } from "@/shared/providers";
|
||||
@@ -16,28 +17,30 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const queryClient = new QueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StoreProvider>
|
||||
<PromptProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>
|
||||
<DndProvider
|
||||
backend={MultiBackend}
|
||||
options={getBackendOptions()}
|
||||
>
|
||||
{children}
|
||||
</DndProvider>
|
||||
</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</PromptProvider>
|
||||
</StoreProvider>
|
||||
<div className="text-2xl">
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
<ClientIpGuard>
|
||||
<StoreProvider>
|
||||
<PromptProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>
|
||||
<DndProvider
|
||||
backend={MultiBackend}
|
||||
options={getBackendOptions()}
|
||||
>
|
||||
{children}
|
||||
</DndProvider>
|
||||
</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</PromptProvider>
|
||||
</StoreProvider>
|
||||
<div className="text-2xl">
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</ClientIpGuard>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { IpParams, IP, IpList, IpService } from "../model/ip-interface";
|
||||
import type { IpService, IpParams, IpList, IP } from "../model/ip-interface";
|
||||
|
||||
export class IpApiService implements IpService {
|
||||
public async getIpList(params: IpParams): Promise<IpList> {
|
||||
@@ -0,0 +1,10 @@
|
||||
export { IpApi } from "./api/ip-service";
|
||||
export {
|
||||
ipFormSchema,
|
||||
type IpService,
|
||||
type IPFormData,
|
||||
type IpParams,
|
||||
type IpList,
|
||||
type IP,
|
||||
} from "./model/ip-interface";
|
||||
export { ClientIpGuard } from "./lib/client-ip-guard";
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { SiteApi } from "@/entities/site/@x/ip";
|
||||
import { ENDPOINT, SITE_ID } from "@/shared/config";
|
||||
import { IpApi } from "../api/ip-service";
|
||||
|
||||
type ClientIpResponse = {
|
||||
originalIp: string;
|
||||
normalizedIp: string;
|
||||
};
|
||||
|
||||
const ClientIpGuard = ({ children }: { children: React.ReactNode }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [isAllowed, setIsAllowed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await fetch("/api/getClientIp");
|
||||
const { normalizedIp } = (await response.json()) as ClientIpResponse;
|
||||
|
||||
if (!normalizedIp || !SITE_ID) return;
|
||||
|
||||
const siteInfo = await queryClient.ensureQueryData({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, SITE_ID],
|
||||
queryFn: () => SiteApi.siteDetail(SITE_ID),
|
||||
});
|
||||
|
||||
if (siteInfo.siteKndCd === "O") return;
|
||||
|
||||
const { content: ipList } = await IpApi.getIpList({
|
||||
siteId: SITE_ID,
|
||||
prmYn: "Y",
|
||||
});
|
||||
|
||||
const allowedClientIp = ipList.some((ip) => ip.ipAddr === normalizedIp);
|
||||
setIsAllowed(allowedClientIp);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (!isAllowed) {
|
||||
alert("허용되지 않은 IP로 접근하였습니다.");
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { ClientIpGuard };
|
||||
@@ -0,0 +1 @@
|
||||
export { SiteApi } from "../api/site-service";
|
||||
@@ -24,11 +24,10 @@ import {
|
||||
Checkbox,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
import { IpApi, ipFormSchema, type IP, type IPFormData } from "@/entities/ip";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { IpApi } from "../api/ip-service";
|
||||
import { ipFormSchema, type IP, type IPFormData } from "../model/ip-interface";
|
||||
|
||||
type IPManagementDialogProps = {
|
||||
siteId: string;
|
||||
|
||||
Reference in New Issue
Block a user