- 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>
116 lines
4.8 KiB
Python
Executable file
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()
|