""" User session management routes. """ from fastapi import APIRouter, Depends, HTTPException, Request, Response, Cookie from sqlalchemy.orm import Session from ...shared.config.database import get_db from ...shared.config.logging_config import get_logger from ...shared.config.settings import settings from ...security.middleware.auth import get_current_user from ...auth.models.user import User from ...auth.models.user_session import UserSession from ...auth.services.session_service import session_service from ...shared.utils.response_helpers import success_response from jose import jwt logger = get_logger(__name__) router = APIRouter(prefix='/sessions', tags=['sessions']) @router.get('/') async def get_my_sessions( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Get current user's active sessions.""" try: sessions = session_service.get_user_sessions( db=db, user_id=current_user.id, active_only=True ) return success_response(data={ 'sessions': [{ 'id': s.id, 'ip_address': s.ip_address, 'user_agent': s.user_agent, 'device_info': s.device_info, 'last_activity': s.last_activity.isoformat() if s.last_activity else None, 'created_at': s.created_at.isoformat() if s.created_at else None, 'expires_at': s.expires_at.isoformat() if s.expires_at else None } for s in sessions] }) except Exception as e: logger.error(f'Error getting sessions: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.delete('/{session_id}') async def revoke_session( session_id: int, request: Request, response: Response, current_user: User = Depends(get_current_user), access_token: str = Cookie(None, alias='accessToken'), db: Session = Depends(get_db) ): """Revoke a specific session.""" try: # Verify session belongs to user session = db.query(UserSession).filter( UserSession.id == session_id, UserSession.user_id == current_user.id ).first() if not session: raise HTTPException(status_code=404, detail='Session not found') # Check if this is the current session being revoked # We detect this by checking if: # 1. The session IP matches the request IP (if available) # 2. The session is the most recent active session is_current_session = False try: client_ip = request.client.host if request.client else None user_agent = request.headers.get('User-Agent', '') # Check if session matches current request characteristics if client_ip and session.ip_address == client_ip: # Also check if it's the most recent session recent_session = db.query(UserSession).filter( UserSession.user_id == current_user.id, UserSession.is_active == True ).order_by(UserSession.last_activity.desc()).first() if recent_session and recent_session.id == session_id: is_current_session = True except Exception as e: logger.warning(f'Could not determine if session is current: {str(e)}') # If we can't determine, check if it's the only active session active_sessions_count = db.query(UserSession).filter( UserSession.user_id == current_user.id, UserSession.is_active == True ).count() if active_sessions_count <= 1: is_current_session = True success = session_service.revoke_session(db=db, session_token=session.session_token) if not success: raise HTTPException(status_code=404, detail='Session not found') # If this was the current session, clear cookies and indicate logout needed if is_current_session: from ...shared.config.settings import settings samesite_value = 'strict' if settings.is_production else 'lax' # Clear access token cookie response.delete_cookie( key='accessToken', path='/', samesite=samesite_value, secure=settings.is_production ) # Clear refresh token cookie response.delete_cookie( key='refreshToken', path='/', samesite=samesite_value, secure=settings.is_production ) return success_response( message='Session revoked successfully. You have been logged out.', data={'logout_required': True} ) return success_response(message='Session revoked successfully') except HTTPException: raise except Exception as e: logger.error(f'Error revoking session: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.post('/revoke-all') async def revoke_all_sessions( request: Request, response: Response, current_user: User = Depends(get_current_user), access_token: str = Cookie(None, alias='accessToken'), db: Session = Depends(get_db) ): """Revoke all sessions for current user.""" try: count = session_service.revoke_all_user_sessions( db=db, user_id=current_user.id, exclude_token=None # Don't exclude current session, revoke all ) # Clear cookies since all sessions (including current) are revoked from ...shared.config.settings import settings samesite_value = 'strict' if settings.is_production else 'lax' # Clear access token cookie response.delete_cookie( key='accessToken', path='/', samesite=samesite_value, secure=settings.is_production ) # Clear refresh token cookie response.delete_cookie( key='refreshToken', path='/', samesite=samesite_value, secure=settings.is_production ) return success_response( data={'revoked_count': count, 'logout_required': True}, message=f'Revoked {count} session(s). You have been logged out.' ) except Exception as e: logger.error(f'Error revoking all sessions: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail=str(e))