"""User management API routes for the GuardDen dashboard.""" from collections.abc import AsyncIterator from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, status from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from guardden.dashboard.auth import require_owner from guardden.dashboard.config import DashboardSettings from guardden.dashboard.db import DashboardDatabase from guardden.dashboard.schemas import CreateUserNote, UserNote, UserProfile from guardden.models import Guild, ModerationLog, UserActivity from guardden.models import UserNote as UserNoteModel def create_users_router( settings: DashboardSettings, database: DashboardDatabase, ) -> APIRouter: """Create the user management API router.""" router = APIRouter(prefix="/api/users") async def get_session() -> AsyncIterator[AsyncSession]: async for session in database.session(): yield session def require_owner_dep(request: Request) -> None: require_owner(settings, request) @router.get( "/search", response_model=list[UserProfile], dependencies=[Depends(require_owner_dep)], ) async def search_users( guild_id: int | None = Query(default=None), username: str | None = Query(default=None), min_strikes: int | None = Query(default=None, ge=0), limit: int = Query(default=50, ge=1, le=200), session: AsyncSession = Depends(get_session), ) -> list[UserProfile]: """Search for users with optional guild and filter parameters.""" query = select(UserActivity, Guild.name).join(Guild, Guild.id == UserActivity.guild_id) if guild_id: query = query.where(UserActivity.guild_id == guild_id) if username: query = query.where(UserActivity.username.ilike(f"%{username}%")) if min_strikes is not None: query = query.where(UserActivity.strike_count >= min_strikes) query = query.order_by(UserActivity.last_seen.desc()).limit(limit) result = await session.execute(query) users = result.all() # Get last moderation action for each user profiles = [] for user, guild_name in users: last_action_query = ( select(ModerationLog.created_at) .where(ModerationLog.guild_id == user.guild_id) .where(ModerationLog.target_id == user.user_id) .order_by(ModerationLog.created_at.desc()) .limit(1) ) last_action_result = await session.execute(last_action_query) last_action = last_action_result.scalar() profiles.append( UserProfile( guild_id=user.guild_id, guild_name=guild_name, user_id=user.user_id, username=user.username, strike_count=user.strike_count, total_warnings=user.warning_count, total_kicks=user.kick_count, total_bans=user.ban_count, total_timeouts=user.timeout_count, first_seen=user.first_seen, last_action=last_action, ) ) return profiles @router.get( "/{user_id}/profile", response_model=UserProfile, dependencies=[Depends(require_owner_dep)], ) async def get_user_profile( user_id: int = Path(...), guild_id: int = Query(...), session: AsyncSession = Depends(get_session), ) -> UserProfile: """Get detailed profile for a specific user.""" query = ( select(UserActivity, Guild.name) .join(Guild, Guild.id == UserActivity.guild_id) .where(UserActivity.guild_id == guild_id) .where(UserActivity.user_id == user_id) ) result = await session.execute(query) row = result.one_or_none() if not row: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found in this guild", ) user, guild_name = row # Get last moderation action last_action_query = ( select(ModerationLog.created_at) .where(ModerationLog.guild_id == guild_id) .where(ModerationLog.target_id == user_id) .order_by(ModerationLog.created_at.desc()) .limit(1) ) last_action_result = await session.execute(last_action_query) last_action = last_action_result.scalar() return UserProfile( guild_id=user.guild_id, guild_name=guild_name, user_id=user.user_id, username=user.username, strike_count=user.strike_count, total_warnings=user.warning_count, total_kicks=user.kick_count, total_bans=user.ban_count, total_timeouts=user.timeout_count, first_seen=user.first_seen, last_action=last_action, ) @router.get( "/{user_id}/notes", response_model=list[UserNote], dependencies=[Depends(require_owner_dep)], ) async def get_user_notes( user_id: int = Path(...), guild_id: int = Query(...), session: AsyncSession = Depends(get_session), ) -> list[UserNote]: """Get all notes for a specific user.""" query = ( select(UserNoteModel) .where(UserNoteModel.guild_id == guild_id) .where(UserNoteModel.user_id == user_id) .order_by(UserNoteModel.created_at.desc()) ) result = await session.execute(query) notes = result.scalars().all() return [ UserNote( id=note.id, user_id=note.user_id, guild_id=note.guild_id, moderator_id=note.moderator_id, moderator_name=note.moderator_name, content=note.content, created_at=note.created_at, ) for note in notes ] @router.post( "/{user_id}/notes", response_model=UserNote, dependencies=[Depends(require_owner_dep)], ) async def create_user_note( user_id: int = Path(...), guild_id: int = Query(...), note_data: CreateUserNote = ..., request: Request = ..., session: AsyncSession = Depends(get_session), ) -> UserNote: """Create a new note for a user.""" # Get moderator info from session moderator_id = request.session.get("discord_id") if not moderator_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Discord authentication required", ) # Create the note new_note = UserNoteModel( user_id=user_id, guild_id=guild_id, moderator_id=int(moderator_id), moderator_name="Dashboard User", # TODO: Fetch actual username content=note_data.content, created_at=datetime.now(), ) session.add(new_note) await session.commit() await session.refresh(new_note) return UserNote( id=new_note.id, user_id=new_note.user_id, guild_id=new_note.guild_id, moderator_id=new_note.moderator_id, moderator_name=new_note.moderator_name, content=new_note.content, created_at=new_note.created_at, ) @router.delete( "/{user_id}/notes/{note_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_owner_dep)], ) async def delete_user_note( user_id: int = Path(...), note_id: int = Path(...), guild_id: int = Query(...), session: AsyncSession = Depends(get_session), ) -> None: """Delete a user note.""" query = ( select(UserNoteModel) .where(UserNoteModel.id == note_id) .where(UserNoteModel.guild_id == guild_id) .where(UserNoteModel.user_id == user_id) ) result = await session.execute(query) note = result.scalar_one_or_none() if not note: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Note not found", ) await session.delete(note) await session.commit() return router