build: rewrite installer as single interactive wizard with Authelia config generation
This commit is contained in:
parent
f6fba3ab62
commit
827c8ef116
2 changed files with 360 additions and 348 deletions
17
README.md
17
README.md
|
|
@ -64,6 +64,23 @@ NextWks/
|
||||||
- Go 1.22+
|
- Go 1.22+
|
||||||
- Authelia v4.38 (install with `bash scripts/install-authelia.sh`)
|
- Authelia v4.38 (install with `bash scripts/install-authelia.sh`)
|
||||||
|
|
||||||
|
### Production Install (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.lohmar.co.uk/lexton-it/NextWks/raw/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer walks you through:
|
||||||
|
- Email configuration (SMTP/IMAP)
|
||||||
|
- Admin user creation
|
||||||
|
- URL setup (workspace + auth + proxy)
|
||||||
|
- All secrets auto-generated
|
||||||
|
- Authelia configuration written
|
||||||
|
- Systemd service created
|
||||||
|
- Smoke test verification
|
||||||
|
|
||||||
|
To re-run with saved answers: `./install.sh --from-env`
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
691
install.sh
691
install.sh
|
|
@ -1,308 +1,260 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Next Workspace (NextWks) - Bare-Metal Installer
|
# ============================================================
|
||||||
# Deploys compiled binaries to /opt/nextwks/ for production use
|
# Next Workspace (NextWks) — Installer
|
||||||
|
#
|
||||||
|
# curl -fsSL https://git.lohmar.co.uk/lexton-it/NextWks/raw/main/install.sh | bash
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./install.sh Build and install
|
# ./install.sh Full interactive install
|
||||||
# ./install.sh --skip-build Install existing binaries only
|
# ./install.sh --from-env Load answers from .env
|
||||||
# ./install.sh --build-only Compile binary only (no install)
|
# ./install.sh --status Check installation health
|
||||||
# ./install.sh --config-only Generate config only
|
# ./install.sh --uninstall Remove everything
|
||||||
# ./install.sh --status Check installation health
|
# ============================================================
|
||||||
# ./install.sh --uninstall Remove installation
|
|
||||||
# ./install.sh --help Show this help
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
RED='\033[0;31m'
|
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'
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
error() { echo -e "${RED}Error:${NC} $1" >&2; }
|
error() { echo -e "${RED}Error:${NC} $1" >&2; }
|
||||||
success() { echo -e "${GREEN}$1${NC}"; }
|
success() { echo -e "${GREEN}$1${NC}"; }
|
||||||
info() { echo -e "${BLUE}$1${NC}"; }
|
info() { echo -e "${BLUE}$1${NC}"; }
|
||||||
warn() { echo -e "${YELLOW}Warning:${NC} $1"; }
|
warn() { echo -e "${YELLOW}$1${NC}"; }
|
||||||
|
header() { echo -e "\n${BOLD}${CYAN}$1${NC}"; }
|
||||||
|
|
||||||
# Configuration
|
# ============================================================
|
||||||
|
# PATHS
|
||||||
|
# ============================================================
|
||||||
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
INSTALL_DIR="/opt/nextwks"
|
INSTALL_DIR="/opt/nextwks"
|
||||||
BIN_DIR="${INSTALL_DIR}/bin"
|
BIN_DIR="${INSTALL_DIR}/bin"
|
||||||
DATA_DIR="${INSTALL_DIR}/data"
|
DATA_DIR="${INSTALL_DIR}/data"
|
||||||
MODULES_DIR="${INSTALL_DIR}/modules"
|
|
||||||
STATIC_DIR="${INSTALL_DIR}/static"
|
STATIC_DIR="${INSTALL_DIR}/static"
|
||||||
CONFIG_FILE="${INSTALL_DIR}/config.yaml"
|
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"
|
SERVICE_FILE="/etc/systemd/system/nextwks.service"
|
||||||
HEALTH_URL="http://localhost:8080/api/health"
|
|
||||||
|
|
||||||
# Parse arguments
|
# ============================================================
|
||||||
SKIP_BUILD=false
|
# DEFAULTS
|
||||||
BUILD_ONLY=false
|
# ============================================================
|
||||||
CONFIG_ONLY=false
|
SMTP_HOST_DEFAULT="smtp.openxchange.eu"
|
||||||
UNINSTALL=false
|
SMTP_PORT_DEFAULT="587"
|
||||||
CHECK_STATUS=false
|
SMTP_USER_DEFAULT="post@2-4-h.app"
|
||||||
ADMIN_USER=""
|
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
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--skip-build) SKIP_BUILD=true ;;
|
--from-env) FROM_ENV=true ;;
|
||||||
--build-only) BUILD_ONLY=true ;;
|
--status) MODE="status" ;;
|
||||||
--config-only) CONFIG_ONLY=true ;;
|
--uninstall) MODE="uninstall" ;;
|
||||||
--uninstall) UNINSTALL=true ;;
|
|
||||||
--status) CHECK_STATUS=true ;;
|
|
||||||
--admin=*) ADMIN_USER="${arg#*=}" ;;
|
|
||||||
--admin)
|
|
||||||
error "Use --admin=username,email (e.g., --admin=cclohmar,claus@lohmar.co.uk)"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
--help)
|
--help)
|
||||||
cat << 'HELPEOF'
|
head -20 "$0" | grep "^#" | sed 's/^# //; 1s/.*/NextWks Installer v2026.6.0001/'
|
||||||
NextWks Installer — Bare-metal deployment tool
|
exit 0 ;;
|
||||||
|
*) error "Unknown: $arg (use --help)"; exit 1 ;;
|
||||||
./install.sh Build and install
|
|
||||||
./install.sh --skip-build Install existing binary only
|
|
||||||
./install.sh --build-only Compile only (no install)
|
|
||||||
./install.sh --config-only Generate config only
|
|
||||||
./install.sh --admin=user,email Create initial admin user
|
|
||||||
./install.sh --status Check installation health
|
|
||||||
./install.sh --uninstall Remove installation
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
./install.sh Full build + install
|
|
||||||
./install.sh --admin=cclohmar,cl@sechpoint.app Create admin during install
|
|
||||||
./install.sh --status Check what's running
|
|
||||||
HELPEOF
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*) error "Unknown argument: $arg (use --help for options)"; exit 1 ;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# --- Status Check ---
|
# ============================================================
|
||||||
if [ "$CHECK_STATUS" = true ]; then
|
# UNINSTALL
|
||||||
echo ""
|
# ============================================================
|
||||||
info "Next Workspace Installation Status"
|
if [ "$MODE" = "uninstall" ]; then
|
||||||
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..."
|
info "Uninstalling Next Workspace..."
|
||||||
|
systemctl stop nextwks 2>/dev/null || true
|
||||||
if [ -f "$SERVICE_FILE" ]; then
|
systemctl disable nextwks 2>/dev/null || true
|
||||||
systemctl stop nextwks 2>/dev/null || true
|
rm -f "$SERVICE_FILE"
|
||||||
systemctl disable nextwks 2>/dev/null || true
|
systemctl daemon-reload
|
||||||
rm -f "$SERVICE_FILE"
|
[ -d "$INSTALL_DIR" ] && { rm -rf "$INSTALL_DIR"; success "Removed $INSTALL_DIR"; }
|
||||||
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"
|
success "Uninstall complete"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Prerequisites ---
|
# ============================================================
|
||||||
info "Checking prerequisites..."
|
# STATUS
|
||||||
|
# ============================================================
|
||||||
if [ "$SKIP_BUILD" = false ] && [ "$CONFIG_ONLY" = false ] && [ "$BUILD_ONLY" = false ]; then
|
if [ "$MODE" = "status" ]; then
|
||||||
if ! command -v go &>/dev/null; then
|
echo ""; header "Next Workspace Status"
|
||||||
error "Go is not installed. Install Go 1.22+ first."
|
[ -f "${BIN_DIR}/core" ] && success "✓ Binary: ${BIN_DIR}/core" || warn "✗ Binary: not found"
|
||||||
exit 1
|
[ -f "$CONFIG_FILE" ] && success "✓ Config: $CONFIG_FILE" || warn "✗ Config: not found"
|
||||||
fi
|
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
|
fi
|
||||||
|
|
||||||
# Check Authelia
|
# ============================================================
|
||||||
if [ ! -f "/opt/authelia/authelia" ]; then
|
# INTERACTIVE WIZARD
|
||||||
warn "Authelia is not installed at /opt/authelia/"
|
# ============================================================
|
||||||
warn "Admin user management requires Authelia to function."
|
gather_inputs() {
|
||||||
warn "Install with: bash $REPO_DIR/scripts/install-authelia.sh"
|
# 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 ""
|
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
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# BUILD
|
||||||
|
# ============================================================
|
||||||
|
header "── Building ──"
|
||||||
|
if ! command -v go &>/dev/null; then
|
||||||
|
error "Go 1.22+ required"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Static Assets ---
|
cd "$REPO_DIR/src"
|
||||||
if [ "$CONFIG_ONLY" = false ] && [ "$BUILD_ONLY" = false ]; then
|
VERSION=$(cat "$REPO_DIR/VERSION" 2>/dev/null || echo "dev")
|
||||||
info "Installing static assets..."
|
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
if [ -d "$REPO_DIR/app/static" ]; then
|
COMMIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
mkdir -p "$STATIC_DIR"
|
|
||||||
cp -r "$REPO_DIR/app/static"/* "$STATIC_DIR/"
|
|
||||||
success "Static assets installed to $STATIC_DIR"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Build ---
|
info "Running tests..."
|
||||||
if [ "$SKIP_BUILD" = false ] && [ "$CONFIG_ONLY" = false ]; then
|
go test -count=1 ./... 2>&1 | tail -2 || warn "Some tests failed — continuing"
|
||||||
info "Building Next Workspace core..."
|
|
||||||
|
|
||||||
# Build with version info from ldflags
|
info "Compiling (${VERSION})..."
|
||||||
VERSION=$(cat "$REPO_DIR/VERSION" 2>/dev/null || echo "dev")
|
go build -ldflags="-s -w \
|
||||||
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
-X git.lohmar.co.uk/lexton-it/NextWks/core/version.Version=${VERSION} \
|
||||||
COMMIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
-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})"
|
||||||
|
|
||||||
cd "$REPO_DIR/src"
|
# ============================================================
|
||||||
|
# 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/"
|
||||||
|
|
||||||
# Run tests first
|
# ============================================================
|
||||||
info "Running tests..."
|
# CONFIG
|
||||||
if ! go test -count=1 ./... 2>&1 | tail -1; then
|
# ============================================================
|
||||||
warn "Some tests failed — continuing build anyway"
|
header "── Configuration ──"
|
||||||
fi
|
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)
|
||||||
# Build with stripped symbols and version info
|
|
||||||
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 "Core binary built: app/core (${VERSION})"
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
cat > "$CONFIG_FILE" << CONFIGEOF
|
||||||
|
# Next Workspace — $(date +%Y-%m-%d)
|
||||||
server:
|
server:
|
||||||
host: "0.0.0.0"
|
host: "0.0.0.0"
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
admin:
|
admin:
|
||||||
secret_token: "${ADMIN_SECRET}"
|
secret_token: "${ADMIN_TOKEN}"
|
||||||
|
|
||||||
database:
|
database:
|
||||||
type: "sqlite"
|
type: "sqlite"
|
||||||
path: "${DATA_DIR}/nextwks.db"
|
path: "${DATA_DIR}/nextwks.db"
|
||||||
|
|
||||||
authelia:
|
authelia:
|
||||||
host: "http://127.0.0.1:9091"
|
host: "http://127.0.0.1:9091"
|
||||||
config_path: "/opt/authelia/configuration.yml"
|
config_path: "${AUTHELIA_CONFIG}"
|
||||||
users_db_path: "/opt/authelia/users_database.yml"
|
users_db_path: "${AUTHELIA_DIR}/users_database.yml"
|
||||||
|
|
||||||
oidc:
|
oidc:
|
||||||
issuer_url: "https://auth.lohmar.co.uk"
|
issuer_url: "${AUTH_URL}"
|
||||||
client_id: "nextwks"
|
client_id: "nextwks"
|
||||||
client_secret: ""
|
client_secret: ""
|
||||||
redirect_url: "https://wks.lohmar.co.uk/auth/callback"
|
redirect_url: "${NEXTWKS_URL}/auth/callback"
|
||||||
domain: "wks.lohmar.co.uk"
|
domain: "${NEXTWKS_DOMAIN}"
|
||||||
|
|
||||||
smtp:
|
smtp:
|
||||||
host: ""
|
host: "${SMTP_HOST}"
|
||||||
port: 587
|
port: ${SMTP_PORT}
|
||||||
username: ""
|
username: "${SMTP_USER}"
|
||||||
password: ""
|
password: "${SMTP_PASS}"
|
||||||
from: "noreply@nextwks.local"
|
from: "${SMTP_USER}"
|
||||||
|
|
||||||
session:
|
session:
|
||||||
secret: "${SESSION_SECRET}"
|
secret: "${SESSION_KEY}"
|
||||||
expiry_minutes: 60
|
expiry_minutes: 60
|
||||||
CONFIGEOF
|
CONFIGEOF
|
||||||
|
chmod 600 "$CONFIG_FILE"
|
||||||
|
success "Config: $CONFIG_FILE"
|
||||||
|
|
||||||
chmod 600 "$CONFIG_FILE"
|
# ============================================================
|
||||||
success "Configuration generated: $CONFIG_FILE"
|
# SYSTEMD
|
||||||
echo ""
|
# ============================================================
|
||||||
warn " Admin Secret Token: ${ADMIN_SECRET}"
|
header "── Systemd ──"
|
||||||
warn " Store this securely! Required for admin API access."
|
cat > "$SERVICE_FILE" << SERVICEEOF
|
||||||
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]
|
[Unit]
|
||||||
Description=Next Workspace (NextWks) Core
|
Description=Next Workspace (NextWks) Core
|
||||||
After=network.target authelia.service
|
After=network.target authelia.service
|
||||||
|
|
@ -318,126 +270,169 @@ RestartSec=5
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
|
|
||||||
# Security hardening
|
|
||||||
NoNewPrivileges=yes
|
NoNewPrivileges=yes
|
||||||
PrivateTmp=yes
|
PrivateTmp=yes
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ProtectHome=yes
|
ProtectHome=yes
|
||||||
ReadWritePaths=${DATA_DIR} ${MODULES_DIR} /opt/authelia/users_database.yml
|
ReadWritePaths=${DATA_DIR} /opt/authelia/users_database.yml
|
||||||
ReadOnlyPaths=${INSTALL_DIR}/config.yaml ${INSTALL_DIR}/static
|
ReadOnlyPaths=${INSTALL_DIR}/config.yaml ${INSTALL_DIR}/static
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
SERVICEEOF
|
SERVICEEOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
success "Service: $SERVICE_FILE"
|
||||||
|
|
||||||
systemctl daemon-reload
|
# ============================================================
|
||||||
success "Systemd service created: $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
|
||||||
|
default_redirection_url: "${NEXTWKS_URL}"
|
||||||
|
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}"
|
||||||
|
default_redirection_url: "${NEXTWKS_URL}"
|
||||||
|
notifier:
|
||||||
|
smtp:
|
||||||
|
address: "${SMTP_HOST}:${SMTP_PORT}"
|
||||||
|
username: "${SMTP_USER}"
|
||||||
|
password: "${SMTP_PASS}"
|
||||||
|
sender: "Authelia <${SMTP_USER}>"
|
||||||
|
storage:
|
||||||
|
encryption_key: "${STORAGE_KEY}"
|
||||||
|
local:
|
||||||
|
path: "${AUTHELIA_DIR}/db.sqlite3"
|
||||||
|
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_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"
|
||||||
|
systemctl restart authelia 2>/dev/null && info "Authelia restarted" || info "Authelia not running (will start later)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Smoke Test ---
|
# ============================================================
|
||||||
if [ "$CONFIG_ONLY" = false ]; then
|
# SMOKE TEST + ADMIN CREATION
|
||||||
info "Starting smoke test..."
|
# ============================================================
|
||||||
|
header "── Verification ──"
|
||||||
|
|
||||||
# Start the server temporarily to verify it works
|
"$BIN_DIR/core" -config "$CONFIG_FILE" &
|
||||||
if [ -f "$BIN_DIR/core" ] && [ -f "$CONFIG_FILE" ]; then
|
SMOKE_PID=$!
|
||||||
"$BIN_DIR/core" -config "$CONFIG_FILE" &
|
sleep 2
|
||||||
SMOKE_PID=$!
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
if command -v curl &>/dev/null; then
|
if command -v curl &>/dev/null; then
|
||||||
RESPONSE=$(curl -s --max-time 3 "$HEALTH_URL" 2>/dev/null || echo "")
|
RESPONSE=$(curl -s --max-time 3 http://localhost:8080/api/health 2>/dev/null || echo "")
|
||||||
if [ "$RESPONSE" = '{"status":"ok"}' ]; then
|
if [ "$RESPONSE" = '{"status":"ok"}' ]; then
|
||||||
success "Smoke test passed — server responds OK"
|
success "Server started OK"
|
||||||
else
|
|
||||||
warn "Smoke test: server started but health check returned '$RESPONSE'"
|
# Create admin user
|
||||||
fi
|
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
|
fi
|
||||||
|
else
|
||||||
# --- Admin User Creation ---
|
warn "Health check failed: $RESPONSE"
|
||||||
if [ "$CONFIG_ONLY" = false ]; then
|
|
||||||
# Read admin token from config
|
|
||||||
ADMIN_TOKEN=$(grep secret_token "$CONFIG_FILE" | head -1 | sed 's/.*: *"*//;s/"*$//' | xargs)
|
|
||||||
ADMIN_API="http://localhost:8080/admin/api/users"
|
|
||||||
|
|
||||||
# Check if interactive or --admin flag was provided
|
|
||||||
if [ -t 0 ] && [ -z "$ADMIN_USER" ] && [ ! -f "$CONFIG_FILE.initialized" ]; then
|
|
||||||
echo ""
|
|
||||||
info "No --admin flag provided. Create an initial admin user?"
|
|
||||||
read -p "Enter username:email (or press Enter to skip): " ADMIN_INPUT
|
|
||||||
if [ -n "$ADMIN_INPUT" ]; then
|
|
||||||
ADMIN_USER="$ADMIN_INPUT"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$ADMIN_USER" ]; then
|
|
||||||
# Parse username,email
|
|
||||||
ADMIN_UNAME="${ADMIN_USER%%,*}"
|
|
||||||
ADMIN_EMAIL="${ADMIN_USER#*,}"
|
|
||||||
if [ "$ADMIN_UNAME" = "$ADMIN_EMAIL" ]; then
|
|
||||||
ADMIN_EMAIL=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "Creating admin user: $ADMIN_UNAME..."
|
|
||||||
RESULT=$(curl -s -X POST "$ADMIN_API" \
|
|
||||||
-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\"}]}")
|
|
||||||
|
|
||||||
PASSWORD=$(echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['results'][0].get('generated_password',''))" 2>/dev/null || echo "")
|
|
||||||
ERROR=$(echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['results'][0].get('error',''))" 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
if [ -n "$PASSWORD" ]; then
|
|
||||||
success "Admin user created!"
|
|
||||||
echo ""
|
|
||||||
warn " ┌─────────────────────────────────────────┐"
|
|
||||||
warn " │ Username: $ADMIN_UNAME"
|
|
||||||
warn " │ Email: ${ADMIN_EMAIL:-<not set>}"
|
|
||||||
warn " │ Password: $PASSWORD"
|
|
||||||
warn " │ Groups: admins"
|
|
||||||
warn " └─────────────────────────────────────────┘"
|
|
||||||
echo ""
|
|
||||||
warn " Save this password! It cannot be recovered."
|
|
||||||
warn " User will be synced to Authelia automatically."
|
|
||||||
echo ""
|
|
||||||
elif [ -n "$ERROR" ]; then
|
|
||||||
warn "Admin creation failed: $ERROR"
|
|
||||||
else
|
|
||||||
warn "Could not parse response from admin API"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Mark as initialized to skip interactive prompt next time
|
|
||||||
touch "$CONFIG_FILE.initialized" 2>/dev/null || true
|
|
||||||
|
|
||||||
kill $SMOKE_PID 2>/dev/null || true
|
|
||||||
wait $SMOKE_PID 2>/dev/null || true
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Summary ---
|
kill $SMOKE_PID 2>/dev/null; wait $SMOKE_PID 2>/dev/null
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# SUMMARY
|
||||||
|
# ============================================================
|
||||||
echo ""
|
echo ""
|
||||||
success "═══════════════════════════════════════════"
|
success "════════════════════════════════════════"
|
||||||
success " Next Workspace (NextWks) installed!"
|
success " Next Workspace v${VERSION} installed"
|
||||||
success "═══════════════════════════════════════════"
|
success "════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
info " Binary: ${BIN_DIR}/core"
|
info " Workspace: ${NEXTWKS_URL}"
|
||||||
info " Config: ${CONFIG_FILE}"
|
info " Auth: ${AUTH_URL}"
|
||||||
info " Data: ${DATA_DIR}/"
|
info " Admin UI: ${NEXTWKS_URL}/admin"
|
||||||
info " Static assets: ${STATIC_DIR}/"
|
|
||||||
info " Service: systemctl start nextwks"
|
|
||||||
echo ""
|
echo ""
|
||||||
info " Workspace: https://wks.lohmar.co.uk/"
|
info " Start: systemctl enable --now nextwks"
|
||||||
info " Admin UI: http://localhost:8080/admin"
|
info " Logs: journalctl -u nextwks -f"
|
||||||
info " Health API: http://localhost:8080/api/health"
|
info " Status: ./install.sh --status"
|
||||||
info " Auth status: http://localhost:8080/auth/status"
|
|
||||||
echo ""
|
echo ""
|
||||||
info " Start: systemctl enable --now nextwks"
|
if [ -n "${ADMIN_PASS:-}" ]; then
|
||||||
info " Logs: journalctl -u nextwks -f"
|
warn " ┌─────────────────────────────────────────┐"
|
||||||
info " Status: ./install.sh --status"
|
warn " │ Admin login: ${AUTH_URL}"
|
||||||
echo ""
|
warn " │ Username: ${ADMIN_UNAME}"
|
||||||
warn " Next step: Register NextWks as OIDC client in Authelia:"
|
warn " │ Password: ${ADMIN_PASS}"
|
||||||
warn " • Edit /opt/authelia/configuration.yml"
|
warn " │ Role: admin"
|
||||||
warn " • Add client_id: nextwks with redirect_uri: https://wks.lohmar.co.uk/auth/callback"
|
warn " └─────────────────────────────────────────┘"
|
||||||
warn " • Restart: systemctl restart authelia"
|
echo ""
|
||||||
|
warn " Save this password! It cannot be recovered."
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue