← back to Dw Yolo Loop

artmura-site/deploy-vendor.sh

105 lines

#!/usr/bin/env bash
# Generic DW vendor-landing deploy — any line built with build-line.js (CDN images).
#   VENDOR=thibaut DOMAIN=thibaut.designerwallcoverings.com bash deploy-vendor.sh
# Self-contained: ships the template app + the line's data snapshot; pm2 (BUNDLE=1) + nginx + Let's Encrypt.
# For Artmura specifically (local downloaded images) use deploy-landing.sh instead.
set -euo pipefail
KAM="${KAM:-root@45.61.58.125}"
VENDOR="${VENDOR:?set VENDOR=<slug>}"
DOMAIN="${DOMAIN:?set DOMAIN=<sub.designerwallcoverings.com>}"
APP="/root/Projects/${VENDOR}-site"
PM2="${VENDOR}-site"
SITE="$(cd "$(dirname "$0")" && pwd)"          # the canonical template app dir (artmura-site)
DATA="$SITE/lines/${VENDOR}.json"
HANDLES="$SITE/lines/${VENDOR}-handles.json"
COLORS="$SITE/lines/${VENDOR}-colors.json"     # optional

# TK-11200 — showroom-only guard. Refuse to stand up a public microsite (a discovery
# surface) for a showroom-only line; those stay "addressable but not discoverable".
# Checks the SLUG, the DOMAIN and the snapshot's own vendor field, so a renamed slug
# or a mismatched domain can't slip past. Override: ALLOW_SHOWROOM=1.
node -e '
  const s = require(process.argv[4]), fs = require("fs");
  const cand = new Set([process.argv[1], process.argv[2].split(".")[0]]);
  try { cand.add(JSON.parse(fs.readFileSync(process.argv[3], "utf8")).vendor); } catch {}
  for (const c of cand) if (c && !s.assertNotShowroom(c, "deploying a public vendor microsite")) process.exit(2);
' "$VENDOR" "$DOMAIN" "$DATA" "$SITE/showroom.js" || exit 2

[ -f "$DATA" ]    || { echo "missing $DATA — run: node build-line.js \"<Vendor>\" first"; exit 1; }
[ -f "$HANDLES" ] || { echo "missing $HANDLES"; exit 1; }


echo "→ resolving port (reuse if $PM2 already deployed, else a free one)"
PORT=$(ssh "$KAM" "
  cur=\$(pm2 jlist 2>/dev/null | node -e \"try{const a=JSON.parse(require('fs').readFileSync(0));const p=a.find(x=>x.name==='$PM2');process.stdout.write(String((p&&p.pm2_env&&p.pm2_env.env&&p.pm2_env.env.PORT)||''))}catch(e){}\" 2>/dev/null)
  if [ -n \"\$cur\" ]; then echo \"\$cur\"; else
    for p in 9941 9942 9943 9944 9945 9946 9947 9948 9949; do ss -ltn | grep -q \":\$p \" || { echo \$p; break; }; done
  fi")
[ -z "$PORT" ] && { echo "no free port found"; exit 1; }
echo "  using port $PORT"

echo "→ shipping app + line data to $KAM:$APP"
ssh "$KAM" "mkdir -p $APP/_data"
rsync -az --delete --exclude node_modules --exclude _images --exclude _data --exclude lines "$SITE/" "$KAM:$APP/"
rsync -az "$DATA" "$HANDLES" "$KAM:$APP/_data/"
[ -f "$COLORS" ] && rsync -az "$COLORS" "$KAM:$APP/_data/" || true

echo "→ install + pm2 (BUNDLE=1, VENDOR=$VENDOR, port $PORT)"
ssh "$KAM" "cd $APP && npm install --omit=dev --silent; pm2 delete $PM2 2>/dev/null; cd $APP && VENDOR=$VENDOR BUNDLE=1 PORT=$PORT pm2 start server.js --name $PM2 && pm2 save"

# TK-11391 — gate by default on the fleet's own naming convention. An "-internal" host is
# a trade/spec viewer and must sit behind the same Basic Auth as the other ~453 of them; a
# public vendor LANDING (artmura, grasscloth) must not. This template previously emitted NO
# auth_basic at all, which is how 65 "-internal" hosts ended up publicly browsable — including
# phillip-jeffries-internal, a showroom line. Override either way with GATE=1 / GATE=0.
HTPASSWD="${HTPASSWD:-/etc/nginx/.htpasswd-dwvendors}"
REALM="${REALM:-Designer Wallcoverings — Trade Lines (Restricted)}"
case "${GATE:-auto}" in
  1) DO_GATE=1 ;;
  0) DO_GATE=0 ;;
  *) case "$DOMAIN" in *-internal.*) DO_GATE=1 ;; *) DO_GATE=0 ;; esac ;;
esac
if [ "$DO_GATE" = 1 ]; then
  ssh "$KAM" "[ -f '$HTPASSWD' ]" || { echo "ABORT: $HTPASSWD missing on $KAM — refusing to deploy an internal host ungated"; exit 1; }
  AUTH_BLOCK="        auth_basic \"$REALM\";
        auth_basic_user_file $HTPASSWD;
"
  echo "  gate: ON  (auth_basic via $HTPASSWD)"
else
  AUTH_BLOCK=""
  echo "  gate: off (public landing)"
fi

echo "→ nginx vhost"
ssh "$KAM" "cat > /etc/nginx/sites-available/$DOMAIN <<NGINX
server {
    listen 80;
    server_name $DOMAIN;
    location / {
$AUTH_BLOCK        proxy_pass http://127.0.0.1:$PORT;
        proxy_set_header Host \\\$host;
        proxy_set_header X-Real-IP \\\$remote_addr;
        proxy_set_header X-Forwarded-For \\\$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \\\$scheme;
    }
}
NGINX
ln -sf /etc/nginx/sites-available/$DOMAIN /etc/nginx/sites-enabled/$DOMAIN
nginx -t && systemctl reload nginx"

echo "→ Let's Encrypt"
ssh "$KAM" "certbot --nginx -d $DOMAIN --non-interactive --agree-tos -m steve@designerwallcoverings.com --redirect || echo 'certbot: ensure DNS A record exists first, then re-run certbot'"

echo "→ smoke test"
ssh "$KAM" "curl -s -o /dev/null -w 'local pm2 → %{http_code}\n' http://127.0.0.1:$PORT/api/config"
if [ "$DO_GATE" = 1 ]; then
  echo "→ gate check (public edge must be 401, not 200)"
  code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "https://$DOMAIN/" 2>/dev/null)
  case "$code" in
    401|403) echo "  OK — $DOMAIN is gated ($code)" ;;
    000)     echo "  (edge not reachable yet — DNS/cert pending; re-check after certbot)" ;;
    *)       echo "  !! WARNING — $DOMAIN returned $code, expected 401. An internal host is PUBLIC." ;;
  esac
fi
echo "DONE. Ensure Cloudflare DNS: A $DOMAIN → 45.61.58.125 (proxied)."