feat: add billing UI, SMTP support and expiry settings

This commit is contained in:
Bot
2026-04-16 21:36:55 +08:00
parent 53cb369e4a
commit d9ec7c362c
6 changed files with 175 additions and 63 deletions
+93 -62
View File
@@ -58,6 +58,7 @@ const notificationFormSchema = z.object({
verify_tls: asOptionalField(z.boolean()),
skip_check: asOptionalField(z.boolean()),
format_metric_units: asOptionalField(z.boolean()),
type: z.coerce.number().int().default(1),
})
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,
skip_check: (data as any).skip_check ?? false,
format_metric_units: (data as any).format_metric_units ?? false,
type: data.type ?? 1,
}
: {
name: "",
@@ -88,6 +90,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
verify_tls: false,
skip_check: false,
format_metric_units: false,
type: 1,
},
resetOptions: {
keepDefaultValues: false,
@@ -143,12 +146,36 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
</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>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>URL</FormLabel>
<FormLabel>{form.watch("type") == 2 ? "SMTP Server (host:port)" : "URL"}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -156,72 +183,76 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="request_method"
render={({ field }) => (
<FormItem>
<FormLabel>{t("RequestMethod")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Request Method" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(nrequestMethods).map(
([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
),
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="request_type"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Type")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Request Type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(nrequestTypes).map(([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{form.watch("type") != 2 && (
<>
<FormField
control={form.control}
name="request_method"
render={({ field }) => (
<FormItem>
<FormLabel>{t("RequestMethod")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Request Method" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(nrequestMethods).map(
([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
),
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="request_type"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Type")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Request Type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(nrequestTypes).map(([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<FormField
control={form.control}
name="request_header"
render={({ field }) => (
<FormItem>
<FormLabel>{t("RequestHeader")}</FormLabel>
<FormLabel>{form.watch("type") == 2 ? "SMTP User:Pass" : t("RequestHeader")}</FormLabel>
<FormControl>
<Textarea
className="resize-y"
placeholder='{"User-Agent":"Nezha-Agent"}'
placeholder={form.watch("type") == 2 ? "user:pass" : '{"User-Agent":"Nezha-Agent"}'}
{...field}
/>
</FormControl>
@@ -234,11 +265,11 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
name="request_body"
render={({ field }) => (
<FormItem>
<FormLabel>{t("RequestBody")}</FormLabel>
<FormLabel>{form.watch("type") == 2 ? "Recipient Email" : 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;}'
className={form.watch("type") == 2 ? "resize-y" : "resize-y h-[240px]"}
placeholder={form.watch("type") == 2 ? "target@example.com" : '...'}
{...field}
/>
</FormControl>
+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 }) => {
@@ -249,6 +254,35 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
</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
control={form.control as any}
name="public_note"
+2
View File
@@ -151,6 +151,8 @@
"AgentRealIPHeader": "Agent real IP request header",
"UseDirectConnectingIP": "Use direct connection IP",
"IPChangeNotification": "IP Change Notification",
"IPChangeNotificationGroupID": "IP Change Notification Group ID",
"ExpiryNotificationGroupID": "Expiry Notification Group ID",
"FullIPNotification": "Show Full IP Address in Notification Messages",
"EditService": "Edit Service",
"CreateService": "Create Service",
+3 -1
View File
@@ -157,7 +157,9 @@
"WebRealIPHeader": "前端真实IP请求头",
"AgentRealIPHeader": "Agent真实IP请求头",
"UseDirectConnectingIP": "使用直连 IP",
"IPChangeNotification": "IP变更通知",
"IPChangeNotification": "IP 变更通知",
"IPChangeNotificationGroupID": "IP 变更通知组 ID",
"ExpiryNotificationGroupID": "到期通知组 ID",
"FullIPNotification": "在通知消息中显示完整的 IP 地址",
"LoginFailed": "登录失败",
"BruteForceAttackingToken": "暴力攻击令牌",
+26
View File
@@ -131,6 +131,32 @@ export default function SettingsPage() {
<div>
<Form {...form}>
<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
control={form.control}
name="site_name"
+17
View File
@@ -389,6 +389,7 @@ export interface ModelNotification {
created_at?: string;
id?: number;
name: string;
type?: number;
request_body: string;
request_header: string;
request_method: number;
@@ -401,6 +402,7 @@ export interface ModelNotification {
export interface ModelNotificationForm {
/** @minLength 1 */
name?: string;
type?: number;
request_body?: string;
request_header?: string;
request_method?: number;
@@ -464,6 +466,17 @@ export interface ModelProfileForm {
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 {
/** 覆盖范围 RuleCoverAll/IgnoreAll */
cover: number;
@@ -519,6 +532,7 @@ export interface ModelServer {
/** 公开备注 */
public_note?: string;
state?: ModelHostState;
billing_data?: ModelBillingDataMod;
updated_at?: string;
uuid?: string;
}
@@ -546,6 +560,7 @@ export interface ModelServerForm {
override_ddns_domains?: Record<string, string[]>;
/** 公开备注 */
public_note?: string;
billing_data?: ModelBillingDataMod;
}
export interface ModelServerGroup {
@@ -655,6 +670,7 @@ export interface ModelSetting {
enable_ip_change_notification?: boolean;
/** 通知信息IP不打码 */
enable_plain_ip_in_notification?: boolean;
expiry_notification_group_id?: number;
/** 特定服务器IP(多个服务器用逗号分隔) */
ignored_ip_notification?: string;
ignored_ip_notification_server_ids?: Record<string, boolean>;
@@ -684,6 +700,7 @@ export interface ModelSettingForm {
dns_servers?: string;
enable_ip_change_notification?: boolean;
enable_plain_ip_in_notification?: boolean;
expiry_notification_group_id?: number;
ignored_ip_notification?: string;
install_host?: string;
/** IP变更提醒的通知组 */