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:
2025-12-17 22:27:32 +01:00
parent f698aa4d51
commit 8c4a555b88
76 changed files with 9751 additions and 323 deletions

View 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()