fix(desktop): Downloads aus der Seite (blob/data) in der App speichern
Tessera CI/CD / Lint & Type Check (push) Successful in 2m38s
Tessera CI/CD / Tests (push) Failing after 1m26s
Tessera CI/CD / Desktop-Pakete bauen (push) Has been skipped
Tessera CI/CD / Build & Publish Images (push) Has been skipped

Der Client reichte jeden Download an den System-Browser weiter. Dateien,
die die Seite selbst erzeugt (blob:/data:, z. B. alle Downloads im
Zertifikat-Manager), kann der Browser nicht abrufen - Windows zeigte
nur "Holen Sie sich eine App, um diesen blob-Link zu oeffnen" (VM 8233).
Solche Downloads speichert die App jetzt selbst (Ordner Downloads) und
meldet Dateiname und Ordner; http/https-Downloads gehen weiter an den
Browser.

Dazu CHANGELOG und Quick-Doku zu 261001-l4q.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-01 15:28:59 +02:00
parent c07b0cfaf0
commit dfc4e9b781
5 changed files with 124 additions and 4 deletions
+1
View File
@@ -486,6 +486,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
| 261001-cxo | Desktop-Client: Links mit target=_blank (Favoriten) oeffnen jetzt im System-Browser (DesktopExternalLinks -> window.open) | 2026-10-01 | 61a971c | [261001-cxo](./quick/261001-cxo-desktop-client-links-mit-target-blank-oe/) | | 261001-cxo | Desktop-Client: Links mit target=_blank (Favoriten) oeffnen jetzt im System-Browser (DesktopExternalLinks -> window.open) | 2026-10-01 | 61a971c | [261001-cxo](./quick/261001-cxo-desktop-client-links-mit-target-blank-oe/) |
| 261001-g68 | Erinnerung: Cursor sprang beim Schreiben der Beschreibung in den Titel (Fokus-Effekt hing an inline onClose, Kachel zeichnet alle 10 s neu) – Fokus nur beim Oeffnen | 2026-10-01 | siehe git log | [261001-g68](./quick/261001-g68-erinnerung-cursor-springt-aus-beschreibu/) | | 261001-g68 | Erinnerung: Cursor sprang beim Schreiben der Beschreibung in den Titel (Fokus-Effekt hing an inline onClose, Kachel zeichnet alle 10 s neu) – Fokus nur beim Oeffnen | 2026-10-01 | siehe git log | [261001-g68](./quick/261001-g68-erinnerung-cursor-springt-aus-beschreibu/) |
| 261001-hbi | Favoriten: Logo fuer per JavaScript gesetzte Symbole (hosteurope.de) – Rueckfall auf DuckDuckGo-Symboldienst beim Ausliefern, nur oeffentliche Seiten | 2026-10-01 | siehe git log | [261001-hbi](./quick/261001-hbi-favoriten-logo-fuer-per-javascript-geset/) | | 261001-hbi | Favoriten: Logo fuer per JavaScript gesetzte Symbole (hosteurope.de) – Rueckfall auf DuckDuckGo-Symboldienst beim Ausliefern, nur oeffentliche Seiten | 2026-10-01 | siehe git log | [261001-hbi](./quick/261001-hbi-favoriten-logo-fuer-per-javascript-geset/) |
| 261001-l4q | Zertifikat-Manager: Reiter Übersicht (Paket/ZIP hochladen, Teile erkennen/zuordnen, jedes Teil in jedem Format) + Desktop speichert blob-Downloads selbst | 2026-10-01 | siehe git log | [261001-l4q](./quick/261001-l4q-zertifikatsmodul-paket-hochladen-uebersi/) |
## Deferred Items ## Deferred Items
@@ -0,0 +1,16 @@
---
quick_id: 261001-l4q
description: "Zertifikatsmodul: Paket hochladen, Uebersicht, Download in jedem Format"
date: 2026-10-01
---
# Zertifikatsmodul: Paket hochladen, Uebersicht, Download in jedem Format
**Auftrag (User):** Testdatei = ZIP vom Aussteller (pem mit Server+Zwischen, key, csr, pfx mit unbekanntem Passwort, .dnstxtrecord). Nach dem Hochladen soll angezeigt werden, welches Zertifikat was ist, darunter jedes Zertifikat in jedem Format herunterladbar.
**Befund vorher:** Modul nimmt nur EINE Datei; .key/.csr/.zip nicht waehlbar; keine Uebersicht; Labels teils englisch; Texte duzen.
## Tasks
1. API `cert-bundle.ts`: `analyzeBundle` (Dateien + ZIP mit Grenzen vor dem Entpacken; PEM/DER/PFX/P7B; Duplikate per SHA-256/Modulus; Schluessel/CSR-Zuordnung; Kette) und `exportBundleItem` (crt, cer, fullchain, p7b, pfx inkl. Schluessel+Kette; key PKCS#8/PKCS#1/DER; csr PEM/DER). Endpunkte POST analyze / export.
2. Web: Reiter „Übersicht“ (Standard), Mehrfach-Ablage, Karten je Teil mit Erklaerung, Status, Zuordnung, Download-Knoepfen, PFX-Passwort; geschuetzte PFX entsperren. Texte de/en, Sie-Form.
3. Desktop: blob:/data:-Downloads in der App speichern (Downloads-Ordner) + Meldung; vorher landete der Klick als „blob-Link“ im System-Browser (VM gemessen).
@@ -0,0 +1,11 @@
---
quick_id: 261001-l4q
status: complete
date: 2026-10-01
---
# Summary
- Echte Aussteller-ZIP (nur lokal, nicht im Repo): 4 Teile erkannt (Server, Zwischen, Schluessel→Server, CSR→Server), PFX als geschuetzt gemeldet, .dnstxtrecord als nicht verwendet; alle 15 Exporte mit OpenSSL gueltig (PFX mit 3 Bloecken, Schluessel-Modulus passt).
- Tests: API cert-bundle.spec 11 (selbst erzeugte PKI), Web OverviewTab.test 5 + Seitentest angepasst; gesamt Web 1189, API 1686, Rust 65 gruen; tsc/clippy/rustfmt sauber.
- Browser lokal: Uebersicht + Downloads (fullchain, pfx, rsa.key, csr.der) geprueft.
- Desktop-Client (VM, lokal): Upload/Anzeige ok; Download zeigte „blob-Link“-Fehler → Client-Fix (on_download speichert blob:/data: selbst, Meldung danach). Nachweis auf VM folgt mit neuem Client-Build.
+9
View File
@@ -4,6 +4,15 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
## Unveröffentlicht ## Unveröffentlicht
### Neu
- Zertifikat-Manager: Neuer Reiter „Übersicht“. Ziehen Sie alle Dateien, die Sie vom Zertifikatsaussteller bekommen haben, auf einmal hinein – gern auch direkt die ZIP-Datei. Tessera zeigt, was jede Datei ist (Serverzertifikat, Zwischenzertifikat, Stammzertifikat, privater Schlüssel, Zertifikatsanfrage), wofür sie gebraucht wird, wie lange sie gültig ist und was zusammengehört. Unter jedem Teil können Sie es in jedem passenden Format herunterladen: Zertifikate als PEM (.crt), DER (.cer), mit Kette, PKCS#7 (.p7b) oder als PFX mit Schlüssel und Kette; den Schlüssel als PEM, RSA-PEM oder DER; die Anfrage als PEM oder DER. Passwortgeschützte PFX-Dateien lassen sich mit dem Passwort entsperren.
### Behoben
- Desktop-App: Downloads, die Tessera erst im Fenster erstellt (zum Beispiel im Zertifikat-Manager), werden jetzt im Ordner „Downloads“ gespeichert; eine Meldung nennt den Dateinamen. Bisher öffnete Windows nur den Hinweis „Holen Sie sich eine App, um diesen ‚blob‘-Link zu öffnen“. Dafür ist die neue Version der Desktop-App nötig.
- Zertifikat-Manager: Die Texte sprechen Sie jetzt durchgehend mit „Sie“ an.
## 1.9.1 – 2026-10-01 ## 1.9.1 – 2026-10-01
### Behoben ### Behoben
+87 -4
View File
@@ -939,6 +939,41 @@ fn external_target(url: &tauri::Url) -> Option<String> {
} }
} }
/// Downloads, die die App selbst speichert statt sie an den System-Browser
/// zu geben: Inhalte, die die Seite im Speicher erzeugt hat (`blob:`,
/// `data:`) -- der Browser kann sie nicht abrufen.
fn saves_in_app(url: &tauri::Url) -> bool {
matches!(url.scheme(), "blob" | "data")
}
/// Titel und Text der Meldung nach einem in der App gespeicherten Download.
fn download_notice(path: Option<&std::path::Path>, success: bool) -> (String, String) {
let name = path
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned());
let folder = path
.and_then(|p| p.parent())
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned());
match (success, name) {
(true, Some(name)) => (
"Download gespeichert".to_string(),
match folder {
Some(folder) => format!("„{name}“ liegt im Ordner {folder}."),
None => format!("„{name}“ wurde gespeichert."),
},
),
(true, None) => (
"Download gespeichert".to_string(),
"Die Datei wurde gespeichert.".to_string(),
),
(false, _) => (
"Download fehlgeschlagen".to_string(),
"Die Datei konnte nicht gespeichert werden.".to_string(),
),
}
}
/// Hauptfenster nach vorne holen. Ein minimiertes Fenster (z. B. per Win+D) /// Hauptfenster nach vorne holen. Ein minimiertes Fenster (z. B. per Win+D)
/// wird zuerst wiederhergestellt. /// wird zuerst wiederhergestellt.
fn show_main_window(app: &AppHandle) { fn show_main_window(app: &AppHandle) {
@@ -1009,15 +1044,36 @@ pub fn run() {
tauri::webview::NewWindowResponse::Deny tauri::webview::NewWindowResponse::Deny
} }
}) })
.on_download(|webview, event| { .on_download(|webview, event| match event {
if let tauri::webview::DownloadEvent::Requested { url, .. } = event { // Dateien, die die Seite selbst erzeugt (blob:/data:, z. B.
// Zertifikats-Downloads), kennt der System-Browser nicht --
// Windows meldete „Holen Sie sich eine App, um diesen
// ‚blob‘-Link zu öffnen“ (VM 8233, 01.10.2026). Die speichert
// die App selbst im Ordner Downloads und meldet es danach.
tauri::webview::DownloadEvent::Requested { url, .. } => {
if saves_in_app(&url) {
return true;
}
let _ = webview let _ = webview
.app_handle() .app_handle()
.opener() .opener()
.open_url(url.to_string(), None::<&str>); .open_url(url.to_string(), None::<&str>);
return false; false
} }
true tauri::webview::DownloadEvent::Finished { url, path, success } => {
if saves_in_app(&url) {
let (title, body) = download_notice(path.as_deref(), success);
let _ = webview
.app_handle()
.notification()
.builder()
.title(title)
.body(body)
.show();
}
true
}
_ => true,
}) })
.build()?; .build()?;
@@ -1500,6 +1556,33 @@ mod tests {
); );
} }
#[test]
fn saves_in_app_nur_blob_und_data() {
for (url, expected) in [
("blob:http://localhost:3000/0c1d-11", true),
("data:application/x-pem-file;base64,QUJD", true),
("https://alpha.tessera.ctl.de/desktop/download/x.exe", false),
("http://intranet.local/datei.pdf", false),
] {
let parsed = tauri::Url::parse(url).unwrap();
assert_eq!(saves_in_app(&parsed), expected, "{url}");
}
}
#[test]
fn download_notice_nennt_datei_und_ordner() {
let path = std::path::Path::new("/home/tessera/Downloads/www.example.de.crt");
let (title, body) = download_notice(Some(path), true);
assert_eq!(title, "Download gespeichert");
assert_eq!(body, "„www.example.de.crt“ liegt im Ordner Downloads.");
let (title, _) = download_notice(None, false);
assert_eq!(title, "Download fehlgeschlagen");
let (_, body) = download_notice(None, true);
assert_eq!(body, "Die Datei wurde gespeichert.");
}
#[test] #[test]
fn external_target_nur_http_und_https() { fn external_target_nur_http_und_https() {
let https = tauri::Url::parse("https://www.google.com/search?q=tessera").unwrap(); let https = tauri::Url::parse("https://www.google.com/search?q=tessera").unwrap();