diff --git a/backend/src/routes/exports.ts b/backend/src/routes/exports.ts index e849f97..e30ad07 100644 --- a/backend/src/routes/exports.ts +++ b/backend/src/routes/exports.ts @@ -177,11 +177,49 @@ export default async function exportRoutes(server: FastifyInstance) { .from(generatedexports) .where(eq(generatedexports.tenantId, req.user.tenant_id)) - console.log(data) - reply.send(data) + reply.send(data.map(item => ({ + ...item, + url: `/api/exports/${item.id}/download`, + }))) }) + server.get("/exports/:id/download", async (req, reply) => { + const { id } = req.params as { id: string } + const exportId = Number(id) + if (!Number.isSafeInteger(exportId)) { + return reply.code(404).send({ error: "Export not found" }) + } + + const [item] = await server.db + .select() + .from(generatedexports) + .where(eq(generatedexports.id, exportId)) + + if ( + !item || + item.tenantId !== req.user.tenant_id || + item.validUntil.getTime() <= Date.now() + ) { + return reply.code(404).send({ error: "Export not found" }) + } + + try { + const { Body, ContentType } = await s3.send(new GetObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: item.filePath, + })) + const filename = item.filePath.split("/").pop() || `export-${item.id}` + + reply.header("Content-Type", ContentType || "application/octet-stream") + reply.header("Content-Disposition", `attachment; filename="${filename}"`) + return reply.send(Body as any) + } catch (error) { + console.error(error) + return reply.code(500).send({ error: "Could not download export" }) + } + }) + diff --git a/backend/src/routes/files.ts b/backend/src/routes/files.ts index cff9141..1494510 100644 --- a/backend/src/routes/files.ts +++ b/backend/src/routes/files.ts @@ -4,7 +4,6 @@ import { s3 } from "../utils/s3" import { GetObjectCommand } from "@aws-sdk/client-s3" -import { getSignedUrl } from "@aws-sdk/s3-request-presigner" import archiver from "archiver" import { secrets } from "../utils/secrets" import { saveFile } from "../utils/files" @@ -241,6 +240,28 @@ export default async function fileRoutes(server: FastifyInstance) { } }) + // Dateien über das Backend ausliefern, damit interne S3-Endpunkte wie + // http://minio:9000 niemals als URL an den Browser gelangen. + server.get("/files/content/:id", async (req, reply) => { + try { + const { id } = req.params as { id: string } + const file = await loadSingleFileForRequest(req, id) + if (!file) return reply.code(404).send({ error: "File not found" }) + + const { Body, ContentType } = await s3.send(new GetObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: file.path! + })) + + reply.header("Content-Type", ContentType || "application/octet-stream") + reply.header("Content-Disposition", "inline") + return reply.send(Body as any) + } catch (err) { + console.error(err) + return reply.code(500).send({ error: "Could not load file" }) + } + }) + // ------------------------------------------------------------- @@ -261,13 +282,7 @@ export default async function fileRoutes(server: FastifyInstance) { const file = await loadSingleFileForRequest(req, id) if (!file) return reply.code(404).send({ error: "Not found" }) - const url = await getSignedUrl( - s3, - new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }), - { expiresIn: 900 } - ) - - return { ...file, url } + return { ...file, url: `/api/files/content/${file.id}` } } else { // ------------------------------------------------- // MULTIPLE PRESIGNED URLs @@ -283,26 +298,10 @@ export default async function fileRoutes(server: FastifyInstance) { const selected = rows.filter(f => ids.includes(f.id) && f.path) - console.log(selected) - - const url = await getSignedUrl( - s3, - new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: selected[0].path! }), - { expiresIn: 900 } - ) - console.log(url) - console.log(selected.filter(f => !f.path)) - - const output = await Promise.all( - selected.map(async (file) => { - const url = await getSignedUrl( - s3, - new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }), - { expiresIn: 900 } - ) - return { ...file, url } - }) - ) + const output = selected.map(file => ({ + ...file, + url: `/api/files/content/${file.id}` + })) return { files: output } }