feat: support real browser auth import
This commit is contained in:
@@ -10,6 +10,11 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.services.auth_capture_service import extract_all
|
||||
from app.services.browser_import_service import (
|
||||
ImportSessionError,
|
||||
browser_imports,
|
||||
build_import_result,
|
||||
)
|
||||
from app.services.browser_session_service import (
|
||||
BrowserDependencyError,
|
||||
BrowserSessionError,
|
||||
@@ -43,6 +48,32 @@ class CaptureExtractResponse(BaseModel):
|
||||
candidates: list[dict] = []
|
||||
|
||||
|
||||
class ImportSessionCreate(BaseModel):
|
||||
target_url: str = Field(..., description="Target page URL opened in the user's real browser")
|
||||
|
||||
|
||||
class ImportSessionCreateResponse(BaseModel):
|
||||
session_id: str
|
||||
secret: str
|
||||
expires_in_seconds: int
|
||||
|
||||
|
||||
class ImportSessionStatusResponse(BaseModel):
|
||||
session_id: str
|
||||
ready: bool = False
|
||||
expires_at: float
|
||||
result: Optional[CaptureExtractResponse] = None
|
||||
|
||||
|
||||
class ImportSessionSubmit(BaseModel):
|
||||
secret: str
|
||||
page_url: str = ""
|
||||
cookies: list[dict] = []
|
||||
local_storage: dict[str, Any] = {}
|
||||
session_storage: dict[str, Any] = {}
|
||||
auth_headers: list[dict] = []
|
||||
|
||||
|
||||
def _sanitize_candidate(candidate: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
key: value
|
||||
@@ -134,3 +165,65 @@ async def close_capture_session(
|
||||
await browser_sessions.close(session_id)
|
||||
except Exception as exc:
|
||||
raise _browser_error(exc)
|
||||
|
||||
|
||||
@router.post("/import-sessions", response_model=ImportSessionCreateResponse, status_code=201)
|
||||
async def create_import_session(
|
||||
body: ImportSessionCreate,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
"""Create a one-time real-browser import session.
|
||||
|
||||
The returned secret is intended for the local browser extension only.
|
||||
"""
|
||||
target_url = body.target_url.strip()
|
||||
if not target_url.startswith(("http://", "https://")):
|
||||
raise HTTPException(400, "Only http/https URLs are allowed")
|
||||
session, secret = browser_imports.create(target_url=target_url, created_by=user.email)
|
||||
return ImportSessionCreateResponse(
|
||||
session_id=session.id,
|
||||
secret=secret,
|
||||
expires_in_seconds=600,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/import-sessions/{session_id}", response_model=ImportSessionStatusResponse)
|
||||
async def get_import_session(
|
||||
session_id: str,
|
||||
include_raw: bool = Query(default=False),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
session = browser_imports.get(session_id, created_by=user.email)
|
||||
except ImportSessionError as exc:
|
||||
raise HTTPException(404, str(exc))
|
||||
|
||||
result = None
|
||||
if session.payload is not None:
|
||||
full_result = build_import_result(session.payload)
|
||||
if not include_raw:
|
||||
candidates = [_sanitize_candidate(candidate) for candidate in full_result.get("candidates", [])]
|
||||
result = CaptureExtractResponse(candidates=candidates)
|
||||
else:
|
||||
result = CaptureExtractResponse(**full_result)
|
||||
return ImportSessionStatusResponse(
|
||||
session_id=session.id,
|
||||
ready=session.payload is not None,
|
||||
expires_at=session.expires_at,
|
||||
result=result,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/import-sessions/{session_id}/submit", status_code=204)
|
||||
async def submit_import_session(
|
||||
session_id: str,
|
||||
body: ImportSessionSubmit,
|
||||
):
|
||||
try:
|
||||
browser_imports.submit(
|
||||
session_id=session_id,
|
||||
secret=body.secret,
|
||||
payload=body.model_dump(exclude={"secret"}),
|
||||
)
|
||||
except ImportSessionError as exc:
|
||||
raise HTTPException(400, str(exc))
|
||||
|
||||
Reference in New Issue
Block a user