panel-redesign: the 2.0 schema from zero - one baseline, no 1.x chain
Stage 1, step 1. BREAKING: a 1.x database is not migrated.
Schema
- internal/store/migrations/ holds one file, 0001_init.sql, equal to the
2.0 schema; 0002..0010 are deleted (they stay in history at 1.9.5). No
admin table created and dropped, no ALTERs.
- users.email replaces users.dmarc_report_email: the address is the
user's own. The default DMARC report address is a user's setting
(dmarc_default_mode hosted/account/custom/none, dmarc_default_address);
the instance setting that mirrored the global user's field is gone.
- domains.dmarc_rua_user_id: a domain follows a named user's default, or
has an address of its own in dmarc_rua ('' = none). ON DELETE SET NULL
leaves it at "no reports", never at someone else's default.
- user_inbound_domains, users.all_domains, users.all_inbound_domains: in
the schema for the inbound delegation of the next step.
- users.role is 'global' / 'domain' (was 'domain_admin');
rate_limits.allowed_ips, unused since 1.9.0, is dropped.
Opening a database
- The baseline sets PRAGMA application_id ("SP20"). store.Open refuses a
file that has a schema version without it (ErrForeignSchema) and one
newer than the build. A 1.x file at user_version 10 would otherwise read
as already migrated. The refused file is left untouched.
Code
- Store.DomainDMARCRua is the one place a domain's report address is
resolved; dnscheck.ResolveDMARCRua and Store.GlobalDMARCReportEmail are
gone. dmarc.AllowedRecipients, the domain page, the dashboard and the
DNS re-check go through it.
- The old templates keep working on the new model until their pages are
restyled: the Settings form's address field is the account e-mail and
switches the user's default between it and none; a domain's "Same as
Settings" makes it follow the user who chose it; a new or imported
domain follows its creator.
- Domain export/import: a domain that follows a user exports no address.
Tests: the baseline pinned table by table (TestBaselineSchema), refusal of a
1.x and of a newer database, constraints and cascades of the new columns and
table, resolution in every default mode, the deleted-user case, and the three
bridged handlers.
docs/schema-migrations.md is rewritten for the baseline; roadmap 11/27,
schema-squash marked built.
go test ./... (Windows)
ok for every package except two that fail the same way before this commit:
internal/domain TestWriteLoadPrivateKeyRoundtrip (file mode on NTFS)
internal/logtail TestFollowTailsAndRotates (rename of an open file)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -153,29 +153,37 @@ func (h *Handlers) renderDomainDetail(w http.ResponseWriter, r *http.Request, st
|
||||
// What DNS actually publishes for the domain today, checked against the key
|
||||
// this server signs with. Cached by the checker, so re-rendering the page
|
||||
// after a form post costs nothing.
|
||||
profileEmail, err := h.store.GlobalDMARCReportEmail()
|
||||
reportEmail, err := h.domainReportAddress(d)
|
||||
if err != nil {
|
||||
logf("panel: domain %d: global dmarc email: %v", d.ID, err)
|
||||
logf("panel: domain %d: dmarc report address: %v", d.ID, err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
reportEmail := dnscheck.ResolveDMARCRua(d.DMARCRua, profileEmail)
|
||||
dns, srv := h.domainDNS(d, record, profileEmail, false)
|
||||
dns, srv := h.domainDNS(d, record, reportEmail, false)
|
||||
reportAuthName, reportAuthValue, needsReportAuth := dnscheck.ExternalReportAuth(d.Name, reportEmail)
|
||||
hostedAddr := h.hostedDMARCAddress(d.Name)
|
||||
// The form's "inherit" is a domain that follows a user's default. What it
|
||||
// offers next to that choice is the address following would give: the
|
||||
// followed user's when the domain already follows one, otherwise the
|
||||
// signed-in user's own — choosing it makes the domain follow them.
|
||||
dmarcMode := "inherit"
|
||||
dmarcCustom := ""
|
||||
hostedAddr := ""
|
||||
if h.dmarc != nil && h.cfg.DMARCEnabled {
|
||||
hostedAddr = dmarc.HostedReportAddress(h.cfg.Hostname, d.Name)
|
||||
}
|
||||
if d.DMARCRua.Valid {
|
||||
if d.DMARCRua.String == "" {
|
||||
profileEmail := reportEmail
|
||||
if !d.DMARCRuaUserID.Valid {
|
||||
profileEmail = ""
|
||||
if p, ok := h.principal(r); ok {
|
||||
if def, err := h.store.GetDMARCDefault(p.ID); err == nil {
|
||||
profileEmail = def.Resolve(hostedAddr)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case d.DMARCRua == "":
|
||||
dmarcMode = "none"
|
||||
} else if hostedAddr != "" && strings.EqualFold(d.DMARCRua.String, hostedAddr) {
|
||||
case hostedAddr != "" && strings.EqualFold(d.DMARCRua, hostedAddr):
|
||||
dmarcMode = "hosted"
|
||||
} else {
|
||||
default:
|
||||
dmarcMode = "custom"
|
||||
dmarcCustom = d.DMARCRua.String
|
||||
dmarcCustom = d.DMARCRua
|
||||
}
|
||||
}
|
||||
dmarcSource := "policy"
|
||||
@@ -186,7 +194,7 @@ func (h *Handlers) renderDomainDetail(w http.ResponseWriter, r *http.Request, st
|
||||
dmarcSource = "custom"
|
||||
case dmarcMode == "none":
|
||||
dmarcSource = "none"
|
||||
case profileEmail != "":
|
||||
case reportEmail != "":
|
||||
dmarcSource = "settings"
|
||||
}
|
||||
|
||||
@@ -250,7 +258,7 @@ func (h *Handlers) renderDomainDetail(w http.ResponseWriter, r *http.Request, st
|
||||
// and no extra environment variable is needed. That server result is returned
|
||||
// alongside, because the page's suggested SPF record is built from the same
|
||||
// addresses. force bypasses the cache, for the Re-check button.
|
||||
func (h *Handlers) domainDNS(d store.Domain, record domain.DKIMRecord, profileEmail string, force bool) (dnscheck.Domain, dnscheck.Server) {
|
||||
func (h *Handlers) domainDNS(d store.Domain, record domain.DKIMRecord, reportEmail string, force bool) (dnscheck.Domain, dnscheck.Server) {
|
||||
srv := h.dns.Server(h.cfg.Hostname, false)
|
||||
return h.dns.Domain(dnscheck.Query{
|
||||
Name: d.Name,
|
||||
@@ -258,7 +266,7 @@ func (h *Handlers) domainDNS(d store.Domain, record domain.DKIMRecord, profileEm
|
||||
ExpectedDKIM: record.Value,
|
||||
Hostname: srv.Hostname,
|
||||
ServerIPs: srv.IPs,
|
||||
DMARCReportEmail: dnscheck.ResolveDMARCRua(d.DMARCRua, profileEmail),
|
||||
DMARCReportEmail: reportEmail,
|
||||
}, force), srv
|
||||
}
|
||||
|
||||
@@ -275,13 +283,13 @@ func (h *Handlers) HandleDomainDNSRecheck(w http.ResponseWriter, r *http.Request
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
profileEmail, err := h.store.GlobalDMARCReportEmail()
|
||||
reportEmail, err := h.domainReportAddress(d)
|
||||
if err != nil {
|
||||
logf("panel: domain %d: global dmarc email: %v", d.ID, err)
|
||||
logf("panel: domain %d: dmarc report address: %v", d.ID, err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.domainDNS(d, record, profileEmail, true)
|
||||
h.domainDNS(d, record, reportEmail, true)
|
||||
http.Redirect(w, r, fmt.Sprintf("/domains/%d?rechecked=1", d.ID), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user