quick commit
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 6m9s
CI/CD Pipeline / Security Scanning (push) Successful in 26s
CI/CD Pipeline / Tests (3.11) (push) Failing after 5m24s
CI/CD Pipeline / Tests (3.12) (push) Failing after 5m23s
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Deploy to Staging (push) Has been skipped
CI/CD Pipeline / Deploy to Production (push) Has been skipped
CI/CD Pipeline / Notification (push) Successful in 1s

This commit is contained in:
2026-01-17 20:24:43 +01:00
parent 95cc3cdb8f
commit 831eed8dbc
82 changed files with 8860 additions and 167 deletions

View File

@@ -0,0 +1,246 @@
"""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 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 = Query(...),
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 in a guild with optional filters."""
query = select(UserActivity).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.scalars().all()
# Get last moderation action for each user
profiles = []
for user in users:
last_action_query = (
select(ModerationLog.created_at)
.where(ModerationLog.guild_id == 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(
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)
.where(UserActivity.guild_id == guild_id)
.where(UserActivity.user_id == user_id)
)
result = await session.execute(query)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found in this guild",
)
# 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(
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