mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
refactor: remove unused remarks and add quick domain create
Drop remark fields from Zone, Zone domains, proxy routes, WAF rule groups and IP groups across models, APIs, UI and DB columns (keep certificate/origin remarks). Add quick-create domain input for short labels, @ apex and full FQDNs when binding domains.
This commit is contained in:
@@ -37,6 +37,7 @@ sidebar: false
|
||||
### 移除
|
||||
|
||||
- 移除托管域名(managed-domains)管理 API 与前端 `WebsiteService`;请改用 Zone / Zone 域名 API。
|
||||
- 移除 Zone、Zone 域名、反代路由、WAF 规则组与 IP 组的备注字段(前后端与数据库列同步删除);证书与源站备注保留。
|
||||
|
||||
### 修复
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ erDiagram
|
||||
ZONES {
|
||||
uint id PK
|
||||
string domain UK
|
||||
string remark
|
||||
}
|
||||
ZONE_DOMAINS {
|
||||
uint id PK
|
||||
@@ -35,17 +34,16 @@ erDiagram
|
||||
uint proxy_route_id
|
||||
string domain UK
|
||||
uint cert_id
|
||||
string remark
|
||||
}
|
||||
```
|
||||
|
||||
### `of_zones`
|
||||
|
||||
保存根域、备注、创建时间与更新时间。根域全局唯一且创建后不可原地修改;需要变更时新建 Zone 并迁移域名。删除 Zone 前必须先清空其 Zone 域名。
|
||||
保存根域、创建时间与更新时间。根域全局唯一且创建后不可原地修改;需要变更时新建 Zone 并迁移域名。删除 Zone 前必须先清空其 Zone 域名。
|
||||
|
||||
### `of_zone_domains`
|
||||
|
||||
保存 `zone_id`、明确 `domain`、可空的 `proxy_route_id`、可空的 `cert_id`、备注及时间戳。`domain` 全局唯一;所有关系字段建立索引但不建立物理外键。`proxy_route_id` 允许为空,以承接已准备证书但尚未配置反代的历史域名。
|
||||
保存 `zone_id`、明确 `domain`、可空的 `proxy_route_id`、可空的 `cert_id` 及时间戳。`domain` 全局唯一;所有关系字段建立索引但不建立物理外键。`proxy_route_id` 允许为空,以承接已准备证书但尚未配置反代的历史域名。
|
||||
|
||||
`of_proxy_routes` 逐步移除 `domain`、`domains`、`cert_id`、`cert_ids` 与 `domain_cert_ids` 等域名/证书冗余列。路由不得再指定任何 TLS 证书;路由名称 `site_name` 成为稳定的人类可读标识,编译器从关联的 Zone 域名读取 `server_name` 与其 `cert_id`。这使每个明确域名的证书只有一个来源。
|
||||
|
||||
|
||||
@@ -15436,9 +15436,6 @@ const docTemplate = `{
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -15462,9 +15459,6 @@ const docTemplate = `{
|
||||
"proxy_route_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -17133,9 +17127,6 @@ const docTemplate = `{
|
||||
"redirect_http": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"site_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -17240,9 +17231,6 @@ const docTemplate = `{
|
||||
"redirect_http": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"site_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -18553,9 +18541,6 @@ const docTemplate = `{
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"subscription_format": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -18644,9 +18629,6 @@ const docTemplate = `{
|
||||
"referenced_by_rule_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"subscription_format": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -18796,9 +18778,6 @@ const docTemplate = `{
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18889,9 +18868,6 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -18934,9 +18910,6 @@ const docTemplate = `{
|
||||
},
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18945,9 +18918,6 @@ const docTemplate = `{
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18966,9 +18936,6 @@ const docTemplate = `{
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -15429,9 +15429,6 @@
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -15455,9 +15452,6 @@
|
||||
"proxy_route_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -17126,9 +17120,6 @@
|
||||
"redirect_http": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"site_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -17233,9 +17224,6 @@
|
||||
"redirect_http": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"site_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -18546,9 +18534,6 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"subscription_format": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -18637,9 +18622,6 @@
|
||||
"referenced_by_rule_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"subscription_format": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -18789,9 +18771,6 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18882,9 +18861,6 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -18927,9 +18903,6 @@
|
||||
},
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18938,9 +18911,6 @@
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18959,9 +18929,6 @@
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -1631,8 +1631,6 @@ definitions:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
remark:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
@@ -1648,8 +1646,6 @@ definitions:
|
||||
type: integer
|
||||
proxy_route_id:
|
||||
type: integer
|
||||
remark:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
zone_id:
|
||||
@@ -2742,8 +2738,6 @@ definitions:
|
||||
type: integer
|
||||
redirect_http:
|
||||
type: boolean
|
||||
remark:
|
||||
type: string
|
||||
site_name:
|
||||
type: string
|
||||
tunnel_id:
|
||||
@@ -2813,8 +2807,6 @@ definitions:
|
||||
type: integer
|
||||
redirect_http:
|
||||
type: boolean
|
||||
remark:
|
||||
type: string
|
||||
site_name:
|
||||
type: string
|
||||
tunnel_id:
|
||||
@@ -3697,8 +3689,6 @@ definitions:
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
remark:
|
||||
type: string
|
||||
subscription_format:
|
||||
type: string
|
||||
subscription_mapping_rule:
|
||||
@@ -3757,8 +3747,6 @@ definitions:
|
||||
type: string
|
||||
referenced_by_rule_count:
|
||||
type: integer
|
||||
remark:
|
||||
type: string
|
||||
subscription_format:
|
||||
type: string
|
||||
subscription_mapping_rule:
|
||||
@@ -3858,8 +3846,6 @@ definitions:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
remark:
|
||||
type: string
|
||||
type: object
|
||||
waf.RuleGroupView:
|
||||
properties:
|
||||
@@ -3919,8 +3905,6 @@ definitions:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
remark:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
@@ -3949,15 +3933,11 @@ definitions:
|
||||
type: integer
|
||||
domain:
|
||||
type: string
|
||||
remark:
|
||||
type: string
|
||||
type: object
|
||||
zone.Input:
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
remark:
|
||||
type: string
|
||||
type: object
|
||||
zone.ListItem:
|
||||
properties:
|
||||
@@ -3969,8 +3949,6 @@ definitions:
|
||||
type: integer
|
||||
id:
|
||||
type: integer
|
||||
remark:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
|
||||
@@ -338,7 +338,6 @@ export function buildPayloadFromRoute(
|
||||
cache_policy: route.cache_policy || 'url',
|
||||
cache_rules: route.cache_rule_list ?? [],
|
||||
custom_headers: route.custom_header_list ?? [],
|
||||
remark: route.remark || '',
|
||||
basic_auth_enabled: route.basic_auth_enabled,
|
||||
basic_auth_username: route.basic_auth_username,
|
||||
basic_auth_password: route.basic_auth_password,
|
||||
|
||||
@@ -11,7 +11,6 @@ import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, Form
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle,} from '@/components/ui/sheet';
|
||||
import {Switch} from '@/components/ui/switch';
|
||||
import {Textarea} from '@/components/ui/textarea';
|
||||
import type {ProxyRouteItem} from '@/lib/services/openflare';
|
||||
import {ProxyRouteService, ZoneService, zoneQueryKey} from '@/lib/services/openflare';
|
||||
|
||||
@@ -24,7 +23,6 @@ const createProxyRouteSchema = z
|
||||
zone_domain_ids: z.array(z.number().int().positive()).min(1, '请至少选择一个域名'),
|
||||
origin_url: z.string().trim().min(1, '请输入上游地址'),
|
||||
enabled: z.boolean(),
|
||||
remark: z.string().max(255, '备注不能超过 255 个字符'),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
try {
|
||||
@@ -52,7 +50,6 @@ const defaultValues: CreateProxyRouteFormValues = {
|
||||
zone_domain_ids: [],
|
||||
origin_url: '',
|
||||
enabled: true,
|
||||
remark: '',
|
||||
};
|
||||
|
||||
interface ProxyRouteCreateSheetProps {
|
||||
@@ -120,7 +117,6 @@ export function ProxyRouteCreateSheet({
|
||||
cache_rules: [],
|
||||
custom_headers: [],
|
||||
basic_auth_enabled: false,
|
||||
remark: values.remark.trim(),
|
||||
upstream_type: 'direct',
|
||||
});
|
||||
|
||||
@@ -174,10 +170,13 @@ export function ProxyRouteCreateSheet({
|
||||
domains={domainsQuery.data ?? []}
|
||||
zones={zonesQuery.data ?? []}
|
||||
disabled={domainsQuery.isLoading}
|
||||
onDomainCreated={async () => {
|
||||
await domainsQuery.refetch();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
域名与证书在 Zone 中维护;此处只选择本站点要绑定的 FQDN。
|
||||
勾选已登记域名,或使用「快捷新增域名」创建后自动勾选。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -214,20 +213,6 @@ export function ProxyRouteCreateSheet({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remark"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>备注</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="可选备注" rows={3} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{form.formState.errors.root ? (
|
||||
<p className="text-sm text-destructive">{form.formState.errors.root.message}</p>
|
||||
) : null}
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
import {useMemo, useState} from 'react';
|
||||
import Link from 'next/link';
|
||||
import {ExternalLink} from 'lucide-react';
|
||||
import {ExternalLink, Plus} from 'lucide-react';
|
||||
|
||||
import {Badge} from '@/components/ui/badge';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Checkbox} from '@/components/ui/checkbox';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {cn} from '@/lib/utils';
|
||||
import type {ZoneDomainItem, ZoneItem} from '@/lib/services/openflare';
|
||||
|
||||
import {QuickCreateZoneDomainDialog} from '../../websites/components/quick-create-zone-domain-dialog';
|
||||
|
||||
export interface ZoneDomainSelectorProps {
|
||||
value: number[];
|
||||
onChange: (ids: number[]) => void;
|
||||
@@ -19,6 +22,8 @@ export interface ZoneDomainSelectorProps {
|
||||
currentRouteId?: number | null;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
/** Called after a domain is created so parent can refresh the catalog. */
|
||||
onDomainCreated?: (domain: ZoneDomainItem) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ZoneDomainSelector({
|
||||
@@ -29,17 +34,31 @@ export function ZoneDomainSelector({
|
||||
currentRouteId = null,
|
||||
disabled = false,
|
||||
className,
|
||||
onDomainCreated,
|
||||
}: ZoneDomainSelectorProps) {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const selected = useMemo(() => new Set(value), [value]);
|
||||
const zoneMap = useMemo(
|
||||
() => new Map(zones.map((zone) => [zone.id, zone.domain])),
|
||||
[zones],
|
||||
);
|
||||
|
||||
/** Hide domains already bound to another route; keep unbound + current route. */
|
||||
const availableDomains = useMemo(
|
||||
() =>
|
||||
domains.filter(
|
||||
(domain) =>
|
||||
domain.proxy_route_id == null || domain.proxy_route_id === currentRouteId,
|
||||
),
|
||||
[currentRouteId, domains],
|
||||
);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const normalized = keyword.trim().toLowerCase();
|
||||
const list = [...domains].sort((a, b) => a.domain.localeCompare(b.domain));
|
||||
const list = [...availableDomains].sort((a, b) =>
|
||||
a.domain.localeCompare(b.domain),
|
||||
);
|
||||
if (!normalized) {
|
||||
return list;
|
||||
}
|
||||
@@ -51,16 +70,16 @@ export function ZoneDomainSelector({
|
||||
String(domain.id).includes(normalized)
|
||||
);
|
||||
});
|
||||
}, [domains, keyword, zoneMap]);
|
||||
}, [availableDomains, keyword, zoneMap]);
|
||||
|
||||
const toggle = (domain: ZoneDomainItem) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
const boundElsewhere =
|
||||
if (
|
||||
domain.proxy_route_id != null &&
|
||||
domain.proxy_route_id !== currentRouteId;
|
||||
if (boundElsewhere) {
|
||||
domain.proxy_route_id !== currentRouteId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,12 +92,26 @@ export function ZoneDomainSelector({
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
<Input
|
||||
value={keyword}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
placeholder="搜索域名或 Zone…"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Input
|
||||
value={keyword}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
placeholder="搜索域名或 Zone…"
|
||||
disabled={disabled}
|
||||
className="sm:flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-8 shrink-0 text-xs"
|
||||
disabled={disabled}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="mr-1 size-3.5" />
|
||||
快捷新增域名
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
@@ -88,8 +121,10 @@ export function ZoneDomainSelector({
|
||||
<Link href="/websites" className="text-primary underline-offset-4 hover:underline">
|
||||
网站管理
|
||||
</Link>{' '}
|
||||
中添加 FQDN。
|
||||
中添加 FQDN,或使用「快捷新增域名」。
|
||||
</>
|
||||
) : availableDomains.length === 0 ? (
|
||||
'没有可绑定的域名(其余域名已绑定其他路由)。可用「快捷新增域名」创建。'
|
||||
) : (
|
||||
'没有匹配的域名'
|
||||
)}
|
||||
@@ -97,9 +132,6 @@ export function ZoneDomainSelector({
|
||||
) : (
|
||||
<div className="max-h-72 space-y-1 overflow-y-auto rounded-lg border p-2">
|
||||
{filtered.map((domain) => {
|
||||
const boundElsewhere =
|
||||
domain.proxy_route_id != null &&
|
||||
domain.proxy_route_id !== currentRouteId;
|
||||
const checked = selected.has(domain.id);
|
||||
const zoneRoot = zoneMap.get(domain.zone_id);
|
||||
|
||||
@@ -108,13 +140,12 @@ export function ZoneDomainSelector({
|
||||
key={domain.id}
|
||||
className={cn(
|
||||
'flex cursor-pointer items-start gap-3 rounded-md px-2 py-2 transition-colors hover:bg-muted/60',
|
||||
boundElsewhere && 'cursor-not-allowed opacity-60',
|
||||
checked && !boundElsewhere && 'bg-muted/40',
|
||||
checked && 'bg-muted/40',
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
disabled={disabled || boundElsewhere}
|
||||
disabled={disabled}
|
||||
onCheckedChange={() => toggle(domain)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
@@ -130,11 +161,6 @@ export function ZoneDomainSelector({
|
||||
无证书
|
||||
</Badge>
|
||||
)}
|
||||
{boundElsewhere ? (
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
路由 #{domain.proxy_route_id}
|
||||
</Badge>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
{zoneRoot ? (
|
||||
@@ -156,6 +182,19 @@ export function ZoneDomainSelector({
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<QuickCreateZoneDomainDialog
|
||||
open={createOpen}
|
||||
onOpenChange={setCreateOpen}
|
||||
zones={zones}
|
||||
onCreated={async (domain) => {
|
||||
// Auto-select the new domain for this route binding.
|
||||
if (!value.includes(domain.id)) {
|
||||
onChange([...value, domain.id].sort((a, b) => a - b));
|
||||
}
|
||||
await onDomainCreated?.(domain);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,6 @@ export function DomainSection({ route, onRouteUpdate, onSavingChange }: DomainSe
|
||||
proxy_route_id: route.id,
|
||||
domain: bound.domain,
|
||||
cert_id: bound.cert_id,
|
||||
remark: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
};
|
||||
@@ -177,10 +176,14 @@ export function DomainSection({ route, onRouteUpdate, onSavingChange }: DomainSe
|
||||
zones={zonesQuery.data ?? []}
|
||||
currentRouteId={route.id}
|
||||
disabled={domainsQuery.isLoading}
|
||||
onDomainCreated={async () => {
|
||||
await domainsQuery.refetch();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
仅选择绑定关系;证书与 FQDN 请到对应 Zone 详情页管理。
|
||||
从已登记域名中勾选绑定;可用「快捷新增域名」在 Zone 下创建 FQDN
|
||||
并自动勾选。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -35,7 +35,6 @@ const reverseProxySchema = z
|
||||
tunnel_target_protocol: z.enum(['http', 'https']).optional(),
|
||||
pages_project_id: z.string().optional(),
|
||||
custom_headers_text: z.string(),
|
||||
remark: z.string().max(255, '备注不能超过 255 个字符'),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
if (value.upstream_type === 'direct') {
|
||||
@@ -137,7 +136,6 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
(route.tunnel_target_protocol as 'http' | 'https') || 'http',
|
||||
pages_project_id: route.pages_project_id ? String(route.pages_project_id) : '',
|
||||
custom_headers_text: customHeadersToText(route.custom_header_list),
|
||||
remark: route.remark || '',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -152,7 +150,6 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
(route.tunnel_target_protocol as 'http' | 'https') || 'http',
|
||||
pages_project_id: route.pages_project_id ? String(route.pages_project_id) : '',
|
||||
custom_headers_text: customHeadersToText(route.custom_header_list),
|
||||
remark: route.remark || '',
|
||||
});
|
||||
}, [form, route]);
|
||||
|
||||
@@ -210,7 +207,6 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
origin_host: values.origin_host.trim(),
|
||||
upstreams,
|
||||
custom_headers: headers,
|
||||
remark: values.remark.trim(),
|
||||
upstream_type: values.upstream_type,
|
||||
tunnel_node_id:
|
||||
values.upstream_type === 'tunnel' && values.tunnel_id
|
||||
@@ -417,20 +413,6 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remark"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>备注</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="例如:多活回源,优先使用上海入口" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SectionShell>
|
||||
|
||||
@@ -72,7 +72,6 @@ export function ProxyRoutesPageClient() {
|
||||
getRoutePrimaryDomain(route),
|
||||
...getRouteDomainNames(route),
|
||||
route.origin_url,
|
||||
route.remark,
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
|
||||
@@ -81,7 +81,6 @@ export const emptyRuleGroupDraft: WAFRuleGroupPayload = {
|
||||
region_blacklist: [],
|
||||
pow_enabled: false,
|
||||
pow_config: defaultPowConfig,
|
||||
remark: '',
|
||||
};
|
||||
|
||||
export const defaultRuleModalState: RuleModalState = {
|
||||
@@ -147,7 +146,6 @@ export function buildRuleGroupDraft(group: WAFRuleGroup | null): WAFRuleGroupPay
|
||||
region_blacklist: group.region_blacklist ?? [],
|
||||
pow_enabled: group.pow_enabled ?? false,
|
||||
pow_config: group.pow_config ?? defaultPowConfig,
|
||||
remark: group.remark ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -293,7 +291,6 @@ export function buildIPGroupPayloadFromGroup(
|
||||
subscription_format: group.subscription_format ?? 'text',
|
||||
subscription_mapping_rule: group.subscription_mapping_rule ?? '',
|
||||
sync_interval_minutes: group.sync_interval_minutes || 1440,
|
||||
remark: group.remark ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ const ipGroupSchema = z
|
||||
subscription_format: z.enum(['text', 'json']),
|
||||
subscription_mapping_rule: z.string(),
|
||||
sync_interval_minutes: z.number().int().min(5),
|
||||
remark: z.string().max(500),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
if (value.type === 'subscription' && !value.subscription_url.trim()) {
|
||||
@@ -75,7 +74,6 @@ const defaultValues: IPGroupFormValues = {
|
||||
subscription_format: 'text',
|
||||
subscription_mapping_rule: '',
|
||||
sync_interval_minutes: 1440,
|
||||
remark: '',
|
||||
};
|
||||
|
||||
function buildFormValues(group: WAFIPGroup | null): IPGroupFormValues {
|
||||
@@ -90,7 +88,6 @@ function buildFormValues(group: WAFIPGroup | null): IPGroupFormValues {
|
||||
subscription_format: group.subscription_format ?? 'text',
|
||||
subscription_mapping_rule: group.subscription_mapping_rule ?? '',
|
||||
sync_interval_minutes: group.sync_interval_minutes || 1440,
|
||||
remark: group.remark ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,7 +106,6 @@ function buildPayload(values: IPGroupFormValues): WAFIPGroupPayload {
|
||||
subscription_format: values.subscription_format,
|
||||
subscription_mapping_rule: values.subscription_mapping_rule.trim(),
|
||||
sync_interval_minutes: values.sync_interval_minutes,
|
||||
remark: values.remark.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -247,19 +243,6 @@ export function IPGroupDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remark"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2">
|
||||
<FormLabel>备注</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{type === 'subscription' ? (
|
||||
|
||||
@@ -43,7 +43,6 @@ const ruleGroupSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
block_status_code: z.number().int().min(400).max(599),
|
||||
block_response_body: z.string(),
|
||||
remark: z.string().max(500, '备注不能超过 500 个字符'),
|
||||
pow_enabled: z.boolean(),
|
||||
});
|
||||
|
||||
@@ -82,7 +81,6 @@ export function RuleGroupSheet({
|
||||
enabled: true,
|
||||
block_status_code: 418,
|
||||
block_response_body: '',
|
||||
remark: '',
|
||||
pow_enabled: false,
|
||||
},
|
||||
});
|
||||
@@ -96,7 +94,6 @@ export function RuleGroupSheet({
|
||||
enabled: draft.enabled,
|
||||
block_status_code: draft.block_status_code,
|
||||
block_response_body: draft.block_response_body,
|
||||
remark: draft.remark,
|
||||
pow_enabled: draft.pow_enabled,
|
||||
});
|
||||
setRuleEntryOpen(false);
|
||||
@@ -148,7 +145,6 @@ export function RuleGroupSheet({
|
||||
enabled: values.enabled,
|
||||
block_status_code: values.block_status_code,
|
||||
block_response_body: values.block_response_body,
|
||||
remark: values.remark,
|
||||
pow_enabled: values.pow_enabled,
|
||||
};
|
||||
try {
|
||||
@@ -220,19 +216,6 @@ export function RuleGroupSheet({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remark"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2">
|
||||
<FormLabel>备注</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -1,152 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||
import {zodResolver} from '@hookform/resolvers/zod';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {Loader2} from 'lucide-react';
|
||||
import {toast} from 'sonner';
|
||||
import {z} from 'zod';
|
||||
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
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 {TlsCertificateService, ZoneDomainService} from '@/lib/services/openflare';
|
||||
|
||||
const schema = z.object({
|
||||
domain: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, '请输入完整域名')
|
||||
.refine((value) => !value.includes('*.'), 'Zone 域名不支持通配符'),
|
||||
cert_id: z.string(),
|
||||
remark: z.string().max(255),
|
||||
});
|
||||
|
||||
type Values = z.infer<typeof schema>;
|
||||
import {QuickCreateZoneDomainDialog} from '../../components/quick-create-zone-domain-dialog';
|
||||
|
||||
/** Zone 详情页添加域名(固定 Zone,支持简写 / @ / 完整 FQDN)。 */
|
||||
export function ZoneDomainDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
zoneId,
|
||||
zoneRoot,
|
||||
onSaved,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange(open: boolean): void;
|
||||
zoneId: number;
|
||||
zoneRoot: string;
|
||||
onSaved(): Promise<unknown> | void;
|
||||
}) {
|
||||
const form = useForm<Values>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {domain: '', cert_id: '', remark: ''},
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const certificatesQuery = useQuery({
|
||||
queryKey: ['openflare', 'tls-certificates'],
|
||||
queryFn: () => TlsCertificateService.list(),
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.reset({domain: '', cert_id: '', remark: ''});
|
||||
}
|
||||
}, [form, open]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (values: Values) =>
|
||||
ZoneDomainService.create(zoneId, {
|
||||
domain: values.domain.toLowerCase(),
|
||||
cert_id: values.cert_id ? Number(values.cert_id) : null,
|
||||
remark: values.remark.trim(),
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
toast.success('域名已添加');
|
||||
await Promise.all([
|
||||
onSaved(),
|
||||
queryClient.invalidateQueries({queryKey: ['openflare', 'zones']}),
|
||||
]);
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : '保存失败'),
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加 Zone 域名</DialogTitle>
|
||||
<DialogDescription>
|
||||
填写明确 FQDN;通配符仅可存在于所选证书的 SAN 中。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
id="zone-domain-create"
|
||||
className="space-y-4"
|
||||
onSubmit={form.handleSubmit((values) => mutation.mutate(values))}
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="fqdn">完整域名</Label>
|
||||
<Input id="fqdn" placeholder="api.arctel.de" {...form.register('domain')} />
|
||||
{form.formState.errors.domain ? (
|
||||
<p className="text-xs text-destructive">
|
||||
{form.formState.errors.domain.message}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>证书</Label>
|
||||
<Select
|
||||
value={form.watch('cert_id') || '__none'}
|
||||
onValueChange={(value) =>
|
||||
form.setValue('cert_id', value === '__none' ? '' : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="不绑定证书" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none">不绑定证书</SelectItem>
|
||||
{(certificatesQuery.data ?? []).map((certificate) => (
|
||||
<SelectItem key={certificate.id} value={String(certificate.id)}>
|
||||
{certificate.name} · {certificate.primary_domain}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="domain-remark">备注</Label>
|
||||
<Textarea id="domain-remark" rows={2} {...form.register('remark')} />
|
||||
</div>
|
||||
</form>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form="zone-domain-create"
|
||||
disabled={mutation.isPending}
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<Loader2 className="mr-1 size-4 animate-spin" />
|
||||
) : null}
|
||||
添加域名
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<QuickCreateZoneDomainDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
fixedZoneId={zoneId}
|
||||
fixedZoneRoot={zoneRoot}
|
||||
onCreated={async () => {
|
||||
await onSaved();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {ZoneDomainDialog} from './zone-domain-dialog';
|
||||
|
||||
export function ZoneDomainsTable({
|
||||
zoneId,
|
||||
zoneRoot,
|
||||
domains,
|
||||
certificates,
|
||||
routes,
|
||||
@@ -39,6 +40,7 @@ export function ZoneDomainsTable({
|
||||
onChanged,
|
||||
}: {
|
||||
zoneId: number;
|
||||
zoneRoot: string;
|
||||
domains: ZoneDomainItem[];
|
||||
certificates: TlsCertificateItem[];
|
||||
routes: ProxyRouteItem[];
|
||||
@@ -98,9 +100,6 @@ export function ZoneDomainsTable({
|
||||
<TableHead className="h-8 whitespace-nowrap py-2 min-w-[200px]">
|
||||
上游
|
||||
</TableHead>
|
||||
<TableHead className="h-8 whitespace-nowrap py-2 min-w-[120px]">
|
||||
备注
|
||||
</TableHead>
|
||||
<TableHead className="sticky right-0 z-10 h-8 w-[90px] bg-background py-2 text-center">
|
||||
操作
|
||||
</TableHead>
|
||||
@@ -160,9 +159,6 @@ export function ZoneDomainsTable({
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[160px] truncate py-1 font-mono text-[10px] text-muted-foreground">
|
||||
{domain.remark || '—'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="sticky right-0 z-10 bg-background py-1 text-center"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
@@ -220,6 +216,7 @@ export function ZoneDomainsTable({
|
||||
open={createOpen}
|
||||
onOpenChange={setCreateOpen}
|
||||
zoneId={zoneId}
|
||||
zoneRoot={zoneRoot}
|
||||
onSaved={onChanged}
|
||||
/>
|
||||
|
||||
|
||||
@@ -11,15 +11,78 @@ import {Button} from '@/components/ui/button'
|
||||
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog'
|
||||
import {Input} from '@/components/ui/input'
|
||||
import {Label} from '@/components/ui/label'
|
||||
import {Textarea} from '@/components/ui/textarea'
|
||||
import {ZoneService, zoneQueryKey, type ZoneItem} from '@/lib/services/openflare'
|
||||
|
||||
const schema = z.object({domain: z.string().trim().min(1, '请输入 Zone 根域').refine((value) => !/[*/?#@]|:\/\//.test(value), '请输入不含协议或通配符的根域'), remark: z.string().max(255)})
|
||||
const schema = z.object({
|
||||
domain: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, '请输入 Zone 根域')
|
||||
.refine((value) => !/[*/?#@]|:\/\//.test(value), '请输入不含协议或通配符的根域'),
|
||||
})
|
||||
type Values = z.infer<typeof schema>
|
||||
|
||||
export function ZoneEditorDialog({open, onOpenChange, zone}: {open: boolean; onOpenChange(open: boolean): void; zone?: ZoneItem | null}) {
|
||||
const queryClient = useQueryClient(); const form = useForm<Values>({resolver: zodResolver(schema), defaultValues: {domain: '', remark: ''}})
|
||||
useEffect(() => { if (open) form.reset({domain: zone?.domain ?? '', remark: zone?.remark ?? ''}) }, [form, open, zone])
|
||||
const mutation = useMutation({mutationFn: (values: Values) => zone ? ZoneService.update(zone.id, values) : ZoneService.create(values), onSuccess: async () => { toast.success(zone ? 'Zone 已更新' : 'Zone 已创建'); await queryClient.invalidateQueries({queryKey: zoneQueryKey}); onOpenChange(false) }, onError: (error) => toast.error(error instanceof Error ? error.message : '保存失败')})
|
||||
return <Dialog open={open} onOpenChange={onOpenChange}><DialogContent><DialogHeader><DialogTitle>{zone ? '编辑 Zone' : '新增 Zone'}</DialogTitle><DialogDescription>Zone 仅接受可注册根域,例如 arctel.de。</DialogDescription></DialogHeader><form id="zone-editor" className="space-y-4" onSubmit={form.handleSubmit((values) => mutation.mutate({domain: values.domain.toLowerCase(), remark: values.remark.trim()}))}><div className="space-y-1.5"><Label htmlFor="zone-domain">根域</Label><Input id="zone-domain" placeholder="arctel.de" {...form.register('domain')} />{form.formState.errors.domain && <p className="text-xs text-destructive">{form.formState.errors.domain.message}</p>}</div><div className="space-y-1.5"><Label htmlFor="zone-remark">备注</Label><Textarea id="zone-remark" rows={2} {...form.register('remark')} /></div></form><DialogFooter><Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button><Button form="zone-editor" type="submit" disabled={mutation.isPending}>{mutation.isPending && <Loader2 className="mr-1 size-4 animate-spin" />}{zone ? '保存修改' : '新增 Zone'}</Button></DialogFooter></DialogContent></Dialog>
|
||||
export function ZoneEditorDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
zone,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange(open: boolean): void
|
||||
zone?: ZoneItem | null
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
const form = useForm<Values>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {domain: ''},
|
||||
})
|
||||
useEffect(() => {
|
||||
if (open) form.reset({domain: zone?.domain ?? ''})
|
||||
}, [form, open, zone])
|
||||
const mutation = useMutation({
|
||||
mutationFn: (values: Values) =>
|
||||
zone
|
||||
? ZoneService.update(zone.id, {domain: values.domain.toLowerCase()})
|
||||
: ZoneService.create({domain: values.domain.toLowerCase()}),
|
||||
onSuccess: async () => {
|
||||
toast.success(zone ? 'Zone 已更新' : 'Zone 已创建')
|
||||
await queryClient.invalidateQueries({queryKey: zoneQueryKey})
|
||||
onOpenChange(false)
|
||||
},
|
||||
onError: (error) => toast.error(error instanceof Error ? error.message : '保存失败'),
|
||||
})
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{zone ? '编辑 Zone' : '新增 Zone'}</DialogTitle>
|
||||
<DialogDescription>Zone 仅接受可注册根域,例如 arctel.de。</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
id="zone-editor"
|
||||
className="space-y-4"
|
||||
onSubmit={form.handleSubmit((values) =>
|
||||
mutation.mutate({domain: values.domain.toLowerCase()}),
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="zone-domain">根域</Label>
|
||||
<Input id="zone-domain" placeholder="arctel.de" {...form.register('domain')} />
|
||||
{form.formState.errors.domain && (
|
||||
<p className="text-xs text-destructive">{form.formState.errors.domain.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button form="zone-editor" type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending && <Loader2 className="mr-1 size-4 animate-spin" />}
|
||||
{zone ? '保存修改' : '新增 Zone'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@ export function ZoneOverviewPanel({overview}: {overview: ZoneOverview}) {
|
||||
<Card className="shadow-none"><CardHeader className="pb-2"><CardTitle className="text-sm">域名</CardTitle><CardDescription>显式 FQDN</CardDescription></CardHeader><CardContent className="text-2xl font-semibold">{overview.domains.length}</CardContent></Card>
|
||||
<Card className="shadow-none"><CardHeader className="pb-2"><CardTitle className="text-sm">关联路由</CardTitle><CardDescription>已绑定的不同规则</CardDescription></CardHeader><CardContent className="text-2xl font-semibold">{boundRoutes}</CardContent></Card>
|
||||
<Card className="shadow-none"><CardHeader className="pb-2"><CardTitle className="text-sm">已配证书</CardTitle><CardDescription>域名级 TLS 绑定</CardDescription></CardHeader><CardContent className="text-2xl font-semibold">{certificates}</CardContent></Card>
|
||||
<Card className="shadow-none md:col-span-3"><CardHeader className="pb-2"><CardTitle className="text-sm">Zone 信息</CardTitle></CardHeader><CardContent className="grid gap-3 text-sm sm:grid-cols-2"><div><p className="text-xs text-muted-foreground">根域</p><p className="mt-1 font-medium">{overview.zone.domain}</p></div><div><p className="text-xs text-muted-foreground">创建时间</p><p className="mt-1">{formatDateTime(overview.zone.created_at)}</p></div><div className="sm:col-span-2"><p className="text-xs text-muted-foreground">备注</p><p className="mt-1">{overview.zone.remark || '暂无备注'}</p></div></CardContent></Card>
|
||||
<Card className="shadow-none md:col-span-3"><CardHeader className="pb-2"><CardTitle className="text-sm">Zone 信息</CardTitle></CardHeader><CardContent className="grid gap-3 text-sm sm:grid-cols-2"><div><p className="text-xs text-muted-foreground">根域</p><p className="mt-1 font-medium">{overview.zone.domain}</p></div><div><p className="text-xs text-muted-foreground">创建时间</p><p className="mt-1">{formatDateTime(overview.zone.created_at)}</p></div></CardContent></Card>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ export function ZonePageClient({zoneId}: {zoneId: number}) {
|
||||
<TabsContent value="domains">
|
||||
<ZoneDomainsTable
|
||||
zoneId={zoneId}
|
||||
zoneRoot={overview.zone.domain}
|
||||
domains={overview.domains}
|
||||
certificates={certificates}
|
||||
routes={routes}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect, useMemo} from 'react';
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||
import {zodResolver} from '@hookform/resolvers/zod';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {Loader2} from 'lucide-react';
|
||||
import {toast} from 'sonner';
|
||||
import {z} from 'zod';
|
||||
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select';
|
||||
import {
|
||||
TlsCertificateService,
|
||||
ZoneDomainService,
|
||||
ZoneService,
|
||||
zoneQueryKey,
|
||||
type ZoneDomainItem,
|
||||
type ZoneItem,
|
||||
} from '@/lib/services/openflare';
|
||||
|
||||
import {previewZoneDomainInput, resolveZoneDomainInput} from './resolve-zone-domain-input';
|
||||
|
||||
const schema = z.object({
|
||||
zone_id: z.string().min(1, '请选择 Zone'),
|
||||
domain_input: z.string().trim().min(1, '请输入域名'),
|
||||
cert_id: z.string(),
|
||||
});
|
||||
|
||||
type Values = z.infer<typeof schema>;
|
||||
|
||||
export function QuickCreateZoneDomainDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
fixedZoneId,
|
||||
fixedZoneRoot,
|
||||
zones: zonesProp,
|
||||
onCreated,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange(open: boolean): void;
|
||||
/** When set, Zone 选择器隐藏 */
|
||||
fixedZoneId?: number;
|
||||
fixedZoneRoot?: string;
|
||||
zones?: ZoneItem[];
|
||||
onCreated(domain: ZoneDomainItem): void | Promise<void>;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const zonesQuery = useQuery({
|
||||
queryKey: zoneQueryKey,
|
||||
queryFn: () => ZoneService.list(),
|
||||
enabled: open && !fixedZoneId && !zonesProp,
|
||||
});
|
||||
const certificatesQuery = useQuery({
|
||||
queryKey: ['openflare', 'tls-certificates'],
|
||||
queryFn: () => TlsCertificateService.list(),
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const zones = useMemo(
|
||||
() => zonesProp ?? zonesQuery.data ?? [],
|
||||
[zonesProp, zonesQuery.data],
|
||||
);
|
||||
const fixedZone = useMemo(() => {
|
||||
if (!fixedZoneId) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
zones.find((zone) => zone.id === fixedZoneId) ??
|
||||
(fixedZoneRoot
|
||||
? ({id: fixedZoneId, domain: fixedZoneRoot, created_at: '', updated_at: ''} as ZoneItem)
|
||||
: undefined)
|
||||
);
|
||||
}, [fixedZoneId, fixedZoneRoot, zones]);
|
||||
|
||||
const form = useForm<Values>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
zone_id: fixedZoneId ? String(fixedZoneId) : '',
|
||||
domain_input: '',
|
||||
cert_id: '',
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
form.reset({
|
||||
zone_id: fixedZoneId ? String(fixedZoneId) : '',
|
||||
domain_input: '',
|
||||
cert_id: '',
|
||||
});
|
||||
}, [fixedZoneId, form, open]);
|
||||
|
||||
const watchedZoneId = form.watch('zone_id');
|
||||
const watchedInput = form.watch('domain_input');
|
||||
const selectedZone = useMemo(() => {
|
||||
if (fixedZone) {
|
||||
return fixedZone;
|
||||
}
|
||||
const id = Number(watchedZoneId);
|
||||
return zones.find((zone) => zone.id === id);
|
||||
}, [fixedZone, watchedZoneId, zones]);
|
||||
|
||||
const preview = selectedZone
|
||||
? previewZoneDomainInput(watchedInput, selectedZone.domain)
|
||||
: '';
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: Values) => {
|
||||
const zoneId = Number(values.zone_id);
|
||||
const zone =
|
||||
fixedZone ?? zones.find((item) => item.id === zoneId);
|
||||
if (!zone) {
|
||||
throw new Error('请选择 Zone');
|
||||
}
|
||||
const resolved = resolveZoneDomainInput(values.domain_input, zone.domain);
|
||||
if (resolved.error || !resolved.domain) {
|
||||
throw new Error(resolved.error || '域名格式不合法');
|
||||
}
|
||||
return ZoneDomainService.create(zone.id, {
|
||||
domain: resolved.domain,
|
||||
cert_id: values.cert_id ? Number(values.cert_id) : null,
|
||||
});
|
||||
},
|
||||
onSuccess: async (domain) => {
|
||||
toast.success('域名已添加', {description: domain.domain});
|
||||
await Promise.all([
|
||||
onCreated(domain),
|
||||
queryClient.invalidateQueries({queryKey: zoneQueryKey}),
|
||||
queryClient.invalidateQueries({queryKey: [...zoneQueryKey, 'all-domains']}),
|
||||
]);
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : '添加失败'),
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>快捷新增域名</DialogTitle>
|
||||
<DialogDescription>
|
||||
选择 Zone 后输入:子域名称(如 api)、@(根域)或完整 FQDN。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form
|
||||
id="quick-create-zone-domain"
|
||||
className="space-y-4"
|
||||
onSubmit={form.handleSubmit((values) => {
|
||||
if (!selectedZone) {
|
||||
form.setError('zone_id', {message: '请选择 Zone'});
|
||||
return;
|
||||
}
|
||||
const resolved = resolveZoneDomainInput(
|
||||
values.domain_input,
|
||||
selectedZone.domain,
|
||||
);
|
||||
if (resolved.error) {
|
||||
form.setError('domain_input', {message: resolved.error});
|
||||
return;
|
||||
}
|
||||
mutation.mutate(values);
|
||||
})}
|
||||
>
|
||||
{!fixedZoneId ? (
|
||||
<div className="space-y-1.5">
|
||||
<Label>Zone</Label>
|
||||
<Select
|
||||
value={form.watch('zone_id') || undefined}
|
||||
onValueChange={(value) => form.setValue('zone_id', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择注册根域" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{zones.map((zone) => (
|
||||
<SelectItem key={zone.id} value={String(zone.id)}>
|
||||
{zone.domain}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{form.formState.errors.zone_id ? (
|
||||
<p className="text-xs text-destructive">
|
||||
{form.formState.errors.zone_id.message}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border bg-muted/30 px-3 py-2 text-sm">
|
||||
Zone:
|
||||
<span className="ml-1 font-medium">
|
||||
{fixedZoneRoot || fixedZone?.domain || `#${fixedZoneId}`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="domain-input">域名</Label>
|
||||
<Input
|
||||
id="domain-input"
|
||||
placeholder={
|
||||
selectedZone
|
||||
? `api 或 @ 或 api.${selectedZone.domain}`
|
||||
: 'api / @ / 完整域名'
|
||||
}
|
||||
{...form.register('domain_input')}
|
||||
/>
|
||||
{preview ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
将创建:
|
||||
<code className="ml-1 rounded bg-muted px-1 py-0.5 font-mono text-[11px]">
|
||||
{preview}
|
||||
</code>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
示例:输入 <code className="font-mono">api</code> →{' '}
|
||||
<code className="font-mono">api.zone.com</code>;
|
||||
<code className="font-mono">@</code> → 根域本身
|
||||
</p>
|
||||
)}
|
||||
{form.formState.errors.domain_input ? (
|
||||
<p className="text-xs text-destructive">
|
||||
{form.formState.errors.domain_input.message}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>证书(可选)</Label>
|
||||
<Select
|
||||
value={form.watch('cert_id') || '__none'}
|
||||
onValueChange={(value) =>
|
||||
form.setValue('cert_id', value === '__none' ? '' : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="不绑定证书" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none">不绑定证书</SelectItem>
|
||||
{(certificatesQuery.data ?? []).map((certificate) => (
|
||||
<SelectItem key={certificate.id} value={String(certificate.id)}>
|
||||
{certificate.name} · {certificate.primary_domain}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form="quick-create-zone-domain"
|
||||
disabled={mutation.isPending}
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<Loader2 className="mr-1 size-4 animate-spin" />
|
||||
) : null}
|
||||
添加域名
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Resolve user input into a full FQDN under a Zone root.
|
||||
*
|
||||
* - `@` → apex (zone root itself), e.g. `example.com`
|
||||
* - single label `name` → `name.example.com`
|
||||
* - full FQDN must equal the root or end with `.root`
|
||||
*/
|
||||
export function resolveZoneDomainInput(
|
||||
rawInput: string,
|
||||
zoneRoot: string,
|
||||
): {domain: string; error?: string} {
|
||||
const input = rawInput.trim().toLowerCase();
|
||||
const root = zoneRoot.trim().toLowerCase();
|
||||
|
||||
if (!root) {
|
||||
return {domain: '', error: '请先选择 Zone'};
|
||||
}
|
||||
if (!input) {
|
||||
return {domain: '', error: '请输入域名'};
|
||||
}
|
||||
if (input.includes('*')) {
|
||||
return {domain: '', error: 'Zone 域名不支持通配符'};
|
||||
}
|
||||
if (input.includes('://') || input.includes('/') || input.includes('?') || input.includes('#')) {
|
||||
return {domain: '', error: '域名格式不合法'};
|
||||
}
|
||||
|
||||
if (input === '@') {
|
||||
return {domain: root};
|
||||
}
|
||||
|
||||
// Short label: no dots → prefix under zone root
|
||||
if (!input.includes('.')) {
|
||||
if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(input)) {
|
||||
return {domain: '', error: '子域名称格式不合法'};
|
||||
}
|
||||
return {domain: `${input}.${root}`};
|
||||
}
|
||||
|
||||
// Full FQDN
|
||||
if (input === root || input.endsWith(`.${root}`)) {
|
||||
return {domain: input};
|
||||
}
|
||||
|
||||
return {domain: '', error: `域名必须属于 Zone ${root}`};
|
||||
}
|
||||
|
||||
/** Live preview string for the input helper text. */
|
||||
export function previewZoneDomainInput(rawInput: string, zoneRoot: string): string {
|
||||
const resolved = resolveZoneDomainInput(rawInput, zoneRoot);
|
||||
if (resolved.error || !resolved.domain) {
|
||||
return '';
|
||||
}
|
||||
return resolved.domain;
|
||||
}
|
||||
@@ -94,7 +94,6 @@ export default function WebsitesPage() {
|
||||
<h2 className="truncate text-sm font-semibold">{zone.domain}</h2>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{zone.domain_count ?? 0} 个域名
|
||||
{zone.remark ? ` · ${zone.remark}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -261,7 +261,6 @@ export interface ProxyRouteItem {
|
||||
basic_auth_enabled: boolean;
|
||||
basic_auth_username: string;
|
||||
basic_auth_password: string;
|
||||
remark: string;
|
||||
upstream_type: 'direct' | 'tunnel' | 'pages';
|
||||
tunnel_node_id?: number | null;
|
||||
tunnel_id?: number | null;
|
||||
@@ -296,7 +295,6 @@ export interface ProxyRouteMutationPayload {
|
||||
basic_auth_enabled: boolean;
|
||||
basic_auth_username?: string;
|
||||
basic_auth_password?: string;
|
||||
remark: string;
|
||||
upstream_type?: 'direct' | 'tunnel' | 'pages';
|
||||
tunnel_node_id?: number | null;
|
||||
tunnel_id?: number | null;
|
||||
@@ -708,7 +706,6 @@ export interface WAFRuleGroup {
|
||||
region_blacklist: string[];
|
||||
pow_enabled: boolean;
|
||||
pow_config: ProxyRoutePoWConfig;
|
||||
remark: string;
|
||||
applied_site_ids: number[];
|
||||
applied_site_count: number;
|
||||
created_at: string;
|
||||
@@ -730,7 +727,6 @@ export interface WAFRuleGroupPayload {
|
||||
region_blacklist: string[];
|
||||
pow_enabled: boolean;
|
||||
pow_config: ProxyRoutePoWConfig;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface WAFSiteRuleGroups {
|
||||
@@ -765,7 +761,6 @@ export interface WAFIPGroup {
|
||||
next_sync_at?: string;
|
||||
last_sync_status: string;
|
||||
last_sync_message: string;
|
||||
remark: string;
|
||||
referenced_by_rule_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -781,7 +776,6 @@ export interface WAFIPGroupPayload {
|
||||
subscription_format: WAFIPGroupSubscriptionFormat;
|
||||
subscription_mapping_rule: string;
|
||||
sync_interval_minutes: number;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface WAFIPGroupSyncResult {
|
||||
@@ -964,7 +958,6 @@ export interface DashboardOverviewCompact {
|
||||
export interface ZoneItem {
|
||||
id: number;
|
||||
domain: string;
|
||||
remark: string;
|
||||
/** Present on list API; may be omitted on nested zone objects. */
|
||||
domain_count?: number;
|
||||
created_at: string;
|
||||
@@ -973,7 +966,6 @@ export interface ZoneItem {
|
||||
|
||||
export interface ZoneMutationPayload {
|
||||
domain: string;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface ZoneDomainItem {
|
||||
@@ -982,7 +974,6 @@ export interface ZoneDomainItem {
|
||||
proxy_route_id: number | null;
|
||||
domain: string;
|
||||
cert_id: number | null;
|
||||
remark: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -990,7 +981,6 @@ export interface ZoneDomainItem {
|
||||
export interface ZoneDomainMutationPayload {
|
||||
domain: string;
|
||||
cert_id: number | null;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface ZoneOverview {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
import {resolveZoneDomainInput} from '@/app/(main)/websites/components/resolve-zone-domain-input';
|
||||
|
||||
describe('resolveZoneDomainInput', () => {
|
||||
it('maps short label and apex @ under zone root', () => {
|
||||
expect(resolveZoneDomainInput('api', 'example.com')).toEqual({
|
||||
domain: 'api.example.com',
|
||||
});
|
||||
expect(resolveZoneDomainInput('@', 'example.com')).toEqual({
|
||||
domain: 'example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts full FQDN under the zone', () => {
|
||||
expect(resolveZoneDomainInput('www.api.example.com', 'example.com')).toEqual({
|
||||
domain: 'www.api.example.com',
|
||||
});
|
||||
expect(resolveZoneDomainInput('example.com', 'example.com')).toEqual({
|
||||
domain: 'example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects foreign domains and wildcards', () => {
|
||||
expect(resolveZoneDomainInput('evil.com', 'example.com').error).toBeTruthy();
|
||||
expect(resolveZoneDomainInput('*.example.com', 'example.com').error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -31,7 +31,6 @@ describe('WebsitesPage', () => {
|
||||
{
|
||||
id: 42,
|
||||
domain: 'arctel.de',
|
||||
remark: '生产站点',
|
||||
domain_count: 3,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
@@ -39,7 +38,6 @@ describe('WebsitesPage', () => {
|
||||
{
|
||||
id: 43,
|
||||
domain: 'example.com',
|
||||
remark: '',
|
||||
domain_count: 0,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
||||
import {render, screen} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {describe, expect, it, vi} from 'vitest';
|
||||
@@ -12,7 +13,7 @@ vi.mock('next/link', () => ({
|
||||
}));
|
||||
|
||||
const zones: ZoneItem[] = [
|
||||
{id: 1, domain: 'arctel.de', remark: '', created_at: '', updated_at: ''},
|
||||
{id: 1, domain: 'arctel.de', created_at: '', updated_at: ''},
|
||||
];
|
||||
|
||||
const domains: ZoneDomainItem[] = [
|
||||
@@ -22,7 +23,6 @@ const domains: ZoneDomainItem[] = [
|
||||
proxy_route_id: null,
|
||||
domain: 'api.arctel.de',
|
||||
cert_id: 9,
|
||||
remark: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
},
|
||||
@@ -32,18 +32,22 @@ const domains: ZoneDomainItem[] = [
|
||||
proxy_route_id: 99,
|
||||
domain: 'bound.arctel.de',
|
||||
cert_id: null,
|
||||
remark: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
},
|
||||
];
|
||||
|
||||
function renderSelector(ui: React.ReactElement) {
|
||||
const client = new QueryClient({defaultOptions: {queries: {retry: false}}});
|
||||
return render(<QueryClientProvider client={client}>{ui}</QueryClientProvider>);
|
||||
}
|
||||
|
||||
describe('ZoneDomainSelector', () => {
|
||||
it('renders domains and toggles selection', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
|
||||
render(
|
||||
renderSelector(
|
||||
<ZoneDomainSelector
|
||||
value={[7]}
|
||||
onChange={onChange}
|
||||
@@ -53,29 +57,41 @@ describe('ZoneDomainSelector', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText('api.arctel.de')).toBeVisible();
|
||||
expect(screen.getByText('bound.arctel.de')).toBeVisible();
|
||||
expect(screen.getByText('路由 #99')).toBeVisible();
|
||||
// Bound to another route — hidden by default
|
||||
expect(screen.queryByText('bound.arctel.de')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', {name: /快捷新增域名/})).toBeVisible();
|
||||
|
||||
// Deselect already selected domain
|
||||
await user.click(screen.getByText('api.arctel.de'));
|
||||
expect(onChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('does not select domains bound to another route', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
|
||||
render(
|
||||
it('hides domains bound to another route', () => {
|
||||
renderSelector(
|
||||
<ZoneDomainSelector
|
||||
value={[]}
|
||||
onChange={onChange}
|
||||
onChange={vi.fn()}
|
||||
domains={domains}
|
||||
zones={zones}
|
||||
currentRouteId={1}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('bound.arctel.de'));
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
expect(screen.getByText('api.arctel.de')).toBeVisible();
|
||||
expect(screen.queryByText('bound.arctel.de')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('still shows domains already bound to the current route', () => {
|
||||
renderSelector(
|
||||
<ZoneDomainSelector
|
||||
value={[8]}
|
||||
onChange={vi.fn()}
|
||||
domains={domains}
|
||||
zones={zones}
|
||||
currentRouteId={99}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('bound.arctel.de')).toBeVisible();
|
||||
expect(screen.getByText('api.arctel.de')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('ZonePageClient', () => {
|
||||
|
||||
it('loads the overview by stable ID and exposes all tabs including empty domains', async () => {
|
||||
vi.mocked(ZoneService.getOverview).mockImplementation(async () => ({
|
||||
zone: {id: 42, domain: 'arctel.de', remark: '', created_at: '', updated_at: ''},
|
||||
zone: {id: 42, domain: 'arctel.de', created_at: '', updated_at: ''},
|
||||
domains: [],
|
||||
}));
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ type snapshotRoute struct {
|
||||
BasicAuthEnabled bool `json:"basic_auth_enabled,omitempty"`
|
||||
BasicAuthUsername string `json:"basic_auth_username,omitempty"`
|
||||
BasicAuthPassword string `json:"basic_auth_password,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
UpstreamType string `json:"upstream_type,omitempty"`
|
||||
TunnelNodeID *uint `json:"tunnel_node_id,omitempty"`
|
||||
TunnelTargetAddr string `json:"tunnel_target_addr,omitempty"`
|
||||
@@ -297,7 +296,6 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna
|
||||
BasicAuthEnabled: route.BasicAuthEnabled,
|
||||
BasicAuthUsername: route.BasicAuthUsername,
|
||||
BasicAuthPassword: route.BasicAuthPassword,
|
||||
Remark: route.Remark,
|
||||
UpstreamType: upstreamType,
|
||||
TunnelNodeID: tunnelNodeID,
|
||||
TunnelTargetAddr: tunnelTargetAddr,
|
||||
|
||||
@@ -114,7 +114,6 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
||||
"ip_whitelist": []string{"192.0.2.1"},
|
||||
"ip_blacklist": []string{"203.0.113.10"},
|
||||
"country_blacklist": []string{"CN"},
|
||||
"remark": "integration rule group",
|
||||
}, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
@@ -179,7 +178,6 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
||||
"name": "edge-security-updated",
|
||||
"enabled": true,
|
||||
"block_status_code": 451,
|
||||
"remark": "updated by integration test",
|
||||
},
|
||||
adminAuthHeaders(seed.Token),
|
||||
)
|
||||
@@ -197,7 +195,6 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
||||
"type": "manual",
|
||||
"enabled": true,
|
||||
"ip_list": []string{"203.0.113.0/24", "198.51.100.10"},
|
||||
"remark": "manual deny list",
|
||||
}, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
@@ -306,7 +303,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) {
|
||||
assert.NotEmpty(t, errResp.ErrorMsg)
|
||||
|
||||
rec = performJSONRequest(t, engine, http.MethodPost, fmt.Sprintf("%s/zones/%d/domains", apiPath(""), zoneID), map[string]any{
|
||||
"domain": "security.example.com", "cert_id": certID, "remark": "primary security domain",
|
||||
"domain": "security.example.com", "cert_id": certID,
|
||||
}, adminAuthHeaders(seed.Token))
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ func populateProxyRouteFields(
|
||||
jsonFields *proxyRouteJSONFields,
|
||||
originID *uint,
|
||||
upstreams []string,
|
||||
originHost, remark, cachePolicy string,
|
||||
originHost, cachePolicy string,
|
||||
limitConnPerServer, limitConnPerIP int,
|
||||
limitRate, upstreamType string,
|
||||
) {
|
||||
@@ -110,7 +110,6 @@ func populateProxyRouteFields(
|
||||
route.BasicAuthEnabled = input.BasicAuthEnabled
|
||||
route.BasicAuthUsername = input.BasicAuthUsername
|
||||
route.BasicAuthPassword = input.BasicAuthPassword
|
||||
route.Remark = remark
|
||||
route.UpstreamType = upstreamType
|
||||
}
|
||||
|
||||
@@ -151,7 +150,7 @@ func updateProxyRouteRecord(tx *gorm.DB, route *model.ProxyRoute) error {
|
||||
"limit_rate": route.LimitRate, "cache_enabled": route.CacheEnabled, "cache_policy": route.CachePolicy,
|
||||
"cache_rules": route.CacheRules, "custom_headers": route.CustomHeaders,
|
||||
"basic_auth_enabled": route.BasicAuthEnabled, "basic_auth_username": route.BasicAuthUsername,
|
||||
"basic_auth_password": route.BasicAuthPassword, "remark": route.Remark,
|
||||
"basic_auth_password": route.BasicAuthPassword,
|
||||
"upstream_type": route.UpstreamType, "tunnel_node_id": route.TunnelNodeID,
|
||||
"tunnel_target_addr": route.TunnelTargetAddr, "tunnel_target_protocol": route.TunnelTargetProtocol,
|
||||
"pages_project_id": route.PagesProjectID,
|
||||
|
||||
@@ -45,7 +45,6 @@ type Input struct {
|
||||
BasicAuthEnabled bool `json:"basic_auth_enabled"`
|
||||
BasicAuthUsername string `json:"basic_auth_username"`
|
||||
BasicAuthPassword string `json:"basic_auth_password"`
|
||||
Remark string `json:"remark"`
|
||||
UpstreamType string `json:"upstream_type"`
|
||||
TunnelNodeID *uint `json:"tunnel_node_id"`
|
||||
TunnelID *uint `json:"tunnel_id"`
|
||||
@@ -80,7 +79,6 @@ type View struct {
|
||||
BasicAuthEnabled bool `json:"basic_auth_enabled"`
|
||||
BasicAuthUsername string `json:"basic_auth_username"`
|
||||
BasicAuthPassword string `json:"basic_auth_password"`
|
||||
Remark string `json:"remark"`
|
||||
UpstreamType string `json:"upstream_type"`
|
||||
TunnelNodeID *uint `json:"tunnel_node_id"`
|
||||
TunnelID *uint `json:"tunnel_id"`
|
||||
@@ -187,7 +185,6 @@ func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input)
|
||||
return nil, nil, err
|
||||
}
|
||||
originHost := strings.TrimSpace(input.OriginHost)
|
||||
remark := strings.TrimSpace(input.Remark)
|
||||
cachePolicy := strings.TrimSpace(input.CachePolicy)
|
||||
cacheRules, err := normalizeCacheRules(input.CacheEnabled, cachePolicy, input.CacheRules)
|
||||
if err != nil {
|
||||
@@ -245,7 +242,6 @@ func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input)
|
||||
originID,
|
||||
upstreams,
|
||||
originHost,
|
||||
remark,
|
||||
cachePolicy,
|
||||
limitConnPerServer,
|
||||
limitConnPerIP,
|
||||
@@ -321,7 +317,6 @@ func buildProxyRouteView(ctx context.Context, route *model.ProxyRoute) (*View, e
|
||||
BasicAuthEnabled: route.BasicAuthEnabled,
|
||||
BasicAuthUsername: route.BasicAuthUsername,
|
||||
BasicAuthPassword: route.BasicAuthPassword,
|
||||
Remark: route.Remark,
|
||||
UpstreamType: route.UpstreamType,
|
||||
TunnelNodeID: route.TunnelNodeID,
|
||||
TunnelID: route.TunnelNodeID,
|
||||
|
||||
@@ -55,7 +55,6 @@ type RuleGroupInput struct {
|
||||
CountryBlacklist []string `json:"country_blacklist"`
|
||||
RegionWhitelist []string `json:"region_whitelist"`
|
||||
RegionBlacklist []string `json:"region_blacklist"`
|
||||
Remark string `json:"remark"`
|
||||
PoWEnabled bool `json:"pow_enabled"`
|
||||
PoWConfig json.RawMessage `json:"pow_config"`
|
||||
}
|
||||
@@ -95,7 +94,6 @@ type RuleGroupView struct {
|
||||
CountryBlacklist []string `json:"country_blacklist"`
|
||||
RegionWhitelist []string `json:"region_whitelist"`
|
||||
RegionBlacklist []string `json:"region_blacklist"`
|
||||
Remark string `json:"remark"`
|
||||
PoWEnabled bool `json:"pow_enabled"`
|
||||
PoWConfig *PoWConfig `json:"pow_config"`
|
||||
AppliedSiteIDs []uint `json:"applied_site_ids"`
|
||||
@@ -129,7 +127,6 @@ type IPGroupInput struct {
|
||||
SubscriptionFormat string `json:"subscription_format"`
|
||||
SubscriptionMappingRule string `json:"subscription_mapping_rule"`
|
||||
SyncIntervalMinutes int `json:"sync_interval_minutes"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// IPGroupExtIPView is an external IP entry in API responses.
|
||||
@@ -155,7 +152,6 @@ type IPGroupView struct {
|
||||
NextSyncAt string `json:"next_sync_at,omitempty"`
|
||||
LastSyncStatus string `json:"last_sync_status"`
|
||||
LastSyncMessage string `json:"last_sync_message"`
|
||||
Remark string `json:"remark"`
|
||||
ReferencedByRuleCount int `json:"referenced_by_rule_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
@@ -597,7 +593,6 @@ func buildRuleGroup(ctx context.Context, group *model.OpenFlareWAFRuleGroup, inp
|
||||
group.RegionBlacklist = string(regionBlacklistJSON)
|
||||
group.PoWEnabled = input.PoWEnabled
|
||||
group.PoWConfig = string(powConfigJSON)
|
||||
group.Remark = strings.TrimSpace(input.Remark)
|
||||
return group, nil
|
||||
}
|
||||
|
||||
@@ -613,7 +608,6 @@ func buildRuleGroupView(group *model.OpenFlareWAFRuleGroup, appliedSiteIDs []uin
|
||||
IsGlobal: group.IsGlobal,
|
||||
BlockStatusCode: group.BlockStatusCode,
|
||||
BlockResponseBody: group.BlockResponseBody,
|
||||
Remark: group.Remark,
|
||||
PoWEnabled: group.PoWEnabled,
|
||||
AppliedSiteIDs: appliedSiteIDs,
|
||||
AppliedSiteCount: len(appliedSiteIDs),
|
||||
@@ -709,7 +703,6 @@ func buildIPGroup(group *model.OpenFlareWAFIPGroup, input IPGroupInput) (*model.
|
||||
group.SubscriptionMappingRule = mappingRule
|
||||
group.SyncIntervalMinutes = syncInterval
|
||||
group.NextSyncAt = nextIPGroupSyncAt(group.Type, group.Enabled, syncInterval, group.NextSyncAt)
|
||||
group.Remark = strings.TrimSpace(input.Remark)
|
||||
return group, nil
|
||||
}
|
||||
|
||||
@@ -750,7 +743,6 @@ func buildIPGroupView(group *model.OpenFlareWAFIPGroup, referenceCount int) (IPG
|
||||
SyncIntervalMinutes: group.SyncIntervalMinutes,
|
||||
LastSyncStatus: group.LastSyncStatus,
|
||||
LastSyncMessage: group.LastSyncMessage,
|
||||
Remark: group.Remark,
|
||||
ReferencedByRuleCount: referenceCount,
|
||||
CreatedAt: group.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: group.UpdatedAt.Format(time.RFC3339),
|
||||
|
||||
@@ -100,7 +100,6 @@ func TestUpdateIPGroupPrunesAutomaticExtIPs(t *testing.T) {
|
||||
Enabled: created.Enabled,
|
||||
IPList: []string{"203.0.113.10"},
|
||||
AutoConfig: created.AutoConfig,
|
||||
Remark: created.Remark,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, updated.IPList, 1)
|
||||
|
||||
@@ -14,5 +14,5 @@ const (
|
||||
errDomainExists = "域名已存在"
|
||||
errCertificateNotFound = "所选证书不存在"
|
||||
errDomainBoundToRoute = "域名已绑定反代路由,请先解除绑定"
|
||||
errZoneHasDomains = "Zone 下仍有域名,请先删除全部域名"
|
||||
errZoneHasDomains = "根域下仍有域名,请先删除全部域名"
|
||||
)
|
||||
|
||||
@@ -33,7 +33,6 @@ func (r ImportReport) LogAndReturn(err error) error {
|
||||
type legacyDomain struct {
|
||||
Domain string
|
||||
CertID *uint
|
||||
Remark string
|
||||
ProxyRouteID *uint
|
||||
}
|
||||
|
||||
@@ -139,9 +138,9 @@ func ImportLegacyTx(ctx context.Context, tx *sql.Tx, postgres bool) (report Impo
|
||||
}
|
||||
|
||||
if _, insErr := tx.ExecContext(ctx, q(`
|
||||
INSERT INTO of_zone_domains (zone_id, proxy_route_id, domain, cert_id, remark, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`), zoneID, nullableUint(item.ProxyRouteID), domain, nullableUint(item.CertID), item.Remark); insErr != nil {
|
||||
INSERT INTO of_zone_domains (zone_id, proxy_route_id, domain, cert_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`), zoneID, nullableUint(item.ProxyRouteID), domain, nullableUint(item.CertID)); insErr != nil {
|
||||
return report, insErr
|
||||
}
|
||||
report.Domains++
|
||||
@@ -169,8 +168,8 @@ func ensureZone(
|
||||
return 0, err
|
||||
}
|
||||
if _, execErr := tx.ExecContext(ctx, q(`
|
||||
INSERT INTO of_zones (domain, remark, created_at, updated_at)
|
||||
VALUES (?, '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
INSERT INTO of_zones (domain, created_at, updated_at)
|
||||
VALUES (?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`), root); execErr != nil {
|
||||
return 0, execErr
|
||||
}
|
||||
@@ -187,12 +186,12 @@ func collectLegacyRouteDomainsImpl(
|
||||
q func(string) string,
|
||||
) (items []legacyDomain, hasRouteDomains bool, conflicts []string, err error) {
|
||||
// Probe domain_cert_ids: if SELECT fails, fall back without it.
|
||||
queryWithCert := q(`SELECT id, domain, domains, COALESCE(domain_cert_ids, '[]'), remark FROM of_proxy_routes`)
|
||||
queryWithCert := q(`SELECT id, domain, domains, COALESCE(domain_cert_ids, '[]') FROM of_proxy_routes`)
|
||||
rows, err := tx.QueryContext(ctx, queryWithCert)
|
||||
useCert := true
|
||||
if err != nil {
|
||||
useCert = false
|
||||
rows, err = tx.QueryContext(ctx, q(`SELECT id, domain, domains, remark FROM of_proxy_routes`))
|
||||
rows, err = tx.QueryContext(ctx, q(`SELECT id, domain, domains FROM of_proxy_routes`))
|
||||
if err != nil {
|
||||
return nil, false, nil, err
|
||||
}
|
||||
@@ -205,14 +204,13 @@ func collectLegacyRouteDomainsImpl(
|
||||
domain string
|
||||
domains string
|
||||
certIDs string
|
||||
remark string
|
||||
)
|
||||
if useCert {
|
||||
if err := rows.Scan(&id, &domain, &domains, &certIDs, &remark); err != nil {
|
||||
if err := rows.Scan(&id, &domain, &domains, &certIDs); err != nil {
|
||||
return nil, false, nil, err
|
||||
}
|
||||
} else {
|
||||
if err := rows.Scan(&id, &domain, &domains, &remark); err != nil {
|
||||
if err := rows.Scan(&id, &domain, &domains); err != nil {
|
||||
return nil, false, nil, err
|
||||
}
|
||||
certIDs = "[]"
|
||||
@@ -236,7 +234,6 @@ func collectLegacyRouteDomainsImpl(
|
||||
items = append(items, legacyDomain{
|
||||
Domain: d,
|
||||
CertID: certID,
|
||||
Remark: remark,
|
||||
ProxyRouteID: &routeID,
|
||||
})
|
||||
}
|
||||
@@ -245,7 +242,7 @@ func collectLegacyRouteDomainsImpl(
|
||||
}
|
||||
|
||||
func collectLegacyManagedDomains(ctx context.Context, tx *sql.Tx, q func(string) string) ([]legacyDomain, error) {
|
||||
rows, err := tx.QueryContext(ctx, q(`SELECT domain, cert_id, remark FROM of_managed_domains`))
|
||||
rows, err := tx.QueryContext(ctx, q(`SELECT domain, cert_id FROM of_managed_domains`))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -256,12 +253,11 @@ func collectLegacyManagedDomains(ctx context.Context, tx *sql.Tx, q func(string)
|
||||
var (
|
||||
domain string
|
||||
certID sql.NullInt64
|
||||
remark string
|
||||
)
|
||||
if err := rows.Scan(&domain, &certID, &remark); err != nil {
|
||||
if err := rows.Scan(&domain, &certID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := legacyDomain{Domain: domain, Remark: remark}
|
||||
item := legacyDomain{Domain: domain}
|
||||
if certID.Valid && certID.Int64 > 0 {
|
||||
v := uint(certID.Int64)
|
||||
item.CertID = &v
|
||||
|
||||
@@ -27,7 +27,6 @@ func setupLegacyImportDB(t *testing.T) (*sql.DB, func()) {
|
||||
`CREATE TABLE of_zones (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain TEXT NOT NULL UNIQUE,
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
@@ -37,7 +36,6 @@ func setupLegacyImportDB(t *testing.T) (*sql.DB, func()) {
|
||||
proxy_route_id INTEGER,
|
||||
domain TEXT NOT NULL UNIQUE,
|
||||
cert_id INTEGER,
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
@@ -124,7 +122,6 @@ func TestImportLegacyTxNoOpWithoutLegacyColumns(t *testing.T) {
|
||||
CREATE TABLE of_zones (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain TEXT NOT NULL UNIQUE,
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME, updated_at DATETIME
|
||||
);
|
||||
CREATE TABLE of_zone_domains (
|
||||
@@ -133,7 +130,6 @@ func TestImportLegacyTxNoOpWithoutLegacyColumns(t *testing.T) {
|
||||
proxy_route_id INTEGER,
|
||||
domain TEXT NOT NULL UNIQUE,
|
||||
cert_id INTEGER,
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME, updated_at DATETIME
|
||||
);
|
||||
CREATE TABLE of_proxy_routes (
|
||||
|
||||
@@ -19,14 +19,12 @@ import (
|
||||
// Input is the mutable Zone payload.
|
||||
type Input struct {
|
||||
Domain string `json:"domain"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// DomainInput is the mutable Zone-domain payload.
|
||||
type DomainInput struct {
|
||||
Domain string `json:"domain"`
|
||||
CertID *uint `json:"cert_id"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// Overview joins a Zone with its explicit domains.
|
||||
@@ -39,7 +37,6 @@ type Overview struct {
|
||||
type ListItem struct {
|
||||
ID uint `json:"id"`
|
||||
Domain string `json:"domain"`
|
||||
Remark string `json:"remark"`
|
||||
DomainCount int64 `json:"domain_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -76,7 +73,7 @@ func Create(ctx context.Context, input Input) (*model.Zone, error) {
|
||||
if err != nil || root != domain {
|
||||
return nil, errors.New(errZoneRootInvalid)
|
||||
}
|
||||
zone := &model.Zone{Domain: domain, Remark: strings.TrimSpace(input.Remark)}
|
||||
zone := &model.Zone{Domain: domain}
|
||||
if err := db.DB(ctx).Create(zone).Error; err != nil {
|
||||
if isUnique(err) {
|
||||
return nil, errors.New(errDomainExists)
|
||||
@@ -100,7 +97,7 @@ func Update(ctx context.Context, id uint, input Input) (*model.Zone, error) {
|
||||
if err != nil || root != domain {
|
||||
return nil, errors.New(errZoneRootInvalid)
|
||||
}
|
||||
zone.Domain, zone.Remark = domain, strings.TrimSpace(input.Remark)
|
||||
zone.Domain = domain
|
||||
if err := db.DB(ctx).Save(&zone).Error; err != nil {
|
||||
if isUnique(err) {
|
||||
return nil, errors.New(errDomainExists)
|
||||
@@ -138,7 +135,6 @@ func List(ctx context.Context) ([]ListItem, error) {
|
||||
items = append(items, ListItem{
|
||||
ID: zone.ID,
|
||||
Domain: zone.Domain,
|
||||
Remark: zone.Remark,
|
||||
DomainCount: counts[zone.ID],
|
||||
CreatedAt: zone.CreatedAt,
|
||||
UpdatedAt: zone.UpdatedAt,
|
||||
@@ -179,7 +175,7 @@ func CreateDomain(ctx context.Context, zoneID uint, input DomainInput) (*model.Z
|
||||
return nil, errors.New(errCertificateNotFound)
|
||||
}
|
||||
}
|
||||
item := &model.ZoneDomain{ZoneID: zoneID, Domain: domain, CertID: input.CertID, Remark: strings.TrimSpace(input.Remark)}
|
||||
item := &model.ZoneDomain{ZoneID: zoneID, Domain: domain, CertID: input.CertID}
|
||||
if err := db.DB(ctx).Create(item).Error; err != nil {
|
||||
if isUnique(err) {
|
||||
return nil, errors.New(errDomainExists)
|
||||
@@ -212,7 +208,7 @@ func UpdateDomain(ctx context.Context, zoneID, id uint, input DomainInput) (*mod
|
||||
return nil, errors.New(errCertificateNotFound)
|
||||
}
|
||||
}
|
||||
item.Domain, item.CertID, item.Remark = domain, input.CertID, strings.TrimSpace(input.Remark)
|
||||
item.Domain, item.CertID = domain, input.CertID
|
||||
if err = db.DB(ctx).Save(&item).Error; err != nil {
|
||||
if isUnique(err) {
|
||||
return nil, errors.New(errDomainExists)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- +goose Up
|
||||
-- 移除业务实体上未再使用的备注列(证书与源站备注保留)。
|
||||
|
||||
ALTER TABLE of_zones DROP COLUMN IF EXISTS remark;
|
||||
ALTER TABLE of_zone_domains DROP COLUMN IF EXISTS remark;
|
||||
ALTER TABLE of_proxy_routes DROP COLUMN IF EXISTS remark;
|
||||
ALTER TABLE of_waf_rule_groups DROP COLUMN IF EXISTS remark;
|
||||
ALTER TABLE of_waf_ip_groups DROP COLUMN IF EXISTS remark;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE of_zones ADD COLUMN IF NOT EXISTS remark VARCHAR(255) NOT NULL DEFAULT '';
|
||||
ALTER TABLE of_zone_domains ADD COLUMN IF NOT EXISTS remark VARCHAR(255) NOT NULL DEFAULT '';
|
||||
ALTER TABLE of_proxy_routes ADD COLUMN IF NOT EXISTS remark VARCHAR(255) NOT NULL DEFAULT '';
|
||||
ALTER TABLE of_waf_rule_groups ADD COLUMN IF NOT EXISTS remark VARCHAR(255) NOT NULL DEFAULT '';
|
||||
ALTER TABLE of_waf_ip_groups ADD COLUMN IF NOT EXISTS remark VARCHAR(255) NOT NULL DEFAULT '';
|
||||
@@ -0,0 +1,17 @@
|
||||
-- +goose Up
|
||||
-- 移除业务实体上未再使用的备注列(证书与源站备注保留)。
|
||||
-- SQLite 3.35+ 支持 DROP COLUMN;goose 仅在列存在的既有库上执行。
|
||||
|
||||
ALTER TABLE of_zones DROP COLUMN remark;
|
||||
ALTER TABLE of_zone_domains DROP COLUMN remark;
|
||||
ALTER TABLE of_proxy_routes DROP COLUMN remark;
|
||||
ALTER TABLE of_waf_rule_groups DROP COLUMN remark;
|
||||
ALTER TABLE of_waf_ip_groups DROP COLUMN remark;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE of_zones ADD COLUMN remark TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE of_zone_domains ADD COLUMN remark TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE of_proxy_routes ADD COLUMN remark TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE of_waf_rule_groups ADD COLUMN remark TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE of_waf_ip_groups ADD COLUMN remark TEXT NOT NULL DEFAULT '';
|
||||
@@ -32,7 +32,6 @@ type ProxyRoute struct {
|
||||
BasicAuthEnabled bool `json:"basic_auth_enabled" gorm:"not null;default:false"`
|
||||
BasicAuthUsername string `json:"basic_auth_username" gorm:"size:255;not null;default:''"`
|
||||
BasicAuthPassword string `json:"basic_auth_password" gorm:"size:255;not null;default:''"`
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
UpstreamType string `json:"upstream_type" gorm:"size:32;not null;default:'direct'"`
|
||||
TunnelNodeID *uint `json:"tunnel_node_id" gorm:"index"`
|
||||
TunnelTargetAddr string `json:"tunnel_target_addr" gorm:"size:512"`
|
||||
@@ -92,7 +91,6 @@ func UpdateProxyRouteRecord(ctx context.Context, route *ProxyRoute) error {
|
||||
"basic_auth_enabled": route.BasicAuthEnabled,
|
||||
"basic_auth_username": route.BasicAuthUsername,
|
||||
"basic_auth_password": route.BasicAuthPassword,
|
||||
colRemark: route.Remark,
|
||||
"upstream_type": route.UpstreamType,
|
||||
"tunnel_node_id": route.TunnelNodeID,
|
||||
"tunnel_target_addr": route.TunnelTargetAddr,
|
||||
|
||||
@@ -30,7 +30,6 @@ type OpenFlareWAFRuleGroup struct {
|
||||
RegionBlacklist string `json:"region_blacklist" gorm:"type:text;not null;default:'[]'"`
|
||||
PoWEnabled bool `json:"pow_enabled" gorm:"column:pow_enabled;not null;default:false"`
|
||||
PoWConfig string `json:"pow_config" gorm:"column:pow_config;type:text;not null;default:'{}'"`
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
@@ -57,7 +56,6 @@ type OpenFlareWAFIPGroup struct {
|
||||
NextSyncAt *time.Time `json:"next_sync_at" gorm:"index"`
|
||||
LastSyncStatus string `json:"last_sync_status" gorm:"size:32;not null;default:''"`
|
||||
LastSyncMessage string `json:"last_sync_message" gorm:"type:text;not null;default:''"`
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
@@ -158,7 +156,6 @@ func UpdateOpenFlareWAFRuleGroup(ctx context.Context, group *OpenFlareWAFRuleGro
|
||||
"region_blacklist": group.RegionBlacklist,
|
||||
"pow_enabled": group.PoWEnabled,
|
||||
"pow_config": group.PoWConfig,
|
||||
colRemark: group.Remark,
|
||||
}).Error
|
||||
}
|
||||
|
||||
@@ -242,7 +239,6 @@ func UpdateOpenFlareWAFIPGroup(ctx context.Context, group *OpenFlareWAFIPGroup)
|
||||
"next_sync_at": group.NextSyncAt,
|
||||
"last_sync_status": group.LastSyncStatus,
|
||||
"last_sync_message": group.LastSyncMessage,
|
||||
"remark": group.Remark,
|
||||
}).Error
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ var errZoneDomainBoundToAnotherRoute = errors.New("zone domain is already bound
|
||||
type Zone struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Domain string `json:"domain" gorm:"uniqueIndex:idx_of_zones_domain;size:255;not null"`
|
||||
Remark string `json:"remark" gorm:"size:255;not null;default:''"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
@@ -42,7 +41,6 @@ type ZoneDomain struct {
|
||||
ProxyRouteID *uint `json:"proxy_route_id" gorm:"index:idx_of_zone_domains_proxy_route_id"`
|
||||
Domain string `json:"domain" gorm:"uniqueIndex:idx_of_zone_domains_domain;size:255;not null"`
|
||||
CertID *uint `json:"cert_id" gorm:"index:idx_of_zone_domains_cert_id"`
|
||||
Remark string `json:"remark" gorm:"size:255;not null;default:''"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
@@ -155,7 +155,6 @@ type Route struct {
|
||||
BasicAuthEnabled bool `json:"basic_auth_enabled,omitempty"`
|
||||
BasicAuthUsername string `json:"basic_auth_username,omitempty"`
|
||||
BasicAuthPassword string `json:"basic_auth_password,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
UpstreamType string `json:"upstream_type,omitempty"`
|
||||
PagesDeployment *PagesDeployment `json:"pages_deployment,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user