Files
app-service/backend/app/api/v1/analytics.py
matteoscrugli 8c4a555b88 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>
2025-12-17 22:27:32 +01:00

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}