import os import uuid from fastapi import APIRouter, Depends, HTTPException, Header, UploadFile, File, Form, status from passlib.context import CryptContext from sqlalchemy.orm import Session from database import get_db from models.site import Site from models.menu_snapshot import MenuSnapshot from models.product_image import ProductImage from schemas.menu import MenuSyncRequest, MenuSyncResponse router = APIRouter() _pwd = CryptContext(schemes=["bcrypt"], deprecated="auto") PRODUCT_IMAGE_DIR = "/app/data/product_images" def _require_site( x_site_id: str = Header(..., alias="X-Site-ID"), x_site_key: str = Header(..., alias="X-Site-Key"), db: Session = Depends(get_db), ) -> Site: site = db.query(Site).filter(Site.site_id == x_site_id).first() if not site or not _pwd.verify(x_site_key, site.secret_key_hash): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid site credentials") return site # ── Public ──────────────────────────────────────────────────────────────────── @router.get("/{site_slug}") def get_menu(site_slug: str, db: Session = Depends(get_db)): """Return the latest menu snapshot for a site. Used by the public menu SPA.""" site = db.query(Site).filter(Site.site_id == site_slug).first() if not site: raise HTTPException(status_code=404, detail="Site not found") snapshot = db.query(MenuSnapshot).filter(MenuSnapshot.site_id == site.id).first() if not snapshot: raise HTTPException(status_code=404, detail="No menu published yet") import json data = json.loads(snapshot.snapshot_json) data["menu_mode"] = site.menu_mode data["restaurant"] = { "name": site.menu_display_name, "tagline": {"en": site.menu_tagline_en, "gr": site.menu_tagline_gr}, "blurb": {"en": site.menu_blurb_en, "gr": site.menu_blurb_gr}, "hours": site.menu_hours, "headerImageUrl": site.menu_header_image_url, } cloud_images = { img.product_id: img.image_url for img in db.query(ProductImage).filter(ProductImage.site_id == site.id).all() } for cat in data.get("categories", []): for product in cat.get("products", []): # image_url as pushed by local_backend points at the restaurant's own # LAN/local server and isn't reachable from the internet — always # replace it with the cloud-hosted copy (or None) unless a manual # digital_image_url override is set. if not product.get("digital_image_url"): product["image_url"] = cloud_images.get(product.get("id")) return data # ── Internal (site API key) ─────────────────────────────────────────────────── @router.post("/sync", response_model=MenuSyncResponse) def sync_menu(body: MenuSyncRequest, site: Site = Depends(_require_site), db: Session = Depends(get_db)): """Upsert the menu snapshot for this site. Called by local_backend on each sync.""" snapshot = db.query(MenuSnapshot).filter(MenuSnapshot.site_id == body.site_id).first() if snapshot: snapshot.snapshot_json = body.snapshot_json else: snapshot = MenuSnapshot(site_id=body.site_id, snapshot_json=body.snapshot_json) db.add(snapshot) db.commit() return MenuSyncResponse(ok=True) @router.post("/sync-image") async def sync_product_image( product_id: int = Form(...), file: UploadFile = File(...), site: Site = Depends(_require_site), db: Session = Depends(get_db), ): """Upload/replace the cloud-hosted copy of a product's image. Called by local_backend during menu sync for products whose image changed.""" if not file.content_type or not file.content_type.startswith("image/"): raise HTTPException(status_code=400, detail="File must be an image") contents = await file.read() import hashlib image_hash = hashlib.sha256(contents).hexdigest() record = ( db.query(ProductImage) .filter(ProductImage.site_id == site.id, ProductImage.product_id == product_id) .first() ) os.makedirs(PRODUCT_IMAGE_DIR, exist_ok=True) if record and record.image_hash == image_hash: return {"ok": True, "image_url": record.image_url, "unchanged": True} if record: old_path = os.path.join(PRODUCT_IMAGE_DIR, os.path.basename(record.image_url)) if os.path.exists(old_path): os.remove(old_path) ext = file.filename.rsplit(".", 1)[-1].lower() if file.filename and "." in file.filename else "jpg" filename = f"{site.id}_{product_id}_{uuid.uuid4().hex[:8]}.{ext}" filepath = os.path.join(PRODUCT_IMAGE_DIR, filename) with open(filepath, "wb") as f: f.write(contents) image_url = f"/static/product_images/{filename}" if record: record.image_url = image_url record.image_hash = image_hash else: record = ProductImage(site_id=site.id, product_id=product_id, image_url=image_url, image_hash=image_hash) db.add(record) db.commit() return {"ok": True, "image_url": image_url, "unchanged": False}