from typing import Any from app.auth.utils import revoke_token, is_token_revoked from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.ext.asyncio import AsyncSession from app.auth.security import authenticate_user, create_access_token, create_refresh_token, decode_token from app.core.database import get_db from app.models.user import User from app.schemas.token import TokenResponse, TokenPayload, RefreshToken from app.schemas.user import UserResponse router = APIRouter() oauth2_scheme = OAuth2PasswordRequestForm # Existing: User Login Endpoint @router.post( "/auth/login", response_model=TokenResponse, summary="Authenticate user and provide tokens" ) async def login( form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db) ) -> Any: """ Authenticate a user with their credentials and return an access and refresh token. """ user = await authenticate_user(email=form_data.username, password=form_data.password, db=db) if not user: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials.") # Generate access and refresh tokens access_token = create_access_token({"sub": str(user.id), "type": "access"}) refresh_token = create_refresh_token({"sub": str(user.id), "type": "refresh"}) return TokenResponse( access_token=access_token, refresh_token=refresh_token, token_type="bearer", expires_in=1800, # Example: 30 minutes for access token user_id=str(user.id), ) # New: Logout Endpoint (Revoke Token) @router.post( "/auth/logout", summary="Revoke the current token", response_model=dict, status_code=status.HTTP_200_OK ) async def logout( token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db), current_user: User = Depends( lambda token=Depends(oauth2_scheme), db=Depends(get_db): decode_token(token, db=db)) ): """ Logout the user by revoking the current token. """ # Decode the token and revoke it payload: TokenPayload = await decode_token(token, db=db) await revoke_token(payload.jti, payload.type, payload.sub, db) return {"message": "Successfully logged out."} # New: Bulk Logout (Revoke All of a User's Tokens) @router.post( "/auth/logout-all", summary="Revoke all active tokens for the user", response_model=dict, status_code=status.HTTP_200_OK ) async def logout_all( db: AsyncSession = Depends(get_db), current_user: User = Depends( lambda token=Depends(oauth2_scheme), db=Depends(get_db): decode_token(token, db=db)) ): """ Revoke all tokens for the current user, effectively logging them out across all devices. """ await db.execute("DELETE FROM revoked_tokens WHERE user_id = :user_id", {"user_id": str(current_user.id)}) await db.commit() return {"message": "Logged out from all devices."} # Updated: Refresh Token Endpoint @router.post( "/auth/refresh-token", response_model=TokenResponse, summary="Generate a new access token using a refresh token" ) async def refresh_token( refresh_token: RefreshToken, db: AsyncSession = Depends(get_db) ) -> TokenResponse: """ Refresh the user's access token using their refresh token while ensuring it has not been revoked. """ payload: TokenPayload = await decode_token(refresh_token.refresh_token, required_type="refresh", db=db) if await is_token_revoked(payload.jti, db): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Token has been revoked.") # Generate a new access token with the user's info new_access_token = create_access_token({"sub": payload.sub, "type": "access"}) return TokenResponse( access_token=new_access_token, refresh_token=refresh_token.refresh_token, # Reuse existing refresh token expires_in=1800, # Example: 30 minutes expiry for access token token_type="bearer", user_id=payload.sub, ) # Existing: Get Current User Endpoint @router.get( "/auth/me", response_model=UserResponse, summary="Get user details from the token" ) async def read_users_me( current_user: User = Depends( lambda token=Depends(oauth2_scheme), db=Depends(get_db): decode_token(token, db=db)) ) -> UserResponse: """ Retrieves the details of the currently authenticated user. """ return current_user