Compare commits

...

5 Commits

Author SHA1 Message Date
HalbeBruno
1356302060 Remocao documentacao desnecessaria 2026-03-19 09:38:15 -03:00
HalbeBruno
d709d4cb09 add upload anexos pos ordem criada 2026-03-18 13:58:20 -03:00
HalbeBruno
b4799a1d42 Correcao limite upload 2026-03-18 10:24:24 -03:00
HalbeBruno
6142f0cc65 Ajustes persiistência conf php 2026-03-17 12:32:51 -03:00
HalbeBruno
64f8524cd1 Ajustes documentacao 2026-03-17 11:20:17 -03:00
15 changed files with 217 additions and 115 deletions

View File

@@ -1,15 +1,4 @@
mysql_data
node_modules
agent
.git .git
.gitignore
.env
.env.example
docker-compose.yml
Dockerfile
README.md
DEPLOY.md
agent/
database/
.idea/
.vscode/
vendor/
node_modules/
storage/

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ agent/dist/*
.DS_Store .DS_Store
mysql_data/ mysql_data/
storage/ storage/

2
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -18,6 +18,9 @@ RUN apt-get clean && rm -rf /var/lib/apt/lists/*
# Install PHP extensions # Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd curl RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd curl
# Configure PHP upload limits
RUN echo "upload_max_filesize = 40M\npost_max_size = 50M" > /usr/local/etc/php/conf.d/uploads.ini
# Enable Apache mod_rewrite # Enable Apache mod_rewrite
RUN a2enmod rewrite RUN a2enmod rewrite

View File

@@ -1,86 +0,0 @@
# Guia de Migração para Produção
Este guia descreve o procedimento seguro para atualizar o ambiente de produção para a nova arquitetura de persistência local, garantindo que nenhum dado seja perdido.
## Pré-requisitos
1. Acesso SSH ao servidor de produção.
2. Permissões de `sudo` ou docker.
## Procedimento Passo-a-Passo
### 1. Preparação e Backup
Antes de atualizar qualquer código, faça o backup do banco de dados atual (que está rodando no volume Docker antigo).
Se você já baixou o novo `Makefile`, pode usar o comando abaixo. Caso contrário, copie o `Makefile` para o servidor ou execute o comando manual.
**Opção A: Usando Makefile (Recomendado)**
```bash
make backup
```
*Isso criará um arquivo `.sql` na pasta `backups/`.*
**Opção B: Manual**
```bash
mkdir -p backups
docker exec dnsblock-db mysqldump -u root -p<SUA_SENHA_ATUAL> --all-databases > backups/backup_pre_migration.sql
```
### 2. Atualização do Código
Baixe a versão mais recente do código que contém a nova configuração do `docker-compose.yml` (com volume `./mysql_data`) e o `.env` seguro.
```bash
git pull origin main
# Ou copie os arquivos manualmente se não usar git
```
### 3. Configuração do Ambiente
1. Certifique-se de que o arquivo `.env` está configurado corretamente com as novas variáveis seguras (caso tenha mudado).
2. Garanta que o `.env` **NÃO** está versionado no git.
### 4. Reinicialização dos Serviços
Agora vamos trocar a infraestrutura. Ao rodar este comando, o container do banco será recriado apontando para a nova pasta `./mysql_data` (que estará vazia inicialmente).
```bash
make down
make up
```
*Ou `docker-compose down && docker-compose up -d`*
### 5. Restauração dos Dados
Agora que o banco novo está vazio, vamos importar o backup que fizemos no Passo 1.
Identifique o nome do arquivo gerado em `backups/` (ex: `backups/backup_20241217_100000.sql`).
```bash
make restore FILE=backups/backup_YYYYMMDD_HHMMSS.sql
```
**Manual:**
```bash
docker exec -i dnsblock-db mysql -u root -p<NOVA_SENHA_ENV> < backups/backup_pre_migration.sql
```
### 6. Verificação
Verifique se os dados foram restaurados corretamente acessando a aplicação ou consultando o banco:
```bash
# Verifica se o cliente "IPv0 Soluções" está correto
docker exec -i dnsblock-db mysql -u root -p<NOVA_SENHA_ENV> dnsblock -e "SELECT name FROM clients WHERE id=1;"
```
### 7. Limpeza (Opcional)
Após confirmar que tudo está funcionando, você pode listar e remover o volume antigo do Docker, se desejar liberar espaço.
```bash
docker volume ls
# Remova o volume 'dnsblock_dbdata' se ele existir e não for mais usado
# docker volume rm dnsblock_dbdata
```

View File

@@ -80,7 +80,12 @@ class OrderController
} }
if (!isset($_FILES['csv_file']) || $_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) { if (!isset($_FILES['csv_file']) || $_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) {
$_SESSION['flash_error'] = "Erro no upload do arquivo CSV."; $csvError = $_FILES['csv_file']['error'] ?? 'desconhecido';
if ($csvError === UPLOAD_ERR_INI_SIZE || $csvError === UPLOAD_ERR_FORM_SIZE) {
$_SESSION['flash_error'] = "O arquivo CSV/TXT excede o limite de tamanho permitido pelo servidor.";
} else {
$_SESSION['flash_error'] = "Erro no upload do arquivo CSV. (código " . $csvError . ")";
}
View::redirect('/admin/orders/create'); View::redirect('/admin/orders/create');
return; return;
} }
@@ -111,6 +116,7 @@ class OrderController
$conn = \App\Config\Database::getInstance()->getConnection(); $conn = \App\Config\Database::getInstance()->getConnection();
$conn->beginTransaction();
try { try {
// Create Order // Create Order
$sql = "INSERT INTO orders (title, type, content, received_at) VALUES (:title, :type, :content, :received_at)"; $sql = "INSERT INTO orders (title, type, content, received_at) VALUES (:title, :type, :content, :received_at)";
@@ -144,10 +150,13 @@ class OrderController
'type' => $typeLabel 'type' => $typeLabel
], $count); ], $count);
$conn->commit();
$_SESSION['flash_success'] = "Ordem criada com sucesso! $count domínios processados."; $_SESSION['flash_success'] = "Ordem criada com sucesso! $count domínios processados.";
View::redirect('/admin/orders'); View::redirect('/admin/orders');
} catch (\Exception $e) { } catch (\Exception $e) {
$conn->rollBack();
$_SESSION['flash_error'] = "Erro ao processar ordem: " . $e->getMessage(); $_SESSION['flash_error'] = "Erro ao processar ordem: " . $e->getMessage();
View::redirect('/admin/orders/create'); View::redirect('/admin/orders/create');
} }
@@ -211,4 +220,36 @@ class OrderController
readfile($filePath); readfile($filePath);
exit; exit;
} }
public function uploadAttachments($id)
{
$orderModel = new \App\Models\Order();
$order = $orderModel->find($id);
if (!$order) {
$_SESSION['flash_error'] = "Ordem não encontrada.";
View::redirect('/admin/orders');
return;
}
try {
if (isset($_FILES['new_attachments']) && !empty($_FILES['new_attachments']['name'][0])) {
$attachments = $_FILES['new_attachments'];
$attachmentService = new AttachmentService();
$savedCount = $attachmentService->storeFiles((int) $id, $attachments);
if ($savedCount > 0) {
$_SESSION['flash_success'] = "{$savedCount} anexo(s) adicionado(s) com sucesso à ordem!";
} else {
$_SESSION['flash_error'] = "Nenhum arquivo válido foi recebido.";
}
} else {
$_SESSION['flash_error'] = "Nenhum arquivo foi selecionado.";
}
} catch (\Exception $e) {
$_SESSION['flash_error'] = "Erro de upload: " . $e->getMessage();
}
View::redirect('/admin/orders/view/' . $id);
}
} }

View File

@@ -22,8 +22,23 @@ class AttachmentService
'text/plain' => 'txt', 'text/plain' => 'txt',
]; ];
/** Tamanho máximo por arquivo: 20 MB */ /**
private const MAX_SIZE = 20 * 1024 * 1024; * Retorna o tamanho máximo de upload, lendo do PHP ini.
*/
private function getMaxSize(): int
{
$val = ini_get('upload_max_filesize');
if (empty($val)) return 20 * 1024 * 1024;
$val = trim($val);
$last = strtolower($val[strlen($val)-1]);
$val = (int)$val;
switch($last) {
case 'g': $val *= 1024;
case 'm': $val *= 1024;
case 'k': $val *= 1024;
}
return $val > 0 ? $val : 20 * 1024 * 1024;
}
/** /**
* Raiz do diretório de uploads (fora do public/). * Raiz do diretório de uploads (fora do public/).
@@ -81,8 +96,10 @@ class AttachmentService
throw new \Exception("Erro no upload do arquivo '{$file['name']}': código {$file['error']}"); throw new \Exception("Erro no upload do arquivo '{$file['name']}': código {$file['error']}");
} }
if ($file['size'] > self::MAX_SIZE) { $maxSize = $this->getMaxSize();
throw new \Exception("O arquivo '{$file['name']}' excede o tamanho máximo permitido de 20 MB."); if ($file['size'] > $maxSize) {
$maxSizeMB = floor($maxSize / (1024 * 1024));
throw new \Exception("O arquivo '{$file['name']}' excede o tamanho máximo permitido de {$maxSizeMB} MB.");
} }
// Detecta o tipo MIME real do arquivo (não confiar só no header HTTP) // Detecta o tipo MIME real do arquivo (não confiar só no header HTTP)

View File

@@ -36,7 +36,10 @@ class OrderProcessor
$domains = array_unique($domains); // Remove duplicates in batch $domains = array_unique($domains); // Remove duplicates in batch
$conn->beginTransaction(); $inTransaction = $conn->inTransaction();
if (!$inTransaction) {
$conn->beginTransaction();
}
try { try {
$stmtCheck = $conn->prepare("SELECT id FROM domains WHERE name = :name"); $stmtCheck = $conn->prepare("SELECT id FROM domains WHERE name = :name");
@@ -77,11 +80,15 @@ class OrderProcessor
]); ]);
} }
$conn->commit(); if (!$inTransaction) {
$conn->commit();
}
return count($domains); return count($domains);
} catch (\Exception $e) { } catch (\Exception $e) {
$conn->rollBack(); if (!$inTransaction) {
$conn->rollBack();
}
throw $e; throw $e;
} }
} }

View File

@@ -87,6 +87,9 @@ $router->addMiddleware(\App\Middleware\AdminMiddleware::class);
$router->get('/admin/orders/attachments/{id}/download', [\App\Controllers\OrderController::class, 'downloadAttachment']); $router->get('/admin/orders/attachments/{id}/download', [\App\Controllers\OrderController::class, 'downloadAttachment']);
$router->addMiddleware(\App\Middleware\AdminMiddleware::class); $router->addMiddleware(\App\Middleware\AdminMiddleware::class);
$router->post('/admin/orders/attachments/upload/{id}', [\App\Controllers\OrderController::class, 'uploadAttachments']);
$router->addMiddleware(\App\Middleware\AdminMiddleware::class);
// Settings // Settings
$router->get('/admin/settings', [\App\Controllers\SettingsController::class, 'index']); $router->get('/admin/settings', [\App\Controllers\SettingsController::class, 'index']);
$router->addMiddleware(\App\Middleware\AdminMiddleware::class); $router->addMiddleware(\App\Middleware\AdminMiddleware::class);

84
doc/ATUALIZACAO.md Normal file
View File

@@ -0,0 +1,84 @@
# Procedimento de Atualização
Este documento descreve os passos necessários para atualizar a aplicação DNSBlock para a nova versão, incluindo a aplicação de migrações no banco de dados.
## 1. Backup do Banco de Dados (Recomendado)
Antes de qualquer atualização, é altamente recomendável fazer um backup do banco de dados atual para evitar perda de dados em caso de falhas.
```bash
make backup
# O backup será salvo na pasta backups/
```
## 2. Atualização do Código-Fonte
Baixe as atualizações mais recentes do repositório (se estiver usando git) ou substitua os arquivos da aplicação pela nova versão.
```bash
git pull origin main
```
## 3. Reconstrução e Reinício dos Containers
Após atualizar o código, é fundamental reconstruir a imagem Docker da aplicação e reiniciar os containers. Isso garante que as novas configurações de ambiente (como aumento do limite de upload do PHP) sejam aplicadas corretamente.
```bash
docker compose up -d --build
```
*(Nota: O comando `make restart` do Makefile apenas reinicia os containers, mas não força a reconstrução da imagem. Portanto, para atualizações de versão, utilize o comando acima).*
## 4. Aplicação das Migrações de Banco de Dados (SQL)
As atualizações que alteram a estrutura do banco de dados incluem arquivos `.sql` na pasta `database/migrations/`. Para aplicá-los, utilize o comando `docker exec` direcionando o arquivo para o container do MySQL.
Para aplicar a migração mais recente referente ao suporte a anexos nas ordens judiciais, execute na raiz do projeto:
```bash
docker exec -i dnsblock-db mysql -u root -p"$(grep DB_ROOT_PASSWORD .env | cut -d '=' -f2 | tr -d '\r')" dnsblock < database/migrations/2026_03_12_create_order_attachments.sql
```
**Regra Geral para Migrações:**
Caso existam múltiplas migrações que ainda não foram aplicadas em seu ambiente de produção, aplique-as uma a uma, respeitando a ordem cronológica dos prefixos dos arquivos (ex: `2025_...`, `2026_...`). Exemplo genérico:
```bash
docker exec -i dnsblock-db mysql -u root -p"$(grep DB_ROOT_PASSWORD .env | cut -d '=' -f2 | tr -d '\r')" dnsblock < database/migrations/ARQUIVO_DE_MIGRACAO.sql
```
## 5. Permissões de Diretório (Armazenamento Persistente)
Para garantir que a aplicação consiga salvar arquivos enviados (como os PDFs de anexos) no diretório mapeado para o hospedeiro, é obrigatório ajustar as permissões do diretório `storage`.
Após os containers estarem em execução, rode o seguinte comando na raiz do projeto para conceder a posse e permissão correta ao usuário do container web (`www-data`):
```bash
docker exec dnsblock-app chown -R www-data:www-data /var/www/html/storage
docker exec dnsblock-app chmod -R 775 /var/www/html/storage
```
## 6. Validação da Atualização
1. Acesse o sistema pelo navegador.
2. Limpe o cache do navegador (CTRL + F5) para garantir o carregamento de novos scripts, se houver.
3. Valide o funcionamento das novas entregas (ex: anexe um arquivo grande na página de Ordens Judiciais).
4. Verifique os logs se encontrar algum problema de comunicação:
```bash
make logs
```
## 7. Limpeza e Otimização (Opcional)
Após a atualização bem-sucedida, é recomendado realizar uma limpeza no ambiente Docker para remover compilações antigas (imagens) e dados que não são mais utilizados, liberando espaço em disco no servidor.
**Atenção:** Os comandos abaixo não afetarão conteinêres em execução nem os volumes que estão associados a eles (seus dados estarão seguros).
Para remover **imagens antigas**, containers parados e redes não utilizadas:
```bash
docker system prune -f
```
Se você mudou volumes recentemente (como na migração do banco de dados) e tem certeza de que os **volumes antigos não são mais necessários**:
```bash
docker volume prune -f
```
*(Nota: O comando de volumes remove TODOS os volumes Docker na máquina que não estejam atrelados a um container ativo. Use com cautela se houver outras aplicações no mesmo servidor).*

View File

@@ -12,6 +12,7 @@ services:
- .env - .env
volumes: volumes:
- ./:/var/www/html - ./:/var/www/html
- ./docker/php/custom.ini:/usr/local/etc/php/conf.d/custom.ini
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
networks: networks:

3
docker/php/custom.ini Normal file
View File

@@ -0,0 +1,3 @@
upload_max_filesize = 40M
post_max_size = 50M
session.gc_maxlifetime = 14400

View File

@@ -4,8 +4,30 @@ $errorMsg = $_SESSION['flash_error'] ?? '';
if ($hasError) { if ($hasError) {
unset($_SESSION['flash_error']); // Prevent toast from showing unset($_SESSION['flash_error']); // Prevent toast from showing
} }
function getBytesFromIni($val) {
if (empty($val)) return 0;
$val = trim($val);
$last = strtolower($val[strlen($val)-1]);
$val = (int)$val;
switch($last) {
case 'g': $val *= 1024;
case 'm': $val *= 1024;
case 'k': $val *= 1024;
}
return $val;
}
$maxUploadSizeBytes = getBytesFromIni(ini_get('upload_max_filesize'));
$maxPostSizeBytes = getBytesFromIni(ini_get('post_max_size'));
// Fallbacks caso não consiga ler corretamente
if ($maxUploadSizeBytes <= 0) $maxUploadSizeBytes = 20 * 1024 * 1024;
if ($maxPostSizeBytes <= 0) $maxPostSizeBytes = 20 * 1024 * 1024;
$maxUploadSizeMB = floor($maxUploadSizeBytes / (1024 * 1024));
?> ?>
<div class="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-6" x-data='orderForm(<?= $hasError ? 'true' : 'false' ?>, <?= json_encode($errorMsg) ?>)'> <div class="max-w-3xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-6" x-data='orderForm(<?= $hasError ? 'true' : 'false' ?>, <?= htmlspecialchars(json_encode($errorMsg), ENT_QUOTES, 'UTF-8') ?>)'>
<!-- Modal de Erro --> <!-- Modal de Erro -->
<div x-show="showModal" class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true" x-cloak> <div x-show="showModal" class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true" x-cloak>
@@ -115,7 +137,7 @@ if ($hasError) {
<label class="block text-sm font-medium text-gray-700 mb-1"> <label class="block text-sm font-medium text-gray-700 mb-1">
Anexos <span class="text-gray-400 font-normal">(PDF, imagens, documentos — opcional)</span> Anexos <span class="text-gray-400 font-normal">(PDF, imagens, documentos — opcional)</span>
</label> </label>
<p class="text-xs text-gray-500 mb-2">Arquivos da ordem judicial (decisão, ofícios, etc.). Tipos aceitos: PDF, PNG, JPG, GIF, DOC, DOCX, XLS, XLSX, TXT — máx. 20 MB por arquivo.</p> <p class="text-xs text-gray-500 mb-2">Arquivos da ordem judicial (decisão, ofícios, etc.). Tipos aceitos: PDF, PNG, JPG, GIF, DOC, DOCX, XLS, XLSX, TXT — máx. <?= $maxUploadSizeMB ?> MB por arquivo.</p>
<div x-data="{ attachNames: [] }" class="mt-1"> <div x-data="{ attachNames: [] }" class="mt-1">
<label class="flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer hover:border-primary-500 transition-colors bg-white"> <label class="flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer hover:border-primary-500 transition-colors bg-white">
<div class="space-y-1 text-center w-full"> <div class="space-y-1 text-center w-full">
@@ -188,12 +210,13 @@ if ($hasError) {
validateForm(e) { validateForm(e) {
let totalSize = 0; let totalSize = 0;
let maxSize = 20 * 1024 * 1024; // 20 MB let maxSize = <?= $maxUploadSizeBytes ?>;
let maxPostSize = <?= $maxPostSizeBytes ?>;
let attachments = document.querySelector('input[name=\'attachments[]\']').files; let attachments = document.querySelector('input[name=\'attachments[]\']').files;
for(let i = 0; i < attachments.length; i++) { for(let i = 0; i < attachments.length; i++) {
if (attachments[i].size > maxSize) { if (attachments[i].size > maxSize) {
this.showError('Arquivo Muito Grande', 'O anexo \'' + attachments[i].name + '\' excede o limite permitido de 20 MB.'); this.showError('Arquivo Muito Grande', 'O anexo \'' + attachments[i].name + '\' excede o limite permitido de <?= $maxUploadSizeMB ?> MB.');
e.preventDefault(); e.preventDefault();
return false; return false;
} }
@@ -205,7 +228,7 @@ if ($hasError) {
totalSize += csvFile.size; totalSize += csvFile.size;
} }
if (totalSize > 40 * 1024 * 1024) { // 40 MB limite de segurança frontend if (totalSize > maxPostSize) {
this.showError('Tamanho Total Excedido', 'O tamanho total de todos os arquivos excede o limite de submissão do servidor.'); this.showError('Tamanho Total Excedido', 'O tamanho total de todos os arquivos excede o limite de submissão do servidor.');
e.preventDefault(); e.preventDefault();
return false; return false;

View File

@@ -29,7 +29,6 @@
</div> </div>
</div> </div>
<?php if (!empty($attachments)): ?>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 bg-gray-50 flex items-center gap-2"> <div class="px-6 py-4 border-b border-gray-100 bg-gray-50 flex items-center gap-2">
<svg class="w-4 h-4 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="w-4 h-4 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
@@ -37,6 +36,8 @@
</svg> </svg>
<h3 class="text-sm font-semibold text-gray-800">Anexos (<?= count($attachments) ?>)</h3> <h3 class="text-sm font-semibold text-gray-800">Anexos (<?= count($attachments) ?>)</h3>
</div> </div>
<?php if (!empty($attachments)): ?>
<ul class="divide-y divide-gray-100"> <ul class="divide-y divide-gray-100">
<?php foreach ($attachments as $attachment): ?> <?php foreach ($attachments as $attachment): ?>
<li class="flex items-center justify-between px-6 py-3 hover:bg-gray-50 transition-colors"> <li class="flex items-center justify-between px-6 py-3 hover:bg-gray-50 transition-colors">
@@ -60,8 +61,21 @@
</li> </li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>
<?php else: ?>
<div class="px-6 py-4 text-sm text-gray-500">
Nenhum anexo salvo para esta ordem.
</div>
<?php endif; ?>
<div class="px-6 py-4 bg-gray-50 border-t border-gray-100">
<form action="/admin/orders/attachments/upload/<?= (int) $order['id'] ?>" method="POST" enctype="multipart/form-data" class="flex flex-col sm:flex-row items-center gap-3">
<input type="file" name="new_attachments[]" multiple class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-gray-200 file:text-gray-700 hover:file:bg-gray-300 cursor-pointer" accept=".pdf,.png,.jpg,.jpeg,.gif,.doc,.docx,.xls,.xlsx,.txt" required>
<button type="submit" class="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-semibold hover:bg-primary-700 w-full sm:w-auto flex-shrink-0 transition-colors">
Adicionar Anexos
</button>
</form>
</div>
</div> </div>
<?php endif; ?>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 bg-gray-50"> <div class="px-6 py-4 border-b border-gray-100 bg-gray-50">