#!/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" default_qortal_repo_url="https://github.com/Qortal/qortal.git" default_external_auth_repo_url="https://gitea.qortal.link/crowetic/Qortal-External-Auth.git" occ_path="/var/www/nextcloud/occ" broker_dir="/opt/qortal-broker" apache_site_dir="/etc/apache2/sites-available" apache_site_name="qortalbroker.conf" dry_run="0" external_auth_mode="bundled" qortal_mode="bundled" nextcloud_url="" nextcloud_root="" nextcloud_custom_apps_path="" broker_host="" broker_url="" external_auth_url="" external_auth_app_id="" external_auth_app_secret="" qortal_node_url="" qortal_node_api_key="" service_user="nuqloud_oidc_service" service_password="" broker_db_password="" oidc_client_id="nextcloud-local" oidc_client_secret="" broker_internal_api_token="" external_auth_context="" qortal_node_context="" external_auth_port="3191" broker_port="3000" qortal_node_gateway_host_port="12390" qortal_node_api_host_port="12391" qortal_node_p2p_host_port="12392" qortal_node_qdn_host_port="12394" external_auth_context_explicit="0" qortal_node_context_explicit="0" usage() { cat < Host Nextcloud occ path (default: /var/www/nextcloud/occ) --broker-dir Output/runtime dir for broker sidecars (default: /opt/qortal-broker) --nextcloud-url Explicit Nextcloud public URL (auto-detected from occ when omitted) --broker-host Broker public hostname (default: qortalbroker.) --broker-url Broker public URL (default: https://) --external-auth bundled|existing External Auth source (default: bundled) --external-auth-url External Auth base URL (required for existing mode) --external-auth-app-id External Auth app ID (required for existing mode) --external-auth-app-secret External Auth app secret (required for existing mode) --with-qortal bundled|skip Qortal node source (default: bundled) --qortal-node-url Qortal node base URL (default: bundled node URL) --qortal-node-api-key Qortal node API key (optional; auto-generated later when bundled) --service-user Nextcloud service auth user (default: nuqloud_oidc_service) --service-password Nextcloud service auth password (auto-generated when omitted) --broker-db-password Broker DB password (auto-generated when omitted) --oidc-client-id OIDC client ID (default: nextcloud-local) --oidc-client-secret OIDC client secret (auto-generated when omitted) --broker-internal-api-token Broker internal API token (auto-generated when omitted) --external-auth-context Qortal External Auth repo path (default: ../Qortal-External-Auth) --qortal-node-context Qortal node repo path (default: ../qortal) --nextcloud-custom-apps-path Host Nextcloud app path (default: detected existing apps path, else /apps) --broker-port Broker bind port (default: 3000) --external-auth-port External Auth bind port (default: 3191) --apache-site-dir Apache vhost dir (default: /etc/apache2/sites-available) --apache-site-name Apache vhost filename (default: qortalbroker.conf) --repo-root Repository root (default: auto) --dry-run Print intended actions without writing files -h, --help Show this help USAGE } gen_secret() { local generated="" while [[ "${#generated}" -lt 32 ]]; do generated+="$( LC_ALL=C tr -dc 'A-Za-z0-9' /dev/null | dd bs=64 count=1 status=none 2>/dev/null || true )" done printf '%.32s' "${generated}" } trim() { printf '%s' "${1:-}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' } run_cmd() { if [[ "${dry_run}" == "1" ]]; then printf '[dry-run] ' printf '%q ' "$@" printf '\n' return 0 fi "$@" } occ_cmd() { if [[ ! -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 [[ "${EUID:-$(id -u)}" -eq 0 ]]; then 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 fi php "${occ_path}" "$@" } 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 } resolve_context_path() { local value="${1:-}" if [[ -z "${value}" ]]; then printf '%s' "" return 0 fi if [[ "${value}" == /* ]]; then printf '%s' "${value}" else printf '%s' "$(cd "${repo_root}" 2>/dev/null && pwd)/${value}" fi } ensure_context_repo() { local label="$1" local target_path="$2" local repo_url="$3" if [[ -d "${target_path}/.git" ]]; then echo "Using existing ${label} repo at ${target_path}" return 0 fi if [[ -e "${target_path}" ]]; then echo "${label} context exists but is not a git repo: ${target_path}" >&2 echo "Remove it or pass a valid repo path explicitly." >&2 return 1 fi if [[ "${dry_run}" != "1" ]] && ! command -v git >/dev/null 2>&1; then echo "git is required to clone ${label} into ${target_path}" >&2 return 1 fi echo "Cloning ${label} repo into ${target_path}" run_cmd mkdir -p "$(dirname "${target_path}")" run_cmd git clone --depth 1 "${repo_url}" "${target_path}" } while [[ $# -gt 0 ]]; do case "$1" in --occ-path) occ_path="${2:-}" shift 2 ;; --occ-path=*) occ_path="${1#*=}" shift ;; --broker-dir) broker_dir="${2:-}" shift 2 ;; --broker-dir=*) broker_dir="${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-url) external_auth_url="${2:-}" shift 2 ;; --external-auth-url=*) external_auth_url="${1#*=}" shift ;; --external-auth) external_auth_mode="${2:-}" shift 2 ;; --external-auth=*) external_auth_mode="${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:-}" external_auth_context_explicit="1" shift 2 ;; --external-auth-context=*) external_auth_context="${1#*=}" external_auth_context_explicit="1" shift ;; --qortal-node-context) qortal_node_context="${2:-}" qortal_node_context_explicit="1" shift 2 ;; --qortal-node-context=*) qortal_node_context="${1#*=}" qortal_node_context_explicit="1" shift ;; --nextcloud-custom-apps-path) nextcloud_custom_apps_path="${2:-}" shift 2 ;; --nextcloud-custom-apps-path=*) nextcloud_custom_apps_path="${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 ;; --repo-root) repo_root="${2:-}" shift 2 ;; --repo-root=*) repo_root="${1#*=}" shift ;; --dry-run) dry_run="1" shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage exit 1 ;; esac done echo "Preparing host-nextcloud-hybrid bootstrap..." echo " repo_root: ${repo_root}" echo " occ_path: ${occ_path}" echo " dry_run: ${dry_run}" if [[ "${repo_root}" != /* ]]; then repo_root="$(cd "${repo_root}" 2>/dev/null && pwd)" fi external_auth_context="${external_auth_context:-../Qortal-External-Auth}" qortal_node_context="${qortal_node_context:-../qortal}" external_auth_context="$(resolve_context_path "${external_auth_context}")" qortal_node_context="$(resolve_context_path "${qortal_node_context}")" nextcloud_root="$(cd "$(dirname "${occ_path}")" 2>/dev/null && pwd || true)" if [[ -z "${nextcloud_root}" ]]; then echo "Unable to derive Nextcloud root from occ path: ${occ_path}" >&2 exit 1 fi if [[ -z "${nextcloud_custom_apps_path}" ]]; then nextcloud_custom_apps_path="$(nextcloud_select_apps_path "${nextcloud_root}")" else if [[ "${nextcloud_custom_apps_path}" != /* ]]; then nextcloud_custom_apps_path="$(resolve_context_path "${nextcloud_custom_apps_path}")" fi selected_nextcloud_apps_path="$(nextcloud_select_apps_path "${nextcloud_root}" "${nextcloud_custom_apps_path}")" if [[ -n "${selected_nextcloud_apps_path}" ]]; then if [[ "${selected_nextcloud_apps_path}" != "${nextcloud_custom_apps_path}" ]]; then echo "Requested Nextcloud app path is not configured; using ${selected_nextcloud_apps_path} instead." fi nextcloud_custom_apps_path="${selected_nextcloud_apps_path}" fi fi if [[ -z "${nextcloud_custom_apps_path}" ]]; then echo "Unable to determine a usable Nextcloud app path from config.php or ${nextcloud_root}/apps." >&2 exit 1 fi mkdir -p "${nextcloud_custom_apps_path}" if [[ "${external_auth_mode}" != "bundled" && "${external_auth_mode}" != "existing" ]]; then echo "Invalid --external-auth value: ${external_auth_mode}" >&2 exit 1 fi if [[ "${qortal_mode}" != "bundled" && "${qortal_mode}" != "skip" ]]; then echo "Invalid --with-qortal value: ${qortal_mode}" >&2 exit 1 fi if [[ "${external_auth_mode}" == "bundled" && "${qortal_mode}" != "bundled" ]]; then echo "Bundled external_auth currently requires bundled qortal_node." >&2 exit 1 fi if [[ "${qortal_mode}" == "skip" && -z "${qortal_node_url}" ]]; then echo "--qortal-node-url is required when using --with-qortal skip." >&2 exit 1 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 detect Nextcloud public URL from occ. Pass --nextcloud-url." >&2 exit 1 fi nextcloud_host="$(printf '%s' "${nextcloud_url}" | sed -E 's#^[a-z]+://##; s#/.*$##')" base_domain="$(infer_base_domain "${nextcloud_host}")" if [[ -z "${broker_host}" ]]; then broker_host="qortalbroker.${base_domain}" fi if [[ -z "${broker_url}" ]]; then broker_url="https://${broker_host}" fi if [[ "${external_auth_mode}" == "bundled" ]]; then 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 if [[ -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 fi if [[ -z "${qortal_node_url}" && "${qortal_mode}" == "bundled" ]]; then qortal_node_url="http://qortal_node:12391" fi host_external_auth_url="${external_auth_url}" if [[ "${external_auth_mode}" == "bundled" ]]; then host_external_auth_url="http://127.0.0.1:${external_auth_port}" fi host_qortal_node_url="${qortal_node_url}" if [[ "${qortal_mode}" == "bundled" ]]; then host_qortal_node_url="http://127.0.0.1:${qortal_node_api_host_port}" fi if [[ -z "${service_password}" ]]; then service_password="$(gen_secret)" fi if [[ -z "${broker_db_password}" ]]; then broker_db_password="$(gen_secret)" fi if [[ -z "${oidc_client_secret}" ]]; then oidc_client_secret="$(gen_secret)" fi if [[ -z "${broker_internal_api_token}" ]]; then broker_internal_api_token="$(gen_secret)" fi compose_src="${repo_root}/deploy/docker-compose.host-hybrid.yml" compose_dst="${broker_dir}/docker-compose.yml" env_dst="${broker_dir}/.env" apache_site_path="${apache_site_dir%/}/${apache_site_name}" apache_preview_path="${broker_dir}/${apache_site_name}" notes_path="${broker_dir}/NEXT-STEPS.txt" compose_profiles_list=() if [[ "${external_auth_mode}" == "bundled" ]]; then compose_profiles_list+=("external-auth") fi if [[ "${qortal_mode}" == "bundled" ]]; then compose_profiles_list+=("qortal") fi compose_profiles="$(IFS=,; printf '%s' "${compose_profiles_list[*]}")" if [[ ! -f "${compose_src}" ]]; then echo "Missing compose source file: ${compose_src}" >&2 exit 1 fi if [[ "${external_auth_mode}" == "bundled" ]]; then if [[ ! -d "${external_auth_context}/.git" ]]; then if [[ "${external_auth_context_explicit}" == "1" ]]; then echo "External Auth context not found or is not a git repo: ${external_auth_context}" >&2 echo "Pass a valid --external-auth-context or omit it to auto-clone the default repo." >&2 exit 1 fi ensure_context_repo "Qortal External Auth" "${external_auth_context}" "${default_external_auth_repo_url}" else echo "Using existing Qortal External Auth repo at ${external_auth_context}" fi fi if [[ "${qortal_mode}" == "bundled" ]]; then if [[ ! -d "${qortal_node_context}/.git" ]]; then if [[ "${qortal_node_context_explicit}" == "1" ]]; then echo "Qortal node context not found or is not a git repo: ${qortal_node_context}" >&2 echo "Pass a valid --qortal-node-context or omit it to auto-clone the default repo." >&2 exit 1 fi ensure_context_repo "Qortal Core" "${qortal_node_context}" "${default_qortal_repo_url}" else echo "Using existing Qortal Core repo at ${qortal_node_context}" fi fi env_content=$(cat < ServerName ${broker_host} ProxyPreserveHost On ProxyPass / http://127.0.0.1:${broker_port}/ ProxyPassReverse / http://127.0.0.1:${broker_port}/ RequestHeader set X-Forwarded-Proto "https" ErrorLog \${APACHE_LOG_DIR}/qortalbroker_error.log CustomLog \${APACHE_LOG_DIR}/qortalbroker_access.log combined EOF ) notes_content=$(cat < "${env_dst}" fi if [[ "${dry_run}" == "1" ]]; then echo "[dry-run] write ${apache_preview_path}" else printf '%s\n' "${apache_content}" > "${apache_preview_path}" fi if [[ "${dry_run}" == "1" ]]; then echo "[dry-run] write ${notes_path}" else printf '%s\n' "${notes_content}" > "${notes_path}" fi if [[ "${external_auth_mode}" == "bundled" ]]; then echo "Bundled external_auth mode selected." echo "Start the sidecars first, then register an app against http://127.0.0.1:${external_auth_port}/apps/register." echo elif [[ -z "${external_auth_app_id}" || -z "${external_auth_app_secret}" ]]; then echo "Warning: External Auth app credentials are empty in generated .env." echo "Fill QORTAL_EXTERNAL_AUTH_APP_ID / QORTAL_EXTERNAL_AUTH_APP_SECRET before starting broker." echo fi echo "Bootstrap artifacts ready." echo " compose: ${compose_dst}" echo " env: ${env_dst}" echo " apache: ${apache_preview_path}" echo " notes: ${notes_path}"