""" Session Management API for P2 security features """ from datetime import datetime, timezone, timedelta from typing import List, Optional, Dict, Any from fastapi import APIRouter, Depends, HTTPException, status, Request from sqlalchemy.orm import Session from pydantic import BaseModel, Field from app.database.base import get_db from app.auth.security import get_current_user, get_admin_user from app.models.user import User from app.models.sessions import UserSession, SessionConfiguration, SessionSecurityEvent from app.utils.session_manager import SessionManager, get_session_manager from app.utils.responses import create_success_response as success_response from app.core.logging import get_logger logger = get_logger(__name__) router = APIRouter(prefix="/api/session", tags=["Session Management"]) # Pydantic schemas class SessionInfo(BaseModel): """Session information response""" session_id: str user_id: int ip_address: Optional[str] = None user_agent: Optional[str] = None device_fingerprint: Optional[str] = None country: Optional[str] = None city: Optional[str] = None is_suspicious: bool = False risk_score: int = 0 status: str created_at: datetime last_activity: datetime expires_at: datetime login_method: Optional[str] = None class Config: from_attributes = True class SessionConfigurationSchema(BaseModel): """Session configuration schema""" max_concurrent_sessions: int = Field(default=3, ge=1, le=20) session_timeout_minutes: int = Field(default=480, ge=30, le=1440) # 30 min to 24 hours idle_timeout_minutes: int = Field(default=60, ge=5, le=240) # 5 min to 4 hours require_session_renewal: bool = True renewal_interval_hours: int = Field(default=24, ge=1, le=168) # 1 hour to 1 week force_logout_on_ip_change: bool = False suspicious_activity_threshold: int = Field(default=5, ge=1, le=20) allowed_countries: Optional[List[str]] = None blocked_countries: Optional[List[str]] = None class SessionConfigurationUpdate(BaseModel): """Session configuration update request""" max_concurrent_sessions: Optional[int] = Field(None, ge=1, le=20) session_timeout_minutes: Optional[int] = Field(None, ge=30, le=1440) idle_timeout_minutes: Optional[int] = Field(None, ge=5, le=240) require_session_renewal: Optional[bool] = None renewal_interval_hours: Optional[int] = Field(None, ge=1, le=168) force_logout_on_ip_change: Optional[bool] = None suspicious_activity_threshold: Optional[int] = Field(None, ge=1, le=20) allowed_countries: Optional[List[str]] = None blocked_countries: Optional[List[str]] = None class SecurityEventInfo(BaseModel): """Security event information""" id: int event_type: str severity: str description: str ip_address: Optional[str] = None country: Optional[str] = None action_taken: Optional[str] = None resolved: bool = False timestamp: datetime class Config: from_attributes = True # Session Management Endpoints @router.get("/current", response_model=SessionInfo) async def get_current_session( request: Request, current_user: User = Depends(get_current_user), session_manager: SessionManager = Depends(get_session_manager) ): """Get current session information""" try: # Extract session ID from request session_id = request.headers.get("X-Session-ID") or request.cookies.get("session_id") if not session_id: # For JWT-based sessions, use a portion of the JWT as session identifier auth_header = request.headers.get("authorization") if auth_header and auth_header.startswith("Bearer "): session_id = auth_header[7:][:32] if not session_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No session identifier found" ) session = session_manager.validate_session(session_id, request) if not session: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Session not found or expired" ) return SessionInfo.from_orm(session) except Exception as e: logger.error(f"Error getting current session: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve session information" ) @router.get("/list", response_model=List[SessionInfo]) async def list_user_sessions( current_user: User = Depends(get_current_user), session_manager: SessionManager = Depends(get_session_manager) ): """List all active sessions for current user""" try: sessions = session_manager.get_active_sessions(current_user.id) return [SessionInfo.from_orm(session) for session in sessions] except Exception as e: logger.error(f"Error listing sessions: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve sessions" ) @router.delete("/revoke/{session_id}") async def revoke_session( session_id: str, current_user: User = Depends(get_current_user), session_manager: SessionManager = Depends(get_session_manager) ): """Revoke a specific session""" try: # Verify the session belongs to the current user session = session_manager.db.query(UserSession).filter( UserSession.session_id == session_id, UserSession.user_id == current_user.id ).first() if not session: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Session not found" ) success = session_manager.revoke_session(session_id, "user_revocation") if success: return success_response("Session revoked successfully") else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Failed to revoke session" ) except HTTPException: raise except Exception as e: logger.error(f"Error revoking session: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to revoke session" ) @router.delete("/revoke-all") async def revoke_all_sessions( current_user: User = Depends(get_current_user), session_manager: SessionManager = Depends(get_session_manager) ): """Revoke all sessions for current user""" try: count = session_manager.revoke_all_user_sessions(current_user.id, "user_revoke_all") return success_response(f"Revoked {count} sessions successfully") except Exception as e: logger.error(f"Error revoking all sessions: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to revoke sessions" ) @router.get("/configuration", response_model=SessionConfigurationSchema) async def get_session_configuration( current_user: User = Depends(get_current_user), session_manager: SessionManager = Depends(get_session_manager) ): """Get session configuration for current user""" try: config = session_manager._get_session_config(current_user) return SessionConfigurationSchema( max_concurrent_sessions=config.max_concurrent_sessions, session_timeout_minutes=config.session_timeout_minutes, idle_timeout_minutes=config.idle_timeout_minutes, require_session_renewal=config.require_session_renewal, renewal_interval_hours=config.renewal_interval_hours, force_logout_on_ip_change=config.force_logout_on_ip_change, suspicious_activity_threshold=config.suspicious_activity_threshold, allowed_countries=config.allowed_countries.split(",") if config.allowed_countries else None, blocked_countries=config.blocked_countries.split(",") if config.blocked_countries else None ) except Exception as e: logger.error(f"Error getting session configuration: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve session configuration" ) @router.put("/configuration") async def update_session_configuration( config_update: SessionConfigurationUpdate, current_user: User = Depends(get_current_user), session_manager: SessionManager = Depends(get_session_manager) ): """Update session configuration for current user""" try: config = session_manager._get_session_config(current_user) # Ensure user-specific config exists if config.user_id is None: # Create user-specific config based on global config user_config = SessionConfiguration( user_id=current_user.id, max_concurrent_sessions=config.max_concurrent_sessions, session_timeout_minutes=config.session_timeout_minutes, idle_timeout_minutes=config.idle_timeout_minutes, require_session_renewal=config.require_session_renewal, renewal_interval_hours=config.renewal_interval_hours, force_logout_on_ip_change=config.force_logout_on_ip_change, suspicious_activity_threshold=config.suspicious_activity_threshold, allowed_countries=config.allowed_countries, blocked_countries=config.blocked_countries ) session_manager.db.add(user_config) session_manager.db.flush() config = user_config # Update configuration update_data = config_update.dict(exclude_unset=True) for field, value in update_data.items(): if field in ["allowed_countries", "blocked_countries"] and value: setattr(config, field, ",".join(value)) else: setattr(config, field, value) config.updated_at = datetime.now(timezone.utc) session_manager.db.commit() return success_response("Session configuration updated successfully") except Exception as e: logger.error(f"Error updating session configuration: {str(e)}") session_manager.db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update session configuration" ) @router.get("/security-events", response_model=List[SecurityEventInfo]) async def get_security_events( limit: int = 50, resolved: Optional[bool] = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Get security events for current user""" try: query = db.query(SessionSecurityEvent).filter( SessionSecurityEvent.user_id == current_user.id ) if resolved is not None: query = query.filter(SessionSecurityEvent.resolved == resolved) events = query.order_by(SessionSecurityEvent.timestamp.desc()).limit(limit).all() return [SecurityEventInfo.from_orm(event) for event in events] except Exception as e: logger.error(f"Error getting security events: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve security events" ) @router.get("/statistics") async def get_session_statistics( current_user: User = Depends(get_current_user), session_manager: SessionManager = Depends(get_session_manager) ): """Get session statistics for current user""" try: stats = session_manager.get_session_statistics(current_user.id) return success_response(data=stats) except Exception as e: logger.error(f"Error getting session statistics: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve session statistics" ) # Admin endpoints @router.get("/admin/sessions", response_model=List[SessionInfo]) async def admin_list_all_sessions( user_id: Optional[int] = None, limit: int = 100, admin_user: User = Depends(get_admin_user), session_manager: SessionManager = Depends(get_session_manager) ): """Admin: List sessions for all users or specific user""" try: if user_id: sessions = session_manager.get_active_sessions(user_id) else: sessions = session_manager.db.query(UserSession).filter( UserSession.status == "active" ).order_by(UserSession.last_activity.desc()).limit(limit).all() return [SessionInfo.from_orm(session) for session in sessions] except Exception as e: logger.error(f"Error getting admin sessions: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve sessions" ) @router.delete("/admin/revoke/{session_id}") async def admin_revoke_session( session_id: str, reason: str = "admin_revocation", admin_user: User = Depends(get_admin_user), session_manager: SessionManager = Depends(get_session_manager) ): """Admin: Revoke any session""" try: success = session_manager.revoke_session(session_id, f"admin_revocation: {reason}") if success: return success_response(f"Session {session_id} revoked successfully") else: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Session not found" ) except HTTPException: raise except Exception as e: logger.error(f"Error admin revoking session: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to revoke session" ) @router.delete("/admin/revoke-user/{user_id}") async def admin_revoke_user_sessions( user_id: int, reason: str = "admin_action", admin_user: User = Depends(get_admin_user), session_manager: SessionManager = Depends(get_session_manager) ): """Admin: Revoke all sessions for a specific user""" try: count = session_manager.revoke_all_user_sessions(user_id, f"admin_action: {reason}") return success_response(f"Revoked {count} sessions for user {user_id}") except Exception as e: logger.error(f"Error admin revoking user sessions: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to revoke user sessions" ) @router.get("/admin/global-configuration", response_model=SessionConfigurationSchema) async def admin_get_global_configuration( admin_user: User = Depends(get_admin_user), db: Session = Depends(get_db) ): """Admin: Get global session configuration""" try: config = db.query(SessionConfiguration).filter( SessionConfiguration.user_id.is_(None) ).first() if not config: # Create default global config config = SessionConfiguration() db.add(config) db.commit() return SessionConfigurationSchema( max_concurrent_sessions=config.max_concurrent_sessions, session_timeout_minutes=config.session_timeout_minutes, idle_timeout_minutes=config.idle_timeout_minutes, require_session_renewal=config.require_session_renewal, renewal_interval_hours=config.renewal_interval_hours, force_logout_on_ip_change=config.force_logout_on_ip_change, suspicious_activity_threshold=config.suspicious_activity_threshold, allowed_countries=config.allowed_countries.split(",") if config.allowed_countries else None, blocked_countries=config.blocked_countries.split(",") if config.blocked_countries else None ) except Exception as e: logger.error(f"Error getting global session configuration: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve global session configuration" ) @router.put("/admin/global-configuration") async def admin_update_global_configuration( config_update: SessionConfigurationUpdate, admin_user: User = Depends(get_admin_user), db: Session = Depends(get_db) ): """Admin: Update global session configuration""" try: config = db.query(SessionConfiguration).filter( SessionConfiguration.user_id.is_(None) ).first() if not config: config = SessionConfiguration() db.add(config) db.flush() # Update configuration update_data = config_update.dict(exclude_unset=True) for field, value in update_data.items(): if field in ["allowed_countries", "blocked_countries"] and value: setattr(config, field, ",".join(value)) else: setattr(config, field, value) config.updated_at = datetime.now(timezone.utc) db.commit() return success_response("Global session configuration updated successfully") except Exception as e: logger.error(f"Error updating global session configuration: {str(e)}") db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update global session configuration" ) @router.get("/admin/statistics") async def admin_get_global_statistics( admin_user: User = Depends(get_admin_user), session_manager: SessionManager = Depends(get_session_manager) ): """Admin: Get global session statistics""" try: stats = session_manager.get_session_statistics() return success_response(data=stats) except Exception as e: logger.error(f"Error getting global session statistics: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve global session statistics" )