delete bulk and export/import
Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
26
app/api/domains/[domain]/bulk-delete/route.js
Archivo normal
26
app/api/domains/[domain]/bulk-delete/route.js
Archivo normal
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/api/domains/[domain]/export/route.js
Archivo normal
27
app/api/domains/[domain]/export/route.js
Archivo normal
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/api/domains/[domain]/import/route.js
Archivo normal
37
app/api/domains/[domain]/import/route.js
Archivo normal
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
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 DNSManager = () => {
|
||||||
const [domains, setDomains] = useState([]);
|
const [domains, setDomains] = useState([]);
|
||||||
@@ -25,6 +25,10 @@ const DNSManager = () => {
|
|||||||
target: '',
|
target: '',
|
||||||
type: 'A'
|
type: 'A'
|
||||||
});
|
});
|
||||||
|
const [showImport, setShowImport] = useState(false);
|
||||||
|
const [importContent, setImportContent] = useState('');
|
||||||
|
const [replaceAll, setReplaceAll] = useState(false);
|
||||||
|
const [importResult, setImportResult] = useState(null);
|
||||||
|
|
||||||
const fetchDomains = async () => {
|
const fetchDomains = async () => {
|
||||||
try {
|
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 toggleRecordSelection = (recordId) => {
|
||||||
const newSelection = new Set(selectedRecords);
|
const newSelection = new Set(selectedRecords);
|
||||||
if (newSelection.has(recordId)) {
|
if (newSelection.has(recordId)) {
|
||||||
@@ -271,12 +366,30 @@ const DNSManager = () => {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-7">
|
<div className="pt-7 flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleExport}
|
||||||
|
disabled={!selectedDomain}
|
||||||
|
className="inline-flex items-center px-4 py-3 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 transition-all"
|
||||||
|
title="Exportar zona BIND9"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
Exportar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowImport(true)}
|
||||||
|
disabled={!selectedDomain}
|
||||||
|
className="inline-flex items-center px-4 py-3 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 transition-all"
|
||||||
|
title="Importar zona BIND9"
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
|
Importar
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAddRecord(true)}
|
onClick={() => setShowAddRecord(true)}
|
||||||
className="inline-flex items-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all"
|
className="inline-flex items-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Add Record
|
Add Record
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -320,12 +433,22 @@ const DNSManager = () => {
|
|||||||
<span className="text-sm font-medium text-blue-900">
|
<span className="text-sm font-medium text-blue-900">
|
||||||
{selectedRecords.size} registro(s) seleccionado(s)
|
{selectedRecords.size} registro(s) seleccionado(s)
|
||||||
</span>
|
</span>
|
||||||
<button
|
<div className="flex gap-2">
|
||||||
onClick={() => setBulkUpdate({ ...bulkUpdate, show: true })}
|
<button
|
||||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all"
|
onClick={() => setBulkUpdate({ ...bulkUpdate, show: true })}
|
||||||
>
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all"
|
||||||
Actualización masiva
|
>
|
||||||
</button>
|
<Edit className="w-4 h-4 mr-2" />
|
||||||
|
Actualización masiva
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleBulkDelete}
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all"
|
||||||
|
>
|
||||||
|
<Trash className="w-4 h-4 mr-2" />
|
||||||
|
Eliminar seleccionados
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -605,6 +728,94 @@ const DNSManager = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Import modal */}
|
||||||
|
{showImport && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||||
|
<div className="bg-white rounded-2xl shadow-2xl max-w-3xl w-full p-8 max-h-[90vh] overflow-y-auto">
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 mb-6">Importar Zona BIND9</h3>
|
||||||
|
|
||||||
|
{importResult ? (
|
||||||
|
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||||
|
<h4 className="font-semibold text-green-900 mb-2">Importación completada</h4>
|
||||||
|
<p className="text-sm text-green-800">
|
||||||
|
Total: {importResult.summary.total} |
|
||||||
|
Exitosos: {importResult.summary.success} |
|
||||||
|
Fallidos: {importResult.summary.failed}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Cargar archivo .zone
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".zone,.txt"
|
||||||
|
onChange={handleFileImport}
|
||||||
|
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
O pegar contenido de zona BIND9
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={importContent}
|
||||||
|
onChange={(e) => setImportContent(e.target.value)}
|
||||||
|
placeholder="example.com. 3600 IN A 192.168.1.1 www 3600 IN CNAME example.com."
|
||||||
|
rows={12}
|
||||||
|
className="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 py-2 px-3 font-mono text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={replaceAll}
|
||||||
|
onChange={(e) => setReplaceAll(e.target.checked)}
|
||||||
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<span className="ml-2 text-sm text-gray-700">
|
||||||
|
Reemplazar todos los registros existentes (excepto SOA y NS raíz)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{replaceAll && (
|
||||||
|
<p className="mt-2 text-sm text-red-600">
|
||||||
|
⚠️ Advertencia: Esta acción eliminará todos los registros existentes antes de importar.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-end space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowImport(false);
|
||||||
|
setImportContent('');
|
||||||
|
setReplaceAll(false);
|
||||||
|
}}
|
||||||
|
className="px-6 py-2 border border-gray-300 rounded-lg shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-all"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 inline mr-1" />
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={!importContent.trim()}
|
||||||
|
className="px-6 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 disabled:opacity-50 transition-all"
|
||||||
|
>
|
||||||
|
<Upload className="h-4 w-4 inline mr-1" />
|
||||||
|
Importar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -166,6 +166,232 @@ export class OVHService {
|
|||||||
return results;
|
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() {
|
getConfig() {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user