homebox_ai_fronted/backend/routers/homebox.py

243 lines
8.2 KiB
Python

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
url: str # Direct link to item in Homebox
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()
# Construct direct link to item in Homebox
item_url = f"{settings.homebox_url}/item/{data['id']}"
return ItemResponse(id=data["id"], name=data["name"], url=item_url)
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)}")