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
255 lines
8.5 KiB
Python
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
|