feat: add API proxy for MinIO files - fixes file access in production
This commit is contained in:
@@ -37,7 +37,9 @@ services:
|
|||||||
MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-admin}
|
MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-admin}
|
||||||
MINIO_SECRET_KEY: ${MINIO_ROOT_PASSWORD:-password123}
|
MINIO_SECRET_KEY: ${MINIO_ROOT_PASSWORD:-password123}
|
||||||
MINIO_USE_SSL: "false"
|
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:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- minio
|
- minio
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ import { PutObjectCommand } from "@aws-sdk/client-s3";
|
|||||||
import { s3Client, BUCKET_NAME } from "@/lib/s3";
|
import { s3Client, BUCKET_NAME } from "@/lib/s3";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
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) {
|
export async function uploadFile(formData: FormData) {
|
||||||
try {
|
try {
|
||||||
const file = formData.get("file") as File;
|
const file = formData.get("file") as File;
|
||||||
@@ -23,11 +31,12 @@ export async function uploadFile(formData: FormData) {
|
|||||||
|
|
||||||
await s3Client.send(command);
|
await s3Client.send(command);
|
||||||
|
|
||||||
// Return the URL to access the file (in MinIO local development)
|
// Return the URL to access the file
|
||||||
const url = `http://localhost:9000/${BUCKET_NAME}/${fileName}`;
|
const url = getPublicUrl(fileName);
|
||||||
return { success: true, url, fileName };
|
return { success: true, url, fileName };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Upload error:", error);
|
console.error("Upload error:", error);
|
||||||
return { success: false, error: "Failed to upload file" };
|
return { success: false, error: "Failed to upload file" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
60
src/app/api/files/[...path]/route.ts
Normal file
60
src/app/api/files/[...path]/route.ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,25 @@ function xorEncrypt(data: Uint8Array, key: string): Uint8Array {
|
|||||||
return result;
|
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(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ 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 });
|
return new NextResponse("Documento não encontrado ou não publicado.", { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Buscar o arquivo original
|
// 2. Buscar o arquivo original (usando URL interna do MinIO)
|
||||||
const fileResponse = await fetch(doc.fileUrl);
|
const internalUrl = getInternalFileUrl(doc.fileUrl);
|
||||||
|
const fileResponse = await fetch(internalUrl);
|
||||||
if (!fileResponse.ok) {
|
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 });
|
return new NextResponse("Erro ao buscar arquivo na origem.", { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user