Complete Phase 3, Phase 5 polish and hardening

Phase 3 — Investments:
- Multi-currency support: holdings track purchase currency, FX rates convert to base for totals
- Capital gains report using UK Section 104 pool method, grouped by tax year
- Capital Gains tab added to Reports page

Phase 5 — Polish & Hardening:
- Mobile-responsive layout: bottom nav, sidebar hidden on mobile, logo in TopBar, compact header buttons, hover-only actions now always visible on touch
- Backup system: encrypted GPG backups via backup.sh, nightly scheduler job, admin API (list/trigger/download/restore), Settings UI with drag-to-restore confirmation
- Docker entrypoint with gosu privilege drop to fix bind-mount ownership on fresh deployments
- OWASP fixes: refresh token now bound to its session (new refresh_token_hash column + migration), CSRF secure flag tied to environment, IP-level rate limiting on login, TOTPEnableRequest Pydantic schema replaces raw dict
- AES-256-GCM key rotation script (rotate_keys.py) with dry-run mode and atomic DB transaction
- CLAUDE.md added for AI-assisted development context
- README updated: correct reverse proxy port, accurate backup/restore commands, key rotation instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-04-22 14:59:11 +00:00
parent 74e57a35c0
commit fe4e69b9ad
40 changed files with 2079 additions and 127 deletions

View file

@ -80,7 +80,7 @@ Ten independent security layers:
3. **Security headers** — CSP, HSTS, X-Frame-Options DENY, X-Content-Type-Options, form-action, Permissions-Policy
4. **CSRF** — double-submit cookie on all mutating endpoints
5. **Authentication** — Argon2id password hashing; RS256 JWT (15-min access + 7-day HttpOnly refresh cookie); TOTP (RFC 6238) with backup codes; brute-force lockout (exponential backoff via Redis)
6. **Rate limiting** — Redis sliding window: 10/min on auth, 10/min on TOTP, 20/min on predictions, 300/min general API
6. **Rate limiting** — Redis sliding window: 20/min on login (per IP), 10/min on TOTP, 20/min on predictions, 300/min general API
7. **Field-level encryption** — AES-256-GCM on all PII fields (account names, transaction descriptions, merchant, notes, TOTP secret); IV ‖ ciphertext ‖ tag stored in `bytea`
8. **Database encryption** — pgcrypto as a second layer on backup dumps
9. **Row-level security** — PostgreSQL RLS policies enforce user isolation at the DB layer; buggy queries cannot leak another user's data
@ -168,44 +168,62 @@ docker compose up -d backend
### 5. Point your reverse proxy
Forward your domain to `http://<host>:8090`. The frontend is served from port `4000` internally but the backend proxies frontend assets — point everything at `8090`.
Forward your domain to `http://<host>:4000`. The frontend nginx serves the React app and proxies all `/api/` requests to the backend internally.
---
## Backups
Encrypted nightly backup runs automatically at 3 AM:
Encrypted backups run automatically every night at 3 AM (GPG AES-256 symmetric encryption). Backups are stored in `./data/backups/` and retained for 30 days.
**Via the web UI:** Settings → Backups — trigger a manual backup, download a copy, or restore from any listed backup file.
**Manual backup from the command line:**
```bash
# Manual backup
docker compose exec -e BACKUP_PASSPHRASE="$BACKUP_PASSPHRASE" backend bash scripts/backup.sh
# List backups
docker compose exec backend bash scripts/restore.sh --list
# Restore
docker compose exec -e BACKUP_PASSPHRASE="$BACKUP_PASSPHRASE" \
-e DATABASE_URL="$DATABASE_URL" \
backend bash scripts/restore.sh 20240101_030000.sql.gz.gpg
docker compose exec backend bash /app/scripts/backup.sh
```
Backups are stored in `./data/backups/` and retained for 30 days.
**Manual restore** (stop the backend first to avoid lock contention):
```bash
docker compose stop backend
docker compose run --rm backend bash -c '
export GNUPGHOME=/tmp/.gnupg; mkdir -p $GNUPGHOME && chmod 700 $GNUPGHOME
PG_URL=${DATABASE_URL/postgresql+asyncpg/postgresql}
gpg --batch --yes --no-symkey-cache --pinentry-mode loopback \
--passphrase "$BACKUP_PASSPHRASE" \
--decrypt $(ls /app/backups/*.sql.gz.gpg | tail -1) \
| gunzip | psql "$PG_URL"
'
docker compose start backend
```
## Key Rotation
To rotate the AES-256-GCM encryption key without data loss:
```bash
# Generate new key
# Generate a new key
NEW_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
echo "New key: $NEW_KEY"
# Stop the app, rotate, update .env, restart
docker compose stop backend
NEW_ENCRYPTION_KEY="$NEW_KEY" ./scripts/rotate_keys.sh
# Update ENCRYPTION_KEY in .env to $NEW_KEY
# Dry-run first — validates decryption works, no DB changes
docker compose exec backend python /app/scripts/rotate_keys.py \
--old-key "$ENCRYPTION_KEY" --new-key "$NEW_KEY" --dry-run
# If dry-run passes, rotate for real (atomic — rolls back on any failure)
docker compose exec backend python /app/scripts/rotate_keys.py \
--old-key "$ENCRYPTION_KEY" --new-key "$NEW_KEY"
# Update ENCRYPTION_KEY in .env, then restart
docker compose up -d backend
```
Do not lose your current `ENCRYPTION_KEY` — without it, all encrypted fields are permanently unreadable.
---
## Environment Variables
@ -242,7 +260,7 @@ MyMidas/
│ ├── pages/ # Route-level page components
│ └── store/ # Zustand state (auth, UI/theme)
├── postgres/init/ # PostgreSQL init SQL (extensions, RLS policies)
├── scripts/ # backup.sh, restore.sh, rotate_keys.sh
├── backend/scripts/ # backup.sh, rotate_keys.py, entrypoint.sh
├── secrets/ # JWT keys (git-ignored, generate locally)
└── docker-compose.yml
```