first commit
This commit is contained in:
commit
cce9a8c9ba
73 changed files with 8665 additions and 0 deletions
249
backend/app/routers/domains.py
Normal file
249
backend/app/routers/domains.py
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
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_provisioning import (
|
||||
activate_company_domain,
|
||||
create_smtp_credential,
|
||||
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=[],
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
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 = validate_domain(payload.domain_name)
|
||||
except ValueError as error:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=str(error),
|
||||
) from error
|
||||
|
||||
if normalized == settings.platform_domain:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Use the platform domain endpoint for getall.help.",
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue