a89c7894cf
Backend: - FastAPI avec PostgreSQL et Redis - Authentification JWT complète - API REST pour musique, playlists, recherche - Streaming audio via yt-dlp - SQLAlchemy 2.0 async Frontend: - Flutter avec thème néon cyberpunk - State management Riverpod - Layout adaptatif desktop/mobile - Lecteur audio avec mini-player Infrastructure: - Docker Compose (PostgreSQL + Redis) - Scripts d'installation automatisés - Scripts de build pour exécutables Fichiers ajoutés: - BUILD_CLIENT_*.bat/sh: Scripts de compilation - BUILD_CLIENT_README.md: Documentation compilation - CHECK_FLUTTER.sh: Vérificateur d'environnement - requirements.txt mis à jour pour Python 3.13 - Modèles SQLAlchemy corrigés (metadata -> extra_metadata) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
179 lines
4.7 KiB
Python
179 lines
4.7 KiB
Python
"""Authentication API routes."""
|
|
from fastapi import APIRouter, HTTPException, status
|
|
|
|
from app.api.dependencies import AuthServiceDep, CurrentUser, DBSession
|
|
from app.schemas.auth import (
|
|
LoginRequest,
|
|
RefreshTokenRequest,
|
|
Token,
|
|
UserCreate,
|
|
UserResponse,
|
|
UserUpdate,
|
|
)
|
|
from app.services.auth_service import AuthService
|
|
|
|
router = APIRouter(prefix="/auth", tags=["authentication"])
|
|
|
|
|
|
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
|
async def register(
|
|
user_data: UserCreate,
|
|
auth_service: AuthServiceDep,
|
|
):
|
|
"""
|
|
Register a new user.
|
|
|
|
- **email**: Valid email address
|
|
- **username**: 3-50 characters, unique
|
|
- **password**: Min 8 characters
|
|
- **display_name**: Optional display name
|
|
"""
|
|
try:
|
|
user = await auth_service.register(
|
|
email=user_data.email,
|
|
username=user_data.username,
|
|
password=user_data.password,
|
|
display_name=user_data.display_name,
|
|
)
|
|
return UserResponse.model_validate(user)
|
|
except ValueError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=str(e),
|
|
)
|
|
|
|
|
|
@router.post("/login", response_model=Token)
|
|
async def login(
|
|
credentials: LoginRequest,
|
|
auth_service: AuthServiceDep,
|
|
):
|
|
"""
|
|
Login with email and password.
|
|
|
|
Returns access and refresh tokens.
|
|
"""
|
|
try:
|
|
user = await auth_service.login(
|
|
email=credentials.email,
|
|
password=credentials.password,
|
|
)
|
|
access_token, refresh_token = auth_service.create_tokens(user.id)
|
|
|
|
return Token(
|
|
access_token=access_token,
|
|
refresh_token=refresh_token,
|
|
expires_in=15 * 60, # 15 minutes
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail=str(e),
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
|
|
@router.post("/refresh", response_model=Token)
|
|
async def refresh_token(
|
|
token_data: RefreshTokenRequest,
|
|
auth_service: AuthServiceDep,
|
|
):
|
|
"""
|
|
Refresh access token using refresh token.
|
|
|
|
Returns new access and refresh tokens.
|
|
"""
|
|
from app.core.security import decode_token
|
|
|
|
try:
|
|
payload = decode_token(token_data.refresh_token)
|
|
user_id = payload.get("sub")
|
|
token_type = payload.get("type")
|
|
|
|
if user_id is None or token_type != "refresh":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid refresh token",
|
|
)
|
|
|
|
# Verify user still exists
|
|
user = await auth_service.get_user_by_id(user_id)
|
|
if user is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="User not found",
|
|
)
|
|
|
|
# Create new tokens
|
|
access_token, refresh_token = auth_service.create_tokens(user.id)
|
|
|
|
return Token(
|
|
access_token=access_token,
|
|
refresh_token=refresh_token,
|
|
expires_in=15 * 60,
|
|
)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid refresh token",
|
|
)
|
|
|
|
|
|
@router.get("/me", response_model=UserResponse)
|
|
async def get_current_user(
|
|
current_user: CurrentUser,
|
|
):
|
|
"""
|
|
Get current authenticated user profile.
|
|
|
|
Requires authentication.
|
|
"""
|
|
return UserResponse.model_validate(current_user)
|
|
|
|
|
|
@router.put("/me", response_model=UserResponse)
|
|
async def update_current_user(
|
|
user_data: UserUpdate,
|
|
current_user: CurrentUser,
|
|
auth_service: AuthServiceDep,
|
|
):
|
|
"""
|
|
Update current user profile.
|
|
|
|
Requires authentication.
|
|
"""
|
|
try:
|
|
updated_user = await auth_service.update_user(
|
|
user_id=current_user.id,
|
|
display_name=user_data.display_name,
|
|
avatar_url=user_data.avatar_url,
|
|
date_of_birth=user_data.date_of_birth,
|
|
country=user_data.country,
|
|
)
|
|
return UserResponse.model_validate(updated_user)
|
|
except ValueError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=str(e),
|
|
)
|
|
|
|
|
|
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def logout(
|
|
current_user: CurrentUser,
|
|
):
|
|
"""
|
|
Logout current user.
|
|
|
|
In a stateless JWT setup, this is mainly for client-side cleanup.
|
|
The token will expire automatically.
|
|
|
|
Requires authentication.
|
|
"""
|
|
# In production, you might want to:
|
|
# - Add token to blacklist (Redis)
|
|
# - Remove refresh token from database
|
|
# - Log the logout event
|
|
|
|
return None
|