Files
Qortal-Nextcloud-Integration/scripts/install-host-nextcloud-hybrid.sh

1446 lines
49 KiB
Bash
Executable File

#!/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 <<USAGE
Usage: ./scripts/install-host-nextcloud-hybrid.sh [options]
Apply the host-hybrid adoption flow to an existing Nextcloud instance.
Options:
--occ-path <path> Host Nextcloud occ path
--nextcloud-path <path> Host Nextcloud root path (derived occ/ path)
--nextcloud-custom-apps-path <path> Host Nextcloud app path
--broker-dir <path> Broker runtime dir (default: /opt/qortal-broker)
--analysis-output <path> Path for analyzer JSON output
--nextcloud-url <url> Explicit Nextcloud public URL
--broker-host <host> Broker public hostname
--broker-url <url> Broker public URL
--external-auth bundled|existing External Auth source
--external-auth-url <url> External Auth base URL (required for existing mode)
--external-auth-app-id <id> External Auth app ID (required for existing mode)
--external-auth-app-secret <sec> External Auth app secret (required for existing mode)
--with-qortal bundled|skip Qortal node source
--qortal-node-url <url> Qortal node base URL
--qortal-node-api-key <key> Qortal node API key
--service-user <user> Nextcloud service auth user
--service-password <pass> Nextcloud service auth password
--broker-db-password <pass> Broker DB password
--oidc-client-id <id> OIDC client ID
--oidc-client-secret <sec> OIDC client secret
--broker-internal-api-token <tok> Broker internal API token
--external-auth-context <path> Qortal External Auth repo path
--qortal-node-context <path> Qortal node repo path
--broker-port <port> Broker bind port
--external-auth-port <port> External Auth bind port
--apache-site-dir <path> Apache vhost dir
--apache-site-name <file> 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 <path> 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/urandom 2>/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:-<empty>}" >&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:-<empty>}" >&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: <empty>" >&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 '<client-secret>' -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}"