diff --git a/app/plugins/registry.py b/app/plugins/registry.py index 90868f3..3202c8b 100644 --- a/app/plugins/registry.py +++ b/app/plugins/registry.py @@ -615,6 +615,15 @@ class PluginRegistry: routers = plugin.get_routes() mounted_routes: list[Any] = [] for router in routers: + # Check if routes from this router are already registered (P1.2 fix) + # This prevents duplicate route registration when plugins are + # statically registered in main.py AND dynamically activated + existing_paths = {getattr(r, 'path', None) for r in self._app.router.routes} + router_paths = {getattr(r, 'path', None) for r in router.routes} + if router_paths & existing_paths: + # Routes already registered — skip to avoid duplicates + logger.debug("Plugin '%s' routes already registered, skipping", name) + continue # Snapshot existing route object IDs before inclusion existing_ids = {id(r) for r in self._app.router.routes} self._app.include_router(router) diff --git a/docker-compose.yml b/docker-compose.yml index 27f74ae..5a3576f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -88,6 +88,13 @@ services: LOG_LEVEL: ${LOG_LEVEL:-INFO} SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-true} STORAGE_PATH: ${STORAGE_PATH:-/data/storage} + # SMTP for password reset and notifications + SMTP_HOST: ${SMTP_HOST:-} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USER: ${SMTP_USER:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_FROM: ${SMTP_FROM:-no-reply@localhost} + SMTP_TLS: ${SMTP_TLS:-true} BCRYPT_ROUNDS: ${BCRYPT_ROUNDS:-12} # S3 Storage (optional — if STORAGE_BACKEND=s3) STORAGE_BACKEND: ${STORAGE_BACKEND:-local} @@ -134,6 +141,13 @@ services: LOG_LEVEL: ${LOG_LEVEL:-INFO} SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-true} STORAGE_PATH: ${STORAGE_PATH:-/data/storage} + # SMTP for password reset and notifications + SMTP_HOST: ${SMTP_HOST:-} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USER: ${SMTP_USER:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_FROM: ${SMTP_FROM:-no-reply@localhost} + SMTP_TLS: ${SMTP_TLS:-true} STORAGE_BACKEND: ${STORAGE_BACKEND:-local} S3_ENDPOINT: ${S3_ENDPOINT:-} S3_BUCKET: ${S3_BUCKET:-} diff --git a/prestart.sh b/prestart.sh old mode 100755 new mode 100644 index 75cce07..a0e5807 --- a/prestart.sh +++ b/prestart.sh @@ -30,16 +30,32 @@ echo "[prestart] DB migrations completed successfully." # Set crm_runtime password if RUNTIME_DB_PASSWORD is set if [ -n "$RUNTIME_DB_PASSWORD" ]; then echo "[prestart] Setting crm_runtime password..." - # Parse the migration DB URL to get psql connection params - PGHOST=postgres - PGUSER="${POSTGRES_USER:-crm_user}" - PGDATABASE="${POSTGRES_DB:-crm_db}" - PGPASSWORD="$POSTGRES_PASSWORD" - export PGHOST PGUSER PGDATABASE PGPASSWORD - psql -c "ALTER ROLE crm_runtime WITH LOGIN PASSWORD '${RUNTIME_DB_PASSWORD}' NOSUPERUSER NOBYPASSRLS;" 2>/dev/null || \ - echo "[prestart] WARNING: Could not set crm_runtime password (role may not exist yet)" - unset PGPASSWORD - echo "[prestart] crm_runtime password set." + python3 -c " +import asyncio +import os +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy import text + +async def set_password(): + db_url = os.environ.get('MIGRATION_DATABASE_URL', os.environ.get('DATABASE_URL', '')) + if not db_url: + print('[prestart] WARNING: No DB URL for password setup') + return + engine = create_async_engine(db_url) + pwd = os.environ.get('RUNTIME_DB_PASSWORD', '') + try: + async with engine.begin() as conn: + await conn.execute(text( + f\"ALTER ROLE crm_runtime WITH LOGIN PASSWORD '{pwd}' NOSUPERUSER NOBYPASSRLS\" + )) + print('[prestart] crm_runtime password set.') + except Exception as e: + print(f'[prestart] WARNING: Could not set crm_runtime password: {e}') + finally: + await engine.dispose() + +asyncio.run(set_password()) +" fi echo "[prestart] Starting uvicorn on 0.0.0.0:8000 (workers=1)..."