Cybersecurity-Projects/PROJECTS/advanced/encrypted-p2p-chat/backend/app/api/auth.py

160 lines
4.3 KiB
Python

"""
©AngelaMos | 2026
auth.py
"""
import logging
from typing import Any
from fastapi import APIRouter, Depends, Request, Response, status
from slowapi import Limiter
from slowapi.util import get_remote_address
from sqlmodel.ext.asyncio.session import AsyncSession
from app.config import (
RATE_LIMIT_AUTH_BEGIN,
RATE_LIMIT_AUTH_COMPLETE,
RATE_LIMIT_REGISTER_BEGIN,
RATE_LIMIT_USER_SEARCH,
)
from app.core.dependencies import (
current_user,
issue_session,
revoke_session,
session_token_from_cookie,
)
from app.models.Base import get_session
from app.models.User import User
from app.schemas.auth import (
AuthenticationBeginRequest,
AuthenticationCompleteRequest,
RegistrationBeginRequest,
RegistrationCompleteRequest,
UserResponse,
UserSearchRequest,
UserSearchResponse,
)
from app.services.auth_service import auth_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix = "/auth", tags = ["authentication"])
limiter = Limiter(key_func = get_remote_address)
def _user_to_response(user: User) -> UserResponse:
"""
Adapt the ORM User into the API response model
"""
return UserResponse(
id = str(user.id),
username = user.username,
display_name = user.display_name,
is_active = user.is_active,
is_verified = user.is_verified,
created_at = user.created_at.isoformat(),
)
@router.post("/register/begin", status_code = status.HTTP_200_OK)
@limiter.limit(RATE_LIMIT_REGISTER_BEGIN)
async def register_begin(
request: Request,
body: RegistrationBeginRequest,
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
Begin WebAuthn passkey registration flow
"""
return await auth_service.begin_registration(session, body)
@router.post("/register/complete", status_code = status.HTTP_201_CREATED)
@limiter.limit(RATE_LIMIT_AUTH_COMPLETE)
async def register_complete(
request: Request,
body: RegistrationCompleteRequest,
response: Response,
session: AsyncSession = Depends(get_session),
) -> UserResponse:
"""
Complete WebAuthn passkey registration and start a session
"""
user = await auth_service.complete_registration(session, body, body.username)
await issue_session(response, user)
return _user_to_response(user)
@router.post("/authenticate/begin", status_code = status.HTTP_200_OK)
@limiter.limit(RATE_LIMIT_AUTH_BEGIN)
async def authenticate_begin(
request: Request,
body: AuthenticationBeginRequest,
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
Begin WebAuthn passkey authentication flow
"""
return await auth_service.begin_authentication(session, body)
@router.post("/authenticate/complete", status_code = status.HTTP_200_OK)
@limiter.limit(RATE_LIMIT_AUTH_COMPLETE)
async def authenticate_complete(
request: Request,
body: AuthenticationCompleteRequest,
response: Response,
session: AsyncSession = Depends(get_session),
) -> UserResponse:
"""
Complete WebAuthn passkey authentication and start a session
"""
user = await auth_service.complete_authentication(session, body)
await issue_session(response, user)
return _user_to_response(user)
@router.get("/me", status_code = status.HTTP_200_OK)
async def me(user: User = Depends(current_user)) -> UserResponse:
"""
Return the user bound to the current session
"""
return _user_to_response(user)
@router.post("/logout", status_code = status.HTTP_204_NO_CONTENT)
async def logout(
response: Response,
token: str | None = Depends(session_token_from_cookie),
) -> None:
"""
Invalidate the current session and clear its cookie
"""
await revoke_session(response, token)
@router.post("/users/search", status_code = status.HTTP_200_OK)
@limiter.limit(RATE_LIMIT_USER_SEARCH)
async def search_users(
request: Request,
body: UserSearchRequest,
user: User = Depends(current_user),
session: AsyncSession = Depends(get_session),
) -> UserSearchResponse:
"""
Search for users by username or display name
"""
users = await auth_service.search_users(
session,
body.query,
body.limit,
exclude_user_id = user.id,
)
return UserSearchResponse(
users = [_user_to_response(u) for u in users]
)