Files

470 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
echo "Run this installer as root (sudo)."
exit 1
fi
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "${script_dir}/.." && pwd)"
default_nc_path="/var/www/nextcloud"
read -r -p "Nextcloud path [${default_nc_path}]: " nc_path
nc_path="${nc_path:-$default_nc_path}"
if [[ ! -f "${nc_path}/occ" ]]; then
echo "Could not find occ at ${nc_path}/occ. Please check the Nextcloud path."
exit 1
fi
occ() {
if command -v sudo >/dev/null 2>&1; then
sudo -u www-data php "${nc_path}/occ" "$@"
else
runuser -u www-data -- php "${nc_path}/occ" "$@"
fi
}
occ_app_get() {
local app_id="${1:-}"
local key="${2:-}"
local value=""
if [[ -z "${app_id}" || -z "${key}" ]]; then
return 1
fi
value="$(occ config:app:get "${app_id}" "${key}" 2>/dev/null | tr -d '\r' || true)"
if [[ "${value}" == "false" ]]; then
value=""
fi
echo "${value}"
}
normalize_bool_01() {
local raw="${1:-}"
case "$(echo "${raw}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" in
1|true|yes|y|on)
echo "1"
;;
0|false|no|n|off)
echo "0"
;;
*)
echo ""
;;
esac
}
gen_secret() {
tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 32
}
detect_nc_url() {
local url
set +e
url="$(occ config:system:get overwrite.cli.url 2>/dev/null)"
if [[ -z "$url" ]]; then
url="$(occ config:system:get trusted_domains 0 2>/dev/null)"
if [[ -n "$url" && "$url" != http* ]]; then
url="https://${url}"
fi
fi
set -e
echo "$url"
}
nc_url="$(detect_nc_url)"
read -r -p "Nextcloud URL [${nc_url:-https://cloud.example.com}]: " nc_url_input
nc_url="${nc_url_input:-$nc_url}"
if [[ -z "$nc_url" ]]; then
echo "Nextcloud URL is required."
exit 1
fi
nc_host="${nc_url#*://}"
nc_host="${nc_host%%/*}"
base_domain="$nc_host"
if [[ "$nc_host" == *.* ]]; then
base_domain="${nc_host#*.}"
fi
default_broker_host="qortalbroker.${base_domain}"
read -r -p "Broker hostname [${default_broker_host}]: " broker_host
broker_host="${broker_host:-$default_broker_host}"
read -r -p "Broker external URL [https://${broker_host}]: " broker_url
broker_url="${broker_url:-https://${broker_host}}"
read -r -p "Nextcloud service user [nuqloud_oidc_service]: " nc_service_user
nc_service_user="${nc_service_user:-nuqloud_oidc_service}"
default_nc_service_password="$(gen_secret)"
read -r -p "Nextcloud service password [${default_nc_service_password}]: " nc_service_password
nc_service_password="${nc_service_password:-$default_nc_service_password}"
read -r -p "External Auth base URL [http://127.0.0.1:3191]: " ext_auth_url
ext_auth_url="${ext_auth_url:-http://127.0.0.1:3191}"
read -r -p "External Auth app ID: " ext_auth_app_id
read -r -p "External Auth app secret: " ext_auth_app_secret
if [[ ! "${ext_auth_url}" =~ ^https?://(127\.0\.0\.1|localhost) ]]; then
echo "Warning: External Auth is not local. Ensure secondary wallet encryption is enabled."
fi
default_db_pass="$(gen_secret)"
read -r -p "Broker DB password [${default_db_pass}]: " broker_db_password
broker_db_password="${broker_db_password:-$default_db_pass}"
default_oidc_secret="$(gen_secret)"
read -r -p "OIDC client secret [${default_oidc_secret}]: " oidc_secret
oidc_secret="${oidc_secret:-$default_oidc_secret}"
default_broker_internal_token="$(gen_secret)"
read -r -p "Broker internal API token [${default_broker_internal_token}]: " broker_internal_token
broker_internal_token="${broker_internal_token:-$default_broker_internal_token}"
libresign_default_cn() {
if [[ -n "${LIBRESIGN_CERT_CN:-}" ]]; then
echo "${LIBRESIGN_CERT_CN}"
return 0
fi
if [[ -n "${nc_host:-}" ]]; then
echo "NuQloud ${nc_host} Root CA"
return 0
fi
echo "NuQloud Root CA"
}
configure_libresign_vm() {
local auto_setup="1"
local auto_install="1"
local java_path=""
local jsignpdf_path=""
local pdftk_path=""
local root_cert=""
local engine=""
local cn=""
local org=""
local country=""
local state=""
local locality=""
local ou_raw=""
local status_local=0
local occ_args=()
local ou_value=""
auto_setup="$(normalize_bool_01 "${LIBRESIGN_AUTO_SETUP:-1}")"
if [[ -z "${auto_setup}" ]]; then
auto_setup="1"
fi
auto_install="$(normalize_bool_01 "${LIBRESIGN_AUTO_INSTALL_BINARIES:-1}")"
if [[ -z "${auto_install}" ]]; then
auto_install="1"
fi
java_path="$(occ_app_get "libresign" "java_path")"
jsignpdf_path="$(occ_app_get "libresign" "jsignpdf_jar_path")"
pdftk_path="$(occ_app_get "libresign" "pdftk_path")"
root_cert="$(occ_app_get "libresign" "rootCert")"
if [[ "${auto_install}" == "1" && -z "${java_path}" ]]; then
occ libresign:install --java >/dev/null 2>&1 || status_local=1
java_path="$(occ_app_get "libresign" "java_path")"
fi
if [[ "${auto_install}" == "1" && -z "${jsignpdf_path}" ]]; then
occ libresign:install --jsignpdf >/dev/null 2>&1 || status_local=1
jsignpdf_path="$(occ_app_get "libresign" "jsignpdf_jar_path")"
fi
if [[ "${auto_install}" == "1" && -z "${pdftk_path}" ]]; then
occ libresign:install --pdftk >/dev/null 2>&1 || status_local=1
pdftk_path="$(occ_app_get "libresign" "pdftk_path")"
fi
if [[ "${auto_setup}" == "1" && -z "${root_cert}" ]]; then
engine="$(echo "${LIBRESIGN_CERT_ENGINE:-openssl}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
if [[ "${engine}" != "cfssl" ]]; then
engine="openssl"
fi
cn="$(libresign_default_cn)"
org="${LIBRESIGN_CERT_O:-${NUQLOUD_BILLING_COMPANY_NAME:-NuQloud}}"
country="${LIBRESIGN_CERT_C:-}"
state="${LIBRESIGN_CERT_ST:-}"
locality="${LIBRESIGN_CERT_L:-}"
ou_raw="${LIBRESIGN_CERT_OU:-Document Signing}"
occ_args=("libresign:configure:${engine}" "--cn=${cn}")
if [[ -n "${org}" ]]; then
occ_args+=("--o=${org}")
fi
if [[ -n "${country}" ]]; then
occ_args+=("--c=${country}")
fi
if [[ -n "${state}" ]]; then
occ_args+=("--st=${state}")
fi
if [[ -n "${locality}" ]]; then
occ_args+=("--l=${locality}")
fi
IFS=',;' read -r -a ou_values <<< "${ou_raw}"
for ou_value in "${ou_values[@]}"; do
ou_value="$(echo "${ou_value}" | xargs)"
if [[ -n "${ou_value}" ]]; then
occ_args+=("--ou=${ou_value}")
fi
done
occ "${occ_args[@]}" >/dev/null 2>&1 || status_local=1
root_cert="$(occ_app_get "libresign" "rootCert")"
fi
if [[ "${auto_install}" == "1" ]]; then
if [[ -z "${java_path}" || -z "${jsignpdf_path}" || -z "${pdftk_path}" ]]; then
status_local=1
fi
fi
if [[ "${auto_setup}" == "1" && -z "${root_cert}" ]]; then
status_local=1
fi
return "${status_local}"
}
broker_dir="/opt/qortal-broker"
mkdir -p "${broker_dir}"
compose_src="${repo_root}/deploy/docker-compose.broker.yml"
compose_dst="${broker_dir}/docker-compose.yml"
cp "${compose_src}" "${compose_dst}"
cat > "${broker_dir}/.env" <<EOF
BROKER_BUILD_CONTEXT=${repo_root}/services/qortal-oidc-broker
BROKER_PORT=3000
BROKER_DB_NAME=qortal_broker
BROKER_DB_USER=qortal_broker
BROKER_DB_PASSWORD=${broker_db_password}
BROKER_DATABASE_URL=postgresql://qortal_broker:${broker_db_password}@broker_db:5432/qortal_broker
NEXTCLOUD_BASE_URL=${nc_url}
NEXTCLOUD_PUBLIC_URL=${nc_url}
NEXTCLOUD_SERVICE_USER=${nc_service_user}
NEXTCLOUD_SERVICE_PASSWORD=${nc_service_password}
QORTAL_EXTERNAL_AUTH_BASE_URL=${ext_auth_url}
QORTAL_EXTERNAL_AUTH_APP_ID=${ext_auth_app_id}
QORTAL_EXTERNAL_AUTH_APP_SECRET=${ext_auth_app_secret}
BROKER_INTERNAL_API_TOKEN=${broker_internal_token}
# Optional comma-separated browser origins for CORS.
BROKER_CORS_ALLOWED_ORIGINS=${nc_url}
OIDC_ISSUER=${broker_url}
OIDC_CLIENT_ID=nextcloud-local
OIDC_CLIENT_SECRET=${oidc_secret}
OIDC_REDIRECT_URI_ALLOWLIST=${nc_url}/apps/user_oidc/code
OIDC_POLICY_MODE=auto_provision
OIDC_AUTO_PROVISION_GUARD=invite_or_allowlist
OIDC_REQUIRE_EMAIL_FOR_NEW_ACCOUNT=true
OIDC_INVITE_TTL_SECONDS=604800
OIDC_AUTH_REQUEST_TTL_SECONDS=600
OIDC_AUTH_CODE_TTL_SECONDS=120
OIDC_ACCESS_TOKEN_TTL_SECONDS=600
OIDC_ID_TOKEN_TTL_SECONDS=600
EOF
echo "Copying Nextcloud app..."
if [[ -x "${repo_root}/scripts/ensure-user-oidc-app.sh" ]]; then
"${repo_root}/scripts/ensure-user-oidc-app.sh"
fi
install_custom_pwa="${INSTALL_CUSTOM_PWA:-1}"
install_custom_pwa="$(echo "${install_custom_pwa}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
custom_pwa_enabled="1"
case "${install_custom_pwa}" in
0|false|no|n|off)
custom_pwa_enabled="0"
;;
esac
mkdir -p "${nc_path}/custom_apps"
app_dirs=(
"qortal_integration"
"chd_admin"
"qortal_files_bridge"
"qortal_talk_bridge"
"nuqloud_scrum"
)
if [[ "${custom_pwa_enabled}" == "1" ]]; then
app_dirs=("qortal_integration" "custom_pwa" "chd_admin" "qortal_files_bridge" "qortal_talk_bridge" "nuqloud_scrum")
fi
if command -v rsync >/dev/null 2>&1; then
for app_name in "${app_dirs[@]}"; do
if [[ -d "${repo_root}/nextcloud/custom_apps/${app_name}" ]]; then
rsync -a "${repo_root}/nextcloud/custom_apps/${app_name}/" "${nc_path}/custom_apps/${app_name}/"
fi
done
if [[ -d "${repo_root}/nextcloud/custom_apps/user_oidc" ]]; then
rsync -a "${repo_root}/nextcloud/custom_apps/user_oidc/" "${nc_path}/custom_apps/user_oidc/"
fi
else
for app_name in "${app_dirs[@]}"; do
if [[ -d "${repo_root}/nextcloud/custom_apps/${app_name}" ]]; then
rm -rf "${nc_path}/custom_apps/${app_name}"
cp -R "${repo_root}/nextcloud/custom_apps/${app_name}" "${nc_path}/custom_apps/${app_name}"
fi
done
if [[ -d "${repo_root}/nextcloud/custom_apps/user_oidc" ]]; then
rm -rf "${nc_path}/custom_apps/user_oidc"
cp -R "${repo_root}/nextcloud/custom_apps/user_oidc" "${nc_path}/custom_apps/user_oidc"
fi
fi
echo "Enabling Nextcloud apps..."
set +e
install_libresign="$(normalize_bool_01 "${INSTALL_LIBRESIGN:-1}")"
if [[ -z "${install_libresign}" ]]; then
install_libresign="1"
fi
enable_list=(chd_admin qortal_files_bridge qortal_talk_bridge nuqloud_scrum)
if [[ "${custom_pwa_enabled}" == "1" ]]; then
enable_list=(custom_pwa "${enable_list[@]}")
fi
for app_name in "${enable_list[@]}"; do
occ app:enable "${app_name}" >/dev/null 2>&1
done
occ app:enable --force user_oidc >/dev/null 2>&1
enable_oidc_status=$?
install_oidc_status=0
if [[ "${enable_oidc_status}" -ne 0 ]]; then
occ app:install user_oidc >/dev/null 2>&1
install_oidc_status=$?
occ app:enable --force user_oidc >/dev/null 2>&1
enable_oidc_status=$?
fi
occ app:enable qortal_integration >/dev/null 2>&1
enable_qortal_status=$?
occ user_oidc:provider --help >/dev/null 2>&1
verify_oidc_status=$?
libresign_enable_status=0
libresign_install_status=0
libresign_setup_status=0
if [[ "${install_libresign}" == "1" ]]; then
occ app:enable --force libresign >/dev/null 2>&1
libresign_enable_status=$?
if [[ "${libresign_enable_status}" -ne 0 ]]; then
occ app:install libresign >/dev/null 2>&1
libresign_install_status=$?
occ app:enable --force libresign >/dev/null 2>&1
libresign_enable_status=$?
fi
if [[ "${libresign_enable_status}" -eq 0 ]]; then
configure_libresign_vm
libresign_setup_status=$?
fi
fi
occ config:app:set qortal_integration broker_internal_api_token --value="${broker_internal_token}" >/dev/null 2>&1
libresign_failure=0
if [[ "${install_libresign}" == "1" && ( "${libresign_enable_status}" -ne 0 || "${libresign_setup_status}" -ne 0 ) ]]; then
libresign_failure=1
fi
set -e
if [[ "${enable_qortal_status}" -ne 0 || "${enable_oidc_status}" -ne 0 || "${verify_oidc_status}" -ne 0 || "${libresign_failure}" -ne 0 ]]; then
echo "Warning: one or more app enable checks failed."
echo "Run manually:"
echo " sudo -u www-data php ${nc_path}/occ app:enable qortal_integration"
echo " sudo -u www-data php ${nc_path}/occ app:install user_oidc"
echo " sudo -u www-data php ${nc_path}/occ app:enable --force user_oidc"
echo " sudo -u www-data php ${nc_path}/occ user_oidc:provider --help"
if [[ "${install_libresign}" == "1" ]]; then
echo " sudo -u www-data php ${nc_path}/occ app:install libresign"
echo " sudo -u www-data php ${nc_path}/occ app:enable --force libresign"
echo " sudo -u www-data php ${nc_path}/occ libresign:install --all"
echo " sudo -u www-data php ${nc_path}/occ libresign:configure:openssl --cn=\"$(libresign_default_cn)\" --o=\"${LIBRESIGN_CERT_O:-${NUQLOUD_BILLING_COMPANY_NAME:-NuQloud}}\" --ou=\"${LIBRESIGN_CERT_OU:-Document Signing}\""
fi
if [[ "${install_oidc_status}" -ne 0 && "${enable_oidc_status}" -ne 0 ]]; then
echo " Note: app:install user_oidc fallback failed; check appstore connectivity or custom_apps path."
fi
if [[ "${install_libresign}" == "1" && "${libresign_install_status}" -ne 0 && "${libresign_enable_status}" -ne 0 ]]; then
echo " Note: app:install libresign fallback failed; check appstore connectivity."
fi
fi
echo "Configuring user_oidc provider..."
set +e
occ user_oidc:provider qortal \
-c nextcloud-local \
-s "${oidc_secret}" \
-d "${broker_url}/.well-known/openid-configuration" \
--scope="openid profile email" \
--mapping-uid=sub \
--mapping-display-name=name \
--mapping-email=email
provider_config_status=$?
oidc_login_label_status=0
sovereign_mode="$(occ config:app:get qortal_integration sc_mode 2>/dev/null | tr -d '\r' || true)"
if [[ "${sovereign_mode}" == "full_qortal" ]]; then
occ config:system:delete user_oidc login_label >/dev/null 2>&1
oidc_login_label_status=$?
else
occ config:system:set user_oidc login_label --value="Create Account / Login" >/dev/null 2>&1
oidc_login_label_status=$?
fi
set -e
if [[ "${provider_config_status}" -ne 0 ]]; then
echo "Warning: failed to configure user_oidc provider automatically."
echo "Run manually:"
echo " sudo -u www-data php ${nc_path}/occ user_oidc:provider qortal -c nextcloud-local -s '<client-secret>' -d ${broker_url}/.well-known/openid-configuration --scope='openid profile email' --mapping-uid=sub --mapping-display-name=name --mapping-email=email"
fi
if [[ "${oidc_login_label_status}" -ne 0 ]]; then
echo "Warning: failed to set user_oidc login label."
echo "Run manually:"
echo " sudo -u www-data php ${nc_path}/occ config:system:set user_oidc login_label --value='Create Account / Login'"
fi
if ! command -v docker >/dev/null 2>&1; then
echo "Docker is required but not installed. Install Docker and re-run docker compose manually."
exit 1
fi
if docker compose version >/dev/null 2>&1; then
docker_compose="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
docker_compose="docker-compose"
else
echo "Docker Compose not found."
exit 1
fi
echo "Starting broker containers..."
${docker_compose} -f "${compose_dst}" --env-file "${broker_dir}/.env" up -d
echo "Configuring Apache reverse proxy..."
if ! a2enmod proxy proxy_http headers ssl >/dev/null 2>&1; then
echo "Warning: failed to enable Apache modules (proxy/proxy_http/headers/ssl)."
fi
apache_site="/etc/apache2/sites-available/qortalbroker.conf"
cat > "${apache_site}" <<EOF
<VirtualHost *:80>
ServerName ${broker_host}
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
RequestHeader set X-Forwarded-Proto "https"
ErrorLog \${APACHE_LOG_DIR}/qortalbroker_error.log
CustomLog \${APACHE_LOG_DIR}/qortalbroker_access.log combined
</VirtualHost>
EOF
a2ensite qortalbroker.conf >/dev/null 2>&1 || true
systemctl reload apache2 || true
read -r -p "Run certbot for ${broker_host}? (y/N): " run_certbot
if [[ "${run_certbot}" == "y" || "${run_certbot}" == "Y" ]]; then
read -r -p "Let's Encrypt email: " le_email
if command -v certbot >/dev/null 2>&1; then
certbot --apache -d "${broker_host}" -m "${le_email}" --agree-tos --redirect --non-interactive
else
echo "certbot is not installed. Install it and rerun manually."
fi
fi
echo "Install complete."
echo "Broker URL: ${broker_url}"
echo "Nextcloud URL: ${nc_url}"
echo "OIDC provider configured with client_id=nextcloud-local"
echo "If login fails, verify DNS for ${broker_host} and Apache proxy."