update
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
for await (const zone of this.cloudflareClient.zones.list()) {
|
||||||
|
zoneList.push(zone);
|
||||||
|
this.zones.set(zone.name, zone.id);
|
||||||
|
}
|
||||||
|
|
||||||
if (!zoneID) {
|
if (zoneList.length === 0) {
|
||||||
// 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');
|
logger.warn('No Cloudflare zones found for this account');
|
||||||
return;
|
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;
|
logger.info(`Loaded ${zoneList.length} Cloudflare zone(s):`);
|
||||||
logger.info('DNS manager initialized with Cloudflare');
|
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,13 +261,21 @@ 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}`);
|
||||||
|
|
||||||
|
const records = [];
|
||||||
|
for await (const record of this.cloudflareClient!.dns.records.list({
|
||||||
|
zone_id: zoneID,
|
||||||
|
})) {
|
||||||
|
records.push(record);
|
||||||
|
}
|
||||||
|
|
||||||
// Only sync A records
|
// Only sync A records
|
||||||
const aRecords = records.filter((r: any) => r.type === 'A');
|
const aRecords = records.filter((r) => r.type === 'A');
|
||||||
|
|
||||||
for (const record of aRecords) {
|
for (const record of aRecords) {
|
||||||
// Check if exists in database
|
// Check if exists in database
|
||||||
@@ -248,13 +284,14 @@ export class OneboxDnsManager {
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
// Add to database
|
// Add to 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 (?, ?, ?, ?, ?, ?, ?)',
|
||||||
[record.name, record.type, record.content, record.id, Date.now(), Date.now()]
|
[record.name, record.type, record.content, record.id, zoneID, Date.now(), Date.now()]
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`Synced DNS record: ${record.name}`);
|
logger.info(`Synced DNS record: ${record.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.success('DNS records synced from Cloudflare');
|
logger.success('DNS records synced from Cloudflare');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user