from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.config import get_settings from app.database import get_postgres_session from app.models import Account, MailDomain from app.routers.auth import get_current_account from app.schemas import ( DnsVerificationResponse, DomainCreate, MailDomainRead, SmtpCredentialCreate, SmtpCredentialCreated, SmtpCredentialRead, ) from app.services.dns_verification import verify_company_dns from app.services.domain_policy import assert_company_domain_allowed from app.services.domain_provisioning import ( activate_company_domain, create_smtp_credential, ensure_ownership_dns_record, ensure_platform_domain, get_account_company_domain, provision_mail_domain, validate_domain, ) router = APIRouter(prefix="/domains", tags=["domains"]) async def get_domain_or_404( domain_id: UUID, session: AsyncSession, ) -> MailDomain: domain = await session.scalar( select(MailDomain) .options( selectinload(MailDomain.dns_records), selectinload(MailDomain.smtp_credentials), ) .where(MailDomain.id == domain_id) ) if domain is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Domain not found.", ) return domain @router.get("/me", response_model=MailDomainRead) async def get_my_company_domain( account: Account = Depends(get_current_account), session: AsyncSession = Depends(get_postgres_session), ) -> MailDomain: if account.account_type != "company" or not account.company_domain: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No company domain is linked to this account.", ) domain = await get_account_company_domain( session, account.id, account.company_domain, ) if domain is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Company domain has not been provisioned yet.", ) return domain @router.post("/me/verify-dns", response_model=DnsVerificationResponse) async def verify_my_company_dns( account: Account = Depends(get_current_account), session: AsyncSession = Depends(get_postgres_session), ) -> DnsVerificationResponse: settings = get_settings() if account.account_type != "company" or not account.company_domain: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No company domain is linked to this account.", ) domain = await get_account_company_domain( session, account.id, account.company_domain, ) if domain is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Company domain has not been provisioned yet.", ) if domain.status == "active": return DnsVerificationResponse( domain=domain.domain_name, status=domain.status, verified=True, checks=[], ) verification_token = await ensure_ownership_dns_record(session, domain) checks = verify_company_dns( domain_name=domain.domain_name, shared_mail_host=settings.smtp_public_host, dkim_selector=domain.dkim_selector, dkim_public_key=domain.dkim_public_key, platform_spf_include=settings.platform_spf_include, verification_token=verification_token, ) verified = all(check["verified"] for check in checks) if verified: await activate_company_domain(session, domain) await session.commit() return DnsVerificationResponse( domain=domain.domain_name, status=domain.status if not verified else "active", verified=verified, checks=checks, ) @router.get("/platform", response_model=MailDomainRead) async def get_platform_domain( session: AsyncSession = Depends(get_postgres_session), ) -> MailDomain: domain = await ensure_platform_domain(session) await session.commit() return await get_domain_or_404(domain.id, session) @router.get("", response_model=list[MailDomainRead]) async def list_domains( session: AsyncSession = Depends(get_postgres_session), ) -> list[MailDomain]: await ensure_platform_domain(session) await session.commit() result = await session.scalars( select(MailDomain) .options(selectinload(MailDomain.dns_records)) .order_by(MailDomain.created_at.desc()) ) return list(result.all()) @router.post("", response_model=MailDomainRead, status_code=status.HTTP_201_CREATED) async def register_company_domain( payload: DomainCreate, session: AsyncSession = Depends(get_postgres_session), ) -> MailDomain: settings = get_settings() try: normalized = assert_company_domain_allowed( payload.domain_name, settings.platform_domain, ) except ValueError as error: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(error), ) from error existing = await session.scalar( select(MailDomain).where(MailDomain.domain_name == normalized) ) if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="That domain is already registered.", ) domain = await provision_mail_domain( session, domain_name=normalized, domain_type="company", account_id=payload.account_id, ) await session.commit() return await get_domain_or_404(domain.id, session) @router.get("/{domain_id}", response_model=MailDomainRead) async def get_domain( domain_id: UUID, session: AsyncSession = Depends(get_postgres_session), ) -> MailDomain: return await get_domain_or_404(domain_id, session) @router.get("/{domain_id}/dns-records", response_model=list) async def list_dns_records( domain_id: UUID, session: AsyncSession = Depends(get_postgres_session), ): domain = await get_domain_or_404(domain_id, session) return [ { "type": record.record_type, "host": record.host, "value": record.value, "purpose": record.purpose, "ttl": record.ttl, } for record in domain.dns_records ] @router.get("/{domain_id}/smtp-credentials", response_model=list[SmtpCredentialRead]) async def list_smtp_credentials( domain_id: UUID, session: AsyncSession = Depends(get_postgres_session), ): domain = await get_domain_or_404(domain_id, session) return domain.smtp_credentials @router.post( "/{domain_id}/smtp-credentials", response_model=SmtpCredentialCreated, status_code=status.HTTP_201_CREATED, ) async def create_domain_smtp_credential( domain_id: UUID, payload: SmtpCredentialCreate, session: AsyncSession = Depends(get_postgres_session), ): settings = get_settings() domain = await get_domain_or_404(domain_id, session) credential, plain_password = await create_smtp_credential( session, domain, label=payload.label, ) await session.commit() return SmtpCredentialCreated( id=credential.id, username=credential.username, label=credential.label, is_active=credential.is_active, created_at=credential.created_at, password=plain_password, smtp_host=settings.smtp_public_host, smtp_port=settings.smtp_port, use_tls=settings.smtp_use_tls, )