Compare commits

..

4 Commits

8 changed files with 222 additions and 81 deletions
+5
View File
@@ -36,4 +36,9 @@ export const deleteDomain = (id: number) => {
// 更新一个域名(包括公开状态和配置信息) // 更新一个域名(包括公开状态和配置信息)
export const updateDomain = (id: number, data: { is_public: boolean, billing_data: BillingDataMod }) => { export const updateDomain = (id: number, data: { is_public: boolean, billing_data: BillingDataMod }) => {
return fetcher<Domain>(FetcherMethod.PUT, `/api/v1/domains/${id}`, data) return fetcher<Domain>(FetcherMethod.PUT, `/api/v1/domains/${id}`, data)
}
// 同步 Whois 信息
export const syncDomainWHOIS = (id: number) => {
return fetcher<Domain>(FetcherMethod.POST, `/api/v1/domains/${id}/sync`)
} }
+121 -78
View File
@@ -50,7 +50,7 @@ interface NotifierCardProps {
const notificationFormSchema = z.object({ const notificationFormSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
url: z.string().url(), url: z.string().min(1),
request_method: z.coerce.number().int().min(1).max(255), request_method: z.coerce.number().int().min(1).max(255),
request_type: z.coerce.number().int().min(1).max(255), request_type: z.coerce.number().int().min(1).max(255),
request_header: z.string(), request_header: z.string(),
@@ -58,6 +58,7 @@ const notificationFormSchema = z.object({
verify_tls: asOptionalField(z.boolean()), verify_tls: asOptionalField(z.boolean()),
skip_check: asOptionalField(z.boolean()), skip_check: asOptionalField(z.boolean()),
format_metric_units: asOptionalField(z.boolean()), format_metric_units: asOptionalField(z.boolean()),
type: z.coerce.number().int().default(1),
}) })
export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => { export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
@@ -77,6 +78,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
verify_tls: (data as any).verify_tls ?? false, verify_tls: (data as any).verify_tls ?? false,
skip_check: (data as any).skip_check ?? false, skip_check: (data as any).skip_check ?? false,
format_metric_units: (data as any).format_metric_units ?? false, format_metric_units: (data as any).format_metric_units ?? false,
type: data.type ?? 1,
} }
: { : {
name: "", name: "",
@@ -88,6 +90,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
verify_tls: false, verify_tls: false,
skip_check: false, skip_check: false,
format_metric_units: false, format_metric_units: false,
type: 1,
}, },
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
@@ -143,102 +146,123 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Notification Type</FormLabel>
<Select
onValueChange={field.onChange}
value={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="1">Webhook</SelectItem>
<SelectItem value="2">SMTP (Email)</SelectItem>
<SelectItem value="3">Telegram</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="url" name="url"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>URL</FormLabel> <FormLabel>
{form.watch("type") == 2 ? "SMTP Server (host:port)" :
form.watch("type") == 3 ? "Bot Token" : "URL"}
</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} placeholder={form.watch("type") == 3 ? "123456:ABC-DEF" : ""} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField {form.watch("type") != 2 && form.watch("type") != 3 && (
control={form.control} <>
name="request_method" <FormField
render={({ field }) => ( control={form.control}
<FormItem> name="request_method"
<FormLabel>{t("RequestMethod")}</FormLabel> render={({ field }) => (
<Select <FormItem>
onValueChange={field.onChange} <FormLabel>{t("RequestMethod")}</FormLabel>
defaultValue={`${field.value}`} <Select
> onValueChange={field.onChange}
<FormControl> defaultValue={`${field.value}`}
<SelectTrigger> >
<SelectValue placeholder="Request Method" /> <FormControl>
</SelectTrigger> <SelectTrigger>
</FormControl> <SelectValue placeholder="Request Method" />
<SelectContent> </SelectTrigger>
{Object.entries(nrequestMethods).map( </FormControl>
([k, v]) => ( <SelectContent>
<SelectItem key={k} value={k}> {Object.entries(nrequestMethods).map(
{v} ([k, v]) => (
</SelectItem> <SelectItem key={k} value={k}>
), {v}
)} </SelectItem>
</SelectContent> ),
</Select> )}
<FormMessage /> </SelectContent>
</FormItem> </Select>
)} <FormMessage />
/> </FormItem>
<FormField )}
control={form.control} />
name="request_type" <FormField
render={({ field }) => ( control={form.control}
<FormItem> name="request_type"
<FormLabel>{t("Type")}</FormLabel> render={({ field }) => (
<Select <FormItem>
onValueChange={field.onChange} <FormLabel>{t("Type")}</FormLabel>
defaultValue={`${field.value}`} <Select
> onValueChange={field.onChange}
<FormControl> defaultValue={`${field.value}`}
<SelectTrigger> >
<SelectValue placeholder="Request Type" /> <FormControl>
</SelectTrigger> <SelectTrigger>
</FormControl> <SelectValue placeholder="Request Type" />
<SelectContent> </SelectTrigger>
{Object.entries(nrequestTypes).map(([k, v]) => ( </FormControl>
<SelectItem key={k} value={k}> <SelectContent>
{v} {Object.entries(nrequestTypes).map(([k, v]) => (
</SelectItem> <SelectItem key={k} value={k}>
))} {v}
</SelectContent> </SelectItem>
</Select> ))}
<FormMessage /> </SelectContent>
</FormItem> </Select>
)} <FormMessage />
/> </FormItem>
)}
/>
</>
)}
<FormField <FormField
control={form.control} control={form.control}
name="request_header" name="request_header"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("RequestHeader")}</FormLabel> <FormLabel>
{form.watch("type") == 2 ? "SMTP User:Pass" :
form.watch("type") == 3 ? "Chat ID" : t("RequestHeader")}
</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
className="resize-y" className="resize-y"
placeholder='{"User-Agent":"Nezha-Agent"}' placeholder={
{...field} form.watch("type") == 2 ? "user:pass" :
/> form.watch("type") == 3 ? "123456789" : '{"User-Agent":"Nezha-Agent"}'
</FormControl> }
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="request_body"
render={({ field }) => (
<FormItem>
<FormLabel>{t("RequestBody")}</FormLabel>
<FormControl>
<Textarea
className="resize-y h-[240px]"
placeholder='{&#13;&#10; "content":"#NEZHA#",&#13;&#10; "ServerName":"#SERVER.NAME#",&#13;&#10; "ServerIP":"#SERVER.IP#",&#13;&#10; "ServerIPV4":"#SERVER.IPV4#",&#13;&#10; "ServerIPV6":"#SERVER.IPV6#",&#13;&#10; "CPU":"#SERVER.CPU#",&#13;&#10; "MEM":"#SERVER.MEM#",&#13;&#10; "SWAP":"#SERVER.SWAP#",&#13;&#10; "DISK":"#SERVER.DISK#",&#13;&#10; "NetInSpeed":"#SERVER.NETINSPEED#",&#13;&#10; "NetOutSpeed":"#SERVER.NETOUTSPEED#",&#13;&#10; "TransferIn":"#SERVER.TRANSFERIN#",&#13;&#10; "TranferOut":"#SERVER.TRANSFEROUT#",&#13;&#10; "Load1":"#SERVER.LOAD1#",&#13;&#10; "Load5":"#SERVER.LOAD5#",&#13;&#10; "Load15":"#SERVER.LOAD15#",&#13;&#10; "TCP_CONN_COUNT":"#SERVER.TCPCONNCOUNT",&#13;&#10; "UDP_CONN_COUNT":"#SERVER.UDPCONNCOUNT"&#13;&#10;}'
{...field} {...field}
/> />
</FormControl> </FormControl>
@@ -246,6 +270,25 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
</FormItem> </FormItem>
)} )}
/> />
{form.watch("type") != 3 && (
<FormField
control={form.control}
name="request_body"
render={({ field }) => (
<FormItem>
<FormLabel>{form.watch("type") == 2 ? "Recipient Email" : t("RequestBody")}</FormLabel>
<FormControl>
<Textarea
className={form.watch("type") == 2 ? "resize-y" : "resize-y h-[240px]"}
placeholder={form.watch("type") == 2 ? "target@example.com" : '...'}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField <FormField
control={form.control} control={form.control}
name="verify_tls" name="verify_tls"
+34
View File
@@ -65,6 +65,11 @@ const serverFormSchema = z.object({
}, },
), ),
), ),
billing_data: z.object({
registrar: asOptionalField(z.string()),
endDate: asOptionalField(z.string()),
notes: asOptionalField(z.string()),
}).optional(),
}) })
export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => { export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
@@ -249,6 +254,35 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
</FormItem> </FormItem>
)} )}
/> />
<div className="p-3 border rounded-md border-dashed space-y-2">
<Label className="text-xs text-muted-foreground uppercase font-bold">Billing & Expiry</Label>
<FormField
control={form.control as any}
name="billing_data.registrar"
render={({ field }) => (
<FormItem>
<FormLabel>Registrar</FormLabel>
<FormControl>
<Input placeholder="AWS / Azure /阿里云" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control as any}
name="billing_data.endDate"
render={({ field }) => (
<FormItem>
<FormLabel>Expiry Date</FormLabel>
<FormControl>
<Input type="date" {...field} value={field.value?.split('T')[0] || ''} onChange={(e) => field.onChange(e.target.value ? new Date(e.target.value).toISOString() : '')} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField <FormField
control={form.control as any} control={form.control as any}
name="public_note" name="public_note"
+2
View File
@@ -151,6 +151,8 @@
"AgentRealIPHeader": "Agent real IP request header", "AgentRealIPHeader": "Agent real IP request header",
"UseDirectConnectingIP": "Use direct connection IP", "UseDirectConnectingIP": "Use direct connection IP",
"IPChangeNotification": "IP Change Notification", "IPChangeNotification": "IP Change Notification",
"IPChangeNotificationGroupID": "IP Change Notification Group ID",
"ExpiryNotificationGroupID": "Expiry Notification Group ID",
"FullIPNotification": "Show Full IP Address in Notification Messages", "FullIPNotification": "Show Full IP Address in Notification Messages",
"EditService": "Edit Service", "EditService": "Edit Service",
"CreateService": "Create Service", "CreateService": "Create Service",
+3 -1
View File
@@ -157,7 +157,9 @@
"WebRealIPHeader": "前端真实IP请求头", "WebRealIPHeader": "前端真实IP请求头",
"AgentRealIPHeader": "Agent真实IP请求头", "AgentRealIPHeader": "Agent真实IP请求头",
"UseDirectConnectingIP": "使用直连 IP", "UseDirectConnectingIP": "使用直连 IP",
"IPChangeNotification": "IP变更通知", "IPChangeNotification": "IP 变更通知",
"IPChangeNotificationGroupID": "IP 变更通知组 ID",
"ExpiryNotificationGroupID": "到期通知组 ID",
"FullIPNotification": "在通知消息中显示完整的 IP 地址", "FullIPNotification": "在通知消息中显示完整的 IP 地址",
"LoginFailed": "登录失败", "LoginFailed": "登录失败",
"BruteForceAttackingToken": "暴力攻击令牌", "BruteForceAttackingToken": "暴力攻击令牌",
+14 -2
View File
@@ -1,7 +1,7 @@
// src/routes/domain.tsx (最终 Bug 修复版) // src/routes/domain.tsx (最终 Bug 修复版)
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { PlusCircle, RefreshCw, MoreVertical, Trash2, Edit, CheckCircle } from 'lucide-react' import { PlusCircle, RefreshCw, MoreVertical, Trash2, Edit, CheckCircle, RefreshCcw } from 'lucide-react'
// 导入 shadcn/ui 组件 // 导入 shadcn/ui 组件
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@@ -19,7 +19,7 @@ import { toast } from 'sonner'
// 导入 API 类型和函数 // 导入 API 类型和函数
import type { Domain, BillingDataMod } from '@/types/api' import type { Domain, BillingDataMod } from '@/types/api'
import { useDomainList, addDomain, verifyDomain, deleteDomain, updateDomain } from '@/api/domain' import { useDomainList, addDomain, verifyDomain, deleteDomain, updateDomain, syncDomainWHOIS } from '@/api/domain'
import useSWR from 'swr' import useSWR from 'swr'
@@ -83,6 +83,17 @@ export default function DomainPage() {
} }
} }
const handleSyncWhois = async (domainId: number) => {
const loadingToast = toast.loading('正在同步 Whois 信息...')
try {
await syncDomainWHOIS(domainId)
toast.success('同步成功', { id: loadingToast, description: '域名 Whois 信息已更新。' })
mutate()
} catch (err) {
toast.error('同步失败', { id: loadingToast, description: (err as Error).message })
}
}
const handleDelete = async (domainId: number, domainName: string) => { const handleDelete = async (domainId: number, domainName: string) => {
if (window.confirm(`确定要删除域名 ${domainName} 吗?`)) { if (window.confirm(`确定要删除域名 ${domainName} 吗?`)) {
try { try {
@@ -213,6 +224,7 @@ export default function DomainPage() {
<DropdownMenuTrigger asChild><Button variant="ghost" size="icon"><MoreVertical className="h-4 w-4" /></Button></DropdownMenuTrigger> <DropdownMenuTrigger asChild><Button variant="ghost" size="icon"><MoreVertical className="h-4 w-4" /></Button></DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
{domain.Status === 'pending' && (<DropdownMenuItem onClick={() => handleVerify(domain.ID)}><CheckCircle className="mr-2 h-4 w-4" /> </DropdownMenuItem>)} {domain.Status === 'pending' && (<DropdownMenuItem onClick={() => handleVerify(domain.ID)}><CheckCircle className="mr-2 h-4 w-4" /> </DropdownMenuItem>)}
{domain.Status === 'verified' && (<DropdownMenuItem onClick={() => handleSyncWhois(domain.ID)}><RefreshCcw className="mr-2 h-4 w-4" /> Whois</DropdownMenuItem>)}
<DropdownMenuItem onClick={() => handleEditClick(domain)}><Edit className="mr-2 h-4 w-4" /> </DropdownMenuItem> <DropdownMenuItem onClick={() => handleEditClick(domain)}><Edit className="mr-2 h-4 w-4" /> </DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => handleDelete(domain.ID, domain.Domain)}><Trash2 className="mr-2 h-4 w-4" /> </DropdownMenuItem> <DropdownMenuItem className="text-red-600" onClick={() => handleDelete(domain.ID, domain.Domain)}><Trash2 className="mr-2 h-4 w-4" /> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
+26
View File
@@ -131,6 +131,32 @@ export default function SettingsPage() {
<div> <div>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
<FormField
control={form.control}
name="ip_change_notification_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("IPChangeNotificationGroupID")}</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiry_notification_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>Expiry Notification Group ID</FormLabel>
<FormControl>
<Input type="number" placeholder="Enter Group ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="site_name" name="site_name"
+17
View File
@@ -389,6 +389,7 @@ export interface ModelNotification {
created_at?: string; created_at?: string;
id?: number; id?: number;
name: string; name: string;
type?: number;
request_body: string; request_body: string;
request_header: string; request_header: string;
request_method: number; request_method: number;
@@ -401,6 +402,7 @@ export interface ModelNotification {
export interface ModelNotificationForm { export interface ModelNotificationForm {
/** @minLength 1 */ /** @minLength 1 */
name?: string; name?: string;
type?: number;
request_body?: string; request_body?: string;
request_header?: string; request_header?: string;
request_method?: number; request_method?: number;
@@ -464,6 +466,17 @@ export interface ModelProfileForm {
reject_password?: boolean; reject_password?: boolean;
} }
export interface ModelBillingDataMod {
registrar?: string;
registeredDate?: string;
endDate?: string;
renewalPrice?: string;
autoRenewal?: string;
notes?: string;
cycle?: string;
amount?: string;
}
export interface ModelRule { export interface ModelRule {
/** 覆盖范围 RuleCoverAll/IgnoreAll */ /** 覆盖范围 RuleCoverAll/IgnoreAll */
cover: number; cover: number;
@@ -519,6 +532,7 @@ export interface ModelServer {
/** 公开备注 */ /** 公开备注 */
public_note?: string; public_note?: string;
state?: ModelHostState; state?: ModelHostState;
billing_data?: ModelBillingDataMod;
updated_at?: string; updated_at?: string;
uuid?: string; uuid?: string;
} }
@@ -546,6 +560,7 @@ export interface ModelServerForm {
override_ddns_domains?: Record<string, string[]>; override_ddns_domains?: Record<string, string[]>;
/** 公开备注 */ /** 公开备注 */
public_note?: string; public_note?: string;
billing_data?: ModelBillingDataMod;
} }
export interface ModelServerGroup { export interface ModelServerGroup {
@@ -655,6 +670,7 @@ export interface ModelSetting {
enable_ip_change_notification?: boolean; enable_ip_change_notification?: boolean;
/** 通知信息IP不打码 */ /** 通知信息IP不打码 */
enable_plain_ip_in_notification?: boolean; enable_plain_ip_in_notification?: boolean;
expiry_notification_group_id?: number;
/** 特定服务器IP(多个服务器用逗号分隔) */ /** 特定服务器IP(多个服务器用逗号分隔) */
ignored_ip_notification?: string; ignored_ip_notification?: string;
ignored_ip_notification_server_ids?: Record<string, boolean>; ignored_ip_notification_server_ids?: Record<string, boolean>;
@@ -684,6 +700,7 @@ export interface ModelSettingForm {
dns_servers?: string; dns_servers?: string;
enable_ip_change_notification?: boolean; enable_ip_change_notification?: boolean;
enable_plain_ip_in_notification?: boolean; enable_plain_ip_in_notification?: boolean;
expiry_notification_group_id?: number;
ignored_ip_notification?: string; ignored_ip_notification?: string;
install_host?: string; install_host?: string;
/** IP变更提醒的通知组 */ /** IP变更提醒的通知组 */