Added admin panel.

This commit is contained in:
2026-01-11 18:50:26 -06:00
parent e50b2f31d3
commit a97912188e
109 changed files with 6651 additions and 249 deletions

View File

@ -0,0 +1,265 @@
from pydantic import BaseModel, Field
from datetime import datetime
from decimal import Decimal
from typing import Optional, Literal
from enum import Enum
# ============================================================
# Audit Log Schemas
# ============================================================
class AuditLogAction(str, Enum):
DATA_WIPE = "DATA_WIPE"
DATA_SEED = "DATA_SEED"
SIMULATION_START = "SIMULATION_START"
SIMULATION_STOP = "SIMULATION_STOP"
USER_STATUS_CHANGE = "USER_STATUS_CHANGE"
USER_BALANCE_ADJUST = "USER_BALANCE_ADJUST"
USER_ADMIN_GRANT = "USER_ADMIN_GRANT"
USER_ADMIN_REVOKE = "USER_ADMIN_REVOKE"
USER_UPDATE = "USER_UPDATE"
SETTINGS_UPDATE = "SETTINGS_UPDATE"
EVENT_CREATE = "EVENT_CREATE"
EVENT_UPDATE = "EVENT_UPDATE"
EVENT_DELETE = "EVENT_DELETE"
class AuditLogResponse(BaseModel):
id: int
admin_id: int
admin_username: str
action: str
target_type: Optional[str] = None
target_id: Optional[int] = None
description: str
details: Optional[str] = None
ip_address: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
class AuditLogListResponse(BaseModel):
logs: list[AuditLogResponse]
total: int
page: int
page_size: int
# ============================================================
# Data Wiper Schemas
# ============================================================
class WipePreviewResponse(BaseModel):
"""Preview of what will be deleted in a wipe operation."""
users_count: int
wallets_count: int
transactions_count: int
bets_count: int
spread_bets_count: int
events_count: int
event_comments_count: int
match_comments_count: int
admin_settings_preserved: bool = True
admin_users_preserved: bool = True
can_wipe: bool = True
cooldown_remaining_seconds: int = 0
last_wipe_at: Optional[datetime] = None
class WipeRequest(BaseModel):
"""Request to execute a data wipe."""
confirmation_phrase: str = Field(..., description="Must be exactly 'CONFIRM WIPE'")
preserve_admin_users: bool = Field(default=True, description="Keep admin users and their wallets")
preserve_events: bool = Field(default=False, description="Keep sport events but delete bets")
class WipeResponse(BaseModel):
"""Response after a successful wipe operation."""
success: bool
message: str
deleted_counts: dict[str, int]
preserved_counts: dict[str, int]
executed_at: datetime
executed_by: str
# ============================================================
# Data Seeder Schemas
# ============================================================
class SeedRequest(BaseModel):
"""Request to seed the database with test data."""
num_users: int = Field(default=10, ge=1, le=100, description="Number of users to create")
num_events: int = Field(default=5, ge=0, le=50, description="Number of sport events to create")
num_bets_per_event: int = Field(default=3, ge=0, le=20, description="Average bets per event")
starting_balance: Decimal = Field(default=Decimal("1000.00"), ge=Decimal("100"), le=Decimal("10000"))
create_admin: bool = Field(default=True, description="Create a test admin user")
class SeedResponse(BaseModel):
"""Response after seeding the database."""
success: bool
message: str
created_counts: dict[str, int]
test_admin: Optional[dict] = None # Contains username/password if admin created
# ============================================================
# Simulation Schemas
# ============================================================
class SimulationConfig(BaseModel):
"""Configuration for activity simulation."""
delay_seconds: float = Field(default=2.0, ge=0.5, le=30.0, description="Delay between actions")
actions_per_iteration: int = Field(default=3, ge=1, le=10, description="Actions per iteration")
create_users: bool = Field(default=True, description="Allow creating new users")
create_bets: bool = Field(default=True, description="Allow creating bets")
take_bets: bool = Field(default=True, description="Allow taking/matching bets")
add_comments: bool = Field(default=True, description="Allow adding comments")
cancel_bets: bool = Field(default=True, description="Allow cancelling bets")
class SimulationStatusResponse(BaseModel):
"""Response with current simulation status."""
is_running: bool
started_at: Optional[datetime] = None
started_by: Optional[str] = None
iterations_completed: int = 0
config: Optional[SimulationConfig] = None
last_activity: Optional[str] = None
class SimulationStartRequest(BaseModel):
"""Request to start simulation."""
config: Optional[SimulationConfig] = None
class SimulationStartResponse(BaseModel):
"""Response after starting simulation."""
success: bool
message: str
status: SimulationStatusResponse
class SimulationStopResponse(BaseModel):
"""Response after stopping simulation."""
success: bool
message: str
total_iterations: int
ran_for_seconds: float
# ============================================================
# User Management Schemas
# ============================================================
class AdminUserListItem(BaseModel):
"""User item in admin user list."""
id: int
email: str
username: str
display_name: Optional[str] = None
is_admin: bool
status: str
balance: Decimal
escrow: Decimal
total_bets: int
wins: int
losses: int
win_rate: float
created_at: datetime
class Config:
from_attributes = True
class AdminUserListResponse(BaseModel):
"""Paginated list of users."""
users: list[AdminUserListItem]
total: int
page: int
page_size: int
class AdminUserDetailResponse(BaseModel):
"""Detailed user info for admin."""
id: int
email: str
username: str
display_name: Optional[str] = None
avatar_url: Optional[str] = None
bio: Optional[str] = None
is_admin: bool
status: str
created_at: datetime
updated_at: datetime
# Wallet info
balance: Decimal
escrow: Decimal
# Stats
total_bets: int
wins: int
losses: int
win_rate: float
# Counts
open_bets_count: int
matched_bets_count: int
transaction_count: int
class Config:
from_attributes = True
class AdminUserUpdateRequest(BaseModel):
"""Request to update user details."""
display_name: Optional[str] = None
email: Optional[str] = None
is_admin: Optional[bool] = None
class AdminUserStatusRequest(BaseModel):
"""Request to change user status."""
status: Literal["active", "suspended"]
reason: Optional[str] = None
class AdminBalanceAdjustRequest(BaseModel):
"""Request to adjust user balance."""
amount: Decimal = Field(..., description="Positive to add, negative to subtract")
reason: str = Field(..., min_length=5, max_length=500, description="Reason for adjustment")
class AdminBalanceAdjustResponse(BaseModel):
"""Response after balance adjustment."""
success: bool
user_id: int
username: str
previous_balance: Decimal
adjustment: Decimal
new_balance: Decimal
reason: str
transaction_id: int
# ============================================================
# Admin Dashboard Stats
# ============================================================
class AdminDashboardStats(BaseModel):
"""Dashboard statistics for admin panel."""
total_users: int
active_users: int
suspended_users: int
admin_users: int
total_events: int
upcoming_events: int
live_events: int
total_bets: int
open_bets: int
matched_bets: int
total_volume: Decimal
escrow_locked: Decimal
simulation_running: bool

View File

@ -42,6 +42,7 @@ class UserResponse(BaseModel):
losses: int
win_rate: float
status: UserStatus
is_admin: bool = False
created_at: datetime
model_config = ConfigDict(from_attributes=True)