#!/usr/bin/env bash set -Eeuo pipefail umask 077 SERVICE_USER=kaidi SERVICE_GROUP=kaidi CONFIG_ROOT=/etc/kaidi STATE_ROOT=/var/lib/kaidi UPDATE_STATE_ROOT=/var/lib/kaidi-update LOG_ROOT=/var/log/kaidi PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 RELEASE_API_URL=https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest INIT_ARMED=false INIT_COMMITTED=false INIT_RELEASE_ROOT= INIT_APP_ROOT= log() { printf '[kaidi-baota-init] %s\n' "$*"; } die() { printf '[kaidi-baota-init] ERROR: %s\n' "$*" >&2; exit 1; } rollback_initialization() { local rc=$? trap - EXIT HUP INT TERM if [ "$rc" -ne 0 ] && [ "$INIT_ARMED" = true ] && [ "$INIT_COMMITTED" != true ]; then systemctl disable --now kaidi-update.path >/dev/null 2>&1 || true systemctl stop kaidi-update.service >/dev/null 2>&1 || true rm -f /etc/systemd/system/kaidi-update.service /etc/systemd/system/kaidi-update.path systemctl daemon-reload >/dev/null 2>&1 || true rm -f "$CONFIG_ROOT/kaidi.env" "$CONFIG_ROOT/update.env" \ "$CONFIG_ROOT/release-public.pem" \ "$UPDATE_STATE_ROOT/status.json" /root/kaidi-first-login.txt rm -f "$INIT_APP_ROOT/current.next" if [ -n "$INIT_RELEASE_ROOT" ] \ && [ "$(readlink "$INIT_APP_ROOT/current" 2>/dev/null || true)" = "$INIT_RELEASE_ROOT" ]; then rm -f "$INIT_APP_ROOT/current" fi log "Initialization failed; files created by this attempt were rolled back and the command can be retried" fi exit "$rc" } trap rollback_initialization EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM sha256_file() { sha256sum "$1" | awk '{print $1}' } random_secret() { openssl rand -base64 36 | tr -d '\n/+=' | cut -c1-36 } write_env_file() { local output=$1 temporary name value escaped shift temporary="${output}.next.$$" : > "$temporary" while [ "$#" -gt 0 ]; do name=$1 value=$2 shift 2 case "$value" in *$'\n'*|*$'\r'*) die "$name contains a line break" ;; esac escaped=${value//\\/\\\\} escaped=${escaped//\"/\\\"} printf '%s="%s"\n' "$name" "$escaped" >> "$temporary" done mv -f "$temporary" "$output" } ensure_service_identity() { local existing_home nologin_path if ! getent group "$SERVICE_GROUP" >/dev/null 2>&1; then groupadd --system "$SERVICE_GROUP" fi if id "$SERVICE_USER" >/dev/null 2>&1; then existing_home=$(getent passwd "$SERVICE_USER" | awk -F: '{print $6}') [ "$existing_home" = "$STATE_ROOT" ] \ || die "Existing user $SERVICE_USER has unexpected home directory $existing_home" id -nG "$SERVICE_USER" | tr ' ' '\n' | grep -Fxq "$SERVICE_GROUP" \ || die "Existing user $SERVICE_USER is not a member of group $SERVICE_GROUP" return 0 fi nologin_path=$(command -v nologin 2>/dev/null || true) [ -n "$nologin_path" ] || nologin_path=/usr/sbin/nologin [ -x "$nologin_path" ] || die "A nologin shell is required" useradd --system --gid "$SERVICE_GROUP" --home-dir "$STATE_ROOT" --shell "$nologin_path" "$SERVICE_USER" } main() { local script_dir release_root releases_root app_root version app_port field_key setup_code setup_hash [ "$(id -u)" -eq 0 ] || die "Run with sudo or as root" [ "$(uname -s)" = Linux ] || die "Baota initialization only supports Linux" command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ] \ || die "systemd is required for the privileged background updater" for command in find openssl sha256sum; do command -v "$command" >/dev/null 2>&1 || die "$command is required" done script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P) release_root=$(cd "$script_dir/.." && pwd -P) releases_root=$(dirname "$release_root") app_root=$(dirname "$releases_root") [ "$(basename "$releases_root")" = releases ] \ || die "Extract the release into APP_ROOT/releases/VERSION before initialization" [ "$app_root" = /www/wwwroot/kaidi ] \ || die "The supported Baota project root is /www/wwwroot/kaidi" INIT_RELEASE_ROOT=$release_root INIT_APP_ROOT=$app_root version=$(tr -d '\r\n' < "$release_root/VERSION") [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.+-]+)?$ ]] \ && [ -s "$release_root/app.jar" ] && [ -s "$release_root/public/index.html" ] \ || die "The extracted release is incomplete" [ -x "$release_root/ops/update.sh" ] && [ -x "$release_root/ops/baota-start.sh" ] \ && [ -x "$release_root/ops/baota-init.sh" ] \ || die "The extracted operations scripts are incomplete" [ -s "$release_root/ops/release-public.pem" ] \ || die "The release public key is missing" [ "$(sha256_file "$release_root/ops/release-public.pem")" = "$PUBLIC_KEY_SHA256" ] \ || die "The release public-key fingerprint is invalid" [ ! -e "$CONFIG_ROOT/kaidi.env" ] && [ ! -e "$CONFIG_ROOT/update.env" ] \ && [ ! -e "$STATE_ROOT/setup/locked" ] \ || die "Kaidi is already initialized; run the published purge workflow before a fresh installation" ! systemctl cat kaidi-finance.service >/dev/null 2>&1 \ || die "The old kaidi-finance.service still exists; run the published purge workflow first" [ ! -e /etc/systemd/system/kaidi-update.service ] \ && [ ! -e /etc/systemd/system/kaidi-update.path ] \ || die "Old Kaidi update units still exist; run the published purge workflow first" [ ! -e "$app_root/current" ] && [ ! -L "$app_root/current" ] \ || die "The Baota current release link already exists; run the published purge workflow first" app_port=${1:-18080} [[ "$app_port" =~ ^[0-9]{1,5}$ ]] && [ "$app_port" -ge 1024 ] && [ "$app_port" -le 65535 ] \ || die "Port must be an integer between 1024 and 65535" INIT_ARMED=true ensure_service_identity install -d -o root -g "$SERVICE_GROUP" -m 0750 "$app_root" "$releases_root" install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 \ "$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" "$LOG_ROOT" install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0700 "$STATE_ROOT/setup" install -d -o root -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT" install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT/inbox" install -d -o root -g "$SERVICE_GROUP" -m 0750 "$CONFIG_ROOT" chown -R root:"$SERVICE_GROUP" "$release_root" find "$release_root" -type d -exec chmod 0750 {} + find "$release_root" -type f -exec chmod 0640 {} + chmod 0750 "$release_root/ops/update.sh" "$release_root/ops/baota-start.sh" "$release_root/ops/baota-init.sh" ln -sfn "$release_root" "$app_root/current.next" mv -Tf "$app_root/current.next" "$app_root/current" field_key=$(openssl rand -base64 32 | tr -d '\n') setup_code="KD-$(random_secret)" setup_hash=$(printf '%s' "$setup_code" | sha256sum | awk '{print $1}') # The Baota process manager does not own a restartable application unit. # Keep its updater files for diagnostics, but do not expose online update # actions until the panel lifecycle is integrated with the transaction state machine. # The setup context has no datasource. Empty values avoid Baota treating # a development placeholder as a real local MySQL dependency. write_env_file "$CONFIG_ROOT/kaidi.env" \ SPRING_PROFILES_ACTIVE production \ SERVER_ADDRESS 127.0.0.1 \ SERVER_PORT "$app_port" \ SESSION_COOKIE_SECURE false \ DB_URL '' \ DB_USERNAME '' \ DB_PASSWORD '' \ FIELD_ENCRYPTION_KEY "$field_key" \ FILE_STORAGE_ROOT "$STATE_ROOT/files" \ FILE_STORAGE_TEMP "$STATE_ROOT/tmp" \ FILE_SCANNER_ENABLED false \ FINANCE_BOOTSTRAP_ENABLED false \ FINANCE_BOOTSTRAP_PASSWORD '' \ FINANCE_SETUP_ENABLED true \ FINANCE_SETUP_TOKEN_SHA256 "$setup_hash" \ FINANCE_SETUP_ENV_FILE "$STATE_ROOT/setup/application.env" \ FINANCE_SETUP_MARKER_FILE "$STATE_ROOT/setup/locked" \ FINANCE_SETUP_RESTART_AFTER_COMPLETE true \ FINANCE_UPDATE_ENABLED false \ UPDATE_RELEASE_BASE_URL '' \ UPDATE_RELEASE_API_URL "$RELEASE_API_URL" \ UPDATE_RELEASE_TOKEN '' \ UPDATE_REQUEST_FILE "$UPDATE_STATE_ROOT/inbox/request.json" \ UPDATE_STATUS_FILE "$UPDATE_STATE_ROOT/status.json" \ KAIDI_PID_FILE "$STATE_ROOT/kaidi.pid" chown root:"$SERVICE_GROUP" "$CONFIG_ROOT/kaidi.env" chmod 0640 "$CONFIG_ROOT/kaidi.env" # Baota owns the Java process in this mode. Do not install a second # systemd updater; online update is intentionally systemd-only until the # panel lifecycle can participate in the transaction state machine. install -m 0644 "$release_root/ops/release-public.pem" "$CONFIG_ROOT/release-public.pem" printf '{"state":"CURRENT","message":"Baota release is prepared","targetVersion":"%s","updatedAt":"%s"}\n' \ "$version" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$UPDATE_STATE_ROOT/status.json" chmod 0644 "$UPDATE_STATE_ROOT/status.json" cat > /root/kaidi-first-login.txt <