#!/bin/bash
#
# Encrypted PostgreSQL backup for one install.
#
# Dumps the database, compresses it, and encrypts it with AES-256 before it
# ever touches the disk. The dump is streamed through the pipeline, so an
# unencrypted copy is never written anywhere, not even briefly.
#
# The passphrase lives in /root/.backup-pass, readable only by root, and must
# also be kept somewhere off this server. Without it the backups cannot be
# restored by anyone, including you.
#
# ── One script, many installs ────────────────────────────────────────────────
#
# This used to hardcode one site's database name, user and status path, so a
# second install could not use it and every new tenant silently had no nightly
# backup at all. The panel showed "Unknown, not reporting" and that was the
# only sign.
#
# It now takes the system user and derives everything from the convention
# ft-tenant-install.sh uses: <user>_db owned by <user>_app. Override either
# with an environment variable for an install that predates the convention.
#
# Usage:
#   ft-backup.sh <sysuser>
#
#   ft-backup.sh mostafaei
#   DB_USER=legacyname ft-backup.sh mysite    # install predating the convention
#
# Install, once per server:
#   chmod 700 /root/ft-backup.sh
#   openssl rand -base64 32 > /root/.backup-pass && chmod 600 /root/.backup-pass
#   # copy the contents of that file into your password manager, then one
#   # cron line per install, staggered so they do not all dump at once:
#   0 3 * * * /root/ft-backup.sh sitea >> /var/log/ft-backup.log 2>&1
#   20 3 * * * /root/ft-backup.sh siteb >> /var/log/ft-backup.log 2>&1
#
# Or use ft-backup-all.sh, which finds every install and needs one cron line.
#
# One passphrase covers every install on the box. That is a deliberate
# trade: separate passphrases would contain a leak to one client, but it is
# also four more secrets to lose, and losing one loses that client's history
# permanently. Set PASS_FILE per site if you would rather have the isolation.
#
set -euo pipefail

SYSUSER="${1:?usage: ft-backup.sh <sysuser>}"

DB_NAME="${DB_NAME:-${SYSUSER}_db}"
DB_USER="${DB_USER:-${SYSUSER}_app}"
BACKUP_DIR="${BACKUP_DIR:-/root/backups/${SYSUSER}}"
PASS_FILE="${PASS_FILE:-/root/.backup-pass}"
KEEP_DAYS="${KEEP_DAYS:-30}"

# Where the panel looks. /root/backups is mode 700 and the site user cannot
# read it, which is correct and must stay that way, so the panel cannot see
# whether a backup ran. This one file is the bridge: it is written into the
# app's own logs directory, world readable, and carries no path to the
# passphrase and nothing that helps anyone decrypt anything.
STATUS_FILE="${STATUS_FILE:-/home/${SYSUSER}/public_html/backend/logs/backup-status.json}"

write_status() {
    # $1 ok|failed   $2 note   $3 size in bytes   $4 how many kept
    local dir; dir="$(dirname "$STATUS_FILE")"
    [ -d "$dir" ] || return 0
    cat > "$STATUS_FILE" <<JSON
{"at": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')", "result": "$1", "note": "$2", "bytes": ${3:-0}, "kept": ${4:-0}}
JSON
    chmod 644 "$STATUS_FILE" 2>/dev/null || true
    # Written as root into a directory the site user owns. Without this the
    # file is root:root and gunicorn cannot read it, so the card keeps saying
    # "Unknown" while the backup runs perfectly every night. Same fault that
    # kept paneltest restarting 59,269 times over one log file.
    chown "${SYSUSER}:${SYSUSER}" "$STATUS_FILE" 2>/dev/null || true
}

# Any exit before the end is a failure, and the panel must hear about it.
# Without this the card would show yesterday's success forever while the job
# died at the first step every night, which is exactly how the ticket fetcher
# stayed broken for months.
trap 'write_status failed "exited early at line $LINENO" 0 0' ERR

STAMP=$(date +%F-%H%M)
TARGET="${BACKUP_DIR}/${DB_NAME}-${STAMP}.sql.gz.enc"

if [ ! -f "$PASS_FILE" ]; then
    echo "$(date '+%F %T') ERROR: $PASS_FILE is missing. Refusing to write an unencrypted backup."
    write_status failed "passphrase file missing" 0 0
    exit 1
fi

mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"

# sudo -u postgres starts in root's home, which the postgres user cannot enter,
# and prints a harmless "could not change directory" warning every run. Work
# from a directory it can read so the logs stay clean.
#
# This has to come before the first psql call, not just before pg_dump, or the
# warning still appears once per site and makes a clean run look broken.
cd /tmp

# A typo in the user name would otherwise produce a confident backup of a
# database that does not exist.
if ! sudo -u postgres psql -lqt | cut -d'|' -f1 | grep -qw "$DB_NAME"; then
    echo "$(date '+%F %T') ERROR: database $DB_NAME does not exist."
    write_status failed "database ${DB_NAME} not found" 0 0
    exit 1
fi

# Dump, compress and encrypt in one stream. If any stage fails the whole
# pipeline fails, thanks to `set -o pipefail`, so a truncated backup is never
# left behind looking healthy.
sudo -u postgres pg_dump "$DB_NAME" \
  | gzip -9 \
  | openssl enc -aes-256-cbc -pbkdf2 -iter 200000 -salt \
        -pass "file:${PASS_FILE}" \
        -out "$TARGET"

chmod 600 "$TARGET"

SIZE=$(du -h "$TARGET" | cut -f1)
echo "$(date '+%F %T') OK: $TARGET ($SIZE)"

# A backup that cannot be read is not a backup. Verify the file decrypts and
# gunzips, without writing the plain text anywhere.
if openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
       -pass "file:${PASS_FILE}" -in "$TARGET" 2>/dev/null \
     | gunzip -t 2>/dev/null; then
    echo "$(date '+%F %T') Verified: decrypts and unpacks cleanly."
else
    echo "$(date '+%F %T') ERROR: $TARGET failed verification. Investigate before trusting it."
    write_status failed "wrote but failed verification" 0 0
    exit 1
fi

# Retention
DELETED=$(find "$BACKUP_DIR" -name "${DB_NAME}-*.sql.gz.enc" -mtime "+${KEEP_DAYS}" -print -delete | wc -l)
if [ "$DELETED" -gt 0 ]; then
    echo "$(date '+%F %T') Removed $DELETED backup(s) older than ${KEEP_DAYS} days."
fi

KEPT=$(find "$BACKUP_DIR" -name "${DB_NAME}-*.sql.gz.enc" -type f | wc -l)
BYTES=$(stat -c %s "$TARGET" 2>/dev/null || echo 0)
trap - ERR
write_status ok "verified" "$BYTES" "$KEPT"

#
# To restore:
#
#   openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
#       -pass file:/root/.backup-pass \
#       -in /root/backups/<user>/<user>_db-YYYY-MM-DD-HHMM.sql.gz.enc \
#     | gunzip \
#     | sudo -u postgres psql <user>_db
#
