recipe2 / app.py
sharktide's picture
Finish security updates
4ac7918 verified
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)