Files
root 801e6a050b prod: UI Optimisée mise en production
- Documentation archivée et réorganisée
- Backend: Ajout tests, migrations, library service, rate limiting
- Frontend: Suppression Flutter, focus sur interface web HTML/JS
- Tailwind CSS ajouté pour le style
- Améliorations UX et corrections bugs

Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-20 09:56:39 +00:00

227 lines
6.1 KiB
Python

"""Authentication API routes."""
from fastapi import APIRouter, HTTPException, status
from app.api.dependencies import AuthServiceDep, CurrentUser, DBSession
from app.schemas.auth import (
ChangePasswordRequest,
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
@router.post("/change-password")
async def change_password(
password_data: ChangePasswordRequest,
current_user: CurrentUser,
auth_service: AuthServiceDep,
db: DBSession,
):
"""
Change user password.
Requires authentication and current password verification.
- **password_data**: Object containing old_password and new_password
"""
from app.core.security import verify_password, hash_password
# Verify old password
if not verify_password(password_data.old_password, current_user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Current password is incorrect"
)
# Validate new password
if len(password_data.new_password) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="New password must be at least 8 characters"
)
if password_data.old_password == password_data.new_password:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="New password must be different from current password"
)
# Hash new password
new_password_hash = hash_password(password_data.new_password)
# Update password
current_user.password_hash = new_password_hash
await db.commit()
await db.refresh(current_user)
return {"message": "Password changed successfully"}