diff --git a/app/api/domains/[domain]/bulk-delete/route.js b/app/api/domains/[domain]/bulk-delete/route.js
new file mode 100644
index 0000000..ffcccb0
--- /dev/null
+++ b/app/api/domains/[domain]/bulk-delete/route.js
@@ -0,0 +1,26 @@
+import { NextResponse } from 'next/server';
+import ovhService from '@/lib/ovh-service';
+
+export async function POST(request, { params }) {
+ try {
+ const { domain } = await params;
+ const { recordIds } = await request.json();
+
+ if (!recordIds || recordIds.length === 0) {
+ return NextResponse.json(
+ { success: false, error: 'Record IDs are required' },
+ { status: 400 }
+ );
+ }
+
+ const results = await ovhService.bulkDeleteRecords(domain, recordIds);
+
+ return NextResponse.json({ success: true, results });
+ } catch (error) {
+ console.error('Error bulk deleting DNS records:', error);
+ return NextResponse.json(
+ { success: false, error: error.message },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/domains/[domain]/export/route.js b/app/api/domains/[domain]/export/route.js
new file mode 100644
index 0000000..851c4fb
--- /dev/null
+++ b/app/api/domains/[domain]/export/route.js
@@ -0,0 +1,27 @@
+import { NextResponse } from 'next/server';
+import ovhService from '@/lib/ovh-service';
+
+export async function GET(request, { params }) {
+ try {
+ const { domain } = await params;
+
+ // Get all records
+ const records = await ovhService.getDNSRecords(domain);
+
+ // Export to BIND9 format
+ const zoneFile = ovhService.exportToBind9(domain, records);
+
+ return new NextResponse(zoneFile, {
+ headers: {
+ 'Content-Type': 'text/plain',
+ 'Content-Disposition': `attachment; filename="${domain}.zone"`
+ }
+ });
+ } catch (error) {
+ console.error('Error exporting DNS zone:', error);
+ return NextResponse.json(
+ { success: false, error: error.message },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/domains/[domain]/import/route.js b/app/api/domains/[domain]/import/route.js
new file mode 100644
index 0000000..10dcf4d
--- /dev/null
+++ b/app/api/domains/[domain]/import/route.js
@@ -0,0 +1,37 @@
+import { NextResponse } from 'next/server';
+import ovhService from '@/lib/ovh-service';
+
+export async function POST(request, { params }) {
+ try {
+ const { domain } = await params;
+ const { zoneContent, replaceAll = false } = await request.json();
+
+ if (!zoneContent) {
+ return NextResponse.json(
+ { success: false, error: 'Zone content is required' },
+ { status: 400 }
+ );
+ }
+
+ const results = await ovhService.importFromBind9(domain, zoneContent, replaceAll);
+
+ const successCount = results.filter(r => r.success).length;
+ const failureCount = results.filter(r => !r.success).length;
+
+ return NextResponse.json({
+ success: true,
+ results,
+ summary: {
+ total: results.length,
+ success: successCount,
+ failed: failureCount
+ }
+ });
+ } catch (error) {
+ console.error('Error importing DNS zone:', error);
+ return NextResponse.json(
+ { success: false, error: error.message },
+ { status: 500 }
+ );
+ }
+}
diff --git a/components/DNSManager.js b/components/DNSManager.js
index f0f3549..da4b6fd 100644
--- a/components/DNSManager.js
+++ b/components/DNSManager.js
@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
-import { Globe, Plus, Edit, Trash2, RefreshCw, Check, X, Search, Filter } from 'lucide-react';
+import { Globe, Plus, Edit, Trash2, RefreshCw, Check, X, Search, Filter, Download, Upload, Trash } from 'lucide-react';
const DNSManager = () => {
const [domains, setDomains] = useState([]);
@@ -25,6 +25,10 @@ const DNSManager = () => {
target: '',
type: 'A'
});
+ const [showImport, setShowImport] = useState(false);
+ const [importContent, setImportContent] = useState('');
+ const [replaceAll, setReplaceAll] = useState(false);
+ const [importResult, setImportResult] = useState(null);
const fetchDomains = async () => {
try {
@@ -168,6 +172,97 @@ const DNSManager = () => {
}
};
+ const handleBulkDelete = async () => {
+ if (selectedRecords.size === 0) return;
+
+ if (!confirm(`¿Está seguro de que desea eliminar ${selectedRecords.size} registro(s)?`)) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/domains/${selectedDomain}/bulk-delete`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ recordIds: Array.from(selectedRecords)
+ })
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ setSelectedRecords(new Set());
+ fetchRecords();
+ }
+ } catch (error) {
+ console.error('Error bulk deleting records:', error);
+ }
+ };
+
+ const handleExport = async () => {
+ if (!selectedDomain) return;
+
+ try {
+ const response = await fetch(`/api/domains/${selectedDomain}/export`);
+
+ if (response.ok) {
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${selectedDomain}.zone`;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+ }
+ } catch (error) {
+ console.error('Error exporting zone:', error);
+ }
+ };
+
+ const handleImport = async () => {
+ if (!importContent.trim()) {
+ alert('Por favor, ingrese el contenido de la zona BIND9');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/domains/${selectedDomain}/import`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ zoneContent: importContent,
+ replaceAll: replaceAll
+ })
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ setImportResult(data);
+ setTimeout(() => {
+ setShowImport(false);
+ setImportContent('');
+ setReplaceAll(false);
+ setImportResult(null);
+ fetchRecords();
+ }, 3000);
+ }
+ } catch (error) {
+ console.error('Error importing zone:', error);
+ }
+ };
+
+ const handleFileImport = (e) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ setImportContent(event.target?.result || '');
+ };
+ reader.readAsText(file);
+ }
+ };
+
const toggleRecordSelection = (recordId) => {
const newSelection = new Set(selectedRecords);
if (newSelection.has(recordId)) {
@@ -271,12 +366,30 @@ const DNSManager = () => {
))}
-
+
+
+
@@ -320,12 +433,22 @@ const DNSManager = () => {
{selectedRecords.size} registro(s) seleccionado(s)
-
+
+
+
+
)}
@@ -605,6 +728,94 @@ const DNSManager = () => {
)}
+
+ {/* Import modal */}
+ {showImport && (
+
+
+
Importar Zona BIND9
+
+ {importResult ? (
+
+
Importación completada
+
+ Total: {importResult.summary.total} |
+ Exitosos: {importResult.summary.success} |
+ Fallidos: {importResult.summary.failed}
+
+
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {replaceAll && (
+
+ ⚠️ Advertencia: Esta acción eliminará todos los registros existentes antes de importar.
+
+ )}
+
+
+
+
+
+
+ >
+ )}
+
+
+ )}
);
};
diff --git a/lib/ovh-service.js b/lib/ovh-service.js
index 15d57ca..575eefb 100644
--- a/lib/ovh-service.js
+++ b/lib/ovh-service.js
@@ -166,6 +166,232 @@ export class OVHService {
return results;
}
+ async bulkDeleteRecords(zoneName, recordIds) {
+ const results = [];
+
+ for (const recordId of recordIds) {
+ try {
+ await this.deleteDNSRecord(zoneName, recordId);
+ results.push({ recordId, success: true });
+ } catch (error) {
+ results.push({ recordId, success: false, error: error.message });
+ }
+ }
+
+ return results;
+ }
+
+ exportToBind9(zoneName, records) {
+ const lines = [];
+ lines.push(`; Zone file for ${zoneName}`);
+ lines.push(`; Generated on ${new Date().toISOString()}`);
+ lines.push('');
+ lines.push(`$ORIGIN ${zoneName}.`);
+ lines.push('');
+
+ // Sort records by type for better readability
+ const sortedRecords = [...records].sort((a, b) => {
+ const typeOrder = { SOA: 0, NS: 1, A: 2, AAAA: 3, CNAME: 4, MX: 5, TXT: 6, SRV: 7 };
+ return (typeOrder[a.fieldType] || 99) - (typeOrder[b.fieldType] || 99);
+ });
+
+ for (const record of sortedRecords) {
+ const subdomain = record.subDomain || '@';
+ const ttl = record.ttl || 3600;
+
+ let line = '';
+
+ switch (record.fieldType) {
+ case 'A':
+ case 'AAAA':
+ line = `${subdomain}\t${ttl}\tIN\t${record.fieldType}\t${record.target}`;
+ break;
+ case 'CNAME':
+ line = `${subdomain}\t${ttl}\tIN\tCNAME\t${record.target}${record.target.endsWith('.') ? '' : '.'}`;
+ break;
+ case 'MX':
+ const priority = record.priority || 10;
+ line = `${subdomain}\t${ttl}\tIN\tMX\t${priority} ${record.target}${record.target.endsWith('.') ? '' : '.'}`;
+ break;
+ case 'TXT':
+ const txtValue = record.target.includes(' ') && !record.target.startsWith('"')
+ ? `"${record.target}"`
+ : record.target;
+ line = `${subdomain}\t${ttl}\tIN\tTXT\t${txtValue}`;
+ break;
+ case 'SRV':
+ const priority_srv = record.priority || 0;
+ const weight = record.weight || 0;
+ const port = record.port || 0;
+ line = `${subdomain}\t${ttl}\tIN\tSRV\t${priority_srv} ${weight} ${port} ${record.target}${record.target.endsWith('.') ? '' : '.'}`;
+ break;
+ case 'NS':
+ line = `${subdomain}\t${ttl}\tIN\tNS\t${record.target}${record.target.endsWith('.') ? '' : '.'}`;
+ break;
+ case 'CAA':
+ const flags = record.flags || 0;
+ const tag = record.tag || 'issue';
+ line = `${subdomain}\t${ttl}\tIN\tCAA\t${flags} ${tag} "${record.target}"`;
+ break;
+ default:
+ line = `${subdomain}\t${ttl}\tIN\t${record.fieldType}\t${record.target}`;
+ }
+
+ lines.push(line);
+ }
+
+ lines.push('');
+ return lines.join('\n');
+ }
+
+ parseFromBind9(zoneName, zoneContent) {
+ const records = [];
+ const lines = zoneContent.split('\n');
+
+ let currentOrigin = zoneName;
+
+ for (let line of lines) {
+ // Remove comments
+ const commentIndex = line.indexOf(';');
+ if (commentIndex !== -1) {
+ line = line.substring(0, commentIndex);
+ }
+
+ line = line.trim();
+
+ // Skip empty lines
+ if (!line) continue;
+
+ // Handle $ORIGIN directive
+ if (line.startsWith('$ORIGIN')) {
+ const parts = line.split(/\s+/);
+ if (parts[1]) {
+ currentOrigin = parts[1].replace(/\.$/, '');
+ }
+ continue;
+ }
+
+ // Skip $TTL and other directives for now
+ if (line.startsWith('$')) continue;
+
+ // Parse record line
+ const parts = line.split(/\s+/);
+ if (parts.length < 4) continue;
+
+ let idx = 0;
+ let subdomain = parts[idx++];
+
+ // Handle @ symbol
+ if (subdomain === '@') {
+ subdomain = '';
+ }
+
+ // Remove trailing dot from subdomain
+ if (subdomain.endsWith('.')) {
+ subdomain = subdomain.slice(0, -1);
+ }
+
+ let ttl = 3600;
+ let recordClass = 'IN';
+ let recordType = '';
+
+ // Parse TTL (if it's a number)
+ if (!isNaN(parts[idx])) {
+ ttl = parseInt(parts[idx++]);
+ }
+
+ // Parse class (usually IN)
+ if (parts[idx] === 'IN' || parts[idx] === 'CH' || parts[idx] === 'HS') {
+ recordClass = parts[idx++];
+ }
+
+ // Parse record type
+ recordType = parts[idx++];
+
+ // Parse record data based on type
+ const recordData = {
+ fieldType: recordType,
+ subDomain: subdomain,
+ ttl: ttl
+ };
+
+ switch (recordType) {
+ case 'A':
+ case 'AAAA':
+ recordData.target = parts[idx];
+ break;
+ case 'CNAME':
+ case 'NS':
+ recordData.target = parts[idx].replace(/\.$/, '');
+ break;
+ case 'MX':
+ recordData.priority = parseInt(parts[idx++]);
+ recordData.target = parts[idx].replace(/\.$/, '');
+ break;
+ case 'TXT':
+ // Join remaining parts and remove quotes
+ const txtValue = parts.slice(idx).join(' ');
+ recordData.target = txtValue.replace(/^"|"$/g, '');
+ break;
+ case 'SRV':
+ recordData.priority = parseInt(parts[idx++]);
+ recordData.weight = parseInt(parts[idx++]);
+ recordData.port = parseInt(parts[idx++]);
+ recordData.target = parts[idx].replace(/\.$/, '');
+ break;
+ case 'CAA':
+ recordData.flags = parseInt(parts[idx++]);
+ recordData.tag = parts[idx++];
+ const caaValue = parts.slice(idx).join(' ');
+ recordData.target = caaValue.replace(/^"|"$/g, '');
+ break;
+ default:
+ recordData.target = parts.slice(idx).join(' ');
+ }
+
+ // Skip SOA records as they are managed by OVH
+ if (recordType !== 'SOA') {
+ records.push(recordData);
+ }
+ }
+
+ return records;
+ }
+
+ async importFromBind9(zoneName, zoneContent, replaceAll = false) {
+ const records = this.parseFromBind9(zoneName, zoneContent);
+
+ if (replaceAll) {
+ // Delete all existing records (except SOA and NS for zone apex)
+ const existingRecords = await this.getDNSRecords(zoneName);
+ const recordsToDelete = existingRecords.filter(r =>
+ r.fieldType !== 'SOA' &&
+ !(r.fieldType === 'NS' && (!r.subDomain || r.subDomain === ''))
+ );
+
+ for (const record of recordsToDelete) {
+ try {
+ await this.deleteDNSRecord(zoneName, record.id);
+ } catch (error) {
+ console.error(`Failed to delete record ${record.id}:`, error);
+ }
+ }
+ }
+
+ // Create all imported records
+ const results = [];
+ for (const recordData of records) {
+ try {
+ const result = await this.createDNSRecord(zoneName, recordData);
+ results.push({ success: true, record: result });
+ } catch (error) {
+ results.push({ success: false, error: error.message, record: recordData });
+ }
+ }
+
+ return results;
+ }
+
getConfig() {
return this.config;
}