ac-tool/backend/server/config_runtime.py
Vadym Samoilenko 8da149b84e Migrate storage from JSON files to PostgreSQL (asyncpg)
- Add asyncpg connection pool (db/pool.py) with JSONB codec registration
- Add schema.sql with users, clients, dropdown_categories, export_templates, sheets tables
- Add migrate_json.py one-time migration script for existing JSON data
- Rewrite user_store, sheets/manager, api/clients, api/dropdowns, api/export as async DB-backed
- Update all callers (auth, sheets, admin, ai_command, export) to await async functions
- Add postgres:16-alpine service to docker-compose with named volume and health check
- App container depends_on postgres; DATABASE_URL injected via env
- Schema applied automatically on startup; global categories seeded if DB is empty

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:51:37 +00:00

116 lines
4.8 KiB
Python
Executable file

"""
Runtime configuration for AC Tool server
"""
import os
from typing import List
from dotenv import load_dotenv
load_dotenv()
class ServerConfig:
# Server
HOST: str = os.getenv('SERVER_HOST', '0.0.0.0')
PORT: int = int(os.getenv('SERVER_PORT', '8000'))
WORKERS: int = int(os.getenv('SERVER_WORKERS', '2'))
DEBUG: bool = os.getenv('DEBUG', 'false').lower() == 'true'
# Development Mode
DEV_MODE: bool = os.getenv('DEV_MODE', 'true').lower() == 'true'
DEV_USER_ID: str = os.getenv('DEV_USER_ID', 'dev-user-id')
DEV_USER_EMAIL: str = os.getenv('DEV_USER_EMAIL', 'dev@localhost')
DEV_USER_NAME: str = os.getenv('DEV_USER_NAME', 'Dev User')
DEV_USER_ROLE: str = os.getenv('DEV_USER_ROLE', 'admin') # 'user' or 'admin'
# CORS
ALLOWED_ORIGINS: List[str] = [
origin.strip()
for origin in os.getenv(
'ALLOWED_ORIGINS',
'http://localhost:3000,http://localhost:5173,https://ai-sandbox.oliver.solutions'
).split(',')
]
# Azure AD / MSAL (SPA PKCE flow — no client secret needed)
MSAL_CLIENT_ID: str = os.getenv('MSAL_CLIENT_ID', '9079054c-9620-4757-a256-23413042f1ef')
MSAL_TENANT_ID: str = os.getenv('MSAL_TENANT_ID', 'e519c2e6-bc6d-4fdf-8d9c-923c2f002385')
MSAL_REDIRECT_URI: str = os.getenv('MSAL_REDIRECT_URI', 'https://ai-sandbox.oliver.solutions/ac-helper/')
MSAL_AUTHORITY: str = f'https://login.microsoftonline.com/{os.getenv("MSAL_TENANT_ID", "e519c2e6-bc6d-4fdf-8d9c-923c2f002385")}'
# Admin bootstrap — emails that always get admin role on first login
ADMIN_EMAIL: str = os.getenv('ADMIN_EMAIL', 'daveporter@oliver.agency')
ADMIN_EMAILS: list = [
e.strip().lower()
for e in os.getenv('ADMIN_EMAILS', 'daveporter@oliver.agency,vadymsamoilenko@oliver.agency').split(',')
if e.strip()
]
# Emergency access — set EMERGENCY_TOKEN to a long random string to allow
# token-based login bypassing SSO (useful when 2FA / Azure AD is unavailable).
# Leave blank to disable this bypass entirely.
EMERGENCY_TOKEN: str = os.getenv('EMERGENCY_TOKEN', '')
EMERGENCY_USER_EMAIL: str = os.getenv('EMERGENCY_USER_EMAIL', 'daveporter@oliver.agency')
EMERGENCY_USER_NAME: str = os.getenv('EMERGENCY_USER_NAME', 'Emergency Access')
# Security
SESSION_SECRET: str = os.getenv('SESSION_SECRET', 'change-me-in-production')
SECURE_COOKIES: bool = os.getenv('SECURE_COOKIES', 'false').lower() == 'true'
HTTPS_ONLY: bool = os.getenv('HTTPS_ONLY', 'false').lower() == 'true'
# File Upload
MAX_UPLOAD_SIZE_MB: int = int(os.getenv('MAX_UPLOAD_SIZE_MB', '200'))
MAX_CONTENT_LENGTH: int = MAX_UPLOAD_SIZE_MB * 1024 * 1024
ALLOWED_EXTENSIONS: set = {'.pdf', '.pptx', '.docx', '.xlsx', '.ppt', '.doc', '.xls'}
# Job Management
MAX_CONCURRENT_JOBS: int = int(os.getenv('MAX_CONCURRENT_JOBS', '2'))
FILE_RETENTION_HOURS: int = int(os.getenv('FILE_RETENTION_HOURS', '24'))
# WebSocket
WS_PING_INTERVAL_SECONDS: int = int(os.getenv('WS_PING_INTERVAL_SECONDS', '30'))
# AI
GEMINI_API_KEY: str = os.getenv('GEMINI_API_KEY', '')
GEMINI_MODEL: str = os.getenv('GEMINI_MODEL', 'gemini-3-flash-preview')
# PostgreSQL
DATABASE_URL: str = os.getenv('DATABASE_URL', 'postgresql://achelper:achelper@localhost:5432/achelper')
# Data paths — mounted as Docker volume
DATA_DIR: str = os.getenv(
'DATA_DIR',
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data')
)
UPLOAD_DIR: str = os.path.join(DATA_DIR, 'uploads')
OUTPUT_DIR: str = os.path.join(DATA_DIR, 'outputs')
SHEETS_DIR: str = os.path.join(DATA_DIR, 'sheets')
USERS_FILE: str = os.path.join(DATA_DIR, 'users.json')
DROPDOWNS_FILE: str = os.path.join(DATA_DIR, 'dropdowns.json')
CLIENTS_FILE: str = os.path.join(DATA_DIR, 'clients.json')
CLIENTS_DROPDOWNS_DIR: str = os.path.join(DATA_DIR, 'client_dropdowns')
EXPORT_TEMPLATE_FILE: str = os.path.join(DATA_DIR, 'export_template.json')
USER_EXPORT_TEMPLATES_DIR: str = os.path.join(DATA_DIR, 'user_export_templates')
@classmethod
def ensure_directories(cls):
for d in [cls.DATA_DIR, cls.UPLOAD_DIR, cls.OUTPUT_DIR, cls.SHEETS_DIR, cls.CLIENTS_DROPDOWNS_DIR, cls.USER_EXPORT_TEMPLATES_DIR]:
os.makedirs(d, exist_ok=True)
@classmethod
def validate_auth_config(cls) -> bool:
if cls.DEV_MODE:
return True
return bool(cls.MSAL_CLIENT_ID and cls.MSAL_TENANT_ID)
@classmethod
def get_cors_config(cls) -> dict:
return {
'allow_origin': cls.ALLOWED_ORIGINS,
'allow_methods': ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
'allow_headers': ['Content-Type', 'Authorization', 'Accept'],
'allow_credentials': True,
}
server_config = ServerConfig()