"""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}