"""Pydantic schemas exchanged with the admin UI.""" from __future__ import annotations from datetime import datetime from typing import Any from uuid import UUID from pydantic import BaseModel, Field # ─── Driver-declared field schema ────────────────────────────────── class IntegrationFieldInfo(BaseModel): """A single config field in the admin form. Driver implementations declare an ordered list of these so the frontend can render a category-specific edit form without hardcoding anything per provider. """ key: str label: str hint: str | None = None placeholder: str | None = None is_secret: bool = False required: bool = False is_textarea: bool = False options: list[dict[str, Any]] | None = None visible_when: dict[str, Any] | None = None default: Any = None class IntegrationDriverMetadata(BaseModel): """Driver self-description used by the ``/kinds`` endpoint.""" kind: str provider: str display_name: str description: str # Tenant-level config: the credentials + global settings stored on the # ``integrations`` row itself. Rendered in the "add/edit integration" # form under /admin/integrations. fields: list[IntegrationFieldInfo] # Per-binding config: only meaningful for ``software_provisioning`` # drivers where each software has its own binding row with params # specific to that software (group_email, group_chat_id, operation...). # Rendered in the binding form under /admin/provisioning. Empty for # kinds that don't have per-binding params (HRIS, cloud, approval). binding_fields: list[IntegrationFieldInfo] = Field(default_factory=list) supports_multi_instance: bool = True supports_test_connection: bool = True capabilities: list[str] = Field(default_factory=list) # ─── Integration CRUD ────────────────────────────────────────────── class IntegrationResponse(BaseModel): id: UUID kind: str provider: str name: str slug: str description: str | None enabled: bool is_primary: bool # Non-secret values verbatim; secret keys surface as ``_set: bool`` # so the admin knows whether one is on file without leaking plaintext. config: dict[str, Any] # ``capabilities`` is the raw admin-saved JSON column — None means # "never set, fall back to driver default", [] means "admin explicitly # cleared". The integration admin editor displays this verbatim so # admins can see what's saved. capabilities: list[str] | None = None # ``effective_capabilities`` is the resolved set used by feature # gates everywhere else (DNS picker, sync orchestrator UI hints). # When ``capabilities`` is None/[], we fall back to the cmdb provider # class's ``supported_capabilities()`` — same rule as # ``resolve_sync_capabilities`` in scheduled_jobs_provisioning so # the FE can't see a different capability set than the cron scheduler. # Prod bug 2026-06-01: AWS integration row had capabilities=NULL, so # the DNS picker filtered AWS out and silently picked AliCloud # (海外) as "first capable" for unrelated zones. Surfacing the # driver-derived set here removes the silent mis-classification. effective_capabilities: list[str] = Field(default_factory=list) last_tested_at: datetime | None last_test_status: str | None last_test_message: str | None created_at: datetime updated_at: datetime created_by: UUID | None = None updated_by: UUID | None = None updated_by_name: str | None = None class IntegrationUpsertRequest(BaseModel): # New integrations require kind + provider; edits keep them immutable # (change requires delete + recreate to avoid silent driver swaps). kind: str | None = None provider: str | None = None name: str | None = None slug: str | None = None description: str | None = None enabled: bool | None = None is_primary: bool | None = None capabilities: list[str] | None = None # Plain field dict. Secret keys: empty-string or absent keeps # existing value, any other value rotates it. config: dict[str, Any] = Field(default_factory=dict) class WebhookIpAllowlistRequest(BaseModel): # Bare IPs or CIDRs (IPv4/IPv6). Empty list clears the allowlist # (= allow all source IPs). Validated/normalized server-side. cidrs: list[str] = Field(default_factory=list) class IntegrationTestResult(BaseModel): ok: bool message: str detail: dict[str, Any] = Field(default_factory=dict) # ─── Approval flows ──────────────────────────────────────────────── class ApprovalFlowResponse(BaseModel): id: UUID integration_id: UUID flow_key: str label: str external_code: str payload_schema: dict[str, Any] | None active: bool created_at: datetime updated_at: datetime class ApprovalFlowUpsertRequest(BaseModel): flow_key: str label: str external_code: str payload_schema: dict[str, Any] | None = None active: bool = True # ─── Platform settings (non-integration config) ─────────────────── class PlatformSettingResponse(BaseModel): key: str value: Any description: str | None updated_by: UUID | None = None updated_by_name: str | None = None updated_at: datetime created_at: datetime class PlatformSettingUpsertRequest(BaseModel): value: Any description: str | None = None