From 43576ee52db488d78e4addb89d4327c9f7643121 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Mon, 18 May 2026 01:38:49 +0200 Subject: [PATCH] feat: controle granular de DNS real-time da Hostinger e exclusao de subdominios --- backend/server.js | 147 +++++++++++ frontend/src/App.tsx | 576 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 615 insertions(+), 108 deletions(-) diff --git a/backend/server.js b/backend/server.js index 4acf064..bf00f44 100644 --- a/backend/server.js +++ b/backend/server.js @@ -90,6 +90,12 @@ app.post('/api/subdomains', async (req, res) => { return res.status(400).json({ error: 'Campos obrigatórios ausentes' }); } + // Validação de nível único (Sem subdomínios aninhados) + const subdomainRegex = /^[a-zA-Z0-9][-a-zA-Z0-9]*$/; + if (!subdomainRegex.test(subdomain)) { + return res.status(400).json({ error: 'O subdomínio deve conter apenas letras, números e hifens, sem pontos (nível único).' }); + } + try { // 1. Criar na Hostinger const r = await fetch(`https://developers.hostinger.com/api/dns/v1/zones/${domainName}`, { @@ -121,6 +127,147 @@ app.post('/api/subdomains', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } +// Excluir Subdomínio (Hostinger + Banco) +app.delete('/api/subdomains/:id', async (req, res) => { + if (!HOSTINGER_API_KEY) return res.status(400).json({ error: 'API key não configurada' }); + const { id } = req.params; + + try { + // 1. Obter informações do subdomínio no banco + const subResult = await query( + `SELECT s.*, d.name as domain_name + FROM subdomains s + JOIN domains d ON s.domain_id = d.id + WHERE s.id = $1`, + [id] + ); + + if (subResult.rows.length === 0) { + return res.status(404).json({ error: 'Subdomínio não encontrado.' }); + } + + const { subdomain, domain_name, type } = subResult.rows[0]; + + // 2. Excluir na Hostinger + const r = await fetch(`https://developers.hostinger.com/api/dns/v1/zones/${domain_name}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${HOSTINGER_API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + filters: [{ name: subdomain, type: type }] + }) + }); + + if (!r.ok) { + const errText = await r.text(); + if (r.status !== 404) { + return res.status(r.status).json({ error: `Erro ao excluir na Hostinger: ${errText}` }); + } + } + + // 3. Excluir do Banco + await query('DELETE FROM subdomains WHERE id = $1', [id]); + + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// --- Gerenciamento Direto de DNS Real-Time (Hostinger) --- + +// Listar registros DNS diretamente da Hostinger +app.get('/api/domains/:domainName/dns-records', async (req, res) => { + if (!HOSTINGER_API_KEY) return res.status(400).json({ error: 'API key não configurada' }); + const { domainName } = req.params; + + try { + const r = await fetch(`https://developers.hostinger.com/api/dns/v1/zones/${domainName}`, { + headers: { 'Authorization': `Bearer ${HOSTINGER_API_KEY}`, 'Accept': 'application/json' } + }); + + if (!r.ok) { + const errText = await r.text(); + return res.status(r.status).json({ error: `Erro Hostinger: ${errText}` }); + } + + const records = await r.json(); + res.json(records); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Criar/Adicionar registro DNS diretamente na Hostinger +app.post('/api/domains/:domainName/dns-records', async (req, res) => { + if (!HOSTINGER_API_KEY) return res.status(400).json({ error: 'API key não configurada' }); + const { domainName } = req.params; + const { name, type, value, ttl = 300 } = req.body; + + if (!name || !type || !value) { + return res.status(400).json({ error: 'Campos name, type e value são obrigatórios' }); + } + + try { + const r = await fetch(`https://developers.hostinger.com/api/dns/v1/zones/${domainName}`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${HOSTINGER_API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + overwrite: false, + zone: [{ name, type, records: [{ content: value, ttl }] }] + }) + }); + + if (!r.ok) { + const errText = await r.text(); + return res.status(r.status).json({ error: `Erro Hostinger: ${errText}` }); + } + + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Deletar registro DNS diretamente na Hostinger +app.delete('/api/domains/:domainName/dns-records', async (req, res) => { + if (!HOSTINGER_API_KEY) return res.status(400).json({ error: 'API key não configurada' }); + const { domainName } = req.params; + const { name, type } = req.body; + + if (!name || !type) { + return res.status(400).json({ error: 'Campos name e type são obrigatórios' }); + } + + try { + const r = await fetch(`https://developers.hostinger.com/api/dns/v1/zones/${domainName}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${HOSTINGER_API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + filters: [{ name, type }] + }) + }); + + if (!r.ok) { + const errText = await r.text(); + return res.status(r.status).json({ error: `Erro Hostinger: ${errText}` }); + } + + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); // --- Branding / Identidade Visual --- diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1f1926b..7467c7f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,6 +24,13 @@ interface Subdomain { favicon_url?: string; } +interface DnsRecord { + name: string; + type: string; + ttl: number; + records: Array<{ content: string; is_disabled?: boolean }>; +} + export default function App() { const [hasKey, setHasKey] = useState(null); const [apiKeyInput, setApiKeyInput] = useState(''); @@ -35,15 +42,29 @@ export default function App() { // Navigation states const [selectedDomain, setSelectedDomain] = useState(null); + const [activeTab, setActiveTab] = useState<'subdomains' | 'dns'>('subdomains'); const [subdomains, setSubdomains] = useState([]); const [loadingSubdomains, setLoadingSubdomains] = useState(false); const [editingBranding, setEditingBranding] = useState(null); - // Form states + // Real-time DNS states + const [dnsRecords, setDnsRecords] = useState([]); + const [loadingDns, setLoadingDns] = useState(false); + const [dnsSearchQuery, setDnsSearchQuery] = useState(''); + const [dnsFilterType, setDnsFilterType] = useState('ALL'); + + // Subdomain Form states const [newSubdomain, setNewSubdomain] = useState(''); const [newValue, setNewValue] = useState(''); const [creating, setCreating] = useState(false); + // Custom DNS Form states + const [dnsFormName, setDnsFormName] = useState(''); + const [dnsFormType, setDnsFormType] = useState('A'); + const [dnsFormValue, setDnsFormValue] = useState(''); + const [dnsFormTtl, setDnsFormTtl] = useState(300); + const [addingDnsRecord, setAddingDnsRecord] = useState(false); + // Branding states const [brandingForm, setBrandingForm] = useState({ title: '', @@ -61,9 +82,13 @@ export default function App() { useEffect(() => { if (selectedDomain) { - fetchSubdomains(selectedDomain.id); + if (activeTab === 'subdomains') { + fetchSubdomains(selectedDomain.id); + } else { + fetchDnsRecords(selectedDomain.name); + } } - }, [selectedDomain]); + }, [selectedDomain, activeTab]); async function fetchConfig() { try { @@ -89,11 +114,11 @@ export default function App() { if (r.ok) { setHasKey(true); setApiKeyInput(''); - setSuccess('Chave API salva e configurada.'); + setSuccess('Chave API Hostinger ativada com sucesso!'); fetchDomains(); } else { const d = await r.json(); - setError(d.error || 'Erro ao salvar chave.'); + setError(d.error || 'Erro ao validar a chave API.'); } } finally { setSaving(false); } } @@ -118,7 +143,7 @@ export default function App() { const d = await r.json(); if (r.ok) { setDomains(d.domains); - setSuccess('Sincronização concluída com sucesso!'); + setSuccess('Domínios sincronizados com sucesso da Hostinger!'); } else { setError(d.error || 'Erro na sincronização.'); } @@ -154,7 +179,7 @@ export default function App() { }) }); if (r.ok) { - setSuccess(`Subdomínio ${newSubdomain}.${selectedDomain.name} criado.`); + setSuccess(`Subdomínio ${newSubdomain}.${selectedDomain.name} criado e publicado na Hostinger!`); setNewSubdomain(''); setNewValue(''); fetchSubdomains(selectedDomain.id); @@ -165,6 +190,89 @@ export default function App() { } finally { setCreating(false); } } + async function deleteSubdomain(id: number, fullDomain: string) { + if (!confirm(`Tem certeza que deseja excluir o subdomínio ${fullDomain}? Esta ação removerá a entrada DNS na Hostinger e todas as configurações de identidade visual!`)) return; + setLoadingSubdomains(true); + setError(''); + setSuccess(''); + try { + const r = await fetch(`${API}/subdomains/${id}`, { method: 'DELETE' }); + if (r.ok) { + setSuccess(`Subdomínio ${fullDomain} removido com sucesso.`); + if (selectedDomain) fetchSubdomains(selectedDomain.id); + } else { + const d = await r.json(); + setError(d.error || 'Erro ao remover subdomínio.'); + } + } finally { setLoadingSubdomains(false); } + } + + // --- Real-time Hostinger DNS Operations --- + async function fetchDnsRecords(domainName: string) { + setLoadingDns(true); + try { + const r = await fetch(`${API}/domains/${domainName}/dns-records`); + if (r.ok) { + const d = await r.json(); + setDnsRecords(d); + } else { + const d = await r.json(); + setError(d.error || 'Erro ao buscar registros DNS em tempo real.'); + } + } finally { setLoadingDns(false); } + } + + async function addDnsRecord(e: React.FormEvent) { + e.preventDefault(); + if (!selectedDomain || !dnsFormName || !dnsFormValue) return; + setAddingDnsRecord(true); + setError(''); + setSuccess(''); + try { + const r = await fetch(`${API}/domains/${selectedDomain.name}/dns-records`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: dnsFormName, + type: dnsFormType, + value: dnsFormValue, + ttl: dnsFormTtl + }) + }); + if (r.ok) { + setSuccess(`Registro DNS (${dnsFormType}) adicionado com sucesso na Hostinger!`); + setDnsFormName(''); + setDnsFormValue(''); + setDnsFormTtl(300); + fetchDnsRecords(selectedDomain.name); + } else { + const d = await r.json(); + setError(d.error || 'Erro ao criar registro DNS.'); + } + } finally { setAddingDnsRecord(false); } + } + + async function deleteDnsRecord(name: string, type: string) { + if (!confirm(`Deseja remover o registro DNS (${type}) de "${name}" diretamente na Hostinger?`)) return; + setLoadingDns(true); + setError(''); + setSuccess(''); + try { + const r = await fetch(`${API}/domains/${selectedDomain!.name}/dns-records`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, type }) + }); + if (r.ok) { + setSuccess(`Registro DNS removido com sucesso da Hostinger.`); + fetchDnsRecords(selectedDomain!.name); + } else { + const d = await r.json(); + setError(d.error || 'Erro ao remover registro DNS.'); + } + } finally { setLoadingDns(false); } + } + function openBranding(sub: Subdomain) { setEditingBranding(sub); setBrandingForm({ @@ -179,6 +287,8 @@ export default function App() { async function saveBranding() { if (!editingBranding) return; + setError(''); + setSuccess(''); try { const r = await fetch(`${API}/branding/${editingBranding.id}`, { method: 'PUT', @@ -186,35 +296,59 @@ export default function App() { body: JSON.stringify(brandingForm) }); if (r.ok) { - setSuccess('Identidade visual atualizada!'); + setSuccess('Identidade visual atualizada com sucesso!'); setEditingBranding(null); if (selectedDomain) fetchSubdomains(selectedDomain.id); + } else { + const d = await r.json(); + setError(d.error || 'Erro ao salvar identidade visual.'); } } catch (e: any) { setError(e.message); } } + // Helper placeholder for DNS values + const getDnsValuePlaceholder = () => { + switch (dnsFormType) { + case 'A': return 'ex: 158.220.109.237'; + case 'AAAA': return 'ex: 2001:db8::1'; + case 'CNAME': return 'ex: ghs.google.com'; + case 'TXT': return 'ex: v=spf1 include:_spf.google.com ~all'; + case 'MX': return 'ex: 10 mail.dominio.com'; + default: return 'Valor / Destino'; + } + }; + + // Filter and search logic for real-time DNS records + const filteredDnsRecords = dnsRecords.filter(rec => { + const matchesSearch = rec.name.toLowerCase().includes(dnsSearchQuery.toLowerCase()) || + rec.records.some(r => r.content.toLowerCase().includes(dnsSearchQuery.toLowerCase())); + const matchesType = dnsFilterType === 'ALL' || rec.type === dnsFilterType; + return matchesSearch && matchesType; + }); + return ( -
+
{/* BACKGROUND DECORATION */} -
-
-
+
+
+
+
-
+
{/* HEADER */} -
+
S

Subdominio.Manager

-

Identidade Visual & DNS

+

Controle de DNS e Identidade Visual

@@ -225,7 +359,7 @@ export default function App() { disabled={loadingDomains} className="group flex items-center gap-3 bg-gray-900/50 hover:bg-orange-500 border border-gray-800 hover:border-orange-400 px-6 py-3 rounded-2xl transition-all" > -
+
Sincronizar Hostinger )} @@ -238,19 +372,23 @@ export default function App() { {/* ALERTS */} {(error || success) && (
- {error &&
🚨 {error}
} - {success &&
✅ {success}
} + {error &&
🚨 {error}
} + {success &&
{success}
}
)} + {/* API CONFIGURATION FALLBACK */} {!hasKey && hasKey !== null && ( -
-

Configuração da API

-

Insira sua chave de API da Hostinger para começar a gerenciar seus domínios e identidades visuais.

+
+
+ 🔑 +
+

Configuração da API Hostinger

+

Insira sua chave de desenvolvedor API da Hostinger para gerenciar seus domínios, rotear subdomínios de Staging/Produção e controlar marcas visuais de forma automatizada.

setApiKeyInput(e.target.value)} className="w-full bg-gray-950/50 border border-gray-800 rounded-2xl px-6 py-5 text-sm focus:outline-none focus:border-orange-500 transition-all text-center" @@ -258,9 +396,9 @@ export default function App() {
@@ -272,8 +410,8 @@ export default function App() { {/* DOMAIN CARDS GRID */}
-

Selecione um Domínio

-
+

Seus Domínios Sincronizados

+
@@ -294,14 +432,14 @@ export default function App() {
- {dom.name.charAt(0).toUpperCase()} + 🌐

{dom.name}

- ID: {dom.hostinger_id || 'LOCAL'} + ID: {dom.hostinger_id || 'EXCLUSIVO'}

))} @@ -310,95 +448,317 @@ export default function App() { {/* SELECTED DOMAIN VIEW */} {selectedDomain && ( -
+
- {/* SUBDOMAINS LIST */} -
-
-

Subdomínios de {selectedDomain.name}

- - {subdomains.length} registros - + {/* DOMAIN INTERFACE TABS */} +
+
+ +

{selectedDomain.name}

-
- {subdomains.map((sub) => ( -
-
-
- {sub.logo_url ? ( - Logo - ) : ( -
- {sub.subdomain.charAt(0).toUpperCase()} +
+ + +
+
+ + {/* TAB 1: SUBDOMAINS & BRANDING */} + {activeTab === 'subdomains' && ( +
+ + {/* SUBDOMAINS LIST */} +
+
+

Subdomínios Ativos no Banco

+ + {subdomains.length} registros salvos + +
+ + {loadingSubdomains ? ( +
+
+ Carregando subdomínios... +
+ ) : subdomains.length === 0 ? ( +
+ Nenhum subdomínio configurado com identidade visual. Crie o primeiro ao lado! +
+ ) : ( +
+ {subdomains.map((sub) => ( +
+
+
+ {sub.logo_url ? ( + Logo + ) : ( +
+ {sub.subdomain.charAt(0).toUpperCase()} +
+ )} +
+

{sub.full_domain}

+

Destino: {sub.value}

+
+
+
+ + +
- )} -
-

{sub.full_domain}

-

Aponta para: {sub.value}

+ +
+
+
+
+
+ ))} +
+ )} +
+ + {/* CREATE FORM */} +
+
+

Novo Subdomínio

+

Aponta o DNS na Hostinger e já cria o espaço no banco para marca visual.

+ +
+
+ +
+ setNewSubdomain(e.target.value)} + className="flex-1 bg-transparent px-5 py-4 text-sm focus:outline-none" + /> + .{selectedDomain.name}
- -
- -
-
-
-
+
- ))} - - {/* ADD NEW BOX */} -
-
- + -
-

Novo Subdomínio

-
-
- {/* CREATE FORM */} -
-
-

Criar Subdomínio

-
-
- -
+
+ )} + + {/* TAB 2: REAL-TIME HOSTINGER DNS CONTROL */} + {activeTab === 'dns' && ( +
+ + {/* DNS RECORDS LIST TABLE */} +
+
+

Tabela de DNS em Tempo Real

+ +
setNewSubdomain(e.target.value)} - className="flex-1 bg-transparent px-5 py-4 text-sm focus:outline-none" + type="text" + placeholder="Buscar registros..." + value={dnsSearchQuery} + onChange={e => setDnsSearchQuery(e.target.value)} + className="bg-gray-950 border border-gray-800 rounded-xl px-4 py-2 text-xs focus:outline-none focus:border-orange-500 transition-all" /> - .{selectedDomain.name}
-
- - setNewValue(e.target.value)} - className="w-full bg-gray-950 border border-gray-800 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:border-orange-500 transition-all" - /> + + {/* PILL FILTERS */} +
+ {['ALL', 'A', 'CNAME', 'TXT', 'MX', 'NS', 'SRV'].map((type) => ( + + ))}
- - + + {loadingDns ? ( +
+
+ Consultando zona DNS na Hostinger... +
+ ) : filteredDnsRecords.length === 0 ? ( +
+ Nenhum registro DNS encontrado para os filtros selecionados. +
+ ) : ( +
+
+ + + + + + + + + + + + {filteredDnsRecords.map((rec, index) => ( + + + + + + + + ))} + +
Nome / HostTipoConteúdo / DestinoTTLAções
+ {rec.name} + + + {rec.type} + + r.content).join(', ')}> + {rec.records.map(r => r.content).join(', ')} + + {rec.ttl}s + + +
+
+
+ )} +
+ + {/* QUICK ADD DNS RECORD */} +
+
+

Novo Apontamento

+

Adiciona qualquer tipo de registro DNS em tempo real na Hostinger.

+ +
+
+ + +
+ +
+ +
+ setDnsFormName(e.target.value)} + className="flex-1 bg-transparent px-5 py-4 text-sm focus:outline-none" + /> + .{selectedDomain.name} +
+
+ +
+ + setDnsFormValue(e.target.value)} + className="w-full bg-gray-950 border border-gray-800 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:border-orange-500 transition-all" + /> +
+ +
+ + setDnsFormTtl(parseInt(e.target.value) || 300)} + className="w-full bg-gray-950 border border-gray-800 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:border-orange-500 transition-all" + /> +
+ + +
+
+
+
-
+ )}
)} @@ -417,7 +777,7 @@ export default function App() {
-
+
@@ -428,7 +788,7 @@ export default function App() { />
- +