172 lines
6.6 KiB
Python
172 lines
6.6 KiB
Python
"""
|
|
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))
|
|
|