Add tests for authentication and utilities, and update dependencies
- Created a new test package for services and added tests for AuthService. - Implemented tests for user registration, login, and token creation. - Added a new test package for utilities and included tests for password and JWT utilities. - Updated `uv.lock` to include new dependencies: bcrypt, email-validator, pyjwt, and pytest-asyncio.
This commit is contained in:
198
app/api/v1/auth.py
Normal file
198
app/api/v1/auth.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""Authentication endpoints."""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Cookie, Depends, HTTPException, Response, status
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.dependencies import get_auth_service, get_current_active_user
|
||||
from app.core.logging import get_logger
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import (
|
||||
UserLoginRequest,
|
||||
UserRegisterRequest,
|
||||
UserResponse,
|
||||
)
|
||||
from app.services.auth import AuthService
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/register",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def register(
|
||||
request: UserRegisterRequest,
|
||||
response: Response,
|
||||
auth_service: Annotated[AuthService, Depends(get_auth_service)],
|
||||
) -> UserResponse:
|
||||
"""Register a new user account."""
|
||||
try:
|
||||
auth_response = await auth_service.register(request)
|
||||
|
||||
# Create and store refresh token - need to get User object from service
|
||||
user = await auth_service.get_current_user(auth_response.user.id)
|
||||
refresh_token = await auth_service.create_and_store_refresh_token(user)
|
||||
|
||||
# Set HTTP-only cookies for both tokens
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=auth_response.token.access_token,
|
||||
max_age=auth_response.token.expires_in,
|
||||
httponly=True,
|
||||
secure=settings.COOKIE_SECURE,
|
||||
samesite=settings.COOKIE_SAMESITE,
|
||||
)
|
||||
response.set_cookie(
|
||||
key="refresh_token",
|
||||
value=refresh_token,
|
||||
max_age=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60, # Convert days to seconds
|
||||
httponly=True,
|
||||
secure=settings.COOKIE_SECURE,
|
||||
samesite=settings.COOKIE_SAMESITE,
|
||||
)
|
||||
|
||||
# Return only user data, tokens are now in cookies
|
||||
return auth_response.user
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("Registration failed for email: %s", request.email)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Registration failed",
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(
|
||||
request: UserLoginRequest,
|
||||
response: Response,
|
||||
auth_service: Annotated[AuthService, Depends(get_auth_service)],
|
||||
) -> UserResponse:
|
||||
"""Authenticate a user and return access token."""
|
||||
try:
|
||||
auth_response = await auth_service.login(request)
|
||||
|
||||
# Create and store refresh token - need to get User object from service
|
||||
user = await auth_service.get_current_user(auth_response.user.id)
|
||||
refresh_token = await auth_service.create_and_store_refresh_token(user)
|
||||
|
||||
# Set HTTP-only cookies for both tokens
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=auth_response.token.access_token,
|
||||
max_age=auth_response.token.expires_in,
|
||||
httponly=True,
|
||||
secure=settings.COOKIE_SECURE,
|
||||
samesite=settings.COOKIE_SAMESITE,
|
||||
)
|
||||
response.set_cookie(
|
||||
key="refresh_token",
|
||||
value=refresh_token,
|
||||
max_age=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60, # Convert days to seconds
|
||||
httponly=True,
|
||||
secure=settings.COOKIE_SECURE,
|
||||
samesite=settings.COOKIE_SAMESITE,
|
||||
)
|
||||
|
||||
# Return only user data, tokens are now in cookies
|
||||
return auth_response.user
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("Login failed for email: %s", request.email)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Login failed",
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def get_current_user_info(
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
auth_service: Annotated[AuthService, Depends(get_auth_service)],
|
||||
) -> UserResponse:
|
||||
"""Get current user information."""
|
||||
try:
|
||||
return await auth_service.create_user_response(current_user)
|
||||
except Exception as e:
|
||||
logger.exception("Failed to get current user info")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve user information",
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
async def refresh_token(
|
||||
response: Response,
|
||||
refresh_token: Annotated[str | None, Cookie()],
|
||||
auth_service: Annotated[AuthService, Depends(get_auth_service)],
|
||||
) -> dict[str, str]:
|
||||
"""Refresh access token using refresh token."""
|
||||
try:
|
||||
if not refresh_token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="No refresh token provided",
|
||||
)
|
||||
|
||||
# Get new access token
|
||||
token_response = await auth_service.refresh_access_token(refresh_token)
|
||||
|
||||
# Set new access token cookie
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=token_response.access_token,
|
||||
max_age=token_response.expires_in,
|
||||
httponly=True,
|
||||
secure=settings.COOKIE_SECURE,
|
||||
samesite=settings.COOKIE_SAMESITE,
|
||||
)
|
||||
|
||||
return {"message": "Token refreshed successfully"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("Token refresh failed")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Token refresh failed",
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(
|
||||
response: Response,
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
auth_service: Annotated[AuthService, Depends(get_auth_service)],
|
||||
) -> dict[str, str]:
|
||||
"""Logout endpoint - clears cookies and revokes refresh token."""
|
||||
try:
|
||||
# Revoke refresh token from database
|
||||
await auth_service.revoke_refresh_token(current_user)
|
||||
|
||||
# Clear both cookies
|
||||
response.delete_cookie(
|
||||
key="access_token",
|
||||
httponly=True,
|
||||
secure=settings.COOKIE_SECURE,
|
||||
samesite=settings.COOKIE_SAMESITE,
|
||||
)
|
||||
response.delete_cookie(
|
||||
key="refresh_token",
|
||||
httponly=True,
|
||||
secure=settings.COOKIE_SECURE,
|
||||
samesite=settings.COOKIE_SAMESITE,
|
||||
)
|
||||
|
||||
return {"message": "Successfully logged out"}
|
||||
except Exception as e:
|
||||
logger.exception("Logout failed")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Logout failed",
|
||||
) from e
|
||||
Reference in New Issue
Block a user