feat: native save + auto-backup + folder customization + attachments + UX bundle

- File: native Save As dialog via Bridge.saveFile (replaces WebView2
  browser download popup) for encrypted JSON + CSV exports.
- Auto-backup: silent periodic encrypted JSON to a chosen folder,
  user-set interval + retention, separate DPAPI-stored password, runs
  5s after unlock if due. New file/* bridge cmds (folder/pick,
  file/write, file/listMatch, file/delete).
- Folders: per-folder color + icon (8-swatch palette, 8 icon presets),
  drag-reorder via HTML5 DnD with insert-line indicators, edit pencil
  on hover. New POST /folders/reorder + PUT /folders/{name}. Folder
  chip on cards inherits custom icon + color.
- Recently used: vault_entries.accessed_at + POST /entries/{id}/touch
  (debounced 2s), sidebar Tools entry showing top-10 by accessed_at.
- Encrypted attachments: per-entry file storage (5MB cap), AES-GCM
  with vault key, native Save As download, paperclip upload in
  slideover. New entry_attachments table + PM.Handler.Attachments.
- Password expiry: vault_entries.password_changed_at (conditional bump
  via SQL CASE only when ciphertext differs), passwordExpiryDays
  setting, "Aged" badge on cards + matching Filters chip.
- Recovery: Print button on generated code modal (A4 printable sheet
  via @media print, code in 32px monospace + instructions).
- Audit log viewer (sidebar Tools, GET /audit with pagination cursor).
- Plaintext CSV export + Filters dropdown with 9 predicates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 23:12:06 +01:00
parent 63fac5b3b7
commit fa7ea191be
13 changed files with 3927 additions and 1378 deletions
+52
View File
@@ -384,6 +384,58 @@ Historiquement on a oublié `kind` dans `duplicateEntry` (bug "Site required"
sur duplique-note), et `custom_fields` dans la rotation + duplicate. Cette
liste évite de répéter ces erreurs.
## Encrypted attachments
Per-entry file storage (PDFs, images of backup codes, etc.) encrypted
client-side with the vault key.
- **Table** : `entry_attachments` (id, user_id, entry_id, filename, mime,
size_bytes, encrypted_blob TEXT base64, iv, created_at). FK cascade on
user + entry delete.
- **Endpoints** ([PM.Handler.Attachments.pas](delphi-backend/Handlers/PM.Handler.Attachments.pas)) :
- `GET /entries/{id}/attachments` → metadata array (no blob)
- `POST /entries/{id}/attachments` → full upload, ciphertext capped at
~10 MB base64 (~7.5 MB raw)
- `GET /attachments/{id}` → metadata + blob (fetched on Download click)
- `DELETE /attachments/{id}` → permanent (no trash)
- **Crypto** : `encryptBlobBytes(uint8)` / `decryptBlobBytes(b64, iv)`
use the same AES-GCM 256 + `state.cryptoKey` as passwords. Filename,
mime, size are stored in cleartext (leaked metadata) so the listing
doesn't have to decrypt all rows on slideover-open.
- **Cap** : 5 MB raw client-side check, ~10 MB base64 server-side.
- **Master pw rotation** : attachments are NOT re-encrypted on rotation
→ they become inaccessible. Known limitation, document for users who
rotate master pw (rotate before adding heavy attachments).
- **UI** : `soAttachmentsField(entryId)` rendered in slideover (existing
entries only, never on new). Upload via hidden file input + paperclip
button. Download reuses `Bridge.saveFile` (native Save As dialog).
## Auto-backup (encrypted JSON, silent)
Silent periodic encrypted export. Triggered on unlock (5s defer) if
`autoBackupInterval` days écoulés depuis `autoBackupLast`. Bouton
"Backup now" dans Settings pour trigger manuel.
- **State** (DPAPI prefs via `Bridge.getPref/setPref`) — survivent au
port-change : `autoBackupEnabled`, `autoBackupDir`, `autoBackupInterval`,
`autoBackupKeep`, `autoBackupLast` (ISO), `autoBackupPwd` (prompté une
fois à l'enable).
- **Pwd** : indépendant du master pw, choisi par l'user au premier toggle.
Stocké DPAPI, utilisé silencieusement à chaque run. User le retape pour
restaurer via l'import standard. **Pourquoi pas dérivé du cryptoKey** :
master pw rotation re-génère cryptoKey → backups antérieurs deviennent
inaccessibles. Pwd séparée découple du cycle de vie de la vault key.
- **Filename** : `vault-autobackup-yyyymmdd-HHmmss.json` — sort lexical
= chronologique pour la rétention. Container = même format que
`doExport` user-driven → restore via "Import vault" classique.
- **Bridge cmds** ([UMainForm.pas](delphi-backend/UMainForm.pas)) :
- `folder/pick``SelectDirectory` FMX, callback `Bridge.onFolderPickResult(reqId, path)`
- `file/write?path=&data=<b64>` → silent write (no dialog)
- `file/listMatch?dir=&prefix=` → JSON `[{name,size,mtime}]`
- `file/delete?path=` → single delete
- **Retention** : après chaque write OK, list dir + sort name desc,
delete au-delà de `keep`. Best-effort.
## Settings sync
Per-user blob JSON dans `users.settings_json`, exposé via `GET/PUT