#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "${script_dir}/.." && pwd)" source "${script_dir}/lib-nextcloud-apps-path.sh" broker_dir="/opt/qortal-broker" occ_path="" nextcloud_path="" nextcloud_custom_apps_path="" analysis_output="" nextcloud_url="" broker_host="" broker_url="" external_auth_mode="" external_auth_url="" external_auth_app_id="" external_auth_app_secret="" qortal_mode="" qortal_node_url="" qortal_node_api_key="" service_user="" service_password="" broker_db_password="" oidc_client_id="" oidc_client_secret="" broker_internal_api_token="" external_auth_context="" qortal_node_context="" broker_port="" external_auth_port="" apache_site_dir="" apache_site_name="" refresh_contexts="0" force_refresh_contexts="0" finish_host_nextcloud_upgrade="0" usage() { cat < Host Nextcloud occ path --nextcloud-path Host Nextcloud root path (derived occ/ path) --nextcloud-custom-apps-path Host Nextcloud app path --broker-dir Broker runtime dir (default: /opt/qortal-broker) --analysis-output Path for analyzer JSON output --nextcloud-url Explicit Nextcloud public URL --broker-host Broker public hostname --broker-url Broker public URL --external-auth bundled|existing External Auth source --external-auth-url External Auth base URL (required for existing mode) --external-auth-app-id External Auth app ID (required for existing mode) --external-auth-app-secret External Auth app secret (required for existing mode) --with-qortal bundled|skip Qortal node source --qortal-node-url Qortal node base URL --qortal-node-api-key Qortal node API key --service-user Nextcloud service auth user --service-password Nextcloud service auth password --broker-db-password Broker DB password --oidc-client-id OIDC client ID --oidc-client-secret OIDC client secret --broker-internal-api-token Broker internal API token --external-auth-context Qortal External Auth repo path --qortal-node-context Qortal node repo path --broker-port Broker bind port --external-auth-port External Auth bind port --apache-site-dir Apache vhost dir --apache-site-name Apache vhost filename --refresh-contexts Refresh bundled git contexts before bootstrap --force-refresh-contexts Allow refresh to overwrite dirty bundled git contexts --finish-host-nextcloud-upgrade Run host occ upgrade/repair if host Nextcloud needs DB upgrade --repo-root Repository root (default: auto) -h, --help Show this help USAGE } json_escape() { local input="${1:-}" input="${input//\\/\\\\}" input="${input//\"/\\\"}" input="${input//$'\n'/\\n}" input="${input//$'\r'/}" input="${input//$'\t'/\\t}" printf '%s' "${input}" } trim() { printf '%s' "${1:-}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' } resolve_path() { local raw="${1:-}" if [[ -z "${raw}" ]]; then printf '%s' "" return 0 fi if [[ "${raw}" == /* ]]; then printf '%s' "${raw}" return 0 fi printf '%s' "${repo_root}/${raw}" } gen_secret() { local generated="" while [[ "${#generated}" -lt 32 ]]; do generated+="$( LC_ALL=C tr -dc 'A-Za-z0-9' /dev/null | dd bs=64 count=1 status=none 2>/dev/null || true )" done printf '%.32s' "${generated}" } read_kv() { local key="$1" local file="$2" local line="" if [[ -z "${file}" || ! -f "${file}" ]]; then return 1 fi line="$(grep -m1 -E "^${key}=" "${file}" 2>/dev/null || true)" if [[ -z "${line}" ]]; then return 1 fi printf '%s' "${line#*=}" } read_trimmed_file_value() { local file="$1" if [[ -z "${file}" || ! -f "${file}" ]]; then return 1 fi tr -d '\r\n' < "${file}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' } ensure_qortal_data_permissions() { local data_dir="${1:-}" if [[ -z "${data_dir}" || ! -d "${data_dir}" ]]; then return 0 fi if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then chown -R 1000:100 "${data_dir}" >/dev/null 2>&1 || true chmod -R u+rwX,g+rwX "${data_dir}" >/dev/null 2>&1 || true return 0 fi if command -v sudo >/dev/null 2>&1; then if sudo -n chown -R 1000:100 "${data_dir}" >/dev/null 2>&1 || sudo chown -R 1000:100 "${data_dir}" >/dev/null 2>&1; then sudo chmod -R u+rwX,g+rwX "${data_dir}" >/dev/null 2>&1 || true return 0 fi fi echo "Warning: could not repair Qortal data ownership for ${data_dir}." >&2 } ensure_nextcloud_app_permissions() { local apps_dir="${1:-}" if [[ -z "${apps_dir}" || ! -d "${apps_dir}" ]]; then return 0 fi if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then chown -R www-data:www-data "${apps_dir}" >/dev/null 2>&1 || true chmod -R u=rwX,g=rX,o= "${apps_dir}" >/dev/null 2>&1 || true return 0 fi if command -v sudo >/dev/null 2>&1; then if sudo -n chown -R www-data:www-data "${apps_dir}" >/dev/null 2>&1 || sudo chown -R www-data:www-data "${apps_dir}" >/dev/null 2>&1; then sudo chmod -R u=rwX,g=rX,o= "${apps_dir}" >/dev/null 2>&1 || true return 0 fi fi echo "Warning: could not repair Nextcloud app ownership for ${apps_dir}." >&2 } set_kv() { local key="$1" local value="$2" local file="$3" local escaped escaped="${value//\\/\\\\}" escaped="${escaped//&/\\&}" escaped="${escaped//|/\\|}" mkdir -p "$(dirname "${file}")" if [[ -f "${file}" ]] && grep -qE "^${key}=" "${file}"; then sed -i -E "s|^${key}=.*|${key}=${escaped}|" "${file}" else printf '%s=%s\n' "${key}" "${value}" >> "${file}" fi } ensure_root() { if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then echo "Run this installer as root (sudo)." >&2 exit 1 fi } occ_cmd() { if [[ -z "${occ_path}" || ! -f "${occ_path}" ]]; then echo "OCC path not found: ${occ_path}" >&2 return 1 fi if [[ -z "$(command -v php || true)" ]]; then echo "php is required to query occ." >&2 return 1 fi if command -v sudo >/dev/null 2>&1; then sudo -u www-data php "${occ_path}" "$@" return $? fi if command -v runuser >/dev/null 2>&1; then runuser -u www-data -- php "${occ_path}" "$@" return $? fi php "${occ_path}" "$@" } occ_cmd_with_pass() { local oc_pass="$1" shift if command -v sudo >/dev/null 2>&1; then OC_PASS="${oc_pass}" sudo --preserve-env=OC_PASS -u www-data php "${occ_path}" "$@" return $? fi if command -v runuser >/dev/null 2>&1; then OC_PASS="${oc_pass}" runuser -u www-data -- php "${occ_path}" "$@" return $? fi OC_PASS="${oc_pass}" php "${occ_path}" "$@" } read_occ_status_json() { occ_cmd status --output=json 2>/dev/null || true } json_bool_true() { local json="$1" local key="$2" printf '%s' "${json}" | grep -q "\"${key}\"[[:space:]]*:[[:space:]]*true" } ensure_host_nextcloud_upgrade_state() { local phase="$1" local status_json="" local ran_host_upgrade="0" status_json="$(read_occ_status_json)" if [[ -z "${status_json}" ]]; then echo "Unable to read host Nextcloud status during ${phase}." >&2 echo "Run manually: sudo -u www-data php ${occ_path} status" >&2 return 1 fi if json_bool_true "${status_json}" "needsDbUpgrade"; then if [[ "${finish_host_nextcloud_upgrade}" != "1" ]]; then echo "Host Nextcloud requires a database upgrade during ${phase}; refusing to continue." >&2 echo "Run the NCVM updater first, or finish manually:" >&2 echo " sudo bash /var/scripts/update.sh" >&2 echo " sudo -u www-data php ${occ_path} upgrade" >&2 echo "Then rerun this script." >&2 echo "To let this script run the host occ upgrade/repair steps, rerun with --finish-host-nextcloud-upgrade." >&2 return 1 fi echo "Host Nextcloud needs DB upgrade; running host occ upgrade..." occ_cmd upgrade ran_host_upgrade="1" occ_cmd maintenance:repair occ_cmd db:add-missing-indices || true occ_cmd maintenance:mimetype:update-js || true status_json="$(read_occ_status_json)" if [[ -z "${status_json}" ]] || json_bool_true "${status_json}" "needsDbUpgrade"; then echo "Host Nextcloud still requires a database upgrade after occ upgrade." >&2 return 1 fi fi if json_bool_true "${status_json}" "maintenance"; then if [[ "${finish_host_nextcloud_upgrade}" == "1" && "${ran_host_upgrade}" == "1" ]]; then echo "Host Nextcloud is in maintenance mode; disabling it after upgrade checks..." occ_cmd maintenance:mode --off else echo "Host Nextcloud is in maintenance mode during ${phase}; refusing to continue." >&2 echo "Resolve maintenance mode first, then rerun this script." >&2 return 1 fi fi } compose_cmd() { docker compose -f "${broker_dir}/docker-compose.yml" --env-file "${broker_dir}/.env" "$@" } detect_occ_path() { local candidates=( "/var/www/nextcloud/occ" "/var/www/html/nextcloud/occ" "/var/www/html/occ" "/srv/www/nextcloud/occ" "/var/lib/nextcloud/occ" ) local candidate for candidate in "${candidates[@]}"; do if [[ -f "${candidate}" ]]; then printf '%s' "${candidate}" return 0 fi done return 1 } derive_nextcloud_url() { local detected="" detected="$(occ_cmd config:system:get overwrite.cli.url 2>/dev/null | tr -d '\r' || true)" if [[ -z "${detected}" ]]; then detected="$(occ_cmd config:system:get trusted_domains 0 2>/dev/null | tr -d '\r' || true)" if [[ -n "${detected}" && "${detected}" != http* ]]; then detected="https://${detected}" fi fi printf '%s' "${detected}" } infer_base_domain() { local host="${1:-}" if [[ "${host}" == *.* ]]; then printf '%s' "${host#*.}" else printf '%s' "${host}" fi } wait_for_http() { local url="$1" local label="$2" local attempts="${3:-20}" local delay_seconds="${4:-2}" local attempt for attempt in $(seq 1 "${attempts}"); do if curl -fsS --max-time 10 "${url}" >/dev/null 2>&1; then return 0 fi if [[ "${attempt}" -lt "${attempts}" ]]; then sleep "${delay_seconds}" fi done echo "Timed out waiting for ${label} at ${url}" >&2 return 1 } qortal_node_api_key_retry_count() { local raw="${QNI_QORTAL_NODE_API_KEY_RETRIES:-48}" if [[ "${raw}" =~ ^[0-9]+$ ]] && (( raw > 0 )); then printf '%s' "${raw}" return 0 fi printf '%s' "48" } qortal_node_api_key_retry_sleep() { local raw="${QNI_QORTAL_NODE_API_KEY_SLEEP_SECONDS:-15}" if [[ "${raw}" =~ ^[0-9]+$ ]] && (( raw > 0 )); then printf '%s' "${raw}" return 0 fi printf '%s' "15" } extract_json_value() { local json="$1" local key="$2" if command -v jq >/dev/null 2>&1; then printf '%s' "${json}" | jq -r --arg key "${key}" '.[$key] // .data[$key] // empty' 2>/dev/null || true return 0 fi printf '%s' "${json}" | sed -n "s/.*\"${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n1 } register_external_auth_app_with_retry() { local register_url="$1" local payload="$2" local attempts="${3:-18}" local delay_seconds="${4:-2}" local attempt register_resp new_app_id new_app_secret for attempt in $(seq 1 "${attempts}"); do register_resp="$(curl -fsS --max-time 15 -X POST "${register_url}" -H 'content-type: application/json' -d "${payload}" || true)" new_app_id="$(extract_json_value "${register_resp}" "appId")" new_app_secret="$(extract_json_value "${register_resp}" "appSecret")" if [[ -n "${new_app_id}" && -n "${new_app_secret}" ]]; then printf '%s\n%s\n' "${new_app_id}" "${new_app_secret}" return 0 fi if [[ "${attempt}" -lt "${attempts}" ]]; then sleep "${delay_seconds}" fi done echo "Failed to register app via external-auth." >&2 echo "Last response: ${register_resp:-}" >&2 return 1 } generate_qortal_api_key_with_retry() { local generate_url="$1" local attempts="${2:-}" local delay_seconds="${3:-}" local attempt key_resp generated_key if [[ -z "${attempts}" ]]; then attempts="$(qortal_node_api_key_retry_count)" fi if [[ -z "${delay_seconds}" ]]; then delay_seconds="$(qortal_node_api_key_retry_sleep)" fi for attempt in $(seq 1 "${attempts}"); do key_resp="$(curl -fsS --max-time 20 -X POST "${generate_url}" 2>/dev/null || true)" generated_key="$(extract_json_value "${key_resp}" "apiKey")" if [[ -z "${generated_key}" ]]; then generated_key="$(printf '%s' "${key_resp}" | tr -d '\r\n')" fi if [[ -n "${generated_key}" && "${generated_key}" != "null" ]]; then printf '%s\n' "${generated_key}" return 0 fi if [[ "${attempt}" -lt "${attempts}" ]]; then sleep "${delay_seconds}" fi done echo "Failed to generate node API key after ${attempts} attempts at ${delay_seconds}s intervals." >&2 echo "Last response: ${key_resp:-}" >&2 echo "Check qortal_node logs with:" >&2 echo " docker compose -f ${compose_file} --env-file ${env_file} logs qortal_node" >&2 return 1 } refresh_git_checkout() { local repo_path="$1" local label="$2" local branch="" local dirty_status="" if [[ ! -d "${repo_path}/.git" ]]; then echo "Skipping refresh for ${label}; not a git checkout: ${repo_path}" return 0 fi if [[ -z "$(command -v git || true)" ]]; then echo "git is unavailable; skipping refresh for ${label}." >&2 return 0 fi dirty_status="$(git -C "${repo_path}" status --porcelain 2>/dev/null || true)" if [[ -n "${dirty_status}" && "${force_refresh_contexts}" != "1" ]]; then echo "Refusing to refresh dirty ${label} checkout at ${repo_path}." >&2 echo "Commit/stash local changes, or rerun with --force-refresh-contexts to overwrite them." >&2 return 1 fi echo "Refreshing ${label} checkout at ${repo_path}..." git -C "${repo_path}" fetch --depth 1 origin branch="$(git -C "${repo_path}" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##' || true)" if [[ -z "${branch}" ]]; then branch="$(git -C "${repo_path}" remote show origin 2>/dev/null | sed -n '/HEAD branch/s/.*: //p' | head -n1 || true)" fi if [[ -z "${branch}" ]]; then if git -C "${repo_path}" show-ref --verify --quiet refs/remotes/origin/main; then branch="main" elif git -C "${repo_path}" show-ref --verify --quiet refs/remotes/origin/master; then branch="master" else echo "Could not determine default branch for ${label} at ${repo_path}" >&2 return 1 fi fi git -C "${repo_path}" checkout -B "${branch}" "origin/${branch}" git -C "${repo_path}" reset --hard "origin/${branch}" git -C "${repo_path}" clean -fd } ensure_nextcloud_service_user() { local password="$1" if occ_cmd user:info "${service_user}" >/dev/null 2>&1; then echo "Resetting password for existing Nextcloud service user ${service_user}..." occ_cmd_with_pass "${password}" user:resetpassword --password-from-env "${service_user}" >/dev/null else echo "Creating Nextcloud service user ${service_user}..." occ_cmd_with_pass "${password}" user:add --password-from-env --display-name="${service_user}" "${service_user}" >/dev/null fi occ_cmd group:adduser admin "${service_user}" >/dev/null 2>&1 || true } ensure_app_present() { local app_name="$1" local required="${2:-0}" local allow_appstore_install="${3:-1}" local enable_output="" local install_output="" local app_path="" app_path="$(occ_cmd app:getpath "${app_name}" 2>/dev/null | tr -d '\r' || true)" if enable_output="$(occ_cmd app:enable --force "${app_name}" 2>&1)"; then echo "Enabled ${app_name}" return 0 fi if [[ "${allow_appstore_install}" == "1" ]]; then if install_output="$(occ_cmd app:install "${app_name}" 2>&1)"; then if enable_output="$(occ_cmd app:enable --force "${app_name}" 2>&1)"; then echo "Installed and enabled ${app_name}" return 0 fi fi fi if [[ "${required}" == "1" ]]; then if [[ "${allow_appstore_install}" == "1" ]]; then echo "Failed to enable required app: ${app_name}" >&2 else echo "Failed to enable required local app: ${app_name}" >&2 fi if [[ -n "${enable_output}" ]]; then echo "occ app:enable output:" >&2 echo "${enable_output}" >&2 fi if [[ -n "${app_path}" ]]; then echo "occ app:getpath output: ${app_path}" >&2 else echo "occ app:getpath output: " >&2 echo "Expected local app path: ${nextcloud_custom_apps_path%/}/${app_name}" >&2 fi if [[ -n "${install_output}" ]]; then echo "occ app:install output:" >&2 echo "${install_output}" >&2 fi if [[ "${app_name}" == "qortal_integration" ]]; then local nc_status nc_version nc_status="$(occ_cmd status 2>&1 || true)" nc_version="$(printf '%s\n' "${nc_status}" | sed -n 's/^[[:space:]]*- versionstring:[[:space:]]*//p' | head -n1)" if [[ -n "${nc_version}" ]]; then echo "Detected Nextcloud version: ${nc_version}" >&2 echo "qortal_integration currently declares compatibility only through Nextcloud 34." >&2 fi fi return 1 fi if [[ -n "${enable_output}" ]]; then echo "Warning: could not enable optional app ${app_name}" >&2 echo "${enable_output}" >&2 else echo "Warning: could not enable optional app ${app_name}" >&2 fi return 0 } ensure_oidc_provider() { local issuer_url="$1" local client_secret="$2" if ! occ_cmd user_oidc:provider --help >/dev/null 2>&1; then echo "user_oidc provider command is unavailable." >&2 return 1 fi if ! occ_cmd user_oidc:provider qortal \ -c "${oidc_client_id}" \ -s "${client_secret}" \ -d "${issuer_url}/.well-known/openid-configuration" \ --scope="openid profile email" \ --mapping-uid=sub \ --mapping-display-name=name \ --mapping-email=email >/dev/null 2>&1; then echo "Failed to configure user_oidc provider." >&2 echo "Run manually:" >&2 echo " sudo -u www-data php ${occ_path} user_oidc:provider qortal -c ${oidc_client_id} -s '' -d ${issuer_url}/.well-known/openid-configuration --scope='openid profile email' --mapping-uid=sub --mapping-display-name=name --mapping-email=email" >&2 return 1 fi } ensure_login_label() { local sc_mode="" sc_mode="$(occ_cmd config:app:get qortal_integration sc_mode 2>/dev/null | tr -d '\r' || true)" if [[ "${sc_mode}" == "full_qortal" ]]; then occ_cmd config:system:delete user_oidc login_label >/dev/null 2>&1 || true else occ_cmd config:system:set user_oidc login_label --value="Create Account / Login" >/dev/null 2>&1 || true fi } normalize_oidc_require_email_setting() { local raw="${1:-}" raw="$(echo "${raw}" | tr '[:upper:]' '[:lower:]' | xargs)" case "${raw}" in ""|default|env|required|on|true|1|yes) echo "required" ;; off|false|0|no|optional) echo "off" ;; *) echo "required" ;; esac } sync_qortal_integration_runtime_config() { local broker_base_url="$1" local oidc_issuer_url="$2" local oidc_client_id_value="$3" local oidc_client_secret_value="$4" local oidc_require_email_for_new_account_raw="$5" local host_external_auth_url="$6" local host_external_auth_docs_url="$7" local external_auth_node_url="$8" local host_qortal_node_url="${9:-}" local qortal_gateway_url="${10:-}" local node_api_key="${11:-}" occ_cmd config:app:set qortal_integration broker_base_url --value="${broker_base_url}" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration external_auth_base_url --value="${host_external_auth_url}" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration external_auth_app_id --value="${external_auth_app_id}" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration external_auth_app_secret --value="${external_auth_app_secret}" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration external_auth_docs_url --value="${host_external_auth_docs_url}" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration external_auth_node_url --value="${external_auth_node_url}" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration external_auth_node_api_key --value="${node_api_key}" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration external_auth_node_api_key_mode --value="paths" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration external_auth_node_api_key_paths --value="/" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration oidc_issuer_url --value="${oidc_issuer_url}" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration oidc_client_id --value="${oidc_client_id_value}" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration oidc_client_secret --value="${oidc_client_secret_value}" >/dev/null 2>&1 || return 1 oidc_require_email_for_new_account="$(normalize_oidc_require_email_setting "${oidc_require_email_for_new_account_raw}")" occ_cmd config:app:set qortal_integration oidc_require_email_for_new_account --value="${oidc_require_email_for_new_account}" >/dev/null 2>&1 || return 1 if [[ -n "${broker_base_url}" && -n "${broker_internal_api_token}" ]]; then oidc_require_email_for_new_account_payload="false" if [[ "${oidc_require_email_for_new_account}" == "required" ]]; then oidc_require_email_for_new_account_payload="true" fi curl -fsS --max-time 30 -X POST "${broker_base_url%/}/api/oidc/config" \ -H 'content-type: application/json' \ -H "X-Broker-Internal-Token: ${broker_internal_api_token}" \ -d "{\"requireEmailForNewAccount\":${oidc_require_email_for_new_account_payload}}" >/dev/null 2>&1 || return 1 fi occ_cmd config:app:set qortal_integration qortal_node_url --value="${host_qortal_node_url}" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration qortal_node_api_key --value="${node_api_key}" >/dev/null 2>&1 || return 1 if [[ -n "${qortal_gateway_url}" ]]; then occ_cmd config:app:set qortal_integration qortal_gateway_url --value="${qortal_gateway_url}" >/dev/null 2>&1 || return 1 fi occ_cmd config:app:set qortal_integration nextcloud_public_url --value="${nextcloud_url}" >/dev/null 2>&1 || return 1 occ_cmd config:app:set qortal_integration broker_internal_api_token --value="${broker_internal_api_token}" >/dev/null 2>&1 || return 1 } hydrate_broker_runtime_from_nextcloud() { local current_value="" if [[ -z "${external_auth_app_id}" ]]; then current_value="$(occ_cmd config:app:get qortal_integration external_auth_app_id 2>/dev/null | tr -d '\r' | xargs || true)" if [[ -n "${current_value}" ]]; then external_auth_app_id="${current_value}" set_kv "QORTAL_EXTERNAL_AUTH_APP_ID" "${external_auth_app_id}" "${env_file}" fi fi if [[ -z "${external_auth_app_secret}" ]]; then current_value="$(occ_cmd config:app:get qortal_integration external_auth_app_secret 2>/dev/null | tr -d '\r' | xargs || true)" if [[ -n "${current_value}" ]]; then external_auth_app_secret="${current_value}" set_kv "QORTAL_EXTERNAL_AUTH_APP_SECRET" "${external_auth_app_secret}" "${env_file}" fi fi if [[ -z "${qortal_auth_node_api_key}" ]]; then current_value="$(occ_cmd config:app:get qortal_integration external_auth_node_api_key 2>/dev/null | tr -d '\r' | xargs || true)" if [[ -n "${current_value}" ]]; then qortal_auth_node_api_key="${current_value}" set_kv "QORTAL_AUTH_NODE_API_KEY" "${qortal_auth_node_api_key}" "${env_file}" set_kv "QORTAL_NODE_API_KEY" "${qortal_auth_node_api_key}" "${env_file}" set_kv "QORTAL_AUTH_NODE_API_KEY_MODE" "paths" "${env_file}" set_kv "QORTAL_AUTH_NODE_API_KEY_PATHS" "/" "${env_file}" fi fi } start_compose_services() { local -a services=("$@") if [[ "${#services[@]}" -eq 0 ]]; then return 0 fi compose_cmd up -d --build "${services[@]}" } wait_for_compose_service() { local service_name="$1" local command="$2" compose_cmd exec -T "${service_name}" sh -c "${command}" >/dev/null 2>&1 } while [[ $# -gt 0 ]]; do case "$1" in --occ-path) occ_path="${2:-}" shift 2 ;; --occ-path=*) occ_path="${1#*=}" shift ;; --nextcloud-path) nextcloud_path="${2:-}" shift 2 ;; --nextcloud-path=*) nextcloud_path="${1#*=}" shift ;; --nextcloud-custom-apps-path) nextcloud_custom_apps_path="${2:-}" shift 2 ;; --nextcloud-custom-apps-path=*) nextcloud_custom_apps_path="${1#*=}" shift ;; --broker-dir) broker_dir="${2:-}" shift 2 ;; --broker-dir=*) broker_dir="${1#*=}" shift ;; --analysis-output) analysis_output="${2:-}" shift 2 ;; --analysis-output=*) analysis_output="${1#*=}" shift ;; --nextcloud-url) nextcloud_url="${2:-}" shift 2 ;; --nextcloud-url=*) nextcloud_url="${1#*=}" shift ;; --broker-host) broker_host="${2:-}" shift 2 ;; --broker-host=*) broker_host="${1#*=}" shift ;; --broker-url) broker_url="${2:-}" shift 2 ;; --broker-url=*) broker_url="${1#*=}" shift ;; --external-auth) external_auth_mode="${2:-}" shift 2 ;; --external-auth=*) external_auth_mode="${1#*=}" shift ;; --external-auth-url) external_auth_url="${2:-}" shift 2 ;; --external-auth-url=*) external_auth_url="${1#*=}" shift ;; --external-auth-app-id) external_auth_app_id="${2:-}" shift 2 ;; --external-auth-app-id=*) external_auth_app_id="${1#*=}" shift ;; --external-auth-app-secret) external_auth_app_secret="${2:-}" shift 2 ;; --external-auth-app-secret=*) external_auth_app_secret="${1#*=}" shift ;; --with-qortal) qortal_mode="${2:-}" shift 2 ;; --with-qortal=*) qortal_mode="${1#*=}" shift ;; --qortal-node-url) qortal_node_url="${2:-}" shift 2 ;; --qortal-node-url=*) qortal_node_url="${1#*=}" shift ;; --qortal-node-api-key) qortal_node_api_key="${2:-}" shift 2 ;; --qortal-node-api-key=*) qortal_node_api_key="${1#*=}" shift ;; --service-user) service_user="${2:-}" shift 2 ;; --service-user=*) service_user="${1#*=}" shift ;; --service-password) service_password="${2:-}" shift 2 ;; --service-password=*) service_password="${1#*=}" shift ;; --broker-db-password) broker_db_password="${2:-}" shift 2 ;; --broker-db-password=*) broker_db_password="${1#*=}" shift ;; --oidc-client-id) oidc_client_id="${2:-}" shift 2 ;; --oidc-client-id=*) oidc_client_id="${1#*=}" shift ;; --oidc-client-secret) oidc_client_secret="${2:-}" shift 2 ;; --oidc-client-secret=*) oidc_client_secret="${1#*=}" shift ;; --broker-internal-api-token) broker_internal_api_token="${2:-}" shift 2 ;; --broker-internal-api-token=*) broker_internal_api_token="${1#*=}" shift ;; --external-auth-context) external_auth_context="${2:-}" shift 2 ;; --external-auth-context=*) external_auth_context="${1#*=}" shift ;; --qortal-node-context) qortal_node_context="${2:-}" shift 2 ;; --qortal-node-context=*) qortal_node_context="${1#*=}" shift ;; --broker-port) broker_port="${2:-}" shift 2 ;; --broker-port=*) broker_port="${1#*=}" shift ;; --external-auth-port) external_auth_port="${2:-}" shift 2 ;; --external-auth-port=*) external_auth_port="${1#*=}" shift ;; --apache-site-dir) apache_site_dir="${2:-}" shift 2 ;; --apache-site-dir=*) apache_site_dir="${1#*=}" shift ;; --apache-site-name) apache_site_name="${2:-}" shift 2 ;; --apache-site-name=*) apache_site_name="${1#*=}" shift ;; --refresh-contexts) refresh_contexts="1" shift ;; --force-refresh-contexts) refresh_contexts="1" force_refresh_contexts="1" shift ;; --finish-host-nextcloud-upgrade) finish_host_nextcloud_upgrade="1" shift ;; --repo-root) repo_root="${2:-}" shift 2 ;; --repo-root=*) repo_root="${1#*=}" shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage exit 1 ;; esac done if [[ "${repo_root}" != /* ]]; then repo_root="$(cd "${repo_root}" 2>/dev/null && pwd)" fi ensure_root if [[ -z "${broker_dir}" ]]; then broker_dir="/opt/qortal-broker" fi broker_dir="$(resolve_path "${broker_dir}")" analysis_output="${analysis_output:-${broker_dir}/existing-nextcloud-analysis.json}" analysis_output="$(resolve_path "${analysis_output}")" env_file="${broker_dir}/.env" compose_file="${broker_dir}/docker-compose.yml" if [[ -z "${nextcloud_path}" && -n "${occ_path}" ]]; then nextcloud_path="$(cd "$(dirname "${occ_path}")" 2>/dev/null && pwd || true)" fi if [[ -z "${occ_path}" && -n "${nextcloud_path}" ]]; then occ_path="${nextcloud_path%/}/occ" fi if [[ -z "${occ_path}" ]]; then occ_path="$(detect_occ_path || true)" fi if [[ -n "${occ_path}" ]]; then occ_path="$(resolve_path "${occ_path}")" fi if [[ -z "${nextcloud_path}" && -n "${occ_path}" ]]; then nextcloud_path="$(cd "$(dirname "${occ_path}")" 2>/dev/null && pwd || true)" fi if [[ -n "${nextcloud_path}" ]]; then nextcloud_path="$(resolve_path "${nextcloud_path}")" fi if [[ -z "${nextcloud_path}" && -n "${occ_path}" ]]; then nextcloud_path="$(cd "$(dirname "${occ_path}")" 2>/dev/null && pwd || true)" fi if [[ -z "${nextcloud_path}" ]]; then echo "Unable to determine Nextcloud path. Pass --occ-path or --nextcloud-path." >&2 exit 1 fi ensure_host_nextcloud_upgrade_state "preflight" requested_nextcloud_apps_path="${nextcloud_custom_apps_path}" if [[ -z "${requested_nextcloud_apps_path}" ]]; then requested_nextcloud_apps_path="$(read_kv "HOST_NEXTCLOUD_CUSTOM_APPS_PATH" "${env_file}" || true)" fi if [[ -n "${requested_nextcloud_apps_path}" ]]; then requested_nextcloud_apps_path="$(resolve_path "${requested_nextcloud_apps_path}")" fi selected_nextcloud_apps_path="$(nextcloud_select_apps_path "${nextcloud_path}" "${requested_nextcloud_apps_path}")" if [[ -z "${selected_nextcloud_apps_path}" ]]; then echo "Unable to determine a usable Nextcloud apps path from config.php or ${nextcloud_path}/apps." >&2 exit 1 fi if [[ -n "${requested_nextcloud_apps_path}" && "${selected_nextcloud_apps_path}" != "${requested_nextcloud_apps_path}" ]]; then echo "Requested Nextcloud app path is not configured; using ${selected_nextcloud_apps_path} instead." >&2 fi nextcloud_custom_apps_path="${selected_nextcloud_apps_path}" mkdir -p "${nextcloud_custom_apps_path}" set_kv "HOST_NEXTCLOUD_CUSTOM_APPS_PATH" "${nextcloud_custom_apps_path}" "${env_file}" set_kv "NEXTCLOUD_CUSTOM_APPS_PATH" "${nextcloud_custom_apps_path}" "${env_file}" if [[ -z "${nextcloud_url}" ]]; then nextcloud_url="$(read_kv "NEXTCLOUD_PUBLIC_URL" "${env_file}" || true)" fi if [[ -z "${nextcloud_url}" ]]; then nextcloud_url="$(derive_nextcloud_url)" fi nextcloud_url="$(trim "${nextcloud_url}")" if [[ -z "${nextcloud_url}" ]]; then echo "Could not determine Nextcloud URL. Pass --nextcloud-url." >&2 exit 1 fi if [[ -z "${broker_host}" ]]; then broker_host="$(read_kv "HOST_NEXTCLOUD_BROKER_HOST" "${env_file}" || true)" fi if [[ -z "${broker_host}" ]]; then nextcloud_host="${nextcloud_url#*://}" nextcloud_host="${nextcloud_host%%/*}" broker_host="qortalbroker.$(infer_base_domain "${nextcloud_host}")" fi if [[ -z "${broker_url}" ]]; then broker_url="$(read_kv "HOST_NEXTCLOUD_BROKER_URL" "${env_file}" || true)" fi if [[ -z "${broker_url}" ]]; then broker_url="https://${broker_host}" fi if [[ -z "${external_auth_mode}" ]]; then external_auth_mode="$(read_kv "HOST_NEXTCLOUD_EXTERNAL_AUTH_MODE" "${env_file}" || true)" fi external_auth_mode="${external_auth_mode:-bundled}" if [[ "${external_auth_mode}" != "bundled" && "${external_auth_mode}" != "existing" ]]; then echo "Invalid --external-auth value: ${external_auth_mode}" >&2 exit 1 fi if [[ -z "${qortal_mode}" ]]; then qortal_mode="$(read_kv "HOST_NEXTCLOUD_QORTAL_MODE" "${env_file}" || true)" fi qortal_mode="${qortal_mode:-bundled}" if [[ "${qortal_mode}" != "bundled" && "${qortal_mode}" != "skip" ]]; then echo "Invalid --with-qortal value: ${qortal_mode}" >&2 exit 1 fi if [[ -z "${service_user}" ]]; then service_user="$(read_kv "NEXTCLOUD_SERVICE_USER" "${env_file}" || true)" fi service_user="${service_user:-nuqloud_oidc_service}" if [[ -z "${service_password}" ]]; then service_password="$(read_kv "NEXTCLOUD_SERVICE_PASSWORD" "${env_file}" || true)" fi service_password="${service_password:-$(gen_secret)}" if [[ -z "${broker_db_password}" ]]; then broker_db_password="$(read_kv "BROKER_DB_PASSWORD" "${env_file}" || true)" fi broker_db_password="${broker_db_password:-$(gen_secret)}" if [[ -z "${oidc_client_id}" ]]; then oidc_client_id="$(read_kv "OIDC_CLIENT_ID" "${env_file}" || true)" fi oidc_client_id="${oidc_client_id:-nextcloud-local}" if [[ -z "${oidc_client_secret}" ]]; then oidc_client_secret="$(read_kv "OIDC_CLIENT_SECRET" "${env_file}" || true)" fi oidc_client_secret="${oidc_client_secret:-$(gen_secret)}" oidc_require_email_for_new_account="$(read_kv "OIDC_REQUIRE_EMAIL_FOR_NEW_ACCOUNT" "${env_file}" || true)" if [[ -z "${oidc_require_email_for_new_account}" ]]; then oidc_require_email_for_new_account="true" fi oidc_branding_mode="$(read_kv "OIDC_BRANDING_MODE" "${env_file}" || true)" oidc_branding_mode="${oidc_branding_mode:-powered}" oidc_policy_mode="$(read_kv "OIDC_POLICY_MODE" "${env_file}" || true)" oidc_policy_mode="${oidc_policy_mode:-auto_provision}" oidc_auto_provision_guard="$(read_kv "OIDC_AUTO_PROVISION_GUARD" "${env_file}" || true)" oidc_auto_provision_guard="${oidc_auto_provision_guard:-invite_or_allowlist}" oidc_invite_ttl_seconds="$(read_kv "OIDC_INVITE_TTL_SECONDS" "${env_file}" || true)" oidc_invite_ttl_seconds="${oidc_invite_ttl_seconds:-604800}" oidc_auth_request_ttl_seconds="$(read_kv "OIDC_AUTH_REQUEST_TTL_SECONDS" "${env_file}" || true)" oidc_auth_request_ttl_seconds="${oidc_auth_request_ttl_seconds:-600}" oidc_auth_code_ttl_seconds="$(read_kv "OIDC_AUTH_CODE_TTL_SECONDS" "${env_file}" || true)" oidc_auth_code_ttl_seconds="${oidc_auth_code_ttl_seconds:-120}" oidc_access_token_ttl_seconds="$(read_kv "OIDC_ACCESS_TOKEN_TTL_SECONDS" "${env_file}" || true)" oidc_access_token_ttl_seconds="${oidc_access_token_ttl_seconds:-600}" oidc_id_token_ttl_seconds="$(read_kv "OIDC_ID_TOKEN_TTL_SECONDS" "${env_file}" || true)" oidc_id_token_ttl_seconds="${oidc_id_token_ttl_seconds:-600}" if [[ -z "${broker_internal_api_token}" ]]; then broker_internal_api_token="$(read_kv "BROKER_INTERNAL_API_TOKEN" "${env_file}" || true)" fi broker_internal_api_token="${broker_internal_api_token:-$(gen_secret)}" if [[ -z "${external_auth_context}" ]]; then external_auth_context="$(read_kv "EXTERNAL_AUTH_CONTEXT" "${env_file}" || true)" fi if [[ -z "${external_auth_context}" ]]; then external_auth_context="../Qortal-External-Auth" fi external_auth_context="$(resolve_path "${external_auth_context}")" if [[ -z "${qortal_node_context}" ]]; then qortal_node_context="$(read_kv "QORTAL_NODE_CONTEXT" "${env_file}" || true)" fi if [[ -z "${qortal_node_context}" ]]; then qortal_node_context="../qortal" fi qortal_node_context="$(resolve_path "${qortal_node_context}")" if [[ -z "${broker_port}" ]]; then broker_port="$(read_kv "BROKER_PORT" "${env_file}" || true)" fi broker_port="${broker_port:-3000}" if [[ -z "${external_auth_port}" ]]; then external_auth_port="$(read_kv "EXTERNAL_AUTH_PORT" "${env_file}" || true)" fi external_auth_port="${external_auth_port:-3191}" if [[ -z "${apache_site_dir}" ]]; then apache_site_dir="$(read_kv "HOST_NEXTCLOUD_APACHE_SITE_DIR" "${env_file}" || true)" fi apache_site_dir="${apache_site_dir:-/etc/apache2/sites-available}" if [[ -z "${apache_site_name}" ]]; then apache_site_name="$(read_kv "HOST_NEXTCLOUD_APACHE_SITE_NAME" "${env_file}" || true)" fi apache_site_name="${apache_site_name:-qortalbroker.conf}" if [[ -z "${external_auth_url}" ]]; then external_auth_url="$(read_kv "QORTAL_EXTERNAL_AUTH_BASE_URL" "${env_file}" || true)" fi if [[ "${external_auth_mode}" == "bundled" ]]; then external_auth_url="${external_auth_url:-http://external_auth:3191}" else external_auth_url="$(trim "${external_auth_url}")" if [[ -z "${external_auth_url}" ]]; then echo "--external-auth-url is required when using --external-auth existing." >&2 exit 1 fi fi if [[ -z "${qortal_node_url}" ]]; then qortal_node_url="$(read_kv "QORTAL_AUTH_NODE_URL" "${env_file}" || true)" fi if [[ "${qortal_mode}" == "bundled" ]]; then qortal_node_url="${qortal_node_url:-http://qortal_node:12391}" else qortal_node_url="$(trim "${qortal_node_url}")" if [[ -z "${qortal_node_url}" ]]; then echo "--qortal-node-url is required when using --with-qortal skip." >&2 exit 1 fi fi if [[ -z "${external_auth_app_id}" ]]; then external_auth_app_id="$(read_kv "QORTAL_EXTERNAL_AUTH_APP_ID" "${env_file}" || true)" fi if [[ -z "${external_auth_app_secret}" ]]; then external_auth_app_secret="$(read_kv "QORTAL_EXTERNAL_AUTH_APP_SECRET" "${env_file}" || true)" fi if [[ "${external_auth_mode}" == "existing" && ( -z "${external_auth_app_id}" || -z "${external_auth_app_secret}" ) ]]; then echo "--external-auth-app-id and --external-auth-app-secret are required for external auth existing mode." >&2 exit 1 fi if [[ "${refresh_contexts}" == "1" ]]; then if [[ "${qortal_mode}" == "bundled" ]]; then "${repo_root}/scripts/ensure-qortal-core-repo.sh" --target-path "${qortal_node_context}" --refresh fi if [[ "${external_auth_mode}" == "bundled" ]]; then refresh_git_checkout "${external_auth_context}" "Qortal External Auth" fi fi analysis_output="${analysis_output:-${broker_dir}/existing-nextcloud-analysis.json}" analysis_output="$(resolve_path "${analysis_output}")" mkdir -p "$(dirname "${analysis_output}")" if [[ -x "${repo_root}/scripts/analyze-existing-nextcloud-instance.sh" ]]; then "${repo_root}/scripts/analyze-existing-nextcloud-instance.sh" --json --occ-path "${occ_path}" --output-file "${analysis_output}" fi bootstrap_script="${repo_root}/scripts/bootstrap-host-nextcloud-hybrid.sh" if [[ ! -x "${bootstrap_script}" ]]; then echo "Missing bootstrap script: ${bootstrap_script}" >&2 exit 1 fi bootstrap_cmd=( "${bootstrap_script}" --repo-root "${repo_root}" --occ-path "${occ_path}" --broker-dir "${broker_dir}" --nextcloud-url "${nextcloud_url}" --broker-host "${broker_host}" --broker-url "${broker_url}" --external-auth "${external_auth_mode}" --qortal-node-url "${qortal_node_url}" --service-user "${service_user}" --service-password "${service_password}" --broker-db-password "${broker_db_password}" --oidc-client-id "${oidc_client_id}" --oidc-client-secret "${oidc_client_secret}" --broker-internal-api-token "${broker_internal_api_token}" --external-auth-context "${external_auth_context}" --qortal-node-context "${qortal_node_context}" --broker-port "${broker_port}" --external-auth-port "${external_auth_port}" --apache-site-dir "${apache_site_dir}" --apache-site-name "${apache_site_name}" --nextcloud-custom-apps-path "${nextcloud_custom_apps_path}" ) if [[ -n "${external_auth_app_id}" && -n "${external_auth_app_secret}" ]]; then bootstrap_cmd+=( --external-auth-app-id "${external_auth_app_id}" --external-auth-app-secret "${external_auth_app_secret}" ) fi if [[ "${external_auth_mode}" == "existing" ]]; then bootstrap_cmd+=( --external-auth-url "${external_auth_url}" ) fi if [[ -n "${qortal_node_api_key}" ]]; then bootstrap_cmd+=(--qortal-node-api-key "${qortal_node_api_key}") fi echo "Applying host-hybrid bootstrap..." printf ' repo_root: %s\n' "${repo_root}" printf ' occ_path: %s\n' "${occ_path}" printf ' broker_dir: %s\n' "${broker_dir}" printf ' nextcloud_url: %s\n' "${nextcloud_url}" printf ' broker_url: %s\n' "${broker_url}" printf ' external_auth_mode: %s\n' "${external_auth_mode}" printf ' qortal_mode: %s\n' "${qortal_mode}" printf ' nextcloud_custom_apps_path: %s\n' "${nextcloud_custom_apps_path}" printf ' external_auth_context: %s\n' "${external_auth_context}" printf ' qortal_node_context: %s\n' "${qortal_node_context}" echo "${bootstrap_cmd[@]}" env_file="${broker_dir}/.env" compose_file="${broker_dir}/docker-compose.yml" if [[ ! -f "${env_file}" || ! -f "${compose_file}" ]]; then echo "Bootstrap did not create the expected broker artifacts in ${broker_dir}." >&2 exit 1 fi set_kv "OIDC_POLICY_MODE" "${oidc_policy_mode}" "${env_file}" set_kv "OIDC_AUTO_PROVISION_GUARD" "${oidc_auto_provision_guard}" "${env_file}" set_kv "OIDC_REQUIRE_EMAIL_FOR_NEW_ACCOUNT" "${oidc_require_email_for_new_account}" "${env_file}" set_kv "OIDC_BRANDING_MODE" "${oidc_branding_mode}" "${env_file}" set_kv "OIDC_INVITE_TTL_SECONDS" "${oidc_invite_ttl_seconds}" "${env_file}" set_kv "OIDC_AUTH_REQUEST_TTL_SECONDS" "${oidc_auth_request_ttl_seconds}" "${env_file}" set_kv "OIDC_AUTH_CODE_TTL_SECONDS" "${oidc_auth_code_ttl_seconds}" "${env_file}" set_kv "OIDC_ACCESS_TOKEN_TTL_SECONDS" "${oidc_access_token_ttl_seconds}" "${env_file}" set_kv "OIDC_ID_TOKEN_TTL_SECONDS" "${oidc_id_token_ttl_seconds}" "${env_file}" if [[ -x "${repo_root}/scripts/ensure-oidc-signing-key.sh" ]]; then "${repo_root}/scripts/ensure-oidc-signing-key.sh" "${env_file}" fi host_external_auth_url="$(read_kv "HOST_NEXTCLOUD_EXTERNAL_AUTH_URL" "${env_file}" || true)" host_qortal_node_url="$(read_kv "HOST_NEXTCLOUD_QORTAL_NODE_URL" "${env_file}" || true)" host_qortal_node_gateway_host_port="$(read_kv "QORTAL_NODE_GATEWAY_HOST_PORT" "${env_file}" || true)" host_qortal_node_api_host_port="$(read_kv "QORTAL_NODE_API_HOST_PORT" "${env_file}" || true)" host_external_auth_port="$(read_kv "EXTERNAL_AUTH_PORT" "${env_file}" || true)" broker_port="$(read_kv "BROKER_PORT" "${env_file}" || true)" qortal_auth_node_api_key="$(read_kv "QORTAL_AUTH_NODE_API_KEY" "${env_file}" || true)" qortal_api_key_file="${broker_dir}/qortal/data/apikey.txt" if [[ "${qortal_mode}" == "bundled" && -f "${qortal_api_key_file}" ]]; then file_qortal_auth_node_api_key="$(read_trimmed_file_value "${qortal_api_key_file}" || true)" if [[ -n "${file_qortal_auth_node_api_key}" ]]; then qortal_auth_node_api_key="${file_qortal_auth_node_api_key}" echo "Using existing Qortal node API key from ${qortal_api_key_file}." fi fi if [[ "${qortal_auth_node_api_key}" == "null" ]]; then qortal_auth_node_api_key="" fi host_broker_base_url="http://127.0.0.1:${broker_port:-3000}" if [[ -z "${host_external_auth_url}" ]]; then host_external_auth_url="http://127.0.0.1:${host_external_auth_port:-3191}" fi host_external_auth_docs_url="${host_external_auth_url%/}/docs/static/index.html" if [[ -z "${host_qortal_node_url}" ]]; then host_qortal_node_url="http://127.0.0.1:${host_qortal_node_api_host_port:-12391}" fi host_qortal_gateway_url="" if [[ "${qortal_mode}" == "bundled" ]]; then host_qortal_gateway_url="http://127.0.0.1:${host_qortal_node_gateway_host_port:-12390}" fi if [[ "${qortal_mode}" == "bundled" ]]; then mkdir -p "${broker_dir}/qortal/data" "${repo_root}/scripts/ensure-qortal-settings.sh" --target-dir "${broker_dir}/qortal/data" "${repo_root}/scripts/ensure-qortal-start-args.sh" --target-dir "${broker_dir}/qortal/data" "${env_file}" fi hydrate_broker_runtime_from_nextcloud if [[ -x "${repo_root}/scripts/ensure-user-oidc-app.sh" ]]; then NEXTCLOUD_CUSTOM_APPS_PATH="${nextcloud_custom_apps_path}" "${repo_root}/scripts/ensure-user-oidc-app.sh" fi if [[ -x "${repo_root}/scripts/ensure-qortal-integration-app.sh" ]]; then NEXTCLOUD_CUSTOM_APPS_PATH="${nextcloud_custom_apps_path}" "${repo_root}/scripts/ensure-qortal-integration-app.sh" fi ensure_nextcloud_app_permissions "${nextcloud_custom_apps_path}" if [[ "${qortal_mode}" == "bundled" ]]; then echo "Starting bundled qortal_node..." compose_cmd up -d qortal_node fi if [[ "${qortal_mode}" == "bundled" ]]; then if [[ -z "${qortal_auth_node_api_key}" ]]; then qortal_node_api_key_attempts="$(qortal_node_api_key_retry_count)" qortal_node_api_key_sleep_seconds="$(qortal_node_api_key_retry_sleep)" echo "Generating Qortal node API key (up to ${qortal_node_api_key_attempts} attempts at ${qortal_node_api_key_sleep_seconds}s intervals)..." qortal_auth_node_api_key="$( generate_qortal_api_key_with_retry \ "http://127.0.0.1:${host_qortal_node_api_host_port:-12391}/admin/apikey/generate" \ "${qortal_node_api_key_attempts}" \ "${qortal_node_api_key_sleep_seconds}" )" fi if [[ -z "${qortal_auth_node_api_key}" || "${qortal_auth_node_api_key}" == "null" ]]; then echo "Failed to resolve Qortal node API key." >&2 exit 1 fi set_kv "QORTAL_AUTH_NODE_API_KEY" "${qortal_auth_node_api_key}" "${env_file}" set_kv "QORTAL_NODE_API_KEY" "${qortal_auth_node_api_key}" "${env_file}" set_kv "QORTAL_AUTH_NODE_API_KEY_MODE" "paths" "${env_file}" set_kv "QORTAL_AUTH_NODE_API_KEY_PATHS" "/" "${env_file}" mkdir -p "$(dirname "${qortal_api_key_file}")" printf '%s\n' "${qortal_auth_node_api_key}" > "${qortal_api_key_file}" ensure_qortal_data_permissions "$(dirname "${qortal_api_key_file}")" fi if [[ "${external_auth_mode}" == "bundled" ]]; then echo "Starting bundled external_auth..." compose_cmd up -d --force-recreate external_auth wait_for_http "http://127.0.0.1:${host_external_auth_port:-3191}/health" "bundled external-auth" fi if [[ "${external_auth_mode}" == "bundled" ]]; then app_credentials_valid="0" if [[ -n "${external_auth_app_id}" && -n "${external_auth_app_secret}" ]]; then if curl -fsS --max-time 15 -X POST "http://127.0.0.1:${host_external_auth_port:-3191}/sessions" \ -H 'content-type: application/json' \ -d "{\"appId\":\"$(json_escape "${external_auth_app_id}")\",\"appSecret\":\"$(json_escape "${external_auth_app_secret}")\"}" >/dev/null 2>&1; then app_credentials_valid="1" fi fi if [[ "${app_credentials_valid}" != "1" ]]; then echo "Registering app with external-auth..." register_output="$( register_external_auth_app_with_retry \ "http://127.0.0.1:${host_external_auth_port:-3191}/apps/register" \ "{\"name\":\"qortal-nextcloud-integration\"}" )" external_auth_app_id="$(printf '%s\n' "${register_output}" | sed -n '1p')" external_auth_app_secret="$(printf '%s\n' "${register_output}" | sed -n '2p')" set_kv "QORTAL_EXTERNAL_AUTH_APP_ID" "${external_auth_app_id}" "${env_file}" set_kv "QORTAL_EXTERNAL_AUTH_APP_SECRET" "${external_auth_app_secret}" "${env_file}" fi elif [[ -z "${external_auth_app_id}" || -z "${external_auth_app_secret}" ]]; then echo "External Auth app credentials are missing for existing mode." >&2 exit 1 fi echo "Starting broker..." compose_cmd up -d --force-recreate broker wait_for_http "http://127.0.0.1:${broker_port:-3000}/.well-known/openid-configuration" "broker" echo "Configuring host Nextcloud..." ensure_nextcloud_service_user "${service_password}" ensure_app_present "user_oidc" "1" ensure_app_present "qortal_integration" "1" "0" for optional_app in custom_pwa chd_admin qortal_files_bridge qortal_talk_bridge nuqloud_scrum; do if [[ -d "${nextcloud_custom_apps_path%/}/${optional_app}" ]]; then ensure_app_present "${optional_app}" "0" "0" || true fi done ensure_host_nextcloud_upgrade_state "post-app-enable" ensure_oidc_provider "${broker_url}" "${oidc_client_secret}" ensure_login_label sync_qortal_integration_runtime_config \ "${host_broker_base_url}" \ "${broker_url}" \ "${oidc_client_id}" \ "${oidc_client_secret}" \ "${oidc_require_email_for_new_account}" \ "${host_external_auth_url}" \ "${host_external_auth_docs_url}" \ "${qortal_node_url}" \ "${host_qortal_node_url}" \ "${host_qortal_gateway_url}" \ "${qortal_auth_node_api_key}" apache_site_path="${apache_site_dir%/}/${apache_site_name}" apache_preview_path="${broker_dir}/${apache_site_name}" if command -v a2enmod >/dev/null 2>&1 && command -v a2ensite >/dev/null 2>&1; then mkdir -p "${apache_site_dir}" cp "${apache_preview_path}" "${apache_site_path}" a2enmod proxy proxy_http headers ssl >/dev/null 2>&1 || true a2ensite "${apache_site_name}" >/dev/null 2>&1 || true if command -v systemctl >/dev/null 2>&1; then systemctl reload apache2 >/dev/null 2>&1 || true fi else echo "Apache utilities not available; leaving reverse-proxy preview at ${apache_preview_path}." >&2 fi echo "Validation checks..." curl -fsS --max-time 15 "http://127.0.0.1:${broker_port:-3000}/.well-known/openid-configuration" >/dev/null 2>&1 || true curl -fsS --max-time 15 "http://127.0.0.1:${host_external_auth_port:-3191}/health" >/dev/null 2>&1 || true occ_cmd app:list >/dev/null 2>&1 echo "Host-hybrid install complete." echo " broker_dir: ${broker_dir}" echo " nextcloud_url: ${nextcloud_url}" echo " broker_url: ${broker_url}" echo " host_external_auth_url: ${host_external_auth_url}" echo " host_qortal_node_url: ${host_qortal_node_url}" echo " analysis_report: ${analysis_output}"