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>
281 lines
8.6 KiB
Python
281 lines
8.6 KiB
Python
"""Dashboard analytics endpoints."""
|
|
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
from typing import Any, Optional
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func, desc
|
|
|
|
from app.dependencies import get_db, get_current_superuser
|
|
from app.models.user import User
|
|
from app.models.audit_log import AuditLog
|
|
from app.models.session import UserSession
|
|
from app.models.notification import Notification
|
|
from app.models.api_key import APIKey
|
|
from app import crud
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/overview")
|
|
def get_analytics_overview(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
) -> Any:
|
|
"""
|
|
Get dashboard overview analytics.
|
|
Returns summary statistics for the admin dashboard.
|
|
"""
|
|
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)
|
|
|
|
# User statistics
|
|
total_users = db.query(func.count(User.id)).scalar() or 0
|
|
active_users = db.query(func.count(User.id)).filter(User.is_active == True).scalar() or 0
|
|
new_users_today = db.query(func.count(User.id)).filter(User.created_at >= today_start).scalar() or 0
|
|
new_users_week = db.query(func.count(User.id)).filter(User.created_at >= week_start).scalar() or 0
|
|
new_users_month = db.query(func.count(User.id)).filter(User.created_at >= month_start).scalar() or 0
|
|
|
|
# Active sessions
|
|
active_sessions = db.query(func.count(UserSession.id))\
|
|
.filter(UserSession.is_active == True).scalar() or 0
|
|
|
|
# API Keys
|
|
total_api_keys = db.query(func.count(APIKey.id)).scalar() or 0
|
|
active_api_keys = db.query(func.count(APIKey.id)).filter(APIKey.is_active == True).scalar() or 0
|
|
|
|
# Recent logins (last 24h)
|
|
logins_24h = db.query(func.count(AuditLog.id))\
|
|
.filter(AuditLog.action == "login")\
|
|
.filter(AuditLog.status == "success")\
|
|
.filter(AuditLog.created_at >= now - timedelta(hours=24))\
|
|
.scalar() or 0
|
|
|
|
# Failed logins (last 24h)
|
|
failed_logins_24h = db.query(func.count(AuditLog.id))\
|
|
.filter(AuditLog.action == "login")\
|
|
.filter(AuditLog.status == "failure")\
|
|
.filter(AuditLog.created_at >= now - timedelta(hours=24))\
|
|
.scalar() or 0
|
|
|
|
# Notifications stats
|
|
unread_notifications = db.query(func.count(Notification.id))\
|
|
.filter(Notification.is_read == False).scalar() or 0
|
|
|
|
return {
|
|
"users": {
|
|
"total": total_users,
|
|
"active": active_users,
|
|
"new_today": new_users_today,
|
|
"new_this_week": new_users_week,
|
|
"new_this_month": new_users_month
|
|
},
|
|
"sessions": {
|
|
"active": active_sessions
|
|
},
|
|
"api_keys": {
|
|
"total": total_api_keys,
|
|
"active": active_api_keys
|
|
},
|
|
"security": {
|
|
"logins_24h": logins_24h,
|
|
"failed_logins_24h": failed_logins_24h
|
|
},
|
|
"notifications": {
|
|
"unread_total": unread_notifications
|
|
},
|
|
"generated_at": now.isoformat()
|
|
}
|
|
|
|
|
|
@router.get("/activity")
|
|
def get_recent_activity(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
limit: int = Query(20, ge=1, le=100)
|
|
) -> Any:
|
|
"""
|
|
Get recent activity from audit logs.
|
|
"""
|
|
logs = db.query(AuditLog)\
|
|
.order_by(desc(AuditLog.created_at))\
|
|
.limit(limit)\
|
|
.all()
|
|
|
|
return {
|
|
"items": [
|
|
{
|
|
"id": log.id,
|
|
"user_id": log.user_id,
|
|
"username": log.username,
|
|
"action": log.action,
|
|
"resource_type": log.resource_type,
|
|
"resource_id": log.resource_id,
|
|
"status": log.status,
|
|
"ip_address": log.ip_address,
|
|
"created_at": log.created_at.isoformat()
|
|
}
|
|
for log in logs
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/users/activity")
|
|
def get_user_activity_stats(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
days: int = Query(7, ge=1, le=90)
|
|
) -> Any:
|
|
"""
|
|
Get user activity statistics over time.
|
|
Returns daily active users and new registrations.
|
|
"""
|
|
now = datetime.utcnow()
|
|
start_date = now - timedelta(days=days)
|
|
|
|
# Get daily stats
|
|
daily_stats = []
|
|
for i in range(days):
|
|
day_start = (start_date + timedelta(days=i)).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
day_end = day_start + timedelta(days=1)
|
|
|
|
# Active users (users who logged in that day)
|
|
active = db.query(func.count(func.distinct(AuditLog.user_id)))\
|
|
.filter(AuditLog.action == "login")\
|
|
.filter(AuditLog.status == "success")\
|
|
.filter(AuditLog.created_at >= day_start)\
|
|
.filter(AuditLog.created_at < day_end)\
|
|
.scalar() or 0
|
|
|
|
# New registrations
|
|
new_users = db.query(func.count(User.id))\
|
|
.filter(User.created_at >= day_start)\
|
|
.filter(User.created_at < day_end)\
|
|
.scalar() or 0
|
|
|
|
daily_stats.append({
|
|
"date": day_start.strftime("%Y-%m-%d"),
|
|
"active_users": active,
|
|
"new_users": new_users
|
|
})
|
|
|
|
return {"daily_stats": daily_stats}
|
|
|
|
|
|
@router.get("/actions/breakdown")
|
|
def get_actions_breakdown(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
hours: int = Query(24, ge=1, le=168)
|
|
) -> Any:
|
|
"""
|
|
Get breakdown of actions by type.
|
|
"""
|
|
since = datetime.utcnow() - timedelta(hours=hours)
|
|
|
|
# Group by action
|
|
actions = db.query(
|
|
AuditLog.action,
|
|
func.count(AuditLog.id).label('count')
|
|
).filter(AuditLog.created_at >= since)\
|
|
.group_by(AuditLog.action)\
|
|
.order_by(desc('count'))\
|
|
.all()
|
|
|
|
return {
|
|
"period_hours": hours,
|
|
"actions": [{"action": action, "count": count} for action, count in actions]
|
|
}
|
|
|
|
|
|
@router.get("/top-users")
|
|
def get_top_users(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
limit: int = Query(10, ge=1, le=50),
|
|
days: int = Query(7, ge=1, le=90)
|
|
) -> Any:
|
|
"""
|
|
Get most active users by action count.
|
|
"""
|
|
since = datetime.utcnow() - timedelta(days=days)
|
|
|
|
top_users = db.query(
|
|
AuditLog.user_id,
|
|
AuditLog.username,
|
|
func.count(AuditLog.id).label('action_count')
|
|
).filter(AuditLog.created_at >= since)\
|
|
.filter(AuditLog.user_id.isnot(None))\
|
|
.group_by(AuditLog.user_id, AuditLog.username)\
|
|
.order_by(desc('action_count'))\
|
|
.limit(limit)\
|
|
.all()
|
|
|
|
return {
|
|
"period_days": days,
|
|
"users": [
|
|
{"user_id": uid, "username": uname, "action_count": count}
|
|
for uid, uname, count in top_users
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/security/failed-logins")
|
|
def get_failed_logins(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_superuser),
|
|
hours: int = Query(24, ge=1, le=168)
|
|
) -> Any:
|
|
"""
|
|
Get failed login attempts grouped by IP address.
|
|
"""
|
|
since = datetime.utcnow() - timedelta(hours=hours)
|
|
|
|
# Failed logins by IP
|
|
by_ip = db.query(
|
|
AuditLog.ip_address,
|
|
func.count(AuditLog.id).label('count')
|
|
).filter(AuditLog.action == "login")\
|
|
.filter(AuditLog.status == "failure")\
|
|
.filter(AuditLog.created_at >= since)\
|
|
.group_by(AuditLog.ip_address)\
|
|
.order_by(desc('count'))\
|
|
.limit(20)\
|
|
.all()
|
|
|
|
# Recent failed login details
|
|
recent = db.query(AuditLog)\
|
|
.filter(AuditLog.action == "login")\
|
|
.filter(AuditLog.status == "failure")\
|
|
.filter(AuditLog.created_at >= since)\
|
|
.order_by(desc(AuditLog.created_at))\
|
|
.limit(50)\
|
|
.all()
|
|
|
|
recent_items = []
|
|
for log in recent:
|
|
attempted_username = log.username
|
|
if not attempted_username and log.details:
|
|
try:
|
|
parsed = json.loads(log.details)
|
|
except json.JSONDecodeError:
|
|
parsed = None
|
|
if isinstance(parsed, dict):
|
|
attempted_username = parsed.get("username")
|
|
|
|
recent_items.append(
|
|
{
|
|
"id": log.id,
|
|
"username": attempted_username,
|
|
"ip_address": log.ip_address,
|
|
"user_agent": log.user_agent,
|
|
"created_at": log.created_at.isoformat(),
|
|
}
|
|
)
|
|
|
|
return {"period_hours": hours, "by_ip": [{"ip": ip, "count": count} for ip, count in by_ip], "recent": recent_items}
|