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>
229 lines
7.4 KiB
Python
229 lines
7.4 KiB
Python
"""CRUD operations for Audit Log model."""
|
|
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional, List, Any
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func, desc
|
|
|
|
from app.models.audit_log import AuditLog
|
|
from app.schemas.audit_log import AuditLogCreate, AuditLogFilter
|
|
|
|
|
|
class CRUDAuditLog:
|
|
"""CRUD operations for Audit Log model."""
|
|
|
|
def create(
|
|
self,
|
|
db: Session,
|
|
*,
|
|
obj_in: AuditLogCreate
|
|
) -> AuditLog:
|
|
"""Create a new audit log entry."""
|
|
db_obj = AuditLog(
|
|
user_id=obj_in.user_id,
|
|
username=obj_in.username,
|
|
action=obj_in.action,
|
|
resource_type=obj_in.resource_type,
|
|
resource_id=obj_in.resource_id,
|
|
details=obj_in.details,
|
|
ip_address=obj_in.ip_address,
|
|
user_agent=obj_in.user_agent,
|
|
status=obj_in.status
|
|
)
|
|
db.add(db_obj)
|
|
db.commit()
|
|
db.refresh(db_obj)
|
|
return db_obj
|
|
|
|
def log_action(
|
|
self,
|
|
db: Session,
|
|
*,
|
|
user_id: Optional[str] = None,
|
|
username: Optional[str] = None,
|
|
action: str,
|
|
resource_type: Optional[str] = None,
|
|
resource_id: Optional[str] = None,
|
|
details: Optional[dict] = None,
|
|
ip_address: Optional[str] = None,
|
|
user_agent: Optional[str] = None,
|
|
status: str = "success"
|
|
) -> AuditLog:
|
|
"""Convenience method to log an action."""
|
|
details_str = json.dumps(details) if details else None
|
|
obj_in = AuditLogCreate(
|
|
user_id=user_id,
|
|
username=username,
|
|
action=action,
|
|
resource_type=resource_type,
|
|
resource_id=resource_id,
|
|
details=details_str,
|
|
ip_address=ip_address,
|
|
user_agent=user_agent,
|
|
status=status
|
|
)
|
|
return self.create(db, obj_in=obj_in)
|
|
|
|
def get(self, db: Session, id: str) -> Optional[AuditLog]:
|
|
"""Get a single audit log entry by ID."""
|
|
return db.query(AuditLog).filter(AuditLog.id == id).first()
|
|
|
|
def get_multi(
|
|
self,
|
|
db: Session,
|
|
*,
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
filters: Optional[AuditLogFilter] = None
|
|
) -> tuple[List[AuditLog], int]:
|
|
"""Get multiple audit log entries with optional filtering."""
|
|
query = db.query(AuditLog)
|
|
|
|
if filters:
|
|
if filters.user_id:
|
|
query = query.filter(AuditLog.user_id == filters.user_id)
|
|
if filters.username:
|
|
query = query.filter(AuditLog.username.ilike(f"%{filters.username}%"))
|
|
if filters.action:
|
|
query = query.filter(AuditLog.action == filters.action)
|
|
if filters.resource_type:
|
|
query = query.filter(AuditLog.resource_type == filters.resource_type)
|
|
if filters.resource_id:
|
|
query = query.filter(AuditLog.resource_id == filters.resource_id)
|
|
if filters.status:
|
|
query = query.filter(AuditLog.status == filters.status)
|
|
if filters.start_date:
|
|
query = query.filter(AuditLog.created_at >= filters.start_date)
|
|
if filters.end_date:
|
|
query = query.filter(AuditLog.created_at <= filters.end_date)
|
|
|
|
total = query.count()
|
|
items = query.order_by(desc(AuditLog.created_at)).offset(skip).limit(limit).all()
|
|
|
|
return items, total
|
|
|
|
def get_by_user(
|
|
self,
|
|
db: Session,
|
|
*,
|
|
user_id: str,
|
|
skip: int = 0,
|
|
limit: int = 100
|
|
) -> List[AuditLog]:
|
|
"""Get audit logs for a specific user."""
|
|
return db.query(AuditLog)\
|
|
.filter(AuditLog.user_id == user_id)\
|
|
.order_by(desc(AuditLog.created_at))\
|
|
.offset(skip)\
|
|
.limit(limit)\
|
|
.all()
|
|
|
|
def get_recent(
|
|
self,
|
|
db: Session,
|
|
*,
|
|
hours: int = 24,
|
|
limit: int = 100
|
|
) -> List[AuditLog]:
|
|
"""Get recent audit logs within specified hours."""
|
|
since = datetime.utcnow() - timedelta(hours=hours)
|
|
return db.query(AuditLog)\
|
|
.filter(AuditLog.created_at >= since)\
|
|
.order_by(desc(AuditLog.created_at))\
|
|
.limit(limit)\
|
|
.all()
|
|
|
|
def get_stats(self, db: Session) -> dict[str, Any]:
|
|
"""Get audit log statistics."""
|
|
now = datetime.utcnow()
|
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
week_start = today_start - timedelta(days=today_start.weekday())
|
|
month_start = today_start.replace(day=1)
|
|
|
|
# Total entries
|
|
total = db.query(func.count(AuditLog.id)).scalar()
|
|
|
|
# Entries today
|
|
entries_today = db.query(func.count(AuditLog.id))\
|
|
.filter(AuditLog.created_at >= today_start)\
|
|
.scalar()
|
|
|
|
# Entries this week
|
|
entries_week = db.query(func.count(AuditLog.id))\
|
|
.filter(AuditLog.created_at >= week_start)\
|
|
.scalar()
|
|
|
|
# Entries this month
|
|
entries_month = db.query(func.count(AuditLog.id))\
|
|
.filter(AuditLog.created_at >= month_start)\
|
|
.scalar()
|
|
|
|
# Actions breakdown
|
|
actions_query = db.query(
|
|
AuditLog.action,
|
|
func.count(AuditLog.id).label('count')
|
|
).group_by(AuditLog.action).all()
|
|
actions_breakdown = {action: count for action, count in actions_query}
|
|
|
|
# Top users (by action count)
|
|
top_users_query = db.query(
|
|
AuditLog.user_id,
|
|
AuditLog.username,
|
|
func.count(AuditLog.id).label('count')
|
|
).filter(AuditLog.user_id.isnot(None))\
|
|
.group_by(AuditLog.user_id, AuditLog.username)\
|
|
.order_by(desc('count'))\
|
|
.limit(10)\
|
|
.all()
|
|
top_users = [
|
|
{"user_id": uid, "username": uname, "count": count}
|
|
for uid, uname, count in top_users_query
|
|
]
|
|
|
|
# Recent failures (last 24h)
|
|
recent_failures = db.query(func.count(AuditLog.id))\
|
|
.filter(AuditLog.status == "failure")\
|
|
.filter(AuditLog.created_at >= today_start - timedelta(days=1))\
|
|
.scalar()
|
|
|
|
return {
|
|
"total_entries": total or 0,
|
|
"entries_today": entries_today or 0,
|
|
"entries_this_week": entries_week or 0,
|
|
"entries_this_month": entries_month or 0,
|
|
"actions_breakdown": actions_breakdown,
|
|
"top_users": top_users,
|
|
"recent_failures": recent_failures or 0
|
|
}
|
|
|
|
def delete_old(
|
|
self,
|
|
db: Session,
|
|
*,
|
|
days: int = 90
|
|
) -> int:
|
|
"""Delete audit logs older than specified days."""
|
|
cutoff = datetime.utcnow() - timedelta(days=days)
|
|
count = db.query(AuditLog)\
|
|
.filter(AuditLog.created_at < cutoff)\
|
|
.delete()
|
|
db.commit()
|
|
return count
|
|
|
|
def get_distinct_actions(self, db: Session) -> List[str]:
|
|
"""Get list of distinct action types."""
|
|
result = db.query(AuditLog.action).distinct().all()
|
|
return [r[0] for r in result]
|
|
|
|
def get_distinct_resource_types(self, db: Session) -> List[str]:
|
|
"""Get list of distinct resource types."""
|
|
result = db.query(AuditLog.resource_type)\
|
|
.filter(AuditLog.resource_type.isnot(None))\
|
|
.distinct().all()
|
|
return [r[0] for r in result]
|
|
|
|
|
|
# Create instance
|
|
audit_log = CRUDAuditLog()
|