This commit introduces major enhancements to Talk2Me: ## Database Integration - PostgreSQL support with SQLAlchemy ORM - Redis integration for caching and real-time analytics - Automated database initialization scripts - Migration support infrastructure ## User Authentication System - JWT-based API authentication - Session-based web authentication - API key authentication for programmatic access - User roles and permissions (admin/user) - Login history and session tracking - Rate limiting per user with customizable limits ## Admin Dashboard - Real-time analytics and monitoring - User management interface (create, edit, delete users) - System health monitoring - Request/error tracking - Language pair usage statistics - Performance metrics visualization ## Key Features - Dual authentication support (token + user accounts) - Graceful fallback for missing services - Non-blocking analytics middleware - Comprehensive error handling - Session management with security features ## Bug Fixes - Fixed rate limiting bypass for admin routes - Added missing email validation method - Improved error handling for missing database tables - Fixed session-based authentication for API endpoints 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
213 lines
8.5 KiB
Python
213 lines
8.5 KiB
Python
# Configuration management with secrets integration
|
|
import os
|
|
import logging
|
|
from datetime import timedelta
|
|
from secrets_manager import get_secret, get_secrets_manager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class Config:
|
|
"""Base configuration with secrets management"""
|
|
|
|
def __init__(self):
|
|
self.secrets_manager = get_secrets_manager()
|
|
self._load_config()
|
|
|
|
def _load_config(self):
|
|
"""Load configuration from environment and secrets"""
|
|
# Flask configuration
|
|
self.SECRET_KEY = self._get_secret('FLASK_SECRET_KEY',
|
|
os.environ.get('SECRET_KEY', 'dev-key-change-this'))
|
|
|
|
# Security
|
|
self.SESSION_COOKIE_SECURE = self._get_bool('SESSION_COOKIE_SECURE', True)
|
|
self.SESSION_COOKIE_HTTPONLY = True
|
|
self.SESSION_COOKIE_SAMESITE = 'Lax'
|
|
self.PERMANENT_SESSION_LIFETIME = timedelta(hours=24)
|
|
|
|
# TTS Configuration
|
|
self.TTS_SERVER_URL = os.environ.get('TTS_SERVER_URL', 'http://localhost:5050/v1/audio/speech')
|
|
self.TTS_API_KEY = self._get_secret('TTS_API_KEY', os.environ.get('TTS_API_KEY', ''))
|
|
|
|
# Upload configuration
|
|
self.UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', None)
|
|
|
|
# Request size limits (in bytes)
|
|
self.MAX_CONTENT_LENGTH = int(os.environ.get('MAX_CONTENT_LENGTH', 50 * 1024 * 1024)) # 50MB
|
|
self.MAX_AUDIO_SIZE = int(os.environ.get('MAX_AUDIO_SIZE', 25 * 1024 * 1024)) # 25MB
|
|
self.MAX_JSON_SIZE = int(os.environ.get('MAX_JSON_SIZE', 1 * 1024 * 1024)) # 1MB
|
|
self.MAX_IMAGE_SIZE = int(os.environ.get('MAX_IMAGE_SIZE', 10 * 1024 * 1024)) # 10MB
|
|
|
|
# CORS configuration
|
|
self.CORS_ORIGINS = os.environ.get('CORS_ORIGINS', '*').split(',')
|
|
self.ADMIN_CORS_ORIGINS = os.environ.get('ADMIN_CORS_ORIGINS', 'http://localhost:*').split(',')
|
|
|
|
# Admin configuration
|
|
self.ADMIN_TOKEN = self._get_secret('ADMIN_TOKEN',
|
|
os.environ.get('ADMIN_TOKEN', 'default-admin-token'))
|
|
|
|
# Database configuration
|
|
self.DATABASE_URL = self._get_secret('DATABASE_URL',
|
|
os.environ.get('DATABASE_URL', 'postgresql://localhost/talk2me'))
|
|
self.SQLALCHEMY_DATABASE_URI = self.DATABASE_URL
|
|
self.SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
self.SQLALCHEMY_ENGINE_OPTIONS = {
|
|
'pool_size': 10,
|
|
'pool_recycle': 3600,
|
|
'pool_pre_ping': True
|
|
}
|
|
|
|
# Redis configuration
|
|
self.REDIS_URL = self._get_secret('REDIS_URL',
|
|
os.environ.get('REDIS_URL', 'redis://localhost:6379/0'))
|
|
self.REDIS_DECODE_RESPONSES = False
|
|
self.REDIS_MAX_CONNECTIONS = int(os.environ.get('REDIS_MAX_CONNECTIONS', 50))
|
|
self.REDIS_SOCKET_TIMEOUT = int(os.environ.get('REDIS_SOCKET_TIMEOUT', 5))
|
|
|
|
# Whisper configuration
|
|
self.WHISPER_MODEL_SIZE = os.environ.get('WHISPER_MODEL_SIZE', 'base')
|
|
self.WHISPER_DEVICE = os.environ.get('WHISPER_DEVICE', 'auto')
|
|
|
|
# Ollama configuration
|
|
self.OLLAMA_HOST = os.environ.get('OLLAMA_HOST', 'http://localhost:11434')
|
|
self.OLLAMA_MODEL = os.environ.get('OLLAMA_MODEL', 'gemma3:27b')
|
|
|
|
# Rate limiting configuration
|
|
self.RATE_LIMIT_ENABLED = self._get_bool('RATE_LIMIT_ENABLED', True)
|
|
self.RATE_LIMIT_STORAGE_URL = self._get_secret('RATE_LIMIT_STORAGE_URL',
|
|
os.environ.get('RATE_LIMIT_STORAGE_URL', 'memory://'))
|
|
|
|
# Logging configuration
|
|
self.LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO')
|
|
self.LOG_FILE = os.environ.get('LOG_FILE', 'talk2me.log')
|
|
|
|
# Feature flags
|
|
self.ENABLE_PUSH_NOTIFICATIONS = self._get_bool('ENABLE_PUSH_NOTIFICATIONS', True)
|
|
self.ENABLE_OFFLINE_MODE = self._get_bool('ENABLE_OFFLINE_MODE', True)
|
|
self.ENABLE_STREAMING = self._get_bool('ENABLE_STREAMING', True)
|
|
self.ENABLE_MULTI_SPEAKER = self._get_bool('ENABLE_MULTI_SPEAKER', True)
|
|
|
|
# Performance tuning
|
|
self.WORKER_CONNECTIONS = int(os.environ.get('WORKER_CONNECTIONS', '1000'))
|
|
self.WORKER_TIMEOUT = int(os.environ.get('WORKER_TIMEOUT', '120'))
|
|
|
|
# Validate configuration
|
|
self._validate_config()
|
|
|
|
def _get_secret(self, key: str, default: str = None) -> str:
|
|
"""Get secret from secrets manager or environment"""
|
|
value = self.secrets_manager.get(key)
|
|
if value is None:
|
|
value = default
|
|
|
|
if value is None:
|
|
logger.warning(f"Configuration {key} not set")
|
|
|
|
return value
|
|
|
|
def _get_bool(self, key: str, default: bool = False) -> bool:
|
|
"""Get boolean configuration value"""
|
|
value = os.environ.get(key, '').lower()
|
|
if value in ('true', '1', 'yes', 'on'):
|
|
return True
|
|
elif value in ('false', '0', 'no', 'off'):
|
|
return False
|
|
return default
|
|
|
|
def _validate_config(self):
|
|
"""Validate configuration values"""
|
|
# Check required secrets
|
|
if not self.SECRET_KEY or self.SECRET_KEY == 'dev-key-change-this':
|
|
logger.warning("Using default SECRET_KEY - this is insecure for production!")
|
|
|
|
if not self.TTS_API_KEY:
|
|
logger.warning("TTS_API_KEY not configured - TTS functionality may not work")
|
|
|
|
if self.ADMIN_TOKEN == 'default-admin-token':
|
|
logger.warning("Using default ADMIN_TOKEN - this is insecure for production!")
|
|
|
|
# Validate URLs
|
|
if not self._is_valid_url(self.TTS_SERVER_URL):
|
|
logger.error(f"Invalid TTS_SERVER_URL: {self.TTS_SERVER_URL}")
|
|
|
|
# Check file permissions
|
|
if self.UPLOAD_FOLDER and not os.access(self.UPLOAD_FOLDER, os.W_OK):
|
|
logger.warning(f"Upload folder {self.UPLOAD_FOLDER} is not writable")
|
|
|
|
def _is_valid_url(self, url: str) -> bool:
|
|
"""Check if URL is valid"""
|
|
return url.startswith(('http://', 'https://'))
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Export configuration as dictionary (excluding secrets)"""
|
|
config = {}
|
|
for key in dir(self):
|
|
if key.isupper() and not key.startswith('_'):
|
|
value = getattr(self, key)
|
|
# Mask sensitive values
|
|
if any(sensitive in key for sensitive in ['KEY', 'TOKEN', 'PASSWORD', 'SECRET']):
|
|
config[key] = '***MASKED***'
|
|
else:
|
|
config[key] = value
|
|
return config
|
|
|
|
class DevelopmentConfig(Config):
|
|
"""Development configuration"""
|
|
def _load_config(self):
|
|
super()._load_config()
|
|
self.DEBUG = True
|
|
self.TESTING = False
|
|
self.SESSION_COOKIE_SECURE = False # Allow HTTP in development
|
|
|
|
class ProductionConfig(Config):
|
|
"""Production configuration"""
|
|
def _load_config(self):
|
|
super()._load_config()
|
|
self.DEBUG = False
|
|
self.TESTING = False
|
|
|
|
# Enforce security in production
|
|
if not self.SECRET_KEY or self.SECRET_KEY == 'dev-key-change-this':
|
|
raise ValueError("SECRET_KEY must be set in production")
|
|
|
|
if self.ADMIN_TOKEN == 'default-admin-token':
|
|
raise ValueError("ADMIN_TOKEN must be changed in production")
|
|
|
|
class TestingConfig(Config):
|
|
"""Testing configuration"""
|
|
def _load_config(self):
|
|
super()._load_config()
|
|
self.DEBUG = True
|
|
self.TESTING = True
|
|
self.WTF_CSRF_ENABLED = False
|
|
self.RATE_LIMIT_ENABLED = False
|
|
|
|
# Configuration factory
|
|
def get_config(env: str = None) -> Config:
|
|
"""Get configuration based on environment"""
|
|
if env is None:
|
|
env = os.environ.get('FLASK_ENV', 'development')
|
|
|
|
configs = {
|
|
'development': DevelopmentConfig,
|
|
'production': ProductionConfig,
|
|
'testing': TestingConfig
|
|
}
|
|
|
|
config_class = configs.get(env, DevelopmentConfig)
|
|
return config_class()
|
|
|
|
# Convenience function for Flask app
|
|
def init_app(app):
|
|
"""Initialize Flask app with configuration"""
|
|
config = get_config()
|
|
|
|
# Apply configuration to app
|
|
for key in dir(config):
|
|
if key.isupper():
|
|
app.config[key] = getattr(config, key)
|
|
|
|
# Store config object
|
|
app.app_config = config
|
|
|
|
logger.info(f"Configuration loaded for environment: {os.environ.get('FLASK_ENV', 'development')}") |