import os from typing import List, Optional from fastapi import FastAPI, HTTPException, Request, Query, Depends from fastapi.responses import JSONResponse from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from supabase import create_client, Client from jose import jwt SUPABASE_URL = os.getenv("SUPABASE_URL") SUPABASE_KEY = os.getenv("SUPABASE_KEY") JWT_KEY = os.getenv("JWT_KEY") supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) class Recipe(BaseModel): name: str time: str creator: str category: str diff: str description: str ingredients: List[str] instructions: str class RecipeUpdate(BaseModel): name: Optional[str] time: Optional[str] creator: Optional[str] category: Optional[str] diff: Optional[str] ingredients: Optional[List[str]] description: Optional[str] instructions: Optional[str] class DeleteRecipeRequest(BaseModel): id: str security = HTTPBearer() def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): token = credentials.credentials try: decoded = jwt.decode( token, JWT_KEY, algorithms=["HS256"], options={"verify_aud": False} ) return decoded except Exception as e: raise HTTPException(status_code=401, detail="Invalid or expired authorization") @app.put("/supabase/add/recipe") def add_recipe_to_supabase(recipe: Recipe, token_data=Depends(verify_token)): existing = supabase.table("recipes").select("name").eq("name", recipe.name).execute() user_id = token_data['sub'] if existing.data: raise HTTPException(status_code=400, detail="Recipe with this name already exists.") try: response = supabase.table("recipes").insert({ "name": recipe.name, "time": recipe.time, "creator": recipe.creator, "user_id": user_id, "category": recipe.category, "diff": recipe.diff, "description": recipe.description, "ingredients": recipe.ingredients, "instructions": recipe.instructions }).execute() except Exception as e: raise HTTPException(status_code=500, detail=f"Insert failed: {str(e)}") return {"message": "Recipe stored successfully!"} @app.get("/supabase/recipes") def get_all_recipes(): try: response = supabase.table("recipes").select("*").execute() data = response.data return { "rows": [{ "row": recipe } for recipe in data] } except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to fetch recipes: {str(e)}") @app.get("/supabase/myrecipes") def get_my_recipes(user_id): response = supabase.table("recipes").select("*").eq("user_id", user_id).execute() data = response.data return { "rows": [{ "row": recipe } for recipe in data] } @app.get("/supabase/recipebyid") async def get_recipe_by_id(id: str): try: response = supabase.table("recipes").select("*").eq("id", id).single().execute() if response.data is None: raise HTTPException(status_code=404, detail="Recipe not found") return {"recipe": {"row": response.data}} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.delete("/supabase/delrecipe") async def delete_recipe(data: DeleteRecipeRequest, token_data=Depends(verify_token)): recipe = supabase.table("recipes").select("*").eq("id", data.id).single().execute() user_id = token_data['sub'] if not recipe.data: raise HTTPException(status_code=404, detail="Recipe not found") if recipe.data["user_id"] != user_id: raise HTTPException(status_code=403, detail="Deletion Aborted: Authorization required not found to complete this interaction") supabase.table("recipes").delete().eq("id", data.id).execute() return {"success": True, "message": "Recipe deleted"} @app.get("/status") def status(): return {"status": "ok"} @app.patch("/supabase/edit/recipe") async def edit_recipe(request: Request, id: str, update: RecipeUpdate, token_data=Depends(verify_token)): user_id = token_data['sub'] update_dict = update.dict(exclude_none=True) if not update_dict: raise HTTPException(status_code=400, detail="No fields provided to update.") ownership_check = supabase.table("recipes").select("user_id").eq("id", id).single().execute() if ownership_check.data["user_id"] != user_id: raise HTTPException(status_code=403, detail="Edit Aborted: Authorization required not found to complete this interaction") response = supabase.table("recipes").update(update_dict).eq("id", id).execute() return JSONResponse(content={ "message": "Recipe updated successfully.", "data": response.data }) @app.get("/supabase/recipes/paged") def get_recipes_paged( limit: int = Query(12, ge=1), offset: int = Query(0, ge=0), search: Optional[str] = None, ): try: if search: query_name = supabase.table("recipes").select("*").filter("name", "ilike", f"%{search}%") query_creator = supabase.table("recipes").select("*").filter("creator", "ilike", f"%{search}%") response_name = query_name.execute() response_creator = query_creator.execute() raw_data = {recipe["id"]: recipe for recipe in response_name.data + response_creator.data}.values() total_count = len(raw_data) else: query = supabase.table("recipes").select("*").range(offset, offset + limit - 1) response = query.execute() raw_data = response.data total_count_response = supabase.table("recipes").select("id", count="exact").execute() total_count = total_count_response.count if total_count_response else 0 paged_data = list(raw_data)[offset: offset + limit] wrapped_data = [{"row": recipe} for recipe in paged_data] return { "rows": wrapped_data, "offset": offset, "limit": limit, "total_count": total_count } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=7860)