Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/app/api/router.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import APIRouter
from .v1.auth import router as auth_router
from .v1.health import router as health_router
from .v1.integrations import router as integrations_router

api_router = APIRouter()

Expand All @@ -16,4 +17,10 @@
tags=["Health"]
)

api_router.include_router(
integrations_router,
prefix="/v1/integrations",
tags=["Integrations"]
)

__all__ = ["api_router"]
105 changes: 105 additions & 0 deletions backend/app/api/v1/integrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from fastapi import APIRouter, HTTPException, Depends, status
from uuid import UUID
from app.models.integration import (
IntegrationCreateRequest,
IntegrationUpdateRequest,
IntegrationResponse,
IntegrationListResponse,
IntegrationStatusResponse
)
from app.services.integration_service import integration_service, NotFoundError
from app.core.dependencies import get_current_user

router = APIRouter()


@router.post("/", response_model=IntegrationResponse, status_code=status.HTTP_201_CREATED)
async def create_integration(
request: IntegrationCreateRequest,
user_id: UUID = Depends(get_current_user)
):
"""Create a new organization integration."""
try:
return await integration_service.create_integration(user_id, request)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e


@router.get("/", response_model=IntegrationListResponse)
async def list_integrations(user_id: UUID = Depends(get_current_user)):
"""List all integrations for the current user."""
try:
integrations = await integration_service.get_integrations(user_id)
return IntegrationListResponse(integrations=integrations, total=len(integrations))
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e


@router.get("/status/{platform}", response_model=IntegrationStatusResponse)
async def get_integration_status(
platform: str,
user_id: UUID = Depends(get_current_user)
):
"""Get the status of a specific platform integration."""
try:
return await integration_service.get_integration_status(user_id, platform)
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e

@router.get("/{integration_id}", response_model=IntegrationResponse)
async def get_integration(
integration_id: UUID,
user_id: UUID = Depends(get_current_user)
):
"""Get a specific integration."""
try:
integration = await integration_service.get_integration(user_id, integration_id)

if not integration:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Integration not found"
)

return integration
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e

@router.put("/{integration_id}", response_model=IntegrationResponse)
async def update_integration(
integration_id: UUID,
request: IntegrationUpdateRequest,
user_id: UUID = Depends(get_current_user)
):
"""Update an existing integration."""
try:
return await integration_service.update_integration(user_id, integration_id, request)
except NotFoundError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update integration: {str(e)}"
) from e

@router.delete("/{integration_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_integration(
integration_id: UUID,
user_id: UUID = Depends(get_current_user)
):
"""Delete an integration."""
try:
await integration_service.delete_integration(user_id, integration_id)
except NotFoundError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete integration: {str(e)}"
) from e
67 changes: 62 additions & 5 deletions backend/app/core/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,69 @@
from fastapi import Request
from fastapi import Header, HTTPException, status, Request
from uuid import UUID
from app.database.supabase.client import get_supabase_client
import logging
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from main import DevRAIApplication

async def get_app_instance(request: Request) -> "DevRAIApplication":
logger = logging.getLogger(__name__)


def get_app_instance(request: Request) -> "DevRAIApplication":
"""Get the application instance from FastAPI app state."""
return request.app.state.app_instance


async def get_current_user(authorization: str = Header(None)) -> UUID:
"""
Dependency to get the application instance from FastAPI's state.
This avoids circular imports by using dependency injection.
Get the current authenticated user from the Supabase JWT token.

Args:
authorization: The Authorization header containing the Bearer token

Returns:
UUID: The user's ID

Raises:
HTTPException: If authentication fails
"""
return request.app.state.app_instance
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing authorization header",
headers={"WWW-Authenticate": "Bearer"},
)

if not authorization.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authorization header format. Expected 'Bearer <token>'",
headers={"WWW-Authenticate": "Bearer"},
)

token = authorization.replace("Bearer ", "")

try:
supabase = get_supabase_client()
# Verify the token and get user
user_response = supabase.auth.get_user(token)

if not user_response or not user_response.user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)

return UUID(user_response.user.id)

except HTTPException:
raise
except Exception as e:
logger.exception("Authentication error")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication failed",
headers={"WWW-Authenticate": "Bearer"},
) from e
23 changes: 23 additions & 0 deletions backend/app/models/database/supabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,29 @@ class ConversationContext(BaseModel):

created_at: datetime = Field(default_factory=datetime.now)

class OrganizationIntegration(BaseModel):
"""
Represents a registered organization (just metadata, no credentials).

Attributes:
id (UUID): Unique identifier for the integration.
user_id (UUID): User/Owner who registered this organization.
platform (str): Platform name (github, discord, slack, discourse).
organization_name (str): Name of the organization.
is_active (bool): Whether the integration is active.
created_at (datetime): Timestamp when registered.
updated_at (datetime): Timestamp when last updated.
config (dict): Platform-specific data (org link, guild_id, etc.).
"""
id: UUID
user_id: UUID
platform: str
organization_name: str
is_active: bool = True
created_at: datetime
updated_at: datetime
config: Optional[dict] = None

class IndexedRepository(BaseModel):
"""Model for FalkorDB indexed repositories"""
id: UUID
Expand Down
49 changes: 49 additions & 0 deletions backend/app/models/integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from pydantic import BaseModel, Field
from typing import Optional, Literal
from datetime import datetime
from uuid import UUID


# Request Models
class IntegrationCreateRequest(BaseModel):
"""Request model for registering an organization."""
platform: Literal["github", "discord", "slack", "discourse"]
organization_name: str
organization_link: Optional[str] = None # GitHub org URL, Discord server ID, etc.
config: Optional[dict] = None # Platform-specific data (discord_guild_id, etc.)


class IntegrationUpdateRequest(BaseModel):
"""Request model for updating an integration."""
organization_name: Optional[str] = None
organization_link: Optional[str] = None
is_active: Optional[bool] = None
config: Optional[dict] = None


# Response Models
class IntegrationResponse(BaseModel):
"""Response model for integration data."""
id: UUID
user_id: UUID
platform: str
organization_name: str
is_active: bool
created_at: datetime
updated_at: datetime
config: Optional[dict] = None
# Note: We never return the actual token in responses


class IntegrationListResponse(BaseModel):
"""Response model for listing integrations."""
integrations: list[IntegrationResponse]
total: int


class IntegrationStatusResponse(BaseModel):
"""Response model for checking integration status."""
platform: str
is_connected: bool
organization_name: Optional[str] = None
last_updated: Optional[datetime] = None
Loading