Spaces:
Running
Running
File size: 3,880 Bytes
09801ca | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | """
Auth API Endpoints — /api/v2/auth/*
Production-grade authentication with token rotation and session management.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.services.auth_service import AuthService
from app.core.permissions import get_current_user, AuthenticatedUser
from app.schemas.auth import (
SignUpRequest,
LoginRequest,
RefreshTokenRequest,
MagicLinkRequest,
ChangePasswordRequest,
AuthResponse,
TokenPair,
SessionInfo,
)
from app.schemas.common import MessageResponse
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/auth", tags=["Authentication"])
def _get_client_info(request: Request) -> tuple[str | None, str | None]:
"""Extract IP and user agent from request."""
ip = request.client.host if request.client else None
ua = request.headers.get("User-Agent")
return ip, ua
@router.post("/signup", response_model=AuthResponse, status_code=status.HTTP_201_CREATED)
async def signup(
request: Request,
body: SignUpRequest,
db: AsyncSession = Depends(get_db),
):
"""Register a new user account."""
ip, ua = _get_client_info(request)
service = AuthService(db)
try:
return await service.register(body, ip_address=ip, user_agent=ua)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post("/login", response_model=AuthResponse)
async def login(
request: Request,
body: LoginRequest,
db: AsyncSession = Depends(get_db),
):
"""Authenticate and receive access + refresh tokens."""
ip, ua = _get_client_info(request)
service = AuthService(db)
try:
return await service.login(body, ip_address=ip, user_agent=ua)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
@router.post("/refresh", response_model=TokenPair)
async def refresh_tokens(
request: Request,
body: RefreshTokenRequest,
db: AsyncSession = Depends(get_db),
):
"""Rotate refresh token and get a new access + refresh token pair."""
ip, _ = _get_client_info(request)
service = AuthService(db)
try:
return await service.refresh_tokens(body.refresh_token, ip_address=ip)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
@router.post("/logout", response_model=MessageResponse)
async def logout(
user: AuthenticatedUser = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Revoke the current session."""
service = AuthService(db)
await service.logout(user.id)
return MessageResponse(message="Logged out successfully")
@router.post("/logout-all", response_model=MessageResponse)
async def logout_all(
user: AuthenticatedUser = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Revoke all sessions — logout from all devices."""
service = AuthService(db)
await service.logout_all(user.id)
return MessageResponse(message="All sessions revoked")
@router.get("/sessions", response_model=list[SessionInfo])
async def list_sessions(
user: AuthenticatedUser = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""List all active sessions for the current user."""
service = AuthService(db)
return await service.get_active_sessions(user.id)
@router.get("/me", response_model=dict)
async def get_current_user_info(
user: AuthenticatedUser = Depends(get_current_user),
):
"""Get current authenticated user info from token."""
return {
"id": user.id,
"email": user.email,
"roles": user.roles,
"highest_role": user.highest_role,
}
|