Add settings page with CSV export, SQL dump export/import

New /settings page (linked from the header's burger menu) offers:
- CSV export of the user's links, for spreadsheet apps.
- SQL dump export as INSERT INTO links (...) statements, scoped to
  the current user only — never a raw full-database dump, since that
  would leak other accounts' password hashes and data. Embeddings are
  excluded (regenerated via "KI neu beschreiben lassen" if needed).
- SQL import that re-adds a previously exported dump to the current
  account (additive, doesn't touch existing links).

Import safety (app/backup.py): uploaded SQL is never executed against
the real database. Each non-comment line is required to start with
"insert into links" and is run one statement at a time against an
isolated in-memory SQLite database with only a whitelisted `links`
schema (no id/user_id columns) — sqlite3.execute() also rejects
multiple statements per call. Only after that succeeds are rows
copied into the real DB via the ORM, with user_id forced to the
logged-in user. Verified this rejects DROP TABLE, ATTACH DATABASE,
stacked statements, cross-table subqueries, and user_id injection.
Upload is capped at 2 MB.

Also adds download/upload icons and a proper file-input styling
pattern (visually-hidden input + <label> trigger + filename readout),
since the browser's ::file-selector-button pseudo-element didn't
render reliably in testing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik Thiele
2026-07-20 23:25:27 +02:00
parent 4f99d51f9e
commit 7262aee63d
6 changed files with 336 additions and 7 deletions

View File

@@ -59,6 +59,15 @@
background: var(--panel2); border: 1px solid var(--border); color: var(--text);
border-radius: 8px; padding: 10px 12px; font-size: .95rem; width: 100%;
}
.file-input {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
.file-label {
display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
white-space: nowrap;
}
.file-label:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
input:focus, select:focus { outline: none; border-color: var(--accent); }
select {
background: var(--panel2); border: 1px solid var(--border); color: var(--text);