240 lines
8.0 KiB
Python
240 lines
8.0 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
|
|
|
|
|
|
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)}")
|