This commit is contained in:
2025-11-18 00:36:23 +00:00
parent 8f538ab9c0
commit 1fe3cd7f14
3 changed files with 115 additions and 73 deletions

View File

@@ -120,6 +120,7 @@ export class OneboxDatabase {
type TEXT NOT NULL, type TEXT NOT NULL,
value TEXT NOT NULL, value TEXT NOT NULL,
cloudflare_id TEXT, cloudflare_id TEXT,
zone_id TEXT,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL updated_at INTEGER NOT NULL
) )
@@ -437,7 +438,11 @@ export class OneboxDatabase {
if (!this.db) throw new Error('Database not initialized'); if (!this.db) throw new Error('Database not initialized');
const rows = this.query('SELECT value FROM settings WHERE key = ?', [key]); const rows = this.query('SELECT value FROM settings WHERE key = ?', [key]);
return rows.length > 0 ? String(rows[0][0]) : null; if (rows.length === 0) return null;
// @db/sqlite returns rows as objects with column names as keys
const value = (rows[0] as any).value || rows[0][0];
return value ? String(value) : null;
} }
setSetting(key: string, value: string): void { setSetting(key: string, value: string): void {

View File

@@ -4,15 +4,15 @@
* Manages DNS records via Cloudflare API * Manages DNS records via Cloudflare API
*/ */
import * as plugins from '../plugins.ts'; import Cloudflare from 'npm:cloudflare@5.2.0';
import { logger } from '../logging.ts'; import { logger } from '../logging.ts';
import { OneboxDatabase } from './database.ts'; import { OneboxDatabase } from './database.ts';
export class OneboxDnsManager { export class OneboxDnsManager {
private oneboxRef: any; private oneboxRef: any;
private database: OneboxDatabase; private database: OneboxDatabase;
private cloudflareClient: plugins.cloudflare.CloudflareAccount | null = null; private cloudflareClient: Cloudflare | null = null;
private zoneID: string | null = null; private zones: Map<string, string> = new Map(); // zone name -> zone ID
private serverIP: string | null = null; private serverIP: string | null = null;
constructor(oneboxRef: any) { constructor(oneboxRef: any) {
@@ -27,51 +27,42 @@ export class OneboxDnsManager {
try { try {
// Get Cloudflare credentials from settings // Get Cloudflare credentials from settings
const apiKey = this.database.getSetting('cloudflareAPIKey'); const apiKey = this.database.getSetting('cloudflareAPIKey');
const email = this.database.getSetting('cloudflareEmail');
const serverIP = this.database.getSetting('serverIP'); const serverIP = this.database.getSetting('serverIP');
if (!apiKey || !email) { if (!apiKey) {
logger.warn('Cloudflare credentials not configured. DNS management will be disabled.'); logger.warn('Cloudflare credentials not configured. DNS management will be disabled.');
logger.info('Configure with: onebox config set cloudflareAPIKey <key>'); logger.info('Configure with: onebox config set cloudflareAPIKey <key>');
logger.info('Configure with: onebox config set cloudflareEmail <email>');
return; return;
} }
this.serverIP = serverIP; this.serverIP = serverIP;
// Initialize Cloudflare client // Initialize Cloudflare client
// The CloudflareAccount class expects just the API key/token // The official Cloudflare SDK expects the API token
this.cloudflareClient = new plugins.cloudflare.CloudflareAccount(apiKey); logger.info(`Initializing Cloudflare client with token: ${apiKey.substring(0, 10)}...`);
this.cloudflareClient = new Cloudflare({
apiToken: apiKey,
});
logger.info('Cloudflare client initialized successfully');
// Try to get zone ID from settings, or auto-detect // Load all available zones
let zoneID = this.database.getSetting('cloudflareZoneID'); logger.info('Loading Cloudflare zones...');
const zoneList = [];
if (!zoneID) { for await (const zone of this.cloudflareClient.zones.list()) {
// Auto-detect zones zoneList.push(zone);
logger.info('No zone ID configured, fetching available zones...'); this.zones.set(zone.name, zone.id);
const zones = await this.cloudflareClient.convenience.listZones();
if (zones.length === 0) {
logger.warn('No Cloudflare zones found for this account');
return;
} else if (zones.length === 1) {
// Auto-select the only zone
zoneID = zones[0].id;
this.database.setSetting('cloudflareZoneID', zoneID);
logger.success(`Auto-selected zone: ${zones[0].name} (${zoneID})`);
} else {
// Multiple zones found - log them for user to choose
logger.info(`Found ${zones.length} Cloudflare zones:`);
for (const zone of zones) {
logger.info(` - ${zone.name} (ID: ${zone.id})`);
}
logger.warn('Multiple zones found. Please set one with: onebox config set cloudflareZoneID <id>');
return;
}
} }
this.zoneID = zoneID; if (zoneList.length === 0) {
logger.info('DNS manager initialized with Cloudflare'); logger.warn('No Cloudflare zones found for this account');
return;
}
logger.info(`Loaded ${zoneList.length} Cloudflare zone(s):`);
for (const zone of zoneList) {
logger.info(` - ${zone.name} (ID: ${zone.id})`);
}
logger.success('DNS manager initialized with multi-zone support');
} catch (error) { } catch (error) {
logger.error(`Failed to initialize DNS manager: ${error.message}`); logger.error(`Failed to initialize DNS manager: ${error.message}`);
if (error.message && error.message.includes('Authorization header')) { if (error.message && error.message.includes('Authorization header')) {
@@ -88,7 +79,29 @@ export class OneboxDnsManager {
* Check if DNS manager is configured * Check if DNS manager is configured
*/ */
isConfigured(): boolean { isConfigured(): boolean {
return this.cloudflareClient !== null && this.zoneID !== null; return this.cloudflareClient !== null && this.zones.size > 0;
}
/**
* Find the zone ID for a given domain
* Example: "api.task.vc" -> finds "task.vc" zone
*/
private findZoneForDomain(domain: string): string | null {
// Try exact match first
if (this.zones.has(domain)) {
return this.zones.get(domain)!;
}
// Try finding parent zone (e.g., "api.task.vc" -> "task.vc")
const parts = domain.split('.');
for (let i = 1; i < parts.length; i++) {
const candidate = parts.slice(i).join('.');
if (this.zones.has(candidate)) {
return this.zones.get(candidate)!;
}
}
return null;
} }
/** /**
@@ -107,6 +120,14 @@ export class OneboxDnsManager {
throw new Error('Server IP not configured. Set with: onebox config set serverIP <ip>'); throw new Error('Server IP not configured. Set with: onebox config set serverIP <ip>');
} }
// Find the zone for this domain
const zoneID = this.findZoneForDomain(domain);
if (!zoneID) {
throw new Error(
`No Cloudflare zone found for domain ${domain}. Available zones: ${Array.from(this.zones.keys()).join(', ')}`
);
}
// Check if record already exists // Check if record already exists
const existing = await this.getDNSRecord(domain); const existing = await this.getDNSRecord(domain);
if (existing) { if (existing) {
@@ -115,7 +136,8 @@ export class OneboxDnsManager {
} }
// Create A record // Create A record
const response = await this.cloudflareClient!.zones.dns.records.create(this.zoneID!, { const response = await this.cloudflareClient!.dns.records.create({
zone_id: zoneID,
type: 'A', type: 'A',
name: domain, name: domain,
content: targetIP, content: targetIP,
@@ -125,8 +147,8 @@ export class OneboxDnsManager {
// Store in database // Store in database
await this.database.query( await this.database.query(
'INSERT INTO dns_records (domain, type, value, cloudflare_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', 'INSERT INTO dns_records (domain, type, value, cloudflare_id, zone_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[domain, 'A', targetIP, response.result.id, Date.now(), Date.now()] [domain, 'A', targetIP, response.id, zoneID, Date.now(), Date.now()]
); );
logger.success(`DNS record created for ${domain}${targetIP}`); logger.success(`DNS record created for ${domain}${targetIP}`);
@@ -148,7 +170,7 @@ export class OneboxDnsManager {
logger.info(`Removing DNS record for ${domain}`); logger.info(`Removing DNS record for ${domain}`);
// Get record from database // Get record from database
const rows = this.database.query('SELECT cloudflare_id FROM dns_records WHERE domain = ?', [ const rows = this.database.query('SELECT cloudflare_id, zone_id FROM dns_records WHERE domain = ?', [
domain, domain,
]); ]);
@@ -157,11 +179,14 @@ export class OneboxDnsManager {
return; return;
} }
const cloudflareID = String(rows[0][0]); const cloudflareID = String((rows[0] as any).cloudflare_id || rows[0][0]);
const zoneID = String((rows[0] as any).zone_id || rows[0][1]);
// Delete from Cloudflare // Delete from Cloudflare
if (cloudflareID) { if (cloudflareID && zoneID) {
await this.cloudflareClient!.zones.dns.records.delete(this.zoneID!, cloudflareID); await this.cloudflareClient!.dns.records.delete(cloudflareID, {
zone_id: zoneID,
});
} }
// Delete from database // Delete from database
@@ -187,11 +212,13 @@ export class OneboxDnsManager {
const rows = this.database.query('SELECT * FROM dns_records WHERE domain = ?', [domain]); const rows = this.database.query('SELECT * FROM dns_records WHERE domain = ?', [domain]);
if (rows.length > 0) { if (rows.length > 0) {
const row = rows[0] as any;
return { return {
domain: String(rows[0][1]), domain: String(row.domain || row[1]),
type: String(rows[0][2]), type: String(row.type || row[2]),
value: String(rows[0][3]), value: String(row.value || row[3]),
cloudflareID: rows[0][4] ? String(rows[0][4]) : null, cloudflareID: row.cloudflare_id ? String(row.cloudflare_id) : (row[4] ? String(row[4]) : null),
zoneID: row.zone_id ? String(row.zone_id) : (row[5] ? String(row[5]) : null),
}; };
} }
@@ -209,14 +236,15 @@ export class OneboxDnsManager {
try { try {
const rows = this.database.query('SELECT * FROM dns_records ORDER BY created_at DESC'); const rows = this.database.query('SELECT * FROM dns_records ORDER BY created_at DESC');
return rows.map((row) => ({ return rows.map((row: any) => ({
id: Number(row[0]), id: Number(row.id || row[0]),
domain: String(row[1]), domain: String(row.domain || row[1]),
type: String(row[2]), type: String(row.type || row[2]),
value: String(row[3]), value: String(row.value || row[3]),
cloudflareID: row[4] ? String(row[4]) : null, cloudflareID: row.cloudflare_id ? String(row.cloudflare_id) : (row[4] ? String(row[4]) : null),
createdAt: Number(row[5]), zoneID: row.zone_id ? String(row.zone_id) : (row[5] ? String(row[5]) : null),
updatedAt: Number(row[6]), createdAt: Number(row.created_at || row[6]),
updatedAt: Number(row.updated_at || row[7]),
})); }));
} catch (error) { } catch (error) {
logger.error(`Failed to list DNS records: ${error.message}`); logger.error(`Failed to list DNS records: ${error.message}`);
@@ -233,26 +261,35 @@ export class OneboxDnsManager {
throw new Error('DNS manager not configured'); throw new Error('DNS manager not configured');
} }
logger.info('Syncing DNS records from Cloudflare...'); logger.info('Syncing DNS records from all Cloudflare zones...');
const response = await this.cloudflareClient!.zones.dns.records.list(this.zoneID!); // Sync records from all zones
const records = response.result; for (const [zoneName, zoneID] of this.zones.entries()) {
logger.info(`Syncing zone: ${zoneName}`);
// Only sync A records const records = [];
const aRecords = records.filter((r: any) => r.type === 'A'); for await (const record of this.cloudflareClient!.dns.records.list({
zone_id: zoneID,
})) {
records.push(record);
}
for (const record of aRecords) { // Only sync A records
// Check if exists in database const aRecords = records.filter((r) => r.type === 'A');
const existing = await this.getDNSRecord(record.name);
if (!existing) { for (const record of aRecords) {
// Add to database // Check if exists in database
await this.database.query( const existing = await this.getDNSRecord(record.name);
'INSERT INTO dns_records (domain, type, value, cloudflare_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[record.name, record.type, record.content, record.id, Date.now(), Date.now()]
);
logger.info(`Synced DNS record: ${record.name}`); if (!existing) {
// Add to database
await this.database.query(
'INSERT INTO dns_records (domain, type, value, cloudflare_id, zone_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[record.name, record.type, record.content, record.id, zoneID, Date.now(), Date.now()]
);
logger.info(`Synced DNS record: ${record.name}`);
}
} }
} }

View File

@@ -26,8 +26,8 @@ import { DockerHost } from '@apiclient.xyz/docker';
export const docker = { Docker: DockerHost }; export const docker = { Docker: DockerHost };
// Cloudflare DNS Management // Cloudflare DNS Management
import * as cloudflare from '@apiclient.xyz/cloudflare'; import Cloudflare from 'npm:cloudflare@5.2.0';
export { cloudflare }; export const cloudflare = { Cloudflare };
// Let's Encrypt / ACME // Let's Encrypt / ACME
import * as smartacme from '@push.rocks/smartacme'; import * as smartacme from '@push.rocks/smartacme';