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
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:
246
src/guardden/dashboard/users.py
Normal file
246
src/guardden/dashboard/users.py
Normal 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
|
||||
Reference in New Issue
Block a user