feat(cloudflare): add DNS pointing integration

Implement Cloudflare connection management, pointing groups and members, asynchronous A-record reconciliation, node IP triggers, admin APIs, management pages, migrations, tests, and documentation.
This commit is contained in:
ryan
2026-08-04 12:32:37 +08:00
parent 99f0f63b99
commit 3edcdb9e9f
47 changed files with 7019 additions and 49 deletions
+6
View File
@@ -54,6 +54,11 @@ export const openflareWebsiteNavGroup: OpenFlareNavGroup = {
{ title: '域名列表', url: '/websites', childUrls: ['/websites/detail'] },
{ title: 'TLS证书', url: '/certificates' },
{ title: 'DNS账号', url: '/dns-accounts' },
{
title: 'Cloudflare',
url: '/cloudflare',
childUrls: ['/cloudflare/settings', '/cloudflare/groups'],
},
{ title: '源站地址', url: '/origins', childUrls: ['/origins/detail'] },
],
};
@@ -109,6 +114,7 @@ export const openflareWebsiteSubNav = [
{ title: '网站列表', url: '/websites' },
{ title: '证书', url: '/certificates' },
{ title: 'DNS 账号', url: '/dns-accounts' },
{ title: 'Cloudflare', url: '/cloudflare' },
] as const;
const nonConsoleRoutePrefixes = [
@@ -0,0 +1,105 @@
import { OpenFlareBaseService } from './base.service';
import type {
CloudflareAvailableDomain,
CloudflareConnection,
CloudflareConnectionPayload,
CloudflareGroup,
CloudflareGroupDetail,
CloudflareGroupPayload,
CloudflareMember,
CloudflareMemberCreatePayload,
CloudflareOverview,
CloudflareSyncReceipt,
} from './types';
export const cloudflareQueryKey = ['openflare', 'cloudflare'] as const;
export class CloudflareService extends OpenFlareBaseService {
protected static override readonly basePath = '/api/v1/d/cloudflare';
static getConnection(): Promise<CloudflareConnection> {
return this.get<CloudflareConnection>('/connection');
}
static saveConnection(
payload: CloudflareConnectionPayload,
): Promise<CloudflareConnection> {
return this.put<CloudflareConnection>('/connection', payload);
}
static verifyConnection(): Promise<CloudflareConnection> {
return this.post<CloudflareConnection>('/connection/verify');
}
static clearConnection(): Promise<void> {
return this.post<void>('/connection/clear');
}
static getOverview(): Promise<CloudflareOverview> {
return this.get<CloudflareOverview>('/overview');
}
static listGroups(): Promise<CloudflareGroup[]> {
return this.get<CloudflareGroup[]>('/groups');
}
static getGroup(id: number): Promise<CloudflareGroupDetail> {
return this.get<CloudflareGroupDetail>(`/groups/${id}`);
}
static createGroup(
payload: CloudflareGroupPayload,
): Promise<CloudflareGroup> {
return this.post<CloudflareGroup>('/groups', payload);
}
static updateGroup(
id: number,
payload: CloudflareGroupPayload,
): Promise<CloudflareGroup> {
return this.post<CloudflareGroup>(`/groups/${id}/update`, payload);
}
static deleteGroup(id: number): Promise<void> {
return this.post<void>(`/groups/${id}/delete`);
}
static syncGroup(id: number): Promise<CloudflareSyncReceipt> {
return this.post<CloudflareSyncReceipt>(`/groups/${id}/sync`);
}
static listAvailableDomains(): Promise<CloudflareAvailableDomain[]> {
return this.get<CloudflareAvailableDomain[]>('/domains/available');
}
static createMember(
groupId: number,
payload: CloudflareMemberCreatePayload,
): Promise<CloudflareMember> {
return this.post<CloudflareMember>(`/groups/${groupId}/members`, payload);
}
static updateMember(
groupId: number,
memberId: number,
proxied: boolean,
): Promise<CloudflareMember> {
return this.post<CloudflareMember>(
`/groups/${groupId}/members/${memberId}/update`,
{ proxied },
);
}
static removeMember(groupId: number, memberId: number): Promise<void> {
return this.post<void>(`/groups/${groupId}/members/${memberId}/remove`);
}
static syncMember(
groupId: number,
memberId: number,
): Promise<CloudflareSyncReceipt> {
return this.post<CloudflareSyncReceipt>(
`/groups/${groupId}/members/${memberId}/sync`,
);
}
}
+19
View File
@@ -15,6 +15,10 @@ export { AccessLogService } from './access-log.service';
export { OptionService } from './option.service';
export { UptimeKumaService } from './uptimekuma.service';
export { StatusService } from './status.service';
export {
CloudflareService,
cloudflareQueryKey,
} from './cloudflare.service';
export type {
ApplyLogCleanupPayload,
@@ -143,6 +147,19 @@ export type {
TlsCertificateFileImportPayload,
TlsCertificateItem,
TlsCertificateMutationPayload,
CloudflareAvailableDomain,
CloudflareConnection,
CloudflareConnectionPayload,
CloudflareConnectionSource,
CloudflareGroup,
CloudflareGroupDetail,
CloudflareGroupPayload,
CloudflareMember,
CloudflareMemberCreatePayload,
CloudflareNodeOption,
CloudflareOverview,
CloudflareSyncReceipt,
CloudflareSyncStatus,
} from './types';
import { AccessLogService } from './access-log.service';
@@ -160,6 +177,7 @@ import { ProxyRouteService } from './proxy-route.service';
import { TlsCertificateService } from './tls-certificate.service';
import { WafService } from './waf.service';
import { ZoneDomainService, ZoneService } from './zone.service';
import { CloudflareService } from './cloudflare.service';
export const openflareServices = {
node: NodeService,
@@ -178,4 +196,5 @@ export const openflareServices = {
option: OptionService,
uptimeKuma: UptimeKumaService,
status: StatusService,
cloudflare: CloudflareService,
} as const;
+87
View File
@@ -1347,6 +1347,93 @@ export interface ZoneStats {
series: ZoneStatsPoint[];
}
export type CloudflareConnectionSource = 'dns_account' | 'standalone';
export type CloudflareSyncStatus = 'pending' | 'syncing' | 'ok' | 'error';
export interface CloudflareConnection {
configured: boolean;
ready: boolean;
source: CloudflareConnectionSource | '';
dns_account_id: number | null;
status: string;
verified_at: string | null;
}
export interface CloudflareConnectionPayload {
source: CloudflareConnectionSource;
dns_account_id: number;
api_token: string;
}
export interface CloudflareNodeOption {
id: number;
name: string;
ip: string;
}
export interface CloudflareGroup {
id: number;
name: string;
primary_node: CloudflareNodeOption;
backup_node: CloudflareNodeOption | null;
active_node: CloudflareNodeOption;
default_proxied: boolean;
enabled: boolean;
member_count: number;
created_at: string;
updated_at: string;
}
export interface CloudflareGroupPayload {
name: string;
primary_node_id: number;
backup_node_id: number | null;
default_proxied: boolean;
enabled: boolean;
}
export interface CloudflareMember {
id: number;
group_id: number;
zone_domain_id: number;
domain: string;
zone_id: number;
proxied: boolean;
desired_ip: string;
sync_status: CloudflareSyncStatus;
last_error: string;
synced_at: string | null;
}
export interface CloudflareMemberCreatePayload {
zone_domain_id: number;
proxied?: boolean;
}
export interface CloudflareGroupDetail {
group: CloudflareGroup;
members: CloudflareMember[];
}
export interface CloudflareAvailableDomain {
id: number;
zone_id: number;
domain: string;
}
export interface CloudflareOverview {
connection: CloudflareConnection;
group_count: number;
member_count: number;
ok_count: number;
pending_count: number;
error_count: number;
}
export interface CloudflareSyncReceipt {
task_id: string;
}
export interface TlsCertificateItem {
id: number;
name: string;