feat(backup): admin UI with admin-chosen target directory
The backup destination is now chosen by the on-site admin in the UI (Setup -> Backup), not a server env var. An env-pinned target defeats the purpose: the admin can't point backups at a freshly-plugged USB or a NAS mount without editing .env and restarting. The encryption key stays a server secret. Target storage: - New site_config.backup_target_dir (migration 0016, nullable; null = not configured). BackupService reads it fresh each run, so a UI change takes effect with no restart. Only BACKUP_KEY stays env -- a key must never live in the DB it backs up. Routes: - PUT /api/backup/config -- set/clear the target (backup:update; upserts id=1). - POST /api/backup/test -- probe a candidate path server-side (exists / is a directory / writable) so the admin gets feedback before relying on it. - status() now exposes targetDir + keyPresent, so the UI distinguishes 'no target set' from 'BACKUP_KEY missing'. UI (apps/web/src/BackupSettings.tsx): - A Setup -> Backup tab (gated backup:read): an editable target-path field with a Test-target probe (localized ok/missing/not-a-dir/not-writable), Save, the status panel (config state, last-run size/pruned/error, a distinct amber missing-key warning), a Back up now button, and the restore-is-out-of-band note. Full i18n (sq + en); nav.backup. - API client: fetchBackupStatus / setBackupTarget / testBackupTarget / runBackup. Also includes a small in-progress copy trim to the setup-intro i18n strings. Verified live with Playwright: typed a path -> Test reported writable -> Save persisted it -> status reflected it and showed the key-missing warning. Whole monorepo build/lint/test green. Wiki backup-recovery + open-question #5 updated. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -15,15 +15,17 @@ JWT_SECRET=
|
||||
# them (keyId), so verifyChain still validates a chain that spans a key change.
|
||||
EVENT_SIGNING_KEY=
|
||||
|
||||
# On-site encrypted DB backup (durability for the signed ledger). Disabled until both
|
||||
# BACKUP_TARGET_DIR and BACKUP_KEY are set. A daily timer + an admin "back up now" button
|
||||
# write a consistent, AES-256-GCM-encrypted copy to the target. RESTORE is an out-of-band
|
||||
# runbook action, not a console call. See wiki/concepts/backup-recovery.md.
|
||||
# BACKUP_TARGET_DIR=/mnt/backup # a mounted local/USB/SATA/SMB/NFS path
|
||||
# On-site encrypted DB backup (durability for the signed ledger). A daily timer + an admin
|
||||
# "back up now" button write a consistent, AES-256-GCM-encrypted copy to the target. The
|
||||
# TARGET DIRECTORY is chosen by the admin in the UI (Setup → Backup) and stored in the DB —
|
||||
# NOT here. Only the encryption KEY is an env secret. RESTORE is an out-of-band runbook action,
|
||||
# not a console call. See wiki/concepts/backup-recovery.md.
|
||||
#
|
||||
# Dedicated backup-encryption key (>=16 chars), SEPARATE from EVENT_SIGNING_KEY so it can
|
||||
# rotate without fracturing the signed chain. Generate with: openssl rand -hex 32
|
||||
# Escrow it offsite (alongside EVENT_SIGNING_KEY) — recovery needs both, and neither is ever
|
||||
# stored inside the backup it unlocks.
|
||||
# stored inside the backup it unlocks. Backups stay a no-op until BOTH this key and an in-UI
|
||||
# target directory are set.
|
||||
# BACKUP_KEY=
|
||||
# BACKUP_KEEP_LAST=7 # keep this many newest backups always
|
||||
# BACKUP_KEEP_DAILY_DAYS=30 # plus one-per-day within this window
|
||||
|
||||
Reference in New Issue
Block a user