feat: enable audit logging for authentication and task operations

Add audit_service.log_event() calls to track key user activities:
- auth_login: successful and failed login attempts with IP/user agent
- auth_logout: single session and all sessions logout
- task_delete: task deletion with user context
- file_upload: file upload with filename, size, and type
- admin_cleanup: manual cleanup trigger with statistics

Each event captures client IP (from X-Forwarded-For/X-Real-IP headers),
user agent, and relevant metadata for compliance and debugging.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-14 12:46:20 +08:00
parent 73112db055
commit bbd68a2162
6 changed files with 226 additions and 5 deletions

View File

@@ -405,6 +405,22 @@ async def trigger_cleanup(
f"{result['total_files_deleted']} files, {result['total_bytes_freed']} bytes"
)
# Log admin cleanup action
audit_service.log_event(
db=db,
event_type="admin_cleanup",
event_category="admin",
description=f"Manual cleanup: {result['total_files_deleted']} files, {result['total_bytes_freed']} bytes freed",
user_id=admin_user.id,
success=True,
metadata={
"files_deleted": result['total_files_deleted'],
"bytes_freed": result['total_bytes_freed'],
"users_processed": result['users_processed'],
"max_files_per_user": files_to_keep
}
)
return {
"success": True,
"message": "Cleanup completed successfully",

View File

@@ -17,6 +17,7 @@ from app.models.user import User
from app.models.session import Session as UserSession
from app.schemas.auth import LoginRequest, Token, UserResponse
from app.services.external_auth_service import external_auth_service
from app.services.audit_service import audit_service
logger = logging.getLogger(__name__)
@@ -66,6 +67,17 @@ async def login(
logger.warning(
f"External auth failed for user {login_data.username}: {error_msg}"
)
# Log failed login attempt
audit_service.log_event(
db=db,
event_type="auth_login",
event_category="authentication",
description=f"Login failed for {login_data.username}: {error_msg}",
ip_address=get_client_ip(request),
user_agent=get_user_agent(request),
success=False,
error_message=error_msg
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=error_msg or "Authentication failed",
@@ -151,6 +163,20 @@ async def login(
expires_delta=internal_token_expires
)
# Log successful login
audit_service.log_event(
db=db,
event_type="auth_login",
event_category="authentication",
description=f"User logged in successfully",
user_id=user.id,
ip_address=get_client_ip(request),
user_agent=get_user_agent(request),
resource_type="session",
resource_id=str(session.id),
success=True
)
return {
"access_token": internal_access_token,
"token_type": "bearer",
@@ -165,6 +191,7 @@ async def login(
@router.post("/logout", summary="User logout")
async def logout(
request: Request,
session_id: Optional[int] = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
@@ -174,9 +201,6 @@ async def logout(
- **session_id**: Session ID to logout (optional, logs out all if not provided)
"""
# TODO: Implement proper current_user dependency from JWT token
# For now, this is a placeholder
if session_id:
# Logout specific session
session = db.query(UserSession).filter(
@@ -188,6 +212,20 @@ async def logout(
db.delete(session)
db.commit()
logger.info(f"Logged out session {session_id} for user {current_user.email}")
# Log logout event
audit_service.log_event(
db=db,
event_type="auth_logout",
event_category="authentication",
description=f"User logged out session {session_id}",
user_id=current_user.id,
ip_address=get_client_ip(request),
user_agent=get_user_agent(request),
resource_type="session",
resource_id=str(session_id),
success=True
)
return {"message": "Logged out successfully"}
else:
raise HTTPException(
@@ -206,6 +244,19 @@ async def logout(
db.commit()
logger.info(f"Logged out all {count} sessions for user {current_user.email}")
# Log logout event
audit_service.log_event(
db=db,
event_type="auth_logout",
event_category="authentication",
description=f"User logged out all {count} sessions",
user_id=current_user.id,
ip_address=get_client_ip(request),
user_agent=get_user_agent(request),
success=True,
metadata={"sessions_count": count}
)
return {"message": f"Logged out {count} sessions"}

View File

@@ -9,7 +9,7 @@ from pathlib import Path
import shutil
import hashlib
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File, BackgroundTasks
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File, BackgroundTasks, Request
from fastapi.responses import FileResponse, StreamingResponse
from sqlalchemy.orm import Session
import json
@@ -47,6 +47,7 @@ from app.services.task_service import task_service
from app.services.file_access_service import file_access_service
from app.services.ocr_service import OCRService
from app.services.service_pool import get_service_pool, PoolConfig
from app.services.audit_service import audit_service
# Import dual-track components
try:
@@ -67,6 +68,22 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v2/tasks", tags=["Tasks"])
def get_client_ip(request: Request) -> str:
"""Extract client IP address from request"""
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
real_ip = request.headers.get("X-Real-IP")
if real_ip:
return real_ip
return request.client.host if request.client else "unknown"
def get_user_agent(request: Request) -> str:
"""Extract user agent from request"""
return request.headers.get("User-Agent", "unknown")[:500]
def process_task_ocr(
task_id: str,
task_db_id: int,
@@ -518,6 +535,7 @@ async def update_task(
@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_task(
task_id: str,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
@@ -539,6 +557,20 @@ async def delete_task(
)
logger.info(f"Deleted task {task_id} for user {current_user.email}")
# Log task deletion
audit_service.log_event(
db=db,
event_type="task_delete",
event_category="task",
description=f"Task deleted",
user_id=current_user.id,
ip_address=get_client_ip(request),
user_agent=get_user_agent(request),
resource_type="task",
resource_id=task_id,
success=True
)
return None