From f1037279209518fa7617cd9504d64ae352769592 Mon Sep 17 00:00:00 2001 From: Erik Silva Date: Tue, 20 Jan 2026 14:31:13 -0300 Subject: [PATCH] feat: add API proxy for MinIO files - fixes file access in production --- docker-compose.yml | 4 +- src/app/actions/upload.ts | 13 +++++- src/app/api/files/[...path]/route.ts | 60 ++++++++++++++++++++++++++++ src/app/api/view/[id]/route.ts | 25 +++++++++++- 4 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 src/app/api/files/[...path]/route.ts diff --git a/docker-compose.yml b/docker-compose.yml index 20a49d8..6a6c5e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,9 @@ services: MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-admin} MINIO_SECRET_KEY: ${MINIO_ROOT_PASSWORD:-password123} MINIO_USE_SSL: "false" - MINIO_BUCKET: ${MINIO_BUCKET:-documents} + MINIO_BUCKET: ${MINIO_BUCKET:-portal-transparencia} + JWT_SECRET: ${JWT_SECRET:-supersecretkey123456789} + NODE_ENV: production depends_on: - postgres - minio diff --git a/src/app/actions/upload.ts b/src/app/actions/upload.ts index a9dd55e..13dace8 100644 --- a/src/app/actions/upload.ts +++ b/src/app/actions/upload.ts @@ -4,6 +4,14 @@ import { PutObjectCommand } from "@aws-sdk/client-s3"; import { s3Client, BUCKET_NAME } from "@/lib/s3"; import { v4 as uuidv4 } from "uuid"; +// Get the public URL for MinIO files +// In production, files are served through /api/files proxy +function getPublicUrl(fileName: string): string { + // Always use our API proxy route to serve files + // This ensures files are accessible even when MinIO is not publicly exposed + return `/api/files/${BUCKET_NAME}/${fileName}`; +} + export async function uploadFile(formData: FormData) { try { const file = formData.get("file") as File; @@ -23,11 +31,12 @@ export async function uploadFile(formData: FormData) { await s3Client.send(command); - // Return the URL to access the file (in MinIO local development) - const url = `http://localhost:9000/${BUCKET_NAME}/${fileName}`; + // Return the URL to access the file + const url = getPublicUrl(fileName); return { success: true, url, fileName }; } catch (error) { console.error("Upload error:", error); return { success: false, error: "Failed to upload file" }; } } + diff --git a/src/app/api/files/[...path]/route.ts b/src/app/api/files/[...path]/route.ts new file mode 100644 index 0000000..d5064b4 --- /dev/null +++ b/src/app/api/files/[...path]/route.ts @@ -0,0 +1,60 @@ +import { NextRequest, NextResponse } from "next/server"; + +// Get internal MinIO URL for server-side fetching +function getInternalFileUrl(fileUrl: string): string { + try { + const url = new URL(fileUrl); + const pathParts = url.pathname.split('/').filter(Boolean); + if (pathParts.length >= 2) { + const bucket = pathParts[0]; + const fileName = pathParts.slice(1).join('/'); + const minioEndpoint = process.env.MINIO_ENDPOINT || 'minio'; + const minioPort = process.env.MINIO_PORT || '9000'; + return `http://${minioEndpoint}:${minioPort}/${bucket}/${fileName}`; + } + } catch (e) { + console.error("Error parsing file URL:", e); + } + return fileUrl; +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ path: string[] }> } +) { + try { + const { path } = await params; + + if (!path || path.length < 2) { + return new NextResponse("Invalid path", { status: 400 }); + } + + const bucket = path[0]; + const fileName = path.slice(1).join('/'); + + const minioEndpoint = process.env.MINIO_ENDPOINT || 'minio'; + const minioPort = process.env.MINIO_PORT || '9000'; + const internalUrl = `http://${minioEndpoint}:${minioPort}/${bucket}/${fileName}`; + + const fileResponse = await fetch(internalUrl); + if (!fileResponse.ok) { + console.error("Failed to fetch file from MinIO:", internalUrl, fileResponse.status); + return new NextResponse("File not found", { status: 404 }); + } + + const contentType = fileResponse.headers.get('content-type') || 'application/octet-stream'; + const fileBuffer = await fileResponse.arrayBuffer(); + + return new NextResponse(fileBuffer, { + status: 200, + headers: { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=31536000, immutable', + }, + }); + + } catch (error) { + console.error("Error serving file:", error); + return new NextResponse("Internal server error", { status: 500 }); + } +} diff --git a/src/app/api/view/[id]/route.ts b/src/app/api/view/[id]/route.ts index f939f24..34e0c6b 100644 --- a/src/app/api/view/[id]/route.ts +++ b/src/app/api/view/[id]/route.ts @@ -11,6 +11,25 @@ function xorEncrypt(data: Uint8Array, key: string): Uint8Array { return result; } +// Get internal MinIO URL for server-side fetching +function getInternalFileUrl(fileUrl: string): string { + try { + const url = new URL(fileUrl); + // Extract bucket and file path from URL + const pathParts = url.pathname.split('/').filter(Boolean); + if (pathParts.length >= 2) { + const bucket = pathParts[0]; + const fileName = pathParts.slice(1).join('/'); + const minioEndpoint = process.env.MINIO_ENDPOINT || 'minio'; + const minioPort = process.env.MINIO_PORT || '9000'; + return `http://${minioEndpoint}:${minioPort}/${bucket}/${fileName}`; + } + } catch (e) { + console.error("Error parsing file URL:", e); + } + return fileUrl; // fallback to original URL +} + export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } @@ -34,9 +53,11 @@ export async function GET( return new NextResponse("Documento não encontrado ou não publicado.", { status: 404 }); } - // 2. Buscar o arquivo original - const fileResponse = await fetch(doc.fileUrl); + // 2. Buscar o arquivo original (usando URL interna do MinIO) + const internalUrl = getInternalFileUrl(doc.fileUrl); + const fileResponse = await fetch(internalUrl); if (!fileResponse.ok) { + console.error("Failed to fetch file from MinIO:", internalUrl, fileResponse.status); return new NextResponse("Erro ao buscar arquivo na origem.", { status: 500 }); }