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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user