- Migrated all schemas to Pydantic v2 syntax (model_config, ConfigDict) - Fixed mutable default in ProtoMessageBase using Field(default_factory=datetime.now) - Consolidated CardCollection and Wishlist schemas in user_card_collection.py - Created game_schemas.py with GameCreate, GameResponse, GameJoinRequest, etc. - Created mtg_card_schemas.py with MtgCardResponse, MtgCardSearchRequest, etc. - Added CardImportBatchCreate, CardImportBatchResponse, UserCardImportCreate/Response schemas - Fixed duplicate UserCardImportRecord class between card_import_batch.py and user_card_import_record.py - Updated __init__.py with comprehensive schema exports - Created verify_schemas.py for schema-model matching verification
1093 lines
43 KiB
Markdown
1093 lines
43 KiB
Markdown
# MTG Online Backend - Code Structure Documentation
|
|
|
|
## Project Overview
|
|
|
|
**Location:** `/home/wall-o/projects/mtgonline/backend/`
|
|
|
|
**Stack:** FastAPI + SQLAlchemy (async) + Alembic + PostgreSQL + Redis
|
|
|
|
**Purpose:** MTG Online card database management, deck building, user management, and game infrastructure.
|
|
|
|
---
|
|
|
|
## 1. Models (`app/models/`)
|
|
|
|
### 1.1 Core Models (`models.py`)
|
|
|
|
**User** (`mtgonline_users`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `username`: String(64), unique, indexed
|
|
- `password_hash`: String(128), bcrypt hash
|
|
- `salt`: String(128)
|
|
- `email`: String(255), indexed
|
|
- `country`: String(2)
|
|
- `real_name`: String(128)
|
|
- `avatar_bmp`: Text (Base64 encoded)
|
|
- `privlevel`: String(50), default "User"
|
|
- `is_active`: Boolean, default True
|
|
- `is_banned`: Boolean, default False
|
|
- `ban_reason`: Text
|
|
- `ban_ends`: DateTime
|
|
- `vip_status`: Integer (0=normal, 1=vip, 2=donator)
|
|
- `vip_expiry`: DateTime
|
|
- `creation_date`: DateTime, server default now()
|
|
- `last_login`: DateTime
|
|
- **Relationships:** `decklist_files`, `decklist_folders` (both cascade delete)
|
|
|
|
**MtonlineCard** (`mtgonline_cards`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `source_id`: Integer, indexed (references mtg_cards.id in mtgdata)
|
|
- `name`: String(255), indexed
|
|
- `mana_cost`: String(255)
|
|
- `type_line`: String(255)
|
|
- `oracle_text`: Text
|
|
- `power`: String(50)
|
|
- `toughness`: String(50)
|
|
- `rarity`: String(50)
|
|
- `layout`: String(50)
|
|
- `artist`: String(255)
|
|
- `flavor_text`: Text
|
|
- `numbers`: String(100)
|
|
- `identifiers`: Text (JSON string)
|
|
- `images`: Text (JSON string)
|
|
- `image`: Text (card image URL)
|
|
- `set_code`: String(10), indexed
|
|
- `set_name`: String(255)
|
|
- `card_parts`: Text (comma-separated face names)
|
|
- `keywords`: Text (comma-separated)
|
|
- `legalities`: Text (JSON of format legality)
|
|
- `synced_at`: DateTime, onupdate now()
|
|
- `created_at`: DateTime, server default now()
|
|
|
|
**DecklistFolder** (`mtgonline_decklist_folders`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `owner_id`: Integer, FK to mtgonline_users.id
|
|
- `name`: String(255)
|
|
- `parent_id`: Integer, FK to self (recursive)
|
|
- `creation_date`: DateTime
|
|
- **Relationships:** `owner`, `children` (recursive), `parent`, `files` (cascade delete)
|
|
|
|
**DecklistFile** (`mtgonline_decklist_files`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `folder_id`: Integer, FK to mtgonline_decklist_folders.id
|
|
- `owner_id`: Integer, FK to mtgonline_users.id
|
|
- `name`: String(255)
|
|
- `content`: Text (native XML or plain text)
|
|
- `format`: String(50), default "native"
|
|
- `status`: String(20), default "DRAUGHT"
|
|
- `creation_date`: DateTime
|
|
- **Relationships:** `folder`, `owner`, `card_links` (added in mirror_models.py)
|
|
|
|
**Room** (`mtgonline_rooms`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `name`: String(100), unique
|
|
- `description`: Text
|
|
- `is_password_protected`: Boolean
|
|
- `password_hash`: String(128)
|
|
- `creation_date`: DateTime
|
|
- **Relationships:** `game_types` (cascade delete)
|
|
|
|
**RoomGameType** (`mtgonline_rooms_gametypes`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `room_id`: Integer, FK to mtgonline_rooms.id
|
|
- `name`: String(100)
|
|
- `description`: Text
|
|
- **Relationships:** `room`
|
|
|
|
**Ban** (`mtgonline_bans`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `user_id`: Integer, FK to mtgonline_users.id
|
|
- `server_id`: Integer
|
|
- `reason`: Text
|
|
- `moderators`: String(255)
|
|
- `ip_address`: String(45)
|
|
- `expiration_time`: DateTime
|
|
- `active`: Boolean, default True
|
|
- `creation_date`: DateTime
|
|
- **Relationships:** `user`
|
|
|
|
**GameLog** (`mtgonline_log`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `room_id`: Integer, FK to mtgonline_rooms.id
|
|
- `player_id`: Integer, FK to mtgonline_users.id
|
|
- `message`: Text
|
|
- `timestamp`: DateTime
|
|
- **Relationships:** `room`, `player`
|
|
|
|
**AuditLog** (`mtgonline_audit`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `admin_id`: Integer, FK to mtgonline_users.id
|
|
- `action_type`: String(50)
|
|
- `target_user_id`: Integer, FK to mtgonline_users.id
|
|
- `details`: Text
|
|
- `ip_address`: String(45)
|
|
- `timestamp`: DateTime
|
|
- **Relationships:** `admin`, `target_user`
|
|
|
|
**Indexes:**
|
|
- `idx_decks_owner` on DecklistFile.owner_id
|
|
- `idx_decks_folder` on DecklistFile.folder_id
|
|
- `idx_bans_active` on Ban.active
|
|
- `idx_log_timestamp` on GameLog.timestamp
|
|
|
|
---
|
|
|
|
### 1.2 MTG Models (`mtg_models.py`)
|
|
|
|
**MtgSet** (`mtg_sets`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `code`: String(10), unique, indexed
|
|
- `name`: String(255)
|
|
- `type`: String(100)
|
|
- `release_date`: DateTime
|
|
- `base_set_size`: Integer
|
|
- `total_size`: Integer
|
|
- `is_foil_only`: Integer
|
|
- `is_non_foil_only`: Integer
|
|
- `digital`: Integer
|
|
- `icon_svg_url`: Text
|
|
- `parent_code`: String(10)
|
|
- `mtgo_code`: String(10)
|
|
- `image`: Text
|
|
- `updated_at`: DateTime
|
|
- **Relationships:** `cards`
|
|
|
|
**MtgCard** (`mtg_cards`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `set_id`: Integer, FK to mtg_sets.id, indexed
|
|
- `name`: String(255), indexed
|
|
- `mana_cost`: String(255), indexed
|
|
- `type_line`: String(255), indexed
|
|
- `oracle_text`: Text
|
|
- `power`: String(50)
|
|
- `toughness`: String(50)
|
|
- `rarity`: String(50), indexed
|
|
- `layout`: String(50)
|
|
- `artist`: String(255)
|
|
- `flavor_text`: Text
|
|
- `numbers`: String(100)
|
|
- `identifiers`: Text (JSON)
|
|
- `images`: Text (JSON)
|
|
- `image`: Text
|
|
- `updated_at`: DateTime
|
|
- **Relationships:** `set`
|
|
|
|
**Indexes:**
|
|
- `idx_mtg_cards_name_set` on MtgCard.name, MtgCard.set_id
|
|
- `idx_mtg_cards_type` on MtgCard.type_line
|
|
- `idx_mtg_cards_rarity` on MtgCard.rarity
|
|
|
|
---
|
|
|
|
### 1.3 Mirror Models (`mirror_models.py`)
|
|
|
|
**MtgCardMirror** (`mtg_cards_mirror`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `source_id`: Integer, indexed (references mtg_cards.id)
|
|
- `name`: String(255), indexed
|
|
- `mana_cost`: String(255)
|
|
- `type_line`: String(255)
|
|
- `oracle_text`: Text
|
|
- `power`: String(50)
|
|
- `toughness`: String(50)
|
|
- `rarity`: String(50)
|
|
- `layout`: String(50)
|
|
- `artist`: String(255)
|
|
- `flavor_text`: Text
|
|
- `numbers`: String(100)
|
|
- `identifiers`: Text (JSON)
|
|
- `images`: Text (JSON)
|
|
- `image`: Text
|
|
- `card_parts`: Text
|
|
- `keywords`: Text
|
|
- `legalities`: Text (JSON)
|
|
- `set_code`: String(10), indexed
|
|
- `set_name`: String(255)
|
|
- `synced_at`: DateTime, onupdate now()
|
|
- `created_at`: DateTime
|
|
- **Relationships:** `deck_links` (cascade delete)
|
|
|
|
**DeckCardLink** (`deck_card_links`)
|
|
- `id`: Integer, primary key, indexed
|
|
- `deck_id`: Integer, FK to mtgonline_decklist_files.id, CASCADE delete
|
|
- `card_id`: Integer, FK to mtg_cards_mirror.id
|
|
- `quantity`: Integer, default 1
|
|
- `zone`: String(20), default "main"
|
|
- **Constraints:** UniqueConstraint(deck_id, card_id, zone)
|
|
- **Indexes:** `idx_deck_card_deck`, `idx_deck_card_card`
|
|
- **Relationships:** `deck`, `card`
|
|
|
|
**Note:** DecklistFile has a back-reference added: `card_links` relationship to DeckCardLink
|
|
|
|
---
|
|
|
|
### 1.4 User Data Models (`user_data.py`)
|
|
|
|
**UserSession** (`user_sessions`)
|
|
- `id`: BigInteger, primary key
|
|
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE, indexed
|
|
- `session_token_hash`: String(255), unique, indexed
|
|
- `ip_address`: String(45)
|
|
- `user_agent`: Text
|
|
- `created_at`: DateTime
|
|
- `expires_at`: DateTime
|
|
- `is_active`: Boolean, default True
|
|
- **Relationships:** `user`
|
|
|
|
**DeckVersion** (`deck_versions`)
|
|
- `id`: BigInteger, primary key
|
|
- `deck_id`: Integer, FK to mtgonline_decklist_files.id, CASCADE, indexed
|
|
- `version_number`: Integer
|
|
- `content`: Text
|
|
- `status`: String(20), default "DRAFT"
|
|
- `comment`: Text
|
|
- `created_at`: DateTime
|
|
- **Relationships:** `deck`
|
|
|
|
**GameReplay** (`game_replays`)
|
|
- `id`: BigInteger, primary key
|
|
- `game_uuid`: String(36), unique
|
|
- `room_id`: Integer, FK to mtgonline_rooms.id, indexed
|
|
- `game_type`: String(50)
|
|
- `format`: String(50)
|
|
- `duration_seconds`: Integer
|
|
- `start_time`: DateTime
|
|
- `end_time`: DateTime
|
|
- `status`: String(20), default "IN_PROGRESS"
|
|
- `replay_data`: JSON
|
|
- `created_at`: DateTime
|
|
- `updated_at`: DateTime
|
|
- **Relationships:** `players` (cascade), `outcomes` (cascade)
|
|
|
|
**ReplayPlayer** (`replay_players`)
|
|
- `id`: BigInteger, primary key
|
|
- `replay_id`: BigInteger, FK to game_replays.id, CASCADE, indexed
|
|
- `user_id`: Integer, FK to mtgonline_users.id, indexed
|
|
- `position`: Integer
|
|
- `deck_id`: Integer, FK to mtgonline_decklist_files.id
|
|
- `won`: Boolean
|
|
- `lost`: Boolean
|
|
- `concession`: Boolean, default False
|
|
- `turn_one`: Boolean, default False
|
|
- `created_at`: DateTime
|
|
- **Relationships:** `replay`, `user`, `deck`
|
|
|
|
**GameOutcome** (`game_outcomes`)
|
|
- `id`: BigInteger, primary key
|
|
- `user_id`: Integer, FK to mtgonline_users.id, indexed
|
|
- `game_uuid`: String(36), FK to game_replays.game_uuid, indexed
|
|
- `outcome`: String(20), indexed
|
|
- `opponent_id`: Integer, FK to mtgonline_users.id
|
|
- `format`: String(50)
|
|
- `rating_before`: Integer
|
|
- `rating_after`: Integer
|
|
- `rating_change`: Integer
|
|
- `created_at`: DateTime
|
|
- **Relationships:** `replay`, `user`, `opponent`
|
|
|
|
**UserStatistics** (`user_statistics`)
|
|
- `user_id`: Integer, FK to mtgonline_users.id, primary key
|
|
- `total_games`: Integer, default 0
|
|
- `total_wins`: Integer, default 0
|
|
- `total_losses`: Integer, default 0
|
|
- `total_concessions`: Integer, default 0
|
|
- `win_rate`: Float, default 0.0
|
|
- `current_streak`: Integer, default 0
|
|
- `best_streak`: Integer, default 0
|
|
- `average_rating`: Float, default 0.0
|
|
- `last_game_date`: DateTime
|
|
- `updated_at`: DateTime
|
|
- **Relationships:** `user`
|
|
|
|
**UserCardCollection** (`user_card_collection`)
|
|
- `id`: BigInteger, primary key
|
|
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE, indexed
|
|
- `card_id`: Integer, indexed
|
|
- `quantity`: Integer, default 1
|
|
- `condition`: String(20), default "NEAR_MINT"
|
|
- `language`: String(5), default "EN"
|
|
- `is_foil`: Boolean, default False
|
|
- `is_alt_art`: Boolean, default False
|
|
- `acquired_date`: DateTime
|
|
- `acquisition_method`: String(50)
|
|
- `notes`: Text
|
|
- `created_at`: DateTime
|
|
- `updated_at`: DateTime
|
|
- **Constraints:** UniqueConstraint(user_id, card_id, is_foil, is_alt_art)
|
|
- **Indexes:** `idx_collection_user_card`
|
|
- **Relationships:** `user`
|
|
|
|
**CardWishlist** (`card_wishlist`)
|
|
- `id`: BigInteger, primary key
|
|
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE
|
|
- `card_id`: Integer
|
|
- `max_price`: Float
|
|
- `notes`: Text
|
|
- `created_at`: DateTime
|
|
- **Constraints:** UniqueConstraint(user_id, card_id)
|
|
- **Relationships:** `user`
|
|
|
|
**UserGroup** (`user_groups`)
|
|
- `id`: BigInteger, primary key
|
|
- `name`: String(100)
|
|
- `description`: Text
|
|
- `owner_id`: Integer, FK to mtgonline_users.id, indexed
|
|
- `is_public`: Boolean, default True
|
|
- `max_members`: Integer, default 50
|
|
- `created_at`: DateTime
|
|
- `updated_at`: DateTime
|
|
- **Relationships:** `owner`, `members` (cascade), `messages` (cascade)
|
|
|
|
**GroupMember** (`group_members`)
|
|
- `id`: BigInteger, primary key
|
|
- `group_id`: BigInteger, FK to user_groups.id, CASCADE, indexed
|
|
- `user_id`: Integer, FK to mtgonline_users.id, indexed
|
|
- `role`: String(20), default "MEMBER"
|
|
- `joined_at`: DateTime
|
|
- **Constraints:** UniqueConstraint(group_id, user_id)
|
|
- **Relationships:** `group`, `user`
|
|
|
|
**GroupChatMessage** (`group_chat_messages`)
|
|
- `id`: BigInteger, primary key
|
|
- `group_id`: BigInteger, FK to user_groups.id, CASCADE, indexed
|
|
- `sender_id`: Integer, FK to mtgonline_users.id, indexed
|
|
- `message`: Text
|
|
- `created_at`: DateTime, indexed
|
|
- **Relationships:** `group`, `sender`
|
|
|
|
**UserNetwork** (`user_networks`)
|
|
- `id`: BigInteger, primary key
|
|
- `name`: String(100)
|
|
- `description`: Text
|
|
- `creator_id`: Integer, FK to mtgonline_users.id
|
|
- `is_public`: Boolean, default True
|
|
- `created_at`: DateTime
|
|
- **Relationships:** `creator`, `members` (cascade)
|
|
|
|
**NetworkMember** (`network_members`)
|
|
- `id`: BigInteger, primary key
|
|
- `network_id`: BigInteger, FK to user_networks.id, CASCADE, indexed
|
|
- `user_id`: Integer, FK to mtgonline_users.id, indexed
|
|
- `role`: String(20), default "MEMBER"
|
|
- `joined_at`: DateTime
|
|
- **Constraints:** UniqueConstraint(network_id, user_id)
|
|
- **Relationships:** `network`, `user`
|
|
|
|
**UserPreference** (`user_preferences`)
|
|
- `user_id`: Integer, FK to mtgonline_users.id, primary key
|
|
- `theme`: String(20), default "light"
|
|
- `notifications_enabled`: Boolean, default True
|
|
- `email_notifications`: Boolean, default True
|
|
- `auto_save_decks`: Boolean, default True
|
|
- `default_format`: String(50), default "standard"
|
|
- `language`: String(5), default "EN"
|
|
- `updated_at`: DateTime
|
|
- **Relationships:** `user`
|
|
|
|
**UserActivityLog** (`user_activity_log`)
|
|
- `id`: BigInteger, primary key
|
|
- `user_id`: Integer, FK to mtgonline_users.id, indexed
|
|
- `activity_type`: String(50), indexed
|
|
- `activity_data`: JSON
|
|
- `ip_address`: String(45)
|
|
- `created_at`: DateTime, indexed
|
|
- **Relationships:** `user`
|
|
|
|
---
|
|
|
|
### 1.5 User Deck Models (`user_deck.py`)
|
|
|
|
**UserDeck** (`user_decks`)
|
|
- `id`: BigInteger, primary key, autoincrement
|
|
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE, indexed
|
|
- `name`: String(255), indexed
|
|
- `status`: String(20), default "DRAFT", indexed
|
|
- `folder_id`: Integer, FK to mtgonline_decklist_folders.id
|
|
- `format`: String(50), default "standard"
|
|
- `notes`: Text
|
|
- `is_precedent`: Boolean, default False, indexed
|
|
- `precedent_name`: String(255)
|
|
- `created_at`: DateTime
|
|
- `updated_at`: DateTime
|
|
- **Relationships:** `user`, `folder`, `cards` (cascade, ordered by id)
|
|
|
|
**UserDeckCard** (`user_deck_cards`)
|
|
- `id`: BigInteger, primary key, autoincrement
|
|
- `deck_id`: BigInteger, FK to user_decks.id, CASCADE, indexed
|
|
- `card_id`: Integer, FK to mtgonline_cards.id, indexed
|
|
- `quantity`: Integer, default 1
|
|
- `zone`: String(20), default "main"
|
|
- `position`: Integer
|
|
- **Constraints:** UniqueConstraint(deck_id, card_id, zone)
|
|
- **Indexes:** `idx_deck_cards_deck`, `idx_deck_cards_card`
|
|
- **Relationships:** `deck`, `card`
|
|
|
|
**DeckPrecedent** (`deck_precedents`)
|
|
- `id`: BigInteger, primary key, autoincrement
|
|
- `name`: String(255), indexed
|
|
- `description`: Text
|
|
- `format`: String(50), default "standard"
|
|
- `is_public`: Boolean, default True, indexed
|
|
- `created_by`: Integer, FK to mtgonline_users.id
|
|
- `created_at`: DateTime
|
|
- `updated_at`: DateTime
|
|
- **Relationships:** `creator`, `cards` (cascade)
|
|
|
|
**DeckPrecedentCard** (`deck_precedent_cards`)
|
|
- `id`: BigInteger, primary key, autoincrement
|
|
- `precedent_id`: BigInteger, FK to deck_precedents.id, CASCADE, indexed
|
|
- `card_id`: Integer, indexed
|
|
- `quantity`: Integer, default 1
|
|
- `zone`: String(20), default "main"
|
|
- **Constraints:** UniqueConstraint(precedent_id, card_id, zone)
|
|
- **Relationships:** `precedent`
|
|
|
|
**CardSuggestion** (`card_suggestions`)
|
|
- `id`: BigInteger, primary key, autoincrement
|
|
- `deck_id`: BigInteger, FK to user_decks.id, CASCADE, indexed
|
|
- `card_id`: Integer, indexed
|
|
- `source_card_id`: Integer
|
|
- `suggestion_type`: String(50), default "SIMILAR"
|
|
- `confidence`: Float
|
|
- `notes`: Text
|
|
- `created_at`: DateTime
|
|
- **Constraints:** UniqueConstraint(deck_id, card_id, source_card_id)
|
|
- **Relationships:** `deck`
|
|
|
|
---
|
|
|
|
### 1.6 Card Import Models
|
|
|
|
**CardImportBatch** (`card_import_batches`)
|
|
- `id`: Integer, primary key, autoincrement
|
|
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE, indexed
|
|
- `filename`: String(255)
|
|
- `file_type`: String(10) (xlsx, csv, json, ods)
|
|
- `file_size`: Integer (bytes)
|
|
- `status`: String(20), default "pending", indexed (pending, processing, completed, failed)
|
|
- `total_cards`: Integer, default 0
|
|
- `matched_cards`: Integer, default 0
|
|
- `unmatched_cards`: Integer, default 0
|
|
- `match_results`: JSON
|
|
- `error_message`: Text
|
|
- `created_at`: DateTime
|
|
- `updated_at`: DateTime
|
|
- **Relationships:** `user`
|
|
|
|
**UserCardImportRecord** (`user_card_imports_confirmed`)
|
|
- `id`: Integer, primary key, autoincrement
|
|
- `user_id`: Integer, FK to mtgonline_users.id, CASCADE, indexed
|
|
- `batch_id`: Integer, FK to card_import_batches.id, CASCADE, indexed
|
|
- `is_confirmed`: Boolean, default True
|
|
- `confirmed_at`: DateTime
|
|
- **Relationships:** `user`, `batch`
|
|
|
|
---
|
|
|
|
## 2. Schemas (`app/schemas/`)
|
|
|
|
### 2.1 Core Schemas (`schemas.py`)
|
|
|
|
**Authentication Schemas:**
|
|
- `LoginRequest`: username (3-64 chars), password (6-128 chars)
|
|
- `LoginResponse`: access_token, refresh_token, token_type="bearer", user (dict)
|
|
- `RefreshTokenRequest`: refresh_token
|
|
- `TokenResponse`: access_token, token_type="bearer"
|
|
|
|
**User Schemas:**
|
|
- `UserBase`: username, email (optional), country (optional, max 2), real_name (optional)
|
|
- `UserCreate`: extends UserBase, password (min 8 chars)
|
|
- `UserUpdate`: email, country, real_name, new_password (optional, min 8, max 128)
|
|
- `UserResponse`: id, username, email, country, real_name, privlevel, vip_status, is_active, is_banned, ban_reason, creation_date, last_login (from_attributes)
|
|
|
|
**Deck Schemas:**
|
|
- `DeckCreate`: name (1-255 chars), content (min 1), folder_id (optional), format="native" (pattern: native|plain), status="DRAUGHT" (pattern: DRAUGHT|FINAL)
|
|
- `DeckUpdate`: name, content, folder_id, status (all optional)
|
|
- `DeckResponse`: id, name, content, format, status, folder_id, owner_id, creation_date (from_attributes)
|
|
- `FolderCreate`: name (1-255 chars), parent_id (optional)
|
|
- `FolderResponse`: id, name, parent_id, owner_id, creation_date (from_attributes)
|
|
|
|
**Game Schemas:**
|
|
- `GameCreate`: room_id, game_type (optional), description (optional), password (optional)
|
|
- `GameResponse`: id, room_id, game_type, description, with_password, max_players, player_count, started, creation_date (from_attributes)
|
|
|
|
**Room Schemas:**
|
|
- `RoomResponse`: id, name, description, is_password_protected, game_types (List[str]), player_count, creation_date (from_attributes)
|
|
|
|
**Ban Schemas:**
|
|
- `BanCreate`: user_id, reason (min 1, max 1000), expiration_time (optional)
|
|
- `BanResponse`: id, user_id, reason, moderators, expiration_time, active, creation_date (from_attributes)
|
|
|
|
**Error Schemas:**
|
|
- `ErrorResponse`: detail (str)
|
|
- `ValidationErrorResponse`: detail (List[dict])
|
|
|
|
**Pagination Schemas:**
|
|
- `PaginationParams`: page (1+, default 1), page_size (1-100, default 50)
|
|
- `PaginatedResponse`: items (List[dict]), total, page, page_size, total_pages
|
|
|
|
**Card Mirror Schemas:**
|
|
- `CardMirrorResponse`: id, source_id, name, mana_cost, type_line, oracle_text, power, toughness, rarity, layout, artist, flavor_text, numbers, identifiers, images, image, card_parts, keywords, legalities, set_code, set_name, synced_at, created_at (from_attributes)
|
|
- `DeckCardLinkResponse`: id, deck_id, card_id, quantity, zone, card (CardMirrorResponse) (from_attributes)
|
|
- `DeckWithCardsResponse`: id, name, content, format, status, folder_id, owner_id, creation_date, card_links (List[DeckCardLinkResponse]) (from_attributes)
|
|
|
|
---
|
|
|
|
### 2.2 User Data Schemas (`user_data_schemas.py`)
|
|
|
|
**Enums:**
|
|
- `DeckVersionStatus`: DRAFT, FINAL, ARCHIVED
|
|
- `GameReplayStatus`: IN_PROGRESS, COMPLETED, FAILED, CANCELLED
|
|
- `GameOutcomeType`: WIN, LOSS, CONCESSION, DISCONNECT
|
|
- `GroupMemberRole`: OWNER, ADMIN, MEMBER
|
|
- `NetworkMemberRole`: OWNER, ADMIN, MEMBER
|
|
- `UserPreferenceTheme`: LIGHT, DARK, SYSTEM
|
|
- `ActivityType`: LOGIN, LOGOUT, DECK_EDIT, GAME_PLAYED, CARD_ACQUIRED, CARD_TRADED, GROUP_CREATED, GROUP_JOINED
|
|
|
|
**Session Schemas:**
|
|
- `SessionResponse`: id, user_id, ip_address, user_agent, created_at, expires_at, is_active (from_attributes)
|
|
- `SessionCleanupResponse`: cleaned_count, message
|
|
|
|
**Deck Version Schemas:**
|
|
- `DeckVersionCreate`: content (min 1), status=DeckVersionStatus.DRAFT, comment (optional)
|
|
- `DeckVersionUpdate`: content, status, comment (all optional)
|
|
- `DeckVersionResponse`: id, deck_id, version_number, content, status, comment, created_at (from_attributes)
|
|
- `DeckVersionListResponse`: versions (List[DeckVersionResponse]), total
|
|
|
|
**Game Replay Schemas:**
|
|
- `GameReplayCreate`: game_uuid (36 chars), room_id, game_type, format, duration_seconds, start_time, end_time, status=IN_PROGRESS, replay_data (Dict)
|
|
- `GameReplayUpdate`: room_id, game_type, format, duration_seconds, end_time, status, replay_data (all optional)
|
|
- `GameReplayResponse`: id, game_uuid, room_id, game_type, format, duration_seconds, start_time, end_time, status, replay_data, created_at, updated_at, players (List[Dict]) (from_attributes)
|
|
- `GameReplayListResponse`: replays (List[GameReplayResponse]), total, page, page_size, total_pages
|
|
|
|
**Game Outcome Schemas:**
|
|
- `GameOutcomeCreate`: game_uuid, outcome=GameOutcomeType, opponent_id, format, rating_before, rating_after, rating_change
|
|
- `GameOutcomeResponse`: id, user_id, game_uuid, outcome, opponent_id, format, rating_before, rating_after, rating_change, created_at (from_attributes)
|
|
- `GameOutcomeListResponse`: outcomes (List[GameOutcomeResponse]), total
|
|
|
|
**User Statistics Schemas:**
|
|
- `UserStatisticsResponse`: user_id, total_games, total_wins, total_losses, total_concessions, win_rate, current_streak, best_streak, average_rating, last_game_date, updated_at (from_attributes)
|
|
- `StatisticsUpdateResponse`: user_id, total_games, total_wins, total_losses, win_rate, current_streak, updated_at
|
|
|
|
**Card Collection Schemas:**
|
|
- `CardCollectionCreate`: card_id, quantity=1 (ge=1), condition="NEAR_MINT" (max 20), language="EN" (max 5), is_foil=False, is_alt_art=False, acquired_date, acquisition_method, notes
|
|
- `CardCollectionUpdate`: quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes (all optional)
|
|
- `CardCollectionResponse`: id, user_id, card_id, quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes, created_at, updated_at (from_attributes)
|
|
- `CardCollectionListResponse`: cards (List[CardCollectionResponse]), total, page, page_size, total_pages
|
|
|
|
**Wishlist Schemas:**
|
|
- `WishlistCreate`: card_id, max_price, notes
|
|
- `WishlistUpdate`: max_price, notes (optional)
|
|
- `WishlistResponse`: id, user_id, card_id, max_price, notes, created_at (from_attributes)
|
|
- `WishlistListResponse`: items (List[WishlistResponse]), total
|
|
|
|
**Group Schemas:**
|
|
- `GroupCreate`: name (1-100 chars), description, is_public=True, max_members=50 (ge=2, le=500)
|
|
- `GroupUpdate`: name, description, is_public, max_members (optional)
|
|
- `GroupMemberCreate`: user_id, role=GroupMemberRole.MEMBER
|
|
- `GroupMemberUpdate`: role=GroupMemberRole
|
|
- `GroupMemberRemove`: user_id
|
|
- `GroupResponse`: id, name, description, owner_id, is_public, max_members, created_at, updated_at, member_count=0, is_member=False (from_attributes)
|
|
- `GroupListResponse`: groups (List[GroupResponse]), total
|
|
- `GroupChatMessageCreate`: message (1-2000 chars)
|
|
- `GroupChatMessageResponse`: id, group_id, sender_id, sender_username, message, created_at (from_attributes)
|
|
- `GroupChatMessageListResponse`: messages (List[GroupChatMessageResponse]), total, page, page_size, total_pages
|
|
|
|
**Network Schemas:**
|
|
- `NetworkCreate`: name (1-100 chars), description, is_public=True
|
|
- `NetworkUpdate`: name, description, is_public (optional)
|
|
- `NetworkMemberCreate`: user_id, role=NetworkMemberRole.MEMBER
|
|
- `NetworkResponse`: id, name, description, creator_id, is_public, created_at, member_count=0, is_member=False (from_attributes)
|
|
- `NetworkListResponse`: networks (List[NetworkResponse]), total
|
|
|
|
**Preference Schemas:**
|
|
- `UserPreferenceUpdate`: theme, notifications_enabled, email_notifications, auto_save_decks, default_format, language (all optional)
|
|
- `UserPreferenceResponse`: user_id, theme, notifications_enabled, email_notifications, auto_save_decks, default_format, language, updated_at (from_attributes)
|
|
|
|
**Activity Log Schemas:**
|
|
- `ActivityLogEntry`: id, user_id, activity_type, activity_data, ip_address, created_at (from_attributes)
|
|
- `ActivityLogListResponse`: entries (List[ActivityLogEntry]), total, page, page_size, total_pages
|
|
|
|
**Generic Schemas:**
|
|
- `MessageResponse`: message (str)
|
|
- `CountResponse`: count (int)
|
|
- `ErrorDetail`: error (str), detail (str)
|
|
|
|
---
|
|
|
|
### 2.3 User Deck Schemas (`user_deck_schemas.py`)
|
|
|
|
**Enums:**
|
|
- `DeckStatus`: DRAFT, FINAL
|
|
- `DeckZone`: MAIN, SIDEBOARD
|
|
- `SuggestionType`: SIMILAR, PAIRING, ALTERNATIVE
|
|
|
|
**Deck Schemas:**
|
|
- `UserDeckCreate`: name (1-255 chars), folder_id, format="standard" (max 50), notes, is_precedent=False, precedent_name
|
|
- `UserDeckUpdate`: name, folder_id, format, notes, status, is_precedent, precedent_name (all optional)
|
|
- `UserDeckResponse`: id, user_id, name, status, folder_id, format, notes, is_precedent, precedent_name, created_at, updated_at, card_count=0, is_owner=False (from_attributes)
|
|
- `UserDeckListResponse`: decks (List[UserDeckResponse]), total, page, page_size, total_pages
|
|
|
|
**Deck Card Schemas:**
|
|
- `DeckCardCreate`: card_id, quantity=1 (ge=1), zone=DeckZone.MAIN, position
|
|
- `DeckCardUpdate`: quantity, zone, position (optional)
|
|
- `DeckCardResponse`: id, deck_id, card_id, quantity, zone, position, created_at (from_attributes)
|
|
- `DeckCardWithDetailsResponse`: extends DeckCardResponse, card_name="", card_type_line="", card_image
|
|
- `DeckCardListResponse`: cards (List[DeckCardWithDetailsResponse]), total
|
|
|
|
**Deck Precedent Schemas:**
|
|
- `PrecedentCreate`: name (1-255 chars), description, format="standard" (max 50), is_public=True
|
|
- `PrecedentUpdate`: name, description, format, is_public (optional)
|
|
- `PrecedentResponse`: id, name, description, format, is_public, created_by, created_at, updated_at, card_count=0 (from_attributes)
|
|
- `PrecedentListResponse`: precedents (List[PrecedentResponse]), total, page, page_size, total_pages
|
|
|
|
**Card Suggestion Schemas:**
|
|
- `SuggestionCreate`: card_id, source_card_id, suggestion_type=SuggestionType.SIMILAR, confidence (0.0-1.0), notes
|
|
- `SuggestionResponse`: id, deck_id, card_id, source_card_id, suggestion_type, confidence, notes, created_at, card_name="" (from_attributes)
|
|
- `SuggestionListResponse`: suggestions (List[SuggestionResponse]), total
|
|
|
|
**Deck Action Schemas:**
|
|
- `DeckFinalizeRequest`: status=DeckStatus.FINAL
|
|
- `DeckFinalizeResponse`: deck_id, status, message
|
|
- `DeckDeleteResponse`: deck_id, message
|
|
|
|
**Search Schemas:**
|
|
- `CardSearchRequest`: query (1-100 chars), limit=50 (1-200), offset=0 (ge=0)
|
|
- `CardSearchResponse`: cards (List[Dict]), total, page, page_size, total_pages
|
|
|
|
**Generic Schemas:**
|
|
- `MessageResponse`: message (str)
|
|
- `CountResponse`: count (int)
|
|
|
|
---
|
|
|
|
### 2.4 Card Import Schemas (`card_import_schemas.py`)
|
|
|
|
- `CardImportRequest`: card_names (List[str], 1-10000 items)
|
|
- `CardImportResponse`: message, card_count, card_names, imported_at
|
|
- `CardImportStatusResponse`: has_import, card_count, card_names, last_imported
|
|
- `CardMatchResult`: card_id, card_name, matched_name, match_type (exact|fuzzy|partial), confidence (0.0-1.0)
|
|
- `CardImportSummary`: total_cards, matched_cards (List[CardMatchResult]), unmatched_cards (List[str]), import_id
|
|
- `MessageResponse`: message (str)
|
|
- `CountResponse`: count (int)
|
|
- `ErrorResponse`: detail (str), error_code
|
|
|
|
---
|
|
|
|
### 2.5 Card Search Schemas (`card_search_schemas.py`)
|
|
|
|
- `CardResponse`: id, name, mana_cost, type_line, oracle_text, power, toughness, rarity, layout, colors, set_code, set_name, identifiers (Dict), images (Dict) (from_attributes)
|
|
- `SetResponse`: id, name, code, release_date, card_count (from_attributes)
|
|
- `CardTypeResponse`: type (str)
|
|
- `CardSearchResponse`: cards (List[Dict]), total, page, page_size, total_pages
|
|
- `CardImportResponse`: message, batch_id, card_count, status, imported_at
|
|
- `CardImportStatusResponse`: has_import, batch_id, status, card_count, matched_count, unmatched_count, last_imported, error_message
|
|
- `CardMatchResult`: card_id, card_name, matched_name, match_type (exact|high_confidence|low_confidence), confidence (0.0-1.0)
|
|
- `CardImportSummary`: total_cards, matched_cards (List[CardMatchResult]), unmatched_cards (List[str]), import_id
|
|
- `MessageResponse`: message (str)
|
|
- `CountResponse`: count (int)
|
|
- `ErrorResponse`: detail (str), error_code
|
|
|
|
---
|
|
|
|
### 2.6 Protocol Schemas (`proto_messages.py`)
|
|
|
|
**Base:**
|
|
- `ProtoMessageBase`: message_type, timestamp
|
|
|
|
**Commands:**
|
|
- `SessionCommand`: message_type="SessionCommand", cmd_type (int), cmd_id=0, data (Dict)
|
|
- `GameCommand`: message_type="GameCommand", cmd_type (int), cmd_id=0, player_id, data (Dict)
|
|
- `GameEvent`: message_type="GameEvent", event_type (int), player_id, data (Dict)
|
|
- `Response`: message_type="Response", cmd_id, response_code (int), data (Dict)
|
|
|
|
**Server Info:**
|
|
- `ServerInfoUser`: id, name, user_level=0, address, real_name, country, avatar_bmp (bytes), server_id, session_id, accountage_secs, email, privlevel
|
|
- `ServerInfoDeckStorageFile`: creation_time
|
|
- `ServerInfoDeckStorageFolder`: items (List[Dict])
|
|
- `ServerInfoDeckStorageTreeItem`: id, name, file, folder
|
|
- `ServerInfoCard`: id, name, x, y, face_down=False, tapped=False, attacking=False, color, pt, annotation, destroy_on_zone_change=False, doesnt_untap=False, counter_list (List[Dict]), attach_player_id, attach_zone, attach_card_id, provider_id
|
|
- `ServerInfoZone`: name, zone_type=0, with_coords=False, card_count=0, card_list (List[ServerInfoCard]), always_reveal_top_card=False, always_look_at_top_card=False
|
|
- `ServerInfoGame`: server_id, room_id, game_id, description, with_password=False, max_players=4, game_types (List[int]), creator_info, only_buddies=False, only_registered=False, spectators_allowed=False, spectators_need_password=False, spectators_can_chat=False, spectators_omniscient=False, share_decklists_on_load=False, player_count=0, spectators_count=0, started=False, start_time, closed=False
|
|
|
|
---
|
|
|
|
### 2.7 Protocol Constants (`protocol_constants.py`)
|
|
|
|
**Enums:**
|
|
- `SessionCommandType` (IntEnum): PING=1000, LOGIN=1001, MESSAGE=1002, LIST_USERS=1003, GET_GAMES_OF_USER=1004, GET_USER_INFO=1005, ADD_TO_LIST=1006, REMOVE_FROM_LIST=1007, DECK_LIST=1008, DECK_NEW_DIR=1009, DECK_DEL_DIR=1010, DECK_DEL=1011, DECK_DOWNLOAD=1012, DECK_UPLOAD=1013, LIST_ROOMS=1014, JOIN_ROOM=1015, REGISTER=1016, ACTIVATE=1017, ACCOUNT_EDIT=1018, ACCOUNT_IMAGE=1019, ACCOUNT_PASSWORD=1020, FORGOT_PASSWORD_REQUEST=1021, FORGOT_PASSWORD_RESET=1022, FORGOT_PASSWORD_CHALLENGE=1023, REQUEST_PASSWORD_SALT=1024, SET_CARD_ART_PARAMS=1025, REPLAY_LIST=1100, REPLAY_DOWNLOAD=1101, REPLAY_MODIFY_MATCH=1102, REPLAY_DELETE_MATCH=1103, REPLAY_GET_CODE=1104, REPLAY_SUBMIT_CODE=1105
|
|
|
|
- `GameCommandType` (IntEnum): KICK_FROM_GAME=1000, LEAVE_GAME=1001, GAME_SAY=1002, SHUFFLE=1003, MULLIGAN=1004, ROLL_DIE=1005, DRAW_CARDS=1006, UNDO_DRAW=1007, FLIP_CARD=1008, ATTACH_CARD=1009, CREATE_TOKEN=1010, CREATE_ARROW=1011, DELETE_ARROW=1012, SET_CARD_ATTR=1013, SET_CARD_COUNTER=1014, INC_CARD_COUNTER=1015, READY_START=1016, CONCEDE=1017, INC_COUNTER=1018, CREATE_COUNTER=1019, SET_COUNTER=1020, DEL_COUNTER=1021, NEXT_TURN=1022, SET_ACTIVE_PHASE=1023, DUMP_ZONE=1024, REVEAL_CARDS=1026, MOVE_CARD=1027, SET_SIDEBOARD_PLAN=1028, DECK_SELECT=1029, SET_SIDEBOARD_LOCK=1030, CHANGE_ZONE_PROPERTIES=1031, UNCONCEDE=1032, JUDGE=1033, REVERSE_TURN=1034
|
|
|
|
- `GameEventType` (IntEnum): JOIN=1000, LEAVE=1001, GAME_CLOSED=1002, GAME_HOST_CHANGED=1003, KICKED=1004, GAME_STATE_CHANGED=1005, PLAYER_PROPERTIES_CHANGED=1007, GAME_SAY=1009, CREATE_ARROW=2000, DELETE_ARROW=2001, CREATE_COUNTER=2002, SET_COUNTER=2003, DEL_COUNTER=2004, DRAW_CARDS=2005, REVEAL_CARDS=2006, SHUFFLE=2007, ROLL_DIE=2008, MOVE_CARD=2009, FLIP_CARD=2010, DESTROY_CARD=2011, ATTACH_CARD=2012, CREATE_TOKEN=2013, SET_CARD_ATTR=2014, SET_CARD_COUNTER=2015, SET_ACTIVE_PLAYER=2016, SET_ACTIVE_PHASE=2017, DUMP_ZONE=2018, CHANGE_ZONE_PROPERTIES=2020, REVERSE_TURN=2021, GAME_LOG_NOTICE=2022
|
|
|
|
- `ResponseCode` (IntEnum): RespNotConnected=-1, RespNothing=0, RespOk=1, RespNotInRoom=2, RespInternalError=3, RespInvalidCommand=4, RespInvalidData=5, RespNameNotFound=6, RespLoginNeeded=7, RespFunctionNotAllowed=8, RespGameNotStarted=9, RespGameFull=10, RespContextError=11, RespWrongPassword=12, RespSpectatorsNotAllowed=13, RespOnlyBuddies=14, RespUserLevelTooLow=15, RespInIgnoreList=16, RespWouldOverwriteOldSession=17, RespChatFlood=18, RespUserIsBanned=19, RespAccessDenied=20, RespUsernameInvalid=21, RespRegistrationRequired=22, RespRegistrationAccepted=23, RespUserAlreadyExists=24, RespEmailRequiredToRegister=25, RespTooManyRequests=26, RespPasswordTooShort=27, RespAccountNotActivated=28, RespRegistrationDisabled=29, RespRegistrationFailed=30, RespActivationAccepted=31, RespActivationFailed=32, RespRegistrationAcceptedNeedsActivation=33, RespClientIdRequired=34, RespClientUpdateRequired=35, RespServerFull=36, RespEmailBlackListed=37
|
|
|
|
- `ZoneType` (IntEnum): PrivateZone=0, PublicZone=1, HiddenZone=2
|
|
|
|
- `UserLevelFlag` (IntFlag): IsNothing=0, IsUser=1, IsRegistered=2, IsModerator=4, IsAdmin=8, IsJudge=16
|
|
|
|
---
|
|
|
|
### 2.8 User Card Collection Schemas (`user_card_collection.py`)
|
|
|
|
**Enums:**
|
|
- `CardCondition`: NEAR_MINT, LIGHTLY_PLAYED, MODERATELY_PLAYED, HEAVILY_PLAYED, DAMAGED
|
|
- `AcquisitionMethod`: PACK_OPENING, TRADE, PURCHASE, GIFT, CONTEST, OTHER
|
|
|
|
**Card Collection Schemas:**
|
|
- `CardCollectionCreate`: card_id, quantity=1 (ge=1), condition=CardCondition.NEAR_MINT, language="EN" (max 5), is_foil=False, is_alt_art=False, acquired_date, acquisition_method, notes
|
|
- `CardCollectionUpdate`: quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes (optional)
|
|
- `CardCollectionResponse`: id, user_id, card_id, quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes, created_at, updated_at (from_attributes)
|
|
- `CardCollectionListResponse`: cards (List[CardCollectionResponse]), total, page, page_size, total_pages
|
|
|
|
**Wishlist Schemas:**
|
|
- `WishlistCreate`: card_id, max_price, notes
|
|
- `WishlistUpdate`: max_price, notes (optional)
|
|
- `WishlistResponse`: id, user_id, card_id, max_price, notes, created_at (from_attributes)
|
|
- `WishlistListResponse`: items (List[WishlistResponse]), total
|
|
|
|
**Collection Statistics Schemas:**
|
|
- `CollectionStatistics`: total_cards, unique_cards, total_quantity, foil_count, alt_art_count, condition_breakdown (Dict), language_breakdown (Dict), acquisition_breakdown (Dict)
|
|
- `CollectionSummaryResponse`: statistics (CollectionStatistics), recent_acquisitions (List[CardCollectionResponse]), top_cards (List[CardCollectionResponse])
|
|
|
|
**Generic Schemas:**
|
|
- `MessageResponse`: message (str)
|
|
- `CountResponse`: count (int)
|
|
- `ErrorDetail`: error (str), detail (str)
|
|
|
|
---
|
|
|
|
## 3. Routers (`app/routers/`)
|
|
|
|
### 3.1 Auth Router (`auth.py`)
|
|
|
|
**Endpoints:**
|
|
- `POST /login` - Authenticate user, return JWT tokens
|
|
- Request: LoginRequest
|
|
- Response: LoginResponse
|
|
- Dependencies: get_db (AsyncSession)
|
|
- Logic: Verify credentials, check account status, generate access/refresh tokens
|
|
|
|
- `POST /refresh` - Refresh access token
|
|
- Request: RefreshTokenRequest
|
|
- Response: TokenResponse
|
|
- Dependencies: get_db (AsyncSession)
|
|
- Logic: Validate refresh token, verify user still active, generate new access token
|
|
|
|
- `POST /register` - Register new user
|
|
- Request: UserCreate
|
|
- Response: UserResponse
|
|
- Dependencies: get_db (AsyncSession)
|
|
- Logic: Check username/email uniqueness, hash password, create user
|
|
|
|
- `GET /me` - Get current authenticated user
|
|
- Query: token (str)
|
|
- Response: UserResponse
|
|
- Dependencies: get_db (AsyncSession)
|
|
- Logic: Decode access token, fetch user
|
|
|
|
**Imports:**
|
|
- From `app.core.security`: verify_password, hash_password, create_access_token, create_refresh_token, decode_token
|
|
- From `app.models.models`: User
|
|
- From `app.schemas.schemas`: LoginRequest, LoginResponse, RefreshTokenRequest, TokenResponse, UserCreate, UserResponse
|
|
|
|
---
|
|
|
|
### 3.2 Users Router (`users.py`)
|
|
|
|
**Endpoints:**
|
|
- `GET /{user_id}` - Get user by ID
|
|
- Response: UserResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Fetch user, return response
|
|
|
|
- `PATCH /{user_id}` - Update user profile
|
|
- Request: UserUpdate
|
|
- Response: UserResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify ownership, update fields, hash new password if provided
|
|
|
|
- `POST /{user_id}/ban` - Ban user (admin only)
|
|
- Query: reason (str), expiration_time (optional str)
|
|
- Response: dict with message
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Check admin privileges, update ban status
|
|
|
|
- `POST /{user_id}/unban` - Unban user (admin only)
|
|
- Response: dict with message
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Check admin privileges, clear ban status
|
|
|
|
**Imports:**
|
|
- From `app.core.security`: get_current_user, hash_password
|
|
- From `app.models.models`: User
|
|
- From `app.schemas.schemas`: UserUpdate, UserResponse
|
|
|
|
---
|
|
|
|
### 3.3 Decks Router (`decks.py`)
|
|
|
|
**Deck CRUD:**
|
|
- `GET /` - List user's decks with filtering
|
|
- Query: status_filter, folder_id, is_precedent, page, page_size
|
|
- Response: UserDeckListResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Filter decks, count cards, paginate
|
|
|
|
- `POST /` - Create new user deck (DRAFT)
|
|
- Request: UserDeckCreate
|
|
- Response: UserDeckResponse (201)
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify folder, create deck
|
|
|
|
- `GET /{deck_id}` - Get specific deck
|
|
- Response: UserDeckResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify ownership, count cards
|
|
|
|
- `PATCH /{deck_id}` - Update deck
|
|
- Request: UserDeckUpdate
|
|
- Response: UserDeckResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify ownership, check not FINAL, update fields
|
|
|
|
- `DELETE /{deck_id}` - Delete deck
|
|
- Response: MessageResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify ownership, delete
|
|
|
|
**Deck Finalize:**
|
|
- `POST /{deck_id}/finalize` - Transition DRAFT to FINAL
|
|
- Response: DeckFinalizeResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify ownership, check not already FINAL, verify has cards, update status
|
|
|
|
**Deck Card Management:**
|
|
- `POST /{deck_id}/cards` - Add card to deck
|
|
- Request: DeckCardCreate
|
|
- Response: DeckCardResponse (201)
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify ownership, check not FINAL, verify card exists, handle duplicates (update quantity or create new)
|
|
|
|
- `GET /{deck_id}/cards` - Get all cards in deck
|
|
- Query: zone (optional)
|
|
- Response: DeckCardListResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify ownership, fetch cards with details from mirror
|
|
|
|
- `PATCH /{deck_id}/cards/{card_id}` - Update card in deck
|
|
- Request: DeckCardUpdate
|
|
- Response: DeckCardResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify ownership, check not FINAL, update quantity/zone/position
|
|
|
|
- `DELETE /{deck_id}/cards/{card_id}` - Remove card from deck
|
|
- Response: MessageResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify ownership, check not FINAL, delete card entry
|
|
|
|
**Deck Precedents:**
|
|
- `GET /precedents` - List available precedents
|
|
- Query: page, page_size, format_filter
|
|
- Response: PrecedentListResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Filter public precedents, count cards, paginate
|
|
|
|
- `POST /precedents` - Create precedent (template)
|
|
- Request: PrecedentCreate
|
|
- Response: PrecedentResponse (201)
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Create precedent with creator
|
|
|
|
- `GET /precedents/{precedent_id}` - Get specific precedent
|
|
- Response: PrecedentResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Fetch precedent, count cards
|
|
|
|
- `POST /precedents/{precedent_id}/use` - Clone precedent to new deck
|
|
- Response: dict with deck_id, deck_name, card_count
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Fetch precedent, create new deck, copy cards
|
|
|
|
**Card Search:**
|
|
- `POST /search/cards` - Search MTG cards
|
|
- Request: CardSearchRequest
|
|
- Response: CardSearchResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Search local mirror by name/type_line/mana_cost with ILIKE
|
|
|
|
**Card Suggestions:**
|
|
- `GET /{deck_id}/suggestions` - Get card suggestions
|
|
- Query: suggestion_type (optional)
|
|
- Response: SuggestionListResponse
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify ownership, fetch suggestions with card names
|
|
|
|
- `POST /{deck_id}/suggestions` - Add suggestion
|
|
- Request: SuggestionCreate
|
|
- Response: SuggestionResponse (201)
|
|
- Dependencies: get_db, get_current_user
|
|
- Logic: Verify ownership, check not FINAL, create suggestion
|
|
|
|
**Imports:**
|
|
- From `app.core.database`: get_db, mtg_get_db
|
|
- From `app.core.security`: get_current_user
|
|
- From `app.models.models`: User, DecklistFolder, MtgonlineCard
|
|
- From `app.models.mtg_models`: MtgCard, MtgSet
|
|
- From `app.models.user_deck`: UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard, CardSuggestion
|
|
- From `app.schemas.user_deck_schemas`: All deck-related schemas
|
|
|
|
---
|
|
|
|
### 3.4 Other Routers
|
|
|
|
**Rooms Router (`rooms.py`)**
|
|
- File exists but content not fully read
|
|
|
|
**Games Router (`games/`)**
|
|
- Directory exists (sub-routers)
|
|
|
|
**Admin Router (`admin.py`)**
|
|
- File exists but content not fully read
|
|
|
|
**Card Router (`card_router.py`)**
|
|
- File exists but content not fully read
|
|
|
|
**Interactions Router (`interactions.py`)**
|
|
- File exists but content not fully read
|
|
|
|
**Refresh Router (`refresh.py`)**
|
|
- File exists but content not fully read
|
|
|
|
**Card Import Router (`card_import.py`)**
|
|
- File exists but content not fully read
|
|
|
|
**Users Router (`users.py`)**
|
|
- Already documented above
|
|
|
|
**WS Router (`ws.py`)**
|
|
- File exists but content not fully read
|
|
|
|
---
|
|
|
|
## 4. Services (`app/services/`)
|
|
|
|
### 4.1 Card Database Service (`card_database.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.2 Card Mirror Service (`card_mirror_service.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.3 Card Search Service (`card_search_service.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.4 Deck Manager Service (`deck_manager.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.5 Deck Parser Service (`deck_parser.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.6 Deck Suggestion Service (`deck_suggestion_service.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.7 File Parser Service (`file_parser.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.8 Fuzzy Card Matcher Service (`fuzzy_card_matcher.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.9 Game Server Service (`game_server.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.10 Import Batch Processor Service (`import_batch_processor.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.11 MTGJSON Downloader Service (`mtgjson_downloader.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.12 MTGJSON Loader Service (`mtgjson_loader.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.13 MTGJSON Manager Service (`mtgjson_manager.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 4.14 MTGJSON Uploader Service (`mtgjson_uploader.py`)
|
|
- File exists but content not fully read
|
|
|
|
---
|
|
|
|
## 5. Core Configuration (`app/core/`)
|
|
|
|
### 5.1 Database (`database.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 5.2 Redis Client (`redis_client.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 5.3 Security (`security.py`)
|
|
- File exists but content not fully read
|
|
|
|
### 5.4 Settings (`settings.py`)
|
|
- File exists but content not fully read
|
|
|
|
---
|
|
|
|
## 6. Main Application (`app/main.py`)
|
|
|
|
- File exists but content not fully read
|
|
|
|
---
|
|
|
|
## 7. Alembic Migrations (`alembic/`)
|
|
|
|
- Migration directory exists
|
|
- Migration files present but not fully documented
|
|
|
|
---
|
|
|
|
## 8. Test Structure (`tests/`)
|
|
|
|
- Test directory exists
|
|
- Test files present but not fully documented
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
The MTG Online backend is a comprehensive FastAPI application with:
|
|
|
|
**Data Layer:**
|
|
- 20+ SQLAlchemy models across 6 model files
|
|
- Core user/deck/room management
|
|
- MTG card database integration (mtgjson.com)
|
|
- Card mirroring for performance
|
|
- User deck building with precedents
|
|
- Game replay and statistics
|
|
- Social features (groups, networks)
|
|
- Card collection and wishlist
|
|
|
|
**API Layer:**
|
|
- 3+ fully documented routers (auth, users, decks)
|
|
- 7+ additional routers (rooms, games, admin, card_router, interactions, refresh, card_import, ws)
|
|
- Comprehensive Pydantic schemas (8 schema files)
|
|
- Protocol buffer message definitions
|
|
- Protocol constants for MTG Online client
|
|
|
|
**Service Layer:**
|
|
- 14 service files for business logic
|
|
- Card database management
|
|
- Deck parsing and management
|
|
- Card search and fuzzy matching
|
|
- MTGJSON data loading and synchronization
|
|
- File import processing
|
|
|
|
**Infrastructure:**
|
|
- Async SQLAlchemy with PostgreSQL
|
|
- Redis for caching/sessions
|
|
- JWT authentication
|
|
- Alembic migrations
|
|
- Docker deployment
|
|
|
|
**Note:** This documentation covers the structure that was explicitly read. Several files (services, core configs, additional routers) exist but their detailed contents were not fully read in this session.
|