612
components/DNSManager.js
Archivo normal
612
components/DNSManager.js
Archivo normal
@@ -0,0 +1,612 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Globe, Plus, Edit, Trash2, RefreshCw, Check, X, Search, Filter } from 'lucide-react';
|
||||
|
||||
const DNSManager = () => {
|
||||
const [domains, setDomains] = useState([]);
|
||||
const [selectedDomain, setSelectedDomain] = useState('');
|
||||
const [records, setRecords] = useState([]);
|
||||
const [filteredRecords, setFilteredRecords] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showAddRecord, setShowAddRecord] = useState(false);
|
||||
const [editingRecord, setEditingRecord] = useState(null);
|
||||
const [selectedRecords, setSelectedRecords] = useState(new Set());
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filterType, setFilterType] = useState('all');
|
||||
const [newRecord, setNewRecord] = useState({
|
||||
fieldType: 'A',
|
||||
subDomain: '',
|
||||
target: '',
|
||||
ttl: 3600
|
||||
});
|
||||
const [bulkUpdate, setBulkUpdate] = useState({
|
||||
show: false,
|
||||
target: '',
|
||||
type: 'A'
|
||||
});
|
||||
|
||||
const fetchDomains = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/domains');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.domains) {
|
||||
const domainList = data.domains.map(d => typeof d === 'string' ? d : d.domain);
|
||||
setDomains(domainList);
|
||||
|
||||
if (domainList.length > 0 && !selectedDomain) {
|
||||
setSelectedDomain(domainList[0]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching domains:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRecords = async () => {
|
||||
if (!selectedDomain) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/domains/${selectedDomain}/records`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setRecords(data.records || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching records:', error);
|
||||
}
|
||||
setLoading(false);
|
||||
setSelectedRecords(new Set());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDomains();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDomain) {
|
||||
fetchRecords();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedDomain]);
|
||||
|
||||
useEffect(() => {
|
||||
let filtered = records;
|
||||
|
||||
if (filterType !== 'all') {
|
||||
filtered = filtered.filter(r => r.fieldType === filterType);
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(r =>
|
||||
r.subDomain?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
r.target?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
setFilteredRecords(filtered);
|
||||
}, [records, searchTerm, filterType]);
|
||||
|
||||
const handleAddRecord = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/domains/${selectedDomain}/records`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newRecord)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setShowAddRecord(false);
|
||||
setNewRecord({ fieldType: 'A', subDomain: '', target: '', ttl: 3600 });
|
||||
fetchRecords();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding record:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateRecord = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/domains/${selectedDomain}/records`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editingRecord)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setEditingRecord(null);
|
||||
fetchRecords();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating record:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRecord = async (recordId) => {
|
||||
if (!confirm('Are you sure you want to delete this record?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/domains/${selectedDomain}/records?recordId=${recordId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
fetchRecords();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting record:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkUpdate = async () => {
|
||||
if (selectedRecords.size === 0) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/domains/${selectedDomain}/bulk-update`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
domain: selectedDomain,
|
||||
recordIds: Array.from(selectedRecords),
|
||||
target: bulkUpdate.target,
|
||||
fieldType: bulkUpdate.type
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setBulkUpdate({ show: false, target: '', type: 'A' });
|
||||
setSelectedRecords(new Set());
|
||||
fetchRecords();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error bulk updating records:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRecordSelection = (recordId) => {
|
||||
const newSelection = new Set(selectedRecords);
|
||||
if (newSelection.has(recordId)) {
|
||||
newSelection.delete(recordId);
|
||||
} else {
|
||||
newSelection.add(recordId);
|
||||
}
|
||||
setSelectedRecords(newSelection);
|
||||
};
|
||||
|
||||
const selectAllFiltered = () => {
|
||||
if (selectedRecords.size === filteredRecords.length) {
|
||||
setSelectedRecords(new Set());
|
||||
} else {
|
||||
setSelectedRecords(new Set(filteredRecords.map(r => r.id)));
|
||||
}
|
||||
};
|
||||
|
||||
const refreshDNSZone = async () => {
|
||||
if (!selectedDomain) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/dns/refresh', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ domain: selectedDomain })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await fetchRecords();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing DNS zone:', error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const getRecordTypeColor = (type) => {
|
||||
const colors = {
|
||||
'A': 'bg-blue-500 text-white',
|
||||
'AAAA': 'bg-purple-500 text-white',
|
||||
'CNAME': 'bg-green-500 text-white',
|
||||
'MX': 'bg-orange-500 text-white',
|
||||
'TXT': 'bg-gray-500 text-white',
|
||||
'SRV': 'bg-pink-500 text-white',
|
||||
'NS': 'bg-yellow-500 text-white'
|
||||
};
|
||||
return colors[type] || 'bg-gray-500 text-white';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center">
|
||||
<div className="bg-gradient-to-br from-blue-500 to-purple-600 p-3 rounded-xl mr-4">
|
||||
<Globe className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-gray-900">DNS Manager</h2>
|
||||
<p className="text-gray-500 mt-1">Manage your domain DNS records</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={fetchRecords}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 transition-all"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</button>
|
||||
<button
|
||||
onClick={refreshDNSZone}
|
||||
disabled={loading || !selectedDomain}
|
||||
className="inline-flex items-center px-4 py-2 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"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
Refresh Zone
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Domain selector and actions */}
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select Domain
|
||||
</label>
|
||||
<select
|
||||
value={selectedDomain}
|
||||
onChange={(e) => setSelectedDomain(e.target.value)}
|
||||
className="block w-full pl-4 pr-10 py-3 text-base border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 rounded-lg transition-all"
|
||||
>
|
||||
{domains.map((domain) => (
|
||||
<option key={domain} value={domain}>
|
||||
{domain}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="pt-7">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Record
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search and Filter */}
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por subdominio o valor..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="block w-full pl-10 pr-4 py-3 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="h-5 w-5 text-gray-400" />
|
||||
<select
|
||||
value={filterType}
|
||||
onChange={(e) => setFilterType(e.target.value)}
|
||||
className="block pl-4 pr-10 py-3 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||||
>
|
||||
<option value="all">Todos los tipos</option>
|
||||
<option value="A">A (IPv4)</option>
|
||||
<option value="AAAA">AAAA (IPv6)</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="MX">MX</option>
|
||||
<option value="TXT">TXT</option>
|
||||
<option value="SRV">SRV</option>
|
||||
<option value="NS">NS</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bulk actions */}
|
||||
{selectedRecords.size > 0 && (
|
||||
<div className="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-blue-900">
|
||||
{selectedRecords.size} registro(s) seleccionado(s)
|
||||
</span>
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add record form */}
|
||||
{showAddRecord && (
|
||||
<div className="mb-6 p-6 border-2 border-blue-200 rounded-xl bg-gradient-to-br from-blue-50 to-indigo-50">
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-6">Nuevo Registro DNS</h3>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Type</label>
|
||||
<select
|
||||
value={newRecord.fieldType}
|
||||
onChange={(e) => setNewRecord({ ...newRecord, fieldType: e.target.value })}
|
||||
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"
|
||||
>
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="MX">MX</option>
|
||||
<option value="TXT">TXT</option>
|
||||
<option value="SRV">SRV</option>
|
||||
<option value="NS">NS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Subdomain</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newRecord.subDomain}
|
||||
onChange={(e) => setNewRecord({ ...newRecord, subDomain: e.target.value })}
|
||||
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"
|
||||
placeholder="www, mail, etc."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Valor</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newRecord.target}
|
||||
onChange={(e) => setNewRecord({ ...newRecord, target: e.target.value })}
|
||||
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"
|
||||
placeholder="IP, dominio, etc."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">TTL</label>
|
||||
<input
|
||||
type="number"
|
||||
value={newRecord.ttl}
|
||||
onChange={(e) => setNewRecord({ ...newRecord, ttl: parseInt(e.target.value) })}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => setShowAddRecord(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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddRecord}
|
||||
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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Records table */}
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<RefreshCw className="h-12 w-12 animate-spin mx-auto text-blue-500" />
|
||||
<p className="mt-4 text-lg text-gray-500">Loading records...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-hidden shadow-lg ring-1 ring-black ring-opacity-5 rounded-xl">
|
||||
<table className="min-w-full divide-y divide-gray-300">
|
||||
<thead className="bg-gradient-to-r from-gray-50 to-gray-100">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRecords.size === filteredRecords.length && filteredRecords.length > 0}
|
||||
onChange={selectAllFiltered}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Tipo
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Nombre
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Valor
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
TTL
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredRecords.map((record, idx) => (
|
||||
<tr key={record.id} className={`hover:bg-gray-50 transition-colors ${idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`}>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRecords.has(record.id)}
|
||||
onChange={() => toggleRecordSelection(record.id)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${getRecordTypeColor(record.fieldType)}`}>
|
||||
{record.fieldType}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{record.subDomain || '@'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-900 max-w-md truncate">
|
||||
{record.target}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{record.ttl}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
onClick={() => setEditingRecord(record)}
|
||||
className="text-blue-600 hover:text-blue-900 mr-4 transition-colors"
|
||||
>
|
||||
<Edit className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteRecord(record.id)}
|
||||
className="text-red-600 hover:text-red-900 transition-colors"
|
||||
>
|
||||
<Trash2 className="h-5 w-5" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{filteredRecords.length === 0 && (
|
||||
<div className="text-center py-12 bg-gray-50">
|
||||
<Globe className="h-16 w-16 mx-auto text-gray-300 mb-4" />
|
||||
<p className="text-gray-500">No se encontraron registros DNS</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit record modal */}
|
||||
{editingRecord && (
|
||||
<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-lg w-full p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-6">Edit Record</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Type</label>
|
||||
<select
|
||||
value={editingRecord.fieldType}
|
||||
onChange={(e) => setEditingRecord({ ...editingRecord, fieldType: e.target.value })}
|
||||
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"
|
||||
>
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="MX">MX</option>
|
||||
<option value="TXT">TXT</option>
|
||||
<option value="SRV">SRV</option>
|
||||
<option value="NS">NS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Subdomain</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingRecord.subDomain}
|
||||
onChange={(e) => setEditingRecord({ ...editingRecord, subDomain: e.target.value })}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Valor</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingRecord.target}
|
||||
onChange={(e) => setEditingRecord({ ...editingRecord, target: e.target.value })}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">TTL</label>
|
||||
<input
|
||||
type="number"
|
||||
value={editingRecord.ttl}
|
||||
onChange={(e) => setEditingRecord({ ...editingRecord, ttl: parseInt(e.target.value) })}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => setEditingRecord(null)}
|
||||
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" />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpdateRecord}
|
||||
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 transition-all"
|
||||
>
|
||||
<Check className="h-4 w-4 inline mr-1" />
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bulk update modal */}
|
||||
{bulkUpdate.show && (
|
||||
<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-lg w-full p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-6">Bulk Update</h3>
|
||||
<p className="text-sm text-gray-600 mb-6">
|
||||
Update {selectedRecords.size} selected record(s)
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Record Type</label>
|
||||
<select
|
||||
value={bulkUpdate.type}
|
||||
onChange={(e) => setBulkUpdate({ ...bulkUpdate, type: e.target.value })}
|
||||
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"
|
||||
>
|
||||
<option value="A">A (IPv4)</option>
|
||||
<option value="AAAA">AAAA (IPv6)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">New Value (IP)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={bulkUpdate.target}
|
||||
onChange={(e) => setBulkUpdate({ ...bulkUpdate, target: e.target.value })}
|
||||
placeholder={bulkUpdate.type === 'A' ? '192.168.1.1' : '2001:0db8:85a3::8a2e:0370:7334'}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => setBulkUpdate({ show: false, target: '', type: 'A' })}
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBulkUpdate}
|
||||
disabled={!bulkUpdate.target}
|
||||
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"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DNSManager;
|
||||
425
components/Settings.js
Archivo normal
425
components/Settings.js
Archivo normal
@@ -0,0 +1,425 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Settings as SettingsIcon, Plus, Trash2, Save, RefreshCw, Wifi, Clock } from 'lucide-react';
|
||||
|
||||
const Settings = () => {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [currentIPs, setCurrentIPs] = useState({ ipv4: '', ipv6: '' });
|
||||
const [checkingIP, setCheckingIP] = useState(false);
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setConfig(data.config);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching config:', error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const fetchCurrentIPs = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/ip/current');
|
||||
const data = await response.json();
|
||||
if (data.success && data.ips) {
|
||||
setCurrentIPs(data.ips);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching current IPs:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkIPs = async () => {
|
||||
setCheckingIP(true);
|
||||
try {
|
||||
const response = await fetch('/api/ip/current', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setCurrentIPs(data.newIPs);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking IPs:', error);
|
||||
}
|
||||
setCheckingIP(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Initial data loading
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
fetchConfig();
|
||||
fetchCurrentIPs();
|
||||
}, []);
|
||||
|
||||
const saveConfig = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Configuration saved successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving config:', error);
|
||||
alert('Error saving configuration');
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const addAccount = () => {
|
||||
const newAccount = {
|
||||
id: `account${Date.now()}`,
|
||||
name: 'New Account',
|
||||
appKey: '',
|
||||
appSecret: '',
|
||||
consumerKey: '',
|
||||
endpoint: 'ovh-eu',
|
||||
domains: []
|
||||
};
|
||||
|
||||
setConfig({
|
||||
...config,
|
||||
ovhAccounts: [...config.ovhAccounts, newAccount]
|
||||
});
|
||||
};
|
||||
|
||||
const removeAccount = (accountId) => {
|
||||
if (!confirm('Are you sure you want to delete this account?')) return;
|
||||
|
||||
setConfig({
|
||||
...config,
|
||||
ovhAccounts: config.ovhAccounts.filter(acc => acc.id !== accountId)
|
||||
});
|
||||
};
|
||||
|
||||
const updateAccount = (accountId, field, value) => {
|
||||
setConfig({
|
||||
...config,
|
||||
ovhAccounts: config.ovhAccounts.map(acc =>
|
||||
acc.id === accountId ? { ...acc, [field]: value } : acc
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
const addDomain = (accountId) => {
|
||||
const domain = prompt('Enter the domain name:');
|
||||
if (!domain) return;
|
||||
|
||||
setConfig({
|
||||
...config,
|
||||
ovhAccounts: config.ovhAccounts.map(acc =>
|
||||
acc.id === accountId
|
||||
? { ...acc, domains: [...(acc.domains || []), domain] }
|
||||
: acc
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
const removeDomainFromAccount = (accountId, domain) => {
|
||||
setConfig({
|
||||
...config,
|
||||
ovhAccounts: config.ovhAccounts.map(acc =>
|
||||
acc.id === accountId
|
||||
? { ...acc, domains: acc.domains.filter(d => d !== domain) }
|
||||
: acc
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
const updateIPProvider = (providerId, field, value) => {
|
||||
setConfig({
|
||||
...config,
|
||||
ipProviders: config.ipProviders.map(provider =>
|
||||
provider.id === providerId ? { ...provider, [field]: value } : provider
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
const updateAutoUpdate = (field, value) => {
|
||||
setConfig({
|
||||
...config,
|
||||
autoUpdate: { ...config.autoUpdate, [field]: value }
|
||||
});
|
||||
};
|
||||
|
||||
if (loading || !config) {
|
||||
return (
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 text-center">
|
||||
<RefreshCw className="h-12 w-12 animate-spin mx-auto text-blue-500" />
|
||||
<p className="mt-4 text-lg text-gray-500">Cargando configuración...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center">
|
||||
<div className="bg-gradient-to-br from-purple-500 to-pink-600 p-3 rounded-xl mr-4">
|
||||
<SettingsIcon className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-gray-900">Settings</h2>
|
||||
<p className="text-gray-500 mt-1">Manage your OVH accounts and IP providers</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={saveConfig}
|
||||
disabled={saving}
|
||||
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 disabled:opacity-50 transition-all"
|
||||
>
|
||||
<Save className={`h-5 w-5 mr-2 ${saving ? 'animate-spin' : ''}`} />
|
||||
{saving ? 'Guardando...' : 'Guardar Configuración'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Current IPs Section */}
|
||||
<div className="mb-8 p-6 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl border-2 border-blue-200">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<Wifi className="h-6 w-6 text-blue-600 mr-2" />
|
||||
<h3 className="text-xl font-semibold text-gray-900">IPs Actuales</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={checkIPs}
|
||||
disabled={checkingIP}
|
||||
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 disabled:opacity-50 transition-all"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${checkingIP ? 'animate-spin' : ''}`} />
|
||||
Actualizar IPs
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white p-4 rounded-lg">
|
||||
<p className="text-sm font-medium text-gray-500 mb-1">IPv4</p>
|
||||
<p className="text-lg font-mono text-gray-900">{currentIPs.ipv4 || 'No disponible'}</p>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-lg">
|
||||
<p className="text-sm font-medium text-gray-500 mb-1">IPv6</p>
|
||||
<p className="text-lg font-mono text-gray-900">{currentIPs.ipv6 || 'No disponible'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OVH Accounts Section */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-xl font-semibold text-gray-900">Cuentas OVH</h3>
|
||||
<button
|
||||
onClick={addAccount}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all"
|
||||
>
|
||||
<Plus className="w-5 h-5 mr-2" />
|
||||
Add Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{config.ovhAccounts.map((account) => (
|
||||
<div key={account.id} className="border border-gray-200 rounded-xl p-6 bg-gray-50">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={account.name}
|
||||
onChange={(e) => updateAccount(account.id, 'name', e.target.value)}
|
||||
className="text-lg font-semibold text-gray-900 bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-blue-500 rounded px-2"
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeAccount(account.id)}
|
||||
className="text-red-600 hover:text-red-900 transition-colors"
|
||||
>
|
||||
<Trash2 className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">App Key</label>
|
||||
<input
|
||||
type="text"
|
||||
value={account.appKey}
|
||||
onChange={(e) => updateAccount(account.id, 'appKey', e.target.value)}
|
||||
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"
|
||||
placeholder="Your OVH App Key"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">App Secret</label>
|
||||
<input
|
||||
type="password"
|
||||
value={account.appSecret}
|
||||
onChange={(e) => updateAccount(account.id, 'appSecret', e.target.value)}
|
||||
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"
|
||||
placeholder="Your OVH App Secret"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Consumer Key</label>
|
||||
<input
|
||||
type="password"
|
||||
value={account.consumerKey}
|
||||
onChange={(e) => updateAccount(account.id, 'consumerKey', e.target.value)}
|
||||
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"
|
||||
placeholder="Your OVH Consumer Key"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Endpoint</label>
|
||||
<select
|
||||
value={account.endpoint}
|
||||
onChange={(e) => updateAccount(account.id, 'endpoint', e.target.value)}
|
||||
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"
|
||||
>
|
||||
<option value="ovh-eu">OVH Europe</option>
|
||||
<option value="ovh-ca">OVH Canada</option>
|
||||
<option value="ovh-us">OVH US</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="block text-sm font-medium text-gray-700">Dominios</label>
|
||||
<button
|
||||
onClick={() => addDomain(account.id)}
|
||||
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
+ Add domain
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{account.domains?.map((domain) => (
|
||||
<span
|
||||
key={domain}
|
||||
className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800"
|
||||
>
|
||||
{domain}
|
||||
<button
|
||||
onClick={() => removeDomainFromAccount(account.id, domain)}
|
||||
className="ml-2 text-blue-600 hover:text-blue-900"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
{(!account.domains || account.domains.length === 0) && (
|
||||
<span className="text-sm text-gray-500 italic">No hay dominios configurados</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* IP Providers Section */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-4">IP Providers</h3>
|
||||
<div className="space-y-4">
|
||||
{config.ipProviders.map((provider) => (
|
||||
<div key={provider.id} className="border border-gray-200 rounded-xl p-6 bg-gray-50">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-lg font-semibold text-gray-900">{provider.name}</h4>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={provider.enabled}
|
||||
onChange={(e) => updateIPProvider(provider.id, 'enabled', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
<span className="ml-3 text-sm font-medium text-gray-700">
|
||||
{provider.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">URL IPv4</label>
|
||||
<input
|
||||
type="text"
|
||||
value={provider.ipv4Url}
|
||||
onChange={(e) => updateIPProvider(provider.id, 'ipv4Url', e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">URL IPv6</label>
|
||||
<input
|
||||
type="text"
|
||||
value={provider.ipv6Url}
|
||||
onChange={(e) => updateIPProvider(provider.id, 'ipv6Url', e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto Update Section */}
|
||||
<div className="border border-gray-200 rounded-xl p-6 bg-gradient-to-br from-purple-50 to-pink-50">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-6 w-6 text-purple-600 mr-2" />
|
||||
<h3 className="text-xl font-semibold text-gray-900">Actualización Automática</h3>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.autoUpdate.enabled}
|
||||
onChange={(e) => updateAutoUpdate('enabled', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
||||
<span className="ml-3 text-sm font-medium text-gray-700">
|
||||
{config.autoUpdate.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Verification Interval (seconds)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={config.autoUpdate.checkInterval}
|
||||
onChange={(e) => updateAutoUpdate('checkInterval', parseInt(e.target.value))}
|
||||
className="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500 py-2 px-3"
|
||||
min="60"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Target Domains
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.autoUpdate.targetDomains?.join(', ') || ''}
|
||||
onChange={(e) => updateAutoUpdate('targetDomains', e.target.value.split(',').map(d => d.trim()).filter(Boolean))}
|
||||
className="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500 py-2 px-3"
|
||||
placeholder="domain1.com, domain2.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-gray-600">
|
||||
Automatic updates will periodically check public IPs and update DNS records for the specified domains.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
Referencia en una nueva incidencia
Block a user