feat(ssl): Add domain & certificate management, Cloudflare sync, SQLite cert manager, WebSocket realtime updates, and HTTP API SSL endpoints
This commit is contained in:
165
ts/classes/cloudflare-sync.ts
Normal file
165
ts/classes/cloudflare-sync.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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...');
|
||||
|
||||
// Fetch all zones from Cloudflare
|
||||
const zones = await this.cloudflareAccount!.getZones();
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user