feat: Migrate to MySQL and add unified environment configuration

## Database Migration (SQLite → MySQL)
- Add Alembic migration framework
- Add 'tr_' prefix to all tables to avoid conflicts in shared database
- Remove SQLite support, use MySQL exclusively
- Add pymysql driver dependency
- Change ad_token column to Text type for long JWT tokens

## Unified Environment Configuration
- Centralize all hardcoded settings to environment variables
- Backend: Extend Settings class in app/core/config.py
- Frontend: Use Vite environment variables (import.meta.env)
- Docker: Move credentials to environment variables
- Update .env.example files with comprehensive documentation

## Test Organization
- Move root-level test files to tests/ directory:
  - test_chat_room.py → tests/test_chat_room.py
  - test_websocket.py → tests/test_websocket.py
  - test_realtime_implementation.py → tests/test_realtime_implementation.py
- Fix path references in test_realtime_implementation.py

Breaking Changes:
- CORS now requires explicit origins (no more wildcard)
- All database tables renamed with 'tr_' prefix
- SQLite no longer supported

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-07 14:15:11 +08:00
parent 1d5d4d447d
commit 92834dbe0e
39 changed files with 1558 additions and 136 deletions

View File

@@ -1,6 +1,9 @@
"""Application configuration loaded from environment variables"""
from pydantic_settings import BaseSettings
from pydantic import field_validator
from functools import lru_cache
from typing import List
import logging
class Settings(BaseSettings):
@@ -14,6 +17,7 @@ class Settings(BaseSettings):
# AD API
AD_API_URL: str
AD_API_TIMEOUT_SECONDS: int = 10 # AD API request timeout
# Session Settings
SESSION_INACTIVITY_DAYS: int = 3
@@ -23,7 +27,23 @@ class Settings(BaseSettings):
# Server
HOST: str = "0.0.0.0"
PORT: int = 8000
DEBUG: bool = True
DEBUG: bool = False # Default to False for security
LOG_LEVEL: str = "INFO" # DEBUG, INFO, WARNING, ERROR
# CORS Configuration
CORS_ORIGINS: str = "http://localhost:3000" # Comma-separated list of allowed origins
# System Admin
SYSTEM_ADMIN_EMAIL: str = "" # System administrator email with special permissions
# Realtime Messaging Settings
MESSAGE_EDIT_TIME_LIMIT_MINUTES: int = 15 # Time limit for editing messages
TYPING_TIMEOUT_SECONDS: int = 3 # Typing indicator timeout
# File Upload Size Limits (in MB)
IMAGE_MAX_SIZE_MB: int = 10
DOCUMENT_MAX_SIZE_MB: int = 20
LOG_MAX_SIZE_MB: int = 5
# MinIO Object Storage
MINIO_ENDPOINT: str = "localhost:9000"
@@ -41,6 +61,41 @@ class Settings(BaseSettings):
REPORT_MAX_MESSAGES: int = 200 # Summarize if exceeded
REPORT_STORAGE_PATH: str = "reports" # MinIO path prefix for reports
@field_validator("LOG_LEVEL")
@classmethod
def validate_log_level(cls, v: str) -> str:
"""Validate log level"""
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
v_upper = v.upper()
if v_upper not in valid_levels:
raise ValueError(f"LOG_LEVEL must be one of {valid_levels}")
return v_upper
def get_cors_origins(self) -> List[str]:
"""Parse CORS_ORIGINS into a list"""
if not self.CORS_ORIGINS:
return []
return [origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()]
def get_image_max_size_bytes(self) -> int:
"""Get image max size in bytes"""
return self.IMAGE_MAX_SIZE_MB * 1024 * 1024
def get_document_max_size_bytes(self) -> int:
"""Get document max size in bytes"""
return self.DOCUMENT_MAX_SIZE_MB * 1024 * 1024
def get_log_max_size_bytes(self) -> int:
"""Get log file max size in bytes"""
return self.LOG_MAX_SIZE_MB * 1024 * 1024
def configure_logging(self) -> None:
"""Configure application logging based on LOG_LEVEL"""
logging.basicConfig(
level=getattr(logging, self.LOG_LEVEL),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
class Config:
env_file = ".env"
case_sensitive = True

View File

@@ -1,4 +1,8 @@
"""Database connection and session management"""
"""Database connection and session management
Supports MySQL database with connection pooling.
All tables use 'tr_' prefix to avoid conflicts in shared database.
"""
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
@@ -6,10 +10,13 @@ from app.core.config import get_settings
settings = get_settings()
# Create engine
# Create engine with MySQL connection pooling
engine = create_engine(
settings.DATABASE_URL,
connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {},
pool_size=5,
max_overflow=10,
pool_pre_ping=True, # Verify connection before using
pool_recycle=3600, # Recycle connections after 1 hour
echo=settings.DEBUG,
)