From 75203acff2297f4e546afd81f79a0960375ef97e Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Fri, 10 Jul 2026 15:02:56 +0200 Subject: [PATCH] Move custom domains onto trust centers Custom domains previously hung off the organization. Attach them to the trust center instead, adding default and custom domain references plus a managed flag, and backfill existing rows. Domains now belong to a compliance page rather than the whole organization. Signed-off-by: Bryan Frimin --- pkg/coredata/migrations/20260706T132923Z.sql | 33 +++++ pkg/coredata/trust_center.go | 119 +++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 pkg/coredata/migrations/20260706T132923Z.sql diff --git a/pkg/coredata/migrations/20260706T132923Z.sql b/pkg/coredata/migrations/20260706T132923Z.sql new file mode 100644 index 000000000..21dd42848 --- /dev/null +++ b/pkg/coredata/migrations/20260706T132923Z.sql @@ -0,0 +1,33 @@ +-- Copyright (c) 2026 Probo Inc . +-- +-- Permission to use, copy, modify, and/or distribute this software for any +-- purpose with or without fee is hereby granted, provided that the above +-- copyright notice and this permission notice appear in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +-- PERFORMANCE OF THIS SOFTWARE. + +ALTER TABLE custom_domains ADD COLUMN managed BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE custom_domains ALTER COLUMN managed DROP DEFAULT; + +ALTER TABLE trust_centers ADD COLUMN default_domain_id TEXT REFERENCES custom_domains(id) ON DELETE SET NULL; +ALTER TABLE trust_centers ADD COLUMN custom_domain_id TEXT REFERENCES custom_domains(id) ON DELETE SET NULL; + +UPDATE trust_centers tc +SET custom_domain_id = o.custom_domain_id +FROM organizations o +WHERE o.id = tc.organization_id AND o.custom_domain_id IS NOT NULL; + +DELETE FROM custom_domains cd +WHERE NOT EXISTS ( + SELECT 1 FROM trust_centers tc + WHERE tc.custom_domain_id = cd.id OR tc.default_domain_id = cd.id +); + +DROP INDEX IF EXISTS idx_organizations_custom_domain; +ALTER TABLE organizations DROP COLUMN custom_domain_id; diff --git a/pkg/coredata/trust_center.go b/pkg/coredata/trust_center.go index d7aa7a778..861137bb2 100644 --- a/pkg/coredata/trust_center.go +++ b/pkg/coredata/trust_center.go @@ -47,6 +47,12 @@ type ( LogoFileID *gid.GID `db:"logo_file_id"` DarkLogoFileID *gid.GID `db:"dark_logo_file_id"` NonDisclosureAgreementFileID *gid.GID `db:"non_disclosure_agreement_file_id"` + DefaultDomainID *gid.GID `db:"default_domain_id"` + CustomDomainID *gid.GID `db:"custom_domain_id"` + Description *string `db:"description"` + WebsiteURL *string `db:"website_url"` + Email *string `db:"email"` + HeadquarterAddress *string `db:"headquarter_address"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } @@ -120,6 +126,12 @@ SELECT slug, search_engine_indexing, non_disclosure_agreement_file_id, + default_domain_id, + custom_domain_id, + description, + website_url, + email, + headquarter_address, created_at, updated_at FROM @@ -172,6 +184,12 @@ SELECT slug, search_engine_indexing, non_disclosure_agreement_file_id, + default_domain_id, + custom_domain_id, + description, + website_url, + email, + headquarter_address, created_at, updated_at FROM @@ -224,6 +242,12 @@ SELECT slug, search_engine_indexing, non_disclosure_agreement_file_id, + default_domain_id, + custom_domain_id, + description, + website_url, + email, + headquarter_address, created_at, updated_at FROM @@ -276,6 +300,12 @@ SELECT slug, search_engine_indexing, non_disclosure_agreement_file_id, + default_domain_id, + custom_domain_id, + description, + website_url, + email, + headquarter_address, created_at, updated_at FROM @@ -306,6 +336,65 @@ LIMIT 1; return nil } +// LoadByDomainID loads the compliance page that references the given custom +// domain in either of its two slots (default or custom). It powers the +// reverse SNI lookup: a served host resolves to a custom domain, which resolves +// back to its page. Tenant scope is not applied because SNI resolution happens +// across all tenants for public access. +func (tc *TrustCenter) LoadByDomainID( + ctx context.Context, + conn pg.Querier, + domainID gid.GID, +) error { + q := ` +SELECT + id, + organization_id, + tenant_id, + mailing_list_id, + logo_file_id, + dark_logo_file_id, + active, + slug, + search_engine_indexing, + non_disclosure_agreement_file_id, + default_domain_id, + custom_domain_id, + description, + website_url, + email, + headquarter_address, + created_at, + updated_at +FROM + trust_centers +WHERE + default_domain_id = @domain_id + OR custom_domain_id = @domain_id +LIMIT 1; +` + + args := pgx.StrictNamedArgs{"domain_id": domainID} + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query trust center by domain id: %w", err) + } + + trustCenter, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[TrustCenter]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound + } + + return fmt.Errorf("cannot collect trust center: %w", err) + } + + *tc = trustCenter + + return nil +} + func (tc *TrustCenter) Insert( ctx context.Context, conn pg.Tx, @@ -323,6 +412,12 @@ INSERT INTO trust_centers ( slug, search_engine_indexing, non_disclosure_agreement_file_id, + default_domain_id, + custom_domain_id, + description, + website_url, + email, + headquarter_address, created_at, updated_at ) VALUES ( @@ -336,6 +431,12 @@ INSERT INTO trust_centers ( @slug, @search_engine_indexing, @non_disclosure_agreement_file_id, + @default_domain_id, + @custom_domain_id, + @description, + @website_url, + @email, + @headquarter_address, @created_at, @updated_at ) @@ -352,6 +453,12 @@ INSERT INTO trust_centers ( "slug": tc.Slug, "search_engine_indexing": tc.SearchEngineIndexing, "non_disclosure_agreement_file_id": tc.NonDisclosureAgreementFileID, + "default_domain_id": tc.DefaultDomainID, + "custom_domain_id": tc.CustomDomainID, + "description": tc.Description, + "website_url": tc.WebsiteURL, + "email": tc.Email, + "headquarter_address": tc.HeadquarterAddress, "created_at": tc.CreatedAt, "updated_at": tc.UpdatedAt, } @@ -384,6 +491,12 @@ SET logo_file_id = @logo_file_id, dark_logo_file_id = @dark_logo_file_id, non_disclosure_agreement_file_id = @non_disclosure_agreement_file_id, + default_domain_id = @default_domain_id, + custom_domain_id = @custom_domain_id, + description = @description, + website_url = @website_url, + email = @email, + headquarter_address = @headquarter_address, updated_at = @updated_at WHERE %s @@ -400,6 +513,12 @@ WHERE "slug": tc.Slug, "search_engine_indexing": tc.SearchEngineIndexing, "non_disclosure_agreement_file_id": tc.NonDisclosureAgreementFileID, + "default_domain_id": tc.DefaultDomainID, + "custom_domain_id": tc.CustomDomainID, + "description": tc.Description, + "website_url": tc.WebsiteURL, + "email": tc.Email, + "headquarter_address": tc.HeadquarterAddress, "updated_at": tc.UpdatedAt, } maps.Copy(args, scope.SQLArguments())