#!/bin/bash # ============================================================ # Next Workspace (NextWks) — Installer # # curl -fsSL https://git.lohmar.co.uk/lexton-it/NextWks/raw/main/install.sh | bash # # Usage: # ./install.sh Full interactive install # ./install.sh --from-env Load answers from .env # ./install.sh --status Check installation health # ./install.sh --uninstall Remove everything # ============================================================ set -euo pipefail # Colors RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; 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}$1${NC}"; } header() { echo -e "\n${BOLD}${CYAN}$1${NC}"; } # ============================================================ # PATHS # ============================================================ REPO_DIR="$(cd "$(dirname "$0")" && pwd)" INSTALL_DIR="/opt/nextwks" BIN_DIR="${INSTALL_DIR}/bin" DATA_DIR="${INSTALL_DIR}/data" STATIC_DIR="${INSTALL_DIR}/static" CONFIG_FILE="${INSTALL_DIR}/config.yaml" ENV_FILE="${REPO_DIR}/.env" AUTHELIA_DIR="/opt/authelia" AUTHELIA_CONFIG="${AUTHELIA_DIR}/configuration.yml" SERVICE_FILE="/etc/systemd/system/nextwks.service" # ============================================================ # DEFAULTS # ============================================================ SMTP_HOST_DEFAULT="smtp.openxchange.eu" SMTP_PORT_DEFAULT="587" SMTP_USER_DEFAULT="post@2-4-h.app" IMAP_HOST_DEFAULT="imap.openxchange.eu" IMAP_PORT_DEFAULT="993" NEXTWKS_URL_DEFAULT="https://wks.lohmar.co.uk" AUTH_URL_DEFAULT="https://auth.lohmar.co.uk" PROXY_IP_DEFAULT="172.16.0.10" # ============================================================ # PARSE FLAGS # ============================================================ MODE="install" FROM_ENV=false for arg in "$@"; do case "$arg" in --from-env) FROM_ENV=true ;; --status) MODE="status" ;; --uninstall) MODE="uninstall" ;; --help) head -20 "$0" | grep "^#" | sed 's/^# //; 1s/.*/NextWks Installer v2026.6.0001/' exit 0 ;; *) error "Unknown: $arg (use --help)"; exit 1 ;; esac done # ============================================================ # UNINSTALL # ============================================================ if [ "$MODE" = "uninstall" ]; then info "Uninstalling Next Workspace..." systemctl stop nextwks 2>/dev/null || true systemctl disable nextwks 2>/dev/null || true rm -f "$SERVICE_FILE" systemctl daemon-reload [ -d "$INSTALL_DIR" ] && { rm -rf "$INSTALL_DIR"; success "Removed $INSTALL_DIR"; } success "Uninstall complete" exit 0 fi # ============================================================ # STATUS # ============================================================ if [ "$MODE" = "status" ]; then echo ""; header "Next Workspace Status" [ -f "${BIN_DIR}/core" ] && success "✓ Binary: ${BIN_DIR}/core" || warn "✗ Binary: not found" [ -f "$CONFIG_FILE" ] && success "✓ Config: $CONFIG_FILE" || warn "✗ Config: not found" systemctl is-active --quiet nextwks 2>/dev/null && success "✓ Service: running" || info " Service: stopped" systemctl is-active --quiet authelia 2>/dev/null && success "✓ Authelia: running" || info " Authelia: stopped" command -v curl &>/dev/null && [ "$(curl -s --max-time 2 http://localhost:8080/api/health 2>/dev/null)" = '{"status":"ok"}' ] && success "✓ API: OK" || info " API: not responding" echo ""; exit 0 fi # ============================================================ # INTERACTIVE WIZARD # ============================================================ gather_inputs() { # Load from .env if --from-env if [ "$FROM_ENV" = true ] && [ -f "$ENV_FILE" ]; then source "$ENV_FILE" success "Loaded configuration from $ENV_FILE" return fi echo "" header "┌─────────────────────────────────────────┐" header "│ Next Workspace — Setup Wizard │" header "└─────────────────────────────────────────┘" info "Press Enter to accept defaults shown in [brackets]" echo "" # --- Email --- header "── Email Configuration ──" read -p " SMTP Host [$SMTP_HOST_DEFAULT]: " SMTP_HOST SMTP_HOST="${SMTP_HOST:-$SMTP_HOST_DEFAULT}" read -p " SMTP Port [$SMTP_PORT_DEFAULT]: " SMTP_PORT SMTP_PORT="${SMTP_PORT:-$SMTP_PORT_DEFAULT}" read -p " IMAP Host [$IMAP_HOST_DEFAULT]: " IMAP_HOST IMAP_HOST="${IMAP_HOST:-$IMAP_HOST_DEFAULT}" read -p " IMAP Port [$IMAP_PORT_DEFAULT]: " IMAP_PORT IMAP_PORT="${IMAP_PORT:-$IMAP_PORT_DEFAULT}" read -p " SMTP Username [$SMTP_USER_DEFAULT]: " SMTP_USER SMTP_USER="${SMTP_USER:-$SMTP_USER_DEFAULT}" echo -n " SMTP Password []: "; read -s SMTP_PASS; echo "" echo "" # --- Admin --- header "── Admin User ──" while [ -z "${ADMIN_UNAME:-}" ]; do read -p " Username: " ADMIN_UNAME [ -z "$ADMIN_UNAME" ] && warn "Username is required" done read -p " Email: " ADMIN_EMAIL echo "" # --- URLs --- header "── URLs ──" read -p " NextWks URL [$NEXTWKS_URL_DEFAULT]: " NEXTWKS_URL NEXTWKS_URL="${NEXTWKS_URL:-$NEXTWKS_URL_DEFAULT}" read -p " Auth URL [$AUTH_URL_DEFAULT]: " AUTH_URL AUTH_URL="${AUTH_URL:-$AUTH_URL_DEFAULT}" read -p " Reverse Proxy [$PROXY_IP_DEFAULT]: " PROXY_IP PROXY_IP="${PROXY_IP:-$PROXY_IP_DEFAULT}" # Extract domains from URLs NEXTWKS_DOMAIN=$(echo "$NEXTWKS_URL" | sed 's|https\?://||;s|/.*||') AUTH_DOMAIN=$(echo "$AUTH_URL" | sed 's|https\?://||;s|/.*||') # --- Confirm --- echo "" header "── Review ──" info " SMTP: ${SMTP_USER}@${SMTP_HOST}:${SMTP_PORT}" info " Admin: ${ADMIN_UNAME} (${ADMIN_EMAIL:-no email})" info " NextWks: ${NEXTWKS_URL}" info " Auth: ${AUTH_URL}" info " Proxy: ${PROXY_IP}" echo "" read -p " Install with these settings? [Y/n]: " CONFIRM [ "$CONFIRM" = "n" ] || [ "$CONFIRM" = "N" ] && { echo "Aborted."; exit 0; } # Save to .env for reuse cat > "$ENV_FILE" << ENVEOF SMTP_HOST="${SMTP_HOST}" SMTP_PORT="${SMTP_PORT}" IMAP_HOST="${IMAP_HOST}" IMAP_PORT="${IMAP_PORT}" SMTP_USER="${SMTP_USER}" SMTP_PASS="${SMTP_PASS}" ADMIN_UNAME="${ADMIN_UNAME}" ADMIN_EMAIL="${ADMIN_EMAIL}" NEXTWKS_URL="${NEXTWKS_URL}" AUTH_URL="${AUTH_URL}" PROXY_IP="${PROXY_IP}" ENVEOF success "Settings saved to $ENV_FILE" } [ "$MODE" = "install" ] && gather_inputs # Derive domains from URLs (needed even when --from-env) NEXTWKS_DOMAIN=$(echo "$NEXTWKS_URL" | sed 's|https\?://||;s|/.*||') AUTH_DOMAIN=$(echo "$AUTH_URL" | sed 's|https\?://||;s|/.*||') # ============================================================ # BUILD (always from source — this is a self-hosted deployment) # ============================================================ header "── Building from Source ──" if ! command -v go &>/dev/null; then error "Go 1.22+ is required. Run: apt install golang" exit 1 fi VERSION=$(cat "$REPO_DIR/VERSION" 2>/dev/null || echo "dev") cd "$REPO_DIR/src" BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") COMMIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") info "Version: ${VERSION}" info "Running tests..." go test -count=1 ./... 2>&1 | tail -2 || warn "Some tests failed — continuing" info "Compiling..." go build -ldflags="-s -w \ -X git.lohmar.co.uk/lexton-it/NextWks/core/version.Version=${VERSION} \ -X git.lohmar.co.uk/lexton-it/NextWks/core/version.BuildTime=${BUILD_TIME} \ -X git.lohmar.co.uk/lexton-it/NextWks/core/version.CommitSHA=${COMMIT_SHA}" \ -o "$REPO_DIR/app/core" . success "Binary built: app/core (${VERSION})" # ============================================================ # INSTALL # ============================================================ header "── Installing ──" mkdir -p "$BIN_DIR" "$DATA_DIR" "$STATIC_DIR" cp "$REPO_DIR/app/core" "$BIN_DIR/core" && chmod 755 "$BIN_DIR/core" [ -d "$REPO_DIR/app/static" ] && cp -r "$REPO_DIR/app/static"/* "$STATIC_DIR/" success "Copied files to $INSTALL_DIR/" # ============================================================ # CONFIG # ============================================================ header "── Configuration ──" ADMIN_TOKEN=$(openssl rand -hex 32 2>/dev/null || head -c32 /dev/urandom | xxd -p -c32) SESSION_KEY=$(openssl rand -hex 32 2>/dev/null || head -c32 /dev/urandom | xxd -p -c32) cat > "$CONFIG_FILE" << CONFIGEOF # Next Workspace — $(date +%Y-%m-%d) server: host: "0.0.0.0" port: 8080 admin: secret_token: "${ADMIN_TOKEN}" database: type: "sqlite" path: "${DATA_DIR}/nextwks.db" authelia: host: "http://127.0.0.1:9091" config_path: "${AUTHELIA_CONFIG}" users_db_path: "${AUTHELIA_DIR}/users_database.yml" oidc: issuer_url: "${AUTH_URL}" client_id: "nextwks" client_secret: "" redirect_url: "${NEXTWKS_URL}/auth/callback" domain: "${NEXTWKS_DOMAIN}" smtp: host: "${SMTP_HOST}" port: ${SMTP_PORT} username: "${SMTP_USER}" password: "${SMTP_PASS}" from: "${SMTP_USER}" session: secret: "${SESSION_KEY}" expiry_minutes: 60 CONFIGEOF chmod 600 "$CONFIG_FILE" success "Config: $CONFIG_FILE" # ============================================================ # SYSTEMD # ============================================================ header "── Systemd ──" 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 NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=${DATA_DIR} /opt/authelia/users_database.yml ReadOnlyPaths=${INSTALL_DIR}/config.yaml ${INSTALL_DIR}/static [Install] WantedBy=multi-user.target SERVICEEOF systemctl daemon-reload success "Service: $SERVICE_FILE" # ============================================================ # AUTHELIA CONFIG # ============================================================ if [ -f "${AUTHELIA_DIR}/authelia" ]; then header "── Authelia Config ──" JWT_SECRET=$(openssl rand -base64 32) SESSION_SECRET=$(openssl rand -base64 32) STORAGE_KEY=$(openssl rand -base64 32) OIDC_HMAC=$(openssl rand -base64 32) # Generate RSA key for OIDC signing openssl genrsa -out /tmp/nw-oidc.key 2048 2>/dev/null OIDC_KEY=$(cat /tmp/nw-oidc.key) rm -f /tmp/nw-oidc.key # Hash current admin password (if users exist) or generate one ADMIN_HASH=$("${AUTHELIA_DIR}/authelia" crypto hash generate --password "$(openssl rand -base64 12)" 2>/dev/null | awk '{print $NF}' || echo "\$argon2id\$v=19\$m=65536,t=3,p=4\$placeholder\$placeholder") cat > "$AUTHELIA_CONFIG" << AUTHEOF theme: light server: host: 0.0.0.0 port: 9091 authentication_backend: password_reset: disable: false file: path: "${AUTHELIA_DIR}/users_database.yml" watch: true session: name: authelia_session secret: "${SESSION_SECRET}" expiration: 1h inactivity: 5m cookies: - domain: "${AUTH_DOMAIN}" authelia_url: "${AUTH_URL}" storage: encryption_key: "${STORAGE_KEY}" local: path: "${AUTHELIA_DIR}/db.sqlite3" notifier: smtp: address: "${SMTP_HOST}:${SMTP_PORT}" username: "${SMTP_USER}" password: "${SMTP_PASS}" sender: "Authelia <${SMTP_USER}>" access_control: default_policy: deny rules: - domain: "${AUTH_DOMAIN}" policy: bypass - domain: "${NEXTWKS_DOMAIN}" policy: one_factor - domain: "*.${NEXTWKS_DOMAIN}" policy: one_factor totp: issuer: authelia.com identity_validation: reset_password: jwt_secret: "${JWT_SECRET}" identity_providers: oidc: hmac_secret: "${OIDC_HMAC}" jwks: - key_id: "nextwks-oidc-key" algorithm: "RS256" use: "sig" key: | $(echo "$OIDC_KEY" | sed 's/^/ /') clients: - client_id: "nextwks" client_name: "Next Workspace" public: true redirect_uris: - "${NEXTWKS_URL}/auth/callback" - "http://localhost:8080/auth/callback" scopes: - "openid" - "profile" - "email" authorization_policy: "one_factor" consent_mode: "pre-configured" pre_configured_consent_duration: "1 year" userinfo_signed_response_alg: "none" AUTHEOF chmod 600 "$AUTHELIA_CONFIG" success "Authelia config written" # Create initial users_database.yml (needed for Authelia to start) if [ ! -f "${AUTHELIA_DIR}/users_database.yml" ]; then info "Creating initial users database..." ADMIN_HASH=$("${AUTHELIA_DIR}/authelia" crypto hash generate --password "$(openssl rand -base64 16)" 2>/dev/null | awk '{print $NF}' || echo "\$argon2id\$v=19\$m=65536,t=3,p=4\$placeholder") cat > "${AUTHELIA_DIR}/users_database.yml" << USERSDB users: placeholder: displayname: "Setup Account" password: "${ADMIN_HASH}" email: "${ADMIN_EMAIL:-admin@local}" groups: [admins] USERSDB chmod 600 "${AUTHELIA_DIR}/users_database.yml" success "Users database created" fi systemctl restart authelia 2>/dev/null && info "Authelia restarted" || info "Authelia not running (will start later)" fi # ============================================================ # SMOKE TEST + ADMIN CREATION # ============================================================ header "── Verification ──" "$BIN_DIR/core" -config "$CONFIG_FILE" & SMOKE_PID=$! sleep 2 if command -v curl &>/dev/null; then RESPONSE=$(curl -s --max-time 3 http://localhost:8080/api/health 2>/dev/null || echo "") if [ "$RESPONSE" = '{"status":"ok"}' ]; then success "Server started OK" # Create admin user info "Creating admin user..." RESULT=$(curl -s -X POST http://localhost:8080/admin/api/users \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"users\":[{\"username\":\"$ADMIN_UNAME\",\"display_name\":\"$ADMIN_UNAME\",\"email\":\"$ADMIN_EMAIL\",\"role\":\"admin\",\"groups\":\"admins\"}]}") ADMIN_PASS=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['results'][0].get('generated_password',''))" 2>/dev/null || echo "") ADMIN_ERROR=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['results'][0].get('error',''))" 2>/dev/null || echo "") if [ -n "$ADMIN_PASS" ]; then success "Admin user created!" elif [ -n "$ADMIN_ERROR" ]; then warn "Admin creation: $ADMIN_ERROR" fi else warn "Health check failed: $RESPONSE" fi fi kill $SMOKE_PID 2>/dev/null; wait $SMOKE_PID 2>/dev/null # ============================================================ # SUMMARY # ============================================================ echo "" success "════════════════════════════════════════" success " Next Workspace v${VERSION} installed" success "════════════════════════════════════════" echo "" info " Workspace: ${NEXTWKS_URL}" info " Auth: ${AUTH_URL}" info " Admin UI: ${NEXTWKS_URL}/admin" echo "" info " Start: systemctl enable --now nextwks" info " Logs: journalctl -u nextwks -f" info " Status: ./install.sh --status" echo "" if [ -n "${ADMIN_PASS:-}" ]; then warn " ┌─────────────────────────────────────────┐" warn " │ Admin login: ${AUTH_URL}" warn " │ Username: ${ADMIN_UNAME}" warn " │ Password: ${ADMIN_PASS}" warn " │ Role: admin" warn " └─────────────────────────────────────────┘" echo "" warn " Save this password! It cannot be recovered." fi echo ""