Initial SeConectai briefing system

This commit is contained in:
2026-10-04 10:42:23 -03:00
commit 45cd8d2f03
163 changed files with 12659 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
import { createReadStream } from "fs";
import { stat } from "fs/promises";
import { NextResponse } from "next/server";
import { Readable } from "stream";
import { getAnexoAutorizado } from "@/lib/anexo-service";
import { db } from "@/lib/db";
import { assertSameOrigin } from "@/lib/request-security";
import { removeStoredFile } from "@/lib/storage";
export const runtime = "nodejs";
function safeFileName(name: string) {
return name.replace(/["\r\n]/g, "_");
}
export async function GET(_request: Request, { params }: { params: { id: string } }) {
const anexo = await getAnexoAutorizado(params.id);
if (!anexo) {
return NextResponse.json({ message: "Anexo nao encontrado." }, { status: 404 });
}
try {
const fileStat = await stat(anexo.caminho);
const stream = Readable.toWeb(createReadStream(anexo.caminho)) as ReadableStream;
return new Response(stream, {
headers: {
"Content-Type": anexo.mime,
"Content-Length": String(fileStat.size),
"Content-Disposition": `attachment; filename="${safeFileName(anexo.nomeOriginal)}"`,
"X-Content-Type-Options": "nosniff"
}
});
} catch {
return NextResponse.json({ message: "Arquivo nao encontrado no armazenamento." }, { status: 404 });
}
}
export async function DELETE(request: Request, { params }: { params: { id: string } }) {
const originError = assertSameOrigin(request);
if (originError) return originError;
const anexo = await getAnexoAutorizado(params.id);
if (!anexo) {
return NextResponse.json({ message: "Anexo nao encontrado." }, { status: 404 });
}
if (anexo.briefing.status !== "RASCUNHO") {
return NextResponse.json(
{ message: "Briefing enviado nao aceita remocao de anexos." },
{ status: 409 }
);
}
await db.anexo.delete({
where: { id: anexo.id }
});
await removeStoredFile(anexo.caminho);
return NextResponse.json({ ok: true });
}
+3
View File
@@ -0,0 +1,3 @@
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
@@ -0,0 +1,86 @@
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getBriefingPerguntaParaUpload } from "@/lib/anexo-service";
import { assertSameOrigin, rateLimitResponse } from "@/lib/request-security";
import { removeStoredFile, validateAndStoreUpload } from "@/lib/storage";
export const runtime = "nodejs";
export async function POST(
request: Request,
{
params
}: {
params: { briefingId: string; perguntaId: string };
}
) {
const originError = assertSameOrigin(request);
if (originError) return originError;
const rateLimitError = rateLimitResponse(`upload:${params.briefingId}:${params.perguntaId}`, 30, 60 * 60 * 1000);
if (rateLimitError) return rateLimitError;
const { briefing } = await getBriefingPerguntaParaUpload(params.briefingId, params.perguntaId);
if (briefing.status !== "RASCUNHO") {
return NextResponse.json(
{ message: "Briefing enviado nao aceita novos anexos." },
{ status: 409 }
);
}
const count = await db.anexo.count({
where: {
briefingId: params.briefingId,
perguntaId: params.perguntaId
}
});
if (count >= 20) {
return NextResponse.json(
{ message: "Limite de 20 arquivos por pergunta atingido." },
{ status: 400 }
);
}
const formData = await request.formData();
const file = formData.get("file");
if (!(file instanceof File)) {
return NextResponse.json({ message: "Arquivo nao enviado." }, { status: 400 });
}
let storedFile: Awaited<ReturnType<typeof validateAndStoreUpload>> | null = null;
try {
storedFile = await validateAndStoreUpload(file);
const anexo = await db.anexo.create({
data: {
briefingId: params.briefingId,
perguntaId: params.perguntaId,
nomeOriginal: storedFile.originalName,
caminho: storedFile.storedPath,
mime: storedFile.mime,
tamanho: storedFile.size
},
select: {
id: true,
nomeOriginal: true,
mime: true,
tamanho: true
}
});
return NextResponse.json({ anexo });
} catch (error) {
if (storedFile) {
await removeStoredFile(storedFile.storedPath);
}
return NextResponse.json(
{ message: error instanceof Error ? error.message : "Nao foi possivel enviar o arquivo." },
{ status: 400 }
);
}
}
+29
View File
@@ -0,0 +1,29 @@
import { NextResponse } from "next/server";
import { gerarBriefingPdfBuffer } from "@/lib/briefing-pdf";
import { getBriefingPdfData } from "@/lib/pdf-data";
export const runtime = "nodejs";
function safeFileName(value: string) {
return value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-zA-Z0-9-_]+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
.toLowerCase();
}
export async function GET(_request: Request, { params }: { params: { id: string } }) {
const data = await getBriefingPdfData(params.id);
const buffer = await gerarBriefingPdfBuffer(data);
const fileName = `${safeFileName(data.briefing.titulo || "briefing")}.pdf`;
return new NextResponse(buffer, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="${fileName}"`,
"X-Content-Type-Options": "nosniff"
}
});
}
+27
View File
@@ -0,0 +1,27 @@
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
export const runtime = "nodejs";
export async function GET() {
try {
await db.$queryRaw`SELECT 1`;
return NextResponse.json({
ok: true,
app: "ok",
db: "ok",
checkedAt: new Date().toISOString()
});
} catch {
return NextResponse.json(
{
ok: false,
app: "ok",
db: "error",
checkedAt: new Date().toISOString()
},
{ status: 503 }
);
}
}
+14
View File
@@ -0,0 +1,14 @@
import { NextResponse } from "next/server";
import { getExportacaoDadosCliente } from "@/lib/account-service";
export async function GET() {
const data = await getExportacaoDadosCliente();
return new NextResponse(JSON.stringify(data, null, 2), {
headers: {
"Content-Type": "application/json; charset=utf-8",
"Content-Disposition": "attachment; filename=\"meus-dados-briefing.json\"",
"X-Content-Type-Options": "nosniff"
}
});
}