diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index ef7228d..e0a1520 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -2,6 +2,7 @@ from fastapi import APIRouter from app.api.auth import router as auth_router +from app.api.oauth import router as oauth_router from app.api.domains import router as domains_router from app.api.check import router as check_router from app.api.subscription import router as subscription_router @@ -12,11 +13,13 @@ from app.api.auctions import router as auctions_router from app.api.webhooks import router as webhooks_router from app.api.contact import router as contact_router from app.api.price_alerts import router as price_alerts_router +from app.api.blog import router as blog_router api_router = APIRouter() # Core API endpoints api_router.include_router(auth_router, prefix="/auth", tags=["Authentication"]) +api_router.include_router(oauth_router, prefix="/oauth", tags=["OAuth"]) api_router.include_router(check_router, prefix="/check", tags=["Domain Check"]) api_router.include_router(domains_router, prefix="/domains", tags=["Domain Management"]) api_router.include_router(subscription_router, prefix="/subscription", tags=["Subscription"]) @@ -31,5 +34,8 @@ api_router.include_router(contact_router, prefix="/contact", tags=["Contact & Ne # Webhooks (external service callbacks) api_router.include_router(webhooks_router, prefix="/webhooks", tags=["Webhooks"]) +# Content +api_router.include_router(blog_router, prefix="/blog", tags=["Blog"]) + # Admin endpoints api_router.include_router(admin_router, prefix="/admin", tags=["Admin"]) diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 63f55f8..caadfa3 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -226,6 +226,65 @@ async def list_users( } +# ============== User Export ============== +# NOTE: This must come BEFORE /users/{user_id} to avoid route conflict + +@router.get("/users/export") +async def export_users_csv( + db: Database, + admin: User = Depends(require_admin), +): + """Export all users as CSV data.""" + import csv + import io + + result = await db.execute(select(User).order_by(User.created_at)) + users_list = result.scalars().all() + + # Create CSV + output = io.StringIO() + writer = csv.writer(output) + + # Header + writer.writerow([ + "ID", "Email", "Name", "Active", "Verified", "Admin", + "Created At", "Last Login", "Tier", "Domain Limit", "Domains Used" + ]) + + for user in users_list: + # Get subscription + sub_result = await db.execute( + select(Subscription).where(Subscription.user_id == user.id) + ) + subscription = sub_result.scalar_one_or_none() + + # Get domain count + domain_count = await db.execute( + select(func.count(Domain.id)).where(Domain.user_id == user.id) + ) + domain_count = domain_count.scalar() + + writer.writerow([ + user.id, + user.email, + user.name or "", + "Yes" if user.is_active else "No", + "Yes" if user.is_verified else "No", + "Yes" if user.is_admin else "No", + user.created_at.strftime("%Y-%m-%d %H:%M"), + user.last_login.strftime("%Y-%m-%d %H:%M") if user.last_login else "", + subscription.tier.value if subscription else "scout", + subscription.domain_limit if subscription else 5, + domain_count, + ]) + + return { + "csv": output.getvalue(), + "count": len(users_list), + "exported_at": datetime.utcnow().isoformat(), + } + + @router.get("/users/{user_id}") async def get_user( user_id: int, @@ -574,3 +633,329 @@ async def make_user_admin( await db.commit() return {"message": f"User {email} is now an admin"} + + +# ============== Price Alerts ============== + +@router.get("/price-alerts") +async def list_price_alerts( + db: Database, + admin: User = Depends(require_admin), + limit: int = 100, + offset: int = 0, +): + """List all active price alerts with user info.""" + query = ( + select(PriceAlert, User) + .join(User, PriceAlert.user_id == User.id) + .where(PriceAlert.is_active == True) + .order_by(desc(PriceAlert.created_at)) + .offset(offset) + .limit(limit) + ) + result = await db.execute(query) + alerts = result.all() + + # Total count + count_query = select(func.count(PriceAlert.id)).where(PriceAlert.is_active == True) + total = await db.execute(count_query) + total = total.scalar() + + return { + "alerts": [ + { + "id": alert.id, + "tld": alert.tld, + "target_price": float(alert.target_price) if alert.target_price else None, + "alert_type": alert.alert_type, + "created_at": alert.created_at.isoformat(), + "user": { + "id": user.id, + "email": user.email, + "name": user.name, + } + } + for alert, user in alerts + ], + "total": total, + } + + +# ============== Domain Health ============== + +@router.post("/domains/check-all") +async def trigger_domain_checks( + background_tasks: BackgroundTasks, + db: Database, + admin: User = Depends(require_admin), +): + """Manually trigger domain availability checks for all watched domains.""" + from app.services.domain_checker import check_all_domains + + # Count domains to check + total_domains = await db.execute(select(func.count(Domain.id))) + total_domains = total_domains.scalar() + + if total_domains == 0: + return {"message": "No domains to check", "domains_queued": 0} + + # Run in background + background_tasks.add_task(check_all_domains, db) + + return { + "message": "Domain checks started", + "domains_queued": total_domains, + "started_at": datetime.utcnow().isoformat(), + } + + +# ============== Email Test ============== + +@router.post("/system/test-email") +async def test_email( + db: Database, + admin: User = Depends(require_admin), +): + """Send a test email to the admin user.""" + from app.services.email_service import email_service + + if not email_service.is_configured: + raise HTTPException( + status_code=400, + detail="Email service is not configured. Check SMTP settings." + ) + + try: + await email_service.send_email( + to_email=admin.email, + subject="pounce Admin Panel - Test Email", + html_content=f""" +
This is a test email from the pounce Admin Panel.
+If you received this, your SMTP configuration is working correctly.
+
+ Sent at: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}
+ Admin: {admin.email}
+
- You don't have admin privileges to access this page. + You don't have admin privileges to access this page.