API security hardening (#571)

* Enhance security and safety around user update API

- Prevent a regular user from promoting themself to admin
- Prevent an admin from demoting themself
- Refactor token fixture to admin + regular user tokens

* Restrict user CRUD API to admins

* Secure admin API routes

* Refactor APIrouter into Admin/UserAPIRouter

* Secure theme routes

* Make 'all recipes' routes public

* Secure favorite routes

* Remove redundant checks

* Fix public routes mistakenly flagged user routes

* Make webhooks changeable only by admin

* Allow users to create categories and tags

* Address lint issues
This commit is contained in:
sephrat
2021-06-22 20:22:15 +02:00
committed by GitHub
parent f5faff66d3
commit 6320ba7ec5
43 changed files with 456 additions and 347 deletions

View File

@@ -42,12 +42,16 @@ def api_routers():
app.include_router(meal_plan_router)
# Settings Routes
app.include_router(settings_router)
app.include_router(theme_routes.router)
app.include_router(theme_routes.public_router)
app.include_router(theme_routes.user_router)
# Backups/Imports Routes
app.include_router(backup_routes.router)
# Migration Routes
app.include_router(migration_routes.router)
app.include_router(debug_routes.router)
# Debug routes
app.include_router(debug_routes.public_router)
app.include_router(debug_routes.admin_router)
# Utility routes
app.include_router(utility_routes.router)

View File

@@ -1,41 +1,36 @@
from http.client import HTTPException
from fastapi import APIRouter, Depends, status
from fastapi import Depends, status
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.event_notifications import EventNotificationIn, EventNotificationOut
from mealie.schema.events import EventsOut, TestEvent
from mealie.schema.user import UserInDB
from mealie.services.events import test_notification
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/events", tags=["App Events"])
router = AdminAPIRouter(prefix="/events", tags=["App Events"])
logger = get_logger()
@router.get("", response_model=EventsOut)
async def get_events(session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)):
async def get_events(session: Session = Depends(generate_session)):
""" Get event from the Database """
# Get Item
return EventsOut(total=db.events.count_all(session), events=db.events.get_all(session, order_by="time_stamp"))
@router.delete("")
async def delete_events(
session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
async def delete_events(session: Session = Depends(generate_session)):
""" Get event from the Database """
# Get Item
return db.events.delete_all(session)
@router.delete("/{id}")
async def delete_event(
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
async def delete_event(id: int, session: Session = Depends(generate_session)):
""" Delete event from the Database """
return db.events.delete(session, id)
@@ -44,7 +39,6 @@ async def delete_event(
async def create_event_notification(
event_data: EventNotificationIn,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Create event_notification in the Database """
@@ -55,7 +49,6 @@ async def create_event_notification(
async def test_notification_route(
test_data: TestEvent,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Create event_notification in the Database """
@@ -71,27 +64,21 @@ async def test_notification_route(
@router.get("/notifications", response_model=list[EventNotificationOut])
async def get_all_event_notification(
session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
async def get_all_event_notification(session: Session = Depends(generate_session)):
""" Get all event_notification from the Database """
# Get Item
return db.event_notifications.get_all(session, override_schema=EventNotificationOut)
@router.put("/notifications/{id}")
async def update_event_notification(
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
async def update_event_notification(id: int, session: Session = Depends(generate_session)):
""" Update event_notification in the Database """
# Update Item
return {"details": "not yet implemented"}
# not yet implemented
raise HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED)
@router.delete("/notifications/{id}")
async def delete_event_notification(
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
async def delete_event_notification(id: int, session: Session = Depends(generate_session)):
""" Delete event_notification from the Database """
# Delete Item
return db.event_notifications.delete(session, id)

View File

@@ -2,19 +2,19 @@ import operator
import shutil
from pathlib import Path
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, UploadFile, status
from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, status
from mealie.core.config import app_dirs
from mealie.core.root_logger import get_logger
from mealie.core.security import create_file_token
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
from mealie.services.backups import imports
from mealie.services.backups.exports import backup_all
from mealie.services.events import create_backup_event
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/backups", tags=["Backups"], dependencies=[Depends(get_current_user)])
router = AdminAPIRouter(prefix="/api/backups", tags=["Backups"])
logger = get_logger()

View File

@@ -1,18 +1,21 @@
from fastapi import APIRouter, Depends
from fastapi import Depends
from fastapi.routing import APIRouter
from mealie.core.config import APP_VERSION, app_dirs, settings
from mealie.core.root_logger import LOGGER_FILE
from mealie.core.security import create_file_token
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.about import AppInfo, AppStatistics, DebugInfo
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/debug", tags=["Debug"])
admin_router = AdminAPIRouter(prefix="/api/debug", tags=["Debug"])
public_router = APIRouter(prefix="/api/debug", tags=["Debug"])
@router.get("")
async def get_debug_info(current_user=Depends(get_current_user)):
@admin_router.get("")
async def get_debug_info():
""" Returns general information about the application for debugging """
return DebugInfo(
@@ -27,7 +30,7 @@ async def get_debug_info(current_user=Depends(get_current_user)):
)
@router.get("/statistics")
@admin_router.get("/statistics")
async def get_app_statistics(session: Session = Depends(generate_session)):
return AppStatistics(
total_recipes=db.recipes.count_all(session),
@@ -38,7 +41,7 @@ async def get_app_statistics(session: Session = Depends(generate_session)):
)
@router.get("/version")
@public_router.get("/version")
async def get_mealie_version():
""" Returns the current version of mealie"""
return AppInfo(
@@ -48,21 +51,21 @@ async def get_mealie_version():
)
@router.get("/last-recipe-json")
async def get_last_recipe_json(current_user=Depends(get_current_user)):
@admin_router.get("/last-recipe-json")
async def get_last_recipe_json():
""" Returns a token to download a file """
return {"fileToken": create_file_token(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"))}
@router.get("/log/{num}")
async def get_log(num: int, current_user=Depends(get_current_user)):
@admin_router.get("/log/{num}")
async def get_log(num: int):
""" Doc Str """
with open(LOGGER_FILE, "rb") as f:
log_text = tail(f, num)
return log_text
@router.get("/log")
@admin_router.get("/log")
async def get_log_file():
""" Returns a token to download a file """
return {"fileToken": create_file_token(LOGGER_FILE)}

View File

@@ -75,6 +75,12 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
return user
async def get_admin_user(current_user=Depends(get_current_user)) -> UserInDB:
if not current_user.admin:
raise HTTPException(status.HTTP_403_FORBIDDEN)
return current_user
def validate_long_live_token(session: Session, client_token: str, id: int) -> UserInDB:
tokens: list[LongLiveTokenInDB] = db.api_tokens.get(session, id, "parent_id", limit=9999)

View File

@@ -1,8 +1,7 @@
from fastapi import APIRouter
from . import crud, groups
from . import groups
groups_router = APIRouter()
groups_router.include_router(crud.router)
groups_router.include_router(groups.router)

View File

@@ -1,17 +1,18 @@
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from fastapi import BackgroundTasks, Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
from mealie.services.events import create_group_event
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/groups", tags=["Groups"])
admin_router = AdminAPIRouter(prefix="/api/groups", tags=["Groups administration"])
user_router = UserAPIRouter(prefix="/api/groups", tags=["Groups"])
@router.get("", response_model=list[GroupInDB])
@admin_router.get("", response_model=list[GroupInDB])
async def get_all_groups(
current_user=Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Returns a list of all groups in the database """
@@ -19,7 +20,7 @@ async def get_all_groups(
return db.groups.get_all(session)
@router.get("/self", response_model=GroupInDB)
@user_router.get("/self", response_model=GroupInDB)
async def get_current_user_group(
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
@@ -30,11 +31,10 @@ async def get_current_user_group(
return db.groups.get(session, current_user.group, "name")
@router.post("", status_code=status.HTTP_201_CREATED)
@admin_router.post("", status_code=status.HTTP_201_CREATED)
async def create_group(
background_tasks: BackgroundTasks,
group_data: GroupBase,
current_user=Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Creates a Group in the Database """
@@ -46,18 +46,17 @@ async def create_group(
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.put("/{id}")
@admin_router.put("/{id}")
async def update_group_data(
id: int,
group_data: UpdateGroup,
current_user=Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Updates a User Group """
db.groups.update(session, id, group_data.dict())
@router.delete("/{id}")
@admin_router.delete("/{id}")
async def delete_user_group(
background_tasks: BackgroundTasks,
id: int,

View File

@@ -3,4 +3,5 @@ from mealie.routes.groups import crud
router = APIRouter()
router.include_router(crud.router)
router.include_router(crud.admin_router)
router.include_router(crud.user_router)

View File

@@ -1,7 +1,8 @@
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from fastapi import BackgroundTasks, Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal import MealPlanIn, MealPlanOut
from mealie.schema.user import GroupInDB, UserInDB
from mealie.services.events import create_group_event
@@ -10,7 +11,7 @@ from mealie.services.meal_services import get_todays_meal, set_mealplan_dates
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
router = UserAPIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@router.get("/all", response_model=list[MealPlanOut])

View File

@@ -1,8 +1,9 @@
from fastapi import APIRouter, Depends
from fastapi import Depends
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal import MealPlanOut
from mealie.schema.recipe import Recipe
from mealie.schema.shopping_list import ListItem, ShoppingListIn, ShoppingListOut
@@ -11,7 +12,7 @@ from sqlalchemy.orm.session import Session
logger = get_logger()
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
router = UserAPIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@router.get("/{id}/shopping-list")

View File

@@ -2,16 +2,15 @@ import operator
import shutil
from typing import List
from fastapi import APIRouter, Depends, File, UploadFile, status
from fastapi import Depends, File, HTTPException, UploadFile, status
from mealie.core.config import app_dirs
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.migration import MigrationFile, Migrations
from mealie.services.migrations import migration
from sqlalchemy.orm.session import Session
from fastapi import HTTPException
router = APIRouter(prefix="/api/migrations", tags=["Migration"], dependencies=[Depends(get_current_user)])
router = AdminAPIRouter(prefix="/api/migrations", tags=["Migration"])
@router.get("", response_model=List[Migrations])

View File

@@ -4,7 +4,12 @@ from mealie.routes.recipe import all_recipe_routes, category_routes, comments, r
recipe_router = APIRouter()
recipe_router.include_router(all_recipe_routes.router)
recipe_router.include_router(recipe_crud_routes.router)
recipe_router.include_router(category_routes.router)
recipe_router.include_router(tag_routes.router)
recipe_router.include_router(recipe_crud_routes.public_router)
recipe_router.include_router(recipe_crud_routes.user_router)
recipe_router.include_router(category_routes.public_router)
recipe_router.include_router(category_routes.user_router)
recipe_router.include_router(category_routes.admin_router)
recipe_router.include_router(tag_routes.admin_router)
recipe_router.include_router(tag_routes.user_router)
recipe_router.include_router(tag_routes.public_router)
recipe_router.include_router(comments.router)

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, is_logged_in
from mealie.routes.deps import is_logged_in
from mealie.schema.recipe import RecipeSummary
from slugify import slugify
from sqlalchemy.orm.session import Session
@@ -36,21 +36,17 @@ async def get_recipe_summary(
)
@router.get(
"/api/recipes/summary/untagged", response_model=list[RecipeSummary], dependencies=[Depends(get_current_user)]
)
@router.get("/api/recipes/summary/untagged", response_model=list[RecipeSummary])
async def get_untagged_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_untagged(session, count=count, override_schema=RecipeSummary)
@router.get(
"/api/recipes/summary/uncategorized", response_model=list[RecipeSummary], dependencies=[Depends(get_current_user)]
)
@router.get("/api/recipes/summary/uncategorized", response_model=list[RecipeSummary])
async def get_uncategorized_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_uncategorized(session, count=count, override_schema=RecipeSummary)
@router.post("/api/recipes/category", deprecated=True, dependencies=[Depends(get_current_user)])
@router.post("/api/recipes/category", deprecated=True)
def filter_by_category(categories: list, session: Session = Depends(generate_session)):
""" pass a list of categories and get a list of recipes associated with those categories """
# ! This should be refactored into a single database call, but I couldn't figure it out
@@ -60,7 +56,7 @@ def filter_by_category(categories: list, session: Session = Depends(generate_ses
return in_category
@router.post("/api/recipes/tag", deprecated=True, dependencies=[Depends(get_current_user)])
@router.post("/api/recipes/tag", deprecated=True)
async def filter_by_tags(tags: list, session: Session = Depends(generate_session)):
""" pass a list of tags and get a list of recipes associated with those tags"""
# ! This should be refactored into a single database call, but I couldn't figure it out

View File

@@ -1,26 +1,29 @@
from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, is_logged_in
from mealie.routes.deps import is_logged_in
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.category import CategoryIn, RecipeCategoryResponse
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/categories", tags=["Recipe Categories"])
public_router = APIRouter(prefix="/api/categories", tags=["Recipe Categories"])
user_router = UserAPIRouter(prefix="/api/categories", tags=["Recipe Categories"])
admin_router = AdminAPIRouter(prefix="/api/categories", tags=["Recipe Categories"])
@router.get("")
@public_router.get("")
async def get_all_recipe_categories(session: Session = Depends(generate_session)):
""" Returns a list of available categories in the database """
return db.categories.get_all_limit_columns(session, ["slug", "name"])
@router.get("/empty")
@public_router.get("/empty")
def get_empty_categories(session: Session = Depends(generate_session)):
""" Returns a list of categories that do not contain any recipes"""
return db.categories.get_empty(session)
@router.get("/{category}", response_model=RecipeCategoryResponse)
@public_router.get("/{category}", response_model=RecipeCategoryResponse)
def get_all_recipes_by_category(
category: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
):
@@ -35,7 +38,7 @@ def get_all_recipes_by_category(
return category_obj
@router.post("", dependencies=[Depends(get_current_user)])
@user_router.post("")
async def create_recipe_category(category: CategoryIn, session: Session = Depends(generate_session)):
""" Creates a Category in the database """
@@ -45,7 +48,7 @@ async def create_recipe_category(category: CategoryIn, session: Session = Depend
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.put("/{category}", response_model=RecipeCategoryResponse, dependencies=[Depends(get_current_user)])
@admin_router.put("/{category}", response_model=RecipeCategoryResponse)
async def update_recipe_category(category: str, new_category: CategoryIn, session: Session = Depends(generate_session)):
""" Updates an existing Tag in the database """
@@ -55,7 +58,7 @@ async def update_recipe_category(category: str, new_category: CategoryIn, sessio
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.delete("/{category}", dependencies=[Depends(get_current_user)])
@admin_router.delete("/{category}")
async def delete_recipe_category(category: str, session: Session = Depends(generate_session)):
"""
Removes a recipe category from the database. Deleting a

View File

@@ -1,14 +1,15 @@
from http.client import HTTPException
from fastapi import APIRouter, Depends, status
from fastapi import Depends, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.comments import CommentIn, CommentOut, CommentSaveToDB
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api", tags=["Recipe Comments"])
router = UserAPIRouter(prefix="/api", tags=["Recipe Comments"])
@router.post("/recipes/{slug}/comments")
@@ -35,7 +36,7 @@ async def update_comment(
old_comment: CommentOut = db.comments.get(session, id)
if current_user.id != old_comment.user.id:
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
raise HTTPException(status.HTTP_403_FORBIDDEN)
return db.comments.update(session, id, new_comment)
@@ -51,4 +52,4 @@ async def delete_comment(
db.comments.delete(session, id)
return
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
raise HTTPException(status.HTTP_403_FORBIDDEN)

View File

@@ -1,4 +1,5 @@
import json
from mealie.routes.routers import UserAPIRouter
import shutil
from shutil import copyfileobj
from zipfile import ZipFile
@@ -21,11 +22,12 @@ from slugify import slugify
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
user_router = UserAPIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
public_router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
logger = get_logger()
@router.post("/create", status_code=201, response_model=str)
@user_router.post("/create", status_code=201, response_model=str)
def create_from_json(
background_tasks: BackgroundTasks,
data: Recipe,
@@ -46,12 +48,12 @@ def create_from_json(
return recipe.slug
@router.post("/test-scrape-url", dependencies=[Depends(get_current_user)])
@user_router.post("/test-scrape-url")
def test_parse_recipe_url(url: RecipeURLIn):
return scrape_url(url.url)
@router.post("/create-url", status_code=201, response_model=str)
@user_router.post("/create-url", status_code=201, response_model=str)
def parse_recipe_url(
background_tasks: BackgroundTasks,
url: RecipeURLIn,
@@ -74,7 +76,7 @@ def parse_recipe_url(
return recipe.slug
@router.get("/{recipe_slug}", response_model=Recipe)
@public_router.get("/{recipe_slug}", response_model=Recipe)
def get_recipe(recipe_slug: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)):
""" Takes in a recipe slug, returns all data for a recipe """
@@ -88,10 +90,10 @@ def get_recipe(recipe_slug: str, session: Session = Depends(generate_session), i
return recipe
else:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, {"details": "unauthorized"})
raise HTTPException(status.HTTP_403_FORBIDDEN)
@router.post("/create-from-zip", dependencies=[Depends(get_current_user)])
@user_router.post("/create-from-zip")
async def create_recipe_from_zip(
session: Session = Depends(generate_session),
temp_path=Depends(temporary_zip_path),
@@ -121,7 +123,7 @@ async def create_recipe_from_zip(
return recipe
@router.get("/{recipe_slug}/zip")
@public_router.get("/{recipe_slug}/zip")
async def get_recipe_as_zip(
recipe_slug: str, session: Session = Depends(generate_session), temp_path=Depends(temporary_zip_path)
):
@@ -139,7 +141,7 @@ async def get_recipe_as_zip(
return FileResponse(temp_path, filename=f"{recipe_slug}.zip")
@router.put("/{recipe_slug}", dependencies=[Depends(get_current_user)])
@user_router.put("/{recipe_slug}")
def update_recipe(
recipe_slug: str,
data: Recipe,
@@ -154,7 +156,7 @@ def update_recipe(
return recipe
@router.patch("/{recipe_slug}", dependencies=[Depends(get_current_user)])
@user_router.patch("/{recipe_slug}")
def patch_recipe(
recipe_slug: str,
data: Recipe,
@@ -171,7 +173,7 @@ def patch_recipe(
return recipe
@router.delete("/{recipe_slug}")
@user_router.delete("/{recipe_slug}")
def delete_recipe(
background_tasks: BackgroundTasks,
recipe_slug: str,
@@ -194,7 +196,7 @@ def delete_recipe(
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.put("/{recipe_slug}/image", dependencies=[Depends(get_current_user)])
@user_router.put("/{recipe_slug}/image")
def update_recipe_image(
recipe_slug: str,
image: bytes = File(...),
@@ -208,7 +210,7 @@ def update_recipe_image(
return {"image": new_version}
@router.post("/{recipe_slug}/image", dependencies=[Depends(get_current_user)])
@user_router.post("/{recipe_slug}/image")
def scrape_image_url(
recipe_slug: str,
url: RecipeURLIn,
@@ -218,7 +220,7 @@ def scrape_image_url(
scrape_image(url.url, recipe_slug)
@router.post("/{recipe_slug}/assets", response_model=RecipeAsset, dependencies=[Depends(get_current_user)])
@user_router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
def upload_recipe_asset(
recipe_slug: str,
name: str = Form(...),

View File

@@ -1,28 +1,29 @@
from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, is_logged_in
from mealie.routes.deps import is_logged_in
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.category import RecipeTagResponse, TagIn
from sqlalchemy.orm.session import Session
router = APIRouter(tags=["Recipes"])
router = APIRouter(prefix="/api/tags", tags=["Recipe Tags"])
public_router = APIRouter(prefix="/api/tags", tags=["Recipe Tags"])
user_router = UserAPIRouter(prefix="/api/tags", tags=["Recipe Tags"])
admin_router = AdminAPIRouter(prefix="/api/tags", tags=["Recipe Tags"])
@router.get("")
@public_router.get("")
async def get_all_recipe_tags(session: Session = Depends(generate_session)):
""" Returns a list of available tags in the database """
return db.tags.get_all_limit_columns(session, ["slug", "name"])
@router.get("/empty")
@public_router.get("/empty")
def get_empty_tags(session: Session = Depends(generate_session)):
""" Returns a list of tags that do not contain any recipes"""
return db.tags.get_empty(session)
@router.get("/{tag}", response_model=RecipeTagResponse)
@public_router.get("/{tag}", response_model=RecipeTagResponse)
def get_all_recipes_by_tag(
tag: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
):
@@ -36,21 +37,21 @@ def get_all_recipes_by_tag(
return tag_obj
@router.post("", dependencies=[Depends(get_current_user)])
@user_router.post("")
async def create_recipe_tag(tag: TagIn, session: Session = Depends(generate_session)):
""" Creates a Tag in the database """
return db.tags.create(session, tag.dict())
@router.put("/{tag}", response_model=RecipeTagResponse, dependencies=[Depends(get_current_user)])
@admin_router.put("/{tag}", response_model=RecipeTagResponse)
async def update_recipe_tag(tag: str, new_tag: TagIn, session: Session = Depends(generate_session)):
""" Updates an existing Tag in the database """
return db.tags.update(session, tag, new_tag.dict())
@router.delete("/{tag}", dependencies=[Depends(get_current_user)])
@admin_router.delete("/{tag}")
async def delete_recipe_tag(tag: str, session: Session = Depends(generate_session)):
"""Removes a recipe tag from the database. Deleting a
tag does not impact a recipe. The tag will be removed

26
mealie/routes/routers.py Normal file
View File

@@ -0,0 +1,26 @@
from fastapi import APIRouter, Depends
from mealie.routes.deps import get_admin_user, get_current_user
from typing import List, Optional
class AdminAPIRouter(APIRouter):
""" Router for functions to be protected behind admin authentication """
def __init__(
self,
tags: Optional[List[str]] = None,
prefix: str = "",
):
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)])
class UserAPIRouter(APIRouter):
""" Router for functions to be protected behind user authentication """
def __init__(
self,
tags: Optional[List[str]] = None,
prefix: str = "",
):
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)])

View File

@@ -1,12 +1,13 @@
from fastapi import APIRouter, Depends
from fastapi import Depends
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.shopping_list import ShoppingListIn, ShoppingListOut
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
shopping_list_router = APIRouter(prefix="/api/shopping-lists", tags=["Shopping Lists"])
shopping_list_router = UserAPIRouter(prefix="/api/shopping-lists", tags=["Shopping Lists"])
@shopping_list_router.post("", response_model=ShoppingListOut)
@@ -28,13 +29,13 @@ async def get_shopping_list(id: int, session: Session = Depends(generate_session
return db.shopping_lists.get(session, id)
@shopping_list_router.put("/{id}", dependencies=[Depends(get_current_user)], response_model=ShoppingListOut)
@shopping_list_router.put("/{id}", response_model=ShoppingListOut)
async def update_shopping_list(id: int, new_data: ShoppingListIn, session: Session = Depends(generate_session)):
""" Update Shopping List in the Database """
return db.shopping_lists.update(session, id, new_data)
@shopping_list_router.delete("/{id}", dependencies=[Depends(get_current_user)])
@shopping_list_router.delete("/{id}")
async def delete_shopping_list(id: int, session: Session = Depends(generate_session)):
""" Delete Shopping List from the Database """
return db.shopping_lists.delete(session, id)

View File

@@ -1,9 +1,10 @@
from fastapi import APIRouter
from . import all_settings, custom_pages, site_settings
from . import custom_pages, site_settings
settings_router = APIRouter()
settings_router.include_router(all_settings.router)
settings_router.include_router(custom_pages.router)
settings_router.include_router(site_settings.router)
settings_router.include_router(custom_pages.public_router)
settings_router.include_router(custom_pages.admin_router)
settings_router.include_router(site_settings.public_router)
settings_router.include_router(site_settings.admin_router)

View File

@@ -1,7 +0,0 @@
from fastapi import APIRouter
from mealie.routes.site_settings import custom_pages, site_settings
router = APIRouter()
router.include_router(custom_pages.router)
router.include_router(site_settings.router)

View File

@@ -1,46 +1,41 @@
from mealie.routes.routers import AdminAPIRouter
from typing import Union
from fastapi import APIRouter, Depends
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.settings import CustomPageBase, CustomPageOut
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/site-settings/custom-pages", tags=["Settings"])
public_router = APIRouter(prefix="/api/site-settings/custom-pages", tags=["Settings"])
admin_router = AdminAPIRouter(prefix="/api/site-settings/custom-pages", tags=["Settings"])
@router.get("")
@public_router.get("")
def get_custom_pages(session: Session = Depends(generate_session)):
""" Returns the sites custom pages """
return db.custom_pages.get_all(session)
@router.post("")
@admin_router.post("")
async def create_new_page(
new_page: CustomPageBase,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Creates a new Custom Page """
db.custom_pages.create(session, new_page.dict())
@router.put("")
async def update_multiple_pages(
pages: list[CustomPageOut],
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
@admin_router.put("")
async def update_multiple_pages(pages: list[CustomPageOut], session: Session = Depends(generate_session)):
""" Update multiple custom pages """
for page in pages:
db.custom_pages.update(session, page.id, page.dict())
@router.get("/{id}")
@public_router.get("/{id}")
async def get_single_page(
id: Union[int, str],
session: Session = Depends(generate_session),
@@ -52,23 +47,21 @@ async def get_single_page(
return db.custom_pages.get(session, id, "slug")
@router.put("/{id}")
@admin_router.put("/{id}")
async def update_single_page(
data: CustomPageOut,
id: int,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
""" Removes a custom page from the database """
return db.custom_pages.update(session, id, data.dict())
@router.delete("/{id}")
@admin_router.delete("/{id}")
async def delete_custom_page(
id: int,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Removes a custom page from the database """

View File

@@ -2,22 +2,24 @@ from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.settings import SiteSettings
from mealie.schema.user import GroupInDB, UserInDB
from mealie.utils.post_webhooks import post_webhooks
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
public_router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
admin_router = AdminAPIRouter(prefix="/api/site-settings", tags=["Settings"])
@router.get("")
@public_router.get("")
def get_main_settings(session: Session = Depends(generate_session)):
""" Returns basic site settings """
return db.settings.get(session, 1)
@router.put("", dependencies=[Depends(get_current_user)])
@admin_router.put("")
def update_settings(
data: SiteSettings,
session: Session = Depends(generate_session),
@@ -26,7 +28,7 @@ def update_settings(
db.settings.update(session, 1, data.dict())
@router.post("/webhooks/test")
@admin_router.post("/webhooks/test")
def test_webhooks(
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),

View File

@@ -1,45 +1,46 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.routing import APIRouter
from fastapi import Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.theme import SiteTheme
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api", tags=["Themes"])
user_router = UserAPIRouter(prefix="/api", tags=["Themes"])
public_router = APIRouter(prefix="/api", tags=["Themes"])
@router.get("/themes")
@public_router.get("/themes")
def get_all_themes(session: Session = Depends(generate_session)):
""" Returns all site themes """
return db.themes.get_all(session)
@router.post("/themes/create", status_code=status.HTTP_201_CREATED)
def create_theme(data: SiteTheme, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
@user_router.post("/themes/create", status_code=status.HTTP_201_CREATED)
def create_theme(data: SiteTheme, session: Session = Depends(generate_session)):
""" Creates a site color theme database entry """
db.themes.create(session, data.dict())
@router.get("/themes/{id}")
@public_router.get("/themes/{id}")
def get_single_theme(id: int, session: Session = Depends(generate_session)):
""" Returns a named theme """
return db.themes.get(session, id)
@router.put("/themes/{id}", status_code=status.HTTP_200_OK)
@user_router.put("/themes/{id}", status_code=status.HTTP_200_OK)
def update_theme(
id: int,
data: SiteTheme,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
""" Update a theme database entry """
db.themes.update(session, id, data.dict())
@router.delete("/themes/{id}", status_code=status.HTTP_200_OK)
def delete_theme(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
@user_router.delete("/themes/{id}", status_code=status.HTTP_200_OK)
def delete_theme(id: int, session: Session = Depends(generate_session)):
""" Deletes theme from the database """
try:
db.themes.delete(session, id)

View File

@@ -1,8 +1,7 @@
from fastapi import APIRouter, Depends
from mealie.routes.routers import UserAPIRouter
from mealie.core.root_logger import get_logger
from mealie.routes.deps import get_current_user
router = APIRouter(prefix="/api/foods", dependencies=[Depends(get_current_user)])
router = UserAPIRouter(prefix="/api/foods")
logger = get_logger()

View File

@@ -1,8 +1,7 @@
from fastapi import APIRouter, Depends
from mealie.routes.routers import UserAPIRouter
from mealie.core.root_logger import get_logger
from mealie.routes.deps import get_current_user
router = APIRouter(prefix="/api/units", dependencies=[Depends(get_current_user)])
router = UserAPIRouter(prefix="/api/units")
logger = get_logger()

View File

@@ -4,7 +4,11 @@ from . import api_tokens, auth, crud, sign_up
user_router = APIRouter()
user_router.include_router(auth.router)
user_router.include_router(sign_up.router)
user_router.include_router(crud.router)
user_router.include_router(auth.public_router)
user_router.include_router(auth.user_router)
user_router.include_router(sign_up.public_router)
user_router.include_router(sign_up.admin_router)
user_router.include_router(crud.public_router)
user_router.include_router(crud.user_router)
user_router.include_router(crud.admin_router)
user_router.include_router(api_tokens.router)

View File

@@ -1,15 +1,16 @@
from datetime import timedelta
from fastapi import APIRouter, HTTPException, status
from fastapi import HTTPException, status
from fastapi.param_functions import Depends
from mealie.core.security import create_access_token
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users", tags=["User API Tokens"])
router = UserAPIRouter(prefix="/api/users", tags=["User API Tokens"])
@router.post("/api-tokens", status_code=status.HTTP_201_CREATED)
@@ -53,4 +54,4 @@ async def delete_api_token(
deleted_token = db.api_tokens.delete(session, token_id)
return {"token_delete": deleted_token.name}
else:
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
raise HTTPException(status.HTTP_403_FORBIDDEN)

View File

@@ -5,15 +5,17 @@ from mealie.core import security
from mealie.core.security import authenticate_user
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import UserInDB
from mealie.services.events import create_user_event
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
public_router = APIRouter(prefix="/api/auth", tags=["Authentication"])
user_router = UserAPIRouter(prefix="/api/auth", tags=["Authentication"])
@router.post("/token/long")
@router.post("/token")
@public_router.post("/token/long")
@public_router.post("/token")
def get_token(
background_tasks: BackgroundTasks,
request: Request,
@@ -38,7 +40,7 @@ def get_token(
return {"access_token": access_token, "token_type": "bearer"}
@router.get("/refresh")
@user_router.get("/refresh")
async def refresh_token(current_user: UserInDB = Depends(get_current_user)):
""" Use a valid token to get another token"""
access_token = security.create_access_token(data=dict(sub=current_user.email))

View File

@@ -1,21 +1,34 @@
import shutil
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, UploadFile, status
from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, status
from fastapi.responses import FileResponse
from fastapi.routing import APIRouter
from mealie.core import security
from mealie.core.config import app_dirs, settings
from mealie.core.security import get_password_hash, verify_password
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.user import ChangePassword, UserBase, UserFavorites, UserIn, UserInDB, UserOut
from mealie.services.events import create_user_event
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users", tags=["Users"])
public_router = APIRouter(prefix="/api/users", tags=["Users"])
user_router = UserAPIRouter(prefix="/api/users", tags=["Users"])
admin_router = AdminAPIRouter(prefix="/api/users", tags=["Users"])
@router.post("", response_model=UserOut, status_code=201)
async def assert_user_change_allowed(
id: int,
current_user: UserInDB = Depends(get_current_user),
):
if current_user.id != id and not current_user.admin:
# only admins can edit other users
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="NOT_AN_ADMIN")
@admin_router.post("", response_model=UserOut, status_code=201)
async def create_user(
background_tasks: BackgroundTasks,
new_user: UserIn,
@@ -30,26 +43,19 @@ async def create_user(
return db.users.create(session, new_user.dict())
@router.get("", response_model=list[UserOut])
async def get_all_users(
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
if not current_user.admin:
raise HTTPException(status.HTTP_403_FORBIDDEN)
@admin_router.get("", response_model=list[UserOut])
async def get_all_users(session: Session = Depends(generate_session)):
return db.users.get_all(session)
@router.get("/self", response_model=UserOut)
@user_router.get("/self", response_model=UserOut)
async def get_logged_in_user(
current_user: UserInDB = Depends(get_current_user),
):
return current_user.dict()
@router.get("/{id}", response_model=UserOut, dependencies=[Depends(get_current_user)])
@admin_router.get("/{id}", response_model=UserOut)
async def get_user_by_id(
id: int,
session: Session = Depends(generate_session),
@@ -57,7 +63,7 @@ async def get_user_by_id(
return db.users.get(session, id)
@router.put("/{id}/reset-password", dependencies=[Depends(get_current_user)])
@user_router.put("/{id}/reset-password")
async def reset_user_password(
id: int,
session: Session = Depends(generate_session),
@@ -67,7 +73,7 @@ async def reset_user_password(
db.users.update_password(session, id, new_password)
@router.put("/{id}")
@user_router.put("/{id}")
async def update_user(
id: int,
new_data: UserBase,
@@ -75,16 +81,24 @@ async def update_user(
session: Session = Depends(generate_session),
):
token = None
if current_user.id == id or current_user.admin:
db.users.update(session, id, new_data.dict())
assert_user_change_allowed(id)
if not current_user.admin and (new_data.admin or current_user.group != new_data.group):
# prevent a regular user from doing admin tasks on themself
raise HTTPException(status.HTTP_403_FORBIDDEN)
if current_user.id == id and current_user.admin and not new_data.admin:
# prevent an admin from demoting themself
raise HTTPException(status.HTTP_403_FORBIDDEN)
db.users.update(session, id, new_data.dict())
if current_user.id == id:
access_token = security.create_access_token(data=dict(sub=new_data.email))
token = {"access_token": access_token, "token_type": "bearer"}
return token
@router.get("/{id}/image")
@public_router.get("/{id}/image")
async def get_user_image(id: str):
""" Returns a users profile picture """
user_dir = app_dirs.USER_DIR.joinpath(id)
@@ -94,13 +108,15 @@ async def get_user_image(id: str):
raise HTTPException(status.HTTP_404_NOT_FOUND)
@router.post("/{id}/image", dependencies=[Depends(get_current_user)])
@user_router.post("/{id}/image")
async def update_user_image(
id: str,
profile_image: UploadFile = File(...),
):
""" Updates a User Image """
assert_user_change_allowed(id)
extension = profile_image.filename.split(".")[-1]
app_dirs.USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
@@ -116,7 +132,7 @@ async def update_user_image(
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
@router.put("/{id}/password")
@user_router.put("/{id}/password")
async def update_password(
id: int,
password_change: ChangePassword,
@@ -125,24 +141,24 @@ async def update_password(
):
""" Resets the User Password"""
assert_user_change_allowed(id)
match_passwords = verify_password(password_change.current_password, current_user.password)
match_id = current_user.id == id
if not (match_passwords and match_id):
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
if not (match_passwords):
raise HTTPException(status.HTTP_400_BAD_REQUEST)
new_password = get_password_hash(password_change.new_password)
db.users.update_password(session, id, new_password)
@router.get("/{id}/favorites", response_model=UserFavorites)
@user_router.get("/{id}/favorites", response_model=UserFavorites)
async def get_favorites(id: str, session: Session = Depends(generate_session)):
""" Adds a Recipe to the users favorites """
""" Get user's favorite recipes """
return db.users.get(session, id, override_schema=UserFavorites)
@router.post("/{id}/favorites/{slug}")
@user_router.post("/{id}/favorites/{slug}")
async def add_favorite(
slug: str,
current_user: UserInDB = Depends(get_current_user),
@@ -150,12 +166,13 @@ async def add_favorite(
):
""" Adds a Recipe to the users favorites """
assert_user_change_allowed(id)
current_user.favorite_recipes.append(slug)
db.users.update(session, current_user.id, current_user)
@router.delete("/{id}/favorites/{slug}")
@user_router.delete("/{id}/favorites/{slug}")
async def remove_favorite(
slug: str,
current_user: UserInDB = Depends(get_current_user),
@@ -163,6 +180,7 @@ async def remove_favorite(
):
""" Adds a Recipe to the users favorites """
assert_user_change_allowed(id)
current_user.favorite_recipes = [x for x in current_user.favorite_recipes if x != slug]
db.users.update(session, current_user.id, current_user)
@@ -170,21 +188,21 @@ async def remove_favorite(
return
@router.delete("/{id}")
@admin_router.delete("/{id}")
async def delete_user(
background_tasks: BackgroundTasks,
id: int,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Removes a user from the database. Must be the current user or a super user"""
assert_user_change_allowed(id)
if id == 1:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
if current_user.id == id or current_user.admin:
try:
db.users.delete(session, id)
background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
try:
db.users.delete(session, id)
background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)

View File

@@ -4,18 +4,19 @@ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from mealie.core.security import get_password_hash
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.routes.deps import get_admin_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.sign_up import SignUpIn, SignUpOut, SignUpToken
from mealie.schema.user import UserIn, UserInDB
from mealie.services.events import create_user_event
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])
public_router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])
admin_router = AdminAPIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])
@router.get("", response_model=list[SignUpOut])
@admin_router.get("", response_model=list[SignUpOut])
async def get_all_open_sign_ups(
current_user=Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Returns a list of open sign up links """
@@ -23,18 +24,15 @@ async def get_all_open_sign_ups(
return db.sign_ups.get_all(session)
@router.post("", response_model=SignUpToken)
@admin_router.post("", response_model=SignUpToken)
async def create_user_sign_up_key(
background_tasks: BackgroundTasks,
key_data: SignUpIn,
current_user: UserInDB = Depends(get_current_user),
current_user: UserInDB = Depends(get_admin_user),
session: Session = Depends(generate_session),
):
""" Generates a Random Token that a new user can sign up with """
if not current_user.admin:
raise HTTPException(status.HTTP_403_FORBIDDEN)
sign_up = {
"token": str(uuid.uuid1().hex),
"name": key_data.name,
@@ -47,7 +45,7 @@ async def create_user_sign_up_key(
return db.sign_ups.create(session, sign_up)
@router.post("/{token}")
@public_router.post("/{token}")
async def create_user_with_token(
background_tasks: BackgroundTasks,
token: str,
@@ -59,7 +57,7 @@ async def create_user_with_token(
# Validate Token
db_entry: SignUpOut = db.sign_ups.get(session, token, limit=1)
if not db_entry:
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
raise HTTPException(status.HTTP_400_BAD_REQUEST)
# Create User
new_user.admin = db_entry.admin
@@ -73,14 +71,10 @@ async def create_user_with_token(
db.sign_ups.delete(session, token)
@router.delete("/{token}")
@admin_router.delete("/{token}")
async def delete_token(
token: str,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Removed a token from the database """
if not current_user.admin:
raise HTTPException(status.HTTP_403_FORBIDDEN)
db.sign_ups.delete(session, token)