diff --git a/control/provision.py b/control/provision.py index 21d7e4c..6383195 100644 --- a/control/provision.py +++ b/control/provision.py @@ -190,18 +190,28 @@ def seed_admin(db_uri, email, username=None, full_name=None, expires_hours=72): # ── domain registration ────────────────────────────────────────────────────── +def _upsert_domain(session, tenant_id, domain, kind, is_primary, verified, + tls_status, verification_token=None): + """Delete any stale row for this domain then insert fresh. Handles orphans + from failed partial runs that left domain rows behind.""" + existing = session.query(TenantDomain).filter_by(domain=domain).first() + if existing is not None: + session.delete(existing) + session.flush() + session.add(TenantDomain( + tenant_id=tenant_id, domain=domain, kind=kind, is_primary=is_primary, + verified=verified, verification_token=verification_token, + tls_status=tls_status, created_at=now_eastern())) + + def _add_domains(session, tenant_id, slug, base_domain, custom_domain=None): primary = f'{slug}.{base_domain}'.lower() - session.add(TenantDomain( - tenant_id=tenant_id, domain=primary, kind='subdomain', - is_primary=True, verified=True, tls_status='pending', - created_at=now_eastern())) + _upsert_domain(session, tenant_id, primary, 'subdomain', + is_primary=True, verified=True, tls_status='pending') if custom_domain: - session.add(TenantDomain( - tenant_id=tenant_id, domain=custom_domain.lower(), kind='custom', - is_primary=False, verified=False, - verification_token=secrets.token_hex(16), - tls_status='pending', created_at=now_eastern())) + _upsert_domain(session, tenant_id, custom_domain.lower(), 'custom', + is_primary=False, verified=False, tls_status='pending', + verification_token=secrets.token_hex(16)) return primary @@ -332,10 +342,8 @@ def register_tenant_zero(slug, name, plan_code, db_host, db_name, db_user, primary = _add_domains(s, tenant_id, slug, base, None) if custom_domain: # tenant-zero's existing domain is trusted/verified (already live) - s.add(TenantDomain( - tenant_id=tenant_id, domain=custom_domain.lower(), kind='custom', - is_primary=False, verified=True, tls_status='active', - created_at=now_eastern())) + _upsert_domain(s, tenant_id, custom_domain.lower(), 'custom', + is_primary=False, verified=True, tls_status='active') s.add(ProvisioningJob(tenant_id=tenant_id, action='create_db', status='ok', created_at=now_eastern(), finished_at=now_eastern(), log=f'registered tenant-zero {slug} in place (head={head})')) @@ -374,11 +382,18 @@ def delete_tenant(slug, drop_db=False, user_host='%'): dropped = True with control_session() as s: + # Delete by tenant_id (normal path) AND by domain string (catches orphans + # from partial failed runs where the tenant row was rolled back but domain + # rows were already flushed to disk). s.query(TenantDomain).filter_by(tenant_id=tenant_id).delete() s.query(ProvisioningJob).filter_by(tenant_id=tenant_id).delete() t = s.get(Tenant, tenant_id) if t: s.delete(t) + # Purge any orphan domain rows for this slug's derived domains. + for orphan_domain in [f'{slug}.{os.environ.get("TENANT_BASE_DOMAIN", "")}']: + if orphan_domain.strip('.'): + s.query(TenantDomain).filter_by(domain=orphan_domain.lower()).delete() return {'slug': slug, 'tenant_id': tenant_id, 'db_dropped': dropped, 'db_name': db_name}