#!/bin/bash # Next Workspace (NextWks) - Bare-Metal Installer # Deploys compiled binaries to /opt/nextwks/ for production use # # Usage: # ./install.sh Build and install # ./install.sh --skip-build Install existing binaries only # ./install.sh --build-only Compile binary only (no install) # ./install.sh --config-only Generate config only # ./install.sh --status Check installation health # ./install.sh --uninstall Remove installation # ./install.sh --help Show this help set -euo pipefail # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' error() { echo -e "${RED}Error:${NC} $1" >&2; } success() { echo -e "${GREEN}$1${NC}"; } info() { echo -e "${BLUE}$1${NC}"; } warn() { echo -e "${YELLOW}Warning:${NC} $1"; } # Configuration REPO_DIR="$(cd "$(dirname "$0")" && pwd)" INSTALL_DIR="/opt/nextwks" BIN_DIR="${INSTALL_DIR}/bin" DATA_DIR="${INSTALL_DIR}/data" MODULES_DIR="${INSTALL_DIR}/modules" STATIC_DIR="${INSTALL_DIR}/static" CONFIG_FILE="${INSTALL_DIR}/config.yaml" SERVICE_FILE="/etc/systemd/system/nextwks.service" HEALTH_URL="http://localhost:8080/api/health" # Parse arguments SKIP_BUILD=false BUILD_ONLY=false CONFIG_ONLY=false UNINSTALL=false CHECK_STATUS=false for arg in "$@"; do case "$arg" in --skip-build) SKIP_BUILD=true ;; --build-only) BUILD_ONLY=true ;; --config-only) CONFIG_ONLY=true ;; --uninstall) UNINSTALL=true ;; --status) CHECK_STATUS=true ;; --help) head -11 "$0" | sed 's/^# //; 1s/.*/NextWks Installer/' | sed 's/^$/ /' exit 0 ;; *) error "Unknown argument: $arg (use --help for options)"; exit 1 ;; esac done # --- Status Check --- if [ "$CHECK_STATUS" = true ]; then echo "" info "Next Workspace Installation Status" echo "-----------------------------------" if [ -f "${INSTALL_DIR}/bin/core" ]; then VERSION=$("${INSTALL_DIR}/bin/core" 2>&1 | head -1 || echo "unknown") success "✓ Binary installed: ${INSTALL_DIR}/bin/core" else warn "✗ Binary not found: ${INSTALL_DIR}/bin/core (not installed)" fi if [ -f "$CONFIG_FILE" ]; then success "✓ Configuration: $CONFIG_FILE" else warn "✗ Configuration: not found" fi if systemctl is-active --quiet nextwks 2>/dev/null; then success "✓ Service running: nextwks" elif systemctl is-enabled --quiet nextwks 2>/dev/null; then warn "⚠ Service nextwks: enabled but not running" else info " Service nextwks: not active" fi if [ -f "/opt/authelia/authelia" ]; then success "✓ Authelia found: /opt/authelia/" if systemctl is-active --quiet authelia 2>/dev/null; then success " Authelia service: running (port 9091)" fi else warn "✗ Authelia: not installed" fi # Try health check if server appears to be running if command -v curl &>/dev/null; then HEALTH=$(curl -s --max-time 2 "$HEALTH_URL" 2>/dev/null || echo "") if [ "$HEALTH" = '{"status":"ok"}' ]; then success "✓ Health endpoint: OK (http://localhost:8080)" elif [ -n "$HEALTH" ]; then warn "⚠ Health endpoint: unexpected response: $HEALTH" else info " Health endpoint: not responding" fi fi echo "" exit 0 fi # --- Uninstall --- if [ "$UNINSTALL" = true ]; then info "Uninstalling Next Workspace..." if [ -f "$SERVICE_FILE" ]; then systemctl stop nextwks 2>/dev/null || true systemctl disable nextwks 2>/dev/null || true rm -f "$SERVICE_FILE" systemctl daemon-reload info "Removed systemd service" fi if [ -d "$INSTALL_DIR" ]; then rm -rf "$INSTALL_DIR" success "Removed $INSTALL_DIR" else info "No installation found at $INSTALL_DIR" fi success "Uninstall complete" exit 0 fi # --- Prerequisites --- info "Checking prerequisites..." if [ "$SKIP_BUILD" = false ] && [ "$CONFIG_ONLY" = false ] && [ "$BUILD_ONLY" = false ]; then if ! command -v go &>/dev/null; then error "Go is not installed. Install Go 1.22+ first." exit 1 fi fi # Check Authelia if [ ! -f "/opt/authelia/authelia" ]; then warn "Authelia is not installed at /opt/authelia/" warn "Admin user management requires Authelia to function." warn "Install with: bash $REPO_DIR/scripts/install-authelia.sh" echo "" fi # --- Static Assets --- if [ "$CONFIG_ONLY" = false ] && [ "$BUILD_ONLY" = false ]; then info "Installing static assets..." if [ -d "$REPO_DIR/app/static" ]; then mkdir -p "$STATIC_DIR" cp -r "$REPO_DIR/app/static"/* "$STATIC_DIR/" success "Static assets installed to $STATIC_DIR" fi fi # --- Build --- if [ "$SKIP_BUILD" = false ] && [ "$CONFIG_ONLY" = false ]; then info "Building Next Workspace core..." cd "$REPO_DIR/src" # Run tests first info "Running tests..." if ! go test -count=1 ./... 2>&1 | tail -1; then warn "Some tests failed — continuing build anyway" fi # Build with stripped symbols for smaller binary go build -o "$REPO_DIR/app/core" -ldflags="-s -w" . success "Core binary built: app/core" if [ "$BUILD_ONLY" = true ]; then success "Build complete (--build-only, skipping install)" exit 0 fi fi # --- Install --- if [ "$CONFIG_ONLY" = false ]; then info "Installing to $INSTALL_DIR..." # Create directory structure mkdir -p "$BIN_DIR" "$DATA_DIR" "$MODULES_DIR" "$STATIC_DIR" # Copy binary if [ -f "$REPO_DIR/app/core" ]; then cp "$REPO_DIR/app/core" "$BIN_DIR/core" chmod 755 "$BIN_DIR/core" success "Installed binary to $BIN_DIR/core" else error "Binary not found at app/core. Run without --skip-build or build manually." exit 1 fi # Copy static assets if not already done if [ -d "$REPO_DIR/app/static" ] && [ ! -f "$STATIC_DIR/manifest.json" ]; then cp -r "$REPO_DIR/app/static"/* "$STATIC_DIR/" fi # Copy modules if any exist if [ -d "$REPO_DIR/app/modules" ] && [ "$(ls -A "$REPO_DIR/app/modules" 2>/dev/null)" ]; then cp -r "$REPO_DIR/app/modules"/* "$MODULES_DIR/" success "Installed modules" fi fi # --- Configuration --- info "Generating configuration..." if [ ! -f "$CONFIG_FILE" ]; then ADMIN_SECRET=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | xxd -p -c 32) SESSION_SECRET=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | xxd -p -c 32) cat > "$CONFIG_FILE" << CONFIGEOF # Next Workspace (NextWks) - Production Configuration # Auto-generated by install.sh on $(date) server: host: "0.0.0.0" port: 8080 admin: secret_token: "${ADMIN_SECRET}" database: type: "sqlite" path: "${DATA_DIR}/nextwks.db" authelia: host: "http://127.0.0.1:9091" config_path: "/opt/authelia/configuration.yml" users_db_path: "/opt/authelia/users_database.yml" oidc: client_id: "nextwks" client_secret: "" redirect_url: "https://wks.lohmar.co.uk/auth/callback" domain: "wks.lohmar.co.uk" smtp: host: "" port: 587 username: "" password: "" from: "noreply@nextwks.local" session: secret: "${SESSION_SECRET}" expiry_minutes: 60 CONFIGEOF chmod 600 "$CONFIG_FILE" success "Configuration generated: $CONFIG_FILE" echo "" warn " Admin Secret Token: ${ADMIN_SECRET}" warn " Store this securely! Required for admin API access." echo "" else info "Configuration already exists at $CONFIG_FILE (not overwritten)" fi # --- Systemd Service --- if [ "$CONFIG_ONLY" = false ]; then info "Setting up systemd service..." cat > "$SERVICE_FILE" << SERVICEEOF [Unit] Description=Next Workspace (NextWks) Core After=network.target authelia.service Wants=authelia.service [Service] Type=simple User=root WorkingDirectory=${INSTALL_DIR} ExecStart=${BIN_DIR}/core -config ${CONFIG_FILE} Restart=always RestartSec=5 StandardOutput=journal StandardError=journal # Security hardening NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=${DATA_DIR} ${MODULES_DIR} ReadOnlyPaths=${INSTALL_DIR}/config.yaml ${INSTALL_DIR}/static [Install] WantedBy=multi-user.target SERVICEEOF systemctl daemon-reload success "Systemd service created: $SERVICE_FILE" fi # --- Smoke Test --- if [ "$CONFIG_ONLY" = false ]; then info "Starting smoke test..." # Start the server temporarily to verify it works if [ -f "$BIN_DIR/core" ] && [ -f "$CONFIG_FILE" ]; then "$BIN_DIR/core" -config "$CONFIG_FILE" & SMOKE_PID=$! sleep 2 if command -v curl &>/dev/null; then RESPONSE=$(curl -s --max-time 3 "$HEALTH_URL" 2>/dev/null || echo "") if [ "$RESPONSE" = '{"status":"ok"}' ]; then success "Smoke test passed — server responds OK" else warn "Smoke test: server started but health check returned '$RESPONSE'" fi fi kill $SMOKE_PID 2>/dev/null || true wait $SMOKE_PID 2>/dev/null || true fi fi # --- Summary --- echo "" success "═══════════════════════════════════════════" success " Next Workspace (NextWks) installed!" success "═══════════════════════════════════════════" echo "" info " Binary: ${BIN_DIR}/core" info " Config: ${CONFIG_FILE}" info " Data: ${DATA_DIR}/" info " Static assets: ${STATIC_DIR}/" info " Service: systemctl start nextwks" echo "" info " Workspace: https://wks.lohmar.co.uk/" info " Admin UI: http://localhost:8080/admin" info " Health API: http://localhost:8080/api/health" info " Auth status: http://localhost:8080/auth/status" echo "" info " Start: systemctl enable --now nextwks" info " Logs: journalctl -u nextwks -f" info " Status: ./install.sh --status" echo "" warn " Next step: Register NextWks as OIDC client in Authelia:" warn " • Edit /opt/authelia/configuration.yml" warn " • Add client_id: nextwks with redirect_uri: https://wks.lohmar.co.uk/auth/callback" warn " • Restart: systemctl restart authelia" echo ""