""" User-related models for Golf game authentication. Defines user accounts, sessions, and guest tracking for the V2 auth system. """ from dataclasses import dataclass, field from datetime import datetime, timezone from enum import Enum from typing import Optional, Any import json class UserRole(str, Enum): """User role levels.""" GUEST = "guest" USER = "user" ADMIN = "admin" @dataclass class User: """ A registered user account. Attributes: id: UUID primary key. username: Unique display name. email: Optional email address. password_hash: bcrypt hash of password. role: User role (guest, user, admin). email_verified: Whether email has been verified. verification_token: Token for email verification. verification_expires: When verification token expires. reset_token: Token for password reset. reset_expires: When reset token expires. guest_id: Guest session ID if converted from guest. deleted_at: Soft delete timestamp. preferences: User preferences as JSON. created_at: When account was created. last_login: Last login timestamp. last_seen_at: Last activity timestamp. is_active: Whether account is active. is_banned: Whether user is banned. ban_reason: Reason for ban (if banned). force_password_reset: Whether user must reset password on next login. """ id: str username: str password_hash: str email: Optional[str] = None role: UserRole = UserRole.USER email_verified: bool = False verification_token: Optional[str] = None verification_expires: Optional[datetime] = None reset_token: Optional[str] = None reset_expires: Optional[datetime] = None guest_id: Optional[str] = None deleted_at: Optional[datetime] = None preferences: dict = field(default_factory=dict) created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) last_login: Optional[datetime] = None last_seen_at: Optional[datetime] = None is_active: bool = True is_banned: bool = False ban_reason: Optional[str] = None force_password_reset: bool = False def is_admin(self) -> bool: """Check if user has admin role.""" return self.role == UserRole.ADMIN def is_guest(self) -> bool: """Check if user has guest role.""" return self.role == UserRole.GUEST def can_login(self) -> bool: """Check if user can log in.""" return self.is_active and self.deleted_at is None and not self.is_banned def to_dict(self, include_sensitive: bool = False) -> dict: """ Serialize user to dictionary. Args: include_sensitive: Include password hash and tokens. """ d = { "id": self.id, "username": self.username, "email": self.email, "role": self.role.value, "email_verified": self.email_verified, "preferences": self.preferences, "created_at": self.created_at.isoformat() if self.created_at else None, "last_login": self.last_login.isoformat() if self.last_login else None, "last_seen_at": self.last_seen_at.isoformat() if self.last_seen_at else None, "is_active": self.is_active, "is_banned": self.is_banned, "ban_reason": self.ban_reason, "force_password_reset": self.force_password_reset, } if include_sensitive: d["password_hash"] = self.password_hash d["verification_token"] = self.verification_token d["verification_expires"] = ( self.verification_expires.isoformat() if self.verification_expires else None ) d["reset_token"] = self.reset_token d["reset_expires"] = ( self.reset_expires.isoformat() if self.reset_expires else None ) d["guest_id"] = self.guest_id d["deleted_at"] = self.deleted_at.isoformat() if self.deleted_at else None return d @classmethod def from_dict(cls, d: dict) -> "User": """Deserialize user from dictionary.""" def parse_dt(val: Any) -> Optional[datetime]: if val is None: return None if isinstance(val, datetime): return val return datetime.fromisoformat(val) return cls( id=d["id"], username=d["username"], password_hash=d.get("password_hash", ""), email=d.get("email"), role=UserRole(d.get("role", "user")), email_verified=d.get("email_verified", False), verification_token=d.get("verification_token"), verification_expires=parse_dt(d.get("verification_expires")), reset_token=d.get("reset_token"), reset_expires=parse_dt(d.get("reset_expires")), guest_id=d.get("guest_id"), deleted_at=parse_dt(d.get("deleted_at")), preferences=d.get("preferences", {}), created_at=parse_dt(d.get("created_at")) or datetime.now(timezone.utc), last_login=parse_dt(d.get("last_login")), last_seen_at=parse_dt(d.get("last_seen_at")), is_active=d.get("is_active", True), is_banned=d.get("is_banned", False), ban_reason=d.get("ban_reason"), force_password_reset=d.get("force_password_reset", False), ) @dataclass class UserSession: """ An active user session. Session tokens are hashed before storage for security. Attributes: id: UUID primary key. user_id: Reference to user. token_hash: SHA256 hash of session token. device_info: Device/browser information. ip_address: Client IP address. created_at: When session was created. expires_at: When session expires. last_used_at: Last activity timestamp. revoked_at: When session was revoked (if any). """ id: str user_id: str token_hash: str device_info: dict = field(default_factory=dict) ip_address: Optional[str] = None created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) expires_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) last_used_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) revoked_at: Optional[datetime] = None def is_valid(self) -> bool: """Check if session is still valid.""" now = datetime.now(timezone.utc) return ( self.revoked_at is None and self.expires_at > now ) def to_dict(self) -> dict: """Serialize session to dictionary.""" return { "id": self.id, "user_id": self.user_id, "token_hash": self.token_hash, "device_info": self.device_info, "ip_address": self.ip_address, "created_at": self.created_at.isoformat() if self.created_at else None, "expires_at": self.expires_at.isoformat() if self.expires_at else None, "last_used_at": self.last_used_at.isoformat() if self.last_used_at else None, "revoked_at": self.revoked_at.isoformat() if self.revoked_at else None, } @classmethod def from_dict(cls, d: dict) -> "UserSession": """Deserialize session from dictionary.""" def parse_dt(val: Any) -> Optional[datetime]: if val is None: return None if isinstance(val, datetime): return val return datetime.fromisoformat(val) return cls( id=d["id"], user_id=d["user_id"], token_hash=d["token_hash"], device_info=d.get("device_info", {}), ip_address=d.get("ip_address"), created_at=parse_dt(d.get("created_at")) or datetime.now(timezone.utc), expires_at=parse_dt(d.get("expires_at")) or datetime.now(timezone.utc), last_used_at=parse_dt(d.get("last_used_at")) or datetime.now(timezone.utc), revoked_at=parse_dt(d.get("revoked_at")), ) @dataclass class GuestSession: """ A guest session for tracking anonymous users. Guests can play games without registering. Their session can later be converted to a full user account. Attributes: id: Guest session ID (stored in client). display_name: Display name for the guest. created_at: When session was created. last_seen_at: Last activity timestamp. games_played: Number of games played as guest. converted_to_user_id: User ID if converted to account. expires_at: When guest session expires. """ id: str display_name: Optional[str] = None created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) last_seen_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) games_played: int = 0 converted_to_user_id: Optional[str] = None expires_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) def is_converted(self) -> bool: """Check if guest has been converted to user.""" return self.converted_to_user_id is not None def is_expired(self) -> bool: """Check if guest session has expired.""" return datetime.now(timezone.utc) > self.expires_at def to_dict(self) -> dict: """Serialize guest session to dictionary.""" return { "id": self.id, "display_name": self.display_name, "created_at": self.created_at.isoformat() if self.created_at else None, "last_seen_at": self.last_seen_at.isoformat() if self.last_seen_at else None, "games_played": self.games_played, "converted_to_user_id": self.converted_to_user_id, "expires_at": self.expires_at.isoformat() if self.expires_at else None, } @classmethod def from_dict(cls, d: dict) -> "GuestSession": """Deserialize guest session from dictionary.""" def parse_dt(val: Any) -> Optional[datetime]: if val is None: return None if isinstance(val, datetime): return val return datetime.fromisoformat(val) return cls( id=d["id"], display_name=d.get("display_name"), created_at=parse_dt(d.get("created_at")) or datetime.now(timezone.utc), last_seen_at=parse_dt(d.get("last_seen_at")) or datetime.now(timezone.utc), games_played=d.get("games_played", 0), converted_to_user_id=d.get("converted_to_user_id"), expires_at=parse_dt(d.get("expires_at")) or datetime.now(timezone.utc), )