import uuid import httpx from fastapi import HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from apps.api.src.models.secret import Secret from apps.api.src.models.service_connection import ServiceConnection from apps.api.src.schemas import service_connection as schemas from apps.api.src.security import encryption from apps.api.src.security.ssrf import is_safe_url async def create_connection( db: AsyncSession, data: schemas.ServiceConnectionCreate, owner_id: uuid.UUID, ) -> ServiceConnection: if not is_safe_url(data.base_url): raise HTTPException(status_code=400, detail="URL is not allowed") secret_id = None if data.credentials: secret = Secret( name=f"{data.name} credentials", owner_id=owner_id, encrypted_value=encryption.encrypt_dict(data.credentials), secret_type="plugin_credentials", scope=f"plugin:{data.plugin_id}", ) db.add(secret) await db.flush() secret_id = secret.id conn = ServiceConnection( name=data.name, plugin_id=data.plugin_id, base_url=data.base_url, verify_tls=data.verify_tls, timeout_seconds=data.timeout_seconds, credentials_id=secret_id, is_enabled=data.is_enabled, extra_headers=data.extra_headers, ) db.add(conn) await db.commit() await db.refresh(conn) return conn async def get_connection(db: AsyncSession, connection_id: uuid.UUID) -> ServiceConnection | None: result = await db.execute(select(ServiceConnection).where(ServiceConnection.id == connection_id)) return result.scalar_one_or_none() async def list_connections(db: AsyncSession, plugin_id: str | None = None) -> list[ServiceConnection]: stmt = select(ServiceConnection) if plugin_id: stmt = stmt.where(ServiceConnection.plugin_id == plugin_id) result = await db.execute(stmt) return list(result.scalars().all()) async def update_connection( db: AsyncSession, connection_id: uuid.UUID, data: schemas.ServiceConnectionUpdate, ) -> ServiceConnection: conn = await get_connection(db, connection_id) if not conn: raise HTTPException(status_code=404, detail="Connection not found") if data.base_url is not None and not is_safe_url(data.base_url): raise HTTPException(status_code=400, detail="URL is not allowed") update_data = data.model_dump(exclude_unset=True) credentials = update_data.pop("credentials", None) for key, value in update_data.items(): setattr(conn, key, value) if credentials is not None: if conn.credentials_id: secret = await db.get(Secret, conn.credentials_id) if secret: secret.encrypted_value = encryption.encrypt_dict(credentials) else: secret = Secret( name=f"{conn.name} credentials", encrypted_value=encryption.encrypt_dict(credentials), secret_type="plugin_credentials", scope=f"plugin:{conn.plugin_id}", ) db.add(secret) await db.flush() conn.credentials_id = secret.id await db.commit() await db.refresh(conn) return conn async def delete_connection(db: AsyncSession, connection_id: uuid.UUID) -> None: conn = await get_connection(db, connection_id) if not conn: raise HTTPException(status_code=404, detail="Connection not found") if conn.credentials_id: secret = await db.get(Secret, conn.credentials_id) if secret: await db.delete(secret) await db.delete(conn) await db.commit() async def test_connection(data: schemas.ServiceConnectionTestRequest) -> dict: if not is_safe_url(data.base_url): raise HTTPException(status_code=400, detail="URL is not allowed") try: async with httpx.AsyncClient( verify=data.verify_tls, timeout=float(data.timeout_seconds), follow_redirects=False, ) as client: response = await client.get( data.base_url, headers=data.extra_headers, ) return { "success": 200 <= response.status_code < 400, "status_code": response.status_code, "message": "Connection test completed", } except httpx.HTTPError as e: return {"success": False, "status_code": None, "message": str(e)}