Files
app-service/backend/app/api/v1/sessions.py
matteoscrugli 8c4a555b88 Add comprehensive backend features and mobile UI improvements
Backend:
- Add 2FA authentication with TOTP support
- Add API keys management system
- Add audit logging for security events
- Add file upload/management system
- Add notifications system with preferences
- Add session management
- Add webhooks integration
- Add analytics endpoints
- Add export functionality
- Add password policy enforcement
- Add new database migrations for core tables

Frontend:
- Add module position system (top/bottom sidebar sections)
- Add search and notifications module configuration tabs
- Add mobile logo replacing hamburger menu
- Center page title absolutely when no tabs present
- Align sidebar footer toggles with navigation items
- Add lighter icon color in dark theme for mobile
- Add API keys management page
- Add notifications page with context
- Add admin analytics and audit logs pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 22:27:32 +01:00

260 lines
7.7 KiB
Python

"""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}