import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import { query } from './db.js'; dotenv.config(); const app = express(); app.use(cors()); app.use(express.json()); // API Key da Hostinger (Fallback para env se não estiver no banco futuramente) let HOSTINGER_API_KEY = process.env.HOSTINGER_API_KEY || ''; // --- Configuração --- app.get('/api/config', (req, res) => { res.json({ has_api_key: !!HOSTINGER_API_KEY }); }); app.post('/api/config/hostinger-key', (req, res) => { const { api_key } = req.body; if (!api_key?.trim()) return res.status(400).json({ error: 'api_key obrigatória' }); HOSTINGER_API_KEY = api_key.trim(); res.json({ ok: true }); }); // --- Domínios --- // Listar domínios salvos no banco app.get('/api/domains', async (req, res) => { try { const result = await query('SELECT * FROM domains ORDER BY name ASC'); res.json(result.rows); } catch (err) { res.status(500).json({ error: err.message }); } }); // Sincronizar domínios da Hostinger para o banco app.post('/api/domains/sync', async (req, res) => { if (!HOSTINGER_API_KEY) return res.status(400).json({ error: 'API key não configurada' }); try { const r = await fetch('https://developers.hostinger.com/api/domains/v1/portfolio', { headers: { 'Authorization': `Bearer ${HOSTINGER_API_KEY}`, 'Accept': 'application/json' } }); if (!r.ok) { const errText = await r.text(); return res.status(r.status).json({ error: `Hostinger Error: ${errText}` }); } const domains = await r.json(); for (const dom of domains) { await query( 'INSERT INTO domains (name, hostinger_id) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET hostinger_id = $2', [dom.domain, dom.id || null] ); } const result = await query('SELECT * FROM domains ORDER BY name ASC'); res.json({ ok: true, domains: result.rows }); } catch (err) { res.status(500).json({ error: err.message }); } }); // --- Subdomínios --- // Listar subdomínios de um domínio (do banco) app.get('/api/domains/:domainId/subdomains', async (req, res) => { try { const result = await query( 'SELECT s.*, b.logo_url, b.primary_color, b.secondary_color, b.title, b.description FROM subdomains s LEFT JOIN branding b ON s.id = b.subdomain_id WHERE s.domain_id = $1 ORDER BY s.subdomain ASC', [req.params.domainId] ); res.json(result.rows); } catch (err) { res.status(500).json({ error: err.message }); } }); // Criar subdomínio (Hostinger + Banco) app.post('/api/subdomains', async (req, res) => { if (!HOSTINGER_API_KEY) return res.status(400).json({ error: 'API key não configurada' }); const { domainId, domainName, subdomain, type = 'A', value } = req.body; if (!domainId || !domainName || !subdomain || !value) { 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}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${HOSTINGER_API_KEY}`, 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ overwrite: false, zone: [{ name: subdomain, type: type, records: [{ content: value, ttl: 300 }] }] }) }); if (!r.ok) { const errText = await r.text(); return res.status(r.status).json({ error: `Erro Hostinger: ${errText}` }); } // 2. Salvar no Banco const fullDomain = `${subdomain}.${domainName}`; const dbResult = await query( 'INSERT INTO subdomains (domain_id, subdomain, full_domain, type, value) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (full_domain) DO UPDATE SET value = $5 RETURNING *', [domainId, subdomain, fullDomain, type, value] ); res.json({ ok: true, subdomain: dbResult.rows[0] }); } 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 --- app.get('/api/branding/:subdomainId', async (req, res) => { try { const result = await query('SELECT * FROM branding WHERE subdomain_id = $1', [req.params.subdomainId]); res.json(result.rows[0] || {}); } catch (err) { res.status(500).json({ error: err.message }); } }); app.put('/api/branding/:subdomainId', async (req, res) => { const { logo_url, primary_color, secondary_color, title, description, favicon_url } = req.body; const subdomainId = req.params.subdomainId; try { const result = await query( `INSERT INTO branding (subdomain_id, logo_url, primary_color, secondary_color, title, description, favicon_url) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (subdomain_id) DO UPDATE SET logo_url = EXCLUDED.logo_url, primary_color = EXCLUDED.primary_color, secondary_color = EXCLUDED.secondary_color, title = EXCLUDED.title, description = EXCLUDED.description, favicon_url = EXCLUDED.favicon_url, updated_at = CURRENT_TIMESTAMP RETURNING *`, [subdomainId, logo_url, primary_color, secondary_color, title, description, favicon_url] ); res.json(result.rows[0]); } catch (err) { res.status(500).json({ error: err.message }); } }); const PORT = process.env.PORT || 8019; app.listen(PORT, () => console.log(`[subdominio-backend] porta ${PORT}`));