feat: Phase 3 - AES encryption, SQL audit engine, audit logs (85 tests)
Phase 3.1: Instance password AES-256-GCM encryption Phase 3.2: SQL audit rule engine (10 rules) Phase 3.3: Auto-audit on ticket creation Phase 3.4: Operation audit logs + GET /audit/logs Tests: 44 -> 85 (41 new), Frontend: AuditLogsPage
This commit is contained in:
1 parent
27989274f5
commit
66b09e4d85
16 files changed
+1146
-6
No files matched your search
@@ -0,0 +1,78 @@
|
||||
"""Audit log service — centralized logging for all operations."""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.audit_log import AuditLog
|
||||
|
||||
|
||||
class AuditLogService:
|
||||
@staticmethod
|
||||
async def log(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
action: str,
|
||||
target_type: str,
|
||||
target_id: Optional[int] = None,
|
||||
detail: Optional[str] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
) -> AuditLog:
|
||||
"""Create an audit log entry."""
|
||||
entry = AuditLog(
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
detail=detail[:500] if detail else None,
|
||||
ip_address=ip_address,
|
||||
)
|
||||
db.add(entry)
|
||||
# Don't commit — let the caller's transaction handle it
|
||||
await db.flush()
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
async def list_logs(
|
||||
db: AsyncSession,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
user_id: Optional[int] = None,
|
||||
action: Optional[str] = None,
|
||||
target_type: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List audit logs with pagination and filters."""
|
||||
stmt = select(AuditLog)
|
||||
|
||||
if user_id:
|
||||
stmt = stmt.where(AuditLog.user_id == user_id)
|
||||
if action:
|
||||
stmt = stmt.where(AuditLog.action == action)
|
||||
if target_type:
|
||||
stmt = stmt.where(AuditLog.target_type == target_type)
|
||||
|
||||
total_stmt = select(func.count()).select_from(stmt.subquery())
|
||||
total = (await db.execute(total_stmt)).scalar()
|
||||
|
||||
stmt = stmt.order_by(AuditLog.created_at.desc())
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(stmt)
|
||||
logs = result.scalars().all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"action": log.action,
|
||||
"target_type": log.target_type,
|
||||
"target_id": log.target_id,
|
||||
"detail": log.detail,
|
||||
"ip_address": log.ip_address,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
}
|
||||
Reference in new issue
Block a user