66912a0b71
- Bug 1: Ajout du champ 'type' dans les dict de AnimeReleasesFetcher (get_seasonal_anime, get_scheduled_anime, get_top_anime, search_anime) et dans _get_fallback_recommendations pour que le filtre content_type fonctionne correctement - Bug 2: Déduplication par mal_id dans get_seasonal_anime() pour éviter les doublons retournés par l'API Jikan - Bug 3: Ajout de is_admin dans get_current_user_from_token(), get_optional_user(), le constructeur User du register, et la réponse /me
240 lines
6.8 KiB
Python
240 lines
6.8 KiB
Python
"""
|
|
Authentication routes for Ohm Stream Downloader API.
|
|
|
|
Endpoints:
|
|
- POST /api/auth/register - Register a new user
|
|
- POST /api/auth/login - Login user and return JWT token
|
|
- GET /api/auth/me - Get current user information
|
|
- POST /api/auth/logout - Logout user (client-side)
|
|
- POST /api/auth/refresh - Refresh access token
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
|
|
from app.auth import (
|
|
create_access_token,
|
|
user_manager,
|
|
verify_token,
|
|
)
|
|
from app.models.auth import User, UserCreate, UserLogin
|
|
|
|
security = HTTPBearer()
|
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
|
|
|
|
|
async def get_current_user_from_token(
|
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
) -> User:
|
|
"""Dependency to get current user from JWT token"""
|
|
token = credentials.credentials
|
|
username = verify_token(token)
|
|
|
|
if username is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid authentication credentials",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
user = user_manager.get_user(username)
|
|
if user is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="User not found",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
return User(
|
|
id=user.id,
|
|
username=user.username,
|
|
email=user.email,
|
|
full_name=user.full_name,
|
|
is_active=user.is_active,
|
|
is_admin=user.is_admin,
|
|
created_at=user.created_at,
|
|
last_login=user.last_login,
|
|
)
|
|
|
|
|
|
async def get_optional_user(
|
|
credentials: Optional[HTTPAuthorizationCredentials] = Depends(
|
|
HTTPBearer(auto_error=False)
|
|
),
|
|
) -> Optional[User]:
|
|
if credentials is None:
|
|
return None
|
|
token = credentials.credentials
|
|
username = verify_token(token)
|
|
if username is None:
|
|
return None
|
|
user = user_manager.get_user(username)
|
|
if user is None:
|
|
return None
|
|
return User(
|
|
id=user.id,
|
|
username=user.username,
|
|
email=user.email,
|
|
full_name=user.full_name,
|
|
is_active=user.is_active,
|
|
is_admin=user.is_admin,
|
|
created_at=user.created_at,
|
|
last_login=user.last_login,
|
|
)
|
|
|
|
|
|
@router.post("/register")
|
|
async def register(user_data: UserCreate):
|
|
"""Register a new user"""
|
|
try:
|
|
existing_user = user_manager.get_user(user_data.username)
|
|
if existing_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Username already registered",
|
|
)
|
|
|
|
user = user_manager.create_user(
|
|
username=user_data.username,
|
|
password=user_data.password,
|
|
email=user_data.email,
|
|
full_name=user_data.full_name,
|
|
)
|
|
|
|
user_response = User(
|
|
id=user.id,
|
|
username=user.username,
|
|
email=user.email,
|
|
full_name=user.full_name,
|
|
is_active=user.is_active,
|
|
is_admin=user.is_admin,
|
|
created_at=user.created_at,
|
|
last_login=user.last_login,
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"message": "User registered successfully",
|
|
"user": user_response,
|
|
}
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
except Exception as e:
|
|
from logging import getLogger
|
|
|
|
logger = getLogger(__name__)
|
|
logger.error(f"Error registering user: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to register user",
|
|
)
|
|
|
|
|
|
@router.post("/login")
|
|
async def login(form_data: UserLogin):
|
|
"""Login user and return JWT token"""
|
|
user = user_manager.authenticate_user(form_data.username, form_data.password)
|
|
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Incorrect username or password",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
if not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN, detail="User account is disabled"
|
|
)
|
|
|
|
access_token = create_access_token(
|
|
data={"sub": user.username}, expires_delta=timedelta(days=7)
|
|
)
|
|
|
|
return {
|
|
"access_token": access_token,
|
|
"token_type": "bearer",
|
|
"user": {
|
|
"id": user.id,
|
|
"username": user.username,
|
|
"email": user.email,
|
|
"full_name": user.full_name,
|
|
},
|
|
}
|
|
|
|
|
|
@router.get("/me")
|
|
async def get_me(current_user: User = Depends(get_current_user_from_token)):
|
|
"""Get current user information"""
|
|
return {
|
|
"user": {
|
|
"id": current_user.id,
|
|
"username": current_user.username,
|
|
"email": current_user.email,
|
|
"full_name": current_user.full_name,
|
|
"is_active": current_user.is_active,
|
|
"is_admin": current_user.is_admin,
|
|
"created_at": current_user.created_at,
|
|
"last_login": current_user.last_login,
|
|
}
|
|
}
|
|
|
|
|
|
@router.post("/logout")
|
|
async def logout():
|
|
"""Logout user (client-side only)"""
|
|
return {
|
|
"status": "success",
|
|
"message": "Logout successful. Please remove the token from client storage.",
|
|
}
|
|
|
|
|
|
@router.post("/refresh")
|
|
async def refresh_token(refresh_request: dict):
|
|
"""Refresh access token using a valid refresh token."""
|
|
from app.auth import (
|
|
verify_refresh_token,
|
|
create_access_refresh_tokens,
|
|
revoke_refresh_token,
|
|
user_manager as um,
|
|
)
|
|
|
|
refresh_token_value = refresh_request.get("refresh_token")
|
|
if not refresh_token_value:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Refresh token is required"
|
|
)
|
|
|
|
username = verify_refresh_token(refresh_token_value)
|
|
if not username:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid or expired refresh token",
|
|
)
|
|
|
|
user = um.get_user(username)
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found"
|
|
)
|
|
if not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN, detail="User account is disabled"
|
|
)
|
|
|
|
revoke_refresh_token(refresh_token_value)
|
|
|
|
access_token, new_refresh_token = create_access_refresh_tokens(
|
|
data={"sub": username}
|
|
)
|
|
|
|
return {
|
|
"access_token": access_token,
|
|
"refresh_token": new_refresh_token,
|
|
"token_type": "bearer",
|
|
}
|