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
+34 -18
View File
@@ -27,11 +27,15 @@ class FavoritesManager:
url: str,
provider: str,
metadata: Optional[Dict] = None,
poster_url: Optional[str] = None
poster_url: Optional[str] = None,
user_id: str = "default"
) -> Dict:
"""Add an anime to favorites"""
with Session(engine) as session:
statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id)
statement = select(FavoriteTable).where(
FavoriteTable.anime_id == anime_id,
FavoriteTable.user_id == user_id
)
existing = session.exec(statement).first()
if existing:
@@ -53,17 +57,21 @@ class FavoritesManager:
url=url,
provider=provider,
anime_metadata=metadata or {},
poster_url=poster_url
poster_url=poster_url,
user_id=user_id
)
session.add(fav)
session.commit()
session.refresh(fav)
return self._to_dict(fav)
async def remove_favorite(self, anime_id: str) -> bool:
async def remove_favorite(self, anime_id: str, user_id: str = "default") -> bool:
"""Remove an anime from favorites"""
with Session(engine) as session:
statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id)
statement = select(FavoriteTable).where(
FavoriteTable.anime_id == anime_id,
FavoriteTable.user_id == user_id
)
existing = session.exec(statement).first()
if existing:
session.delete(existing)
@@ -71,10 +79,13 @@ class FavoritesManager:
return True
return False
async def get_favorite(self, anime_id: str) -> Optional[Dict]:
async def get_favorite(self, anime_id: str, user_id: str = "default") -> Optional[Dict]:
"""Get a specific favorite by ID"""
with Session(engine) as session:
statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id)
statement = select(FavoriteTable).where(
FavoriteTable.anime_id == anime_id,
FavoriteTable.user_id == user_id
)
existing = session.exec(statement).first()
if existing:
return self._to_dict(existing)
@@ -82,6 +93,7 @@ class FavoritesManager:
async def list_favorites(
self,
user_id: str = "default",
sort_by: str = "created_at",
order: str = "desc",
filter_provider: Optional[str] = None,
@@ -89,11 +101,11 @@ class FavoritesManager:
) -> List[Dict]:
"""List all favorites with optional sorting and filtering"""
with Session(engine) as session:
statement = select(FavoriteTable)
statement = select(FavoriteTable).where(FavoriteTable.user_id == user_id)
if filter_provider:
statement = statement.where(FavoriteTable.provider == filter_provider)
# SQLite JSON filtering for genres is complex, handle it in Python
results = session.exec(statement).all()
favorites = [self._to_dict(fav) for fav in results]
@@ -123,10 +135,13 @@ class FavoritesManager:
return favorites
async def is_favorite(self, anime_id: str) -> bool:
async def is_favorite(self, anime_id: str, user_id: str = "default") -> bool:
"""Check if an anime is in favorites"""
with Session(engine) as session:
statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id)
statement = select(FavoriteTable).where(
FavoriteTable.anime_id == anime_id,
FavoriteTable.user_id == user_id
)
return session.exec(statement).first() is not None
async def toggle_favorite(
@@ -136,21 +151,22 @@ class FavoritesManager:
url: str,
provider: str,
metadata: Optional[Dict] = None,
poster_url: Optional[str] = None
poster_url: Optional[str] = None,
user_id: str = "default"
) -> Dict:
"""Toggle an anime in favorites (add if not exists, remove if exists)"""
is_fav = await self.is_favorite(anime_id)
is_fav = await self.is_favorite(anime_id, user_id=user_id)
if is_fav:
await self.remove_favorite(anime_id)
await self.remove_favorite(anime_id, user_id=user_id)
return {"action": "removed", "anime_id": anime_id}
else:
fav = await self.add_favorite(anime_id, title, url, provider, metadata, poster_url)
fav = await self.add_favorite(anime_id, title, url, provider, metadata, poster_url, user_id=user_id)
return {"action": "added", "anime_id": anime_id, "favorite": fav}
async def get_stats(self) -> Dict:
async def get_stats(self, user_id: str = "default") -> Dict:
"""Get statistics about favorites"""
favorites = await self.list_favorites()
favorites = await self.list_favorites(user_id=user_id)
total = len(favorites)
# Count by provider