#!/usr/bin/env bash set -euo pipefail API_BASE="${API_BASE:-http://localhost:5202/api}" AUTH_TOKEN="${AUTH_TOKEN:-}" CURL_TIMEOUT="${CURL_TIMEOUT:-15}" CONNECT_TIMEOUT="${CONNECT_TIMEOUT:-3}" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT note() { printf '%s\n' "$*" } fail() { printf 'ERROR: %s\n' "$*" >&2 exit 1 } json_get() { local file="$1" local path="$2" python3 - "$file" "$path" <<'PY' import json import sys file_path, path = sys.argv[1], sys.argv[2] with open(file_path, 'r', encoding='utf-8') as fh: data = json.load(fh) value = data for part in path.split('.'): if isinstance(value, dict) and part in value: value = value[part] else: raise KeyError(path) if value is None: print('null') elif isinstance(value, bool): print('true' if value else 'false') elif isinstance(value, (dict, list)): print(json.dumps(value, separators=(',', ':'))) else: print(value) PY } validate_json() { local file="$1" if ! python3 - "$file" <<'PY' import json import sys with open(sys.argv[1], 'r', encoding='utf-8') as fh: json.load(fh) PY then printf 'Raw response body:\n' >&2 cat "$file" >&2 fail "Malformed JSON returned by API." fi } request_json() { local name="$1" local url="$2" local body_file="$3" shift 3 local -a headers=("$@") local err_file="$TMP_DIR/${name}.curl.err" local status_file="$TMP_DIR/${name}.status" : >"$err_file" set +e curl -fsS \ --connect-timeout "$CONNECT_TIMEOUT" \ --max-time "$CURL_TIMEOUT" \ -H 'Accept: application/json' \ "${headers[@]}" \ -o "$body_file" \ -w '%{http_code}' \ "$url" >"$status_file" 2>"$err_file" local curl_exit=$? set -e if [[ $curl_exit -eq 0 ]]; then validate_json "$body_file" return 0 fi local status='000' if [[ -s "$status_file" ]]; then status="$(tr -d '\r\n' <"$status_file")" fi case "$curl_exit:$status" in 7:*|28:*|52:*|56:*|6:*) printf 'curl: %s\n' "$(tr -d '\r' <"$err_file")" >&2 fail "Cannot reach ${url}. Start the API from JobTrackerApi/ with 'dotnet run' and verify it is listening on ${API_BASE%/api}." ;; 22:401|22:403) note "${name}: auth required." note "Hint: export AUTH_TOKEN with an admin bearer token from POST ${API_BASE}/auth/login before rerunning this preflight." return 22 ;; 22:*) local diag_body="$TMP_DIR/${name}.diag.body" local diag_status="$TMP_DIR/${name}.diag.status" local diag_err="$TMP_DIR/${name}.diag.err" curl -sS \ --connect-timeout "$CONNECT_TIMEOUT" \ --max-time "$CURL_TIMEOUT" \ -H 'Accept: application/json' \ "${headers[@]}" \ -o "$diag_body" \ -w '%{http_code}' \ "$url" >"$diag_status" 2>"$diag_err" || true local http_status="$(tr -d '\r\n' <"$diag_status")" printf 'curl: %s\n' "$(tr -d '\r' <"$err_file")" >&2 if [[ -s "$diag_body" ]]; then printf 'Response body:\n' >&2 cat "$diag_body" >&2 printf '\n' >&2 fi fail "${name} probe failed with HTTP ${http_status:-unknown}." ;; *) printf 'curl: %s\n' "$(tr -d '\r' <"$err_file")" >&2 fail "${name} probe failed with curl exit ${curl_exit}." ;; esac } auth_config_body="$TMP_DIR/auth-config.json" request_json "auth config" "$API_BASE/auth/config" "$auth_config_body" require_auth="$(json_get "$auth_config_body" 'requireAuth')" google_enabled="$(json_get "$auth_config_body" 'googleEnabled')" local_enabled="$(json_get "$auth_config_body" 'localEnabled')" allow_registration="$(json_get "$auth_config_body" 'allowRegistration')" note "Preflight target: ${API_BASE}" note "cors.requiredOriginPair=UI http://localhost:3000 -> API http://localhost:5202/api" note "auth.requireAuth=${require_auth}" note "auth.localEnabled=${local_enabled}" note "auth.googleEnabled=${google_enabled}" note "auth.allowRegistration=${allow_registration}" system_body="$TMP_DIR/admin-system.json" declare -a auth_headers=() if [[ -n "$AUTH_TOKEN" ]]; then auth_headers+=(-H "Authorization: Bearer ${AUTH_TOKEN}") fi if request_json "admin system" "$API_BASE/admin/system" "$system_body" "${auth_headers[@]}"; then db_provider="$(json_get "$system_body" 'database.provider')" db_configured="$(json_get "$system_body" 'database.looksConfigured')" db_connect="$(json_get "$system_body" 'database.canConnect')" db_target="$(json_get "$system_body" 'database.target')" db_warning="$(json_get "$system_body" 'database.warning')" auth_required_runtime="$(json_get "$system_body" 'auth.required')" gmail_configured="$(json_get "$system_body" 'auth.gmailConfigured')" ai_healthy="$(json_get "$system_body" 'ai.healthy')" ai_model="$(json_get "$system_body" 'ai.model')" ai_last_error="$(json_get "$system_body" 'ai.lastError')" note "db.provider=${db_provider}" note "db.looksConfigured=${db_configured}" note "db.canConnect=${db_connect}" note "db.target=${db_target}" note "db.warning=${db_warning}" note "auth.runtimeRequired=${auth_required_runtime}" note "gmailConfigured=${gmail_configured}" note "ai.healthy=${ai_healthy}" note "ai.model=${ai_model}" note "ai.lastError=${ai_last_error}" if [[ "$db_connect" != "true" ]]; then fail "Database is not connectable according to /admin/system." fi note "Preflight passed." exit 0 fi note "db.provider=unknown" note "db.looksConfigured=unknown" note "db.canConnect=unknown" note "db.target=unknown" note "db.warning=admin token required to inspect database status" note "gmailConfigured=unknown" note "ai.healthy=unknown" note "ai.model=unknown" note "ai.lastError=unknown" note "Preflight partially passed: anonymous auth config is reachable, but admin/system requires an admin bearer token for DB/Gmail/AI details."