import base64 from typing import Optional import httpx from fastapi import APIRouter, HTTPException, Depends, Header from pydantic import BaseModel from config import get_settings, Settings router = APIRouter() class LoginRequest(BaseModel): username: str password: str class LoginResponse(BaseModel): token: str expires_at: Optional[str] = None class ItemCreate(BaseModel): name: str description: str = "" location_id: str # Required by Homebox label_ids: list[str] = [] quantity: int = 1 serial_number: str = "" model_number: str = "" manufacturer: str = "" purchase_price: float = 0 purchase_from: str = "" notes: str = "" insured: bool = False class ItemResponse(BaseModel): id: str name: str class AttachmentUpload(BaseModel): image: str # Base64 encoded filename: str = "photo.jpg" class LabelCreate(BaseModel): name: str class LabelResponse(BaseModel): id: str name: str def get_homebox_client(settings: Settings = Depends(get_settings)): """Create httpx client for Homebox API.""" return httpx.AsyncClient( base_url=settings.homebox_url, timeout=30.0, ) def get_auth_header( authorization: Optional[str] = Header(None), settings: Settings = Depends(get_settings), ) -> dict: """Get authorization header from request or config.""" if authorization: return {"Authorization": authorization} if settings.homebox_token: return {"Authorization": f"Bearer {settings.homebox_token}"} return {} @router.post("/login", response_model=LoginResponse) async def login(request: LoginRequest, settings: Settings = Depends(get_settings)): """Login to Homebox and get auth token.""" async with httpx.AsyncClient(base_url=settings.homebox_url, timeout=30.0) as client: try: response = await client.post( "/api/v1/users/login", json={ "username": request.username, "password": request.password, }, ) response.raise_for_status() data = response.json() token = data.get("token", "") # Homebox returns "Bearer ..." prefix, strip it if token.startswith("Bearer "): token = token[7:] return LoginResponse( token=token, expires_at=data.get("expiresAt"), ) except httpx.HTTPStatusError as e: raise HTTPException( status_code=e.response.status_code, detail="Login failed - check credentials", ) except httpx.RequestError as e: raise HTTPException(status_code=502, detail=f"Cannot connect to Homebox: {str(e)}") @router.get("/locations") async def get_locations( settings: Settings = Depends(get_settings), auth: dict = Depends(get_auth_header), ): """Get all locations from Homebox.""" async with httpx.AsyncClient(base_url=settings.homebox_url, timeout=30.0) as client: try: response = await client.get("/api/v1/locations", headers=auth) response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: raise HTTPException(status_code=e.response.status_code, detail="Failed to fetch locations") except httpx.RequestError as e: raise HTTPException(status_code=502, detail=f"Cannot connect to Homebox: {str(e)}") @router.get("/labels") async def get_labels( settings: Settings = Depends(get_settings), auth: dict = Depends(get_auth_header), ): """Get all labels/tags from Homebox.""" async with httpx.AsyncClient(base_url=settings.homebox_url, timeout=30.0) as client: try: response = await client.get("/api/v1/labels", headers=auth) response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: raise HTTPException(status_code=e.response.status_code, detail="Failed to fetch labels") except httpx.RequestError as e: raise HTTPException(status_code=502, detail=f"Cannot connect to Homebox: {str(e)}") @router.post("/labels", response_model=LabelResponse) async def create_label( label: LabelCreate, settings: Settings = Depends(get_settings), auth: dict = Depends(get_auth_header), ): """Create a new label in Homebox.""" async with httpx.AsyncClient(base_url=settings.homebox_url, timeout=30.0) as client: try: response = await client.post( "/api/v1/labels", json={"name": label.name}, headers=auth, ) response.raise_for_status() data = response.json() return LabelResponse(id=data["id"], name=data["name"]) except httpx.HTTPStatusError as e: raise HTTPException(status_code=e.response.status_code, detail=f"Failed to create label: {e.response.text}") except httpx.RequestError as e: raise HTTPException(status_code=502, detail=f"Cannot connect to Homebox: {str(e)}") @router.post("/items", response_model=ItemResponse) async def create_item( item: ItemCreate, settings: Settings = Depends(get_settings), auth: dict = Depends(get_auth_header), ): """Create a new item in Homebox.""" async with httpx.AsyncClient(base_url=settings.homebox_url, timeout=30.0) as client: try: payload = { "name": item.name, "description": item.description, "locationId": item.location_id, "quantity": item.quantity, "serialNumber": item.serial_number, "modelNumber": item.model_number, "manufacturer": item.manufacturer, "purchasePrice": item.purchase_price, "purchaseFrom": item.purchase_from, "notes": item.notes, "insured": item.insured, } if item.label_ids: payload["labelIds"] = item.label_ids response = await client.post("/api/v1/items", json=payload, headers=auth) response.raise_for_status() data = response.json() return ItemResponse(id=data["id"], name=data["name"]) except httpx.HTTPStatusError as e: raise HTTPException(status_code=e.response.status_code, detail=f"Failed to create item: {e.response.text}") except httpx.RequestError as e: raise HTTPException(status_code=502, detail=f"Cannot connect to Homebox: {str(e)}") @router.post("/items/{item_id}/attachments") async def upload_attachment( item_id: str, attachment: AttachmentUpload, settings: Settings = Depends(get_settings), auth: dict = Depends(get_auth_header), ): """Upload an image attachment to an item.""" # Strip data URL prefix if present image_data = attachment.image if "," in image_data: image_data = image_data.split(",", 1)[1] try: image_bytes = base64.b64decode(image_data) except Exception: raise HTTPException(status_code=400, detail="Invalid base64 image data") async with httpx.AsyncClient(base_url=settings.homebox_url, timeout=60.0) as client: try: files = { "file": (attachment.filename, image_bytes, "image/jpeg"), } data = { "name": attachment.filename, "type": "photo", "primary": "true", } response = await client.post( f"/api/v1/items/{item_id}/attachments", files=files, data=data, headers=auth, ) response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: raise HTTPException(status_code=e.response.status_code, detail=f"Failed to upload attachment: {e.response.text}") except httpx.RequestError as e: raise HTTPException(status_code=502, detail=f"Cannot connect to Homebox: {str(e)}")