Compare commits
3 Commits
6142f0cc65
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1356302060 | ||
|
|
d709d4cb09 | ||
|
|
b4799a1d42 |
86
MIGRATION.md
86
MIGRATION.md
@@ -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
|
|
||||||
```
|
|
||||||
@@ -116,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)";
|
||||||
@@ -149,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');
|
||||||
}
|
}
|
||||||
@@ -216,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ class OrderProcessor
|
|||||||
|
|
||||||
$domains = array_unique($domains); // Remove duplicates in batch
|
$domains = array_unique($domains); // Remove duplicates in batch
|
||||||
|
|
||||||
|
$inTransaction = $conn->inTransaction();
|
||||||
|
if (!$inTransaction) {
|
||||||
$conn->beginTransaction();
|
$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
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$inTransaction) {
|
||||||
$conn->commit();
|
$conn->commit();
|
||||||
|
}
|
||||||
return count($domains);
|
return count($domains);
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
if (!$inTransaction) {
|
||||||
$conn->rollBack();
|
$conn->rollBack();
|
||||||
|
}
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -4,6 +4,28 @@ $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' ?>, <?= htmlspecialchars(json_encode($errorMsg), ENT_QUOTES, 'UTF-8') ?>)'>
|
<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') ?>)'>
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,9 +61,22 @@
|
|||||||
</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>
|
</div>
|
||||||
<?php endif; ?>
|
<?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 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">
|
||||||
<h3 class="text-lg font-semibold text-gray-800">Domínios Afetados (<?= count($items) ?>)</h3>
|
<h3 class="text-lg font-semibold text-gray-800">Domínios Afetados (<?= count($items) ?>)</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user