2025-11-18 19:34:26 +00:00
|
|
|
/**
|
|
|
|
|
* Cloudflare Domain Sync Manager
|
|
|
|
|
*
|
|
|
|
|
* Synchronizes Cloudflare DNS zones with the local Domain table.
|
|
|
|
|
* Automatically imports zones and marks obsolete domains when zones are removed.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import * as plugins from '../plugins.ts';
|
|
|
|
|
import { logger } from '../logging.ts';
|
|
|
|
|
import { OneboxDatabase } from './database.ts';
|
|
|
|
|
import type { IDomain } from '../types.ts';
|
|
|
|
|
|
|
|
|
|
export class CloudflareDomainSync {
|
|
|
|
|
private database: OneboxDatabase;
|
|
|
|
|
private cloudflareAccount: plugins.cloudflare.CloudflareAccount | null = null;
|
|
|
|
|
|
|
|
|
|
constructor(database: OneboxDatabase) {
|
|
|
|
|
this.database = database;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize Cloudflare connection
|
|
|
|
|
*/
|
|
|
|
|
async init(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
const apiKey = this.database.getSetting('cloudflareAPIKey');
|
|
|
|
|
|
|
|
|
|
if (!apiKey) {
|
|
|
|
|
logger.warn('Cloudflare API key not configured. Domain sync will be limited.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.cloudflareAccount = new plugins.cloudflare.CloudflareAccount(apiKey);
|
|
|
|
|
logger.info('Cloudflare domain sync initialized');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to initialize Cloudflare sync: ${error.message}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if Cloudflare is configured
|
|
|
|
|
*/
|
|
|
|
|
isConfigured(): boolean {
|
|
|
|
|
return this.cloudflareAccount !== null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sync all Cloudflare zones with Domain table
|
|
|
|
|
*/
|
|
|
|
|
async syncZones(): Promise<void> {
|
|
|
|
|
if (!this.isConfigured()) {
|
|
|
|
|
logger.warn('Cloudflare not configured, skipping zone sync');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
logger.info('Starting Cloudflare zone synchronization...');
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
// Fetch all zones from Cloudflare (v6+ API uses convenience.listZones())
|
|
|
|
|
const zones = await this.cloudflareAccount!.convenience.listZones();
|
2025-11-18 19:34:26 +00:00
|
|
|
logger.info(`Found ${zones.length} Cloudflare zone(s)`);
|
|
|
|
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const syncedZoneIds = new Set<string>();
|
|
|
|
|
|
|
|
|
|
// Sync each zone to the Domain table
|
|
|
|
|
for (const zone of zones) {
|
|
|
|
|
try {
|
|
|
|
|
const domain = zone.name;
|
|
|
|
|
const zoneId = zone.id;
|
|
|
|
|
|
|
|
|
|
syncedZoneIds.add(zoneId);
|
|
|
|
|
|
|
|
|
|
// Check if domain already exists
|
|
|
|
|
const existingDomain = this.database.getDomainByName(domain);
|
|
|
|
|
|
|
|
|
|
if (existingDomain) {
|
|
|
|
|
// Update existing domain
|
|
|
|
|
this.database.updateDomain(existingDomain.id!, {
|
|
|
|
|
dnsProvider: 'cloudflare',
|
|
|
|
|
cloudflareZoneId: zoneId,
|
|
|
|
|
isObsolete: false, // Re-activate if it was marked obsolete
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
});
|
|
|
|
|
logger.debug(`Updated domain: ${domain}`);
|
|
|
|
|
} else {
|
|
|
|
|
// Create new domain
|
|
|
|
|
this.database.createDomain({
|
|
|
|
|
domain,
|
|
|
|
|
dnsProvider: 'cloudflare',
|
|
|
|
|
cloudflareZoneId: zoneId,
|
|
|
|
|
isObsolete: false,
|
|
|
|
|
defaultWildcard: true, // Default to wildcard certificates
|
|
|
|
|
createdAt: now,
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
});
|
|
|
|
|
logger.info(`Added new domain from Cloudflare: ${domain}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to sync zone ${zone.name}: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark domains as obsolete if their Cloudflare zones no longer exist
|
|
|
|
|
await this.markObsoleteDomains(syncedZoneIds);
|
|
|
|
|
|
|
|
|
|
logger.success(`Cloudflare zone sync completed: ${zones.length} zone(s) synced`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Cloudflare zone sync failed: ${error.message}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mark domains as obsolete if their Cloudflare zones have been removed
|
|
|
|
|
*/
|
|
|
|
|
private async markObsoleteDomains(activeZoneIds: Set<string>): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
// Get all domains managed by Cloudflare
|
|
|
|
|
const cloudflareDomains = this.database.getDomainsByProvider('cloudflare');
|
|
|
|
|
|
|
|
|
|
let obsoleteCount = 0;
|
|
|
|
|
|
|
|
|
|
for (const domain of cloudflareDomains) {
|
|
|
|
|
// If domain has a Cloudflare zone ID but it's not in the active set, mark obsolete
|
|
|
|
|
if (domain.cloudflareZoneId && !activeZoneIds.has(domain.cloudflareZoneId)) {
|
|
|
|
|
this.database.updateDomain(domain.id!, {
|
|
|
|
|
isObsolete: true,
|
|
|
|
|
updatedAt: Date.now(),
|
|
|
|
|
});
|
|
|
|
|
logger.warn(`Marked domain as obsolete (zone removed): ${domain.domain}`);
|
|
|
|
|
obsoleteCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (obsoleteCount > 0) {
|
|
|
|
|
logger.info(`Marked ${obsoleteCount} domain(s) as obsolete`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to mark obsolete domains: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get sync status information
|
|
|
|
|
*/
|
|
|
|
|
getSyncStatus(): {
|
|
|
|
|
configured: boolean;
|
|
|
|
|
totalDomains: number;
|
|
|
|
|
cloudflareDomains: number;
|
|
|
|
|
obsoleteDomains: number;
|
|
|
|
|
} {
|
|
|
|
|
const allDomains = this.database.getAllDomains();
|
|
|
|
|
const cloudflareDomains = allDomains.filter(d => d.dnsProvider === 'cloudflare');
|
|
|
|
|
const obsoleteDomains = allDomains.filter(d => d.isObsolete);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
configured: this.isConfigured(),
|
|
|
|
|
totalDomains: allDomains.length,
|
|
|
|
|
cloudflareDomains: cloudflareDomains.length,
|
|
|
|
|
obsoleteDomains: obsoleteDomains.length,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|