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>
This commit is contained in:
228
backend/app/crud/audit_log.py
Normal file
228
backend/app/crud/audit_log.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user