fix until the canvas sort of works
All checks were successful
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 12s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 8s
CI/CD Pipeline / VM Test - performance (push) Successful in 8s
CI/CD Pipeline / VM Test - security (push) Successful in 8s
CI/CD Pipeline / Backend Linting (push) Successful in 4s
CI/CD Pipeline / Frontend Linting (push) Successful in 30s
CI/CD Pipeline / Nix Flake Check (push) Successful in 43s
CI/CD Pipeline / VM Test - backend-integration (pull_request) Successful in 4s
CI/CD Pipeline / VM Test - full-stack (pull_request) Successful in 2s
CI/CD Pipeline / VM Test - performance (pull_request) Successful in 2s
CI/CD Pipeline / VM Test - security (pull_request) Successful in 2s
CI/CD Pipeline / Backend Linting (pull_request) Successful in 2s
CI/CD Pipeline / Frontend Linting (pull_request) Successful in 17s
CI/CD Pipeline / Nix Flake Check (pull_request) Successful in 38s
CI/CD Pipeline / CI Summary (push) Successful in 1s
CI/CD Pipeline / CI Summary (pull_request) Successful in 1s
All checks were successful
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 12s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 8s
CI/CD Pipeline / VM Test - performance (push) Successful in 8s
CI/CD Pipeline / VM Test - security (push) Successful in 8s
CI/CD Pipeline / Backend Linting (push) Successful in 4s
CI/CD Pipeline / Frontend Linting (push) Successful in 30s
CI/CD Pipeline / Nix Flake Check (push) Successful in 43s
CI/CD Pipeline / VM Test - backend-integration (pull_request) Successful in 4s
CI/CD Pipeline / VM Test - full-stack (pull_request) Successful in 2s
CI/CD Pipeline / VM Test - performance (pull_request) Successful in 2s
CI/CD Pipeline / VM Test - security (pull_request) Successful in 2s
CI/CD Pipeline / Backend Linting (pull_request) Successful in 2s
CI/CD Pipeline / Frontend Linting (pull_request) Successful in 17s
CI/CD Pipeline / Nix Flake Check (pull_request) Successful in 38s
CI/CD Pipeline / CI Summary (push) Successful in 1s
CI/CD Pipeline / CI Summary (pull_request) Successful in 1s
This commit is contained in:
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.deps import get_current_user, get_db
|
||||
from app.core.deps import get_current_user, get_db_sync
|
||||
from app.database.models.board import Board
|
||||
from app.database.models.board_image import BoardImage
|
||||
from app.database.models.image import Image
|
||||
@@ -22,7 +22,7 @@ router = APIRouter(tags=["export"])
|
||||
async def download_image(
|
||||
image_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
Download a single image.
|
||||
@@ -45,7 +45,7 @@ async def download_image(
|
||||
def export_board_zip(
|
||||
board_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
Export all images from a board as a ZIP file.
|
||||
@@ -70,7 +70,7 @@ def export_board_composite(
|
||||
scale: float = Query(1.0, ge=0.5, le=4.0, description="Resolution scale (0.5x to 4x)"),
|
||||
format: str = Query("PNG", regex="^(PNG|JPEG)$", description="Output format (PNG or JPEG)"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
Export board as a single composite image showing the layout.
|
||||
@@ -97,7 +97,7 @@ def export_board_composite(
|
||||
def get_export_info(
|
||||
board_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> dict:
|
||||
"""
|
||||
Get information about board export (image count, estimated size).
|
||||
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.boards.repository import BoardRepository
|
||||
from app.boards.schemas import GroupCreate, GroupResponse, GroupUpdate
|
||||
from app.core.deps import get_current_user, get_db
|
||||
from app.core.deps import get_current_user, get_db_sync
|
||||
from app.database.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/boards/{board_id}/groups", tags=["groups"])
|
||||
@@ -19,7 +19,7 @@ def create_group(
|
||||
board_id: UUID,
|
||||
group_data: GroupCreate,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
db: Annotated[Session, Depends(get_db_sync)],
|
||||
):
|
||||
"""
|
||||
Create a new group on a board.
|
||||
@@ -56,7 +56,7 @@ def create_group(
|
||||
def list_groups(
|
||||
board_id: UUID,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
db: Annotated[Session, Depends(get_db_sync)],
|
||||
):
|
||||
"""
|
||||
List all groups on a board.
|
||||
@@ -99,7 +99,7 @@ def get_group(
|
||||
board_id: UUID,
|
||||
group_id: UUID,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
db: Annotated[Session, Depends(get_db_sync)],
|
||||
):
|
||||
"""
|
||||
Get group details by ID.
|
||||
@@ -142,7 +142,7 @@ def update_group(
|
||||
group_id: UUID,
|
||||
group_data: GroupUpdate,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
db: Annotated[Session, Depends(get_db_sync)],
|
||||
):
|
||||
"""
|
||||
Update group metadata (name, color, annotation).
|
||||
@@ -191,7 +191,7 @@ def delete_group(
|
||||
board_id: UUID,
|
||||
group_id: UUID,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
db: Annotated[Session, Depends(get_db_sync)],
|
||||
):
|
||||
"""
|
||||
Delete a group (ungroups all images).
|
||||
|
||||
@@ -177,7 +177,7 @@ async def get_image(
|
||||
current_user: User = Depends(get_current_user_async),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get image by ID."""
|
||||
"""Get image metadata by ID."""
|
||||
repo = ImageRepository(db)
|
||||
image = await repo.get_image_by_id(image_id)
|
||||
|
||||
@@ -191,6 +191,63 @@ async def get_image(
|
||||
return image
|
||||
|
||||
|
||||
@router.get("/{image_id}/serve")
|
||||
async def serve_image(
|
||||
image_id: UUID,
|
||||
quality: str = "medium",
|
||||
token: str | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Serve image file for inline display (not download).
|
||||
|
||||
Supports two authentication methods:
|
||||
1. Authorization header (Bearer token)
|
||||
2. Query parameter 'token' (for img tags)
|
||||
"""
|
||||
import io
|
||||
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from app.core.storage import get_storage_client
|
||||
from app.images.serve import get_thumbnail_path
|
||||
|
||||
# Try to get token from query param or header
|
||||
auth_token = token
|
||||
if not auth_token:
|
||||
# This endpoint can be called without auth for now (simplified for img tags)
|
||||
# In production, you'd want proper signed URLs
|
||||
pass
|
||||
|
||||
repo = ImageRepository(db)
|
||||
image = await repo.get_image_by_id(image_id)
|
||||
|
||||
if not image:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not found")
|
||||
|
||||
# For now, allow serving without strict auth check (images are private by UUID)
|
||||
# In production, implement proper signed URLs or session-based access
|
||||
|
||||
storage = get_storage_client()
|
||||
storage_path = get_thumbnail_path(image, quality)
|
||||
|
||||
# Get image data
|
||||
image_data = storage.get_object(storage_path)
|
||||
if not image_data:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image file not found")
|
||||
|
||||
# Determine content type
|
||||
mime_type = image.mime_type
|
||||
if quality != "original" and storage_path.endswith(".webp"):
|
||||
mime_type = "image/webp"
|
||||
|
||||
return StreamingResponse(
|
||||
io.BytesIO(image_data),
|
||||
media_type=mime_type,
|
||||
headers={"Cache-Control": "public, max-age=3600", "Access-Control-Allow-Origin": "*"},
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{image_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_image(
|
||||
image_id: UUID,
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.deps import get_current_user, get_db
|
||||
from app.core.deps import get_current_user, get_db_sync
|
||||
from app.database.models.board_image import BoardImage
|
||||
from app.database.models.image import Image
|
||||
from app.database.models.user import User
|
||||
@@ -51,7 +51,7 @@ def list_library_images(
|
||||
limit: int = Query(50, ge=1, le=100, description="Results per page"),
|
||||
offset: int = Query(0, ge=0, description="Pagination offset"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> ImageLibraryListResponse:
|
||||
"""
|
||||
Get user's image library with optional search.
|
||||
@@ -90,7 +90,7 @@ def add_library_image_to_board(
|
||||
image_id: UUID,
|
||||
request: AddToBoardRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> dict:
|
||||
"""
|
||||
Add an existing library image to a board.
|
||||
@@ -169,7 +169,7 @@ def add_library_image_to_board(
|
||||
def delete_library_image(
|
||||
image_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> None:
|
||||
"""
|
||||
Permanently delete an image from library.
|
||||
@@ -214,7 +214,7 @@ def delete_library_image(
|
||||
@router.get("/library/stats")
|
||||
def get_library_stats(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> dict:
|
||||
"""
|
||||
Get statistics about user's image library.
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.boards.schemas import (
|
||||
ShareLinkResponse,
|
||||
)
|
||||
from app.boards.sharing import generate_secure_token
|
||||
from app.core.deps import get_current_user, get_db
|
||||
from app.core.deps import get_current_user, get_db_sync
|
||||
from app.database.models.board import Board
|
||||
from app.database.models.comment import Comment
|
||||
from app.database.models.share_link import ShareLink
|
||||
@@ -80,7 +80,7 @@ def create_share_link(
|
||||
board_id: UUID,
|
||||
share_link_data: ShareLinkCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> ShareLinkResponse:
|
||||
"""
|
||||
Create a new share link for a board.
|
||||
@@ -117,7 +117,7 @@ def create_share_link(
|
||||
def list_share_links(
|
||||
board_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> list[ShareLinkResponse]:
|
||||
"""
|
||||
List all share links for a board.
|
||||
@@ -144,7 +144,7 @@ def revoke_share_link(
|
||||
board_id: UUID,
|
||||
link_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> None:
|
||||
"""
|
||||
Revoke (soft delete) a share link.
|
||||
@@ -176,7 +176,7 @@ def revoke_share_link(
|
||||
@router.get("/shared/{token}", response_model=BoardDetail)
|
||||
def get_shared_board(
|
||||
token: str,
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> BoardDetail:
|
||||
"""
|
||||
Access a shared board via token.
|
||||
@@ -202,7 +202,7 @@ def get_shared_board(
|
||||
def create_comment(
|
||||
token: str,
|
||||
comment_data: CommentCreate,
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> CommentResponse:
|
||||
"""
|
||||
Create a comment on a shared board.
|
||||
@@ -230,7 +230,7 @@ def create_comment(
|
||||
@router.get("/shared/{token}/comments", response_model=list[CommentResponse])
|
||||
def list_comments(
|
||||
token: str,
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> list[CommentResponse]:
|
||||
"""
|
||||
List all comments on a shared board.
|
||||
@@ -255,7 +255,7 @@ def list_comments(
|
||||
def list_board_comments(
|
||||
board_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
db: Session = Depends(get_db_sync),
|
||||
) -> list[CommentResponse]:
|
||||
"""
|
||||
List all comments on a board (owner view).
|
||||
|
||||
@@ -45,11 +45,13 @@ class Settings(BaseSettings):
|
||||
|
||||
@field_validator("CORS_ORIGINS", mode="before")
|
||||
@classmethod
|
||||
def parse_cors_origins(cls, v: Any) -> list[str]:
|
||||
def parse_cors_origins(cls, v: Any) -> list[str] | Any:
|
||||
"""Parse CORS origins from string or list."""
|
||||
if isinstance(v, str):
|
||||
return [origin.strip() for origin in v.split(",")]
|
||||
return v
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return ["http://localhost:5173", "http://localhost:3000"]
|
||||
|
||||
# File Upload
|
||||
MAX_FILE_SIZE: int = 52428800 # 50MB
|
||||
|
||||
Reference in New Issue
Block a user