#!/usr/bin/env bash # sync-prod-db.sh # # Dumps the production database and restores it to the development database, # then sanitizes the copy to prevent outbound notifications and reduce PII exposure. # # Usage: # Add PROD_DATABASE_URL to your .env file, then: # npm run db:sync-prod # # or pass inline: # PROD_DATABASE_URL="postgres://..." npm run db:sync-prod # # or pass as argument: # bash scripts/sync-prod-db.sh "postgres://..." # # Requirements: pg_dump, pg_restore, psql (PostgreSQL client tools) set -euo pipefail # ── Colour helpers ──────────────────────────────────────────────────────────── RED='\033[0;31m' YELLOW='\033[1;33m' GREEN='\033[0;32m' BOLD='\033[1m' RESET='\033[0m' info() { echo -e "${BOLD}$*${RESET}"; } warn() { echo -e "${YELLOW}$*${RESET}"; } success() { echo -e "${GREEN}$*${RESET}"; } error() { echo -e "${RED}ERROR: $*${RESET}" >&2; exit 1; } # ── Check required tools (fix #5) ──────────────────────────────────────────── for cmd in pg_dump pg_restore psql; do command -v "$cmd" &>/dev/null || error "'$cmd' not found. Install PostgreSQL client tools (e.g. brew install libpq)." done # ── Load .env ───────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="$SCRIPT_DIR/../.env" if [[ -f "$ENV_FILE" ]]; then # Export DATABASE_URL and PROD_DATABASE_URL from .env if not already set in the environment DATABASE_URL_FROM_ENV=$(grep -E '^DATABASE_URL=' "$ENV_FILE" | head -1 | cut -d= -f2- | sed 's/^"//' | sed 's/"$//') if [[ -n "$DATABASE_URL_FROM_ENV" && -z "${DATABASE_URL:-}" ]]; then export DATABASE_URL="$DATABASE_URL_FROM_ENV" fi PROD_DATABASE_URL_FROM_ENV=$(grep -E '^PROD_DATABASE_URL=' "$ENV_FILE" | head -1 | cut -d= -f2- | sed 's/^"//' | sed 's/"$//') if [[ -n "$PROD_DATABASE_URL_FROM_ENV" && -z "${PROD_DATABASE_URL:-}" ]]; then export PROD_DATABASE_URL="$PROD_DATABASE_URL_FROM_ENV" fi fi # ── Resolve URLs ────────────────────────────────────────────────────────────── # PROD_DATABASE_URL: env var or first positional argument PROD_DB="${PROD_DATABASE_URL:-${1:-}}" DEV_DB="${DATABASE_URL:-}" [[ -z "$PROD_DB" ]] && error "PROD_DATABASE_URL is not set.\nSet it as an environment variable or pass it as the first argument:\n PROD_DATABASE_URL=\"postgres://...\" npm run db:sync-prod" [[ -z "$DEV_DB" ]] && error "DATABASE_URL is not set. Add it to your .env file." # Guard against accidentally syncing prod → prod if [[ "$PROD_DB" == "$DEV_DB" ]]; then error "PROD_DATABASE_URL and DATABASE_URL are the same. Aborting to prevent data loss." fi # ── Mask password for display (fix #6) ─────────────────────────────────────── # Replaces only the password portion (between : and @) using sed, not bash globs. mask_url() { echo "$1" | sed 's|\(://[^:]*\):[^@]*@|\1:***@|' } # ── Confirmation prompt ─────────────────────────────────────────────────────── echo "" warn "⚠️ WARNING: This will OVERWRITE your development database with production data." echo "" echo -e " Source (prod): ${BOLD}$(mask_url "$PROD_DB")${RESET}" echo -e " Target (dev): ${BOLD}$(mask_url "$DEV_DB")${RESET}" echo "" warn "After restore, the script will:" echo " • NULL out all Discord webhook URLs (prevents notification leaks)" echo " • Anonymize email/name/avatar for non-admin users (reduces PII exposure)" echo " • Admin accounts are left untouched so you can still access admin pages" echo "" read -r -p "Type 'yes' to continue: " CONFIRM if [[ "$CONFIRM" != "yes" ]]; then echo "Aborted." exit 0 fi # ── Dump ────────────────────────────────────────────────────────────────────── DUMP_FILE=$(mktemp /tmp/brackt_prod_dump_XXXXXX.dump) # Ensure the temp file is cleaned up on exit trap 'rm -f "$DUMP_FILE"' EXIT info "" info "1/3 Dumping production database…" pg_dump \ --no-owner \ --no-acl \ --format=custom \ "$PROD_DB" \ --file="$DUMP_FILE" success " Dump complete: $(du -h "$DUMP_FILE" | cut -f1)" # ── Restore (fix #1, #2, #3) ───────────────────────────────────────────────── # Uses --clean --if-exists to drop and recreate all objects within the existing # database, without needing DROP DATABASE / CREATE DATABASE permissions. # This works on managed cloud databases (Railway, Render, Neon, Supabase, etc.). # # pg_restore exits with code 1 for non-fatal warnings (e.g. "role X does not # exist" with --no-owner), so we capture the exit code and only abort on # unexpected failures, not routine restore warnings. info "" info "2/3 Restoring production dump to development database…" RESTORE_EXIT=0 pg_restore \ --no-owner \ --no-acl \ --clean \ --if-exists \ --dbname="$DEV_DB" \ "$DUMP_FILE" || RESTORE_EXIT=$? if [[ $RESTORE_EXIT -eq 0 ]]; then success " Restore complete." elif [[ $RESTORE_EXIT -eq 1 ]]; then warn " Restore completed with warnings (non-fatal). Continuing…" else error "pg_restore failed with exit code $RESTORE_EXIT. Aborting." fi # ── Sanitize (fix #4) ──────────────────────────────────────────────────────── info "" info "3/3 Sanitizing development database…" SANITIZE_RESULT=$(psql "$DEV_DB" --tuples-only --no-align << 'SQL' BEGIN; -- 1. Count leagues with webhooks before clearing (accurate count) SELECT 'discord_webhooks_nulled:' || COUNT(*) FROM leagues WHERE discord_webhook_url IS NOT NULL; UPDATE leagues SET discord_webhook_url = NULL; -- 2. Anonymize PII for non-admin users only -- (admin accounts are untouched so admin pages remain accessible) UPDATE users SET email = 'user_' || id || '@example.com', display_name = 'Test User', image_url = NULL WHERE is_admin = FALSE; SELECT 'users_anonymized:' || COUNT(*) FROM users WHERE is_admin = FALSE; SELECT 'admin_users_kept:' || COUNT(*) FROM users WHERE is_admin = TRUE; COMMIT; SQL ) LEAGUES_COUNT=$(echo "$SANITIZE_RESULT" | grep 'discord_webhooks_nulled:' | cut -d: -f2 | tr -d ' ') ANON_COUNT=$(echo "$SANITIZE_RESULT" | grep 'users_anonymized:' | cut -d: -f2 | tr -d ' ') ADMIN_COUNT=$(echo "$SANITIZE_RESULT" | grep 'admin_users_kept:' | cut -d: -f2 | tr -d ' ') echo "" success "✓ Sync complete. Sanitization summary:" echo " • Discord webhook URLs cleared: ${LEAGUES_COUNT:-?} leagues" echo " • Non-admin users anonymized: ${ANON_COUNT:-?} users" echo " • Admin accounts preserved: ${ADMIN_COUNT:-?} users" echo "" warn "Reminder: ensure your .env does NOT contain:" echo " • RESEND_API_KEY (email sending)" echo " • SENTRY_AUTH_TOKEN (error tracking)" echo " • Production Clerk keys (use dev Clerk instance)" echo ""