Files
GuardDen/src/guardden/dashboard/users.py
latte 574a07d127
Some checks failed
CI/CD Pipeline / Security Scanning (push) Has been cancelled
CI/CD Pipeline / Tests (3.11) (push) Has been cancelled
CI/CD Pipeline / Tests (3.12) (push) Has been cancelled
CI/CD Pipeline / Build Docker Image (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (push) Has been cancelled
Update dashboard and Docker compose
2026-01-24 19:14:00 +01:00

255 lines
8.5 KiB
Python

"""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