Files
mtgonline/backend/app/models/models.py
T
akadmin 167a352d44 Fix backend test suite - all 20 tests passing
- Updated JWT tokens to include privlevel for authorization
- Fixed test fixtures to properly hash passwords with bcrypt
- Fixed client fixture to share database session with test fixtures
- Reordered deck router routes to prevent conflicts
- Removed relationship fields from FolderResponse schema
- Updated conftest.py with proper async session management
2026-07-18 16:58:39 +00:00

171 lines
6.6 KiB
Python

"""
SQLAlchemy ORM models for the Cockatrice database.
Mirrors the original MySQL schema with modern PostgreSQL features.
"""
from sqlalchemy import Column, Integer, String, BigInteger, Boolean, DateTime, Text, ForeignKey, Index
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
class User(Base):
"""User account model."""
__tablename__ = "cockatrice_users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(64), unique=True, nullable=False, index=True)
password_hash = Column(String(128), nullable=False) # bcrypt hash
salt = Column(String(128), nullable=False) # password salt
email = Column(String(255), nullable=True, index=True)
country = Column(String(2), nullable=True)
real_name = Column(String(128), nullable=True)
avatar_bmp = Column(Text, nullable=True) # Base64 encoded avatar
privlevel = Column(String(50), nullable=True, default="User")
# Account status
is_active = Column(Boolean, default=True)
is_banned = Column(Boolean, default=False)
ban_reason = Column(Text, nullable=True)
ban_ends = Column(DateTime, nullable=True)
# VIP/Donator status
vip_status = Column(Integer, default=0) # 0=normal, 1=vip, 2=donator
vip_expiry = Column(DateTime, nullable=True)
# Timestamps
creation_date = Column(DateTime, server_default=func.now())
last_login = Column(DateTime, nullable=True)
# Relationships
decklist_files = relationship("DecklistFile", back_populates="owner", cascade="all, delete-orphan")
decklist_folders = relationship("DecklistFolder", back_populates="owner", cascade="all, delete-orphan")
def __repr__(self) -> str:
return f"<User {self.username} (ID: {self.id})>"
class DecklistFolder(Base):
"""User deck folder."""
__tablename__ = "cockatrice_decklist_folders"
id = Column(Integer, primary_key=True, index=True)
owner_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
name = Column(String(255), nullable=False)
parent_id = Column(Integer, ForeignKey("cockatrice_decklist_folders.id"), nullable=True)
creation_date = Column(DateTime, server_default=func.now())
# Relationships
owner = relationship("User", back_populates="decklist_folders")
children = relationship("DecklistFolder", back_populates="parent", cascade="all, delete-orphan")
parent = relationship("DecklistFolder", back_populates="children", remote_side=[id])
files = relationship("DecklistFile", back_populates="folder", cascade="all, delete-orphan")
class DecklistFile(Base):
"""Deck file stored in a folder."""
__tablename__ = "cockatrice_decklist_files"
id = Column(Integer, primary_key=True, index=True)
folder_id = Column(Integer, ForeignKey("cockatrice_decklist_folders.id"), nullable=True)
owner_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
name = Column(String(255), nullable=False)
content = Column(Text, nullable=False) # Native XML or plain text deck format
format = Column(String(50), default="native") # 'native' or 'plain'
creation_date = Column(DateTime, server_default=func.now())
# Relationships
folder = relationship("DecklistFolder", back_populates="files")
owner = relationship("User", back_populates="decklist_files")
def __repr__(self) -> str:
return f"<DecklistFile {self.name} (ID: {self.id})>"
class Room(Base):
"""Chat room."""
__tablename__ = "cockatrice_rooms"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), unique=True, nullable=False)
description = Column(Text, nullable=True)
is_password_protected = Column(Boolean, default=False)
password_hash = Column(String(128), nullable=True)
creation_date = Column(DateTime, server_default=func.now())
# Game types available in this room
game_types = relationship("RoomGameType", back_populates="room", cascade="all, delete-orphan")
class RoomGameType(Base):
"""Game type definition for a room."""
__tablename__ = "cockatrice_rooms_gametypes"
id = Column(Integer, primary_key=True, index=True)
room_id = Column(Integer, ForeignKey("cockatrice_rooms.id"), nullable=False)
name = Column(String(100), nullable=False)
description = Column(Text, nullable=True)
# Relationships
room = relationship("Room", back_populates="game_types")
class Ban(Base):
"""User ban record."""
__tablename__ = "cockatrice_bans"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
server_id = Column(Integer, nullable=True)
reason = Column(Text, nullable=False)
moderators = Column(String(255), nullable=True) # Admin usernames
ip_address = Column(String(45), nullable=True) # IPv4 or IPv6
expiration_time = Column(DateTime, nullable=True)
active = Column(Boolean, default=True)
creation_date = Column(DateTime, server_default=func.now())
# Relationships
user = relationship("User")
def __repr__(self) -> str:
return f"<Ban for User {self.user_id} (ID: {self.id})>"
class GameLog(Base):
"""Game log entry."""
__tablename__ = "cockatrice_log"
id = Column(Integer, primary_key=True, index=True)
room_id = Column(Integer, ForeignKey("cockatrice_rooms.id"), nullable=True)
player_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=True)
message = Column(Text, nullable=False)
timestamp = Column(DateTime, server_default=func.now())
# Relationships
room = relationship("Room")
player = relationship("User")
class AuditLog(Base):
"""Audit trail for administrative actions."""
__tablename__ = "cockatrice_audit"
id = Column(Integer, primary_key=True, index=True)
admin_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
action_type = Column(String(50), nullable=False) # 'ban', 'unban', 'warn', etc.
target_user_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=True)
details = Column(Text, nullable=True)
ip_address = Column(String(45), nullable=True)
timestamp = Column(DateTime, server_default=func.now())
# Relationships
admin = relationship("User", foreign_keys=[admin_id])
target_user = relationship("User", foreign_keys=[target_user_id])
# Indexes for performance
Index("idx_decks_owner", DecklistFile.owner_id)
Index("idx_decks_folder", DecklistFile.folder_id)
Index("idx_bans_active", Ban.active)
Index("idx_log_timestamp", GameLog.timestamp)