diff --git a/scripts/configure.sh b/scripts/configure.sh index 9586936..4d74e99 100755 --- a/scripts/configure.sh +++ b/scripts/configure.sh @@ -110,7 +110,7 @@ TENANT_BOOTSTRAP_APP_KEY="$(ask "App key" "${TENANT_BOOTSTRAP_APP_KEY:-ak_privat REGISTRY="$(ask "Docker registry" "${REGISTRY:-registry.example.com/xuqm}" "registry.example.com/xuqm")" IMAGE_TAG="$(ask "Image tag" "${IMAGE_TAG:-$(cat "$ROOT_DIR/VERSION" | tr -d '[:space:]')}" "")" -# Write .env (idempotent — only update values that changed) +# Write .env (compose variable substitution — used by ${VAR} in docker-compose.yml) set_env_value "$ROOT_DIR/.env" "CONSOLE_DOMAIN" "$CONSOLE_DOMAIN" set_env_value "$ROOT_DIR/.env" "OPS_DOMAIN" "$OPS_DOMAIN" set_env_value "$ROOT_DIR/.env" "DOCS_DOMAIN" "$DOCS_DOMAIN" @@ -138,6 +138,26 @@ set_env_value "$ROOT_DIR/.env" "IMAGE_TAG" "$IMAGE_TAG" set_env_value "$ROOT_DIR/.env" "TENANT_BOOTSTRAP_EMAIL" "$TENANT_BOOTSTRAP_EMAIL" set_env_value "$ROOT_DIR/.env" "TENANT_BOOTSTRAP_APP_KEY" "$TENANT_BOOTSTRAP_APP_KEY" +# Sync infra settings into xuqm.env so containers receive correct host/port values +# via env_file:, consistent with the SPRING_DATASOURCE_* overrides in docker-compose.yml. +XUQM_ENV="$ROOT_DIR/config/xuqm.env" +set_env_value "$XUQM_ENV" "MYSQL_HOST" "$MYSQL_HOST" +set_env_value "$XUQM_ENV" "MYSQL_PORT" "$MYSQL_PORT" +set_env_value "$XUQM_ENV" "MYSQL_DATABASE" "$MYSQL_DATABASE" +set_env_value "$XUQM_ENV" "MYSQL_USERNAME" "$MYSQL_USERNAME" +set_env_value "$XUQM_ENV" "REDIS_HOST" "$REDIS_HOST" +set_env_value "$XUQM_ENV" "REDIS_PORT" "$REDIS_PORT" +set_env_value "$XUQM_ENV" "CONSOLE_DOMAIN" "$CONSOLE_DOMAIN" +[ -n "${FILE_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "FILE_DOMAIN" "$FILE_DOMAIN" +[ -n "${IM_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "IM_DOMAIN" "$IM_DOMAIN" +[ -n "${PUSH_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "PUSH_DOMAIN" "$PUSH_DOMAIN" +[ -n "${UPDATE_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "UPDATE_DOMAIN" "$UPDATE_DOMAIN" +[ -n "${LICENSE_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "LICENSE_DOMAIN" "$LICENSE_DOMAIN" +set_env_value "$XUQM_ENV" "ENABLE_IM" "$ENABLE_IM" +set_env_value "$XUQM_ENV" "ENABLE_PUSH" "$ENABLE_PUSH" +set_env_value "$XUQM_ENV" "ENABLE_UPDATE" "$ENABLE_UPDATE" +set_env_value "$XUQM_ENV" "ENABLE_LICENSE" "$ENABLE_LICENSE" + # Write secrets.env set_env_value "$ROOT_DIR/config/secrets.env" "MYSQL_PASSWORD" "$MYSQL_PASSWORD" [ -n "${MYSQL_ROOT_PASSWORD:-}" ] && \ diff --git a/scripts/healthcheck.sh b/scripts/healthcheck.sh index c2bb3fd..9536053 100755 --- a/scripts/healthcheck.sh +++ b/scripts/healthcheck.sh @@ -24,8 +24,8 @@ else check_fail "docker" "daemon not available" fi -# Container states -for svc in tenant-service file-service tenant-web ops-web docs-site nginx; do +# Container states — docs-site is optional (image may not exist in all registries) +for svc in tenant-service file-service tenant-web ops-web nginx; do STATE="$(compose ps -q "$svc" 2>/dev/null | head -1 || true)" if [ -n "$STATE" ]; then STATUS="$(docker inspect --format='{{.State.Status}}' "$STATE" 2>/dev/null || echo 'unknown')" @@ -39,8 +39,8 @@ for svc in tenant-service file-service tenant-web ops-web docs-site nginx; do fi done -# Optional service containers -for svc in im-service push-service update-service license-service; do +# Optional service containers (docs-site included here — warn only if present but unhealthy) +for svc in docs-site im-service push-service update-service license-service; do STATE="$(compose ps -q "$svc" 2>/dev/null | head -1 || true)" if [ -n "$STATE" ]; then STATUS="$(docker inspect --format='{{.State.Status}}' "$STATE" 2>/dev/null || echo 'unknown')" @@ -120,9 +120,29 @@ http_check() { fi } +# Resolve the base URL to probe: prefer CONSOLE_DOMAIN, fall back to localhost CONSOLE_DOMAIN_VAL="${CONSOLE_DOMAIN:-}" -[ -n "$CONSOLE_DOMAIN_VAL" ] && http_check "http.tenant-service" "${CONSOLE_DOMAIN_VAL}/actuator/health" -[ -n "$CONSOLE_DOMAIN_VAL" ] && http_check "http.tenant-web" "${CONSOLE_DOMAIN_VAL}/" +if [ -n "$CONSOLE_DOMAIN_VAL" ]; then + HTTP_BASE="$CONSOLE_DOMAIN_VAL" +else + # Derive port from nginx container if running + NGINX_PORT="$(compose port nginx 80 2>/dev/null | cut -d: -f2 || true)" + HTTP_BASE="http://localhost:${NGINX_PORT:-80}" +fi +http_check "http.actuator" "${HTTP_BASE}/actuator/health" +http_check "http.tenant-web" "${HTTP_BASE}/" + +# Verify private mode is active +if command -v curl >/dev/null 2>&1; then + DEPLOY_STATUS="$(curl -skL --max-time 5 "${HTTP_BASE}/api/private/deployment/status" 2>/dev/null || true)" + if printf '%s' "$DEPLOY_STATUS" | grep -q '"mode":"PRIVATE"'; then + check_pass "private-mode" "PRIVATE mode confirmed" + elif printf '%s' "$DEPLOY_STATUS" | grep -q '"mode"'; then + check_warn "private-mode" "deployment/status returned non-PRIVATE mode" + else + check_warn "private-mode" "deployment/status unreachable or unexpected response" + fi +fi [ "${ENABLE_IM:-false}" = "true" ] && [ -n "${IM_DOMAIN:-}" ] && \ http_check "http.im-service" "${IM_DOMAIN}/actuator/health" diff --git a/scripts/install.sh b/scripts/install.sh index add284c..5b596d8 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -13,6 +13,7 @@ load_env PROFILE="${COMPOSE_PROFILES:-base}" OFFLINE_BUNDLE="" +SKIP_LOGIN=false usage() { cat <<'EOF' @@ -23,12 +24,17 @@ Options: --mysql-mode external or managed --redis-mode external or managed --offline-bundle Load images from an offline bundle before deployment + --skip-registry-login Skip docker login (use when already logged in or in offline mode) -h, --help Show help EOF } while [ "$#" -gt 0 ]; do case "$1" in + --skip-registry-login) + SKIP_LOGIN=true + shift + ;; --profile) PROFILE="${2:-}" shift 2 @@ -90,6 +96,22 @@ require_cmd docker if [ -n "$OFFLINE_BUNDLE" ]; then [ -f "$OFFLINE_BUNDLE" ] || fail_json "XUQM_PRIVATE_4004" "offline bundle not found: $OFFLINE_BUNDLE" "install" docker load -i "$OFFLINE_BUNDLE" + SKIP_LOGIN=true +fi + +# Login to image registry (skip for offline bundles or when already authenticated) +if [ "$SKIP_LOGIN" = "false" ] && [ -n "${REGISTRY:-}" ]; then + REGISTRY_HOST="$(printf '%s' "$REGISTRY" | cut -d/ -f1)" + REGISTRY_USER="${REGISTRY_USER:-}" + REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-}" + if [ -n "$REGISTRY_USER" ] && [ -n "$REGISTRY_PASSWORD" ]; then + printf '%s' "$REGISTRY_PASSWORD" | \ + docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin \ + || fail_json "XUQM_PRIVATE_4005" "docker login failed for $REGISTRY_HOST" "install" + audit "install" "REGISTRY_LOGIN_OK" "host=$REGISTRY_HOST" + else + audit "install" "REGISTRY_LOGIN_SKIPPED" "REGISTRY_USER/REGISTRY_PASSWORD not set; assuming pre-authenticated" + fi fi if [ "${MYSQL_MODE:-external}" = "managed" ]; then diff --git a/scripts/migrate-tenant.sh b/scripts/migrate-tenant.sh new file mode 100755 index 0000000..d270790 --- /dev/null +++ b/scripts/migrate-tenant.sh @@ -0,0 +1,332 @@ +#!/usr/bin/env bash +# migrate-tenant.sh — Migrate a tenant from the public platform to this private deployment. +# +# What it does: +# 1. Connects to the source (public) MySQL and locates the tenant by email or username. +# 2. Exports t_tenant, t_app, and t_feature_service rows for that tenant. +# 3. Replaces the bootstrap tenant in the private deployment's MySQL with the migrated data. +# 4. Restarts tenant-service to flush any in-memory state. +# 5. Verifies the migration via the SDK config API. +# +# Usage: +# ./scripts/migrate-tenant.sh [options] +# +# Required options: +# --src-host HOST Source MySQL host (production server) +# --src-user USER Source MySQL username +# --src-password PASS Source MySQL password +# --tenant EMAIL|USERNAME Tenant to migrate (matched against email or username) +# +# Optional options: +# --src-port PORT Source MySQL port (default: 3306) +# --src-db DB Source database name (default: xuqm_tenant) +# --dry-run Print generated SQL but do not apply it +# --reset-password PASS Override tenant password after migration (bcrypt-hashed on server) +# -h, --help Show this help +# +# The source MySQL client is resolved in order: docker exec (managed container), +# then a local mysql binary. At least one must be available. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +. "$ROOT_DIR/scripts/lib.sh" +load_env + +SRC_HOST="" +SRC_PORT="3306" +SRC_USER="" +SRC_PASSWORD="" +SRC_DB="xuqm_tenant" +TENANT_IDENT="" +DRY_RUN=false +RESET_PASSWORD="" + +usage() { + awk '/^# Usage:/,/^[^#]/' "$0" | grep '^#' | sed 's/^# \{0,1\}//' +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --src-host) SRC_HOST="${2:-}"; shift 2 ;; + --src-host=*) SRC_HOST="${1#--src-host=}"; shift ;; + --src-port) SRC_PORT="${2:-}"; shift 2 ;; + --src-port=*) SRC_PORT="${1#--src-port=}"; shift ;; + --src-user) SRC_USER="${2:-}"; shift 2 ;; + --src-user=*) SRC_USER="${1#--src-user=}"; shift ;; + --src-password) SRC_PASSWORD="${2:-}"; shift 2 ;; + --src-password=*) SRC_PASSWORD="${1#--src-password=}"; shift ;; + --src-db) SRC_DB="${2:-}"; shift 2 ;; + --src-db=*) SRC_DB="${1#--src-db=}"; shift ;; + --tenant) TENANT_IDENT="${2:-}"; shift 2 ;; + --tenant=*) TENANT_IDENT="${1#--tenant=}"; shift ;; + --dry-run) DRY_RUN=true; shift ;; + --reset-password) RESET_PASSWORD="${2:-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) fail_json "XUQM_PRIVATE_5001" "unknown option: $1" "migrate-tenant" ;; + esac +done + +[ -n "$SRC_HOST" ] || fail_json "XUQM_PRIVATE_5002" "--src-host is required" "migrate-tenant" +[ -n "$SRC_USER" ] || fail_json "XUQM_PRIVATE_5003" "--src-user is required" "migrate-tenant" +[ -n "$SRC_PASSWORD" ] || fail_json "XUQM_PRIVATE_5004" "--src-password is required" "migrate-tenant" +[ -n "$TENANT_IDENT" ] || fail_json "XUQM_PRIVATE_5005" "--tenant is required" "migrate-tenant" + +audit "migrate-tenant" "STARTED" "src=$SRC_HOST tenant=$TENANT_IDENT dry_run=$DRY_RUN" +progress "migrate-tenant" "STARTED" "src=$SRC_HOST tenant=$TENANT_IDENT" + +# --------------------------------------------------------------------------- +# Resolve source and destination MySQL clients +# --------------------------------------------------------------------------- + +# Source: always a remote host, so require a local mysql binary +require_cmd mysql + +src_mysql() { + MYSQL_PWD="$SRC_PASSWORD" mysql \ + -h "$SRC_HOST" -P "$SRC_PORT" -u "$SRC_USER" \ + --default-character-set=utf8mb4 --connect-timeout=10 \ + "$SRC_DB" "$@" +} + +# Destination: prefer managed container, fall back to local binary +DST_MYSQL_CTR="$(compose ps -q mysql 2>/dev/null | head -1 || true)" + +dst_mysql() { + if [ -n "$DST_MYSQL_CTR" ]; then + docker exec -i "$DST_MYSQL_CTR" \ + mysql -u "${MYSQL_USERNAME:-xuqm}" -p"${MYSQL_PASSWORD:-}" \ + --default-character-set=utf8mb4 \ + "${MYSQL_DATABASE:-xuqm_private}" "$@" + elif command -v mysql >/dev/null 2>&1; then + MYSQL_PWD="${MYSQL_PASSWORD:-}" mysql \ + -h "${MYSQL_HOST:-127.0.0.1}" -P "${MYSQL_PORT:-3306}" \ + -u "${MYSQL_USERNAME:-xuqm}" \ + --default-character-set=utf8mb4 \ + "${MYSQL_DATABASE:-xuqm_private}" "$@" + else + fail_json "XUQM_PRIVATE_5006" "no destination MySQL client: container not running and mysql binary not found" "migrate-tenant" + fi +} + +# --------------------------------------------------------------------------- +# Step 1 — Locate the tenant in the source database +# --------------------------------------------------------------------------- +printf '\n[1/5] Locating tenant "%s" in %s/%s ...\n' "$TENANT_IDENT" "$SRC_HOST" "$SRC_DB" + +TENANT_ROW="$(src_mysql -N -e " + SELECT id, username, nickname, email, status + FROM t_tenant + WHERE (email = '${TENANT_IDENT}' OR username = '${TENANT_IDENT}') + AND type = 'MAIN' + LIMIT 1" 2>/dev/null || true)" + +[ -n "$TENANT_ROW" ] || \ + fail_json "XUQM_PRIVATE_5007" "tenant not found in source DB: $TENANT_IDENT" "migrate-tenant" + +TENANT_ID="$(printf '%s' "$TENANT_ROW" | awk '{print $1}')" +TENANT_USERNAME="$(printf '%s' "$TENANT_ROW" | awk '{print $2}')" +TENANT_NICKNAME="$(printf '%s' "$TENANT_ROW" | awk '{print $3}')" +TENANT_EMAIL="$(printf '%s' "$TENANT_ROW" | awk '{print $4}')" +TENANT_STATUS="$(printf '%s' "$TENANT_ROW" | awk '{print $5}')" + +printf ' Found: id=%s username=%s nickname=%s email=%s status=%s\n' \ + "$TENANT_ID" "$TENANT_USERNAME" "$TENANT_NICKNAME" "$TENANT_EMAIL" "$TENANT_STATUS" + +# Count associated data +APP_COUNT="$(src_mysql -N -e "SELECT COUNT(*) FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null || echo 0)" +FS_COUNT="$(src_mysql -N -e " + SELECT COUNT(*) FROM t_feature_service + WHERE app_key IN (SELECT app_key FROM t_app WHERE tenant_id='${TENANT_ID}')" 2>/dev/null || echo 0)" + +printf ' Apps: %s FeatureServices: %s\n' "$APP_COUNT" "$FS_COUNT" + +# --------------------------------------------------------------------------- +# Step 2 — Collect app keys for this tenant +# --------------------------------------------------------------------------- +APP_KEYS_RAW="$(src_mysql -N -e "SELECT app_key FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null || true)" + +if [ -z "$APP_KEYS_RAW" ]; then + printf ' WARNING: tenant has no apps; only tenant record will be migrated.\n' + APP_KEYS_SQL="''" +else + # Build SQL IN-list: 'key1','key2',... + APP_KEYS_SQL="$(printf '%s' "$APP_KEYS_RAW" | awk '{printf "'"'"'%s'"'"',",$1}' | sed 's/,$//')" +fi + +FIRST_APP_KEY="$(printf '%s' "$APP_KEYS_RAW" | head -1 || true)" + +# --------------------------------------------------------------------------- +# Step 3 — Generate migration SQL +# --------------------------------------------------------------------------- +printf '\n[2/5] Generating migration SQL ...\n' + +SQL_FILE="$(mktemp /tmp/xuqm-migrate-XXXXXX.sql)" +trap 'rm -f "$SQL_FILE"' EXIT + +{ + printf '-- XuqmGroup private deployment tenant migration\n' + printf '-- Source: %s/%s Tenant: %s (%s)\n' "$SRC_HOST" "$SRC_DB" "$TENANT_NICKNAME" "$TENANT_ID" + printf '-- Generated: %s\n\n' "$(now)" + + printf 'SET FOREIGN_KEY_CHECKS=0;\n\n' + + # Clear existing data (private deployment is single-tenant) + printf '-- Clear existing tenant data\n' + printf 'DELETE FROM t_feature_service;\n' + printf 'DELETE FROM t_app;\n' + printf 'DELETE FROM t_tenant;\n\n' + + # t_tenant — use explicit column names to be schema-order agnostic + printf '-- Tenant record\n' + src_mysql -N -e " + SELECT CONCAT( + 'INSERT INTO t_tenant (id,created_at,email,nickname,parent_id,password,phone,status,type,username) VALUES (', + QUOTE(id),',',QUOTE(created_at),',',QUOTE(email),',',QUOTE(nickname),',', + IFNULL(QUOTE(parent_id),'NULL'),',',QUOTE(password),',', + IFNULL(QUOTE(phone),'NULL'),',',QUOTE(status),',',QUOTE(type),',',QUOTE(username),');' + ) FROM t_tenant WHERE id='${TENANT_ID}'" 2>/dev/null + + printf '\n' + + # t_app — explicit columns matching private schema (alphabetical: id,app_key,...,tenant_id) + if [ "$APP_COUNT" -gt 0 ]; then + printf '-- Apps\n' + src_mysql -N -e " + SELECT CONCAT( + 'INSERT INTO t_app (id,app_key,app_secret,created_at,description,harmony_bundle_name,icon_url,ios_bundle_id,name,package_name,tenant_id) VALUES (', + QUOTE(id),',',QUOTE(app_key),',',QUOTE(app_secret),',',QUOTE(created_at),',', + IFNULL(QUOTE(description),'NULL'),',',IFNULL(QUOTE(harmony_bundle_name),'NULL'),',', + IFNULL(QUOTE(icon_url),'NULL'),',',IFNULL(QUOTE(ios_bundle_id),'NULL'),',', + QUOTE(name),',',QUOTE(package_name),',',QUOTE(tenant_id),');' + ) FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null + printf '\n' + fi + + # t_feature_service — explicit columns matching private schema (alphabetical: id,app_key,config,...) + if [ "$FS_COUNT" -gt 0 ]; then + printf '-- Feature services\n' + src_mysql -N -e " + SELECT CONCAT( + 'INSERT INTO t_feature_service (id,app_key,config,created_at,enabled,platform,secret_key,service_type) VALUES (', + QUOTE(id),',',QUOTE(app_key),',',IFNULL(QUOTE(config),'NULL'),',',QUOTE(created_at),',', + CAST(enabled AS UNSIGNED),',',QUOTE(platform),',',QUOTE(secret_key),',',QUOTE(service_type),');' + ) FROM t_feature_service WHERE app_key IN (${APP_KEYS_SQL})" 2>/dev/null + printf '\n' + fi + + # Optional password reset via bcrypt (requires htpasswd or python3 available on the deployment host) + if [ -n "$RESET_PASSWORD" ]; then + printf '-- Password reset\n' + if command -v python3 >/dev/null 2>&1 && python3 -c "import bcrypt" 2>/dev/null; then + BCRYPT_HASH="$(python3 -c "import bcrypt; print(bcrypt.hashpw(b'${RESET_PASSWORD}', bcrypt.gensalt(rounds=10)).decode())")" + elif command -v htpasswd >/dev/null 2>&1; then + BCRYPT_HASH="$(htpasswd -bnBC 10 '' "${RESET_PASSWORD}" | tr -d ':\n' | sed 's/\$2y/\$2a/')" + else + printf '-- WARNING: cannot generate bcrypt hash (install python3-bcrypt or apache2-utils)\n' + BCRYPT_HASH="" + fi + if [ -n "$BCRYPT_HASH" ]; then + printf "UPDATE t_tenant SET password='%s' WHERE id='%s';\n" "$BCRYPT_HASH" "$TENANT_ID" + fi + fi + + printf 'SET FOREIGN_KEY_CHECKS=1;\n' + printf '-- Migration SQL complete (%s apps, %s feature services)\n' "$APP_COUNT" "$FS_COUNT" +} > "$SQL_FILE" + +LINE_COUNT="$(wc -l < "$SQL_FILE")" +printf ' Generated %s lines of SQL\n' "$LINE_COUNT" + +# --------------------------------------------------------------------------- +# Step 4 — Apply (or dry-run) +# --------------------------------------------------------------------------- +if [ "$DRY_RUN" = "true" ]; then + printf '\n[DRY RUN] SQL that would be applied:\n' + printf '%s\n' "---" + cat "$SQL_FILE" + printf '%s\n' "---" + printf '\nDry run complete. Re-run without --dry-run to apply.\n' + audit "migrate-tenant" "DRY_RUN" "tenant=$TENANT_ID apps=$APP_COUNT" + exit 0 +fi + +printf '\n[3/5] Applying migration to private deployment ...\n' +dst_mysql < "$SQL_FILE" +audit "migrate-tenant" "SQL_APPLIED" "tenant=$TENANT_ID apps=$APP_COUNT fs=$FS_COUNT" +printf ' Done.\n' + +# --------------------------------------------------------------------------- +# Step 5 — Restart tenant-service to flush any cached state +# --------------------------------------------------------------------------- +printf '\n[4/5] Restarting tenant-service ...\n' +TENANT_SVC_CTR="$(compose ps -q tenant-service 2>/dev/null | head -1 || true)" +if [ -n "$TENANT_SVC_CTR" ]; then + compose restart tenant-service + # Wait up to 30 s for the service to come back + WAITED=0 + while [ "$WAITED" -lt 30 ]; do + sleep 3 + WAITED=$((WAITED + 3)) + HTTP_CODE="$(curl -skL -o /dev/null -w '%{http_code}' --max-time 3 \ + "http://localhost:${NGINX_PORT:-80}/actuator/health" 2>/dev/null || echo '000')" + [ "$HTTP_CODE" = "200" ] && break + done + [ "$HTTP_CODE" = "200" ] || printf ' WARNING: actuator/health not 200 after restart (HTTP %s); continuing.\n' "$HTTP_CODE" + printf ' tenant-service restarted.\n' +else + printf ' WARNING: tenant-service container not found; skipping restart.\n' +fi + +# --------------------------------------------------------------------------- +# Step 6 — Verify +# --------------------------------------------------------------------------- +printf '\n[5/5] Verifying migration ...\n' + +BASE_URL="http://localhost:${NGINX_PORT:-80}" +[ -n "${CONSOLE_DOMAIN:-}" ] && BASE_URL="${CONSOLE_DOMAIN}" + +# Check tenant record +DST_TENANT="$(dst_mysql -N -e " + SELECT id, username, nickname, email FROM t_tenant WHERE id='${TENANT_ID}'" 2>/dev/null || true)" + +if [ -n "$DST_TENANT" ]; then + printf ' DB check: PASS tenant record present (%s)\n' "$TENANT_NICKNAME" +else + fail_json "XUQM_PRIVATE_5008" "tenant record not found in destination after migration" "migrate-tenant" +fi + +DST_APPS="$(dst_mysql -N -e "SELECT COUNT(*) FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null || echo 0)" +printf ' DB check: PASS %s app(s) migrated\n' "$DST_APPS" + +# Check SDK config endpoint for the first migrated app +if [ -n "$FIRST_APP_KEY" ] && command -v curl >/dev/null 2>&1; then + SDK_CODE="$(curl -skL -o /dev/null -w '%{http_code}' --max-time 5 \ + "${BASE_URL}/api/sdk/config?appKey=${FIRST_APP_KEY}&platform=ANDROID" 2>/dev/null || echo '000')" + if [ "$SDK_CODE" = "200" ]; then + printf ' API check: PASS /api/sdk/config returned 200 for %s\n' "$FIRST_APP_KEY" + else + printf ' API check: WARN /api/sdk/config returned %s for %s (service may still be warming up)\n' \ + "$SDK_CODE" "$FIRST_APP_KEY" + fi +fi + +# Confirm deployment status +if command -v curl >/dev/null 2>&1; then + DEPLOY_STATUS="$(curl -skL --max-time 5 "${BASE_URL}/api/private/deployment/status" 2>/dev/null || true)" + if printf '%s' "$DEPLOY_STATUS" | grep -q '"mode":"PRIVATE"'; then + REG_ENABLED="$(printf '%s' "$DEPLOY_STATUS" | grep -o '"tenantRegisterEnabled":[^,}]*' | cut -d: -f2)" + printf ' API check: PASS PRIVATE mode active, tenantRegisterEnabled=%s\n' "$REG_ENABLED" + else + printf ' API check: WARN deployment/status did not confirm PRIVATE mode\n' + fi +fi + +printf '\nMigration complete.\n' +printf ' Tenant: %s (%s)\n' "$TENANT_NICKNAME" "$TENANT_EMAIL" +printf ' Apps: %s\n' "$DST_APPS" +[ -n "$RESET_PASSWORD" ] && printf ' Password: reset to provided value\n' +printf ' Login URL: %s\n' "${BASE_URL:-http://localhost}" + +audit "migrate-tenant" "DONE" "tenant=$TENANT_ID apps=$DST_APPS" +progress "migrate-tenant" "DONE" "tenant=$TENANT_NICKNAME email=$TENANT_EMAIL apps=$DST_APPS"