Files
admin-frontend-domain/src/routes/settings.tsx
T

815 lines
44 KiB
TypeScript

import { updateSettings } from "@/api/settings"
import { SettingsTab } from "@/components/settings-tab"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { Combobox } from "@/components/ui/combobox"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { useAuth } from "@/hooks/useAuth"
import { useNotification } from "@/hooks/useNotfication"
import useSetting from "@/hooks/useSetting"
import { asOptionalField, safeExternalHref } from "@/lib/utils"
import { nezhaLang, settingCoverageTypes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { Navigate } from "react-router-dom"
import { toast } from "sonner"
import { z } from "zod"
const settingFormSchema = z.object({
dns_servers: asOptionalField(z.string()),
ignored_ip_notification: asOptionalField(z.string()),
ip_change_notification_group_id: z.coerce.number().int().min(0),
cover: z.coerce.number().int().min(1),
site_name: z.string().min(1),
language: z.string().min(2),
user_template: z.string().min(1),
install_host: asOptionalField(z.string()),
dashboard_host: asOptionalField(z.string()),
reserved_hosts: asOptionalField(z.string()),
custom_code: asOptionalField(z.string()),
custom_code_dashboard: asOptionalField(z.string()),
web_real_ip_header: asOptionalField(z.string()),
agent_real_ip_header: asOptionalField(z.string()),
tls: asOptionalField(z.boolean()),
enable_ip_change_notification: asOptionalField(z.boolean()),
enable_plain_ip_in_notification: asOptionalField(z.boolean()),
custom_logo: asOptionalField(z.string()),
custom_description: asOptionalField(z.string()),
custom_links: asOptionalField(z.string()),
background_image_day: asOptionalField(z.string()),
background_image_night: asOptionalField(z.string()),
telegram_bot_token: asOptionalField(z.string()),
telegram_admin_chat_id: asOptionalField(z.string()),
smtp_server: asOptionalField(z.string()),
smtp_user: asOptionalField(z.string()),
smtp_password: asOptionalField(z.string()),
admin_email: asOptionalField(z.string()),
domain_expiry_notification_days: asOptionalField(z.string()),
server_expiry_notification_days: asOptionalField(z.string()),
expiry_notification_group_id: asOptionalField(z.coerce.number().int().min(0)),
enable_mcp: asOptionalField(z.boolean()),
})
export default function SettingsPage() {
const { t, i18n } = useTranslation()
const { data: config, mutate } = useSetting()
const { profile, loading: authLoading } = useAuth()
const { notifierGroup } = useNotification()
const ngroupList = notifierGroup?.map((ng) => ({
value: `${ng.group.id}`,
label: ng.group.name,
})) || [{ value: "", label: "" }]
const isAdmin = profile?.role === 0
// 所有 hooks 必须在条件 return 之前调用,否则违反 rules-of-hooks。
const form = useForm<z.infer<typeof settingFormSchema>>({
resolver: zodResolver(settingFormSchema) as any,
defaultValues: config
? {
...config.config,
user_template:
config.config?.user_template ||
Object.keys(config.frontend_templates?.filter((t) => !t.is_admin) || {})[0] ||
"user-dist",
}
: {
ip_change_notification_group_id: 0,
cover: 1,
site_name: "",
language: "",
user_template: "user-dist",
expiry_notification_group_id: 0,
domain_expiry_notification_days: "",
server_expiry_notification_days: "",
},
resetOptions: {
keepDefaultValues: false,
},
})
useEffect(() => {
if (config?.config) {
form.reset(config?.config)
}
}, [config?.config, form])
if (authLoading) {
return null
}
if (!isAdmin) {
return <Navigate to="/dashboard/settings/api-tokens" replace />
}
const onSubmit = async (values: any) => {
try {
await updateSettings(values)
form.reset()
await mutate()
} catch (e) {
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", {
error: e?.toString(),
}),
})
return
}
if (values.language != i18n.language) {
i18n.changeLanguage(values.language)
}
toast(t("Success"))
}
return (
<div className="px-3">
<SettingsTab className="mt-6 mb-4 w-full" />
<div>
<Form {...(form as any)}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
<FormField
control={form.control}
name="site_name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("SiteName")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="custom_logo"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Logo URL</FormLabel>
<FormControl>
<Input
placeholder="https://example.com/logo.png"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="custom_description"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Description / Subtitle</FormLabel>
<FormControl>
<Input placeholder="My monitoring dashboard" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="custom_links"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Links (JSON Array)</FormLabel>
<FormControl>
<Input
placeholder='[{"link":"https://loohui.com/","name":"Blog","blank":false}]'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="background_image_day"
render={({ field }) => (
<FormItem>
<FormLabel>Background Image (Day)</FormLabel>
<FormControl>
<Input
placeholder="https://example.com/day.jpg"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="background_image_night"
render={({ field }) => (
<FormItem>
<FormLabel>Background Image (Night)</FormLabel>
<FormControl>
<Input
placeholder="https://example.com/night.jpg"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="telegram_bot_token"
render={({ field }) => (
<FormItem>
<FormLabel>Telegram Bot Token</FormLabel>
<FormControl>
<Input placeholder="123456789:ABCDEF..." {...field} value={field.value as string || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="telegram_admin_chat_id"
render={({ field }) => (
<FormItem>
<FormLabel>Telegram Admin Chat ID</FormLabel>
<FormControl>
<Input placeholder="12345678" {...field} value={field.value as string || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtp_server"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Server (host:port)</FormLabel>
<FormControl>
<Input placeholder="smtp.example.com:465" {...field} value={field.value as string || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtp_user"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP User</FormLabel>
<FormControl>
<Input placeholder="user@example.com" {...field} value={field.value as string || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtp_password"
render={({ field }) => (
<FormItem>
<FormLabel>SMTP Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} value={field.value as string || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="admin_email"
render={({ field }) => (
<FormItem>
<FormLabel>Admin Email (Recipient)</FormLabel>
<FormControl>
<Input type="email" placeholder="admin@example.com" {...field} value={field.value as string || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Language")}</FormLabel>
<FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(nezhaLang).map(([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="user_template"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Theme")}</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(value) => {
const template = config?.frontend_templates?.find(
(t) => t.path === value,
)
if (template) {
form.setValue("user_template", template!.path!)
}
}}
>
<FormControl>
<SelectTrigger className="py-8">
<SelectValue placeholder={t("SelectTheme")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
config?.frontend_templates?.filter(
(t) => !t.is_admin,
) || []
).map((template) => (
<div key={template.path}>
<SelectItem value={template.path!}>
<div className="flex flex-col items-start gap-1">
<div className="font-medium">
{template.name}
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
{t("Author")}:{" "}
{template.author}
</span>
{!template.is_official ? (
<span className="px-1.5 py-0.5 rounded-md bg-red-100 text-red-800 text-xs">
{t("Community")}
</span>
) : (
<span className="px-1.5 py-0.5 rounded-md bg-blue-100 text-blue-800 text-xs">
{t("Official")}
</span>
)}
</div>
</div>
</SelectItem>
<div className="px-8 py-1">
{safeExternalHref(
template.repository,
) ? (
<a
href={safeExternalHref(
template.repository,
)}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
>
{template.repository}
</a>
) : (
<span className="text-sm text-muted-foreground">
{template.repository}
</span>
)}
</div>
</div>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
{!config?.frontend_templates?.find(
(t) => t.path === field.value,
)?.is_official && (
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-2">
<div className="font-medium text-lg mb-1">
{t("CommunityThemeWarning")}
</div>
<div className="text-yellow-700 dark:text-yellow-200">
{t("CommunityThemeDescription")}
</div>
</div>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="custom_code"
render={({ field }) => (
<FormItem>
<FormLabel>{t("CustomCodes")}</FormLabel>
<FormControl>
<Textarea className="resize-y min-h-48" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="custom_code_dashboard"
render={({ field }) => (
<FormItem>
<FormLabel>{t("CustomCodesDashboard")}</FormLabel>
<FormControl>
<Textarea className="resize-y min-h-48" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="install_host"
render={({ field }) => (
<FormItem>
<FormLabel>{t("DashboardOriginalHost")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dashboard_host"
render={({ field }) => (
<FormItem>
<FormLabel>{t("DashboardHost")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>{t("DashboardHostHint")}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="reserved_hosts"
render={({ field }) => (
<FormItem>
<FormLabel>{t("ReservedHosts")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>{t("ReservedHostsHint")}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="tls"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">{t("ConfigTLS")}</Label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dns_servers"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("CustomPublicDNSNameserversforDDNS") +
" " +
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="web_real_ip_header"
render={({ field }) => (
<FormItem>
<FormLabel>{t("WebRealIPHeader")}</FormLabel>
<FormControl>
<div className="flex items-center">
<Input
disabled={field.value == "NZ::Use-Peer-IP"}
className="w-1/2"
placeholder="CF-Connecting-IP"
{...field}
/>
<Checkbox
checked={field.value == "NZ::Use-Peer-IP"}
className="ml-2"
onCheckedChange={(checked) => {
form.setValue(
"web_real_ip_header",
checked ? "NZ::Use-Peer-IP" : "",
)
}}
/>
<FormLabel className="font-normal ml-2">
{t("UseDirectConnectingIP")}
</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="agent_real_ip_header"
render={({ field }) => (
<FormItem>
<FormLabel>{t("AgentRealIPHeader")}</FormLabel>
<FormControl>
<div className="flex items-center">
<Input
disabled={field.value == "NZ::Use-Peer-IP"}
className="w-1/2"
placeholder="CF-Connecting-IP"
{...field}
/>
<Checkbox
checked={field.value == "NZ::Use-Peer-IP"}
className="ml-2"
onCheckedChange={(checked) => {
form.setValue(
"agent_real_ip_header",
checked ? "NZ::Use-Peer-IP" : "",
)
}}
/>
<FormLabel className="font-normal ml-2">
{t("UseDirectConnectingIP")}
</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormItem>
<FormLabel>{t("ExpiryNotification")}</FormLabel>
<Card className="w-full">
<CardContent>
<div className="flex flex-col space-y-4 mt-4">
<FormField
control={form.control}
name="expiry_notification_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("NotificationGroup")}</FormLabel>
<Combobox
options={ngroupList}
defaultValue={`${field.value}`}
onValueChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="domain_expiry_notification_days"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("DomainNotificationDays") +
" " +
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input
placeholder="60,30,15,7,3,1,0"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="server_expiry_notification_days"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("ServerNotificationDays") +
" " +
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input
placeholder="30,15,7,3,1,0"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</FormItem>
<FormItem>
<FormLabel>{t("IPChangeNotification")}</FormLabel>
<Card className="w-full">
<CardContent>
<div className="flex flex-col space-y-4 mt-4">
<FormField
control={form.control}
name="cover"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Coverage")}</FormLabel>
<Select
onValueChange={field.onChange}
value={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(
settingCoverageTypes,
).map(([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="ignored_ip_notification"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("SpecificServers") +
" " +
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input placeholder="1,2,3" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="ip_change_notification_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("NotifierGroup")}</FormLabel>
<FormControl>
<Combobox
placeholder={t("Search")}
options={ngroupList}
onValueChange={field.onChange}
defaultValue={field.value?.toString()}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="enable_ip_change_notification"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
{t("Enable")}
</Label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</FormItem>
<FormField
control={form.control}
name="enable_plain_ip_in_notification"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
{t("FullIPNotification")}
</Label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="enable_mcp"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">{t("EnableMCP")}</Label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">{t("Confirm")}</Button>
</form>
</Form>
</div>
</div>
)
}