update
This commit is contained in:
@@ -120,6 +120,7 @@ export class OneboxDatabase {
|
||||
type TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
cloudflare_id TEXT,
|
||||
zone_id TEXT,
|
||||
created_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');
|
||||
|
||||
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 {
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
* 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 { OneboxDatabase } from './database.ts';
|
||||
|
||||
export class OneboxDnsManager {
|
||||
private oneboxRef: any;
|
||||
private database: OneboxDatabase;
|
||||
private cloudflareClient: plugins.cloudflare.CloudflareAccount | null = null;
|
||||
private zoneID: string | null = null;
|
||||
private cloudflareClient: Cloudflare | null = null;
|
||||
private zones: Map<string, string> = new Map(); // zone name -> zone ID
|
||||
private serverIP: string | null = null;
|
||||
|
||||
constructor(oneboxRef: any) {
|
||||
@@ -27,51 +27,42 @@ export class OneboxDnsManager {
|
||||
try {
|
||||
// Get Cloudflare credentials from settings
|
||||
const apiKey = this.database.getSetting('cloudflareAPIKey');
|
||||
const email = this.database.getSetting('cloudflareEmail');
|
||||
const serverIP = this.database.getSetting('serverIP');
|
||||
|
||||
if (!apiKey || !email) {
|
||||
if (!apiKey) {
|
||||
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 cloudflareEmail <email>');
|
||||
return;
|
||||
}
|
||||
|
||||
this.serverIP = serverIP;
|
||||
|
||||
// Initialize Cloudflare client
|
||||
// The CloudflareAccount class expects just the API key/token
|
||||
this.cloudflareClient = new plugins.cloudflare.CloudflareAccount(apiKey);
|
||||
// The official Cloudflare SDK expects the API token
|
||||
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
|
||||
let zoneID = this.database.getSetting('cloudflareZoneID');
|
||||
|
||||
if (!zoneID) {
|
||||
// Auto-detect zones
|
||||
logger.info('No zone ID configured, fetching available zones...');
|
||||
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;
|
||||
}
|
||||
// Load all available zones
|
||||
logger.info('Loading Cloudflare zones...');
|
||||
const zoneList = [];
|
||||
for await (const zone of this.cloudflareClient.zones.list()) {
|
||||
zoneList.push(zone);
|
||||
this.zones.set(zone.name, zone.id);
|
||||
}
|
||||
|
||||
this.zoneID = zoneID;
|
||||
logger.info('DNS manager initialized with Cloudflare');
|
||||
if (zoneList.length === 0) {
|
||||
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) {
|
||||
logger.error(`Failed to initialize DNS manager: ${error.message}`);
|
||||
if (error.message && error.message.includes('Authorization header')) {
|
||||
@@ -88,7 +79,29 @@ export class OneboxDnsManager {
|
||||
* Check if DNS manager is configured
|
||||
*/
|
||||
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>');
|
||||
}
|
||||
|
||||
// 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
|
||||
const existing = await this.getDNSRecord(domain);
|
||||
if (existing) {
|
||||
@@ -115,7 +136,8 @@ export class OneboxDnsManager {
|
||||
}
|
||||
|
||||
// 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',
|
||||
name: domain,
|
||||
content: targetIP,
|
||||
@@ -125,8 +147,8 @@ export class OneboxDnsManager {
|
||||
|
||||
// Store in database
|
||||
await this.database.query(
|
||||
'INSERT INTO dns_records (domain, type, value, cloudflare_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[domain, 'A', targetIP, response.result.id, Date.now(), Date.now()]
|
||||
'INSERT INTO dns_records (domain, type, value, cloudflare_id, zone_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[domain, 'A', targetIP, response.id, zoneID, Date.now(), Date.now()]
|
||||
);
|
||||
|
||||
logger.success(`DNS record created for ${domain} → ${targetIP}`);
|
||||
@@ -148,7 +170,7 @@ export class OneboxDnsManager {
|
||||
logger.info(`Removing DNS record for ${domain}`);
|
||||
|
||||
// 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,
|
||||
]);
|
||||
|
||||
@@ -157,11 +179,14 @@ export class OneboxDnsManager {
|
||||
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
|
||||
if (cloudflareID) {
|
||||
await this.cloudflareClient!.zones.dns.records.delete(this.zoneID!, cloudflareID);
|
||||
if (cloudflareID && zoneID) {
|
||||
await this.cloudflareClient!.dns.records.delete(cloudflareID, {
|
||||
zone_id: zoneID,
|
||||
});
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
@@ -187,11 +212,13 @@ export class OneboxDnsManager {
|
||||
const rows = this.database.query('SELECT * FROM dns_records WHERE domain = ?', [domain]);
|
||||
|
||||
if (rows.length > 0) {
|
||||
const row = rows[0] as any;
|
||||
return {
|
||||
domain: String(rows[0][1]),
|
||||
type: String(rows[0][2]),
|
||||
value: String(rows[0][3]),
|
||||
cloudflareID: rows[0][4] ? String(rows[0][4]) : null,
|
||||
domain: String(row.domain || row[1]),
|
||||
type: String(row.type || row[2]),
|
||||
value: String(row.value || row[3]),
|
||||
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 {
|
||||
const rows = this.database.query('SELECT * FROM dns_records ORDER BY created_at DESC');
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: Number(row[0]),
|
||||
domain: String(row[1]),
|
||||
type: String(row[2]),
|
||||
value: String(row[3]),
|
||||
cloudflareID: row[4] ? String(row[4]) : null,
|
||||
createdAt: Number(row[5]),
|
||||
updatedAt: Number(row[6]),
|
||||
return rows.map((row: any) => ({
|
||||
id: Number(row.id || row[0]),
|
||||
domain: String(row.domain || row[1]),
|
||||
type: String(row.type || row[2]),
|
||||
value: String(row.value || row[3]),
|
||||
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),
|
||||
createdAt: Number(row.created_at || row[6]),
|
||||
updatedAt: Number(row.updated_at || row[7]),
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error(`Failed to list DNS records: ${error.message}`);
|
||||
@@ -233,26 +261,35 @@ export class OneboxDnsManager {
|
||||
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!);
|
||||
const records = response.result;
|
||||
// Sync records from all zones
|
||||
for (const [zoneName, zoneID] of this.zones.entries()) {
|
||||
logger.info(`Syncing zone: ${zoneName}`);
|
||||
|
||||
// Only sync A records
|
||||
const aRecords = records.filter((r: any) => r.type === 'A');
|
||||
const records = [];
|
||||
for await (const record of this.cloudflareClient!.dns.records.list({
|
||||
zone_id: zoneID,
|
||||
})) {
|
||||
records.push(record);
|
||||
}
|
||||
|
||||
for (const record of aRecords) {
|
||||
// Check if exists in database
|
||||
const existing = await this.getDNSRecord(record.name);
|
||||
// Only sync A records
|
||||
const aRecords = records.filter((r) => r.type === 'A');
|
||||
|
||||
if (!existing) {
|
||||
// Add to database
|
||||
await this.database.query(
|
||||
'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()]
|
||||
);
|
||||
for (const record of aRecords) {
|
||||
// Check if exists in database
|
||||
const existing = await this.getDNSRecord(record.name);
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user