Specs stay at repo root (cross-VM). Move deploy and code into logical projects with README per domain, updated manifest.yaml, and symlinks at legacy paths for VM122 backward compatibility.
158 lines
4.3 KiB
Python
158 lines
4.3 KiB
Python
"""Migration API routes — Spec 019."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app import auth
|
|
from app.migration import gate, runner, store
|
|
from app.permissions import can_manage_migration, can_read_migration
|
|
|
|
router = APIRouter(prefix="/api/v1/migration", tags=["migration"])
|
|
|
|
|
|
class MailboxIn(BaseModel):
|
|
email: str
|
|
source_type: str = "imap"
|
|
source_host: str | None = None
|
|
source_user: str | None = None
|
|
|
|
|
|
class CreateJobBody(BaseModel):
|
|
domain: str = Field(..., min_length=3)
|
|
tenant_id: int = 1
|
|
ticket_id: int | None = None
|
|
source_server_label: str = ""
|
|
dest_imap_host: str = ""
|
|
notes: str = ""
|
|
mailboxes: list[MailboxIn] = Field(default_factory=list)
|
|
|
|
|
|
class ApproveGateBody(BaseModel):
|
|
note: str = ""
|
|
|
|
|
|
def _reader(user: auth.DeskUser = Depends(auth.get_current_user)) -> auth.DeskUser:
|
|
if not can_read_migration(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
return user
|
|
|
|
|
|
def _manager(user: auth.DeskUser = Depends(auth.get_current_user)) -> auth.DeskUser:
|
|
if not can_manage_migration(user.role):
|
|
raise HTTPException(403, "permissão insuficiente")
|
|
return user
|
|
|
|
|
|
@router.get("/jobs")
|
|
def list_migration_jobs(
|
|
domain: str = "",
|
|
limit: int = Query(100, ge=1, le=500),
|
|
user: auth.DeskUser = Depends(_reader),
|
|
):
|
|
conn = auth.db()
|
|
try:
|
|
return store.list_jobs(conn, domain=domain.strip() or None, limit=limit)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@router.post("/jobs")
|
|
def create_migration_job(body: CreateJobBody, user: auth.DeskUser = Depends(_manager)):
|
|
conn = auth.db()
|
|
try:
|
|
job = store.create_job(
|
|
conn,
|
|
domain=body.domain,
|
|
tenant_id=body.tenant_id,
|
|
ticket_id=body.ticket_id,
|
|
source_server_label=body.source_server_label,
|
|
dest_imap_host=body.dest_imap_host,
|
|
notes=body.notes,
|
|
mailboxes=[m.model_dump() for m in body.mailboxes],
|
|
)
|
|
return job
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@router.get("/jobs/{job_id}")
|
|
def get_migration_job(job_id: int, user: auth.DeskUser = Depends(_reader)):
|
|
conn = auth.db()
|
|
try:
|
|
job = store.get_job(conn, job_id)
|
|
finally:
|
|
conn.close()
|
|
if not job:
|
|
raise HTTPException(404, "job não encontrado")
|
|
return job
|
|
|
|
|
|
@router.post("/jobs/{job_id}/preflight")
|
|
def migration_preflight(job_id: int, user: auth.DeskUser = Depends(_manager)):
|
|
conn = auth.db()
|
|
try:
|
|
return runner.run_preflight(conn, job_id, user.username)
|
|
except ValueError as e:
|
|
raise HTTPException(404, str(e)) from e
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@router.post("/jobs/{job_id}/sync")
|
|
def migration_sync(
|
|
job_id: int,
|
|
run_type: str = Query("initial", pattern="^(initial|delta|final)$"),
|
|
user: auth.DeskUser = Depends(_manager),
|
|
):
|
|
conn = auth.db()
|
|
try:
|
|
return runner.run_sync(conn, job_id, user.username, run_type=run_type)
|
|
except ValueError as e:
|
|
raise HTTPException(404, str(e)) from e
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@router.get("/jobs/{job_id}/verify")
|
|
def migration_verify(job_id: int, user: auth.DeskUser = Depends(_reader)):
|
|
conn = auth.db()
|
|
try:
|
|
return runner.run_verify(conn, job_id, user.username)
|
|
except ValueError as e:
|
|
raise HTTPException(404, str(e)) from e
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@router.post("/jobs/{job_id}/approve-gate")
|
|
def migration_approve_gate(
|
|
job_id: int,
|
|
body: ApproveGateBody,
|
|
user: auth.DeskUser = Depends(_manager),
|
|
):
|
|
conn = auth.db()
|
|
try:
|
|
job = store.get_job(conn, job_id)
|
|
if not job:
|
|
raise HTTPException(404, "job não encontrado")
|
|
result = gate.approve_gate(conn, job_id, user.username)
|
|
if body.note:
|
|
store.update_job(conn, job_id, notes=(job.get("notes") or "") + f"\n[gate] {body.note}")
|
|
return {"ok": True, **result, "job": store.get_job(conn, job_id)}
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@router.get("/gate")
|
|
def migration_gate_lookup(
|
|
domain: str = Query(..., min_length=3),
|
|
user: auth.DeskUser | None = None,
|
|
):
|
|
"""VM112 consulta antes de DNS — auth opcional via query interna."""
|
|
conn = auth.db()
|
|
try:
|
|
return store.get_gate_for_domain(conn, domain)
|
|
finally:
|
|
conn.close()
|