feat(FN-004): merge fusion/fn-004
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from app.auth.dependencies import get_current_active_user, get_current_user
|
||||
|
||||
__all__ = ["get_current_user", "get_current_active_user"]
|
||||
@@ -0,0 +1,93 @@
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.jwt import decode_token
|
||||
from app.config import settings
|
||||
from app.db import get_db_session
|
||||
from app.models.user import User
|
||||
|
||||
bearer_scheme = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
async def _get_or_create_dev_user(session: AsyncSession) -> User:
|
||||
"""Return or create the fixed development user."""
|
||||
result = await session.execute(
|
||||
select(User).where(User.authentik_sub == "dev-user")
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
if user is None:
|
||||
user = User(
|
||||
authentik_sub="dev-user",
|
||||
email="dev@localhost",
|
||||
display_name="Dev User",
|
||||
is_active=True,
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
token: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> User:
|
||||
if token is None:
|
||||
if settings.debug and settings.auth_dev_bypass:
|
||||
return await _get_or_create_dev_user(session)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
claims = decode_token(token.credentials)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Invalid token: {exc}",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
) from exc
|
||||
|
||||
authentik_sub = claims.get("sub")
|
||||
email = claims.get("email", "")
|
||||
display_name = claims.get("name") or claims.get("preferred_username") or email
|
||||
|
||||
if not authentik_sub:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token missing 'sub' claim",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
result = await session.execute(
|
||||
select(User).where(User.authentik_sub == authentik_sub)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
user = User(
|
||||
authentik_sub=authentik_sub,
|
||||
email=email,
|
||||
display_name=display_name,
|
||||
is_active=True,
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Inactive user",
|
||||
)
|
||||
return current_user
|
||||
@@ -0,0 +1,53 @@
|
||||
from typing import Any
|
||||
|
||||
import jwt
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
def decode_token(token: str) -> dict[str, Any]:
|
||||
"""Decode a JWT token.
|
||||
|
||||
When authentik_issuer_url is configured, validates the token
|
||||
against the OIDC discovery document JWKS.
|
||||
Otherwise, decodes without verification (local development only).
|
||||
"""
|
||||
if settings.authentik_issuer_url:
|
||||
import httpx
|
||||
|
||||
issuer = settings.authentik_issuer_url.rstrip("/")
|
||||
discovery_url = f"{issuer}/.well-known/openid-configuration"
|
||||
with httpx.Client() as client:
|
||||
resp = client.get(discovery_url)
|
||||
resp.raise_for_status()
|
||||
discovery = resp.json()
|
||||
jwks_uri = discovery["jwks_uri"]
|
||||
|
||||
jwks_resp = client.get(jwks_uri)
|
||||
jwks_resp.raise_for_status()
|
||||
jwks = jwks_resp.json()
|
||||
|
||||
signing_key = jwt.algorithms.RSAAlgorithm.from_jwk(
|
||||
_find_matching_key(jwks, token)
|
||||
)
|
||||
|
||||
return jwt.decode(
|
||||
token,
|
||||
signing_key, # type: ignore[arg-type]
|
||||
algorithms=["RS256"],
|
||||
audience=settings.authentik_client_id,
|
||||
issuer=settings.authentik_issuer_url,
|
||||
)
|
||||
|
||||
return jwt.decode(token, options={"verify_signature": False})
|
||||
|
||||
|
||||
def _find_matching_key(jwks: dict[str, Any], token: str) -> dict[str, Any]:
|
||||
"""Find the key in JWKS that matches the token's kid header."""
|
||||
unverified_header = jwt.get_unverified_header(token)
|
||||
kid = unverified_header.get("kid")
|
||||
for key in jwks.get("keys", []):
|
||||
key_dict: dict[str, Any] = key
|
||||
if key_dict.get("kid") == kid:
|
||||
return key_dict
|
||||
raise RuntimeError(f"No matching JWKS key found for kid={kid}")
|
||||
@@ -12,17 +12,22 @@ class Settings(BaseSettings):
|
||||
debug: bool = False
|
||||
api_v1_prefix: str = "/api/v1"
|
||||
|
||||
# Authentik OIDC placeholders (to be wired in FN-004)
|
||||
# Authentik OIDC
|
||||
authentik_issuer_url: str = ""
|
||||
authentik_client_id: str = ""
|
||||
authentik_client_secret: str = ""
|
||||
|
||||
# Database (to be wired in FN-004)
|
||||
# Database
|
||||
database_url: str = "postgresql://postgres:postgres@localhost:5432/headquarter"
|
||||
|
||||
# Deployment
|
||||
root_domain: str = "localhost"
|
||||
tool_subdomain_pattern: str = "{tool}-{project}-{user}.tools.{root_domain}"
|
||||
|
||||
# Auth & encryption
|
||||
secret_encryption_key: str = "change-me-in-production"
|
||||
access_token_expire_minutes: int = 60
|
||||
auth_dev_bypass: bool = False
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from app.config import settings
|
||||
|
||||
# Rewrite sync postgres URL to asyncpg
|
||||
DATABASE_URL = settings.database_url
|
||||
if DATABASE_URL.startswith("postgresql://"):
|
||||
DATABASE_URL = DATABASE_URL.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=settings.debug)
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
|
||||
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
@@ -0,0 +1,30 @@
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
def _derive_fernet_key(key: str) -> bytes:
|
||||
"""Derive a URL-safe base64-encoded 32-byte Fernet key from any string."""
|
||||
digest = hashlib.sha256(key.encode("utf-8")).digest()
|
||||
return base64.urlsafe_b64encode(digest)
|
||||
|
||||
|
||||
_fernet = Fernet(_derive_fernet_key(settings.secret_encryption_key))
|
||||
|
||||
|
||||
def encrypt_value(plain_text: str) -> str:
|
||||
"""Encrypt a plaintext string and return the ciphertext as a string."""
|
||||
token = _fernet.encrypt(plain_text.encode("utf-8"))
|
||||
return token.decode("utf-8")
|
||||
|
||||
|
||||
def decrypt_value(cipher_text: str) -> str:
|
||||
"""Decrypt a ciphertext string and return the plaintext."""
|
||||
try:
|
||||
plain = _fernet.decrypt(cipher_text.encode("utf-8"))
|
||||
except InvalidToken as exc:
|
||||
raise RuntimeError("Invalid encryption token — secret cannot be decrypted") from exc
|
||||
return plain.decode("utf-8")
|
||||
+41
-3
@@ -1,22 +1,60 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.config import settings
|
||||
from app.db import AsyncSessionLocal, engine
|
||||
from app.routers import routers
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
await session.execute(text("SELECT 1"))
|
||||
except Exception:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning("Database connectivity check failed on startup")
|
||||
yield
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.app_name,
|
||||
debug=settings.debug,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
allow_origins = ["*"] if settings.debug else []
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173"],
|
||||
allow_origins=allow_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
for router in routers:
|
||||
app.include_router(router, prefix=settings.api_v1_prefix)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict[str, str]:
|
||||
return {"status": "ok", "service": settings.app_name}
|
||||
async def health() -> JSONResponse:
|
||||
db_status = "connected"
|
||||
try:
|
||||
async with AsyncSessionLocal() as session:
|
||||
await session.execute(text("SELECT 1"))
|
||||
except Exception:
|
||||
db_status = "unreachable"
|
||||
|
||||
content = {
|
||||
"status": "ok" if db_status == "connected" else "degraded",
|
||||
"service": settings.app_name,
|
||||
"database": db_status,
|
||||
}
|
||||
status_code = 200 if db_status == "connected" else 503
|
||||
return JSONResponse(status_code=status_code, content=content)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from app.models.access_route import AccessRoute
|
||||
from app.models.base import Base
|
||||
from app.models.config import Config
|
||||
from app.models.project import Project
|
||||
from app.models.repository import Repository
|
||||
from app.models.secret import Secret
|
||||
from app.models.tool_definition import ToolDefinition
|
||||
from app.models.tool_instance import ToolInstance
|
||||
from app.models.user import User
|
||||
from app.models.workspace import Workspace
|
||||
|
||||
__all__ = [
|
||||
"Base",
|
||||
"AccessRoute",
|
||||
"Config",
|
||||
"Project",
|
||||
"Repository",
|
||||
"Secret",
|
||||
"ToolDefinition",
|
||||
"ToolInstance",
|
||||
"User",
|
||||
"Workspace",
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from sqlalchemy import JSON, Boolean, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.tool_instance import ToolInstance
|
||||
|
||||
|
||||
class AccessRoute(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "access_route"
|
||||
|
||||
tool_instance_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("tool_instance.id"), index=True
|
||||
)
|
||||
domain: Mapped[str] = mapped_column(Text)
|
||||
path_prefix: Mapped[str] = mapped_column(
|
||||
String(255), default="/"
|
||||
)
|
||||
provider_type: Mapped[str] = mapped_column(
|
||||
String(50), default="traefik"
|
||||
)
|
||||
provider_config: Mapped[dict[str, Any] | None] = mapped_column(
|
||||
JSON, nullable=True
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True
|
||||
)
|
||||
|
||||
tool_instance: Mapped["ToolInstance"] = relationship(
|
||||
back_populates="access_routes"
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class UUIDMixin:
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
primary_key=True,
|
||||
default=uuid.uuid4,
|
||||
)
|
||||
|
||||
|
||||
class TimestampMixin:
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
server_default=func.now(),
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import JSON, ForeignKey, String, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||
|
||||
|
||||
class Config(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "config"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("scope_type", "scope_id", "tool_definition_id", "key"),
|
||||
)
|
||||
|
||||
scope_type: Mapped[str] = mapped_column(String(50))
|
||||
scope_id: Mapped[uuid.UUID] = mapped_column(index=True)
|
||||
tool_definition_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
ForeignKey("tool_definition.id"), nullable=True, index=True
|
||||
)
|
||||
key: Mapped[str] = mapped_column(String(255))
|
||||
value: Mapped[dict[str, Any]] = mapped_column(JSON)
|
||||
@@ -0,0 +1,36 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.repository import Repository
|
||||
from app.models.tool_instance import ToolInstance
|
||||
from app.models.user import User
|
||||
from app.models.workspace import Workspace
|
||||
|
||||
|
||||
class Project(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "project"
|
||||
__table_args__ = (UniqueConstraint("owner_id", "slug"),)
|
||||
|
||||
owner_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("user.id"), index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255))
|
||||
slug: Mapped[str] = mapped_column(String(255))
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
owner: Mapped["User"] = relationship(back_populates="projects")
|
||||
repositories: Mapped[list["Repository"]] = relationship(
|
||||
back_populates="project"
|
||||
)
|
||||
workspaces: Mapped[list["Workspace"]] = relationship(
|
||||
back_populates="project"
|
||||
)
|
||||
tool_instances: Mapped[list["ToolInstance"]] = relationship(
|
||||
back_populates="project"
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.project import Project
|
||||
|
||||
|
||||
class Repository(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "repository"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("project.id"), index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255))
|
||||
git_url: Mapped[str] = mapped_column(Text)
|
||||
provider_type: Mapped[str] = mapped_column(
|
||||
String(50), default="generic"
|
||||
)
|
||||
default_branch: Mapped[str] = mapped_column(
|
||||
String(100), default="main"
|
||||
)
|
||||
|
||||
project: Mapped["Project"] = relationship(
|
||||
back_populates="repositories"
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||
|
||||
|
||||
class Secret(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "secret"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("scope_type", "scope_id", "key"),
|
||||
)
|
||||
|
||||
scope_type: Mapped[str] = mapped_column(String(50))
|
||||
scope_id: Mapped[uuid.UUID] = mapped_column(index=True)
|
||||
key: Mapped[str] = mapped_column(String(255))
|
||||
encrypted_value: Mapped[str] = mapped_column(Text)
|
||||
@@ -0,0 +1,32 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from sqlalchemy import JSON, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.tool_instance import ToolInstance
|
||||
|
||||
|
||||
class ToolDefinition(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "tool_definition"
|
||||
|
||||
key: Mapped[str] = mapped_column(
|
||||
String(100), unique=True, index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255))
|
||||
version: Mapped[str] = mapped_column(
|
||||
String(50), default="1.0.0"
|
||||
)
|
||||
description: Mapped[str | None] = mapped_column(
|
||||
Text, nullable=True
|
||||
)
|
||||
image: Mapped[str] = mapped_column(Text)
|
||||
manifest_data: Mapped[dict[str, Any] | None] = mapped_column(
|
||||
JSON, nullable=True
|
||||
)
|
||||
|
||||
instances: Mapped[list["ToolInstance"]] = relationship(
|
||||
back_populates="tool_definition"
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from sqlalchemy import JSON, ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.access_route import AccessRoute
|
||||
from app.models.project import Project
|
||||
from app.models.tool_definition import ToolDefinition
|
||||
|
||||
|
||||
class ToolInstance(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "tool_instance"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("project.id"), index=True
|
||||
)
|
||||
tool_definition_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("tool_definition.id"), index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255))
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(50), default="pending"
|
||||
)
|
||||
container_id: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True
|
||||
)
|
||||
subdomain: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True, unique=True
|
||||
)
|
||||
config_override: Mapped[dict[str, Any] | None] = mapped_column(
|
||||
JSON, nullable=True
|
||||
)
|
||||
|
||||
project: Mapped["Project"] = relationship(
|
||||
back_populates="tool_instances"
|
||||
)
|
||||
tool_definition: Mapped["ToolDefinition"] = relationship(
|
||||
back_populates="instances"
|
||||
)
|
||||
access_routes: Mapped[list["AccessRoute"]] = relationship(
|
||||
back_populates="tool_instance"
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.project import Project
|
||||
|
||||
|
||||
class User(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "user"
|
||||
|
||||
authentik_sub: Mapped[str] = mapped_column(
|
||||
String(255), unique=True, index=True
|
||||
)
|
||||
email: Mapped[str] = mapped_column(
|
||||
String(255), unique=True, index=True
|
||||
)
|
||||
display_name: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True
|
||||
)
|
||||
|
||||
projects: Mapped[list["Project"]] = relationship(
|
||||
back_populates="owner"
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.project import Project
|
||||
|
||||
|
||||
class Workspace(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "workspace"
|
||||
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("project.id"), index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255))
|
||||
mount_path: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
project: Mapped["Project"] = relationship(
|
||||
back_populates="workspaces"
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.routers.access_routes import router as access_routes_router
|
||||
from app.routers.configs import router as configs_router
|
||||
from app.routers.projects import router as projects_router
|
||||
from app.routers.repositories import router as repositories_router
|
||||
from app.routers.secrets import router as secrets_router
|
||||
from app.routers.tool_definitions import router as tool_definitions_router
|
||||
from app.routers.tool_instances import router as tool_instances_router
|
||||
from app.routers.users import router as users_router
|
||||
from app.routers.workspaces import router as workspaces_router
|
||||
|
||||
routers: list[APIRouter] = [
|
||||
access_routes_router,
|
||||
configs_router,
|
||||
projects_router,
|
||||
repositories_router,
|
||||
secrets_router,
|
||||
tool_definitions_router,
|
||||
tool_instances_router,
|
||||
users_router,
|
||||
workspaces_router,
|
||||
]
|
||||
|
||||
__all__ = ["routers"]
|
||||
@@ -0,0 +1,103 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.dependencies import get_current_active_user
|
||||
from app.db import get_db_session
|
||||
from app.models.access_route import AccessRoute
|
||||
from app.models.project import Project
|
||||
from app.models.tool_instance import ToolInstance
|
||||
from app.models.user import User
|
||||
from app.schemas.access_route import AccessRouteCreate, AccessRouteRead, AccessRouteUpdate
|
||||
|
||||
router = APIRouter(tags=["access-routes"])
|
||||
|
||||
|
||||
async def _verify_tool_instance_ownership(
|
||||
instance_id: UUID, user: User, session: AsyncSession
|
||||
) -> None:
|
||||
ti = await session.get(ToolInstance, instance_id)
|
||||
if not ti:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tool instance not found")
|
||||
project = await session.get(Project, ti.project_id)
|
||||
if not project or project.owner_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
|
||||
|
||||
@router.post("/tool-instances/{instance_id}/access-routes", response_model=AccessRouteRead, status_code=status.HTTP_201_CREATED) # noqa: E501
|
||||
async def create_access_route(
|
||||
instance_id: UUID,
|
||||
ar_in: AccessRouteCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> AccessRoute:
|
||||
await _verify_tool_instance_ownership(instance_id, current_user, session)
|
||||
ar = AccessRoute(**ar_in.model_dump(), tool_instance_id=instance_id)
|
||||
session.add(ar)
|
||||
await session.commit()
|
||||
await session.refresh(ar)
|
||||
return ar
|
||||
|
||||
|
||||
@router.get("/tool-instances/{instance_id}/access-routes", response_model=list[AccessRouteRead])
|
||||
async def list_access_routes(
|
||||
instance_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[AccessRoute]:
|
||||
await _verify_tool_instance_ownership(instance_id, current_user, session)
|
||||
result = await session.execute(
|
||||
select(AccessRoute).where(AccessRoute.tool_instance_id == instance_id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/tool-instances/{instance_id}/access-routes/{route_id}", response_model=AccessRouteRead) # noqa: E501
|
||||
async def get_access_route(
|
||||
instance_id: UUID,
|
||||
route_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> AccessRoute:
|
||||
await _verify_tool_instance_ownership(instance_id, current_user, session)
|
||||
ar = await session.get(AccessRoute, route_id)
|
||||
if not ar or ar.tool_instance_id != instance_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Access route not found")
|
||||
return ar
|
||||
|
||||
|
||||
@router.put("/tool-instances/{instance_id}/access-routes/{route_id}", response_model=AccessRouteRead) # noqa: E501
|
||||
async def update_access_route(
|
||||
instance_id: UUID,
|
||||
route_id: UUID,
|
||||
ar_in: AccessRouteUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> AccessRoute:
|
||||
await _verify_tool_instance_ownership(instance_id, current_user, session)
|
||||
ar = await session.get(AccessRoute, route_id)
|
||||
if not ar or ar.tool_instance_id != instance_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Access route not found")
|
||||
update_data = ar_in.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(ar, field, value)
|
||||
await session.commit()
|
||||
await session.refresh(ar)
|
||||
return ar
|
||||
|
||||
|
||||
@router.delete("/tool-instances/{instance_id}/access-routes/{route_id}", status_code=status.HTTP_204_NO_CONTENT) # noqa: E501
|
||||
async def delete_access_route(
|
||||
instance_id: UUID,
|
||||
route_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
await _verify_tool_instance_ownership(instance_id, current_user, session)
|
||||
ar = await session.get(AccessRoute, route_id)
|
||||
if not ar or ar.tool_instance_id != instance_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Access route not found")
|
||||
await session.delete(ar)
|
||||
await session.commit()
|
||||
@@ -0,0 +1,122 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.dependencies import get_current_active_user
|
||||
from app.db import get_db_session
|
||||
from app.models.config import Config
|
||||
from app.models.project import Project
|
||||
from app.models.tool_instance import ToolInstance
|
||||
from app.models.user import User
|
||||
from app.schemas.config import ConfigCreate, ConfigRead, ConfigUpdate
|
||||
|
||||
router = APIRouter(tags=["configs"])
|
||||
|
||||
|
||||
async def _verify_config_ownership(
|
||||
config_obj: Config, user: User, session: AsyncSession
|
||||
) -> None:
|
||||
if config_obj.scope_type == "user":
|
||||
if config_obj.scope_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
elif config_obj.scope_type == "project":
|
||||
project = await session.get(Project, config_obj.scope_id)
|
||||
if not project or project.owner_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
elif config_obj.scope_type == "tool_instance":
|
||||
ti = await session.get(ToolInstance, config_obj.scope_id)
|
||||
if not ti:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
project = await session.get(Project, ti.project_id)
|
||||
if not project or project.owner_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
elif config_obj.scope_type == "global":
|
||||
pass
|
||||
else:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid scope_type")
|
||||
|
||||
|
||||
@router.post("/configs", response_model=ConfigRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_config(
|
||||
config_in: ConfigCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Config:
|
||||
cfg = Config(**config_in.model_dump())
|
||||
await _verify_config_ownership(cfg, current_user, session)
|
||||
session.add(cfg)
|
||||
await session.commit()
|
||||
await session.refresh(cfg)
|
||||
return cfg
|
||||
|
||||
|
||||
@router.get("/configs", response_model=list[ConfigRead])
|
||||
async def list_configs(
|
||||
scope_type: str | None = None,
|
||||
scope_id: UUID | None = None,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[Config]:
|
||||
stmt = select(Config)
|
||||
if scope_type:
|
||||
stmt = stmt.where(Config.scope_type == scope_type)
|
||||
if scope_id:
|
||||
stmt = stmt.where(Config.scope_id == scope_id)
|
||||
result = await session.execute(stmt)
|
||||
configs = list(result.scalars().all())
|
||||
allowed = []
|
||||
for cfg in configs:
|
||||
try:
|
||||
await _verify_config_ownership(cfg, current_user, session)
|
||||
allowed.append(cfg)
|
||||
except HTTPException:
|
||||
pass
|
||||
return allowed
|
||||
|
||||
|
||||
@router.get("/configs/{config_id}", response_model=ConfigRead)
|
||||
async def get_config(
|
||||
config_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Config:
|
||||
cfg = await session.get(Config, config_id)
|
||||
if not cfg:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Config not found")
|
||||
await _verify_config_ownership(cfg, current_user, session)
|
||||
return cfg
|
||||
|
||||
|
||||
@router.put("/configs/{config_id}", response_model=ConfigRead)
|
||||
async def update_config(
|
||||
config_id: UUID,
|
||||
config_in: ConfigUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Config:
|
||||
cfg = await session.get(Config, config_id)
|
||||
if not cfg:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Config not found")
|
||||
await _verify_config_ownership(cfg, current_user, session)
|
||||
update_data = config_in.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(cfg, field, value)
|
||||
await session.commit()
|
||||
await session.refresh(cfg)
|
||||
return cfg
|
||||
|
||||
|
||||
@router.delete("/configs/{config_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_config(
|
||||
config_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
cfg = await session.get(Config, config_id)
|
||||
if not cfg:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Config not found")
|
||||
await _verify_config_ownership(cfg, current_user, session)
|
||||
await session.delete(cfg)
|
||||
await session.commit()
|
||||
@@ -0,0 +1,80 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.dependencies import get_current_active_user
|
||||
from app.db import get_db_session
|
||||
from app.models.project import Project
|
||||
from app.models.user import User
|
||||
from app.schemas.project import ProjectCreate, ProjectRead, ProjectUpdate
|
||||
|
||||
router = APIRouter(tags=["projects"])
|
||||
|
||||
|
||||
@router.post("/projects", response_model=ProjectRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_project(
|
||||
project_in: ProjectCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Project:
|
||||
project = Project(**project_in.model_dump(), owner_id=current_user.id)
|
||||
session.add(project)
|
||||
await session.commit()
|
||||
await session.refresh(project)
|
||||
return project
|
||||
|
||||
|
||||
@router.get("/projects", response_model=list[ProjectRead])
|
||||
async def list_projects(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[Project]:
|
||||
result = await session.execute(
|
||||
select(Project).where(Project.owner_id == current_user.id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}", response_model=ProjectRead)
|
||||
async def get_project(
|
||||
project_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Project:
|
||||
project = await session.get(Project, project_id)
|
||||
if not project or project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
return project
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}", response_model=ProjectRead)
|
||||
async def update_project(
|
||||
project_id: UUID,
|
||||
project_in: ProjectUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Project:
|
||||
project = await session.get(Project, project_id)
|
||||
if not project or project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
update_data = project_in.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(project, field, value)
|
||||
await session.commit()
|
||||
await session.refresh(project)
|
||||
return project
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_project(
|
||||
project_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
project = await session.get(Project, project_id)
|
||||
if not project or project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
await session.delete(project)
|
||||
await session.commit()
|
||||
@@ -0,0 +1,100 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.dependencies import get_current_active_user
|
||||
from app.db import get_db_session
|
||||
from app.models.project import Project
|
||||
from app.models.repository import Repository
|
||||
from app.models.user import User
|
||||
from app.schemas.repository import RepositoryCreate, RepositoryRead, RepositoryUpdate
|
||||
|
||||
router = APIRouter(tags=["repositories"])
|
||||
|
||||
|
||||
async def _get_project_for_user(
|
||||
project_id: UUID, user: User, session: AsyncSession
|
||||
) -> Project:
|
||||
project = await session.get(Project, project_id)
|
||||
if not project or project.owner_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
return project
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/repositories", response_model=RepositoryRead, status_code=status.HTTP_201_CREATED) # noqa: E501
|
||||
async def create_repository(
|
||||
project_id: UUID,
|
||||
repo_in: RepositoryCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Repository:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
repo = Repository(**repo_in.model_dump(), project_id=project_id)
|
||||
session.add(repo)
|
||||
await session.commit()
|
||||
await session.refresh(repo)
|
||||
return repo
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/repositories", response_model=list[RepositoryRead])
|
||||
async def list_repositories(
|
||||
project_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[Repository]:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
result = await session.execute(
|
||||
select(Repository).where(Repository.project_id == project_id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/repositories/{repo_id}", response_model=RepositoryRead)
|
||||
async def get_repository(
|
||||
project_id: UUID,
|
||||
repo_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Repository:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
repo = await session.get(Repository, repo_id)
|
||||
if not repo or repo.project_id != project_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Repository not found")
|
||||
return repo
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}/repositories/{repo_id}", response_model=RepositoryRead)
|
||||
async def update_repository(
|
||||
project_id: UUID,
|
||||
repo_id: UUID,
|
||||
repo_in: RepositoryUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Repository:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
repo = await session.get(Repository, repo_id)
|
||||
if not repo or repo.project_id != project_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Repository not found")
|
||||
update_data = repo_in.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(repo, field, value)
|
||||
await session.commit()
|
||||
await session.refresh(repo)
|
||||
return repo
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}/repositories/{repo_id}", status_code=status.HTTP_204_NO_CONTENT) # noqa: E501
|
||||
async def delete_repository(
|
||||
project_id: UUID,
|
||||
repo_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
repo = await session.get(Repository, repo_id)
|
||||
if not repo or repo.project_id != project_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Repository not found")
|
||||
await session.delete(repo)
|
||||
await session.commit()
|
||||
@@ -0,0 +1,153 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.dependencies import get_current_active_user
|
||||
from app.db import get_db_session
|
||||
from app.encryption import decrypt_value, encrypt_value
|
||||
from app.models.project import Project
|
||||
from app.models.secret import Secret
|
||||
from app.models.tool_instance import ToolInstance
|
||||
from app.models.user import User
|
||||
from app.schemas.secret import SecretCreate, SecretRead, SecretUpdate
|
||||
|
||||
router = APIRouter(tags=["secrets"])
|
||||
|
||||
|
||||
async def _verify_secret_ownership(
|
||||
secret_obj: Secret, user: User, session: AsyncSession
|
||||
) -> None:
|
||||
if secret_obj.scope_type == "user":
|
||||
if secret_obj.scope_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
elif secret_obj.scope_type == "project":
|
||||
project = await session.get(Project, secret_obj.scope_id)
|
||||
if not project or project.owner_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
elif secret_obj.scope_type == "tool_instance":
|
||||
ti = await session.get(ToolInstance, secret_obj.scope_id)
|
||||
if not ti:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
project = await session.get(Project, ti.project_id)
|
||||
if not project or project.owner_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
elif secret_obj.scope_type == "global":
|
||||
pass
|
||||
else:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid scope_type")
|
||||
|
||||
|
||||
@router.post("/secrets", response_model=SecretRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_secret(
|
||||
secret_in: SecretCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> SecretRead:
|
||||
secret = Secret(
|
||||
scope_type=secret_in.scope_type,
|
||||
scope_id=secret_in.scope_id,
|
||||
key=secret_in.key,
|
||||
encrypted_value=encrypt_value(secret_in.value),
|
||||
)
|
||||
await _verify_secret_ownership(secret, current_user, session)
|
||||
session.add(secret)
|
||||
await session.commit()
|
||||
await session.refresh(secret)
|
||||
return SecretRead(
|
||||
id=secret.id,
|
||||
scope_type=secret.scope_type,
|
||||
scope_id=secret.scope_id,
|
||||
key=secret.key,
|
||||
value=decrypt_value(secret.encrypted_value),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/secrets", response_model=list[SecretRead])
|
||||
async def list_secrets(
|
||||
scope_type: str | None = None,
|
||||
scope_id: UUID | None = None,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[SecretRead]:
|
||||
stmt = select(Secret)
|
||||
if scope_type:
|
||||
stmt = stmt.where(Secret.scope_type == scope_type)
|
||||
if scope_id:
|
||||
stmt = stmt.where(Secret.scope_id == scope_id)
|
||||
result = await session.execute(stmt)
|
||||
secrets = list(result.scalars().all())
|
||||
allowed = []
|
||||
for s in secrets:
|
||||
try:
|
||||
await _verify_secret_ownership(s, current_user, session)
|
||||
allowed.append(SecretRead(
|
||||
id=s.id,
|
||||
scope_type=s.scope_type,
|
||||
scope_id=s.scope_id,
|
||||
key=s.key,
|
||||
value=decrypt_value(s.encrypted_value),
|
||||
))
|
||||
except HTTPException:
|
||||
pass
|
||||
return allowed
|
||||
|
||||
|
||||
@router.get("/secrets/{secret_id}", response_model=SecretRead)
|
||||
async def get_secret(
|
||||
secret_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> SecretRead:
|
||||
s = await session.get(Secret, secret_id)
|
||||
if not s:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Secret not found")
|
||||
await _verify_secret_ownership(s, current_user, session)
|
||||
return SecretRead(
|
||||
id=s.id,
|
||||
scope_type=s.scope_type,
|
||||
scope_id=s.scope_id,
|
||||
key=s.key,
|
||||
value=decrypt_value(s.encrypted_value),
|
||||
)
|
||||
|
||||
|
||||
@router.put("/secrets/{secret_id}", response_model=SecretRead)
|
||||
async def update_secret(
|
||||
secret_id: UUID,
|
||||
secret_in: SecretUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> SecretRead:
|
||||
s = await session.get(Secret, secret_id)
|
||||
if not s:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Secret not found")
|
||||
await _verify_secret_ownership(s, current_user, session)
|
||||
if secret_in.key is not None:
|
||||
s.key = secret_in.key
|
||||
if secret_in.value is not None:
|
||||
s.encrypted_value = encrypt_value(secret_in.value)
|
||||
await session.commit()
|
||||
await session.refresh(s)
|
||||
return SecretRead(
|
||||
id=s.id,
|
||||
scope_type=s.scope_type,
|
||||
scope_id=s.scope_id,
|
||||
key=s.key,
|
||||
value=decrypt_value(s.encrypted_value),
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/secrets/{secret_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_secret(
|
||||
secret_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
s = await session.get(Secret, secret_id)
|
||||
if not s:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Secret not found")
|
||||
await _verify_secret_ownership(s, current_user, session)
|
||||
await session.delete(s)
|
||||
await session.commit()
|
||||
@@ -0,0 +1,88 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.dependencies import get_current_active_user
|
||||
from app.db import get_db_session
|
||||
from app.models.tool_definition import ToolDefinition
|
||||
from app.models.user import User
|
||||
from app.schemas.tool_definition import (
|
||||
ToolDefinitionCreate,
|
||||
ToolDefinitionRead,
|
||||
ToolDefinitionUpdate,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["tool-definitions"])
|
||||
|
||||
|
||||
@router.post("/tool-definitions", response_model=ToolDefinitionRead, status_code=status.HTTP_201_CREATED) # noqa: E501
|
||||
async def create_tool_definition(
|
||||
td_in: ToolDefinitionCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> ToolDefinition:
|
||||
td = ToolDefinition(**td_in.model_dump())
|
||||
session.add(td)
|
||||
await session.commit()
|
||||
await session.refresh(td)
|
||||
return td
|
||||
|
||||
|
||||
@router.get("/tool-definitions", response_model=list[ToolDefinitionRead])
|
||||
async def list_tool_definitions(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[ToolDefinition]:
|
||||
result = await session.execute(select(ToolDefinition))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/tool-definitions/{tool_def_id}", response_model=ToolDefinitionRead)
|
||||
async def get_tool_definition(
|
||||
tool_def_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> ToolDefinition:
|
||||
td = await session.get(ToolDefinition, tool_def_id)
|
||||
if not td:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Tool definition not found"
|
||||
)
|
||||
return td
|
||||
|
||||
|
||||
@router.put("/tool-definitions/{tool_def_id}", response_model=ToolDefinitionRead)
|
||||
async def update_tool_definition(
|
||||
tool_def_id: UUID,
|
||||
td_in: ToolDefinitionUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> ToolDefinition:
|
||||
td = await session.get(ToolDefinition, tool_def_id)
|
||||
if not td:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Tool definition not found"
|
||||
)
|
||||
update_data = td_in.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(td, field, value)
|
||||
await session.commit()
|
||||
await session.refresh(td)
|
||||
return td
|
||||
|
||||
|
||||
@router.delete("/tool-definitions/{tool_def_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_tool_definition(
|
||||
tool_def_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
td = await session.get(ToolDefinition, tool_def_id)
|
||||
if not td:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Tool definition not found"
|
||||
)
|
||||
await session.delete(td)
|
||||
await session.commit()
|
||||
@@ -0,0 +1,100 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.dependencies import get_current_active_user
|
||||
from app.db import get_db_session
|
||||
from app.models.project import Project
|
||||
from app.models.tool_instance import ToolInstance
|
||||
from app.models.user import User
|
||||
from app.schemas.tool_instance import ToolInstanceCreate, ToolInstanceRead, ToolInstanceUpdate
|
||||
|
||||
router = APIRouter(tags=["tool-instances"])
|
||||
|
||||
|
||||
async def _get_project_for_user(
|
||||
project_id: UUID, user: User, session: AsyncSession
|
||||
) -> Project:
|
||||
project = await session.get(Project, project_id)
|
||||
if not project or project.owner_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
return project
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/tool-instances", response_model=ToolInstanceRead, status_code=status.HTTP_201_CREATED) # noqa: E501
|
||||
async def create_tool_instance(
|
||||
project_id: UUID,
|
||||
ti_in: ToolInstanceCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> ToolInstance:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
ti = ToolInstance(**ti_in.model_dump(), project_id=project_id)
|
||||
session.add(ti)
|
||||
await session.commit()
|
||||
await session.refresh(ti)
|
||||
return ti
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/tool-instances", response_model=list[ToolInstanceRead])
|
||||
async def list_tool_instances(
|
||||
project_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[ToolInstance]:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
result = await session.execute(
|
||||
select(ToolInstance).where(ToolInstance.project_id == project_id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/tool-instances/{instance_id}", response_model=ToolInstanceRead)
|
||||
async def get_tool_instance(
|
||||
project_id: UUID,
|
||||
instance_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> ToolInstance:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
ti = await session.get(ToolInstance, instance_id)
|
||||
if not ti or ti.project_id != project_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tool instance not found")
|
||||
return ti
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}/tool-instances/{instance_id}", response_model=ToolInstanceRead)
|
||||
async def update_tool_instance(
|
||||
project_id: UUID,
|
||||
instance_id: UUID,
|
||||
ti_in: ToolInstanceUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> ToolInstance:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
ti = await session.get(ToolInstance, instance_id)
|
||||
if not ti or ti.project_id != project_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tool instance not found")
|
||||
update_data = ti_in.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(ti, field, value)
|
||||
await session.commit()
|
||||
await session.refresh(ti)
|
||||
return ti
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}/tool-instances/{instance_id}", status_code=status.HTTP_204_NO_CONTENT) # noqa: E501
|
||||
async def delete_tool_instance(
|
||||
project_id: UUID,
|
||||
instance_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
ti = await session.get(ToolInstance, instance_id)
|
||||
if not ti or ti.project_id != project_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tool instance not found")
|
||||
await session.delete(ti)
|
||||
await session.commit()
|
||||
@@ -0,0 +1,17 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.auth.dependencies import get_current_active_user
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserRead
|
||||
|
||||
router = APIRouter(tags=["users"])
|
||||
|
||||
|
||||
@router.get("/users/me", response_model=UserRead)
|
||||
async def read_current_user(current_user: User = Depends(get_current_active_user)) -> User:
|
||||
return current_user
|
||||
|
||||
|
||||
@router.get("/users", response_model=list[UserRead])
|
||||
async def list_users(current_user: User = Depends(get_current_active_user)) -> list[User]:
|
||||
return [current_user]
|
||||
@@ -0,0 +1,100 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.dependencies import get_current_active_user
|
||||
from app.db import get_db_session
|
||||
from app.models.project import Project
|
||||
from app.models.user import User
|
||||
from app.models.workspace import Workspace
|
||||
from app.schemas.workspace import WorkspaceCreate, WorkspaceRead, WorkspaceUpdate
|
||||
|
||||
router = APIRouter(tags=["workspaces"])
|
||||
|
||||
|
||||
async def _get_project_for_user(
|
||||
project_id: UUID, user: User, session: AsyncSession
|
||||
) -> Project:
|
||||
project = await session.get(Project, project_id)
|
||||
if not project or project.owner_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
return project
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/workspaces", response_model=WorkspaceRead, status_code=status.HTTP_201_CREATED) # noqa: E501
|
||||
async def create_workspace(
|
||||
project_id: UUID,
|
||||
ws_in: WorkspaceCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Workspace:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
ws = Workspace(**ws_in.model_dump(), project_id=project_id)
|
||||
session.add(ws)
|
||||
await session.commit()
|
||||
await session.refresh(ws)
|
||||
return ws
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/workspaces", response_model=list[WorkspaceRead])
|
||||
async def list_workspaces(
|
||||
project_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[Workspace]:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
result = await session.execute(
|
||||
select(Workspace).where(Workspace.project_id == project_id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/workspaces/{ws_id}", response_model=WorkspaceRead)
|
||||
async def get_workspace(
|
||||
project_id: UUID,
|
||||
ws_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Workspace:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
ws = await session.get(Workspace, ws_id)
|
||||
if not ws or ws.project_id != project_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found")
|
||||
return ws
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}/workspaces/{ws_id}", response_model=WorkspaceRead)
|
||||
async def update_workspace(
|
||||
project_id: UUID,
|
||||
ws_id: UUID,
|
||||
ws_in: WorkspaceUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Workspace:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
ws = await session.get(Workspace, ws_id)
|
||||
if not ws or ws.project_id != project_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found")
|
||||
update_data = ws_in.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(ws, field, value)
|
||||
await session.commit()
|
||||
await session.refresh(ws)
|
||||
return ws
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}/workspaces/{ws_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_workspace(
|
||||
project_id: UUID,
|
||||
ws_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
await _get_project_for_user(project_id, current_user, session)
|
||||
ws = await session.get(Workspace, ws_id)
|
||||
if not ws or ws.project_id != project_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found")
|
||||
await session.delete(ws)
|
||||
await session.commit()
|
||||
@@ -0,0 +1,42 @@
|
||||
from app.schemas.access_route import AccessRouteCreate, AccessRouteRead, AccessRouteUpdate
|
||||
from app.schemas.config import ConfigCreate, ConfigRead, ConfigUpdate
|
||||
from app.schemas.project import ProjectCreate, ProjectRead, ProjectUpdate
|
||||
from app.schemas.repository import RepositoryCreate, RepositoryRead, RepositoryUpdate
|
||||
from app.schemas.secret import SecretCreate, SecretRead, SecretUpdate
|
||||
from app.schemas.tool_definition import (
|
||||
ToolDefinitionCreate,
|
||||
ToolDefinitionRead,
|
||||
ToolDefinitionUpdate,
|
||||
)
|
||||
from app.schemas.tool_instance import ToolInstanceCreate, ToolInstanceRead, ToolInstanceUpdate
|
||||
from app.schemas.user import UserCreate, UserRead
|
||||
from app.schemas.workspace import WorkspaceCreate, WorkspaceRead, WorkspaceUpdate
|
||||
|
||||
__all__ = [
|
||||
"AccessRouteCreate",
|
||||
"AccessRouteRead",
|
||||
"AccessRouteUpdate",
|
||||
"ConfigCreate",
|
||||
"ConfigRead",
|
||||
"ConfigUpdate",
|
||||
"ProjectCreate",
|
||||
"ProjectRead",
|
||||
"ProjectUpdate",
|
||||
"RepositoryCreate",
|
||||
"RepositoryRead",
|
||||
"RepositoryUpdate",
|
||||
"SecretCreate",
|
||||
"SecretRead",
|
||||
"SecretUpdate",
|
||||
"ToolDefinitionCreate",
|
||||
"ToolDefinitionRead",
|
||||
"ToolDefinitionUpdate",
|
||||
"ToolInstanceCreate",
|
||||
"ToolInstanceRead",
|
||||
"ToolInstanceUpdate",
|
||||
"UserCreate",
|
||||
"UserRead",
|
||||
"WorkspaceCreate",
|
||||
"WorkspaceRead",
|
||||
"WorkspaceUpdate",
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.base import OrmBase
|
||||
|
||||
|
||||
class AccessRouteBase(OrmBase):
|
||||
domain: str
|
||||
path_prefix: str = "/"
|
||||
provider_type: str = "traefik"
|
||||
provider_config: dict[str, Any] | None = None
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class AccessRouteCreate(AccessRouteBase):
|
||||
pass
|
||||
|
||||
|
||||
class AccessRouteRead(AccessRouteBase):
|
||||
id: UUID
|
||||
tool_instance_id: UUID
|
||||
|
||||
|
||||
class AccessRouteUpdate(OrmBase):
|
||||
domain: str | None = None
|
||||
path_prefix: str | None = None
|
||||
provider_type: str | None = None
|
||||
provider_config: dict[str, Any] | None = None
|
||||
is_active: bool | None = None
|
||||
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class OrmBase(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,25 @@
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.base import OrmBase
|
||||
|
||||
|
||||
class ConfigBase(OrmBase):
|
||||
scope_type: str
|
||||
scope_id: UUID
|
||||
tool_definition_id: UUID | None = None
|
||||
key: str
|
||||
value: dict[str, Any]
|
||||
|
||||
|
||||
class ConfigCreate(ConfigBase):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigRead(ConfigBase):
|
||||
id: UUID
|
||||
|
||||
|
||||
class ConfigUpdate(OrmBase):
|
||||
key: str | None = None
|
||||
value: dict[str, Any] | None = None
|
||||
@@ -0,0 +1,24 @@
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.base import OrmBase
|
||||
|
||||
|
||||
class ProjectBase(OrmBase):
|
||||
name: str
|
||||
slug: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class ProjectCreate(ProjectBase):
|
||||
pass
|
||||
|
||||
|
||||
class ProjectRead(ProjectBase):
|
||||
id: UUID
|
||||
owner_id: UUID
|
||||
|
||||
|
||||
class ProjectUpdate(OrmBase):
|
||||
name: str | None = None
|
||||
slug: str | None = None
|
||||
description: str | None = None
|
||||
@@ -0,0 +1,26 @@
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.base import OrmBase
|
||||
|
||||
|
||||
class RepositoryBase(OrmBase):
|
||||
name: str
|
||||
git_url: str
|
||||
provider_type: str = "generic"
|
||||
default_branch: str = "main"
|
||||
|
||||
|
||||
class RepositoryCreate(RepositoryBase):
|
||||
pass
|
||||
|
||||
|
||||
class RepositoryRead(RepositoryBase):
|
||||
id: UUID
|
||||
project_id: UUID
|
||||
|
||||
|
||||
class RepositoryUpdate(OrmBase):
|
||||
name: str | None = None
|
||||
git_url: str | None = None
|
||||
provider_type: str | None = None
|
||||
default_branch: str | None = None
|
||||
@@ -0,0 +1,23 @@
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.base import OrmBase
|
||||
|
||||
|
||||
class SecretBase(OrmBase):
|
||||
scope_type: str
|
||||
scope_id: UUID
|
||||
key: str
|
||||
|
||||
|
||||
class SecretCreate(SecretBase):
|
||||
value: str
|
||||
|
||||
|
||||
class SecretRead(SecretBase):
|
||||
id: UUID
|
||||
value: str
|
||||
|
||||
|
||||
class SecretUpdate(OrmBase):
|
||||
key: str | None = None
|
||||
value: str | None = None
|
||||
@@ -0,0 +1,29 @@
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.base import OrmBase
|
||||
|
||||
|
||||
class ToolDefinitionBase(OrmBase):
|
||||
key: str
|
||||
name: str
|
||||
version: str = "1.0.0"
|
||||
description: str | None = None
|
||||
image: str
|
||||
manifest_data: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ToolDefinitionCreate(ToolDefinitionBase):
|
||||
pass
|
||||
|
||||
|
||||
class ToolDefinitionRead(ToolDefinitionBase):
|
||||
id: UUID
|
||||
|
||||
|
||||
class ToolDefinitionUpdate(OrmBase):
|
||||
name: str | None = None
|
||||
version: str | None = None
|
||||
description: str | None = None
|
||||
image: str | None = None
|
||||
manifest_data: dict[str, Any] | None = None
|
||||
@@ -0,0 +1,30 @@
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.base import OrmBase
|
||||
|
||||
|
||||
class ToolInstanceBase(OrmBase):
|
||||
name: str
|
||||
status: str = "pending"
|
||||
container_id: str | None = None
|
||||
subdomain: str | None = None
|
||||
config_override: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ToolInstanceCreate(ToolInstanceBase):
|
||||
tool_definition_id: UUID
|
||||
|
||||
|
||||
class ToolInstanceRead(ToolInstanceBase):
|
||||
id: UUID
|
||||
project_id: UUID
|
||||
tool_definition_id: UUID
|
||||
|
||||
|
||||
class ToolInstanceUpdate(OrmBase):
|
||||
name: str | None = None
|
||||
status: str | None = None
|
||||
container_id: str | None = None
|
||||
subdomain: str | None = None
|
||||
config_override: dict[str, Any] | None = None
|
||||
@@ -0,0 +1,21 @@
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.base import OrmBase
|
||||
|
||||
|
||||
class UserBase(OrmBase):
|
||||
authentik_sub: str
|
||||
email: str
|
||||
display_name: str | None = None
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
pass
|
||||
|
||||
|
||||
class UserRead(UserBase):
|
||||
id: UUID
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
@@ -0,0 +1,22 @@
|
||||
from uuid import UUID
|
||||
|
||||
from app.schemas.base import OrmBase
|
||||
|
||||
|
||||
class WorkspaceBase(OrmBase):
|
||||
name: str
|
||||
mount_path: str | None = None
|
||||
|
||||
|
||||
class WorkspaceCreate(WorkspaceBase):
|
||||
pass
|
||||
|
||||
|
||||
class WorkspaceRead(WorkspaceBase):
|
||||
id: UUID
|
||||
project_id: UUID
|
||||
|
||||
|
||||
class WorkspaceUpdate(OrmBase):
|
||||
name: str | None = None
|
||||
mount_path: str | None = None
|
||||
Reference in New Issue
Block a user