Skip to content
Open
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
143 changes: 143 additions & 0 deletions servers/fastapi/api/document_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from fastapi import APIRouter, File, UploadFile, HTTPException, BackgroundTasks
from fastapi.responses import FileResponse
from typing import List
import os
import uuid
import mimetypes
from datetime import datetime

router = APIRouter(prefix="/api/v1/documents", tags=["documents"])

# Ensure upload directory exists
UPLOAD_DIRECTORY = "/app/uploads"
os.makedirs(UPLOAD_DIRECTORY, exist_ok=True)

@router.get("/list")
async def list_documents():
"""
List all uploaded documents
"""
documents = []
try:
for filename in os.listdir(UPLOAD_DIRECTORY):
file_path = os.path.join(UPLOAD_DIRECTORY, filename)
if os.path.isfile(file_path):
# Extract metadata from the filename
parts = filename.split('__')
original_filename = parts[0] if len(parts) > 1 else filename
file_id = parts[-1].split('.')[0] if len(parts) > 1 else filename.split('.')[0]

documents.append({
"id": file_id,
"filename": original_filename,
"size": os.path.getsize(file_path),
"upload_date": str(datetime.fromtimestamp(os.path.getctime(file_path))),
"content_type": mimetypes.guess_type(filename)[0] or 'application/octet-stream'
})
return documents
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to list documents: {str(e)}")

@router.post("/upload")
async def upload_document(file: UploadFile = File(...)):
"""
Upload a new document
"""
try:
# Generate unique filename
file_id = str(uuid.uuid4())
file_extension = file.filename.split('.')[-1] if '.' in file.filename else ''

# Create a filename that preserves the original name and includes a unique ID
unique_filename = f"{file.filename}__{file_id}.{file_extension}"
file_path = os.path.join(UPLOAD_DIRECTORY, unique_filename)

# Save the file
with open(file_path, "wb") as buffer:
buffer.write(await file.read())

# Create metadata
return {
"id": file_id,
"filename": file.filename,
"size": os.path.getsize(file_path),
"upload_date": str(datetime.now()),
"content_type": file.content_type or 'application/octet-stream'
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")

@router.delete("/{document_id}")
async def delete_document(document_id: str):
"""
Delete a specific document
"""
try:
for filename in os.listdir(UPLOAD_DIRECTORY):
# Check if the filename contains the document_id
if document_id in filename:
file_path = os.path.join(UPLOAD_DIRECTORY, filename)
os.remove(file_path)
return {"success": True, "message": "Document deleted successfully"}

raise HTTPException(status_code=404, detail="Document not found")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Delete failed: {str(e)}")

@router.get("/preview/{document_id}")
async def preview_document(document_id: str):
"""
Preview a specific document
"""
try:
for filename in os.listdir(UPLOAD_DIRECTORY):
# Check if the filename contains the document_id
if document_id in filename:
file_path = os.path.join(UPLOAD_DIRECTORY, filename)

# Extract original filename
original_filename = filename.split('__')[0]

# Determine MIME type
mime_type = mimetypes.guess_type(original_filename)[0] or 'application/octet-stream'

return FileResponse(
file_path,
media_type=mime_type,
filename=original_filename
)

raise HTTPException(status_code=404, detail="Document not found")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Preview failed: {str(e)}")


@router.get("/download/{document_id}")
async def download_document(document_id: str):
"""
Download a specific document
"""
try:
for filename in os.listdir(UPLOAD_DIRECTORY):
# Check if the filename contains the document_id
if document_id in filename:
file_path = os.path.join(UPLOAD_DIRECTORY, filename)

# Extract original filename
original_filename = filename.split('__')[0]

# Determine MIME type
mime_type = mimetypes.guess_type(original_filename)[0] or 'application/octet-stream'

return FileResponse(
file_path,
media_type=mime_type,
filename=original_filename,
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{original_filename}"
}
)

raise HTTPException(status_code=404, detail="Document not found")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Download failed: {str(e)}")
23 changes: 18 additions & 5 deletions servers/fastapi/api/main.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from api.lifespan import app_lifespan
from api.middlewares import UserConfigEnvUpdateMiddleware
from api.middlewares import UserConfigEnvUpdateMiddleware, ServerActionsValidationMiddleware
from api.v1.ppt.router import API_V1_PPT_ROUTER
from api.v1.router import API_V1_ROUTER

from api.document_router import router as document_router # Add this import

app = FastAPI(lifespan=app_lifespan)


# Routers
app.include_router(API_V1_PPT_ROUTER)
app.include_router(API_V1_ROUTER)
app.include_router(document_router) # Add the document router

# Middlewares
origins = ["*"]
origins = [
"http://localhost:3000", # Next.js development server
"http://localhost:5001", # FastAPI server
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
allow_headers=[
"Content-Type",
"Authorization",
"Access-Control-Allow-Headers",
"Access-Control-Allow-Origin",
"X-Requested-With",
"X-Forwarded-Host",
"Origin",
"*"
],
)

app.add_middleware(UserConfigEnvUpdateMiddleware)
app.add_middleware(ServerActionsValidationMiddleware)
24 changes: 23 additions & 1 deletion servers/fastapi/api/middlewares.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import Request
from fastapi import Request, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response

Expand All @@ -11,3 +11,25 @@ async def dispatch(self, request: Request, call_next):
if get_can_change_keys_env() != "false":
update_env_with_user_config()
return await call_next(request)


class ServerActionsValidationMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Validate Server Actions request headers
forwarded_host = request.headers.get('x-forwarded-host', '')
origin = request.headers.get('origin', '')

# Allowed hosts for Server Actions
allowed_hosts = ['localhost:3000', 'localhost:5001']

# Check if the headers match or are in the allowed hosts
if forwarded_host and origin:
if forwarded_host not in allowed_hosts or origin not in allowed_hosts:
# Detailed error logging
print(f"Header mismatch: x-forwarded-host={forwarded_host}, origin={origin}")
raise HTTPException(
status_code=403,
detail="Invalid Server Actions request. Host and origin headers do not match."
)

return await call_next(request)
111 changes: 111 additions & 0 deletions servers/fastapi/api/v1/documents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import os
from typing import List, Dict, Any
from fastapi import APIRouter, File, UploadFile, Depends, HTTPException
from fastapi.responses import FileResponse

from services.document_service import DocumentService
from dependencies.auth import get_current_user
from models.document_upload import (
DocumentUploadRequest,
DocumentUploadResponse,
DocumentPreviewRequest,
DocumentPreviewResponse,
DocumentDownloadRequest
)

router = APIRouter(prefix="/documents", tags=["documents"])

@router.post("/upload", response_model=DocumentUploadResponse)
async def upload_document(
file: UploadFile = File(...),
current_user: dict = Depends(get_current_user)
):
"""
Upload a document

Args:
file (UploadFile): The file to upload
current_user (dict): Authenticated user details

Returns:
DocumentUploadResponse: Details of the uploaded document
"""
try:
upload_result = DocumentService.upload_document(file, current_user['id'])
return upload_result
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")

@router.post("/preview", response_model=DocumentPreviewResponse)
async def preview_document(
request: DocumentPreviewRequest,
current_user: dict = Depends(get_current_user)
):
"""
Generate a preview for a document

Args:
request (DocumentPreviewRequest): Document preview request
current_user (dict): Authenticated user details

Returns:
DocumentPreviewResponse: Document preview information
"""
try:
preview_result = DocumentService.preview_document(
request.document_id,
current_user['id']
)
return preview_result
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"Preview failed: {str(e)}")

@router.post("/download")
async def download_document(
request: DocumentDownloadRequest,
current_user: dict = Depends(get_current_user)
):
"""
Download a document

Args:
request (DocumentDownloadRequest): Document download request
current_user (dict): Authenticated user details

Returns:
FileResponse: The file to be downloaded
"""
try:
return DocumentService.download_document(
request.document_id,
current_user['id']
)
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"Download failed: {str(e)}")

@router.get("/", response_model=List[Dict[str, Any]])
async def list_documents(
current_user: dict = Depends(get_current_user)
):
"""
List all documents for the current user

Args:
current_user (dict): Authenticated user details

Returns:
List[Dict[str, Any]]: List of documents
"""
try:
documents = DocumentService.list_documents(current_user['id'])
return documents
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to list documents: {str(e)}")
30 changes: 30 additions & 0 deletions servers/fastapi/models/document_upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime

class DocumentUploadRequest(BaseModel):
file_name: str
file_type: str
file_size: int
organization_id: Optional[int] = None

class DocumentUploadResponse(BaseModel):
document_id: int
file_name: str
file_path: str
uploaded_at: datetime
file_type: str
file_size: int

class DocumentPreviewRequest(BaseModel):
document_id: int

class DocumentPreviewResponse(BaseModel):
document_id: int
file_name: str
file_type: str
preview_url: Optional[str] = None
preview_text: Optional[str] = None

class DocumentDownloadRequest(BaseModel):
document_id: int
Loading