Files
Juan DenisandClaude Opus 4.8 d344302ac1 feat(domains): unify domains & DNS into one surface + admin-only Connections
- /domains is one table: app-linked domains + live zones from connected DNS
  providers (Cloudflare, ...) with provider badges. New GET /dns/portfolio
  (aggregates provider zones, marks which are adopted) and idempotent
  POST /dns/adopt (lazy-materialize a managed zone on demand).
- Every row opens one drawer = the unified DNS records surface
  (components/domains/DomainDnsPanel): live Cloudflare records via
  GET /dns/provider-records (no adopt needed), type badges, proxied=>HTTPS hint,
  inline Add record, deep-links to Cloudflare ops / full DNS page. Fixed-layout
  table (no horizontal scroll); actions moved into the drawer header.
- Registration expiry/registrar from Cloudflare Registrar + lazy RDAP fallback
  (GET /dns/registration), persisted/cached in the new domain_registrations
  table and merged into the portfolio so it survives a refresh. New
  frontend/src/utils/expiry.js formatExpiry() date util.
- Connections moved to admin-only (Settings Admin group + isAdmin gate;
  @admin_required on /api/v1/connections); CF connection card links to Domains.
- Cloudflare client: expose each zone's account_id; add list_registrar_domains.
- Tests: backend/tests/test_dns_portfolio.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 00:34:55 -04:00

29 lines
1.2 KiB
Python

from datetime import datetime
from app import db
class DomainRegistration(db.Model):
"""Cached domain registration facts (expiry, registrar) looked up lazily via
RDAP/WHOIS. Persisted so the Domains list can show expiry without re-querying on
every page load — keyed by bare domain so it works whether or not the domain is
adopted as a DNS zone or attached to an app."""
__tablename__ = 'domain_registrations'
id = db.Column(db.Integer, primary_key=True)
domain = db.Column(db.String(256), nullable=False, unique=True, index=True)
expires_at = db.Column(db.DateTime, nullable=True)
registrar = db.Column(db.String(255), nullable=True)
auto_renew = db.Column(db.Boolean, nullable=True)
source = db.Column(db.String(32), nullable=True) # rdap | provider
checked_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self):
return {
'domain': self.domain,
'expires_at': self.expires_at.isoformat() if self.expires_at else None,
'registrar': self.registrar,
'auto_renew': self.auto_renew,
'source': self.source,
'checked_at': self.checked_at.isoformat() if self.checked_at else None,
}