Harden compliance portal data migrations

Mint certificate and domain GIDs with generate_gid, preserve
org profile and domain rows that lack a trust center, and skip
managed default-domain backfill when no base domain is set.

Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
Bryan Frimin
2026-07-17 12:29:36 +02:00
parent 89f999d55c
commit bc78f08334
5 changed files with 83 additions and 46 deletions

View File

@@ -18,6 +18,37 @@ 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;
INSERT INTO trust_centers (
id,
organization_id,
tenant_id,
active,
slug,
created_at,
updated_at
)
SELECT
generate_gid(decode_base64_unpadded(o.tenant_id), 22),
o.id,
o.tenant_id,
false,
LOWER(
REGEXP_REPLACE(
REGEXP_REPLACE(
unaccent(o.name),
'[^a-zA-Z0-9\s]', '', 'g'
),
'\s+', '-', 'g'
)
),
NOW(),
NOW()
FROM organizations o
WHERE o.custom_domain_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM trust_centers tc WHERE tc.organization_id = o.id
);
UPDATE trust_centers tc
SET custom_domain_id = o.custom_domain_id
FROM organizations o

View File

@@ -42,10 +42,9 @@ CREATE INDEX idx_certificates_http_challenge_token ON certificates(http_challeng
WHERE http_challenge_token IS NOT NULL;
-- Backfill one certificate per existing custom domain. A fresh GID is minted
-- for each row: the source domain's tenant (first 8 bytes of its GID) is kept,
-- the entity type is set to 104 (CertificateEntityType), and a millisecond
-- timestamp plus random suffix guarantee uniqueness. The certificate hostname
-- equals the custom domain name, which lets us link the two afterwards.
-- for each row with generate_gid (entity type 104 = CertificateEntityType).
-- The certificate hostname equals the custom domain name, which lets us link
-- the two afterwards.
INSERT INTO certificates (
id,
tenant_id,
@@ -66,17 +65,7 @@ INSERT INTO certificates (
updated_at
)
SELECT
translate(
encode(
substring(decode(translate(cd.id, '-_', '+/'), 'base64') FROM 1 FOR 8)
|| int2send(104::smallint)
|| int8send((floor(extract(epoch FROM clock_timestamp()) * 1000))::bigint)
|| substring(decode(md5(random()::text || cd.id), 'hex') FROM 1 FOR 6),
'base64'
),
'+/',
'-_'
),
generate_gid(decode_base64_unpadded(cd.tenant_id), 104),
cd.tenant_id,
cd.domain,
cd.ssl_certificate,
@@ -100,7 +89,8 @@ ALTER TABLE custom_domains ADD COLUMN certificate_id TEXT REFERENCES certificate
UPDATE custom_domains cd
SET certificate_id = c.id
FROM certificates c
WHERE c.hostname = cd.domain;
WHERE c.hostname = cd.domain
AND c.tenant_id = cd.tenant_id;
-- Repoint the certificate cache from the custom domain to the certificate.
ALTER TABLE cached_certificates ADD COLUMN certificate_id TEXT REFERENCES certificates(id) ON DELETE CASCADE;

View File

@@ -18,6 +18,42 @@ ALTER TABLE trust_centers
ADD COLUMN email CITEXT,
ADD COLUMN headquarter_address TEXT;
INSERT INTO trust_centers (
id,
organization_id,
tenant_id,
active,
slug,
created_at,
updated_at
)
SELECT
generate_gid(decode_base64_unpadded(o.tenant_id), 22),
o.id,
o.tenant_id,
false,
LOWER(
REGEXP_REPLACE(
REGEXP_REPLACE(
unaccent(o.name),
'[^a-zA-Z0-9\s]', '', 'g'
),
'\s+', '-', 'g'
)
),
NOW(),
NOW()
FROM organizations o
WHERE NOT EXISTS (
SELECT 1 FROM trust_centers tc WHERE tc.organization_id = o.id
)
AND (
o.description IS NOT NULL
OR o.website_url IS NOT NULL
OR o.email IS NOT NULL
OR o.headquarter_address IS NOT NULL
);
UPDATE trust_centers tc
SET
description = o.description,

View File

@@ -14,8 +14,9 @@
-- Backfill managed default domains for compliance pages created before
-- default-domain provisioning existed. Hostnames use the managed base domain
-- suffix configured on the database (probo.trust_center_base_domain), falling
-- back to probopage.com for production installs.
-- suffix configured on the database (probo.trust_center_base_domain). Self-
-- managed installs without that setting are left alone — they do not get a
-- SaaS default domain invented for them.
WITH pending_pages AS (
SELECT
@@ -24,14 +25,10 @@ WITH pending_pages AS (
tc.organization_id,
tc.slug,
tc.created_at,
(
tc.slug || '.' || COALESCE(
NULLIF(current_setting('probo.trust_center_base_domain', true), ''),
'probopage.com'
)
)::citext AS hostname
(tc.slug || '.' || current_setting('probo.trust_center_base_domain', true))::citext AS hostname
FROM trust_centers tc
WHERE tc.default_domain_id IS NULL
AND NULLIF(current_setting('probo.trust_center_base_domain', true), '') IS NOT NULL
),
minted_certificates AS (
INSERT INTO certificates (
@@ -44,17 +41,7 @@ minted_certificates AS (
updated_at
)
SELECT
translate(
encode(
substring(decode(translate(pp.trust_center_id, '-_', '+/'), 'base64') FROM 1 FOR 8)
|| int2send(104::smallint)
|| int8send((floor(extract(epoch FROM clock_timestamp()) * 1000))::bigint)
|| substring(decode(md5(random()::text || pp.trust_center_id), 'hex') FROM 1 FOR 6),
'base64'
),
'+/',
'-_'
),
generate_gid(decode_base64_unpadded(pp.tenant_id), 104),
pp.tenant_id,
pp.hostname,
'PENDING'::custom_domain_ssl_status,
@@ -76,17 +63,7 @@ minted_domains AS (
updated_at
)
SELECT
translate(
encode(
substring(decode(translate(mc.id, '-_', '+/'), 'base64') FROM 1 FOR 8)
|| int2send(37::smallint)
|| int8send((floor(extract(epoch FROM clock_timestamp()) * 1000))::bigint)
|| substring(decode(md5(random()::text || mc.id), 'hex') FROM 1 FOR 6),
'base64'
),
'+/',
'-_'
),
generate_gid(decode_base64_unpadded(mc.tenant_id), 37),
mc.tenant_id,
pp.organization_id,
pp.hostname,

View File

@@ -12,6 +12,9 @@
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-- PERFORMANCE OF THIS SOFTWARE.
-- page_title (later renamed to title) is a full page heading, not an org-name
-- fragment for possessives like "{{name}}'s trust center". Seed from the org
-- name so existing pages keep a sensible default until editors customize it.
ALTER TABLE trust_centers
ADD COLUMN page_title TEXT;