From 154d7060f8f547930aa6382f8d7276887e358652 Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Sat, 23 May 2026 21:04:53 +0200 Subject: [PATCH] =?UTF-8?q?E-Mail=20Anh=C3=A4nge=20ohne=20Fetch=20herunter?= =?UTF-8?q?laden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KI-AGENT: Der E-Mail Anhang-Download nutzt jetzt einen nativen Browser-Link statt Cross-Origin-Fetch und erlaubt dafür den bestehenden JWT gezielt als Download-Token. --- backend/src/plugins/auth.ts | 11 ++++++++++- frontend/pages/email/index.vue | 29 +++++++++++++++-------------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/backend/src/plugins/auth.ts b/backend/src/plugins/auth.ts index 2b442a8..ca5532c 100644 --- a/backend/src/plugins/auth.ts +++ b/backend/src/plugins/auth.ts @@ -68,6 +68,15 @@ export default fp(async (server: FastifyInstance) => { return } + const urlPath = req.url.split("?")[0] + const queryToken = (req.query as any)?.downloadToken + const downloadToken = + typeof queryToken === "string" + && urlPath.startsWith("/api/email/attachments/") + && urlPath.endsWith("/download") + ? queryToken + : null + // 1️⃣ Token aus Header oder Cookie lesen const cookieToken = req.cookies?.token const authHeader = req.headers.authorization @@ -78,7 +87,7 @@ export default fp(async (server: FastifyInstance) => { const token = headerToken && headerToken.length > 10 ? headerToken - : cookieToken || null + : cookieToken || downloadToken || null if (!token) { return reply.code(401).send({ error: "Authentication required" }) diff --git a/frontend/pages/email/index.vue b/frontend/pages/email/index.vue index 7d8c1d9..0b42c2c 100644 --- a/frontend/pages/email/index.vue +++ b/frontend/pages/email/index.vue @@ -55,6 +55,7 @@ type EmailMessage = { } const { $api } = useNuxtApp() +const runtimeConfig = useRuntimeConfig() const toast = useToast() const accounts = ref([]) @@ -456,24 +457,22 @@ async function moveSelectedMessage() { async function downloadAttachment(attachment: NonNullable[number]) { actionLoading.value = `attachment-${attachment.id}` try { - const response = await $api.raw(`/api/email/attachments/${attachment.id}/download`, { - responseType: "arrayBuffer", - timeout: 60_000, - }) - const contentType = response.headers.get("content-type") || attachment.contentType || "application/octet-stream" - const blob = new Blob([response._data as ArrayBuffer], { type: contentType }) - const disposition = response.headers.get("content-disposition") || "" - const dispositionFilename = disposition.match(/filename="([^"]+)"/)?.[1] - const filename = dispositionFilename || attachment.filename || "anhang" - const url = URL.createObjectURL(blob) + const apiBase = String(runtimeConfig.public.apiBase || "").replace(/\/$/, "") + const path = `/api/email/attachments/${attachment.id}/download` + const downloadUrl = new URL(apiBase ? `${apiBase}${path}` : path, window.location.origin) + const token = useCookie("token").value + + if (token) { + downloadUrl.searchParams.set("downloadToken", token) + } + const link = document.createElement("a") - link.href = url - link.download = filename + link.href = downloadUrl.toString() + link.download = attachment.filename || "anhang" document.body.appendChild(link) link.click() link.remove() - URL.revokeObjectURL(url) } catch (err: any) { toast.add({ title: "Download fehlgeschlagen", @@ -481,7 +480,9 @@ async function downloadAttachment(attachment: NonNullable { + actionLoading.value = "" + }, 750) } }