597 lines
17 KiB
Python
597 lines
17 KiB
Python
"""
|
|
User model with async support and enhanced security
|
|
"""
|
|
import hashlib
|
|
import secrets
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional, List, Dict, Any
|
|
from enum import Enum
|
|
|
|
from sqlalchemy import Column, String, BigInteger, Boolean, Integer, Index, text, DateTime
|
|
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.future import select
|
|
from sqlalchemy.orm import relationship
|
|
import structlog
|
|
|
|
from app.core.models.base import BaseModel
|
|
from app.core.config import settings
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
|
|
class UserRole(str, Enum):
|
|
"""User role enumeration"""
|
|
USER = "user"
|
|
MODERATOR = "moderator"
|
|
ADMIN = "admin"
|
|
SUPER_ADMIN = "super_admin"
|
|
|
|
|
|
class UserStatus(str, Enum):
|
|
"""User status enumeration"""
|
|
ACTIVE = "active"
|
|
SUSPENDED = "suspended"
|
|
BANNED = "banned"
|
|
PENDING = "pending"
|
|
|
|
|
|
class User(BaseModel):
|
|
"""Enhanced User model with security and async support"""
|
|
|
|
__tablename__ = 'users'
|
|
|
|
# Telegram specific fields
|
|
telegram_id = Column(
|
|
BigInteger,
|
|
nullable=False,
|
|
unique=True,
|
|
index=True,
|
|
comment="Telegram user ID"
|
|
)
|
|
username = Column(
|
|
String(512),
|
|
nullable=True,
|
|
index=True,
|
|
comment="Telegram username"
|
|
)
|
|
first_name = Column(
|
|
String(256),
|
|
nullable=True,
|
|
comment="User first name"
|
|
)
|
|
last_name = Column(
|
|
String(256),
|
|
nullable=True,
|
|
comment="User last name"
|
|
)
|
|
|
|
# Localization
|
|
language_code = Column(
|
|
String(8),
|
|
nullable=False,
|
|
default="en",
|
|
comment="User language code"
|
|
)
|
|
|
|
# Security and access control
|
|
role = Column(
|
|
String(32),
|
|
nullable=False,
|
|
default=UserRole.USER.value,
|
|
index=True,
|
|
comment="User role"
|
|
)
|
|
permissions = Column(
|
|
ARRAY(String),
|
|
nullable=False,
|
|
default=list,
|
|
comment="User permissions list"
|
|
)
|
|
|
|
# Activity tracking
|
|
last_activity = Column(
|
|
"last_use", # Keep old column name for compatibility
|
|
DateTime,
|
|
nullable=False,
|
|
default=datetime.utcnow,
|
|
index=True,
|
|
comment="Last user activity timestamp"
|
|
)
|
|
login_count = Column(
|
|
Integer,
|
|
nullable=False,
|
|
default=0,
|
|
comment="Total login count"
|
|
)
|
|
|
|
# Account status
|
|
is_verified = Column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
comment="Whether user is verified"
|
|
)
|
|
is_premium = Column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
comment="Whether user has premium access"
|
|
)
|
|
|
|
# Security settings
|
|
two_factor_enabled = Column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
comment="Whether 2FA is enabled"
|
|
)
|
|
security_settings = Column(
|
|
JSONB,
|
|
nullable=False,
|
|
default=dict,
|
|
comment="User security settings"
|
|
)
|
|
|
|
# Preferences
|
|
preferences = Column(
|
|
JSONB,
|
|
nullable=False,
|
|
default=dict,
|
|
comment="User preferences and settings"
|
|
)
|
|
|
|
# Statistics
|
|
content_uploaded_count = Column(
|
|
Integer,
|
|
nullable=False,
|
|
default=0,
|
|
comment="Number of content items uploaded"
|
|
)
|
|
content_purchased_count = Column(
|
|
Integer,
|
|
nullable=False,
|
|
default=0,
|
|
comment="Number of content items purchased"
|
|
)
|
|
|
|
# Rate limiting
|
|
rate_limit_reset = Column(
|
|
DateTime,
|
|
nullable=True,
|
|
comment="Rate limit reset timestamp"
|
|
)
|
|
rate_limit_count = Column(
|
|
Integer,
|
|
nullable=False,
|
|
default=0,
|
|
comment="Current rate limit count"
|
|
)
|
|
|
|
# Relationships
|
|
balances = relationship('UserBalance', back_populates='user', cascade="all, delete-orphan")
|
|
transactions = relationship('InternalTransaction', back_populates='user', cascade="all, delete-orphan")
|
|
wallet_connections = relationship('WalletConnection', back_populates='user', cascade="all, delete-orphan")
|
|
content_items = relationship('UserContent', back_populates='user', cascade="all, delete-orphan")
|
|
actions = relationship('UserAction', back_populates='user', cascade="all, delete-orphan")
|
|
activities = relationship('UserActivity', back_populates='user', cascade="all, delete-orphan")
|
|
|
|
# Indexes for performance
|
|
__table_args__ = (
|
|
Index('idx_users_telegram_id', 'telegram_id'),
|
|
Index('idx_users_username', 'username'),
|
|
Index('idx_users_role_status', 'role', 'status'),
|
|
Index('idx_users_last_activity', 'last_use'), # Use actual database column name
|
|
Index('idx_users_created_at', 'created_at'),
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
"""String representation"""
|
|
return f"User({self.id}, telegram_id={self.telegram_id}, username={self.username})"
|
|
|
|
@property
|
|
def full_name(self) -> str:
|
|
"""Get user's full name"""
|
|
parts = [self.first_name, self.last_name]
|
|
return " ".join(filter(None, parts)) or self.username or f"User_{self.telegram_id}"
|
|
|
|
@property
|
|
def display_name(self) -> str:
|
|
"""Get user's display name"""
|
|
return self.username or self.full_name
|
|
|
|
@property
|
|
def is_admin(self) -> bool:
|
|
"""Check if user is admin"""
|
|
return self.role in [UserRole.ADMIN.value, UserRole.SUPER_ADMIN.value]
|
|
|
|
@property
|
|
def is_moderator(self) -> bool:
|
|
"""Check if user is moderator or higher"""
|
|
return self.role in [UserRole.MODERATOR.value, UserRole.ADMIN.value, UserRole.SUPER_ADMIN.value]
|
|
|
|
@property
|
|
def cache_key(self) -> str:
|
|
"""Override cache key to include telegram_id"""
|
|
return f"user:telegram:{self.telegram_id}"
|
|
|
|
def has_permission(self, permission: str) -> bool:
|
|
"""Check if user has specific permission"""
|
|
if self.is_admin:
|
|
return True
|
|
return permission in (self.permissions or [])
|
|
|
|
def add_permission(self, permission: str) -> None:
|
|
"""Add permission to user"""
|
|
if not self.permissions:
|
|
self.permissions = []
|
|
if permission not in self.permissions:
|
|
self.permissions.append(permission)
|
|
|
|
def remove_permission(self, permission: str) -> None:
|
|
"""Remove permission from user"""
|
|
if self.permissions and permission in self.permissions:
|
|
self.permissions.remove(permission)
|
|
|
|
def update_activity(self) -> None:
|
|
"""Update user activity timestamp"""
|
|
self.last_activity = datetime.utcnow()
|
|
self.login_count += 1
|
|
|
|
def check_rate_limit(self, limit: int = None, window: int = None) -> bool:
|
|
"""Check if user is within rate limits"""
|
|
if self.is_admin:
|
|
return True
|
|
|
|
limit = limit or settings.RATE_LIMIT_REQUESTS
|
|
window = window or settings.RATE_LIMIT_WINDOW
|
|
|
|
now = datetime.utcnow()
|
|
|
|
# Reset counter if window has passed
|
|
if not self.rate_limit_reset or now > self.rate_limit_reset:
|
|
self.rate_limit_reset = now + timedelta(seconds=window)
|
|
self.rate_limit_count = 0
|
|
|
|
return self.rate_limit_count < limit
|
|
|
|
def increment_rate_limit(self) -> None:
|
|
"""Increment rate limit counter"""
|
|
if not self.is_admin:
|
|
self.rate_limit_count += 1
|
|
|
|
def set_preference(self, key: str, value: Any) -> None:
|
|
"""Set user preference"""
|
|
if not self.preferences:
|
|
self.preferences = {}
|
|
self.preferences[key] = value
|
|
|
|
def get_preference(self, key: str, default: Any = None) -> Any:
|
|
"""Get user preference"""
|
|
if not self.preferences:
|
|
return default
|
|
return self.preferences.get(key, default)
|
|
|
|
def set_security_setting(self, key: str, value: Any) -> None:
|
|
"""Set security setting"""
|
|
if not self.security_settings:
|
|
self.security_settings = {}
|
|
self.security_settings[key] = value
|
|
|
|
def get_security_setting(self, key: str, default: Any = None) -> Any:
|
|
"""Get security setting"""
|
|
if not self.security_settings:
|
|
return default
|
|
return self.security_settings.get(key, default)
|
|
|
|
def generate_api_token(self) -> str:
|
|
"""Generate secure API token for user"""
|
|
token_data = f"{self.id}:{self.telegram_id}:{datetime.utcnow().timestamp()}:{secrets.token_hex(16)}"
|
|
return hashlib.sha256(token_data.encode()).hexdigest()
|
|
|
|
def can_upload_content(self) -> bool:
|
|
"""Check if user can upload content"""
|
|
if self.status != UserStatus.ACTIVE.value:
|
|
return False
|
|
|
|
if not self.check_rate_limit(limit=10, window=3600): # 10 uploads per hour
|
|
return False
|
|
|
|
return True
|
|
|
|
def can_purchase_content(self) -> bool:
|
|
"""Check if user can purchase content"""
|
|
return self.status == UserStatus.ACTIVE.value
|
|
|
|
@classmethod
|
|
async def get_by_telegram_id(
|
|
cls,
|
|
session: AsyncSession,
|
|
telegram_id: int
|
|
) -> Optional['User']:
|
|
"""Get user by Telegram ID"""
|
|
try:
|
|
stmt = select(cls).where(cls.telegram_id == telegram_id)
|
|
result = await session.execute(stmt)
|
|
return result.scalar_one_or_none()
|
|
except Exception as e:
|
|
logger.error("Error getting user by telegram_id", telegram_id=telegram_id, error=str(e))
|
|
return None
|
|
|
|
@classmethod
|
|
async def get_by_username(
|
|
cls,
|
|
session: AsyncSession,
|
|
username: str
|
|
) -> Optional['User']:
|
|
"""Get user by username"""
|
|
try:
|
|
stmt = select(cls).where(cls.username == username)
|
|
result = await session.execute(stmt)
|
|
return result.scalar_one_or_none()
|
|
except Exception as e:
|
|
logger.error("Error getting user by username", username=username, error=str(e))
|
|
return None
|
|
|
|
@classmethod
|
|
async def get_active_users(
|
|
cls,
|
|
session: AsyncSession,
|
|
days: int = 30,
|
|
limit: Optional[int] = None
|
|
) -> List['User']:
|
|
"""Get active users within specified days"""
|
|
try:
|
|
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
|
stmt = select(cls).where(
|
|
cls.last_activity >= cutoff_date,
|
|
cls.status == UserStatus.ACTIVE.value
|
|
).order_by(cls.last_activity.desc())
|
|
|
|
if limit:
|
|
stmt = stmt.limit(limit)
|
|
|
|
result = await session.execute(stmt)
|
|
return result.scalars().all()
|
|
except Exception as e:
|
|
logger.error("Error getting active users", days=days, error=str(e))
|
|
return []
|
|
|
|
@classmethod
|
|
async def get_admins(cls, session: AsyncSession) -> List['User']:
|
|
"""Get all admin users"""
|
|
try:
|
|
stmt = select(cls).where(
|
|
cls.role.in_([UserRole.ADMIN.value, UserRole.SUPER_ADMIN.value])
|
|
)
|
|
result = await session.execute(stmt)
|
|
return result.scalars().all()
|
|
except Exception as e:
|
|
logger.error("Error getting admin users", error=str(e))
|
|
return []
|
|
|
|
@classmethod
|
|
async def create_from_telegram(
|
|
cls,
|
|
session: AsyncSession,
|
|
telegram_id: int,
|
|
username: Optional[str] = None,
|
|
first_name: Optional[str] = None,
|
|
last_name: Optional[str] = None,
|
|
language_code: str = "en"
|
|
) -> 'User':
|
|
"""Create user from Telegram data"""
|
|
user = cls(
|
|
telegram_id=telegram_id,
|
|
username=username,
|
|
first_name=first_name,
|
|
last_name=last_name,
|
|
language_code=language_code,
|
|
status=UserStatus.ACTIVE.value
|
|
)
|
|
|
|
session.add(user)
|
|
await session.commit()
|
|
await session.refresh(user)
|
|
|
|
logger.info("User created from Telegram", telegram_id=telegram_id, user_id=user.id)
|
|
return user
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary with safe data"""
|
|
data = super().to_dict()
|
|
|
|
# Remove sensitive fields
|
|
sensitive_fields = ['security_settings', 'permissions']
|
|
for field in sensitive_fields:
|
|
data.pop(field, None)
|
|
|
|
return data
|
|
|
|
def to_public_dict(self) -> Dict[str, Any]:
|
|
"""Convert to public dictionary with minimal data"""
|
|
return {
|
|
'id': str(self.id),
|
|
'username': self.username,
|
|
'display_name': self.display_name,
|
|
'is_verified': self.is_verified,
|
|
'is_premium': self.is_premium,
|
|
'created_at': self.created_at.isoformat() if self.created_at else None
|
|
}
|
|
|
|
|
|
class UserSession(BaseModel):
|
|
"""User session model for authentication tracking"""
|
|
|
|
__tablename__ = 'user_sessions'
|
|
|
|
user_id = Column(
|
|
BigInteger,
|
|
nullable=False,
|
|
index=True,
|
|
comment="Associated user ID"
|
|
)
|
|
refresh_token_hash = Column(
|
|
String(255),
|
|
nullable=False,
|
|
comment="Hashed refresh token"
|
|
)
|
|
ip_address = Column(
|
|
String(45),
|
|
nullable=True,
|
|
comment="Session IP address"
|
|
)
|
|
user_agent = Column(
|
|
String(512),
|
|
nullable=True,
|
|
comment="User agent string"
|
|
)
|
|
expires_at = Column(
|
|
DateTime,
|
|
nullable=False,
|
|
comment="Session expiration time"
|
|
)
|
|
last_used_at = Column(
|
|
DateTime,
|
|
nullable=True,
|
|
comment="Last time session was used"
|
|
)
|
|
logged_out_at = Column(
|
|
DateTime,
|
|
nullable=True,
|
|
comment="Session logout time"
|
|
)
|
|
is_active = Column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=True,
|
|
comment="Whether session is active"
|
|
)
|
|
remember_me = Column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
comment="Whether this is a remember me session"
|
|
)
|
|
|
|
# Indexes for performance
|
|
__table_args__ = (
|
|
Index('idx_user_sessions_user_id', 'user_id'),
|
|
Index('idx_user_sessions_expires_at', 'expires_at'),
|
|
Index('idx_user_sessions_active', 'is_active'),
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
return f"UserSession({self.id}, user_id={self.user_id}, active={self.is_active})"
|
|
|
|
def is_expired(self) -> bool:
|
|
"""Check if session is expired"""
|
|
return datetime.utcnow() > self.expires_at
|
|
|
|
def is_valid(self) -> bool:
|
|
"""Check if session is valid and active"""
|
|
return self.is_active and not self.is_expired() and not self.logged_out_at
|
|
|
|
|
|
class UserRole(BaseModel):
|
|
"""User role model for permissions"""
|
|
|
|
__tablename__ = 'user_roles'
|
|
|
|
name = Column(
|
|
String(64),
|
|
nullable=False,
|
|
unique=True,
|
|
index=True,
|
|
comment="Role name"
|
|
)
|
|
description = Column(
|
|
String(255),
|
|
nullable=True,
|
|
comment="Role description"
|
|
)
|
|
permissions = Column(
|
|
ARRAY(String),
|
|
nullable=False,
|
|
default=list,
|
|
comment="Role permissions list"
|
|
)
|
|
is_system = Column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
comment="Whether this is a system role"
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
return f"UserRole({self.name})"
|
|
|
|
def has_permission(self, permission: str) -> bool:
|
|
"""Check if role has specific permission"""
|
|
return permission in (self.permissions or [])
|
|
|
|
|
|
class ApiKey(BaseModel):
|
|
"""API key model for programmatic access"""
|
|
|
|
__tablename__ = 'api_keys'
|
|
|
|
user_id = Column(
|
|
BigInteger,
|
|
nullable=False,
|
|
index=True,
|
|
comment="Associated user ID"
|
|
)
|
|
name = Column(
|
|
String(128),
|
|
nullable=False,
|
|
comment="API key name"
|
|
)
|
|
key_hash = Column(
|
|
String(255),
|
|
nullable=False,
|
|
unique=True,
|
|
comment="Hashed API key"
|
|
)
|
|
permissions = Column(
|
|
ARRAY(String),
|
|
nullable=False,
|
|
default=list,
|
|
comment="API key permissions"
|
|
)
|
|
expires_at = Column(
|
|
DateTime,
|
|
nullable=True,
|
|
comment="API key expiration time"
|
|
)
|
|
last_used_at = Column(
|
|
DateTime,
|
|
nullable=True,
|
|
comment="Last time key was used"
|
|
)
|
|
is_active = Column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=True,
|
|
comment="Whether key is active"
|
|
)
|
|
|
|
# Indexes for performance
|
|
__table_args__ = (
|
|
Index('idx_api_keys_user_id', 'user_id'),
|
|
Index('idx_api_keys_hash', 'key_hash'),
|
|
Index('idx_api_keys_active', 'is_active'),
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
return f"ApiKey({self.id}, name={self.name}, user_id={self.user_id})"
|
|
|
|
def is_expired(self) -> bool:
|
|
"""Check if API key is expired"""
|
|
if not self.expires_at:
|
|
return False
|
|
return datetime.utcnow() > self.expires_at
|
|
|
|
def is_valid(self) -> bool:
|
|
"""Check if API key is valid and active"""
|
|
return self.is_active and not self.is_expired() |