company flows
This commit is contained in:
parent
c04a6ce6fe
commit
a3a6464a99
15 changed files with 963 additions and 60 deletions
|
|
@ -23,7 +23,7 @@ class Settings(BaseSettings):
|
|||
|
||||
platform_domain: str = "gentic.email"
|
||||
platform_spf_include: str = "_spf.gentic.email"
|
||||
app_public_host: str = "email.gentic.email"
|
||||
app_public_host: str = "gentic.email"
|
||||
dkim_selector: str = "agmail"
|
||||
|
||||
# Outbound via self-hosted Postfix (Docker service `smtp` by default)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from app.routers.auth import get_current_account
|
|||
from app.schemas import (
|
||||
DnsVerificationResponse,
|
||||
DomainCreate,
|
||||
DomainNameCreate,
|
||||
MailDomainRead,
|
||||
SmtpCredentialCreate,
|
||||
SmtpCredentialCreated,
|
||||
|
|
@ -20,11 +21,14 @@ from app.schemas import (
|
|||
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 (
|
||||
account_is_domain_owner,
|
||||
activate_company_domain,
|
||||
create_smtp_credential,
|
||||
ensure_ownership_dns_record,
|
||||
ensure_platform_domain,
|
||||
get_account_company_domain,
|
||||
get_owned_domain,
|
||||
list_owned_domains,
|
||||
provision_mail_domain,
|
||||
validate_domain,
|
||||
)
|
||||
|
|
@ -32,6 +36,56 @@ from app.services.domain_provisioning import (
|
|||
router = APIRouter(prefix="/domains", tags=["domains"])
|
||||
|
||||
|
||||
async def require_domain_owner(
|
||||
account: Account,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
if account.account_type != "company":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Company accounts only.",
|
||||
)
|
||||
if not await account_is_domain_owner(session, account.id, account.company_domain):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only the domain owner can manage domains.",
|
||||
)
|
||||
|
||||
|
||||
async def verify_owned_domain_dns(
|
||||
session: AsyncSession,
|
||||
domain: MailDomain,
|
||||
) -> DnsVerificationResponse:
|
||||
settings = get_settings()
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
async def get_domain_or_404(
|
||||
domain_id: UUID,
|
||||
session: AsyncSession,
|
||||
|
|
@ -104,25 +158,86 @@ async def verify_my_company_dns(
|
|||
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,
|
||||
return await verify_owned_domain_dns(session, domain)
|
||||
|
||||
|
||||
@router.get("/me/domains", response_model=list[MailDomainRead])
|
||||
async def list_my_domains(
|
||||
account: Account = Depends(get_current_account),
|
||||
session: AsyncSession = Depends(get_postgres_session),
|
||||
) -> list[MailDomain]:
|
||||
await require_domain_owner(account, session)
|
||||
return await list_owned_domains(session, account.id)
|
||||
|
||||
|
||||
@router.post("/me/domains", response_model=MailDomainRead, status_code=status.HTTP_201_CREATED)
|
||||
async def add_my_domain(
|
||||
payload: DomainNameCreate,
|
||||
account: Account = Depends(get_current_account),
|
||||
session: AsyncSession = Depends(get_postgres_session),
|
||||
) -> MailDomain:
|
||||
settings = get_settings()
|
||||
await require_domain_owner(account, session)
|
||||
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)
|
||||
)
|
||||
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,
|
||||
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=account.id,
|
||||
)
|
||||
await session.commit()
|
||||
return await get_owned_domain(session, domain.id, account.id)
|
||||
|
||||
|
||||
@router.get("/me/domains/{domain_id}", response_model=MailDomainRead)
|
||||
async def get_my_domain(
|
||||
domain_id: UUID,
|
||||
account: Account = Depends(get_current_account),
|
||||
session: AsyncSession = Depends(get_postgres_session),
|
||||
) -> MailDomain:
|
||||
await require_domain_owner(account, session)
|
||||
domain = await get_owned_domain(session, domain_id, account.id)
|
||||
if domain is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Domain not found.",
|
||||
)
|
||||
return domain
|
||||
|
||||
|
||||
@router.post("/me/domains/{domain_id}/verify-dns", response_model=DnsVerificationResponse)
|
||||
async def verify_my_domain_dns(
|
||||
domain_id: UUID,
|
||||
account: Account = Depends(get_current_account),
|
||||
session: AsyncSession = Depends(get_postgres_session),
|
||||
) -> DnsVerificationResponse:
|
||||
await require_domain_owner(account, session)
|
||||
domain = await get_owned_domain(session, domain_id, account.id)
|
||||
if domain is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Domain not found.",
|
||||
)
|
||||
return await verify_owned_domain_dns(session, domain)
|
||||
|
||||
|
||||
@router.get("/platform", response_model=MailDomainRead)
|
||||
|
|
|
|||
|
|
@ -143,6 +143,10 @@ class DomainCreate(BaseModel):
|
|||
account_id: UUID | None = None
|
||||
|
||||
|
||||
class DomainNameCreate(BaseModel):
|
||||
domain_name: str = Field(min_length=3, max_length=253)
|
||||
|
||||
|
||||
class DnsRecordRead(BaseModel):
|
||||
id: UUID
|
||||
record_type: str
|
||||
|
|
|
|||
|
|
@ -385,6 +385,37 @@ async def account_is_domain_owner(
|
|||
return domain_id is not None
|
||||
|
||||
|
||||
async def get_owned_domain(
|
||||
session: AsyncSession,
|
||||
domain_id,
|
||||
account_id,
|
||||
) -> MailDomain | None:
|
||||
return await session.scalar(
|
||||
select(MailDomain)
|
||||
.options(
|
||||
selectinload(MailDomain.dns_records),
|
||||
selectinload(MailDomain.smtp_credentials),
|
||||
)
|
||||
.where(
|
||||
MailDomain.id == domain_id,
|
||||
MailDomain.account_id == account_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def list_owned_domains(
|
||||
session: AsyncSession,
|
||||
account_id,
|
||||
) -> list[MailDomain]:
|
||||
result = await session.scalars(
|
||||
select(MailDomain)
|
||||
.options(selectinload(MailDomain.dns_records))
|
||||
.where(MailDomain.account_id == account_id)
|
||||
.order_by(MailDomain.created_at.asc())
|
||||
)
|
||||
return list(result.all())
|
||||
|
||||
|
||||
async def get_account_company_domain(
|
||||
session: AsyncSession,
|
||||
account_id,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ def test_deployment_sections_split_inbound_and_outbound():
|
|||
mail_server_ip="203.0.113.10",
|
||||
dkim_selector="agmail",
|
||||
dkim_public_key="test-public-key",
|
||||
app_public_host="email.gentic.email",
|
||||
app_public_host="gentic.email",
|
||||
)
|
||||
|
||||
inbound_purposes = [record["purpose"] for record in sections["inbound"]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue