from typing import List, Optional from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select, func from pydantic import BaseModel from database import get_session from models import Report, User, PerformanceNickname, Profile, Rating, Review, Comment, Attendance from schemas import ReportCreate, ReportRead, PerformanceNicknameRead from auth import get_current_user from dependencies import RoleChecker router = APIRouter(prefix="/moderation", tags=["moderation"]) allow_moderator = RoleChecker(["moderator", "admin"]) # ============ SCHEMAS ============ class UserLookupResult(BaseModel): id: int email: str username: Optional[str] = None role: str is_active: bool email_verified: bool ban_expires: Optional[datetime] = None stats: dict class TempBanRequest(BaseModel): user_id: int duration_hours: int # 0 = permanent reason: str class BulkActionRequest(BaseModel): ids: List[int] action: str # approve, reject, resolve, dismiss class ModActionLog(BaseModel): id: int moderator_id: int moderator_email: str action_type: str target_type: str target_id: int reason: Optional[str] = None created_at: datetime # ============ USER LOOKUP ============ @router.get("/users/lookup") def lookup_user( query: str, session: Session = Depends(get_session), mod: User = Depends(allow_moderator) ): """Search for a user by email or username""" # Search by email user = session.exec(select(User).where(User.email.contains(query))).first() if not user: # Try username via profile profile = session.exec(select(Profile).where(Profile.username.contains(query))).first() if profile: user = session.get(User, profile.user_id) if not user: raise HTTPException(status_code=404, detail="User not found") profile = session.exec(select(Profile).where(Profile.user_id == user.id)).first() return { "id": user.id, "email": user.email, "username": profile.username if profile else None, "role": user.role, "is_active": user.is_active, "email_verified": user.email_verified, "stats": { "ratings": session.exec(select(func.count(Rating.id)).where(Rating.user_id == user.id)).one(), "reviews": session.exec(select(func.count(Review.id)).where(Review.user_id == user.id)).one(), "comments": session.exec(select(func.count(Comment.id)).where(Comment.user_id == user.id)).one(), "attendances": session.exec(select(func.count(Attendance.id)).where(Attendance.user_id == user.id)).one(), "reports_submitted": session.exec(select(func.count(Report.id)).where(Report.user_id == user.id)).one(), } } @router.get("/users/{user_id}/activity") def get_user_activity( user_id: int, limit: int = 50, session: Session = Depends(get_session), mod: User = Depends(allow_moderator) ): """Get recent activity for a user""" user = session.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") # Get recent comments comments = session.exec( select(Comment).where(Comment.user_id == user_id).limit(20) ).all() # Get recent reviews reviews = session.exec( select(Review).where(Review.user_id == user_id).limit(20) ).all() # Format activity activity = [] for c in comments: activity.append({ "type": "comment", "id": c.id, "content": c.content[:100] if c.content else "", "entity_type": c.entity_type, "entity_id": c.entity_id, }) for r in reviews: activity.append({ "type": "review", "id": r.id, "content": r.content[:100] if r.content else "", "show_id": r.show_id, }) return activity[:limit] # ============ TEMP BANS ============ @router.post("/users/ban") def ban_user( request: TempBanRequest, session: Session = Depends(get_session), mod: User = Depends(allow_moderator) ): """Ban a user (temporarily or permanently)""" user = session.get(User, request.user_id) if not user: raise HTTPException(status_code=404, detail="User not found") # Don't allow banning admins if user.role == "admin": raise HTTPException(status_code=400, detail="Cannot ban an admin") # Don't allow mods to ban other mods (only admins can) if user.role == "moderator" and mod.role != "admin": raise HTTPException(status_code=400, detail="Only admins can ban moderators") user.is_active = False session.add(user) session.commit() return { "message": "User banned", "user_id": user.id, "duration_hours": request.duration_hours, "reason": request.reason } @router.post("/users/{user_id}/unban") def unban_user( user_id: int, session: Session = Depends(get_session), mod: User = Depends(allow_moderator) ): """Unban a user""" user = session.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") user.is_active = True session.add(user) session.commit() return {"message": "User unbanned", "user_id": user.id} # ============ BULK ACTIONS ============ @router.post("/nicknames/bulk") def bulk_moderate_nicknames( request: BulkActionRequest, session: Session = Depends(get_session), mod: User = Depends(allow_moderator) ): """Bulk approve or reject nicknames""" if request.action not in ("approve", "reject"): raise HTTPException(status_code=400, detail="Invalid action") status = "approved" if request.action == "approve" else "rejected" count = 0 for nickname_id in request.ids: nickname = session.get(PerformanceNickname, nickname_id) if nickname and nickname.status == "pending": nickname.status = status session.add(nickname) count += 1 session.commit() return {"message": f"{count} nicknames {status}", "count": count} @router.post("/reports/bulk") def bulk_moderate_reports( request: BulkActionRequest, session: Session = Depends(get_session), mod: User = Depends(allow_moderator) ): """Bulk resolve or dismiss reports""" if request.action not in ("resolve", "dismiss"): raise HTTPException(status_code=400, detail="Invalid action") status = "resolved" if request.action == "resolve" else "dismissed" count = 0 for report_id in request.ids: report = session.get(Report, report_id) if report and report.status == "pending": report.status = status session.add(report) count += 1 session.commit() return {"message": f"{count} reports {status}", "count": count} # ============ QUEUE STATS ============ @router.get("/queue/stats") def get_queue_stats( session: Session = Depends(get_session), mod: User = Depends(allow_moderator) ): """Get moderation queue statistics""" return { "pending_nicknames": session.exec( select(func.count(PerformanceNickname.id)).where(PerformanceNickname.status == "pending") ).one(), "pending_reports": session.exec( select(func.count(Report.id)).where(Report.status == "pending") ).one(), "total_bans": session.exec( select(func.count(User.id)).where(User.is_active == False) ).one(), } # ============ EXISTING ENDPOINTS ============ @router.post("/reports", response_model=ReportRead) def create_report( report: ReportCreate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): db_report = Report.model_validate(report) db_report.user_id = current_user.id session.add(db_report) session.commit() session.refresh(db_report) return db_report @router.get("/queue/nicknames", response_model=List[PerformanceNicknameRead], dependencies=[Depends(allow_moderator)]) def get_pending_nicknames( status: str = "pending", session: Session = Depends(get_session) ): query = select(PerformanceNickname) if status in ("pending", "approved", "rejected"): query = query.where(PerformanceNickname.status == status) nicknames = session.exec(query.limit(100)).all() return nicknames @router.put("/nicknames/{nickname_id}/{action}", response_model=PerformanceNicknameRead, dependencies=[Depends(allow_moderator)]) def moderate_nickname( nickname_id: int, action: str, # approve, reject session: Session = Depends(get_session) ): nickname = session.get(PerformanceNickname, nickname_id) if not nickname: raise HTTPException(status_code=404, detail="Nickname not found") if action == "approve": nickname.status = "approved" elif action == "reject": nickname.status = "rejected" else: raise HTTPException(status_code=400, detail="Invalid action") session.add(nickname) session.commit() session.refresh(nickname) return nickname @router.get("/queue/reports", response_model=List[ReportRead], dependencies=[Depends(allow_moderator)]) def get_pending_reports(session: Session = Depends(get_session)): reports = session.exec( select(Report).where(Report.status == "pending") ).all() return reports @router.put("/reports/{report_id}/{action}", response_model=ReportRead, dependencies=[Depends(allow_moderator)]) def moderate_report( report_id: int, action: str, # resolve, dismiss session: Session = Depends(get_session) ): report = session.get(Report, report_id) if not report: raise HTTPException(status_code=404, detail="Report not found") if action == "resolve": report.status = "resolved" elif action == "dismiss": report.status = "dismissed" else: raise HTTPException(status_code=400, detail="Invalid action") session.add(report) session.commit() session.refresh(report) return report