240 lines
8.9 KiB
TypeScript
240 lines
8.9 KiB
TypeScript
import { useState } from "react";
|
||
import toast from "react-hot-toast";
|
||
import { IoCopyOutline, IoWifiOutline } from "react-icons/io5";
|
||
import { MdOutlineDns } from "react-icons/md";
|
||
import { FaBolt } from "react-icons/fa6";
|
||
import { BsShieldExclamation } from "react-icons/bs";
|
||
import { useMutation } from "@tanstack/react-query";
|
||
import { AxiosError } from "axios";
|
||
import MuiModal from "../../../../common/modal";
|
||
import Button from "../../../../components/button";
|
||
import Input from "../../../../components/input";
|
||
import Alert from "../../../../components/alert/Alert";
|
||
import { ModalDnsConfigProps } from "../../../../types/props";
|
||
import useAxios from "../../../../hooks/useAxios";
|
||
import { queryClient } from "../../../../main";
|
||
|
||
const IP_ADDRESS_REGEX =
|
||
/^(((\d{1,3}\.){3}\d{1,3})|([0-9a-fA-F:]+:+[0-9a-fA-F:]+))$/;
|
||
|
||
const CLIENT_IP_ENDPOINTS = [
|
||
{
|
||
url: "https://api64.ipify.org?format=json",
|
||
parse: async (res: Response) => (await res.json())?.ip,
|
||
},
|
||
{
|
||
url: "https://api.ipify.org?format=json",
|
||
parse: async (res: Response) => (await res.json())?.ip,
|
||
},
|
||
{
|
||
url: "https://icanhazip.com",
|
||
parse: async (res: Response) => (await res.text()).trim(),
|
||
},
|
||
];
|
||
|
||
const getErrorMessage = (err: AxiosError) => {
|
||
const responseData = err.response?.data;
|
||
if (
|
||
responseData &&
|
||
typeof responseData === "object" &&
|
||
"alert" in responseData
|
||
) {
|
||
return (responseData as { alert: string }).alert;
|
||
}
|
||
return "یک خطای غیرمنتظره رخ داد";
|
||
};
|
||
|
||
const ModalDnsConfig = ({
|
||
isOpen,
|
||
setIsOpen,
|
||
nameConfig,
|
||
orderId,
|
||
primaryIpv4,
|
||
secondaryIpv4,
|
||
primaryIpv6,
|
||
secondaryIpv6,
|
||
dnsIpPending,
|
||
dnsRegistrationExpired,
|
||
}: ModalDnsConfigProps) => {
|
||
const axios = useAxios();
|
||
const [ip, setIp] = useState("");
|
||
const [isDetecting, setIsDetecting] = useState(false);
|
||
|
||
const copyToClipboard = async (label: string, textToCopy?: string | null) => {
|
||
if (!textToCopy) return;
|
||
try {
|
||
await navigator.clipboard.writeText(textToCopy);
|
||
toast.success(`${label} کپی شد!`);
|
||
} catch (err) {
|
||
toast.error(`کپی انجام نشد: ${err}`);
|
||
}
|
||
};
|
||
|
||
const registerIpMutation = useMutation({
|
||
mutationKey: ["registerDnsIp", orderId],
|
||
mutationFn: (ipToRegister: string) => {
|
||
return axios.post(`/profile/dns/${orderId}/register-ip`, {
|
||
ip: ipToRegister,
|
||
});
|
||
},
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||
},
|
||
});
|
||
|
||
const registerIp = async (ipToRegister: string) => {
|
||
const normalizedIp = ipToRegister.trim();
|
||
if (!orderId) {
|
||
toast.error("شناسه سفارش نامعتبر است");
|
||
return;
|
||
}
|
||
if (!IP_ADDRESS_REGEX.test(normalizedIp)) {
|
||
toast.error("آیپی وارد شده معتبر نیست");
|
||
return;
|
||
}
|
||
setIp(normalizedIp);
|
||
try {
|
||
await toast.promise(registerIpMutation.mutateAsync(normalizedIp), {
|
||
loading: "درحال ذخیره آیپی...",
|
||
success: (res) => res?.data?.alert || "آیپی با موفقیت ذخیره شد",
|
||
error: (err: AxiosError) => getErrorMessage(err),
|
||
});
|
||
} catch {
|
||
return;
|
||
}
|
||
};
|
||
|
||
const handleAutoDetectIp = async () => {
|
||
setIsDetecting(true);
|
||
try {
|
||
for (const endpoint of CLIENT_IP_ENDPOINTS) {
|
||
try {
|
||
const res = await fetch(endpoint.url, { cache: "no-store" });
|
||
if (!res.ok) continue;
|
||
|
||
const detectedIp = await endpoint.parse(res);
|
||
if (detectedIp && IP_ADDRESS_REGEX.test(detectedIp)) {
|
||
setIp(detectedIp);
|
||
toast.success("آیپی کاربر دریافت شد");
|
||
return;
|
||
}
|
||
} catch {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
toast.error("دریافت آیپی خودکار ممکن نشد");
|
||
} catch (err) {
|
||
toast.error(`دریافت آیپی خودکار با خطا مواجه شد: ${err}`);
|
||
} finally {
|
||
setIsDetecting(false);
|
||
}
|
||
};
|
||
|
||
const ipRows: { label: string; badge: string; value?: string | null }[] = [
|
||
{ label: "DNS نسل 4", badge: "v4", value: primaryIpv4 },
|
||
{ label: "DNS نسل 4", badge: "v4", value: secondaryIpv4 },
|
||
{ label: "DNS نسل 6", badge: "v6", value: primaryIpv6 },
|
||
{ label: "DNS نسل 6", badge: "v6", value: secondaryIpv6 },
|
||
].filter((row) => row.value);
|
||
|
||
return (
|
||
<MuiModal isOpenModal={isOpen} setIsOpenModal={setIsOpen} nameService={nameConfig}>
|
||
<div className="w-full h-full flex flex-col gap-4 max-sm:text-xs max-md:text-sm text-base">
|
||
{dnsRegistrationExpired && (
|
||
<Alert type="danger" extra="!mb-0 !rounded-lg">
|
||
مهلت ثبت آیپی برای این سرویس به پایان رسیده است
|
||
</Alert>
|
||
)}
|
||
{dnsIpPending && !dnsRegistrationExpired && (
|
||
<Alert type="warning" extra="!mb-0 !rounded-lg">
|
||
ثبت آیپی این سرویس در انتظار تایید است
|
||
</Alert>
|
||
)}
|
||
|
||
{ipRows.length > 0 && (
|
||
<div className="w-full flex flex-col gap-2">
|
||
<div className="flex flex-row-reverse items-center gap-1.5 text-secondTheme font-bold">
|
||
<MdOutlineDns className="w-4 h-4" />
|
||
<span>آدرسهای DNS</span>
|
||
</div>
|
||
<div className="w-full flex flex-col gap-2">
|
||
{ipRows.map((row, index) => (
|
||
<div
|
||
key={`${row.badge}-${row.value}-${index}`}
|
||
className="group w-full flex flex-col gap-1.5 bg-gray-900/60 border border-gray-700 hover:border-secondTheme/50 transition-colors rounded-lg p-2.5"
|
||
>
|
||
<div className="w-full flex flex-row-reverse items-center justify-between gap-2">
|
||
<div className="flex flex-row-reverse items-center gap-2 min-w-0">
|
||
<span className="shrink-0 text-[10px] md:text-xs font-bold px-2 py-0.5 rounded-full bg-secondTheme/10 text-secondTheme border border-secondTheme/30">
|
||
{row.badge}
|
||
</span>
|
||
<span className="text-gray-300 whitespace-nowrap">{row.label}</span>
|
||
</div>
|
||
<button
|
||
onClick={() => copyToClipboard(row.label, row.value)}
|
||
className="shrink-0 p-2 rounded-md bg-gray-800 text-gray-300 hover:bg-secondTheme hover:text-gray-900 transition-colors"
|
||
title="کپی"
|
||
>
|
||
<IoCopyOutline className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
<div
|
||
className="w-full text-start break-all font-mono text-secondTheme/90"
|
||
dir="ltr"
|
||
>
|
||
{row.value}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="w-full flex flex-row-reverse items-start gap-2 bg-yellow-500/10 border border-yellow-500/30 text-yellow-300 rounded-lg p-3">
|
||
<BsShieldExclamation className="w-8 h-8 md:w-5 md:h-5 shrink-0 mt-0.5" />
|
||
<span className="text-start leading-6">
|
||
در صورت روشن بودن فیلترشکن (VPN)، برای ثبت صحیح آیپی، لطفا ابتدا آن را خاموش کنید.
|
||
</span>
|
||
</div>
|
||
|
||
<div className="w-full flex flex-col gap-2.5 bg-gray-900/60 border border-gray-700 rounded-lg p-3">
|
||
<div className="flex flex-row-reverse items-center gap-1.5 text-secondTheme font-bold">
|
||
<IoWifiOutline className="w-4 h-4" />
|
||
<span>ثبت آیپی</span>
|
||
</div>
|
||
<div className="w-full flex flex-row-reverse gap-2">
|
||
<div className="flex-1 min-w-0">
|
||
<Input
|
||
type="text"
|
||
dir="ltr"
|
||
placeholder="آیپی خود را وارد کنید"
|
||
value={ip}
|
||
onChange={(e) => setIp(e.target.value)}
|
||
extra="!text-start"
|
||
/>
|
||
</div>
|
||
<Button
|
||
onClick={() => registerIp(ip)}
|
||
disabled={registerIpMutation.isPending || !ip.trim()}
|
||
extra="!m-0 !px-3 shadow-md shrink-0 disabled:!opacity-50"
|
||
>
|
||
ذخیره آیپی
|
||
</Button>
|
||
</div>
|
||
<Button
|
||
onClick={handleAutoDetectIp}
|
||
disabled={isDetecting || registerIpMutation.isPending}
|
||
extra="!m-0 !px-3 !bg-secondTheme !text-gray-900 font-bold shadow-md shadow-secondTheme/30 hover:!bg-secondTheme/80 flex flex-row-reverse justify-center items-center gap-2 disabled:!opacity-50"
|
||
>
|
||
<FaBolt className={`w-4 h-4 ${isDetecting ? "animate-pulse" : ""}`} />
|
||
{isDetecting ? "در حال تشخیص..." : "دریافت آیپی کاربر"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</MuiModal>
|
||
);
|
||
};
|
||
|
||
export default ModalDnsConfig;
|