feat: enhance DoH profile management and resolver policy in modules
CI / changes (push) Successful in 6s
CI / openapi (push) Successful in 1m2s
CI / go (push) Successful in 27s
CI / docker-web (push) Successful in 1m27s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go (push) Successful in 8m5s
CI / changes (push) Successful in 6s
CI / openapi (push) Successful in 1m2s
CI / go (push) Successful in 27s
CI / docker-web (push) Successful in 1m27s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go (push) Successful in 8m5s
- Added `DohResolverPolicy` schema to OpenAPI documentation, defining policies for domain resolution. - Updated module handling to support multiple DoH profiles via `doh_profile_ids` and introduced `doh_resolver_policy` in the API. - Refactored related functions to accommodate the new DoH profile structure, ensuring backward compatibility with existing `doh_profile_id`. - Enhanced UI components to allow selection and management of DoH profiles and policies in the web interface. - Updated database interactions to handle new fields and ensure proper data normalization.
This commit is contained in:
@@ -8,6 +8,8 @@ export type Page<T> = {
|
||||
// ---- Modules ----
|
||||
export type ModuleType = 'AS_PREFIXES' | 'CDN_CIDRS' | 'DOMAINS' | 'IP_RANGES';
|
||||
|
||||
export type DohResolverPolicy = 'primary_only' | 'failover' | 'union';
|
||||
|
||||
export type ModuleRow = {
|
||||
id: string;
|
||||
type: ModuleType;
|
||||
@@ -17,7 +19,10 @@ export type ModuleRow = {
|
||||
refresh_interval_sec: number | null;
|
||||
cron_expr: string | null;
|
||||
default_community_id: string | null;
|
||||
/** @deprecated use doh_profile_ids */
|
||||
doh_profile_id: string | null;
|
||||
doh_profile_ids: string[];
|
||||
doh_resolver_policy: DohResolverPolicy;
|
||||
last_refreshed_at: string | null;
|
||||
};
|
||||
export type ModulesResponse = Page<ModuleRow>;
|
||||
@@ -36,6 +41,8 @@ export type ModuleCreate = {
|
||||
enabled?: boolean;
|
||||
priority?: number;
|
||||
doh_profile_id?: string | null;
|
||||
doh_profile_ids?: string[];
|
||||
doh_resolver_policy?: DohResolverPolicy;
|
||||
refresh_interval_sec?: number | null;
|
||||
cron_expr?: string | null;
|
||||
default_community_id?: string | null;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type {
|
||||
ModuleRow,
|
||||
ModulePatch,
|
||||
DohResolverPolicy,
|
||||
AsEntry,
|
||||
AsEntryCreate,
|
||||
AsEntryPatch,
|
||||
@@ -57,6 +58,7 @@
|
||||
SelectTrigger,
|
||||
} from '$lib/components/ui/select/index.js';
|
||||
import { Switch } from '$lib/components/ui/switch/index.js';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -82,6 +84,53 @@
|
||||
import Network from '@lucide/svelte/icons/network';
|
||||
import { moduleTypeRu } from '$lib/ui-labels.js';
|
||||
|
||||
const dohPolicyOptions: { value: DohResolverPolicy; label: string; hint: string }[] = [
|
||||
{
|
||||
value: 'primary_only',
|
||||
label: 'Только первый',
|
||||
hint: 'Используется первый выбранный DoH-профиль.'
|
||||
},
|
||||
{
|
||||
value: 'failover',
|
||||
label: 'Резервирование',
|
||||
hint: 'Профили по порядку до первого успешного ответа.'
|
||||
},
|
||||
{
|
||||
value: 'union',
|
||||
label: 'Объединение',
|
||||
hint: 'Все A/AAAA со всех профилей (geo-split DNS).'
|
||||
}
|
||||
];
|
||||
|
||||
function dohPolicyLabel(policy: DohResolverPolicy | null | undefined): string {
|
||||
return dohPolicyOptions.find((o) => o.value === policy)?.label ?? 'Только первый';
|
||||
}
|
||||
|
||||
function moduleDohProfileIds(modRow: ModuleRow | null): string[] {
|
||||
if (!modRow) return [];
|
||||
if (modRow.doh_profile_ids?.length) return modRow.doh_profile_ids;
|
||||
return modRow.doh_profile_id ? [modRow.doh_profile_id] : [];
|
||||
}
|
||||
|
||||
function dohProfileLabel(id: string): string {
|
||||
const p = dohProfiles.find((d) => d.id === id);
|
||||
return p ? (p.name?.trim() ? `${p.name} (${p.url})` : p.url) : id.slice(0, 8) + '…';
|
||||
}
|
||||
|
||||
function toggleEditDohProfile(id: string, checked: boolean) {
|
||||
let ids = [...(editForm.doh_profile_ids ?? [])];
|
||||
if (checked) {
|
||||
if (!ids.includes(id)) ids.push(id);
|
||||
} else {
|
||||
ids = ids.filter((x) => x !== id);
|
||||
}
|
||||
editForm = { ...editForm, doh_profile_ids: ids };
|
||||
}
|
||||
|
||||
function isEditDohProfileSelected(id: string): boolean {
|
||||
return (editForm.doh_profile_ids ?? []).includes(id);
|
||||
}
|
||||
|
||||
const moduleId = $derived(page.params.moduleId);
|
||||
|
||||
let mod = $state<ModuleRow | null>(null);
|
||||
@@ -264,7 +313,8 @@
|
||||
refresh_interval_sec: mod.refresh_interval_sec,
|
||||
cron_expr: mod.cron_expr,
|
||||
default_community_id: mod.default_community_id,
|
||||
doh_profile_id: mod.doh_profile_id
|
||||
doh_profile_ids: moduleDohProfileIds(mod),
|
||||
doh_resolver_policy: mod.doh_resolver_policy ?? 'primary_only'
|
||||
};
|
||||
editDialog = true;
|
||||
}
|
||||
@@ -283,7 +333,8 @@
|
||||
cron_expr: cron ? cron : null,
|
||||
refresh_interval_sec: Number.isFinite(interval) ? interval : null,
|
||||
default_community_id: fromNullableSelect(nullableSelectValue(editForm.default_community_id)),
|
||||
doh_profile_id: fromNullableSelect(nullableSelectValue(editForm.doh_profile_id))
|
||||
doh_profile_ids: editForm.doh_profile_ids ?? [],
|
||||
doh_resolver_policy: editForm.doh_resolver_policy ?? 'primary_only'
|
||||
};
|
||||
await apiMutate<ModuleRow>(`/v1/modules/${moduleId}`, 'PATCH', payload);
|
||||
await loadMod();
|
||||
@@ -847,8 +898,19 @@
|
||||
</p>
|
||||
</Card>
|
||||
<Card class="bg-chart-4/10 p-3">
|
||||
<p class="text-muted-foreground flex items-center gap-1.5 text-xs"><Network class="size-3.5" />DoH профиль</p>
|
||||
<p class="font-semibold text-sm">{mod.doh_profile_id ? mod.doh_profile_id.slice(0, 8) + '…' : '—'}</p>
|
||||
<p class="text-muted-foreground flex items-center gap-1.5 text-xs"><Network class="size-3.5" />DoH</p>
|
||||
{#if mod.type === 'DOMAINS'}
|
||||
<p class="font-semibold text-sm">{dohPolicyLabel(mod.doh_resolver_policy)}</p>
|
||||
<p class="text-muted-foreground text-xs break-all">
|
||||
{#if moduleDohProfileIds(mod).length}
|
||||
{moduleDohProfileIds(mod).map(dohProfileLabel).join('; ')}
|
||||
{:else}
|
||||
Системный DNS
|
||||
{/if}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="font-semibold text-sm">—</p>
|
||||
{/if}
|
||||
</Card>
|
||||
<Card class="bg-chart-2/10 p-3">
|
||||
<p class="text-muted-foreground flex items-center gap-1.5 text-xs"><ShieldCheck class="size-3.5" />Community по умолч.</p>
|
||||
@@ -1349,37 +1411,63 @@
|
||||
Сбросить
|
||||
</Button>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="e-doh">DoH профиль</Label>
|
||||
<Select
|
||||
type="single"
|
||||
value={nullableSelectValue(editForm.doh_profile_id)}
|
||||
onValueChange={(v) => {
|
||||
editForm.doh_profile_id = fromNullableSelect(v);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="e-doh" class="w-full">
|
||||
{editForm.doh_profile_id ? (dohProfiles.find(d => d.id === editForm.doh_profile_id)?.url ?? editForm.doh_profile_id) : 'Не выбрано'}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE_OPTION}>Не выбрано</SelectItem>
|
||||
{#if mod?.type === 'DOMAINS'}
|
||||
<div class="space-y-1.5">
|
||||
<Label for="e-doh-policy">Политика DoH</Label>
|
||||
<Select
|
||||
type="single"
|
||||
value={editForm.doh_resolver_policy ?? 'primary_only'}
|
||||
onValueChange={(v) => {
|
||||
if (v) editForm.doh_resolver_policy = v as DohResolverPolicy;
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="e-doh-policy" class="w-full">
|
||||
{dohPolicyLabel(editForm.doh_resolver_policy)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each dohPolicyOptions as opt (opt.value)}
|
||||
<SelectItem value={opt.value}>{opt.label}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
{dohPolicyOptions.find((o) => o.value === (editForm.doh_resolver_policy ?? 'primary_only'))?.hint}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>DoH-профили</Label>
|
||||
<p class="text-muted-foreground text-xs">Порядок выбора = порядок в списке (сверху вниз).</p>
|
||||
<div class="max-h-40 space-y-2 overflow-y-auto rounded-md border p-3">
|
||||
{#each dohProfiles as d (d.id)}
|
||||
<SelectItem value={d.id}>{d.url}</SelectItem>
|
||||
<label class="flex items-start gap-2 text-sm">
|
||||
<Checkbox
|
||||
checked={isEditDohProfileSelected(d.id)}
|
||||
onCheckedChange={(v) => toggleEditDohProfile(d.id, v === true)}
|
||||
/>
|
||||
<span class="min-w-0 break-all">
|
||||
<span class="font-medium">{d.name?.trim() ? d.name : d.url}</span>
|
||||
{#if d.name?.trim()}
|
||||
<span class="text-muted-foreground block font-mono text-xs">{d.url}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
{:else}
|
||||
<p class="text-muted-foreground text-xs">Нет профилей — создайте в справочниках.</p>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2"
|
||||
onclick={() => {
|
||||
editForm.doh_profile_id = null;
|
||||
}}
|
||||
>
|
||||
Сбросить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2"
|
||||
onclick={() => {
|
||||
editForm = { ...editForm, doh_profile_ids: [] };
|
||||
}}
|
||||
>
|
||||
Сбросить профили
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="border-border bg-muted/30 flex flex-row items-center justify-between gap-4 rounded-lg border p-3"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user