Files
2026-03-27 08:54:34 +01:00

196 lines
5.7 KiB
Bash
Executable File

#!/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."