diff --git a/.env.example b/.env.example index 2dcb8ba..9fdaa54 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,4 @@ NEXT_PUBLIC_CustomDescription=NezhaDash is a dashboard for Nezha. NEXT_PUBLIC_Links='[{"link":"https://github.com/hamster1963/nezha-dash","name":"GitHub"},{"link":"https://buycoffee.top/coffee","name":"Buycoffee☕️"}]' NEXT_PUBLIC_DisableIndex=false NEXT_PUBLIC_ShowTagCount=false +NEXT_PUBLIC_ShowIpInfo=false diff --git a/app/(main)/ClientComponents/ServerIPInfo.tsx b/app/(main)/ClientComponents/ServerIPInfo.tsx new file mode 100644 index 0000000..eb430d8 --- /dev/null +++ b/app/(main)/ClientComponents/ServerIPInfo.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { IPInfo } from "@/app/api/server-ip/route"; +import { Loader } from "@/components/loading/Loader"; +import { Card, CardContent } from "@/components/ui/card"; +import { nezhaFetcher } from "@/lib/utils"; +import { useTranslations } from "next-intl"; +import useSWRImmutable from "swr/immutable"; + +export default function ServerIPInfo({ server_id }: { server_id: number }) { + const t = useTranslations("IPInfo"); + + const { data } = useSWRImmutable( + `/api/server-ip?server_id=${server_id}`, + nezhaFetcher, + ); + + if (!data) { + return ( +
+ +
+ ); + } + + return ( + <> +
+ + +
+

{"ASN"}

+
+ {data.asn.autonomous_system_organization} +
+
+
+
+ + +
+

{t("asn_number")}

+
+ AS{data.asn.autonomous_system_number} +
+
+
+
+ + +
+

+ {t("registered_country")} +

+
+ {data.city.registered_country?.names.en} +
+
+
+
+ + +
+

{"ISO"}

+
{data.city.country?.iso_code}
+
+
+
+ + +
+

{t("city")}

+
{data.city.city?.names.en}
+
+
+
+ + +
+

{t("longitude")}

+
{data.city.location?.longitude}
+
+
+
+ + +
+

{t("latitude")}

+
{data.city.location?.latitude}
+
+
+
+ + +
+

{t("time_zone")}

+
{data.city.location?.time_zone}
+
+
+
+ {data.city.postal && ( + + +
+

+ {t("postal_code")} +

+
{data.city.postal?.code}
+
+
+
+ )} +
+ + ); +} diff --git a/app/(main)/server/[id]/page.tsx b/app/(main)/server/[id]/page.tsx index 4cfebd2..8500c24 100644 --- a/app/(main)/server/[id]/page.tsx +++ b/app/(main)/server/[id]/page.tsx @@ -5,8 +5,11 @@ import ServerDetailChartClient from "@/app/(main)/ClientComponents/ServerDetailC import ServerDetailClient from "@/app/(main)/ClientComponents/ServerDetailClient"; import TabSwitch from "@/components/TabSwitch"; import { Separator } from "@/components/ui/separator"; +import getEnv from "@/lib/env-entry"; import { use, useState } from "react"; +import ServerIPInfo from "../../ClientComponents/ServerIPInfo"; + export default function Page(props: { params: Promise<{ id: string }> }) { const params = use(props.params); const tabs = ["Detail", "Network"]; @@ -32,6 +35,9 @@ export default function Page(props: { params: Promise<{ id: string }> }) { />
+ {getEnv("NEXT_PUBLIC_ShowIpInfo") && ( + + )} (cityDbBuffer); + const asnLookup = new Reader(asnDbBuffer); + + const data: IPInfo = { + city: cityLookup.get(ip) as CityResponse, + asn: asnLookup.get(ip) as AsnResponse, + }; + return NextResponse.json(data, { status: 200 }); + } catch (error) { + const err = error as ResError; + console.error("Error in GET handler:", err); + const statusCode = err.statusCode || 500; + const message = err.message || "Internal Server Error"; + return NextResponse.json({ error: message }, { status: statusCode }); + } +} diff --git a/bun.lockb b/bun.lockb index 7060529..0558470 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/lib/GeoLite2-ASN.mmdb b/lib/GeoLite2-ASN.mmdb new file mode 100644 index 0000000..b8f5807 Binary files /dev/null and b/lib/GeoLite2-ASN.mmdb differ diff --git a/lib/GeoLite2-City.mmdb b/lib/GeoLite2-City.mmdb new file mode 100644 index 0000000..10f9527 Binary files /dev/null and b/lib/GeoLite2-City.mmdb differ diff --git a/lib/serverFetch.tsx b/lib/serverFetch.tsx index 4cbbe32..e985b3f 100644 --- a/lib/serverFetch.tsx +++ b/lib/serverFetch.tsx @@ -130,6 +130,57 @@ export async function GetServerMonitor({ server_id }: { server_id: number }) { } } +export async function GetServerIP({ + server_id, +}: { + server_id: number; +}): Promise { + let nezhaBaseUrl = getEnv("NezhaBaseUrl"); + if (!nezhaBaseUrl) { + console.error("NezhaBaseUrl is not set"); + throw new Error("NezhaBaseUrl is not set"); + } + + // Remove trailing slash + nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, ""); + + try { + const response = await fetch(`${nezhaBaseUrl}/api/v1/server/details`, { + headers: { + Authorization: getEnv("NezhaAuth") as string, + }, + next: { + revalidate: 0, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to fetch data: ${response.status} ${errorText}`); + } + + const resData = await response.json(); + + if (!resData.result) { + throw new Error("NezhaData fetch failed: 'result' field is missing"); + } + + const nezhaData = resData.result as NezhaAPI[]; + + // Find the server with the given ID + const server = nezhaData.find((element) => element.id === server_id); + + if (!server) { + throw new Error(`Server with ID ${server_id} not found`); + } + + return server?.valid_ip || server?.ipv4 || server?.ipv6 || ""; + } catch (error) { + console.error("GetNezhaData error:", error); + throw error; // Rethrow the error to be caught by the caller + } +} + export async function GetServerDetail({ server_id }: { server_id: number }) { let nezhaBaseUrl = getEnv("NezhaBaseUrl"); if (!nezhaBaseUrl) { diff --git a/messages/en.json b/messages/en.json index 0825d9e..364733c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -55,6 +55,15 @@ "avg_delay": "Latency", "chart_fetch_error_message": "Failed to fetch network data, please check if the server monitoring is enabled" }, + "IPInfo": { + "asn_number": "Origin ASN", + "registered_country": "Registered Country", + "time_zone": "Time Zone", + "postal_code": "Postal Code", + "city": "City", + "longitude": "Longitude", + "latitude": "Latitude" + }, "NetworkChart": { "ServerMonitorCount": "Services" }, diff --git a/messages/ja.json b/messages/ja.json index 998ca8b..56d966a 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -55,6 +55,15 @@ "avg_delay": "遅延", "chart_fetch_error_message": "ネットワークデータの取得に失敗しました。サーバーの監視が有効になっているかどうかを確認してください" }, + "IPInfo": { + "asn_number": "ASN", + "registered_country": "Registered Country", + "time_zone": "時刻", + "postal_code": "郵便番号", + "city": "都市", + "longitude": "経度", + "latitude": "緯度" + }, "NetworkChart": { "ServerMonitorCount": "サービス" }, diff --git a/messages/zh-t.json b/messages/zh-t.json index 49dca34..cd6713c 100644 --- a/messages/zh-t.json +++ b/messages/zh-t.json @@ -55,6 +55,15 @@ "avg_delay": "延遲", "chart_fetch_error_message": "獲取網絡數據失敗,請檢查是否開啟服務端監控" }, + "IPInfo": { + "asn_number": "ASN Number", + "registered_country": "注册地區", + "time_zone": "時區", + "postal_code": "郵遞區號", + "city": "城市", + "longitude": "經度", + "latitude": "緯度" + }, "NetworkChart": { "ServerMonitorCount": "個監測服務" }, diff --git a/messages/zh.json b/messages/zh.json index aa1ce1d..f7a3146 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -58,6 +58,15 @@ "NetworkChart": { "ServerMonitorCount": "个监控服务" }, + "IPInfo": { + "asn_number": "ASN 编号", + "registered_country": "注册地", + "time_zone": "时区", + "postal_code": "邮政编码", + "city": "城市", + "longitude": "经度", + "latitude": "纬度" + }, "ServerDetailClient": { "detail_fetch_error_message": "获取服务器详情失败,请检查您的环境变量并检查服务器控制台", "status": "状态", diff --git a/package.json b/package.json index f8c0227..2df84ac 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "framer-motion": "^12.0.0-alpha.2", "lucide-react": "^0.454.0", "luxon": "^3.5.0", + "maxmind": "^4.3.23", "next": "^15.1.0", "next-auth": "^5.0.0-beta.25", "next-intl": "^3.26.0",