1446 lines
49 KiB
Bash
Executable File
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}"
|