diff --git a/backend/app/api/endpoints/admin.py b/backend/app/api/endpoints/admin.py new file mode 100644 index 0000000..8b43ea7 --- /dev/null +++ b/backend/app/api/endpoints/admin.py @@ -0,0 +1,100 @@ +from datetime import datetime, timezone, timedelta + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.core.security import get_current_user +from app.models.user import User +from app.models.playlist import Playlist +from app.models.track import Track +from app.models.recommendation import Recommendation + +router = APIRouter(prefix="/admin", tags=["admin"]) + +ADMIN_EMAILS = ["chris.ryan@deepcutsai.com"] + + +@router.get("/stats") +async def get_stats( + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + if user.email not in ADMIN_EMAILS: + raise HTTPException(status_code=403, detail="Admin only") + + now = datetime.now(timezone.utc) + today = now.replace(hour=0, minute=0, second=0, microsecond=0) + week_ago = now - timedelta(days=7) + month_ago = now - timedelta(days=30) + + # User stats + total_users = (await db.execute(select(func.count(User.id)))).scalar() or 0 + pro_users = (await db.execute(select(func.count(User.id)).where(User.is_pro == True))).scalar() or 0 + + # Playlist stats + total_playlists = (await db.execute(select(func.count(Playlist.id)))).scalar() or 0 + total_tracks = (await db.execute(select(func.count(Track.id)))).scalar() or 0 + + # Recommendation stats + total_recs = (await db.execute(select(func.count(Recommendation.id)))).scalar() or 0 + recs_today = (await db.execute( + select(func.count(Recommendation.id)).where(Recommendation.created_at >= today) + )).scalar() or 0 + recs_this_week = (await db.execute( + select(func.count(Recommendation.id)).where(Recommendation.created_at >= week_ago) + )).scalar() or 0 + recs_this_month = (await db.execute( + select(func.count(Recommendation.id)).where(Recommendation.created_at >= month_ago) + )).scalar() or 0 + + saved_recs = (await db.execute( + select(func.count(Recommendation.id)).where(Recommendation.saved == True) + )).scalar() or 0 + disliked_recs = (await db.execute( + select(func.count(Recommendation.id)).where(Recommendation.disliked == True) + )).scalar() or 0 + + # Per-user breakdown + user_stats_result = await db.execute( + select( + User.id, User.name, User.email, User.is_pro, User.created_at, + func.count(Recommendation.id).label("rec_count"), + ) + .outerjoin(Recommendation, Recommendation.user_id == User.id) + .group_by(User.id) + .order_by(User.id) + ) + user_breakdown = [ + { + "id": row.id, + "name": row.name, + "email": row.email, + "is_pro": row.is_pro, + "created_at": row.created_at.isoformat() if row.created_at else None, + "recommendation_count": row.rec_count, + } + for row in user_stats_result.all() + ] + + return { + "users": { + "total": total_users, + "pro": pro_users, + "free": total_users - pro_users, + }, + "playlists": { + "total": total_playlists, + "total_tracks": total_tracks, + }, + "recommendations": { + "total": total_recs, + "today": recs_today, + "this_week": recs_this_week, + "this_month": recs_this_month, + "saved": saved_recs, + "disliked": disliked_recs, + }, + "user_breakdown": user_breakdown, + } diff --git a/backend/app/main.py b/backend/app/main.py index 5f1910e..be92fa2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,7 +2,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.core.config import settings -from app.api.endpoints import auth, bandcamp, billing, lastfm, manual_import, playlist_fix, playlists, profile, recommendations, youtube_music +from app.api.endpoints import admin, auth, bandcamp, billing, lastfm, manual_import, playlist_fix, playlists, profile, recommendations, youtube_music app = FastAPI(title="Vynl API", version="1.0.0", redirect_slashes=False) @@ -14,6 +14,7 @@ app.add_middleware( allow_headers=["*"], ) +app.include_router(admin.router, prefix="/api") app.include_router(auth.router, prefix="/api") app.include_router(billing.router, prefix="/api") app.include_router(playlists.router, prefix="/api")