feat: adicionar sistema de backup e badge editável na página inicial
This commit is contained in:
@@ -54,7 +54,11 @@ export default function Home() {
|
||||
const hero = content?.hero || {
|
||||
title: 'Engenharia de Excelência para Seus Projetos',
|
||||
subtitle: 'Soluções completas em engenharia veicular, mecânica e segurança do trabalho com mais de 15 anos de experiência.',
|
||||
buttonText: 'Conheça Nossos Serviços'
|
||||
buttonText: 'Conheça Nossos Serviços',
|
||||
badge: {
|
||||
text: 'Coca-Cola',
|
||||
show: true
|
||||
}
|
||||
};
|
||||
|
||||
const features = content?.features || {
|
||||
@@ -163,10 +167,12 @@ export default function Home() {
|
||||
|
||||
<div className="container mx-auto px-4 relative z-20">
|
||||
<div className="max-w-3xl">
|
||||
<div className="inline-flex items-center gap-3 bg-white/10 backdrop-blur-md border border-white/20 rounded-full px-5 py-2 mb-8 hover:bg-white/20 transition-colors cursor-default">
|
||||
<i className="ri-verified-badge-fill text-primary text-xl"></i>
|
||||
<span className="text-sm font-bold tracking-wider uppercase text-white">{t('home.officialProvider')} <span className="text-primary">Coca-Cola</span></span>
|
||||
</div>
|
||||
{hero.badge?.show && (
|
||||
<div className="inline-flex items-center gap-3 bg-white/10 backdrop-blur-md border border-white/20 rounded-full px-5 py-2 mb-8 hover:bg-white/20 transition-colors cursor-default">
|
||||
<i className="ri-verified-badge-fill text-primary text-xl"></i>
|
||||
<span className="text-sm font-bold tracking-wider uppercase text-white">{t('home.officialProvider')} <span className="text-primary">{hero.badge.text}</span></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1 className="text-5xl md:text-6xl font-bold font-headline mb-6 leading-tight">
|
||||
{hero.title}
|
||||
|
||||
@@ -8,51 +8,51 @@ export default function PrivacyPolicy() {
|
||||
return (
|
||||
<main className="py-20 bg-white dark:bg-secondary transition-colors duration-300">
|
||||
<div className="container mx-auto px-4 max-w-4xl">
|
||||
<h1 className="text-4xl font-bold font-headline text-secondary dark:text-white mb-8">{t('footer.privacyPolicy')}</h1>
|
||||
<h1 className="text-4xl font-bold font-headline text-secondary dark:text-white mb-8">{t('privacy.title')}</h1>
|
||||
|
||||
<div className="prose prose-lg text-gray-600 dark:text-gray-300">
|
||||
<p className="mb-6">
|
||||
A Octto Engenharia valoriza a privacidade de seus usuários e clientes. Esta Política de Privacidade descreve como coletamos, usamos e protegemos suas informações pessoais ao utilizar nosso site e serviços.
|
||||
{t('privacy.intro')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">1. Coleta de Informações</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section1.title')}</h2>
|
||||
<p className="mb-4">
|
||||
Coletamos informações que você nos fornece diretamente, como quando preenche nosso formulário de contato, solicita um orçamento ou se inscreve em nossa newsletter. As informações podem incluir nome, e-mail, telefone e detalhes sobre sua empresa ou projeto.
|
||||
{t('privacy.section1.content')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">2. Uso das Informações</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section2.title')}</h2>
|
||||
<p className="mb-4">
|
||||
Utilizamos as informações coletadas para:
|
||||
{t('privacy.section2.intro')}
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-6 space-y-2">
|
||||
<li>Responder a suas consultas e solicitações de orçamento;</li>
|
||||
<li>Fornecer informações sobre nossos serviços de engenharia e laudos técnicos;</li>
|
||||
<li>Melhorar a experiência do usuário em nosso site;</li>
|
||||
<li>Cumprir obrigações legais e regulatórias.</li>
|
||||
<li>{t('privacy.section2.items.0')}</li>
|
||||
<li>{t('privacy.section2.items.1')}</li>
|
||||
<li>{t('privacy.section2.items.2')}</li>
|
||||
<li>{t('privacy.section2.items.3')}</li>
|
||||
</ul>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">3. Proteção de Dados</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section3.title')}</h2>
|
||||
<p className="mb-4">
|
||||
Adotamos medidas de segurança técnicas e organizacionais adequadas para proteger seus dados pessoais contra acesso não autorizado, alteração, divulgação ou destruição.
|
||||
{t('privacy.section3.content')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">4. Compartilhamento de Informações</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section4.title')}</h2>
|
||||
<p className="mb-4">
|
||||
Não vendemos, trocamos ou transferimos suas informações pessoais para terceiros, exceto quando necessário para a prestação de nossos serviços (ex: parceiros técnicos envolvidos em um projeto específico) ou quando exigido por lei.
|
||||
{t('privacy.section4.content')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">5. Cookies</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section5.title')}</h2>
|
||||
<p className="mb-4">
|
||||
Nosso site pode utilizar cookies para melhorar a navegação e entender como os visitantes interagem com nosso conteúdo. Você pode desativar os cookies nas configurações do seu navegador, se preferir.
|
||||
{t('privacy.section5.content')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">6. Contato</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section6.title')}</h2>
|
||||
<p className="mb-4">
|
||||
Se você tiver dúvidas sobre esta Política de Privacidade, entre em contato conosco através do e-mail: contato@octto.com.br.
|
||||
{t('privacy.section6.content')}
|
||||
</p>
|
||||
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-12">
|
||||
Última atualização: Novembro de 2025.
|
||||
{t('privacy.lastUpdate')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,45 +8,45 @@ export default function TermsOfUse() {
|
||||
return (
|
||||
<main className="py-20 bg-white dark:bg-secondary transition-colors duration-300">
|
||||
<div className="container mx-auto px-4 max-w-4xl">
|
||||
<h1 className="text-4xl font-bold font-headline text-secondary dark:text-white mb-8">{t('footer.termsOfUse')}</h1>
|
||||
<h1 className="text-4xl font-bold font-headline text-secondary dark:text-white mb-8">{t('terms.title')}</h1>
|
||||
|
||||
<div className="prose prose-lg text-gray-600 dark:text-gray-300">
|
||||
<p className="mb-6">
|
||||
Bem-vindo ao site da Octto Engenharia. Ao acessar e utilizar este site, você concorda em cumprir e estar vinculado aos seguintes Termos de Uso. Se você não concordar com qualquer parte destes termos, por favor, não utilize nosso site.
|
||||
{t('terms.intro')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">1. Uso do Site</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section1.title')}</h2>
|
||||
<p className="mb-4">
|
||||
O conteúdo deste site é apenas para fins informativos gerais sobre nossos serviços de engenharia mecânica, laudos e projetos. Reservamo-nos o direito de alterar ou descontinuar qualquer aspecto do site a qualquer momento.
|
||||
{t('terms.section1.content')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">2. Propriedade Intelectual</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section2.title')}</h2>
|
||||
<p className="mb-4">
|
||||
Todo o conteúdo presente neste site, incluindo textos, gráficos, logotipos, ícones, imagens e software, é propriedade da Octto Engenharia ou de seus fornecedores de conteúdo e é protegido pelas leis de direitos autorais do Brasil e internacionais.
|
||||
{t('terms.section2.content')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">3. Limitação de Responsabilidade</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section3.title')}</h2>
|
||||
<p className="mb-4">
|
||||
A Octto Engenharia não se responsabiliza por quaisquer danos diretos, indiretos, incidentais ou consequenciais resultantes do uso ou da incapacidade de uso deste site ou de qualquer informação nele contida. As informações técnicas fornecidas no site não substituem a consulta profissional e a emissão de laudos técnicos específicos para cada caso.
|
||||
{t('terms.section3.content')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">4. Links para Terceiros</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section4.title')}</h2>
|
||||
<p className="mb-4">
|
||||
Nosso site pode conter links para sites de terceiros. Estes links são fornecidos apenas para sua conveniência. A Octto Engenharia não tem controle sobre o conteúdo desses sites e não assume responsabilidade por eles.
|
||||
{t('terms.section4.content')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">5. Alterações nos Termos</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section5.title')}</h2>
|
||||
<p className="mb-4">
|
||||
Podemos revisar estes Termos de Uso a qualquer momento. Ao utilizar este site, você concorda em ficar vinculado à versão atual desses Termos de Uso.
|
||||
{t('terms.section5.content')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">6. Legislação Aplicável</h2>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section6.title')}</h2>
|
||||
<p className="mb-4">
|
||||
Estes termos são regidos e interpretados de acordo com as leis da República Federativa do Brasil. Qualquer disputa relacionada a estes termos será submetida à jurisdição exclusiva dos tribunais competentes.
|
||||
{t('terms.section6.content')}
|
||||
</p>
|
||||
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-12">
|
||||
Última atualização: Novembro de 2025.
|
||||
{t('terms.lastUpdate')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
import { BackupManager } from '@/components/admin/BackupManager';
|
||||
|
||||
const PRESET_COLORS = [
|
||||
{ name: 'Laranja (Padrão)', value: '#FF6B35', gradient: 'from-orange-500 to-orange-600' },
|
||||
@@ -252,6 +253,20 @@ export default function ConfiguracoesPage() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backup Manager Section */}
|
||||
<div className="border-t-2 border-gray-200 dark:border-white/10 pt-12">
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="w-12 h-12 bg-linear-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/30">
|
||||
<i className="ri-database-backup-line text-2xl text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white">Backup & Restauração</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400 mt-1">Gerencie backups completos do seu banco de dados e arquivos</p>
|
||||
</div>
|
||||
</div>
|
||||
<BackupManager />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -250,6 +250,10 @@ interface HomeContent {
|
||||
subtitle: string;
|
||||
buttonText: string;
|
||||
imageUrl?: string;
|
||||
badge?: {
|
||||
text: string;
|
||||
show: boolean;
|
||||
};
|
||||
};
|
||||
features: {
|
||||
pretitle: string;
|
||||
@@ -307,7 +311,11 @@ export default function EditHomePage() {
|
||||
hero: {
|
||||
title: 'Engenharia de Ponta para Seus Projetos',
|
||||
subtitle: 'Soluções completas em engenharia veicular, mecânica e segurança do trabalho.',
|
||||
buttonText: 'Conheça Nossos Serviços'
|
||||
buttonText: 'Conheça Nossos Serviços',
|
||||
badge: {
|
||||
text: 'Coca-Cola',
|
||||
show: true
|
||||
}
|
||||
},
|
||||
features: {
|
||||
pretitle: 'Diferenciais',
|
||||
@@ -371,7 +379,11 @@ export default function EditHomePage() {
|
||||
if (data.content) {
|
||||
// Mesclar com valores padrão para garantir que todas as propriedades existam
|
||||
setFormData(prevData => ({
|
||||
hero: data.content.hero || prevData.hero,
|
||||
hero: {
|
||||
...prevData.hero,
|
||||
...data.content.hero,
|
||||
badge: data.content.hero?.badge || prevData.hero.badge
|
||||
},
|
||||
features: data.content.features || prevData.features,
|
||||
services: data.content.services || prevData.services,
|
||||
about: data.content.about || prevData.about,
|
||||
@@ -602,6 +614,64 @@ export default function EditHomePage() {
|
||||
className="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-white/10 pt-6 mt-6">
|
||||
<h3 className="text-sm font-bold text-secondary dark:text-white mb-4 flex items-center gap-2">
|
||||
<i className="ri-verified-badge-fill text-primary"></i>
|
||||
Badge (Crachá) no Banner
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-white/5 rounded-xl border border-gray-200 dark:border-white/10">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hero.badge?.show || false}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
hero: {
|
||||
...formData.hero,
|
||||
badge: {
|
||||
...(formData.hero.badge || { text: '', show: false }),
|
||||
show: e.target.checked
|
||||
}
|
||||
}
|
||||
})}
|
||||
className="w-5 h-5 accent-primary cursor-pointer"
|
||||
/>
|
||||
<label className="text-sm font-bold text-gray-700 dark:text-gray-300 cursor-pointer">
|
||||
Exibir badge no banner principal
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{formData.hero.badge?.show && (
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">
|
||||
Texto da Badge (ex: Coca-Cola, Parceiro Oficial, etc.)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.hero.badge?.text || ''}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
hero: {
|
||||
...formData.hero,
|
||||
badge: {
|
||||
...(formData.hero.badge || { text: '', show: false }),
|
||||
text: e.target.value
|
||||
}
|
||||
}
|
||||
})}
|
||||
placeholder="Digite o nome da empresa ou parceiro"
|
||||
maxLength={50}
|
||||
className="w-full px-4 py-3 bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
{formData.hero.badge?.text?.length || 0}/50 caracteres
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
70
frontend/src/app/api/backup/download/route.ts
Normal file
70
frontend/src/app/api/backup/download/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { createReadStream } from 'fs';
|
||||
|
||||
const BACKUP_DIR = path.join(process.cwd(), '.backups');
|
||||
|
||||
/**
|
||||
* GET /api/backup/download?file=backup-filename.tar.gz
|
||||
* Faz download de um backup específico
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Não autorizado' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const filename = searchParams.get('file');
|
||||
|
||||
if (!filename) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Arquivo não especificado' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validar que o arquivo está dentro do diretório de backups (prevenir path traversal)
|
||||
const backupPath = path.resolve(path.join(BACKUP_DIR, filename));
|
||||
const resolvedBackupDir = path.resolve(BACKUP_DIR);
|
||||
|
||||
if (!backupPath.startsWith(resolvedBackupDir)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Acesso negado' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Arquivo não encontrado' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const stat = fs.statSync(backupPath);
|
||||
const fileStream = createReadStream(backupPath);
|
||||
|
||||
return new NextResponse(fileStream as any, {
|
||||
headers: {
|
||||
'Content-Type': 'application/gzip',
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
'Content-Length': stat.size.toString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[BACKUP] Erro ao fazer download:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
334
frontend/src/app/api/backup/route.ts
Normal file
334
frontend/src/app/api/backup/route.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { createReadStream } from 'fs';
|
||||
|
||||
// Variáveis de ambiente
|
||||
const POSTGRES_USER = process.env.POSTGRES_USER || 'admin';
|
||||
const POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD || 'adminpassword';
|
||||
const POSTGRES_DB = process.env.POSTGRES_DB || 'occto_db';
|
||||
const POSTGRES_HOST = process.env.POSTGRES_HOST || 'postgres';
|
||||
const POSTGRES_PORT = process.env.POSTGRES_PORT || '5432';
|
||||
|
||||
const MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || 'minio';
|
||||
const MINIO_PORT = process.env.MINIO_PORT || '9000';
|
||||
const MINIO_ACCESS_KEY = process.env.MINIO_ACCESS_KEY || 'admin';
|
||||
const MINIO_SECRET_KEY = process.env.MINIO_SECRET_KEY || 'adminpassword';
|
||||
const MINIO_BUCKET_NAME = process.env.MINIO_BUCKET_NAME || 'occto-images';
|
||||
const MINIO_USE_SSL = process.env.MINIO_USE_SSL === 'true';
|
||||
|
||||
// Diretório para armazenar backups
|
||||
const BACKUP_DIR = path.join(process.cwd(), '.backups');
|
||||
|
||||
// Criar diretório de backups se não existir
|
||||
if (!fs.existsSync(BACKUP_DIR)) {
|
||||
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
interface BackupInfo {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
date: string;
|
||||
size: number;
|
||||
filename: string;
|
||||
status: 'success' | 'error';
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface BackupResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
backup?: BackupInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/backup
|
||||
* Cria um backup completo do PostgreSQL e MinIO
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Validar autenticação (você pode adicionar verificação de token)
|
||||
const authHeader = request.headers.get('authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Não autorizado' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('-').slice(0, 4).join('-');
|
||||
const backupId = `backup-${timestamp}`;
|
||||
const backupPath = path.join(BACKUP_DIR, backupId);
|
||||
|
||||
// Criar pasta do backup
|
||||
fs.mkdirSync(backupPath, { recursive: true });
|
||||
|
||||
const backupInfo: BackupInfo = {
|
||||
id: backupId,
|
||||
timestamp: new Date().toISOString(),
|
||||
date: new Date().toLocaleDateString('pt-BR'),
|
||||
size: 0,
|
||||
filename: backupId,
|
||||
status: 'success',
|
||||
message: ''
|
||||
};
|
||||
|
||||
// 1. Fazer backup do PostgreSQL
|
||||
try {
|
||||
console.log('[BACKUP] Iniciando backup do PostgreSQL...');
|
||||
const pgBackupPath = path.join(backupPath, 'database.sql');
|
||||
|
||||
const pgCommand = `PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump -h ${POSTGRES_HOST} -U ${POSTGRES_USER} -d ${POSTGRES_DB} > "${pgBackupPath}"`;
|
||||
execSync(pgCommand, { stdio: 'pipe', env: { ...process.env, PGPASSWORD: POSTGRES_PASSWORD } });
|
||||
|
||||
console.log('[BACKUP] PostgreSQL backup concluído');
|
||||
} catch (error) {
|
||||
console.error('[BACKUP] Erro ao fazer backup do PostgreSQL:', error);
|
||||
backupInfo.status = 'error';
|
||||
backupInfo.message += `Erro PostgreSQL: ${(error as Error).message}. `;
|
||||
}
|
||||
|
||||
// 2. Fazer backup do MinIO (copiar os dados)
|
||||
try {
|
||||
console.log('[BACKUP] Iniciando backup do MinIO...');
|
||||
const minioBackupPath = path.join(backupPath, 'minio-data');
|
||||
|
||||
// Se estiver usando Docker, copiar do volume
|
||||
// Se estiver local, copiar do diretório minio_data
|
||||
const minioDataPath = path.join(process.cwd(), '..', 'minio_data');
|
||||
|
||||
if (fs.existsSync(minioDataPath)) {
|
||||
// Copiar recursivamente
|
||||
copyDirSync(minioDataPath, minioBackupPath);
|
||||
console.log('[BACKUP] MinIO backup concluído');
|
||||
} else {
|
||||
console.warn('[BACKUP] Diretório MinIO não encontrado em:', minioDataPath);
|
||||
backupInfo.message += `Aviso: MinIO local não encontrado. `;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BACKUP] Erro ao fazer backup do MinIO:', error);
|
||||
backupInfo.message += `Erro MinIO: ${(error as Error).message}. `;
|
||||
}
|
||||
|
||||
// 3. Criar arquivo metadata.json
|
||||
const metadataPath = path.join(backupPath, 'metadata.json');
|
||||
fs.writeFileSync(metadataPath, JSON.stringify({
|
||||
timestamp: backupInfo.timestamp,
|
||||
database: POSTGRES_DB,
|
||||
hostname: POSTGRES_HOST,
|
||||
minioEndpoint: MINIO_ENDPOINT,
|
||||
version: '1.0'
|
||||
}, null, 2));
|
||||
|
||||
// 4. Calcular tamanho do backup
|
||||
const size = calculateDirSize(backupPath);
|
||||
backupInfo.size = size;
|
||||
|
||||
// 5. Compactar backup (opcional - melhor para armazenamento)
|
||||
const compressBackup = true;
|
||||
if (compressBackup) {
|
||||
try {
|
||||
console.log('[BACKUP] Compactando backup...');
|
||||
const tarCommand = `tar -czf "${path.join(BACKUP_DIR, backupId)}.tar.gz" -C "${BACKUP_DIR}" "${backupId}"`;
|
||||
execSync(tarCommand, { stdio: 'pipe' });
|
||||
|
||||
// Remover pasta original após compactar
|
||||
fs.rmSync(backupPath, { recursive: true, force: true });
|
||||
|
||||
backupInfo.filename = `${backupId}.tar.gz`;
|
||||
console.log('[BACKUP] Compactação concluída');
|
||||
} catch (error) {
|
||||
console.warn('[BACKUP] Erro ao compactar:', error);
|
||||
backupInfo.message += `Aviso: Não foi possível compactar. `;
|
||||
}
|
||||
}
|
||||
|
||||
if (backupInfo.status === 'error' && backupInfo.message) {
|
||||
return NextResponse.json<BackupResponse>(
|
||||
{
|
||||
success: false,
|
||||
message: 'Backup concluído com erros',
|
||||
backup: backupInfo,
|
||||
error: backupInfo.message
|
||||
},
|
||||
{ status: 207 } // Multi-status
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json<BackupResponse>(
|
||||
{
|
||||
success: true,
|
||||
message: 'Backup realizado com sucesso',
|
||||
backup: backupInfo
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[BACKUP] Erro geral:', error);
|
||||
return NextResponse.json<BackupResponse>(
|
||||
{
|
||||
success: false,
|
||||
message: 'Erro ao criar backup',
|
||||
error: (error as Error).message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/backup
|
||||
* Lista os backups disponíveis
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Não autorizado' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(BACKUP_DIR);
|
||||
const backups: BackupInfo[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(BACKUP_DIR, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
if (stat.isFile()) {
|
||||
const timestamp = file.replace('backup-', '').replace('.tar.gz', '');
|
||||
backups.push({
|
||||
id: file.replace('.tar.gz', ''),
|
||||
timestamp: new Date(timestamp.replace(/-/g, ':')).toISOString(),
|
||||
date: new Date(timestamp.replace(/-/g, ':')).toLocaleDateString('pt-BR'),
|
||||
size: stat.size,
|
||||
filename: file,
|
||||
status: 'success',
|
||||
message: 'Backup disponível'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ordenar por data (mais recente primeiro)
|
||||
backups.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
backups,
|
||||
count: backups.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[BACKUP] Erro ao listar backups:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/backup?id=backup-id
|
||||
* Remove um backup específico
|
||||
*/
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Não autorizado' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const backupId = searchParams.get('id');
|
||||
|
||||
if (!backupId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'ID do backup não fornecido' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const backupPath = path.join(BACKUP_DIR, `${backupId}.tar.gz`);
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Backup não encontrado' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
fs.unlinkSync(backupPath);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Backup removido com sucesso'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[BACKUP] Erro ao deletar backup:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: (error as Error).message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Função auxiliar: copiar diretório recursivamente
|
||||
*/
|
||||
function copyDirSync(src: string, dest: string) {
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(src);
|
||||
|
||||
for (const file of files) {
|
||||
const srcPath = path.join(src, file);
|
||||
const destPath = path.join(dest, file);
|
||||
const stat = fs.statSync(srcPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
copyDirSync(srcPath, destPath);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Função auxiliar: calcular tamanho do diretório
|
||||
*/
|
||||
function calculateDirSize(dirPath: string): number {
|
||||
let size = 0;
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dirPath, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
size += calculateDirSize(filePath);
|
||||
} else {
|
||||
size += stat.size;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao calcular tamanho:', error);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
Reference in New Issue
Block a user