Replace all legacy dotted permission codes (vms.view, proxmox.manage, ...) in PermissionChecker/has_permission across app/api with the new resource:action format (vm:view, server:manage, ...), per LEGACY_PERMISSION_MAP. All 33 codes used by the API are now registered in the permission registry. Also switch the /api/auth/me admin branch to get_all_permissions_v2 so the endpoint emits permissions in the new format only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
208 lines
6.7 KiB
Python
208 lines
6.7 KiB
Python
"""
|
|
Logs API endpoints
|
|
View and manage audit logs
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional, List
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel
|
|
|
|
from ..db import get_db
|
|
from ..auth import get_current_user, PermissionChecker
|
|
from ..models import User, AuditLog
|
|
from ..logging_service import LoggingService
|
|
from ..i18n import I18nService
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# ==================== Schemas ====================
|
|
|
|
class LogResponse(BaseModel):
|
|
id: int
|
|
level: str
|
|
category: str
|
|
action: str
|
|
message: str
|
|
username: Optional[str] = None
|
|
ip_address: Optional[str] = None
|
|
user_agent: Optional[str] = None
|
|
resource_type: Optional[str] = None
|
|
resource_id: Optional[str] = None
|
|
resource_name: Optional[str] = None
|
|
details: Optional[dict] = None
|
|
request_method: Optional[str] = None
|
|
request_path: Optional[str] = None
|
|
response_status: Optional[int] = None
|
|
duration_ms: Optional[int] = None
|
|
success: bool
|
|
error_message: Optional[str] = None
|
|
created_at: datetime
|
|
# Enhanced logging fields
|
|
request_id: Optional[str] = None
|
|
session_id: Optional[str] = None
|
|
geo_location: Optional[str] = None
|
|
server_id: Optional[int] = None
|
|
server_name: Optional[str] = None
|
|
node_name: Optional[str] = None
|
|
request_body: Optional[dict] = None
|
|
response_body: Optional[dict] = None
|
|
query_params: Optional[str] = None
|
|
error_traceback: Optional[str] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class LogsListResponse(BaseModel):
|
|
logs: List[LogResponse]
|
|
total: int
|
|
page: int
|
|
limit: int
|
|
pages: int
|
|
|
|
|
|
class LogStatsResponse(BaseModel):
|
|
period_hours: int
|
|
total: int
|
|
by_level: dict
|
|
by_category: dict
|
|
errors_count: int
|
|
failed_logins: int
|
|
recent_errors: List[dict]
|
|
|
|
|
|
# ==================== API Endpoints ====================
|
|
|
|
@router.get("/api/logs", response_model=LogsListResponse)
|
|
async def get_logs(
|
|
level: Optional[str] = Query(None, description="Filter by log level"),
|
|
category: Optional[str] = Query(None, description="Filter by category"),
|
|
username: Optional[str] = Query(None, description="Filter by username"),
|
|
ip: Optional[str] = Query(None, description="Filter by IP address"),
|
|
resource_type: Optional[str] = Query(None, description="Filter by resource type"),
|
|
resource_id: Optional[str] = Query(None, description="Filter by resource ID"),
|
|
date_from: Optional[str] = Query(None, description="Filter from date (YYYY-MM-DD)"),
|
|
date_to: Optional[str] = Query(None, description="Filter to date (YYYY-MM-DD)"),
|
|
search: Optional[str] = Query(None, description="Search in message"),
|
|
page: int = Query(1, ge=1, description="Page number"),
|
|
limit: int = Query(50, ge=1, le=500, description="Items per page"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(PermissionChecker("log:view"))
|
|
):
|
|
"""Get logs with filtering and pagination"""
|
|
|
|
# Parse dates
|
|
parsed_date_from = None
|
|
parsed_date_to = None
|
|
|
|
if date_from:
|
|
try:
|
|
parsed_date_from = datetime.strptime(date_from, "%Y-%m-%d")
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid date_from format. Use YYYY-MM-DD")
|
|
|
|
if date_to:
|
|
try:
|
|
parsed_date_to = datetime.strptime(date_to, "%Y-%m-%d") + timedelta(days=1) # Include the whole day
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid date_to format. Use YYYY-MM-DD")
|
|
|
|
# Calculate offset
|
|
offset = (page - 1) * limit
|
|
|
|
# Get logs
|
|
logs, total = LoggingService.get_logs(
|
|
db=db,
|
|
level=level,
|
|
category=category,
|
|
username=username,
|
|
ip_address=ip,
|
|
resource_type=resource_type,
|
|
resource_id=resource_id,
|
|
date_from=parsed_date_from,
|
|
date_to=parsed_date_to,
|
|
search=search,
|
|
limit=limit,
|
|
offset=offset
|
|
)
|
|
|
|
# Calculate total pages
|
|
pages = (total + limit - 1) // limit if total > 0 else 1
|
|
|
|
return LogsListResponse(
|
|
logs=[LogResponse.model_validate(log) for log in logs],
|
|
total=total,
|
|
page=page,
|
|
limit=limit,
|
|
pages=pages
|
|
)
|
|
|
|
|
|
@router.get("/api/stats", response_model=LogStatsResponse)
|
|
async def get_log_stats(
|
|
hours: int = Query(24, ge=1, le=720, description="Period in hours"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(PermissionChecker("log:view"))
|
|
):
|
|
"""Get log statistics for the specified period"""
|
|
stats = LoggingService.get_stats(db, hours=hours)
|
|
return LogStatsResponse(**stats)
|
|
|
|
|
|
@router.delete("/api/logs")
|
|
async def cleanup_logs(
|
|
days: int = Query(30, ge=1, le=365, description="Delete logs older than this many days"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(PermissionChecker("log:delete"))
|
|
):
|
|
"""Delete old logs (admin only)"""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(status_code=403, detail="Only admins can delete logs")
|
|
|
|
deleted = LoggingService.cleanup_old_logs(db, days=days)
|
|
|
|
# Log this action
|
|
LoggingService.log(
|
|
db=db,
|
|
level=LoggingService.WARNING,
|
|
category=LoggingService.SYSTEM,
|
|
action="logs_cleanup",
|
|
message=f"Удалено {deleted} логов старше {days} дней",
|
|
username=current_user.username
|
|
)
|
|
|
|
return {"deleted": deleted, "message": f"Удалено {deleted} логов старше {days} дней"}
|
|
|
|
|
|
@router.get("/api/levels")
|
|
async def get_log_levels(lang: str = Query("ru")):
|
|
"""Get available log levels"""
|
|
return {
|
|
"levels": [
|
|
{"value": "debug", "label": I18nService.get("log_level_debug", lang), "color": "#6c757d"},
|
|
{"value": "info", "label": I18nService.get("log_level_info", lang), "color": "#0dcaf0"},
|
|
{"value": "warning", "label": I18nService.get("log_level_warning", lang), "color": "#ffc107"},
|
|
{"value": "error", "label": I18nService.get("log_level_error", lang), "color": "#dc3545"},
|
|
{"value": "critical", "label": I18nService.get("log_level_error", lang), "color": "#dc3545"}
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/api/categories")
|
|
async def get_log_categories():
|
|
"""Get available log categories"""
|
|
return {
|
|
"categories": [
|
|
{"value": "auth", "label": "Авторизация"},
|
|
{"value": "proxmox", "label": "Proxmox"},
|
|
{"value": "ipam", "label": "IPAM"},
|
|
{"value": "system", "label": "Система"},
|
|
{"value": "api", "label": "API"},
|
|
{"value": "docker", "label": "Docker"}
|
|
]
|
|
}
|