#!/bin/bash # ============================================================ # Next Workspace (NextWks) — Installer # ============================================================ 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}"; } # ============================================================ # SERVICE USER # ============================================================ SVC_USER="nextwks" 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" # ============================================================ # AUTO-CLONE: if running standalone (not from repo), clone first # ============================================================ REPO_URL="https://git.lohmar.co.uk/lexton-it/NextWks.git" # If piped from curl (no script file), save and exit if [ ! -t 0 ] && [ ! -f "${BASH_SOURCE[0]}" ]; then SCRIPT_FILE="/tmp/nextwks-install.sh" cat > "$SCRIPT_FILE" chmod +x "$SCRIPT_FILE" echo "Script saved to $SCRIPT_FILE" echo "Run: bash $SCRIPT_FILE" exit 0 fi if [ ! -f "$REPO_DIR/src/main.go" ]; then info "Cloning NextWks repository..." if ! command -v git &>/dev/null; then error "git is required. Install it first: apt install git" exit 1 fi CLONE_DIR="${SYS_HOME}/nextwks" rm -rf "$CLONE_DIR" 2>/dev/null git clone --depth 1 "$REPO_URL" "$CLONE_DIR" REPO_DIR="$CLONE_DIR" success "Cloned to $REPO_DIR" fi # ============================================================ # PARSE FLAGS # ============================================================ FROM_ENV=false [ "${1:-}" = "--from-env" ] && FROM_ENV=true # ============================================================ # INTERACTIVE WIZARD # ============================================================ gather_inputs() { 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" } # Load env if --from-env, otherwise run interactive wizard if [ "$FROM_ENV" = true ] && [ -f "$ENV_FILE" ]; then source "$ENV_FILE" success "Loaded configuration from $ENV_FILE" else gather_inputs fi # 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 || true 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 ──" $SUDO mkdir -p "$BIN_DIR" "$DATA_DIR" "$STATIC_DIR" $SUDO cp "$REPO_DIR/app/core" "$BIN_DIR/core" $SUDO chmod 755 "$BIN_DIR/core" $SUDO chown -R "$SVC_USER:$SVC_USER" "$DATA_DIR" 2>/dev/null || true [ -d "$REPO_DIR/app/static" ] && $SUDO 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) $SUDO tee "$CONFIG_FILE" > /dev/null << 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 $SUDO chmod 600 "$CONFIG_FILE" $SUDO chown "$SVC_USER:$SVC_USER" "$CONFIG_FILE" $SUDO chown -R "$SVC_USER:$SVC_USER" "$DATA_DIR" success "Config: $CONFIG_FILE" # ============================================================ # SYSTEMD # ============================================================ header "── Systemd ──" $SUDO tee "$SERVICE_FILE" > /dev/null << SERVICEEOF [Unit] Description=Next Workspace (NextWks) Core After=network.target authelia.service Wants=authelia.service [Service] Type=simple User=${SVC_USER} 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 $SUDO systemctl daemon-reload success "Service: $SERVICE_FILE" # ============================================================ # ============================================================ # AUTHELIA — Download, configure, and start # ============================================================ header "── Authelia ──" AUTHELIA_VERSION="4.38.0" if [ ! -f "${AUTHELIA_DIR}/authelia" ]; then info "Downloading Authelia v${AUTHELIA_VERSION}..." $SUDO apt-get update -qq && $SUDO apt-get install -y -qq wget tar openssl jq 2>/dev/null $SUDO mkdir -p "$AUTHELIA_DIR" wget -q "https://github.com/authelia/authelia/releases/download/v${AUTHELIA_VERSION}/authelia-v${AUTHELIA_VERSION}-linux-amd64.tar.gz" -O /tmp/authelia.tar.gz $SUDO tar -xzf /tmp/authelia.tar.gz -C "$AUTHELIA_DIR" $SUDO mv "$AUTHELIA_DIR/authelia-linux-amd64" "$AUTHELIA_DIR/authelia" 2>/dev/null || true $SUDO chmod +x "$AUTHELIA_DIR/authelia" rm -f /tmp/authelia.tar.gz success "Authelia downloaded" fi # Generate Authelia secrets and 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 openssl genrsa -out /tmp/nw-oidc.key 2048 2>/dev/null OIDC_KEY=$(cat /tmp/nw-oidc.key) rm -f /tmp/nw-oidc.key # Clean old DB if encryption key changed $SUDO find "${AUTHELIA_DIR}/db.sqlite3" -delete 2>/dev/null || true info "Writing Authelia configuration..." $SUDO tee "$AUTHELIA_CONFIG" > /dev/null << 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 $SUDO chmod 600 "$AUTHELIA_CONFIG" $SUDO chown "$SVC_USER:$SVC_USER" "$AUTHELIA_CONFIG" "${AUTHELIA_DIR}/users_database.yml" "${AUTHELIA_DIR}/db.sqlite3" 2>/dev/null || true # Clean up files from Authelia tar $SUDO rm -f "${AUTHELIA_DIR}/authelia.service" "${AUTHELIA_DIR}/config.template.yml" 2>/dev/null || true # Create initial users database if [ ! -f "${AUTHELIA_DIR}/users_database.yml" ]; then ADMIN_HASH=$("${AUTHELIA_DIR}/authelia" crypto hash generate --password "$(openssl rand -base64 16)" 2>/dev/null | awk '{print $NF}' || echo "placeholder") $SUDO tee "${AUTHELIA_DIR}/users_database.yml" > /dev/null << USERSDB users: placeholder: displayname: "Setup Account" password: "${ADMIN_HASH}" email: "${ADMIN_EMAIL:-admin@local}" groups: [admins] USERSDB $SUDO chmod 600 "${AUTHELIA_DIR}/users_database.yml" fi # Create systemd service for Authelia $SUDO tee /etc/systemd/system/authelia.service > /dev/null << AUTHSVC [Unit] Description=Authelia Identity Provider After=network.target [Service] Type=simple User=${SVC_USER} WorkingDirectory=${AUTHELIA_DIR} ExecStart=${AUTHELIA_DIR}/authelia --config ${AUTHELIA_CONFIG} Restart=always RestartSec=5 [Install] WantedBy=multi-user.target AUTHSVC $SUDO systemctl daemon-reload $SUDO systemctl enable authelia 2>/dev/null || true $SUDO systemctl restart authelia 2>/dev/null sleep 2 if $SUDO systemctl is-active --quiet authelia 2>/dev/null; then success "Authelia v${AUTHELIA_VERSION} running" else warn "Authelia may need manual start — check: sudo journalctl -u authelia" fi # ============================================================ # SMOKE TEST + ADMIN CREATION # ============================================================ # SMOKE TEST + ADMIN CREATION # ============================================================ header "── Verification ──" $SUDO systemctl stop nextwks 2>/dev/null || true sleep 1 "$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 "Health check passed" # 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 "" # Start the service info "Starting services..." $SUDO systemctl enable --now nextwks 2>/dev/null && success "NextWks is running" || warn "Start service manually: sudo systemctl start nextwks" echo "" info " Logs: sudo journalctl -u nextwks -f" 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 ""