pdf: fix route

Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
This commit is contained in:
rodrigo committed 2026-02-27 19:02:18 -03:00
1 parent 61a1631519
commit f4afe83db0
9 files changed
+149 -126

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
# Passos para ativar a funcionalidade de PDF
## 1. Instalar Dependências
Execute o seguinte comando para instalar todas as dependências necessárias:
```bash
npm install pdf-lib pdf-parse pdf2pic tesseract.js
npm install --save-dev @types/pdf-parse
```
## 2. Executar Migration do Banco de Dados
Execute a migration para criar a tabela de operações de PDF:
```bash
npx prisma migrate dev --name add_operacoes_pdf
```
Ou simplesmente:
```bash
npm run prisma:migrate
```
## 3. Gerar Cliente Prisma
Gere o cliente Prisma atualizado:
```bash
npm run prisma:generate
```
## 4. Criar Diretórios
Os diretórios serão criados automaticamente, mas você pode criá-los manualmente:
**No Windows (PowerShell):**
```powershell
New-Item -Path "storage\temp\pdf-ops" -ItemType Directory -Force
New-Item -Path "storage\pdf-output" -ItemType Directory -Force
New-Item -Path "storage\temp\uploads" -ItemType Directory -Force
```
**No Linux/Mac:**
```bash
mkdir -p storage/temp/pdf-ops
mkdir -p storage/pdf-output
mkdir -p storage/temp/uploads
```
## 5. Reiniciar o Servidor
Após instalar as dependências e executar as migrations, reinicie o servidor:
```bash
npm run dev:full
```
Ou apenas o backend:
```bash
npm run server
```
## Verificação
Após seguir todos os passos:
1. Acesse a aplicação no navegador
2. Faça login
3. Clique no item "PDFs" no menu lateral
4. Teste a funcionalidade de compressão enviando um PDF
## Troubleshooting
### Se continuar dando erro 404:
1. Verifique se o servidor foi reiniciado após as mudanças
2. Verifique se as dependências foram instaladas corretamente
3. Verifique os logs do servidor para erros de importação
### Se der erro de módulo não encontrado:
Execute:
```bash
npm install
npm run prisma:generate
```
### Se der erro no banco de dados:
Execute:
```bash
npx prisma migrate reset
npx prisma migrate dev
npm run prisma:generate
```
**ATENÇÃO:** O comando `migrate reset` apaga todos os dados do banco!
-105
View File
@@ -1,105 +0,0 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Adicionar interface para tipo de usuário no Request
declare global {
namespace Express {
interface Request {
user?: {
id: string;
nome: string;
email: string;
perfil: string;
};
}
}
}
export const authenticateToken = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
// Verificar se há token
const authHeader = req.headers.authorization;
if (!authHeader) {
res.status(401).json({ error: 'Token não fornecido' });
return;
}
// Extrair o token do header (remover o Bearer)
const token = authHeader.replace('Bearer ', '');
// Verificar o token
const secret = process.env.JWT_SECRET || 'default-secret';
const decoded = jwt.verify(token, secret) as any;
// Buscar o usuário no banco
const user = await prisma.usuario.findUnique({
where: { id: decoded.id },
select: {
id: true,
nome: true,
email: true,
perfil: true,
ativo: true,
},
});
if (!user || !user.ativo) {
res.status(401).json({ error: 'Usuário inválido ou inativo' });
return;
}
// Adicionar o usuário no request
req.user = {
id: user.id,
nome: user.nome,
email: user.email,
perfil: user.perfil,
};
next();
} catch (error) {
console.error('Erro na autenticação:', error);
if (error instanceof jwt.TokenExpiredError) {
res.status(401).json({ error: 'Token expirado' });
return;
}
if (error instanceof jwt.JsonWebTokenError) {
res.status(401).json({ error: 'Token inválido' });
return;
}
res.status(500).json({ error: 'Erro ao autenticar' });
}
};
export const requireAdmin = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
if (!req.user) {
res.status(401).json({ error: 'Usuário não autenticado' });
return;
}
if (req.user.perfil !== 'ADVOGADO_ADMIN') {
res.status(403).json({ error: 'Acesso negado. Apenas administradores.' });
return;
}
next();
} catch (error) {
console.error('Erro na verificação de permissão:', error);
res.status(500).json({ error: 'Erro ao verificar permissões' });
}
};
@@ -22,12 +22,13 @@ CREATE TABLE "operacoes_pdf" (
"detalhesOCR" JSONB, "detalhesOCR" JSONB,
"tempoProcessamento" INTEGER, "tempoProcessamento" INTEGER,
"criadoEm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "criadoEm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"atualizadoEm" TIMESTAMP(3) NOT NULL, "atualizadoEm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "operacoes_pdf_pkey" PRIMARY KEY ("id") CONSTRAINT "operacoes_pdf_pkey" PRIMARY KEY ("id")
); );
-- Adicionar foreign key para usuariosALTER TABLE "operacoes_pdf" ADD CONSTRAINT "operacoes_pdf_usuarioId_fkey" FOREIGN KEY ("usuarioId") REFERENCES "usuarios"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- Adicionar foreign key para usuarios
ALTER TABLE "operacoes_pdf" ADD CONSTRAINT "operacoes_pdf_usuarioId_fkey" FOREIGN KEY ("usuarioId") REFERENCES "usuarios"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- Criar índice para melhor performance nas consultas -- Criar índice para melhor performance nas consultas
CREATE INDEX "operacoes_pdf_usuarioId_idx" ON "operacoes_pdf"("usuarioId"); CREATE INDEX "operacoes_pdf_usuarioId_idx" ON "operacoes_pdf"("usuarioId");
+2
View File
@@ -18,6 +18,7 @@ import {
CurrencyDollarIcon, CurrencyDollarIcon,
CalculatorIcon, CalculatorIcon,
BriefcaseIcon, BriefcaseIcon,
DocumentDuplicateIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
import api from "../../services/api"; import api from "../../services/api";
@@ -91,6 +92,7 @@ export const MainLayout: React.FC = () => {
{ path: "/atendimentos", label: "Atendimentos", icon: UserIcon }, { path: "/atendimentos", label: "Atendimentos", icon: UserIcon },
{ path: "/tarefas", label: "Tarefas", icon: ClipboardDocumentCheckIcon }, { path: "/tarefas", label: "Tarefas", icon: ClipboardDocumentCheckIcon },
{ path: "/documentos", label: "Documentos", icon: DocumentTextIcon }, { path: "/documentos", label: "Documentos", icon: DocumentTextIcon },
{ path: "/pdf", label: "PDFs", icon: DocumentDuplicateIcon },
{ {
path: null, path: null,
label: "Cálculos", label: "Cálculos",
+16 -4
View File
@@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import axios from 'axios'; import axios from 'axios';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useAuth } from '../../contexts/AuthContext';
import { import {
DocumentIcon, DocumentIcon,
CloudArrowUpIcon, CloudArrowUpIcon,
@@ -36,6 +37,7 @@ type OperationType =
| 'info'; | 'info';
export const PDFManipulationPage: React.FC = () => { export const PDFManipulationPage: React.FC = () => {
const { getToken } = useAuth();
const [selectedOperation, setSelectedOperation] = useState<OperationType>('compress'); const [selectedOperation, setSelectedOperation] = useState<OperationType>('compress');
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
@@ -74,10 +76,6 @@ export const PDFManipulationPage: React.FC = () => {
setResult(null); setResult(null);
}; };
const getToken = () => {
return localStorage.getItem('token');
};
const processFiles = async () => { const processFiles = async () => {
if (files.length === 0) { if (files.length === 0) {
toast.error('Selecione pelo menos um arquivo'); toast.error('Selecione pelo menos um arquivo');
@@ -645,6 +643,20 @@ export const PDFManipulationPage: React.FC = () => {
{op.tempoProcessamento}ms {op.tempoProcessamento}ms
</p> </p>
)} )}
{op.status === 'CONCLUIDO' && op.arquivosResultado && op.arquivosResultado.length > 0 && (
<div className="mt-2 space-y-1">
{op.arquivosResultado.map((arquivo: string, index: number) => (
<button
key={index}
onClick={() => downloadFile(op.id, index)}
className="flex items-center space-x-1 w-full text-xs text-blue-600 hover:text-blue-700 hover:bg-blue-50 p-1 rounded"
>
<ArrowDownTrayIcon className="h-3 w-3" />
<span>Arquivo {index + 1}</span>
</button>
))}
</div>
)}
</div> </div>
))} ))}
</div> </div>
@@ -1,6 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { PDFService } from '../services/pdfService'; import { PDFService } from '../../services/pdfService';
import multer from 'multer'; import multer from 'multer';
import path from 'path'; import path from 'path';
import fs from 'fs/promises'; import fs from 'fs/promises';
@@ -138,9 +138,10 @@ export class PDFController {
success: true, success: true,
operacaoId, operacaoId,
results, results,
outputPaths: arquivosResultado,
totalOriginalSize: Number(tamanhoOriginal), totalOriginalSize: Number(tamanhoOriginal),
totalCompressedSize: Number(tamanhoResultado), totalCompressedSize: Number(tamanhoResultado),
totalReduction: ((Number(tamanhoOriginal) - Number(tamanhoResultado)) / Number(tamanhoOriginal)) * 100, reduction: ((Number(tamanhoOriginal) - Number(tamanhoResultado)) / Number(tamanhoOriginal)) * 100,
processingTime: tempoProcessamento, processingTime: tempoProcessamento,
}); });
} catch (error) { } catch (error) {
@@ -346,6 +347,7 @@ export class PDFController {
success: true, success: true,
operacaoId, operacaoId,
outputPath, outputPath,
outputPaths: [outputPath],
originalSize: Number(tamanhoOriginal), originalSize: Number(tamanhoOriginal),
mergedSize: Number(tamanhoResultado), mergedSize: Number(tamanhoResultado),
processingTime: tempoProcessamento, processingTime: tempoProcessamento,
@@ -551,6 +553,7 @@ export class PDFController {
success: true, success: true,
operacaoId, operacaoId,
outputPath, outputPath,
outputPaths: [outputPath],
imagesProcessed: files.length, imagesProcessed: files.length,
pdfSize: Number(tamanhoResultado), pdfSize: Number(tamanhoResultado),
processingTime: tempoProcessamento, processingTime: tempoProcessamento,
@@ -725,6 +728,7 @@ export class PDFController {
mensagemErro: true, mensagemErro: true,
criadoEm: true, criadoEm: true,
parametros: true, parametros: true,
arquivosResultado: true,
}, },
}), }),
prisma.operacaoPDF.count({ prisma.operacaoPDF.count({
@@ -732,9 +736,16 @@ export class PDFController {
}), }),
]); ]);
// Converter BigInt para Number para evitar erro de serialização JSON
const operacoesSerializadas = operacoes.map(op => ({
...op,
tamanhoOriginal: Number(op.tamanhoOriginal),
tamanhoResultado: op.tamanhoResultado ? Number(op.tamanhoResultado) : null,
}));
res.json({ res.json({
success: true, success: true,
operacoes, operacoes: operacoesSerializadas,
pagination: { pagination: {
page, page,
limit, limit,
+2
View File
@@ -27,6 +27,7 @@ import indicesMonetariosRoutes from './routes/indicesMonetarios';
import pensaoAlimenticiaRouter from './routes/pensaoAlimenticia'; // Importar router de pensão alimentícia import pensaoAlimenticiaRouter from './routes/pensaoAlimenticia'; // Importar router de pensão alimentícia
import financeiroPainelRoutes from './routes/financeiroPainel'; // Importar rotas do financeiro painel import financeiroPainelRoutes from './routes/financeiroPainel'; // Importar rotas do financeiro painel
import audienciasRoutes from './routes/audiencias'; // Importar rotas de audiências import audienciasRoutes from './routes/audiencias'; // Importar rotas de audiências
import pdfRoutes from './routes/pdf'; // Importar rotas de PDF
const app = express(); const app = express();
@@ -1482,3 +1483,4 @@ app.use('/api/indices', indicesMonetariosRoutes);
app.use('/api/pensao-alimenticia', pensaoAlimenticiaRouter); app.use('/api/pensao-alimenticia', pensaoAlimenticiaRouter);
app.use('/api/financeiro-painel', financeiroPainelRoutes); // Adicionar uso das rotas do financeiro painel app.use('/api/financeiro-painel', financeiroPainelRoutes); // Adicionar uso das rotas do financeiro painel
app.use('/api/audiencias', audienciasRoutes); // Adicionar uso das rotas de audiências app.use('/api/audiencias', audienciasRoutes); // Adicionar uso das rotas de audiências
app.use('/api/pdf', pdfRoutes); // Adicionar uso das rotas de PDF
@@ -1,6 +1,6 @@
import express from 'express'; import express from 'express';
import { PDFController, uploadMiddleware } from '../controllers/pdfController'; import { PDFController, uploadMiddleware } from '../controllers/pdfController';
import { authenticateToken } from '../middleware/auth'; import { authMiddleware } from '../../middleware/auth';
const router = express.Router(); const router = express.Router();
@@ -12,7 +12,7 @@ const router = express.Router();
// Comprime PDFs // Comprime PDFs
router.post( router.post(
'/compress', '/compress',
authenticateToken, authMiddleware,
uploadMiddleware.array('files', 10), uploadMiddleware.array('files', 10),
PDFController.compress PDFController.compress
); );
@@ -20,7 +20,7 @@ router.post(
// Divide PDF // Divide PDF
router.post( router.post(
'/split', '/split',
authenticateToken, authMiddleware,
uploadMiddleware.array('files', 1), uploadMiddleware.array('files', 1),
PDFController.split PDFController.split
); );
@@ -28,7 +28,7 @@ router.post(
// Mescla PDFs // Mescla PDFs
router.post( router.post(
'/merge', '/merge',
authenticateToken, authMiddleware,
uploadMiddleware.array('files', 50), uploadMiddleware.array('files', 50),
PDFController.merge PDFController.merge
); );
@@ -36,7 +36,7 @@ router.post(
// Converte PDF para imagens // Converte PDF para imagens
router.post( router.post(
'/pdf-to-images', '/pdf-to-images',
authenticateToken, authMiddleware,
uploadMiddleware.array('files', 1), uploadMiddleware.array('files', 1),
PDFController.pdfToImages PDFController.pdfToImages
); );
@@ -44,7 +44,7 @@ router.post(
// Converte imagens para PDF // Converte imagens para PDF
router.post( router.post(
'/images-to-pdf', '/images-to-pdf',
authenticateToken, authMiddleware,
uploadMiddleware.array('files', 100), uploadMiddleware.array('files', 100),
PDFController.imagesToPDF PDFController.imagesToPDF
); );
@@ -52,7 +52,7 @@ router.post(
// Realiza OCR em PDF // Realiza OCR em PDF
router.post( router.post(
'/ocr', '/ocr',
authenticateToken, authMiddleware,
uploadMiddleware.array('files', 1), uploadMiddleware.array('files', 1),
PDFController.performOCR PDFController.performOCR
); );
@@ -60,7 +60,7 @@ router.post(
// Obtém informações de um PDF // Obtém informações de um PDF
router.post( router.post(
'/info', '/info',
authenticateToken, authMiddleware,
uploadMiddleware.array('files', 1), uploadMiddleware.array('files', 1),
PDFController.getInfo PDFController.getInfo
); );
@@ -68,14 +68,14 @@ router.post(
// Obtém histórico de operações // Obtém histórico de operações
router.get( router.get(
'/history', '/history',
authenticateToken, authMiddleware,
PDFController.getHistory PDFController.getHistory
); );
// Download de arquivo processado // Download de arquivo processado
router.get( router.get(
'/download/:operacaoId/:fileIndex', '/download/:operacaoId/:fileIndex',
authenticateToken, authMiddleware,
PDFController.downloadFile PDFController.downloadFile
); );
+2 -2
View File
@@ -1,5 +1,3 @@
import { Request } from 'express';
declare global { declare global {
namespace Express { namespace Express {
interface Request { interface Request {
@@ -11,3 +9,5 @@ declare global {
} }
} }
} }
export {};