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>
315 lines
13 KiB
Python
315 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Patch App.jsx Spec 037 — multi-conta CF + BYO."""
|
|
from pathlib import Path
|
|
|
|
APP = Path("/opt/ligbox-wizard/frontend/src/App.jsx")
|
|
text = APP.read_text(encoding="utf-8")
|
|
|
|
if "chooseByoDns" in text and "dnsResolve" in text:
|
|
print("App.jsx already patched")
|
|
raise SystemExit(0)
|
|
|
|
text = text.replace(
|
|
" choosePortalDns: false,\n chooseExternalDns: false,",
|
|
" choosePortalDns: false,\n chooseByoDns: false,\n chooseExternalDns: false,",
|
|
)
|
|
|
|
text = text.replace(
|
|
" const [showAdvancedDns, setShowAdvancedDns] = useState(false)",
|
|
" const [showAdvancedDns, setShowAdvancedDns] = useState(false)\n const [dnsResolve, setDnsResolve] = useState(null)\n const [byoToken, setByoToken] = useState('')",
|
|
)
|
|
|
|
OLD_CHOOSE_PORTAL = """ async function choosePortalDns() {
|
|
setDnsChoice('portal')
|
|
setError(null)
|
|
startBusy('dns_zone')
|
|
try {
|
|
const guide = await api('/onboarding/dns/cloudflare/provision-zone', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ domain }),
|
|
})
|
|
setPortalGuide(guide)
|
|
const cf = guide.status || (await refreshCfStatus())
|
|
setCfStatus(cf)
|
|
markActionDone('choosePortalDns')
|
|
if (guide.verification) {
|
|
setVerification(guide.verification)
|
|
setResult({ type: 'dns', data: { verification: guide.verification } })
|
|
markActionDone('verifyZone')
|
|
markActionDone('applyPortalDns')
|
|
} else if (cf?.zone_in_account && cf?.can_apply_mail_records) {
|
|
markActionDone('verifyZone')
|
|
const data = await api('/onboarding/dns/cloudflare/apply', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
domain,
|
|
mail_aliases: sanitizeMailAliases(mailAliasInputs, domain),
|
|
}),
|
|
})
|
|
setVerification(data.verification)
|
|
setResult({ type: 'dns', data })
|
|
markActionDone('applyPortalDns')
|
|
}
|
|
} catch (e) {
|
|
setError(e.message)
|
|
setDnsChoice(null)
|
|
setActionsDone((prev) => ({ ...prev, choosePortalDns: false }))
|
|
} finally {
|
|
stopBusy()
|
|
}
|
|
}"""
|
|
|
|
NEW_CHOOSE_PORTAL = """ async function choosePortalDns() {
|
|
setDnsChoice('portal')
|
|
setError(null)
|
|
startBusy('dns_zone')
|
|
try {
|
|
const resolved = dnsResolve || (await api(`/onboarding/dns/resolve/${domain}`))
|
|
setDnsResolve(resolved)
|
|
if (!resolved.matched) {
|
|
throw new Error(
|
|
resolved.message ||
|
|
'Domínio não está nas contas Ligbox (ligit, itecnologys, ibytera).',
|
|
)
|
|
}
|
|
const cf = await refreshCfStatus()
|
|
setCfStatus(cf)
|
|
markActionDone('choosePortalDns')
|
|
markActionDone('verifyZone')
|
|
const data = await api('/onboarding/dns/cloudflare/apply', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
domain,
|
|
zone_id: resolved.zone_id,
|
|
mail_aliases: sanitizeMailAliases(mailAliasInputs, domain),
|
|
}),
|
|
})
|
|
setPortalGuide({
|
|
message: resolved.message,
|
|
nameservers: cf?.nameservers || [],
|
|
status: cf,
|
|
ligbox_account_id: resolved.account_id,
|
|
})
|
|
setVerification(data.verification)
|
|
setResult({ type: 'dns', data })
|
|
markActionDone('applyPortalDns')
|
|
} catch (e) {
|
|
setError(e.message)
|
|
setDnsChoice(null)
|
|
setActionsDone((prev) => ({ ...prev, choosePortalDns: false }))
|
|
} finally {
|
|
stopBusy()
|
|
}
|
|
}
|
|
|
|
async function chooseByoDns() {
|
|
setDnsChoice('byo')
|
|
setError(null)
|
|
markActionDone('chooseByoDns')
|
|
setShowAdvancedDns(true)
|
|
}
|
|
|
|
async function connectByoAndApply() {
|
|
const token = byoToken.trim()
|
|
if (!token) {
|
|
setError('Cole o API Token da sua conta Cloudflare (DNS Edit na zona).')
|
|
return
|
|
}
|
|
setError(null)
|
|
startBusy('dns_byo')
|
|
try {
|
|
const conn = await api('/onboarding/dns/cloudflare/connect-custom', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ domain, api_token: token }),
|
|
})
|
|
setDnsResolve({ ...conn, matched: true, dns_mode: 'byo_cloudflare' })
|
|
markActionDone('chooseByoDns')
|
|
const data = await api('/onboarding/dns/cloudflare/apply', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
domain,
|
|
zone_id: conn.zone_id,
|
|
mail_aliases: sanitizeMailAliases(mailAliasInputs, domain),
|
|
}),
|
|
})
|
|
setVerification(data.verification)
|
|
setResult({ type: 'dns', data })
|
|
markActionDone('applyPortalDns')
|
|
} catch (e) {
|
|
setError(e.message)
|
|
setActionsDone((prev) => ({ ...prev, chooseByoDns: false, applyPortalDns: false }))
|
|
} finally {
|
|
stopBusy()
|
|
}
|
|
}"""
|
|
|
|
text = text.replace(OLD_CHOOSE_PORTAL, NEW_CHOOSE_PORTAL)
|
|
|
|
text = text.replace(
|
|
""" const DNS_ACTION_KEYS = {
|
|
choosePortalDns: false,
|
|
chooseExternalDns: false,""",
|
|
""" const DNS_ACTION_KEYS = {
|
|
choosePortalDns: false,
|
|
chooseByoDns: false,
|
|
chooseExternalDns: false,""",
|
|
)
|
|
|
|
text = text.replace(
|
|
" if (dnsChoice === 'portal') markActionDone('choosePortalDns')\n if (dnsChoice === 'external') markActionDone('chooseExternalDns')",
|
|
" if (dnsChoice === 'portal') markActionDone('choosePortalDns')\n if (dnsChoice === 'byo') markActionDone('chooseByoDns')\n if (dnsChoice === 'external') markActionDone('chooseExternalDns')",
|
|
)
|
|
|
|
text = text.replace(
|
|
" const data = await api(`/onboarding/dns/instructions/${dom}`)\n setInstructions(data)\n setCfStatus(data.cloudflare)",
|
|
" const data = await api(`/onboarding/dns/instructions/${dom}`)\n setInstructions(data)\n setCfStatus(data.cloudflare)\n setDnsResolve(data.resolve || null)",
|
|
)
|
|
|
|
text = text.replace(
|
|
" if (step !== 1 || !instructions || showAdvancedDns || dnsChoice === 'external' || asmMode) return",
|
|
" if (step !== 1 || !instructions || showAdvancedDns || dnsChoice === 'external' || dnsChoice === 'byo' || asmMode) return",
|
|
)
|
|
|
|
text = text.replace(
|
|
" if (portalDnsApplied || loading || autoDnsAttempted.current) return\n autoDnsAttempted.current = true",
|
|
" if (portalDnsApplied || loading || autoDnsAttempted.current) return\n const resolve = instructions?.resolve || dnsResolve\n if (resolve && !resolve.matched) return\n autoDnsAttempted.current = true",
|
|
)
|
|
|
|
text = text.replace(
|
|
" dns_mode: dnsChoice === 'portal' ? 'Cloudflare Ligbox' : 'Provedor externo',",
|
|
" dns_mode:\n dnsChoice === 'portal'\n ? dnsResolve?.dns_mode || cfStatus?.dns_mode || 'ligbox_cf'\n : dnsChoice === 'byo'\n ? 'byo_cloudflare'\n : 'external_registrar',",
|
|
)
|
|
|
|
# Advanced mode choice grid — add BYO button and update portal label
|
|
OLD_GRID = """ <ActionDoneButton
|
|
block
|
|
done={actionsDone.choosePortalDns}
|
|
label="Trazer DNS para o portal"
|
|
hint="Zona DNS Ligbox + apontamentos de email."
|
|
disabled={loading || actionsDone.chooseExternalDns}
|
|
busy={loading && !actionsDone.choosePortalDns}
|
|
onClick={choosePortalDns}
|
|
attentionId="choosePortalDns"
|
|
/>
|
|
<ActionDoneButton
|
|
block
|
|
secondary
|
|
done={actionsDone.chooseExternalDns}
|
|
label="Manter no provedor atual"
|
|
hint="Apontamentos manuais no provedor atual."
|
|
disabled={loading || actionsDone.choosePortalDns}
|
|
busy={loading && !actionsDone.chooseExternalDns}
|
|
onClick={chooseExternalDns}
|
|
attentionId="chooseExternalDns"
|
|
/>"""
|
|
|
|
NEW_GRID = """ <ActionDoneButton
|
|
block
|
|
done={actionsDone.choosePortalDns}
|
|
label={
|
|
dnsResolve?.matched
|
|
? `Cloudflare Ligbox (${dnsResolve.account_id})`
|
|
: 'Cloudflare Ligbox (indisponível)'
|
|
}
|
|
hint={
|
|
dnsResolve?.matched
|
|
? dnsResolve.message
|
|
: 'Domínio não está nas contas ligit / itecnologys / ibytera.'
|
|
}
|
|
disabled={
|
|
loading ||
|
|
actionsDone.chooseExternalDns ||
|
|
actionsDone.chooseByoDns ||
|
|
!dnsResolve?.matched
|
|
}
|
|
busy={loading && !actionsDone.choosePortalDns}
|
|
onClick={choosePortalDns}
|
|
attentionId="choosePortalDns"
|
|
/>
|
|
<ActionDoneButton
|
|
block
|
|
secondary
|
|
done={actionsDone.chooseByoDns}
|
|
label="Minha conta Cloudflare"
|
|
hint="API Token com DNS Edit na zona do domínio."
|
|
disabled={loading || actionsDone.choosePortalDns || actionsDone.chooseExternalDns}
|
|
busy={loading && !actionsDone.chooseByoDns}
|
|
onClick={chooseByoDns}
|
|
attentionId="chooseByoDns"
|
|
/>
|
|
<ActionDoneButton
|
|
block
|
|
secondary
|
|
done={actionsDone.chooseExternalDns}
|
|
label="Registrador / DNS externo"
|
|
hint="Apontamentos manuais (domínio a registar ou NS noutro sítio)."
|
|
disabled={loading || actionsDone.choosePortalDns || actionsDone.chooseByoDns}
|
|
busy={loading && !actionsDone.chooseExternalDns}
|
|
onClick={chooseExternalDns}
|
|
attentionId="chooseExternalDns"
|
|
/>"""
|
|
|
|
text = text.replace(OLD_GRID, NEW_GRID)
|
|
|
|
BYO_PANEL = """
|
|
{dnsChoice === 'byo' && showAdvancedDns && (
|
|
<div className="message" style={{ background: '#f4f0ff', color: '#3d2d6b' }}>
|
|
<strong>Sua conta Cloudflare (BYO)</strong>
|
|
<p className="sub" style={{ margin: '0.35rem 0 0' }}>
|
|
Crie um API Token com permissão <strong>Zone DNS Edit</strong> apenas na zona{' '}
|
|
<code>{domain}</code>.
|
|
</p>
|
|
<label className="sub" style={{ display: 'block', marginTop: '0.75rem' }}>
|
|
API Token Cloudflare
|
|
<input
|
|
type="password"
|
|
className="input"
|
|
style={{ marginTop: '0.35rem', width: '100%' }}
|
|
value={byoToken}
|
|
onChange={(e) => setByoToken(e.target.value)}
|
|
autoComplete="off"
|
|
/>
|
|
</label>
|
|
<div className="actions" style={{ marginTop: '0.75rem' }}>
|
|
<ActionDoneButton
|
|
done={portalDnsApplied}
|
|
label="Validar token e aplicar apontamentos"
|
|
disabled={loading || portalDnsApplied || !byoToken.trim()}
|
|
busy={loading && !portalDnsApplied}
|
|
onClick={connectByoAndApply}
|
|
attentionId="applyPortalDns"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
"""
|
|
|
|
if "dnsChoice === 'byo'" not in text:
|
|
text = text.replace(
|
|
" {dnsChoice === 'external' && showAdvancedDns && (",
|
|
BYO_PANEL + " {dnsChoice === 'external' && showAdvancedDns && (",
|
|
)
|
|
|
|
# Simple mode message when not matched
|
|
SIMPLE_MSG = """
|
|
{!dnsResolve?.matched && !portalDnsApplied && (
|
|
<WizardStatusPanel variant="warn" icon={ShieldCheck} title="Escolha como apontar o DNS">
|
|
<p>
|
|
Este domínio não está nas contas Cloudflare Ligbox. Use{' '}
|
|
<strong>Minha conta Cloudflare</strong> ou{' '}
|
|
<strong>Registrador / DNS externo</strong> (botão técnico abaixo).
|
|
</p>
|
|
</WizardStatusPanel>
|
|
)}
|
|
|
|
"""
|
|
|
|
if "não está nas contas Cloudflare Ligbox" not in text:
|
|
text = text.replace(
|
|
" {portalDnsApplied && (",
|
|
SIMPLE_MSG + " {portalDnsApplied && (",
|
|
)
|
|
|
|
APP.write_text(text, encoding="utf-8")
|
|
print("App.jsx patched OK")
|