NextWks/install.sh

468 lines
16 KiB
Bash
Executable file

#!/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}"; }
# ============================================================
# USER & SUDO
# ============================================================
SYS_USER="${SUDO_USER:-$USER}"
SYS_HOME=$(eval echo "~$SYS_USER")
[ "$EUID" -eq 0 ] && SUDO="" || SUDO="sudo"
if [ -z "$SUDO" ] && [ "$EUID" -ne 0 ]; then
error "This script needs root for /opt/ and systemd. Use: sudo bash install.sh"
exit 1
fi
# ============================================================
# 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="${SYS_HOME}/.nextwks-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"
# Detect if stdin is piped (curl|bash) — save to file and re-exec
if [ ! -t 0 ] && [ -z "${NEXTWKS_REEXEC:-}" ]; then
SCRIPT_FILE="/tmp/nextwks-install.sh"
cat > "$SCRIPT_FILE"
chmod +x "$SCRIPT_FILE"
echo ""
echo "Script saved to $SCRIPT_FILE"
echo "Run: bash $SCRIPT_FILE"
echo ""
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"
chown -R "$SYS_USER:$SYS_USER" "$CLONE_DIR" 2>/dev/null || true
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() {
# 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"
}
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 ──"
$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 "$SYS_USER:$SYS_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 "$SYS_USER:$SYS_USER" "$CONFIG_FILE"
$SUDO chown -R "$SYS_USER:$SYS_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=${SYS_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 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")
$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"
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..."
# Remove old DB if encryption key changed
find "${AUTHELIA_DIR}/db.sqlite3" -delete 2>/dev/null || true
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
$SUDO chmod 600 "${AUTHELIA_DIR}/users_database.yml"
success "Users database created"
fi
$SUDO 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 ""
# 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 ""