Add .env.example with configuration for database, Redis, security, SMTP, workers, and plugins. Add .gitignore for Python, Node.js, Next.js, Docker volumes, and IDE files. Add MIT License. Update README.md with feature overview, quick start guide, architecture description, plugin system documentation, security details, backup/restore instructions, and developer setup. Add Alembic configuration files and placeholder directories for API, web, worker, and plugin components
135 lines
4.5 KiB
Python
135 lines
4.5 KiB
Python
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)}
|