Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • aolda/proxy-manager-frontend
1 result
Select Git revision
Show changes
Commits on Source (4)
......@@ -38,10 +38,8 @@ export default function ForwardingEdit() {
useEffect(() => {
authFetch(`/api/forwarding?forwardingId=${id}`)
.then((response) => {
if (!response.ok) {
console.error(response);
throw Error();
}
if (!response.ok) throw Error(`포트포워딩 정보 조회 실패: ${response.status}`);
return response.json();
})
.then(({ name, serverPort, instanceIp }) => {
......@@ -55,7 +53,7 @@ export default function ForwardingEdit() {
console.error(error);
toast.error('포트포워딩 정보를 조회할 수 없습니다.');
});
});
}, []);
async function onSubmit(values: z.infer<typeof formSchema>) {
if (!initialData.current) return;
......@@ -74,8 +72,6 @@ export default function ForwardingEdit() {
});
if (!response.ok) {
console.error(response);
const { code } = await response.json();
if (code == 'DUPLICATED_INSTANCE_INFO') {
form.setError('instanceIp', { type: 'custom' });
......
......@@ -29,15 +29,16 @@ export default function ForwardingList() {
useEffect(() => {
authFetch(`/api/forwardings?projectId=${selectedProject?.id}`)
.then((response) => {
if (!response.ok) {
toast.error('포트포워딩 정보를 조회할 수 없습니다.');
return { forwardings: [] };
}
if (!response.ok) throw Error(`포트포워딩 목록 조회 실패: ${response.status}`);
return response.json();
})
.then(({ contents }) => {
setForwardings(contents);
})
.catch((error) => {
console.error(error);
toast.error('포트포워딩 정보를 조회할 수 없습니다.');
});
}, [authFetch, selectedProject]);
......
......@@ -75,14 +75,16 @@ export default function RoutingCreate() {
if (enableSSL) {
authFetch(`/api/certificates?projectId=${selectedProject?.id}&domain=${debouncedDomain}`)
.then((response) => {
if (!response.ok) {
toast.error('SSL 인증서 목록을 불러올 수 없습니다');
return { contents: [] };
}
if (!response.ok) throw Error(`SSL 인증서 목록 조회 실패: ${response.status}`);
return response.json();
})
.then(({ contents }) => {
setCertificates(contents);
})
.catch((error) => {
console.error(error);
toast.error('SSL 인증서 목록을 불러올 수 없습니다.');
});
}
}, [authFetch, enableSSL, selectedProject, debouncedDomain]);
......@@ -103,8 +105,13 @@ export default function RoutingCreate() {
});
if (!response.ok) {
console.error(response);
toast.error('라우팅 설정을 등록할 수 없습니다');
const { code } = await response.json();
if (code == 'DUPLICATED_DOMAIN_NAME') {
form.setError('domain', { type: 'custom', message: '이미 사용중인 도메인입니다' });
toast.error('이미 사용중인 도메인입니다');
} else {
toast.error('라우팅 설정을 등록할 수 없습니다');
}
} else {
toast.success('라우팅 설정을 등록합니다');
navigate('/routing');
......
import { useEffect, useRef, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router';
import { useForm, useWatch } from 'react-hook-form';
import { Check, X } from 'lucide-react';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { Separator } from '@/components/ui/separator';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Skeleton } from '@/components/ui/skeleton';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Certificate } from '@/types/certificate';
import { useAuthStore } from '@/stores/authStore';
import useDebounce from '@/hooks/useDebounce';
const formSchema = z
.object({
name: z.string({ required_error: '서버 이름을 입력해주세요' }).min(1, { message: '서버 이름을 입력해주세요' }),
domain: z
.string({ required_error: '도메인 주소를 입력해주세요' })
.regex(/^(?=.{1,253}$)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, {
message: '올바른 도메인 주소를 입력해주세요',
}),
ip: z
.string({ required_error: '인스턴스 IP를 입력해주세요' })
.regex(/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/, {
message: '올바른 IP 주소를 입력해주세요',
})
.startsWith('10.16.', { message: '인스턴스 IP는 10.16.0.0/16 대역을 사용해야 합니다' }),
port: z.coerce
.number({ invalid_type_error: '포트 번호를 입력해주세요' })
.min(1, { message: '포트 번호를 입력해주세요' })
.max(65535, { message: '올바른 포트 번호를 입력해주세요' }),
enableSSL: z.boolean(),
certificateId: z.coerce.number().optional(),
caching: z.boolean(),
})
.refine((data) => !data.enableSSL || (data.enableSSL && data.certificateId !== undefined), {
message: 'SSL 인증서를 선택해주세요',
path: ['certificateId'],
});
export default function RoutingEdit() {
const navigate = useNavigate();
const { id } = useParams();
const { authFetch, selectedProject } = useAuthStore();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: undefined,
domain: undefined,
ip: undefined,
port: undefined,
enableSSL: false,
certificateId: undefined,
caching: false,
},
});
const domain = useWatch({ control: form.control, name: 'domain' });
const enableSSL = useWatch({ control: form.control, name: 'enableSSL' });
const debouncedDomain = useDebounce(domain, 500);
const [certificates, setCertificates] = useState<Certificate[]>([]);
const initData = useRef<z.infer<typeof formSchema> | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
authFetch(`/api/routing?routingId=${id}`)
.then((response) => {
if (!response.ok) throw Error(`라우팅 정보 조회 실패: ${response.status}`);
return response.json();
})
.then(({ name, domain, ip, port, certificateId, caching }) => {
initData.current = {
name,
domain,
ip,
port: parseInt(port),
enableSSL: certificateId ? true : false,
certificateId: certificateId || -1,
caching,
};
form.setValue('name', initData.current.name);
form.setValue('domain', initData.current.domain);
form.setValue('ip', initData.current.ip);
form.setValue('port', initData.current.port);
form.setValue('enableSSL', initData.current.enableSSL);
form.setValue('certificateId', initData.current.certificateId);
form.setValue('caching', initData.current.caching);
setIsLoading(false);
})
.catch((error) => {
console.error(error);
toast.error('라우팅 설정 정보를 조회할 수 없습니다.');
});
}, []);
useEffect(() => {
if (enableSSL) {
authFetch(`/api/certificates?projectId=${selectedProject?.id}&domain=${debouncedDomain}`)
.then((response) => {
if (!response.ok) throw Error(`SSL 인증서 목록 조회 실패: ${response.status}`);
return response.json();
})
.then(({ contents }) => {
setCertificates(contents);
})
.catch((error) => {
console.error(error);
toast.error('SSL 인증서 목록을 불러올 수 없습니다.');
});
}
}, [authFetch, enableSSL, selectedProject, debouncedDomain]);
async function onSubmit(values: z.infer<typeof formSchema>) {
if (!initData.current) return;
const payload: Partial<z.infer<typeof formSchema>> = {};
if (values.name !== initData.current.name) payload.name = values.name;
if (values.domain !== initData.current.domain) payload.domain = values.domain;
if (values.ip !== initData.current.ip) payload.ip = values.ip;
if (values.port !== initData.current.port) payload.port = values.port;
if (values.caching !== initData.current.caching) payload.caching = values.caching;
if (!values.enableSSL && values.enableSSL !== initData.current.enableSSL) payload.certificateId = -1;
if (values.enableSSL && values.certificateId !== initData.current.certificateId)
payload.certificateId = values.certificateId;
const response = await authFetch(`/api/routing?routingId=${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
const { code } = await response.json();
if (code == 'DUPLICATED_DOMAIN_NAME') {
form.setError('domain', { type: 'custom' });
toast.error('이미 사용중인 도메인입니다');
} else {
toast.error('라우팅 설정 수정에 실패하였습니다');
}
} else {
toast.success('라우팅 설정이 수정되었습니다');
navigate('/routing');
}
}
return (
<div className="flex flex-1 flex-col gap-4 p-6">
<div className="mb-2">
<h1 className="scroll-m-20 text-3xl font-semibold first:mt-0">라우팅 설정 수정</h1>
<p className="mt-1 text-base text-gray-500">웹 프록시 서버의 기존 라우팅 설정을 수정합니다.</p>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card className="mb-4">
<CardHeader>
<CardTitle className="text-xl">라우팅 기본 설정</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{isLoading ? (
<Skeleton className="h-[24rem] w-full" />
) : (
<div className="grid grid-cols-1 gap-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel required>서버 이름</FormLabel>
<FormControl>
<Input placeholder="웹 서버 이름" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem>
<FormLabel required>도메인</FormLabel>
<FormControl>
<Input placeholder="example.ajou.app" {...field} />
</FormControl>
<FormDescription>
*.ajou.app 도메인은 별도 설정 없이 자유롭게 사용할 수 있습니다
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="ip"
render={({ field }) => (
<FormItem>
<FormLabel required>인스턴스 IP</FormLabel>
<FormControl>
<Input placeholder="10.16.x.x" {...field} />
</FormControl>
<FormDescription>인스턴스 IP는 10.16.0.0/16 대역을 사용합니다</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
render={({ field }) => (
<FormItem>
<FormLabel required>인스턴스 포트</FormLabel>
<FormControl>
<Input type="number" placeholder="8080" {...field} />
</FormControl>
<FormDescription>연결할 웹 서비스의 포트 번호를 입력해주세요</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</CardContent>
</Card>
<Card className="mb-6">
<CardHeader>
<CardTitle className="text-xl">추가 설정</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{isLoading ? (
<Skeleton className="h-[8rem] w-full" />
) : (
<>
<FormField
control={form.control}
name="enableSSL"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<FormLabel className="text-base font-medium">SSL 보안 연결</FormLabel>
<FormDescription className="text-sm text-gray-500">
HTTPS 프로토콜을 사용하여 보안 연결을 활성화합니다
</FormDescription>
</div>
<FormControl>
<div className="flex items-center space-x-2">
{!field.value && <X className="h-4 w-4 text-gray-500" />}
{field.value && <Check className="h-4 w-4" />}
<Switch checked={field.value} onCheckedChange={field.onChange} />
</div>
</FormControl>
</FormItem>
)}
/>
{form.watch('enableSSL') && (
<FormField
control={form.control}
name="certificateId"
render={({ field }) => (
<FormItem>
<Select onValueChange={field.onChange}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="SSL 인증서 선택" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectGroup>
{certificates.length === 0 ? (
<SelectLabel className="text-muted-foreground">
사용 가능한 SSL 인증서가 없습니다. &nbsp;
<Link to="/certificate/create" className="font-medium underline underline-offset-4">
추가하기
</Link>
</SelectLabel>
) : (
certificates.map((certificate) => (
<SelectItem value={certificate.id.toString()}>{certificate.domain}</SelectItem>
))
)}
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<Separator />
<FormField
control={form.control}
name="caching"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<FormLabel className="text-base font-medium">정적 파일 캐싱 활성화</FormLabel>
<FormDescription className="text-sm text-gray-500">
빠른 응답을 위해 정적 파일에 대한 캐싱을 활성화합니다
</FormDescription>
</div>
<FormControl>
<div className="flex items-center space-x-2">
{!field.value && <X className="h-4 w-4 text-gray-500" />}
{field.value && <Check className="h-4 w-4" />}
<Switch checked={field.value} onCheckedChange={field.onChange} />
</div>
</FormControl>
</FormItem>
)}
/>
</>
)}
</CardContent>
</Card>
<div className="flex justify-end gap-2">
<Button variant="outline" asChild>
<Link to="..">취소</Link>
</Button>
<Button type="submit">저장</Button>
</div>
</form>
</Form>
</div>
);
}
......@@ -30,15 +30,16 @@ export default function RoutingList() {
useEffect(() => {
authFetch(`/api/routings?projectId=${selectedProject?.id}`)
.then((response) => {
if (!response.ok) {
toast.error('라우팅 정보를 조회할 수 없습니다.');
return { contents: [] };
}
if (!response.ok) throw Error(`라우팅 목록 조회 실패: ${response.status}`);
return response.json();
})
.then(({ contents }) => {
setRoutings(contents);
})
.catch((error) => {
console.error(error);
toast.error('라우팅 정보를 조회할 수 없습니다.');
});
}, [authFetch, selectedProject]);
......@@ -92,6 +93,7 @@ export default function RoutingList() {
<TableHead className="w-48">이름</TableHead>
<TableHead>도메인</TableHead>
<TableHead>인스턴스 IP</TableHead>
<TableHead>포트</TableHead>
<TableHead className="w-32 min-w-32 text-center">기타 설정</TableHead>
<TableHead className="w-32 min-w-32 text-center">작업</TableHead>
</TableRow>
......@@ -100,24 +102,24 @@ export default function RoutingList() {
{routings === null ? (
<>
<TableRow>
<TableCell colSpan={4}>
<TableCell colSpan={6}>
<Skeleton className="w-full h-[1rem] my-2 rounded-full" />
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={4}>
<TableCell colSpan={6}>
<Skeleton className="w-full h-[1rem] my-2 rounded-full" />
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={4}>
<TableCell colSpan={6}>
<Skeleton className="w-full h-[1rem] my-2 rounded-full" />
</TableCell>
</TableRow>
</>
) : routings.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground">
<TableCell colSpan={6} className="text-center text-muted-foreground">
현재 프로젝트에 등록된 웹 프록시 설정이 없습니다.
</TableCell>
</TableRow>
......@@ -132,7 +134,7 @@ export default function RoutingList() {
<div className="space-y-1">
<p className="text-sm font-semibold">{routing.name}</p>
<p className="text-sm">
{routing.domain} ({routing.ip})
{routing.domain} ({routing.ip}:{routing.port})
</p>
<p className="text-xs text-muted-foreground mt-2">{routing.createdAt} 생성</p>
<p className="text-xs text-muted-foreground">{routing.updatedAt} 수정</p>
......@@ -143,6 +145,7 @@ export default function RoutingList() {
</TableCell>
<TableCell>{routing.domain}</TableCell>
<TableCell>{routing.ip}</TableCell>
<TableCell>{routing.port}</TableCell>
<TableCell>
<div className="flex justify-center items-center gap-1">
{routing.certificateId !== undefined ? (
......
......@@ -5,6 +5,7 @@ import Home from '@/pages/Home';
import Login from '@/pages/Login';
import RoutingList from '@/pages/routing/List';
import RoutingCreate from '@/pages/routing/Create';
import RoutingEdit from '@/pages/routing/Edit';
import CertificateList from './pages/certificate/List';
import CertificateCreate from './pages/certificate/Create';
import ForwardingList from '@/pages/forwarding/List';
......@@ -20,6 +21,7 @@ export default function AppRoutes() {
<Route path="routing">
<Route index element={<RoutingList />} />
<Route path="create" element={<RoutingCreate />} />
<Route path="edit/:id" element={<RoutingEdit />} />
</Route>
<Route path="certificate">
<Route index element={<CertificateList />} />
......