"""User Session management endpoints.""" from typing import Any from fastapi import APIRouter, Depends, HTTPException, status, Request from fastapi.security import HTTPAuthorizationCredentials from sqlalchemy.orm import Session from app.dependencies import get_db, get_current_user, security from app.models.user import User from app import crud from app.schemas.session import ( Session as SessionSchema, SessionList, SessionRevokeRequest ) router = APIRouter() def get_client_ip(request: Request) -> str: """Extract client IP from request.""" forwarded = request.headers.get("X-Forwarded-For") if forwarded: return forwarded.split(",")[0].strip() return request.client.host if request.client else "unknown" def serialize_session(db_obj, current_token_hash: str = None) -> dict: """Serialize session for response.""" from app.crud.session import hash_token is_current = False if current_token_hash: is_current = db_obj.token_hash == current_token_hash return { "id": db_obj.id, "user_id": db_obj.user_id, "device_name": db_obj.device_name, "device_type": db_obj.device_type, "browser": db_obj.browser, "os": db_obj.os, "ip_address": db_obj.ip_address, "location": db_obj.location, "is_active": db_obj.is_active, "is_current": is_current or db_obj.is_current, "created_at": db_obj.created_at, "last_active_at": db_obj.last_active_at, "expires_at": db_obj.expires_at } @router.get("", response_model=SessionList) def get_sessions( request: Request, credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> Any: """ Get all active sessions for the current user. """ from app.crud.session import hash_token current_token = credentials.credentials current_token_hash = hash_token(current_token) sessions = crud.session.get_multi_by_user(db, user_id=current_user.id, active_only=True) total = len(sessions) active_count = sum(1 for s in sessions if s.is_active) return { "items": [serialize_session(s, current_token_hash) for s in sessions], "total": total, "active_count": active_count } @router.get("/all", response_model=SessionList) def get_all_sessions( request: Request, credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> Any: """ Get all sessions (including inactive) for the current user. """ from app.crud.session import hash_token current_token = credentials.credentials current_token_hash = hash_token(current_token) sessions = crud.session.get_multi_by_user(db, user_id=current_user.id, active_only=False) total = len(sessions) active_count = sum(1 for s in sessions if s.is_active) return { "items": [serialize_session(s, current_token_hash) for s in sessions], "total": total, "active_count": active_count } @router.get("/current", response_model=SessionSchema) def get_current_session( request: Request, credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> Any: """ Get the current session. """ from app.crud.session import hash_token current_token = credentials.credentials session = crud.session.get_by_token(db, current_token) if not session: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Current session not found" ) return serialize_session(session, hash_token(current_token)) @router.post("/{session_id}/revoke", response_model=SessionSchema) def revoke_session( request: Request, session_id: str, credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> Any: """ Revoke a specific session. """ from app.crud.session import hash_token current_token = credentials.credentials current_token_hash = hash_token(current_token) # Get the session to check if it's the current one target_session = crud.session.get(db, id=session_id) if not target_session or target_session.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Session not found" ) # Don't allow revoking the current session through this endpoint if target_session.token_hash == current_token_hash: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot revoke current session. Use logout instead." ) session = crud.session.revoke(db, id=session_id, user_id=current_user.id) # Log the action crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="session_revoke", resource_type="session", resource_id=session_id, details={"device": session.device_name}, ip_address=get_client_ip(request), user_agent=request.headers.get("User-Agent", "")[:500], status="success" ) return serialize_session(session, current_token_hash) @router.post("/revoke-all") def revoke_all_sessions( request: Request, credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> Any: """ Revoke all sessions except the current one. """ from app.crud.session import hash_token current_token = credentials.credentials current_session = crud.session.get_by_token(db, current_token) except_id = current_session.id if current_session else None count = crud.session.revoke_all_except( db, user_id=current_user.id, except_session_id=except_id ) # Log the action crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="sessions_revoke_all", resource_type="session", details={"revoked_count": count}, ip_address=get_client_ip(request), user_agent=request.headers.get("User-Agent", "")[:500], status="success" ) return {"revoked": count} @router.post("/revoke-multiple") def revoke_multiple_sessions( request: Request, revoke_request: SessionRevokeRequest, credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> Any: """ Revoke multiple sessions. """ from app.crud.session import hash_token current_token = credentials.credentials current_session = crud.session.get_by_token(db, current_token) # Filter out current session if included session_ids = [ sid for sid in revoke_request.session_ids if not current_session or sid != current_session.id ] count = crud.session.revoke_multiple( db, user_id=current_user.id, session_ids=session_ids ) # Log the action crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="sessions_revoke_multiple", resource_type="session", details={"revoked_count": count, "requested_ids": len(revoke_request.session_ids)}, ip_address=get_client_ip(request), user_agent=request.headers.get("User-Agent", "")[:500], status="success" ) return {"revoked": count}