feat(lancamentos): adicionar suporte a anexos com upload para storage S3

Permite vincular arquivos (PDF, imagens) a lançamentos via upload direto
para storage compatível com S3, usando token assinado por arquivo e
validação de propriedade na leitura e remoção.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-03-28 15:13:05 +00:00
parent 32da4f906e
commit f82043127a
14 changed files with 1392 additions and 141 deletions

View File

@@ -0,0 +1,52 @@
import {
DeleteObjectCommand,
GetObjectCommand,
HeadObjectCommand,
PutObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { S3_BUCKET, s3 } from "./s3-client";
export async function createPresignedPutUrl(
fileKey: string,
mimeType: string,
): Promise<string> {
const command = new PutObjectCommand({
Bucket: S3_BUCKET,
Key: fileKey,
ContentType: mimeType,
});
return getSignedUrl(s3, command, { expiresIn: 300 }); // 5 minutos
}
export async function createPresignedGetUrl(fileKey: string): Promise<string> {
const command = new GetObjectCommand({
Bucket: S3_BUCKET,
Key: fileKey,
});
return getSignedUrl(s3, command, { expiresIn: 3600 }); // 1 hora
}
export async function headS3Object(fileKey: string): Promise<{
contentLength: number | null;
contentType: string | null;
}> {
const command = new HeadObjectCommand({
Bucket: S3_BUCKET,
Key: fileKey,
});
const result = await s3.send(command);
return {
contentLength: result.ContentLength ?? null,
contentType: result.ContentType ?? null,
};
}
export async function deleteS3Object(fileKey: string): Promise<void> {
const command = new DeleteObjectCommand({
Bucket: S3_BUCKET,
Key: fileKey,
});
await s3.send(command);
}

View File

@@ -0,0 +1,13 @@
import { S3Client } from "@aws-sdk/client-s3";
export const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT ?? "",
region: process.env.S3_REGION ?? "auto",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY ?? "",
},
forcePathStyle: true,
});
export const S3_BUCKET = process.env.S3_BUCKET ?? "attachments";