Files
root 3dc5dd8fe9
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled
feat: fix auth, provider health checks, search, and redesign UI
- Fix register/login: dict-style access on UserTable ORM objects
- Fix HTMX auth: inject JWT token in all HTMX request headers
- Fix FS7 search: use DLE AJAX endpoint /engine/ajax/search.php
- Fix ZT search: use ?p=series&search=QUERY (not DLE format)
- Fix provider health: load hardcoded providers + domain manager
- Add self.id to all anime/series providers
- Redesign homepage: Netflix-style horizontal scroll cards (.hc)
- Redesign search results: grouped by title, poster + synopsis + 3 buttons
- Add Télécharger dropdown: season download + episode picker
- Fix navbar CSS: restore .tabs flex layout, remove orphan rules
- Fix HTMX spinner: remove inline display:none, use CSS indicator
- Add AGENTS.md files across project for developer documentation
2026-03-28 00:14:31 +00:00

236 lines
6.6 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,
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,
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,
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,
"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",
}