801e6a050b
- 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>
227 lines
6.1 KiB
Python
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"}
|