Entrega read-only de apontamentos DNS (Cloudflare, OpenPanel BIND, público) no Desk e Console /admin/dominio, com spec, scripts de rollback e patches VM112 para painel lateral no passo DNS do onboarding (deploy wizard pendente). Co-authored-by: Cursor <cursoragent@cursor.com>
204 lines
8.1 KiB
JavaScript
204 lines
8.1 KiB
JavaScript
/**
|
|
* Spec 037-DNS-VIEWER — painel DNS unificado (Desk)
|
|
* Depende de: auth.js (api, esc)
|
|
*/
|
|
(function () {
|
|
function dnsPurposeLabel(purpose) {
|
|
return {
|
|
mx: 'MX',
|
|
spf: 'SPF',
|
|
dkim: 'DKIM',
|
|
dmarc: 'DMARC',
|
|
'mail-host': 'Mail host',
|
|
autodiscover: 'Autodiscover',
|
|
'mail-alias': 'Alias',
|
|
other: 'Outro',
|
|
}[purpose] || purpose || '—';
|
|
}
|
|
|
|
function sourceLabel(source) {
|
|
return {
|
|
cf_ligbox: 'CF Ligbox',
|
|
cf_byo: 'CF cliente',
|
|
public_resolver: 'Público',
|
|
planned_ligbox: 'Planeado',
|
|
openpanel_bind: 'OpenPanel',
|
|
}[source] || source || '—';
|
|
}
|
|
|
|
function checkIcon(c) {
|
|
if (!c) return '—';
|
|
if (c.ok) return '✅';
|
|
if (c.warn) return '⚠';
|
|
return '❌';
|
|
}
|
|
|
|
async function fetchDnsViewer(domain, emailService) {
|
|
const q = new URLSearchParams();
|
|
if (emailService != null) q.set('email_service', emailService ? 'true' : 'false');
|
|
const qs = q.toString();
|
|
return api(`/v1/dns/viewer/${encodeURIComponent(domain)}${qs ? `?${qs}` : ''}`);
|
|
}
|
|
|
|
async function fetchDomainDns(domain, emailService) {
|
|
try {
|
|
const viewer = await fetchDnsViewer(domain, emailService);
|
|
if (viewer && viewer.domain) return { ...viewer, _viewer: true };
|
|
} catch (e) {
|
|
if (e?.status === 404) {
|
|
/* DNS_VIEWER_ENABLED=0 — legado */
|
|
} else if (e?.status !== 403) {
|
|
console.warn('DnsViewer fallback:', e?.message || e);
|
|
}
|
|
}
|
|
try {
|
|
const legacy = await api(
|
|
`/v1/dns/cloudflare/records?domain=${encodeURIComponent(domain)}&email_service=${emailService ? 'true' : 'false'}`
|
|
);
|
|
return legacy;
|
|
} catch (e) {
|
|
return {
|
|
domain,
|
|
records: [],
|
|
email_records: [],
|
|
summary: { total: 0, email_related: 0 },
|
|
error: e.message || 'Falha ao carregar DNS',
|
|
};
|
|
}
|
|
}
|
|
|
|
function publicChecksRow(checks) {
|
|
if (!checks || !Object.keys(checks).length) return '';
|
|
const parts = ['mx', 'spf', 'dkim', 'dmarc'].map(
|
|
(k) => `${k.toUpperCase()} ${checkIcon(checks[k])}`
|
|
);
|
|
return `<p class="cf-dns-meta dns-viewer-checks">Verificação pública: ${parts.join(' · ')}</p>`;
|
|
}
|
|
|
|
function nsRow(data) {
|
|
const ns = data.nameservers || {};
|
|
const current = ns.current_public || [];
|
|
if (!current.length) return '';
|
|
const match = ns.match_ligbox;
|
|
const warn = data.display_mode === 'planned' || (data.dns_mode?.startsWith('ligbox_cf') && !match);
|
|
return `<p class="cf-dns-meta ${warn ? 'dns-viewer-ns-warn' : ''}">NS actuais: <code>${current.map((n) => esc(n)).join('</code> · <code>')}</code>${warn ? ' · ⚠ delegação Ligbox pendente' : match ? ' · ✅ Cloudflare' : ''}</p>`;
|
|
}
|
|
|
|
function recordRows(records, compact) {
|
|
return (records || []).map((r) => {
|
|
const src = r.source ? `<span class="dns-source-badge">${esc(sourceLabel(r.source))}</span>` : '';
|
|
const state = r.status === 'planned' ? ' <span class="badge review">planeado</span>' : '';
|
|
if (compact) {
|
|
return `
|
|
<tr class="${r.email_related ? 'dns-email-row' : ''}">
|
|
<td><span class="dns-purpose-badge purpose-${esc(r.purpose || 'other')}">${esc(dnsPurposeLabel(r.purpose))}</span></td>
|
|
<td><code>${esc(r.name)}</code></td>
|
|
<td><strong>${esc(r.type)}</strong></td>
|
|
<td class="dns-content">${esc(r.content)}</td>
|
|
</tr>`;
|
|
}
|
|
return `
|
|
<tr class="${r.email_related ? 'dns-email-row' : ''}">
|
|
<td><span class="dns-purpose-badge purpose-${esc(r.purpose || 'other')}">${esc(dnsPurposeLabel(r.purpose))}</span></td>
|
|
<td><code>${esc(r.name)}</code></td>
|
|
<td><strong>${esc(r.type)}</strong>${r.priority != null ? ` <span class="ticket-meta">prio ${r.priority}</span>` : ''}</td>
|
|
<td class="dns-content">${esc(r.content)}</td>
|
|
<td class="ticket-meta">${src}${state}${r.proxied != null ? (r.proxied ? ' · proxy' : ' · DNS only') : ''}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
function editLinksHtml(links) {
|
|
if (!links?.length) return '';
|
|
return `<div class="dns-viewer-actions">${links.map((l) => `<a class="btn btn-ghost btn-sm" href="${esc(l.url)}" target="_blank" rel="noopener noreferrer">${esc(l.label)} ↗</a>`).join(' ')}</div>`;
|
|
}
|
|
|
|
function renderDnsPanel(data, { compact = false, title = 'DNS do domínio' } = {}) {
|
|
if (!data) {
|
|
return compact
|
|
? '<p class="cf-dns-empty">Dados DNS indisponíveis.</p>'
|
|
: `<div class="modal-section"><h4>${esc(title)}</h4><p class="loading">Dados DNS indisponíveis.</p></div>`;
|
|
}
|
|
|
|
const isViewer = data._viewer || data.dns_mode;
|
|
if (!isViewer) {
|
|
return compact ? legacyInline(data) : legacyCard(data, title);
|
|
}
|
|
|
|
const summary = data.summary || {};
|
|
const zone = data.zone || {};
|
|
const rows = recordRows(data.records, compact);
|
|
const planned = (data.planned_records || []).length
|
|
? `<h5 class="dns-viewer-sub">Planeados (após NS)</h5><div class="cf-dns-table-wrap"><table class="data-table dns-records-table ${compact ? 'dns-records-table-compact' : ''}"><thead><tr><th>Função</th><th>Nome</th><th>Tipo</th><th>Conteúdo</th>${compact ? '' : '<th>Origem</th>'}</tr></thead><tbody>${recordRows(data.planned_records, compact) || '<tr><td colspan="5">—</td></tr>'}</tbody></table></div>`
|
|
: '';
|
|
|
|
const badgeCls = data.display_mode === 'planned' ? 'review' : data.dns_mode?.startsWith('ligbox_cf') ? 'onboard' : 'open';
|
|
const inner = `
|
|
<div class="dns-viewer-head">
|
|
<span class="badge ${badgeCls}">${esc(data.mode_label || data.dns_mode || 'DNS')}</span>
|
|
${data.email_service ? '<span class="badge onboard">E-mail</span>' : ''}
|
|
</div>
|
|
${data.mode_message ? `<p class="cf-dns-meta dns-viewer-msg">${esc(data.mode_message)}</p>` : ''}
|
|
${nsRow(data)}
|
|
<div class="cf-dns-inline-summary">
|
|
<div class="cf-metric-stat"><strong>${summary.total || 0}</strong><span>registos</span></div>
|
|
<div class="cf-metric-stat"><strong>${summary.email_related || 0}</strong><span>e-mail</span></div>
|
|
${summary.planned ? `<div class="cf-metric-stat"><strong>${summary.planned}</strong><span>planeados</span></div>` : ''}
|
|
</div>
|
|
${zone.name ? `<p class="cf-dns-meta">Zona CF <code>${esc(zone.name)}</code></p>` : ''}
|
|
${publicChecksRow(data.public_checks)}
|
|
<div class="cf-dns-table-wrap">
|
|
<table class="data-table dns-records-table ${compact ? 'dns-records-table-compact' : ''}">
|
|
<thead><tr><th>Função</th><th>Nome</th><th>Tipo</th><th>Conteúdo</th>${compact ? '' : '<th>Origem</th>'}</tr></thead>
|
|
<tbody>${rows || '<tr><td colspan="5">Sem registos para este domínio.</td></tr>'}</tbody>
|
|
</table>
|
|
</div>
|
|
${planned}
|
|
${editLinksHtml(data.edit_links)}
|
|
${data.errors?.length ? `<p class="cf-dns-error">${esc(data.errors.join(' · '))}</p>` : ''}`;
|
|
|
|
if (compact) return inner;
|
|
|
|
return `
|
|
<div class="modal-section dns-records-section dns-viewer-section">
|
|
<div class="card-head-row">
|
|
<h4>${esc(title)}</h4>
|
|
</div>
|
|
${inner}
|
|
</div>`;
|
|
}
|
|
|
|
function legacyInline(dns) {
|
|
if (dns.error && !dns.records?.length) {
|
|
return `<p class="cf-dns-error">${esc(dns.error)}</p>`;
|
|
}
|
|
const rows = recordRows(dns.records, true);
|
|
const summary = dns.summary || {};
|
|
return `
|
|
<div class="cf-dns-inline-summary">
|
|
<div class="cf-metric-stat"><strong>${summary.total || 0}</strong><span>registos na zona</span></div>
|
|
<div class="cf-metric-stat"><strong>${summary.email_related || 0}</strong><span>para e-mail</span></div>
|
|
</div>
|
|
<div class="cf-dns-table-wrap">
|
|
<table class="data-table dns-records-table dns-records-table-compact">
|
|
<thead><tr><th>Função</th><th>Nome</th><th>Tipo</th><th>Conteúdo</th></tr></thead>
|
|
<tbody>${rows || '<tr><td colspan="4">Sem registos.</td></tr>'}</tbody>
|
|
</table>
|
|
</div>`;
|
|
}
|
|
|
|
function legacyCard(dns, title) {
|
|
return `
|
|
<div class="modal-section dns-records-section">
|
|
<h4>${esc(title)} (Cloudflare)</h4>
|
|
${legacyInline(dns)}
|
|
</div>`;
|
|
}
|
|
|
|
window.DnsViewer = {
|
|
fetch: fetchDnsViewer,
|
|
fetchDomainDns,
|
|
renderPanel: renderDnsPanel,
|
|
dnsPurposeLabel,
|
|
};
|
|
})();
|