#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "${script_dir}/.." && pwd)" mode="nossl" env_file="${repo_root}/.env.devprod" restart_services="0" env_changed="0" app_config_changed="0" usage() { cat < Env file to update (default: .env.devprod) --restart-services Recreate broker + external_auth if env values change -h, --help Show this help USAGE } read_kv() { local key="$1" local line line="$(grep -m1 -E "^${key}=" "${env_file}" || true)" if [[ -z "${line}" ]]; then return 1 fi echo "${line#*=}" } set_kv() { local key="$1" local value="$2" local esc esc="${value//\\/\\\\}" esc="${esc//&/\\&}" esc="${esc//|/\\|}" if grep -q -E "^${key}=" "${env_file}"; then sed -i -E "s|^${key}=.*|${key}=${esc}|" "${env_file}" else echo "${key}=${value}" >> "${env_file}" fi } json_escape() { local input="$1" input="${input//\\/\\\\}" input="${input//\"/\\\"}" input="${input//$'\n'/ }" input="${input//$'\r'/ }" echo "${input}" } extract_json_value() { local json="$1" local key="$2" if command -v jq >/dev/null 2>&1; then echo "${json}" | jq -r --arg key "${key}" '.[$key] // .data[$key] // empty' return 0 fi echo "${json}" | sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n1 } ensure_docker_access() { local docker_path="" docker_path="$(type -P docker || true)" if [[ -z "${docker_path}" ]]; then echo "docker is required" exit 1 fi if "${docker_path}" info >/dev/null 2>&1; then DOCKER_CMD=("${docker_path}") return 0 fi if ! command -v sudo >/dev/null 2>&1; then echo "docker is installed but this user cannot access it, and sudo is unavailable." exit 1 fi if sudo -n "${docker_path}" info >/dev/null 2>&1; then DOCKER_CMD=(sudo "${docker_path}") return 0 fi echo "docker requires elevated privileges on this machine." echo "You may be prompted for your sudo password." if sudo "${docker_path}" info >/dev/null 2>&1; then DOCKER_CMD=(sudo "${docker_path}") return 0 fi echo "Unable to access docker, even with sudo." exit 1 } docker() { "${DOCKER_CMD[@]}" "$@" } occ_app_get() { local key="$1" docker compose -f "${compose_file}" --env-file "${env_file}" exec -T app \ php occ config:app:get qortal_integration "${key}" 2>/dev/null | tr -d '\r' | xargs || true } occ_app_set() { local key="$1" local value="$2" docker compose -f "${compose_file}" --env-file "${env_file}" exec -T app \ php occ config:app:set qortal_integration "${key}" --value="${value}" >/dev/null 2>&1 } test_external_auth_session() { local app_id="$1" local app_secret="$2" local response token if [[ -z "${app_id}" || -z "${app_secret}" ]]; then return 1 fi response="$( curl -sS --max-time 15 -X POST "http://127.0.0.1:${external_auth_host_port}/sessions" \ -H 'content-type: application/json' \ -d "{\"appId\":\"$(json_escape "${app_id}")\",\"appSecret\":\"$(json_escape "${app_secret}")\"}" || true )" token="$(extract_json_value "${response}" "token")" [[ -n "${token}" ]] } mark_env_external_auth_credentials() { local app_id="$1" local app_secret="$2" set_kv "QORTAL_EXTERNAL_AUTH_APP_ID" "${app_id}" set_kv "QORTAL_EXTERNAL_AUTH_APP_SECRET" "${app_secret}" env_changed="1" } mark_app_config_external_auth_credentials() { local app_id="$1" local app_secret="$2" occ_app_set "external_auth_app_id" "${app_id}" occ_app_set "external_auth_app_secret" "${app_secret}" app_config_changed="1" } mark_env_node_api_key() { local node_api_key="$1" set_kv "QORTAL_AUTH_NODE_API_KEY" "${node_api_key}" set_kv "QORTAL_AUTH_NODE_API_KEY_MODE" "paths" set_kv "QORTAL_AUTH_NODE_API_KEY_PATHS" "/" env_changed="1" } mark_app_config_node_api_key() { local node_api_key="$1" occ_app_set "external_auth_node_api_key" "${node_api_key}" occ_app_set "qortal_node_api_key" "${node_api_key}" app_config_changed="1" } while [[ $# -gt 0 ]]; do case "$1" in --mode) mode="${2:-}" shift 2 ;; --mode=*) mode="${1#*=}" shift ;; --env-file) env_file="${2:-}" shift 2 ;; --env-file=*) env_file="${1#*=}" shift ;; --restart-services) restart_services="1" shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" usage exit 1 ;; esac done if [[ "${mode}" != "ssl" && "${mode}" != "nossl" ]]; then echo "Invalid mode: ${mode}" exit 1 fi if [[ "${env_file}" != /* ]]; then env_file="${repo_root}/${env_file}" fi if [[ ! -f "${env_file}" ]]; then echo "Missing env file: ${env_file}" exit 1 fi compose_file="${repo_root}/docker-compose.devprod.nossl.yml" if [[ "${mode}" == "ssl" ]]; then compose_file="${repo_root}/docker-compose.devprod.yml" fi external_auth_host_port="$(read_kv "EXTERNAL_AUTH_PORT" || true)" external_auth_host_port="${external_auth_host_port:-3191}" ensure_docker_access if ! docker compose -f "${compose_file}" --env-file "${env_file}" exec -T app php occ status >/dev/null 2>&1; then echo "Nextcloud OCC is not ready; skipping bundled runtime env sync." exit 0 fi stored_app_id="$(occ_app_get "external_auth_app_id")" stored_app_secret="$(occ_app_get "external_auth_app_secret")" stored_node_api_key="$(occ_app_get "external_auth_node_api_key")" if [[ -z "${stored_node_api_key}" ]]; then stored_node_api_key="$(occ_app_get "qortal_node_api_key")" fi current_app_id="$(read_kv "QORTAL_EXTERNAL_AUTH_APP_ID" || true)" current_app_secret="$(read_kv "QORTAL_EXTERNAL_AUTH_APP_SECRET" || true)" current_node_api_key="$(read_kv "QORTAL_AUTH_NODE_API_KEY" || true)" stored_creds_valid="0" env_creds_valid="0" if test_external_auth_session "${stored_app_id}" "${stored_app_secret}"; then stored_creds_valid="1" fi if test_external_auth_session "${current_app_id}" "${current_app_secret}"; then env_creds_valid="1" fi if [[ -n "${stored_app_id}" && -n "${stored_app_secret}" ]]; then if [[ "${stored_app_id}" != "${current_app_id}" || "${stored_app_secret}" != "${current_app_secret}" ]]; then if [[ "${stored_creds_valid}" == "1" && "${env_creds_valid}" != "1" ]]; then mark_env_external_auth_credentials "${stored_app_id}" "${stored_app_secret}" elif [[ "${env_creds_valid}" == "1" && "${stored_creds_valid}" != "1" ]]; then mark_app_config_external_auth_credentials "${current_app_id}" "${current_app_secret}" elif [[ "${stored_creds_valid}" == "1" && "${env_creds_valid}" == "1" ]]; then mark_env_external_auth_credentials "${stored_app_id}" "${stored_app_secret}" fi fi fi if [[ -n "${stored_node_api_key}" && "${stored_node_api_key}" != "${current_node_api_key}" ]]; then if [[ -z "${current_node_api_key}" ]]; then mark_env_node_api_key "${stored_node_api_key}" else mark_app_config_node_api_key "${current_node_api_key}" fi elif [[ -n "${current_node_api_key}" && -z "${stored_node_api_key}" ]]; then mark_app_config_node_api_key "${current_node_api_key}" fi if [[ "${stored_creds_valid}" != "1" && "${env_creds_valid}" == "1" ]]; then echo "Bundled runtime note: Nextcloud app config external-auth credentials were invalid; synchronized from env." elif [[ "${env_creds_valid}" != "1" && "${stored_creds_valid}" == "1" ]]; then echo "Bundled runtime note: env external-auth credentials were invalid; synchronized from Nextcloud app config." elif [[ "${stored_creds_valid}" != "1" && "${env_creds_valid}" != "1" ]]; then echo "Bundled runtime credentials match their sources, but external_auth rejected both credential sets." echo "Current App ID source values are not valid for /sessions." if [[ -z "${current_node_api_key}" && -z "${stored_node_api_key}" ]]; then echo "QORTAL_AUTH_NODE_API_KEY is also missing in both env and Nextcloud app config." fi exit 1 fi if [[ -z "${current_node_api_key}" && -z "${stored_node_api_key}" ]]; then echo "Warning: QORTAL_AUTH_NODE_API_KEY is missing in both env and Nextcloud app config." fi if [[ "${env_changed}" == "1" || "${app_config_changed}" == "1" ]]; then echo "Synchronized bundled runtime credentials from Nextcloud app config into ${env_file}." if [[ "${restart_services}" == "1" ]]; then echo "Recreating broker + external_auth to apply synchronized env values..." docker compose -f "${compose_file}" --env-file "${env_file}" up -d --force-recreate --no-deps broker external_auth fi else echo "Bundled runtime credentials are aligned and valid against external_auth." fi