Email-code sign-in for admins only; Overview with a needs-attention list; Songs with search, playback, editing, approve/hide and bulk edit; Albums and Singers with merge; creator applications queue. Adds the admin views, functions and creator_applications table, a deploy-admin script and a how-to guide. The admin nginx config no longer caches unhashed JS and sends its security headers again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
207 lines
8.5 KiB
Bash
207 lines
8.5 KiB
Bash
#!/bin/sh
|
|
# One-command deploy of the Nanga Kathai backend. Run it again any time to apply
|
|
# updates (new migrations, config changes); existing secrets are kept.
|
|
#
|
|
# sh deploy.sh app.nkbmusic.com you@example.com
|
|
#
|
|
# Arguments: <domain> [admin-email]
|
|
#
|
|
# Two modes (NK_PROXY):
|
|
# external (default) The server already has a web server (nginx, HestiaCP...).
|
|
# Supabase listens on 127.0.0.1:18000 and you point the
|
|
# domain's reverse proxy at it. Needs docker + git, no sudo.
|
|
# caddy Fresh server with nothing on ports 80/443. Run with sudo:
|
|
# installs Docker, a firewall, and Caddy gets HTTPS itself.
|
|
#
|
|
# Optional environment:
|
|
# REPO_URL git URL of this repository: cloned (or pulled) and run from there
|
|
# NK_DIR install folder (default ~/nk-backend, /opt/nk-backend with caddy)
|
|
# API_GW_HTTP_PORT local port for the reverse proxy (default 18000)
|
|
# SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASS SMTP_ADMIN_EMAIL
|
|
# email for sign-in codes; asked for if not set
|
|
set -eu
|
|
|
|
DOMAIN="${1:-}"
|
|
ADMIN_EMAIL="${2:-}"
|
|
export NK_PROXY="${NK_PROXY:-external}"
|
|
export API_GW_HTTP_PORT="${API_GW_HTTP_PORT:-18000}"
|
|
|
|
say() { printf '\n==> %s\n' "$*"; }
|
|
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
|
|
|
|
[ -n "$DOMAIN" ] || die "usage: sh deploy.sh <domain> [admin-email]"
|
|
|
|
if [ "$NK_PROXY" = caddy ]; then
|
|
[ "$(id -u)" = 0 ] || die "caddy mode installs packages: run with sudo"
|
|
NK_DIR="${NK_DIR:-/opt/nk-backend}"
|
|
BACKUPS="/opt/nk-backups"
|
|
API="https://$DOMAIN"
|
|
else
|
|
NK_DIR="${NK_DIR:-$HOME/nk-backend}"
|
|
BACKUPS="$HOME/nk-backups"
|
|
API="http://127.0.0.1:$API_GW_HTTP_PORT"
|
|
fi
|
|
PROJECT="$NK_DIR/supabase-project"
|
|
|
|
# --- 1. System (caddy mode only) ---------------------------------------------
|
|
if [ "$NK_PROXY" = caddy ]; then
|
|
say "Installing system packages"
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update -qq
|
|
apt-get install -y -qq git curl ca-certificates dnsutils ufw cron >/dev/null
|
|
command -v docker >/dev/null 2>&1 || curl -fsSL https://get.docker.com | sh >/dev/null
|
|
systemctl enable --now docker >/dev/null 2>&1 || true
|
|
|
|
say "Firewall: allowing SSH, 80 and 443 only"
|
|
SSH_PORT="$(ss -tlnpH 2>/dev/null | awk '/sshd/ {sub(/.*:/, "", $4); print $4; exit}')"
|
|
for rule in "${SSH_PORT:-22}/tcp" 22/tcp 80/tcp 443/tcp 443/udp; do ufw allow "$rule" >/dev/null; done
|
|
ufw --force enable >/dev/null
|
|
fi
|
|
for cmd in git curl docker; do
|
|
command -v "$cmd" >/dev/null 2>&1 || die "'$cmd' is not installed"
|
|
done
|
|
docker compose version >/dev/null 2>&1 || die "docker compose plugin missing"
|
|
docker ps >/dev/null 2>&1 || die "can't use docker: add this user to the docker group"
|
|
|
|
# --- 2. Code -----------------------------------------------------------------
|
|
if [ -n "${REPO_URL:-}" ] && [ "${NK_FROM_CLONE:-}" != 1 ]; then
|
|
CODE="${NK_CODE_DIR:-$(dirname "$NK_DIR")/nanga_kathai}"
|
|
say "Fetching the code from $REPO_URL into $CODE"
|
|
if [ -d "$CODE/.git" ]; then git -C "$CODE" pull --ff-only --quiet
|
|
else git clone --quiet "$REPO_URL" "$CODE"; fi
|
|
NK_FROM_CLONE=1 exec sh "$CODE/server/deploy.sh" "$@"
|
|
fi
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
[ -d "$HERE/../supabase/migrations" ] || die "can't find supabase/migrations next to server/"
|
|
|
|
# --- 3. Port / DNS checks ----------------------------------------------------
|
|
if [ ! -f "$PROJECT/.env" ] && [ "$NK_PROXY" != caddy ] \
|
|
&& ss -tln | grep -qE "[:.]$API_GW_HTTP_PORT\b"; then
|
|
die "port $API_GW_HTTP_PORT is already in use; set API_GW_HTTP_PORT to a free port"
|
|
fi
|
|
DNS_IP="$(getent ahostsv4 "$DOMAIN" | awk 'NR==1 {print $1}')"
|
|
[ -n "$DNS_IP" ] || echo "Warning: $DOMAIN has no DNS record yet."
|
|
|
|
# --- 4. Supabase + NK config -------------------------------------------------
|
|
if [ ! -f "$PROJECT/.env" ]; then
|
|
say "Installing Supabase (first run) into $PROJECT"
|
|
sh "$HERE/install.sh" "$DOMAIN" "$NK_DIR"
|
|
else
|
|
say "Existing install found: refreshing the NK config"
|
|
cp -r "$HERE/caddy" "$HERE/email-templates" "$HERE/sql" "$HERE/nginx" "$PROJECT/nk/"
|
|
cp "$HERE/docker-compose.nk.yml" "$HERE/docker-compose.nk-caddy.yml" "$PROJECT/"
|
|
fi
|
|
cd "$PROJECT"
|
|
|
|
# Admin website: the placeholder until a real build has been published (admin/deploy-admin.sh).
|
|
if [ ! -f nk/admin/index.html ]; then
|
|
mkdir -p nk/admin
|
|
cp "$HERE/admin-placeholder/index.html" nk/admin/
|
|
fi
|
|
|
|
env_get() { grep -E "^$1=" .env | head -n1 | cut -d= -f2-; }
|
|
env_set() { # safe for any characters in the value
|
|
K="$1" V="$2" awk 'BEGIN { k = ENVIRON["K"]; v = ENVIRON["V"]; done = 0 }
|
|
index($0, k "=") == 1 { print k "=" v; done = 1; next } { print }
|
|
END { if (!done) print k "=" v }' .env > .env.tmp
|
|
cat .env.tmp > .env && rm .env.tmp
|
|
}
|
|
|
|
# --- 5. Email (sign-in codes) ------------------------------------------------
|
|
ask() { # ask VAR "question" [default]
|
|
eval "cur=\${$1:-}"
|
|
if [ -z "$cur" ] && [ -t 0 ]; then
|
|
printf '%s%s: ' "$2" "${3:+ [$3]}"; read -r cur
|
|
[ -n "$cur" ] || cur="${3:-}"
|
|
fi
|
|
eval "$1=\$cur"
|
|
}
|
|
if [ "$(env_get SMTP_HOST)" = "supabase-mail" ] || [ -n "${SMTP_HOST:-}" ]; then
|
|
say "Email settings for sign-in codes (Enter on SMTP host to skip for now)"
|
|
ask SMTP_HOST "SMTP host"
|
|
if [ -n "$SMTP_HOST" ]; then
|
|
ask SMTP_PORT "SMTP port" 587
|
|
ask SMTP_USER "SMTP user" "no-reply@${DOMAIN#*.}"
|
|
ask SMTP_PASS "SMTP password"
|
|
ask SMTP_ADMIN_EMAIL "Send codes from" "$SMTP_USER"
|
|
env_set SMTP_HOST "$SMTP_HOST"
|
|
env_set SMTP_PORT "$SMTP_PORT"
|
|
env_set SMTP_USER "$SMTP_USER"
|
|
env_set SMTP_PASS "$SMTP_PASS"
|
|
env_set SMTP_ADMIN_EMAIL "$SMTP_ADMIN_EMAIL"
|
|
env_set SMTP_SENDER_NAME "Nanga Kathai"
|
|
else
|
|
echo "Skipped: sign-in emails won't send until you run deploy.sh again with SMTP details."
|
|
fi
|
|
fi
|
|
|
|
# --- 6. Start ----------------------------------------------------------------
|
|
say "Starting the backend (first start takes a few minutes)"
|
|
sh run.sh start
|
|
|
|
# --- 7. Database -------------------------------------------------------------
|
|
say "Creating tables, access rules and storage buckets"
|
|
sh "$HERE/apply-migrations.sh" "$PROJECT"
|
|
|
|
# --- 8. API check ------------------------------------------------------------
|
|
say "Checking the API at $API"
|
|
n=0
|
|
until curl -fsS -o /dev/null "$API/auth/v1/health" -H "apikey: $(env_get ANON_KEY)"; do
|
|
n=$((n + 1))
|
|
[ $n -lt 30 ] || { echo "API not answering (see: sh run.sh logs api-gw)"; break; }
|
|
sleep 5
|
|
done
|
|
|
|
# --- 9. Admin account --------------------------------------------------------
|
|
if [ -n "$ADMIN_EMAIL" ]; then
|
|
say "Making $ADMIN_EMAIL an admin"
|
|
SERVICE_KEY="$(env_get SERVICE_ROLE_KEY)"
|
|
curl -sS -o /dev/null -X POST "$API/auth/v1/admin/users" \
|
|
-H "apikey: $SERVICE_KEY" -H "Authorization: Bearer $SERVICE_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"email\": \"$ADMIN_EMAIL\", \"email_confirm\": true}" || true # 422 = already exists
|
|
docker compose exec -T -e PGPASSWORD="$(env_get POSTGRES_PASSWORD)" db \
|
|
psql -h localhost -U postgres -d postgres -qtA -c \
|
|
"update public.profiles set role = 'admin'
|
|
where id = (select id from auth.users where email = lower('$ADMIN_EMAIL'))
|
|
returning 'admin set';" | grep -q 'admin set' \
|
|
|| echo "Couldn't set admin yet. Sign in once from the app, then run deploy.sh again."
|
|
fi
|
|
|
|
# --- 10. Backups -------------------------------------------------------------
|
|
say "Scheduling nightly backups to $BACKUPS"
|
|
mkdir -p "$BACKUPS"
|
|
CRON="30 2 * * * sh $HERE/backup.sh $PROJECT $BACKUPS >> $BACKUPS/backup.log 2>&1"
|
|
( crontab -l 2>/dev/null | grep -v 'nk-backups' || true; echo "$CRON" ) | crontab -
|
|
|
|
# --- Done --------------------------------------------------------------------
|
|
PROXY_NOTE=""
|
|
[ "$NK_PROXY" = caddy ] || PROXY_NOTE="
|
|
Reverse proxy (HestiaCP: Web > domain > Proxy, or a custom nginx proxy template):
|
|
https://$DOMAIN -> http://127.0.0.1:$API_GW_HTTP_PORT (API + Studio)
|
|
https://admin.${DOMAIN#*.} -> http://127.0.0.1:${NK_ADMIN_PORT:-18080} (NK Admin)
|
|
"
|
|
cat <<EOF
|
|
|
|
========================================================================
|
|
Nanga Kathai backend is running.
|
|
$PROXY_NOTE
|
|
Admin dashboard : https://$DOMAIN
|
|
user : $(env_get DASHBOARD_USERNAME)
|
|
password : $(env_get DASHBOARD_PASSWORD)
|
|
|
|
App env.json (safe to put in the app):
|
|
{
|
|
"SUPABASE_URL": "https://$DOMAIN",
|
|
"SUPABASE_PUBLISHABLE_KEY": "$(env_get SUPABASE_PUBLISHABLE_KEY)",
|
|
"MEDIA_BASE_URL": "https://$DOMAIN/storage/v1/object/public/audio"
|
|
}
|
|
|
|
For the song importer only (keep secret, never in the app):
|
|
NK_URL=https://$DOMAIN
|
|
NK_SERVICE_KEY=$(env_get SERVICE_ROLE_KEY)
|
|
|
|
Secrets live in $PROJECT/.env. Copy $BACKUPS off the server regularly.
|
|
========================================================================
|
|
EOF
|