elmeg-demo/backend/routers/moderation.py
fullsizemalt 037d2aa463
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
feat: enhance Nickname Queue with status filtering
2025-12-24 13:04:35 -08:00

330 lines
10 KiB
Python

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