diff --git a/deploy.sh b/deploy.sh index 5fca253..a1ede2b 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,8 +1,8 @@ #!/bin/bash # # Dataflow Deploy Script -# First run: creates DB user/database, deploys schema, builds UI, installs service -# Subsequent runs: updates functions, rebuilds UI, restarts service +# First run: full install (database, schema, UI, nginx, systemd) +# Subsequent runs: update functions, UI, and restart service # set -e @@ -10,75 +10,175 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" -echo "Dataflow Deploy" -echo "===============" -echo "" +# ── Helpers ─────────────────────────────────────────────────────────────────── -# ── First-time setup ────────────────────────────────────────────────────────── +BOLD='\033[1m' +DIM='\033[2m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +RESET='\033[0m' + +section() { echo ""; echo -e "${BOLD}── $1 ──${RESET}"; } +step() { printf " %-42s" "$1..."; } +ok() { echo -e "${GREEN}✓${RESET}"; } +fail() { echo -e "${RED}✗ $1${RESET}"; exit 1; } +info() { echo -e " ${DIM}$1${RESET}"; } +warn() { echo -e " ${YELLOW}$1${RESET}"; } + +confirm() { + # confirm "Section name" — prints section header, asks to proceed + # Returns 0 to proceed, 1 to skip + section "$1" + read -p " Proceed? [Y/n]: " _yn + [[ ! "$_yn" =~ ^[Nn]$ ]] +} + +# ── Mode detection ──────────────────────────────────────────────────────────── + +section "Mode" if [ ! -f .env ]; then - echo "No .env found — running first-time setup." - echo "" + echo " No .env found — first-time install" + MODE=install +else + echo " .env found — update mode" + MODE=update + export $(cat .env | grep -v '^#' | xargs) +fi - # Admin credentials (needed to create user/database) - echo "PostgreSQL Admin Credentials" +# ── Phase 1: Collect all config ─────────────────────────────────────────────── + +if [ "$MODE" = "install" ]; then + + section "PostgreSQL Admin" + info "Needed to create the app user and database" read -p " Admin username [postgres]: " ADMIN_USER; ADMIN_USER=${ADMIN_USER:-postgres} read -s -p " Admin password: " ADMIN_PASS; echo "" - echo "" - # App credentials - echo "Application Database" - read -p " Host [localhost]: " DB_HOST; DB_HOST=${DB_HOST:-localhost} - read -p " Port [5432]: " DB_PORT; DB_PORT=${DB_PORT:-5432} - read -p " Database name [dataflow]: " DB_NAME; DB_NAME=${DB_NAME:-dataflow} - read -p " App username [dataflow]: " DB_USER; DB_USER=${DB_USER:-dataflow} - read -s -p " App password: " DB_PASSWORD; echo "" - echo "" + section "Application Database" + read -p " Host [localhost]: " DB_HOST; DB_HOST=${DB_HOST:-localhost} + read -p " Port [5432]: " DB_PORT; DB_PORT=${DB_PORT:-5432} + read -p " Database [dataflow]: " DB_NAME; DB_NAME=${DB_NAME:-dataflow} + read -p " App user [dataflow]: " DB_USER; DB_USER=${DB_USER:-dataflow} + read -s -p " App password: " DB_PASSWORD; echo "" - # API config - echo "API Configuration" - read -p " Port [3020]: " API_PORT; API_PORT=${API_PORT:-3020} - read -p " Environment [production]: " NODE_ENV; NODE_ENV=${NODE_ENV:-production} - echo "" + section "API" + read -p " Port [3020]: " API_PORT; API_PORT=${API_PORT:-3020} + read -p " Environment [production]:" NODE_ENV; NODE_ENV=${NODE_ENV:-production} - # Install Node dependencies - echo "Installing Node.js dependencies..." - npm install --prefix . --omit=dev -q - cd ui && npm install -q && cd .. - echo "✓ Dependencies installed" - echo "" - - # Test admin connection - echo "Testing admin connection..." - export PGPASSWORD="$ADMIN_PASS" - if ! psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres -c '\q' 2>/dev/null; then - echo "✗ Cannot connect as $ADMIN_USER — check credentials" - exit 1 + section "Nginx" + read -p " Set up nginx reverse proxy? [Y/n]: " _yn + if [[ ! "$_yn" =~ ^[Nn]$ ]]; then + read -p " Domain (e.g. dataflow.example.com): " NGINX_DOMAIN fi - echo "✓ Admin connection OK" - # Create app user - if psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres -tAc \ - "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" | grep -q 1; then - echo "✓ User '$DB_USER' already exists" + DO_DEPS=y; DO_DB=y; DO_SCHEMA=y; DO_FN=y; DO_BUILD=y; DO_SERVICE=y; DO_RESTART=y + +else + + section "Database" + echo " Current: ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}" + read -p " Change target? [y/N]: " _yn + if [[ "$_yn" =~ ^[Yy]$ ]]; then + read -p " Host [${DB_HOST}]: " _in; DB_HOST=${_in:-$DB_HOST} + read -p " Port [${DB_PORT}]: " _in; DB_PORT=${_in:-$DB_PORT} + read -p " Database [${DB_NAME}]: " _in; DB_NAME=${_in:-$DB_NAME} + read -p " User [${DB_USER}]: " _in; DB_USER=${_in:-$DB_USER} + read -s -p " Password (blank = keep): " _in; echo "" + if [ -n "$_in" ]; then DB_PASSWORD=$_in; fi + CHANGE_DB=y + fi + + section "Select steps to run" + read -p " Redeploy SQL functions? [Y/n]: " DO_FN; DO_FN=${DO_FN:-y} + read -p " Rebuild UI? [Y/n]: " DO_BUILD; DO_BUILD=${DO_BUILD:-y} + read -p " Set up / update nginx? [y/N]: " DO_NGINX + read -p " Restart API service? [Y/n]: " DO_RESTART; DO_RESTART=${DO_RESTART:-y} + + if [[ "$DO_NGINX" =~ ^[Yy]$ ]]; then + read -p " Nginx domain: " NGINX_DOMAIN + fi +fi + +# ── Phase 2: Plan summary ───────────────────────────────────────────────────── + +section "Plan" +echo " Mode: $( [ "$MODE" = "install" ] && echo "First-time install" || echo "Update" )" +echo " Database: ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}" +echo " API port: ${API_PORT:-3020}" +echo "" +echo " Steps:" + +if [ "$MODE" = "install" ]; then + echo " • Install Node.js dependencies" + echo " • Test admin connection and create DB user/database" + echo " • Deploy schema and SQL functions" + [ -n "$NGINX_DOMAIN" ] && echo " • Configure nginx → $NGINX_DOMAIN" \ + || echo " • Nginx — skipped (no domain provided)" + echo " • Build UI" + echo " • Install systemd service" + echo " • Start service" +else + [ "${CHANGE_DB}" = "y" ] && echo " • Update .env with new database target" + [[ ! "$DO_FN" =~ ^[Nn]$ ]] && echo " • Redeploy SQL functions" \ + || echo " • SQL functions — skipped" + [[ ! "$DO_BUILD" =~ ^[Nn]$ ]] && echo " • Rebuild UI" \ + || echo " • UI build — skipped" + [ -n "$NGINX_DOMAIN" ] && echo " • Configure nginx → $NGINX_DOMAIN" \ + || echo " • Nginx — skipped" + [[ ! "$DO_RESTART" =~ ^[Nn]$ ]] && echo " • Restart API service" \ + || echo " • Service restart — skipped" +fi + +echo "" +read -p " Continue? [Y/n]: " _yn +[[ "$_yn" =~ ^[Nn]$ ]] && echo " Aborted." && exit 0 + +# ── Phase 3: Execute ────────────────────────────────────────────────────────── + +if [ "$MODE" = "install" ]; then + + # Dependencies + if confirm "Dependencies"; then + step "API (npm install)" + npm install --omit=dev -q && ok || fail "npm install failed" + + step "UI (npm install)" + cd ui && npm install -q && cd .. && ok || fail "ui npm install failed" else - psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres \ - -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';" > /dev/null - echo "✓ User '$DB_USER' created" + info "skipped" fi - # Create database - if psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -lqt \ - | cut -d'|' -f1 | grep -qw "$DB_NAME"; then - echo "✓ Database '$DB_NAME' already exists" - else - psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres \ - -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" > /dev/null - echo "✓ Database '$DB_NAME' created" - fi - unset PGPASSWORD + # PostgreSQL + if confirm "PostgreSQL"; then + step "Testing admin connection" + export PGPASSWORD="$ADMIN_PASS" + psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres -c '\q' 2>/dev/null && ok \ + || fail "Cannot connect as $ADMIN_USER" - # Write .env - cat > .env << ENVEOF + step "Creating user '$DB_USER'" + if psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres -tAc \ + "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" 2>/dev/null | grep -q 1; then + echo -e "${DIM}already exists${RESET}" + else + psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres \ + -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';" > /dev/null && ok \ + || fail "Could not create user" + fi + + step "Creating database '$DB_NAME'" + if psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -lqt \ + | cut -d'|' -f1 | grep -qw "$DB_NAME"; then + echo -e "${DIM}already exists${RESET}" + else + psql -U "$ADMIN_USER" -h "$DB_HOST" -p "$DB_PORT" -d postgres \ + -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" > /dev/null && ok \ + || fail "Could not create database" + fi + unset PGPASSWORD + + step "Writing .env" + cat > .env << ENVEOF # Database Configuration DB_HOST=$DB_HOST DB_PORT=$DB_PORT @@ -90,35 +190,27 @@ DB_PASSWORD=$DB_PASSWORD API_PORT=$API_PORT NODE_ENV=$NODE_ENV ENVEOF - echo "✓ .env written" - echo "" + ok - # Deploy schema (first time only) - export PGPASSWORD="$DB_PASSWORD" - echo "Deploying schema..." - psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -f database/schema.sql -q - echo "✓ Schema deployed" + export PGPASSWORD="$DB_PASSWORD" - DEPLOY_FN=y - BUILD_UI=y - INSTALL_SERVICE=y - RESTART=y + step "Deploying schema" + psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -f database/schema.sql -q && ok \ + || fail "Schema deploy failed" + + step "Deploying functions" + psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -f database/functions.sql -q && ok \ + || fail "Functions deploy failed" + else + info "skipped" + fi -# ── Update mode ─────────────────────────────────────────────────────────────── else - export $(cat .env | grep -v '^#' | xargs) - - echo "Current database: ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}" - read -p "Change database target? [y/N]: " IN - if [[ "$IN" =~ ^[Yy]$ ]]; then - echo "" - read -p " Host [${DB_HOST}]: " IN; DB_HOST=${IN:-$DB_HOST} - read -p " Port [${DB_PORT}]: " IN; DB_PORT=${IN:-$DB_PORT} - read -p " Database name [${DB_NAME}]: " IN; DB_NAME=${IN:-$DB_NAME} - read -p " User [${DB_USER}]: " IN; DB_USER=${IN:-$DB_USER} - read -s -p " Password (blank = keep current): " IN; echo "" - if [ -n "$IN" ]; then DB_PASSWORD=$IN; fi + # Update: save .env if DB target changed + section ".env" + if [ "${CHANGE_DB}" = "y" ]; then + step "Writing updated .env" cat > .env << ENVEOF # Database Configuration DB_HOST=$DB_HOST @@ -131,69 +223,174 @@ DB_PASSWORD=$DB_PASSWORD API_PORT=${API_PORT:-3020} NODE_ENV=${NODE_ENV:-production} ENVEOF - echo "✓ .env updated" + ok + else + info "no changes" fi - echo "" + # Test connection + section "Database Connection" + step "Testing" export PGPASSWORD="$DB_PASSWORD" - echo "Testing database connection..." - if ! psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -c '\q' 2>/dev/null; then - echo "✗ Cannot connect — check credentials" - exit 1 - fi - echo "✓ Connected to ${DB_NAME} on ${DB_HOST}:${DB_PORT}" - echo "" + psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -c '\q' 2>/dev/null && ok \ + || fail "Cannot connect — check credentials" - read -p "Redeploy SQL functions? [Y/n]: " DEPLOY_FN - read -p "Rebuild UI? [Y/n]: " BUILD_UI - read -p "Restart API server? [Y/n]: " RESTART - INSTALL_SERVICE=n -fi - -# ── Shared steps ────────────────────────────────────────────────────────────── - -if [[ ! "$DEPLOY_FN" =~ ^[Nn]$ ]]; then - echo "" - echo "Deploying SQL functions..." - export PGPASSWORD="$DB_PASSWORD" - psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -f database/functions.sql -q - echo "✓ Functions deployed" -fi - -if [[ ! "$BUILD_UI" =~ ^[Nn]$ ]]; then - echo "" - echo "Building UI..." - cd ui && npm run build && cd .. - echo "✓ UI built" -fi - -# Systemd service -echo "" -SERVICE_FILE="/etc/systemd/system/dataflow.service" -if [ ! -f "$SERVICE_FILE" ] && [[ "$INSTALL_SERVICE" =~ ^[Yy]$ ]]; then - read -p "Install systemd service? (requires sudo) [Y/n]: " IN - if [[ ! "$IN" =~ ^[Nn]$ ]]; then - sudo cp "$SCRIPT_DIR/dataflow.service" "$SERVICE_FILE" - sudo systemctl daemon-reload - sudo systemctl enable dataflow - echo "✓ Service installed and enabled" - fi -fi - -if [[ ! "$RESTART" =~ ^[Nn]$ ]]; then - echo "" - if [ -f "$SERVICE_FILE" ]; then - sudo systemctl restart dataflow - sleep 1 - if systemctl is-active --quiet dataflow; then - echo "✓ Service restarted and running" + # SQL functions + if [[ ! "$DO_FN" =~ ^[Nn]$ ]]; then + if confirm "SQL Functions"; then + step "Deploying functions" + psql -U "$DB_USER" -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -f database/functions.sql -q && ok \ + || fail "Functions deploy failed" else - echo "✗ Service failed — check: journalctl -u dataflow -n 30" + info "skipped" fi else - echo " Systemd service not installed. Start manually: node api/server.js" + section "SQL Functions" + info "skipped" fi fi +# ── UI ──────────────────────────────────────────────────────────────────────── + +if [ "$MODE" = "install" ] || [[ ! "$DO_BUILD" =~ ^[Nn]$ ]]; then + if confirm "UI Build"; then + step "Building" + cd ui && npm run build > /dev/null 2>&1 && cd .. && ok \ + || fail "UI build failed" + else + info "skipped" + fi +else + section "UI Build" + info "skipped" +fi + +# ── Nginx ───────────────────────────────────────────────────────────────────── + +if [ -n "$NGINX_DOMAIN" ]; then + if confirm "Nginx ($NGINX_DOMAIN)"; then + CONF_NAME=$(echo "$NGINX_DOMAIN" | cut -d. -f1) + CONF_PATH="/etc/nginx/sites-enabled/$CONF_NAME" + CERT_PATH="/etc/letsencrypt/live/$NGINX_DOMAIN/fullchain.pem" + TMP_CONF=$(mktemp) + + if [ -f "$CERT_PATH" ]; then + cat > "$TMP_CONF" << NGINXEOF +server { + listen 80; + listen [::]:80; + server_name $NGINX_DOMAIN; + location / { return 301 https://\$host\$request_uri; } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name $NGINX_DOMAIN; + + ssl_certificate $CERT_PATH; + ssl_certificate_key /etc/letsencrypt/live/$NGINX_DOMAIN/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + + keepalive_timeout 70; + sendfile on; + client_max_body_size 80m; + + location / { + proxy_pass http://localhost:${API_PORT:-3020}; + } +} +NGINXEOF + else + cat > "$TMP_CONF" << NGINXEOF +server { + listen 80; + listen [::]:80; + server_name $NGINX_DOMAIN; + location / { + proxy_pass http://localhost:${API_PORT:-3020}; + } +} +NGINXEOF + fi + + step "Writing /etc/nginx/sites-enabled/$CONF_NAME" + sudo cp "$TMP_CONF" "$CONF_PATH" && rm "$TMP_CONF" && ok \ + || fail "Could not write nginx config (check sudo)" + + step "Testing nginx config" + sudo nginx -t > /dev/null 2>&1 && ok \ + || fail "nginx config invalid — run: sudo nginx -t" + + step "Reloading nginx" + sudo systemctl reload nginx && ok \ + || fail "nginx reload failed" + + if [ ! -f "$CERT_PATH" ]; then + warn "No SSL cert found for $NGINX_DOMAIN" + read -p " Run certbot now? [Y/n]: " _yn + if [[ ! "$_yn" =~ ^[Nn]$ ]]; then + step "Running certbot" + sudo certbot --nginx -d "$NGINX_DOMAIN" --non-interactive --agree-tos \ + --redirect -m "admin@$NGINX_DOMAIN" > /dev/null 2>&1 && ok \ + || fail "certbot failed — run manually: sudo certbot --nginx -d $NGINX_DOMAIN" + fi + fi + else + info "skipped" + fi +else + section "Nginx" + info "skipped" +fi + +# ── Systemd service ─────────────────────────────────────────────────────────── + +SERVICE_FILE="/etc/systemd/system/dataflow.service" + +if confirm "Systemd Service"; then + if [ -f "$SERVICE_FILE" ]; then + info "already installed" + else + step "Installing service" + sudo cp "$SCRIPT_DIR/dataflow.service" "$SERVICE_FILE" && ok \ + || fail "Could not install service" + + step "Enabling on boot" + sudo systemctl daemon-reload && sudo systemctl enable dataflow > /dev/null 2>&1 && ok \ + || fail "Could not enable service" + fi +else + info "skipped" +fi + +# ── API Server ──────────────────────────────────────────────────────────────── + +if [ "$MODE" = "install" ] || [[ ! "$DO_RESTART" =~ ^[Nn]$ ]]; then + if confirm "API Server"; then + if [ -f "$SERVICE_FILE" ]; then + step "Restarting dataflow service" + sudo systemctl restart dataflow && sleep 1 + systemctl is-active --quiet dataflow && ok \ + || fail "Service failed — check: journalctl -u dataflow -n 30" + else + info "systemd service not installed — start manually: node api/server.js" + fi + else + info "skipped" + fi +else + section "API Server" + info "skipped" +fi + +# ── Done ────────────────────────────────────────────────────────────────────── + +section "Done" +echo " API: http://localhost:${API_PORT:-3020}" +[ -n "$NGINX_DOMAIN" ] && echo " Web: https://$NGINX_DOMAIN" +echo " Logs: journalctl -u dataflow -f" echo "" -echo "✓ Done — http://localhost:${API_PORT}"