#!/usr/bin/env sh set -eu if [ -n "${ADMIN_PASSWORD_V2:-}" ]; then ADMIN_PASSWORD="$ADMIN_PASSWORD_V2" echo "Admin password secret source: ADMIN_PASSWORD_V2" else echo "Admin password secret source: ADMIN_PASSWORD" fi required_vars="ADMIN_PASSWORD JWT_SECRET TOTP_ENCRYPTION_KEY" for var_name in $required_vars; do eval "var_value=\${$var_name:-}" if [ -z "$var_value" ]; then echo "Missing required secret: $var_name" >&2 exit 1 fi done ADMIN_EMAIL="${ADMIN_EMAIL:-2691539771@qq.com}" SERVER_PORT="${SERVER_PORT:-7860}" DATA_DIR="${DATA_DIR:-/tmp/gateway-data}" mkdir -p "$DATA_DIR" /tmp/gateway-redis /tmp/gateway-postgres chown -R sub2api:sub2api "$DATA_DIR" /tmp/gateway-redis chown -R postgres:postgres /tmp/gateway-postgres redis-server --daemonize yes --bind 127.0.0.1 --port 6379 --dir /tmp/gateway-redis --save "" --appendonly no if [ -z "${DATABASE_HOST:-}" ]; then if [ ! -s /tmp/gateway-postgres/PG_VERSION ]; then su-exec postgres initdb -D /tmp/gateway-postgres --auth=trust >/tmp/gateway-postgres-init.log fi su-exec postgres pg_ctl -D /tmp/gateway-postgres -l /tmp/gateway-postgres.log -o "-c listen_addresses=127.0.0.1 -c port=5432 -c unix_socket_directories=/tmp" start createdb -h 127.0.0.1 -U postgres gatewaydb 2>/dev/null || true DATABASE_HOST="127.0.0.1" DATABASE_PORT="5432" DATABASE_USER="postgres" DATABASE_PASSWORD="" DATABASE_DBNAME="gatewaydb" DATABASE_SSLMODE="disable" echo "Using ephemeral local PostgreSQL. Configure Supabase secrets for persistent data." else DATABASE_PORT="${DATABASE_PORT:-5432}" DATABASE_USER="${DATABASE_USER:-postgres}" DATABASE_PASSWORD="${DATABASE_PASSWORD:-}" DATABASE_DBNAME="${DATABASE_DBNAME:-postgres}" DATABASE_SSLMODE="${DATABASE_SSLMODE:-require}" fi export AUTO_SETUP="${AUTO_SETUP:-true}" export ADMIN_EMAIL export ADMIN_PASSWORD export JWT_SECRET export TOTP_ENCRYPTION_KEY export DATA_DIR export DATABASE_HOST export DATABASE_PORT export DATABASE_USER export DATABASE_PASSWORD export DATABASE_DBNAME export DATABASE_SSLMODE export REDIS_HOST="${REDIS_HOST:-127.0.0.1}" export REDIS_PORT="${REDIS_PORT:-6379}" export SERVER_HOST="${SERVER_HOST:-0.0.0.0}" export SERVER_PORT export SERVER_MODE="${SERVER_MODE:-release}" export PORT="${PORT:-$SERVER_PORT}" export GIN_MODE="${GIN_MODE:-release}" migration_count() { PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ -h "$DATABASE_HOST" \ -p "$DATABASE_PORT" \ -U "$DATABASE_USER" \ -d "$DATABASE_DBNAME" \ -tAc "select count(1) from schema_migrations;" 2>/dev/null || printf '0\n' } sync_balance_cache_once() { if [ "${BALANCE_CACHE_SYNC_ENABLED:-true}" != "true" ]; then return 0 fi rows="$(PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ -h "$DATABASE_HOST" \ -p "$DATABASE_PORT" \ -U "$DATABASE_USER" \ -d "$DATABASE_DBNAME" \ -qAt \ -F '|' \ -c "select id, balance::double precision from users where deleted_at is null and status = 'active';" 2>/tmp/balance-cache-sync.err || true)" if [ -z "$rows" ]; then return 0 fi ttl="${BALANCE_CACHE_SYNC_TTL_SECONDS:-300}" printf '%s\n' "$rows" | while IFS='|' read -r user_id balance; do case "$user_id" in ''|*[!0-9]*) continue ;; esac if [ -n "${REDIS_PASSWORD:-}" ]; then REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli \ -h "${REDIS_HOST:-127.0.0.1}" \ -p "${REDIS_PORT:-6379}" \ -n "${REDIS_DB:-0}" \ SETEX "billing:balance:${user_id}" "$ttl" "$balance" >/dev/null 2>&1 || true else redis-cli \ -h "${REDIS_HOST:-127.0.0.1}" \ -p "${REDIS_PORT:-6379}" \ -n "${REDIS_DB:-0}" \ SETEX "billing:balance:${user_id}" "$ttl" "$balance" >/dev/null 2>&1 || true fi done } start_balance_cache_sync() { if [ "${BALANCE_CACHE_SYNC_ENABLED:-true}" != "true" ]; then return 0 fi ( interval="${BALANCE_CACHE_SYNC_INTERVAL_SECONDS:-60}" while :; do sync_balance_cache_once || true sleep "$interval" done ) & echo "Balance cache sync loop started." } write_runtime_config() { cat >"$DATA_DIR/config.yaml" <"$DATA_DIR/.installed" chown sub2api:sub2api "$DATA_DIR/config.yaml" "$DATA_DIR/.installed" chmod 600 "$DATA_DIR/config.yaml" chmod 400 "$DATA_DIR/.installed" } maybe_skip_auto_setup() { if [ "${FORCE_RUNTIME_CONFIG:-false}" = "true" ]; then write_runtime_config export AUTO_SETUP=false echo "Runtime config forced; skipping auto setup." return 0 fi if [ "${SKIP_AUTO_SETUP_WHEN_MIGRATED:-true}" != "true" ]; then return 0 fi if [ "$DATABASE_HOST" = "127.0.0.1" ]; then return 0 fi applied_count="$(migration_count | tr -d '[:space:]')" case "$applied_count" in ''|*[!0-9]*) applied_count=0 ;; esac required_count="${MIGRATION_BOOTSTRAP_MIN_COUNT:-178}" echo "Detected applied migrations: ${applied_count}/${required_count}" if [ "$applied_count" -ge "$required_count" ]; then write_runtime_config export AUTO_SETUP=false echo "External database is already bootstrapped; skipping auto setup." fi return 0 } sync_admin_password() { if [ "${SYNC_ADMIN_PASSWORD:-true}" != "true" ]; then return 0 fi echo "Admin password sync loop started." synced_dbs="" candidate_dbs="$DATABASE_DBNAME" if [ "$DATABASE_HOST" = "127.0.0.1" ]; then candidate_dbs="$DATABASE_DBNAME postgres gatewaydb" fi for attempt in $(seq 1 15); do password_hash="$(python3 - <<'PY' import bcrypt import os print(bcrypt.hashpw(os.environ["ADMIN_PASSWORD"].encode("utf-8"), bcrypt.gensalt()).decode("utf-8")) PY )" synced_count=0 for sync_dbname in $candidate_dbs; do case " $synced_dbs " in *" $sync_dbname "*) continue ;; esac table_exists="$(PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ -h "$DATABASE_HOST" \ -p "$DATABASE_PORT" \ -U "$DATABASE_USER" \ -d "$sync_dbname" \ -tAc "select to_regclass('public.users') is not null;" 2>/tmp/admin-user-check.err || true)" table_exists="$(printf '%s' "$table_exists" | tr -d '[:space:]')" admin_count="$(PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ -h "$DATABASE_HOST" \ -p "$DATABASE_PORT" \ -U "$DATABASE_USER" \ -d "$sync_dbname" \ -tAc "select count(1) from users where role = 'admin';" 2>/dev/null || true)" admin_count="$(printf '%s' "$admin_count" | tr -d '[:space:]')" if [ "$table_exists" = "t" ]; then PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ -h "$DATABASE_HOST" \ -p "$DATABASE_PORT" \ -U "$DATABASE_USER" \ -d "$sync_dbname" \ -v ON_ERROR_STOP=1 \ -c "update users set password_hash = '${password_hash}', role = 'admin', status = 'active', updated_at = now() where email = '${ADMIN_EMAIL}'; insert into users (email, password_hash, role, balance, concurrency, status, created_at, updated_at) select '${ADMIN_EMAIL}', '${password_hash}', 'admin', 0, 5, 'active', now(), now() where not exists (select 1 from users where email = '${ADMIN_EMAIL}');" >/tmp/admin-password-sync.out 2>/tmp/admin-password-sync.err stored_hash="$(PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ -h "$DATABASE_HOST" \ -p "$DATABASE_PORT" \ -U "$DATABASE_USER" \ -d "$sync_dbname" \ -tAc "select password_hash from users where role = 'admin' limit 1;" 2>/tmp/admin-password-verify.err || true)" export STORED_ADMIN_PASSWORD_HASH="$stored_hash" verify_result="$(python3 - <<'PY' import bcrypt import os stored = os.environ.get("STORED_ADMIN_PASSWORD_HASH", "").strip() password = os.environ["ADMIN_PASSWORD"].encode("utf-8") print("ok" if stored and bcrypt.checkpw(password, stored.encode("utf-8")) else "failed") PY )" echo "Admin password synchronized from HF secret in db=${sync_dbname}." echo "Admin password bcrypt verification: ${verify_result}" synced_dbs="$synced_dbs $sync_dbname" synced_count=$((synced_count + 1)) fi done if [ -n "$(printf '%s' "$synced_dbs" | tr -d '[:space:]')" ] && [ "$attempt" -ge 5 ]; then return 0 fi sleep 2 done echo "Admin password sync skipped: admin user not found before timeout." >&2 cat /tmp/admin-user-check.err >&2 || true } maybe_skip_auto_setup start_balance_cache_sync attempt=1 max_attempts="${BOOTSTRAP_MAX_ATTEMPTS:-6}" while :; do echo "Starting gateway on ${SERVER_HOST}:${SERVER_PORT} (attempt ${attempt}/${max_attempts})" su-exec sub2api /app/hub-gateway & app_pid="$!" trap 'kill -TERM "$app_pid" 2>/dev/null || true' INT TERM sync_admin_password & set +e wait "$app_pid" app_status="$?" set -e if [ "$app_status" -eq 0 ]; then exit 0 fi if [ "$AUTO_SETUP" != "true" ] || [ "$DATABASE_HOST" = "127.0.0.1" ]; then exit "$app_status" fi maybe_skip_auto_setup if [ "$AUTO_SETUP" != "true" ]; then attempt=$((attempt + 1)) continue fi if [ "$attempt" -ge "$max_attempts" ]; then exit "$app_status" fi attempt=$((attempt + 1)) echo "Gateway exited during bootstrap; retrying to continue migrations." sleep 2 done