fix: sécuriser watchlist, favorites, downloads et recommendations sans auth (#15)

- router_favorites.py: toutes les routes requièrent maintenant l'auth
  - GET utilise get_optional_user + login_prompt.html pour HTMX
  - POST/DELETE/toggle requièrent get_current_user_from_token
  - Filtrage par user_id dans toutes les requêtes favorites
- router_downloads.py: GET list et GET status protégés (401 sans token)
- router_recommendations.py: GET protégé (login_prompt HTMX, 401 JSON)
- router_sonarr.py: tous les endpoints de gestion protégés
  - Webhooks restent publics (reçus de Sonarr)
- app/favorites.py: ajout du paramètre user_id à toutes les méthodes

Closes #15
This commit is contained in:
root
2026-04-02 22:20:29 +00:00
parent c0f9c0c1c4
commit 5d264d8f3b
5 changed files with 154 additions and 45 deletions
+56 -12
View File
@@ -2,24 +2,42 @@
Favorites management routes for Ohm Stream Downloader API.
"""
from fastapi import APIRouter, HTTPException
from fastapi.requests import Request
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
from fastapi.templating import Jinja2Templates
from app.favorites import get_favorites_manager
from app.models.auth import User
from app.routers.router_auth import get_current_user_from_token, get_optional_user
router = APIRouter(prefix="/api/favorites", tags=["favorites"])
templates = Jinja2Templates(directory="templates")
@router.get("")
async def list_favorites(
request: Request,
sort_by: str = "created_at",
order: str = "desc",
filter_provider: str = None,
filter_genre: str = None,
filter_provider: Optional[str] = None,
filter_genre: Optional[str] = None,
html: bool = Query(False),
current_user: Optional[User] = Depends(get_optional_user),
):
"""List all favorite anime with optional sorting and filtering"""
is_htmx = request.headers.get("HX-Request")
if current_user is None and (html or is_htmx):
return templates.TemplateResponse(
"components/login_prompt.html", {"request": request}
)
if current_user is None:
raise HTTPException(status_code=401, detail="Authentication required")
fav_manager = get_favorites_manager()
favorites = await fav_manager.list_favorites(
user_id=current_user.id,
sort_by=sort_by,
order=order,
filter_provider=filter_provider,
@@ -38,7 +56,11 @@ async def list_favorites(
@router.post("")
async def add_favorite(request: Request):
async def add_favorite(
request: Request,
response: Response,
current_user: User = Depends(get_current_user_from_token),
):
"""Add an anime to favorites"""
data = await request.json()
@@ -51,6 +73,7 @@ async def add_favorite(request: Request):
fav_manager = get_favorites_manager()
favorite = await fav_manager.add_favorite(
user_id=current_user.id,
anime_id=data["anime_id"],
title=data["title"],
url=data["url"],
@@ -59,34 +82,45 @@ async def add_favorite(request: Request):
poster_url=data.get("poster_url"),
)
response.headers["HX-Trigger"] = '{"show-toast": {"message": "' + favorite['title'] + ' ajouté aux favoris", "type": "success"}}'
return {"status": "added", "favorite": favorite}
@router.delete("/{anime_id}")
async def remove_favorite(anime_id: str):
async def remove_favorite(
anime_id: str,
response: Response,
current_user: User = Depends(get_current_user_from_token),
):
"""Remove an anime from favorites"""
fav_manager = get_favorites_manager()
removed = await fav_manager.remove_favorite(anime_id)
removed = await fav_manager.remove_favorite(anime_id, user_id=current_user.id)
if not removed:
raise HTTPException(status_code=404, detail="Favorite not found")
response.headers["HX-Trigger"] = '{"show-toast": {"message": "Favori supprimé", "type": "info"}}'
return {"status": "removed", "anime_id": anime_id}
@router.get("/stats")
async def get_favorites_stats():
async def get_favorites_stats(
current_user: User = Depends(get_current_user_from_token),
):
"""Get statistics about favorites"""
fav_manager = get_favorites_manager()
stats = await fav_manager.get_stats()
stats = await fav_manager.get_stats(user_id=current_user.id)
return stats
@router.get("/{anime_id}")
async def get_favorite(anime_id: str):
async def get_favorite(
anime_id: str,
current_user: User = Depends(get_current_user_from_token),
):
"""Get details of a specific favorite anime"""
fav_manager = get_favorites_manager()
favorite = await fav_manager.get_favorite(anime_id)
favorite = await fav_manager.get_favorite(anime_id, user_id=current_user.id)
if not favorite:
raise HTTPException(status_code=404, detail="Favorite not found")
@@ -95,7 +129,11 @@ async def get_favorite(anime_id: str):
@router.post("/toggle")
async def toggle_favorite(request: Request):
async def toggle_favorite(
request: Request,
response: Response,
current_user: User = Depends(get_current_user_from_token),
):
"""Toggle an anime in favorites"""
data = await request.json()
@@ -108,6 +146,7 @@ async def toggle_favorite(request: Request):
fav_manager = get_favorites_manager()
result = await fav_manager.toggle_favorite(
user_id=current_user.id,
anime_id=data["anime_id"],
title=data["title"],
url=data["url"],
@@ -116,4 +155,9 @@ async def toggle_favorite(request: Request):
poster_url=data.get("poster_url"),
)
action = result.get("action", "unknown")
message = f"'{data['title']}' {'ajouté aux' if action == 'added' else 'retiré des'} favoris"
toast_type = "success" if action == "added" else "info"
response.headers["HX-Trigger"] = f'{{"show-toast": {{"message": "{message}", "type": "{toast_type}"}}}}'
return result