diff --git a/backend/src/app.py b/backend/src/app.py index 8a423855..0dd2b2ed 100755 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -12,6 +12,7 @@ # @SIDE_EFFECT: Starts background scheduler and binds network ports for HTTP/WS traffic. # @DATA_CONTRACT: [HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream] +import os from pathlib import Path # project_root is used for static files mounting @@ -28,6 +29,9 @@ from .dependencies import get_task_manager, get_scheduler_service from .core.encryption_key import ensure_encryption_key from .core.utils.network import NetworkError from .core.logger import logger, belief_scope +from .core.database import AuthSessionLocal +from .core.auth.security import get_password_hash +from .models.auth import User, Role from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports, assistant, clean_release, clean_release_v2, profile, health, dataset_review from .api import auth @@ -42,6 +46,54 @@ app = FastAPI( ) # [/DEF:App:Global] +# [DEF:ensure_initial_admin_user:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Ensures initial admin user exists when bootstrap env flags are enabled. +def ensure_initial_admin_user() -> None: + raw_flag = os.getenv("INITIAL_ADMIN_CREATE", "false").strip().lower() + if raw_flag not in {"1", "true", "yes", "on"}: + return + username = os.getenv("INITIAL_ADMIN_USERNAME", "").strip() + password = os.getenv("INITIAL_ADMIN_PASSWORD", "").strip() + if not username or not password: + logger.warning( + "INITIAL_ADMIN_CREATE is enabled but INITIAL_ADMIN_USERNAME/INITIAL_ADMIN_PASSWORD is missing; skipping bootstrap." + ) + return + + db = AuthSessionLocal() + try: + admin_role = db.query(Role).filter(Role.name == "Admin").first() + if not admin_role: + admin_role = Role(name="Admin", description="System Administrator") + db.add(admin_role) + db.commit() + db.refresh(admin_role) + + existing_user = db.query(User).filter(User.username == username).first() + if existing_user: + logger.info("Initial admin bootstrap skipped: user '%s' already exists.", username) + return + + new_user = User( + username=username, + email=None, + password_hash=get_password_hash(password), + auth_source="LOCAL", + is_active=True, + ) + new_user.roles.append(admin_role) + db.add(new_user) + db.commit() + logger.info("Initial admin user '%s' created from environment bootstrap.", username) + except Exception as exc: + db.rollback() + logger.error("Failed to bootstrap initial admin user: %s", exc) + raise + finally: + db.close() +# [/DEF:ensure_initial_admin_user:Function] + # [DEF:startup_event:Function] # @COMPLEXITY: 3 # @PURPOSE: Handles application startup tasks, such as starting the scheduler. @@ -53,6 +105,7 @@ app = FastAPI( async def startup_event(): with belief_scope("startup_event"): ensure_encryption_key() + ensure_initial_admin_user() scheduler = get_scheduler_service() scheduler.start() # [/DEF:startup_event:Function] diff --git a/build.sh b/build.sh index 3c3f37b6..61b0ce5e 100755 --- a/build.sh +++ b/build.sh @@ -7,6 +7,23 @@ cd "$SCRIPT_DIR" BACKEND_ENV_FILE="$SCRIPT_DIR/backend/.env" +PROFILE="${1:-current}" + +case "$PROFILE" in + master) + PROFILE_ENV_FILE="$SCRIPT_DIR/.env.master" + PROJECT_NAME="ss-tools-master" + ;; + current) + PROFILE_ENV_FILE="$SCRIPT_DIR/.env.current" + PROJECT_NAME="ss-tools-current" + ;; + *) + echo "Error: unknown profile '$PROFILE'. Use one of: master, current." + exit 1 + ;; +esac + if ! command -v docker >/dev/null 2>&1; then echo "Error: docker is not installed or not in PATH." exit 1 @@ -80,11 +97,23 @@ PY ensure_backend_encryption_key +COMPOSE_ARGS=(-p "$PROJECT_NAME") +if [[ -f "$PROFILE_ENV_FILE" ]]; then + COMPOSE_ARGS+=(--env-file "$PROFILE_ENV_FILE") +else + echo "[build] Warning: profile env file not found at $PROFILE_ENV_FILE, using compose defaults." +fi + +echo "[build] Profile: $PROFILE (project: $PROJECT_NAME)" +if [[ -f "$PROFILE_ENV_FILE" ]]; then + echo "[build] Env file: $PROFILE_ENV_FILE" +fi + echo "[1/2] Building project images..." -"${COMPOSE_CMD[@]}" build +"${COMPOSE_CMD[@]}" "${COMPOSE_ARGS[@]}" build echo "[2/2] Starting Docker services..." -"${COMPOSE_CMD[@]}" up -d +"${COMPOSE_CMD[@]}" "${COMPOSE_ARGS[@]}" up -d echo "Done. Services are running." -echo "Use '${COMPOSE_CMD[*]} ps' to check status and '${COMPOSE_CMD[*]} logs -f' to stream logs." +echo "Use '${COMPOSE_CMD[*]} ${COMPOSE_ARGS[*]} ps' to check status and '${COMPOSE_CMD[*]} ${COMPOSE_ARGS[*]} logs -f' to stream logs." diff --git a/docker-compose.enterprise-clean.yml b/docker-compose.enterprise-clean.yml index 73be2665..580edd27 100644 --- a/docker-compose.enterprise-clean.yml +++ b/docker-compose.enterprise-clean.yml @@ -2,7 +2,6 @@ services: db: image: ${POSTGRES_IMAGE:?Set POSTGRES_IMAGE in .env.enterprise-clean} pull_policy: never - container_name: ss_tools_db restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB:-ss_tools} @@ -21,7 +20,6 @@ services: backend: image: ${BACKEND_IMAGE:?Set BACKEND_IMAGE in .env.enterprise-clean} pull_policy: never - container_name: ss_tools_backend restart: unless-stopped depends_on: db: @@ -50,7 +48,6 @@ services: frontend: image: ${FRONTEND_IMAGE:?Set FRONTEND_IMAGE in .env.enterprise-clean} pull_policy: never - container_name: ss_tools_frontend restart: unless-stopped depends_on: - backend diff --git a/docker-compose.yml b/docker-compose.yml index 86a80733..55a45c25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,6 @@ services: db: image: ${POSTGRES_IMAGE:-postgres:16-alpine} - container_name: ss_tools_db restart: unless-stopped environment: POSTGRES_DB: ss_tools @@ -21,7 +20,6 @@ services: build: context: . dockerfile: docker/backend.Dockerfile - container_name: ss_tools_backend restart: unless-stopped env_file: - ./backend/.env @@ -34,6 +32,9 @@ services: TASKS_DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools AUTH_DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools BACKEND_PORT: 8000 + INITIAL_ADMIN_CREATE: ${INITIAL_ADMIN_CREATE:-false} + INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:-admin} + INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-} ports: - "${BACKEND_HOST_PORT:-8001}:8000" volumes: @@ -46,7 +47,6 @@ services: build: context: . dockerfile: docker/frontend.Dockerfile - container_name: ss_tools_frontend restart: unless-stopped depends_on: - backend