|
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) |