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:
280
backend/app/api/v1/analytics.py
Normal file
280
backend/app/api/v1/analytics.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user