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;
|
||||
}
|
||||
270
frontend/src/components/admin/BackupManager.tsx
Normal file
270
frontend/src/components/admin/BackupManager.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
|
||||
interface BackupInfo {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
date: string;
|
||||
size: number;
|
||||
filename: string;
|
||||
status: 'success' | 'error';
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface BackupsListResponse {
|
||||
success: boolean;
|
||||
backups: BackupInfo[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function BackupManager() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [backups, setBackups] = useState<BackupInfo[]>([]);
|
||||
const [listLoading, setListLoading] = useState(true);
|
||||
const { success, error: showError } = useToast();
|
||||
|
||||
// Buscar lista de backups ao carregar
|
||||
useEffect(() => {
|
||||
fetchBackups();
|
||||
}, []);
|
||||
|
||||
const fetchBackups = async () => {
|
||||
try {
|
||||
setListLoading(true);
|
||||
const token = localStorage.getItem('auth_token') || '';
|
||||
|
||||
const response = await fetch('/api/backup', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data: BackupsListResponse = await response.json();
|
||||
setBackups(data.backups || []);
|
||||
} else {
|
||||
showError('Erro ao carregar backups');
|
||||
}
|
||||
} catch (err) {
|
||||
showError('Erro ao carregar backups: ' + (err as Error).message);
|
||||
} finally {
|
||||
setListLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const createBackup = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem('auth_token') || '';
|
||||
|
||||
const response = await fetch('/api/backup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok || response.status === 201) {
|
||||
success('Backup criado com sucesso!');
|
||||
// Atualizar lista de backups
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await fetchBackups();
|
||||
} else {
|
||||
showError(data.error || 'Erro ao criar backup');
|
||||
}
|
||||
} catch (err) {
|
||||
showError('Erro ao criar backup: ' + (err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteBackup = async (backupId: string) => {
|
||||
if (!confirm('Tem certeza que deseja remover este backup?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token') || '';
|
||||
|
||||
const response = await fetch(`/api/backup?id=${backupId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
success('Backup removido com sucesso!');
|
||||
await fetchBackups();
|
||||
} else {
|
||||
showError(data.error || 'Erro ao remover backup');
|
||||
}
|
||||
} catch (err) {
|
||||
showError('Erro ao remover backup: ' + (err as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadBackup = async (filename: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token') || '';
|
||||
|
||||
const response = await fetch(`/api/backup/download?file=${filename}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} else {
|
||||
showError('Erro ao baixar backup');
|
||||
}
|
||||
} catch (err) {
|
||||
showError('Erro ao baixar backup: ' + (err as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatDate = (isoString: string) => {
|
||||
return new Date(isoString).toLocaleString('pt-BR');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Botão de Criar Backup */}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 border border-blue-200 dark:border-blue-800 rounded-2xl p-6">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<i className="ri-database-backup-line text-blue-600 dark:text-blue-400 text-2xl"></i>
|
||||
Criar Novo Backup
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Realize um backup completo do banco de dados e arquivos MinIO
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={createBackup}
|
||||
disabled={loading}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap flex items-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<i className="ri-loader-4-line animate-spin"></i>
|
||||
Criando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="ri-download-cloud-2-line"></i>
|
||||
Criar Backup Agora
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lista de Backups */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<i className="ri-archive-line text-primary"></i>
|
||||
Backups Salvos ({backups.length})
|
||||
</h3>
|
||||
|
||||
{listLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<i className="ri-loader-4-line animate-spin text-primary text-3xl"></i>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-2">Carregando backups...</p>
|
||||
</div>
|
||||
) : backups.length === 0 ? (
|
||||
<div className="text-center py-12 bg-gray-50 dark:bg-white/5 rounded-xl border border-gray-200 dark:border-white/10">
|
||||
<i className="ri-file-archive-line text-gray-400 text-4xl mb-3 block"></i>
|
||||
<p className="text-gray-600 dark:text-gray-400">Nenhum backup realizado ainda</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{backups.map((backup) => (
|
||||
<div
|
||||
key={backup.id}
|
||||
className="bg-white dark:bg-secondary p-4 rounded-xl border border-gray-200 dark:border-white/10 hover:shadow-md transition-shadow flex items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
backup.status === 'success'
|
||||
? 'bg-green-500'
|
||||
: 'bg-red-500'
|
||||
}`}></div>
|
||||
<h4 className="font-bold text-gray-900 dark:text-white truncate">
|
||||
{backup.filename}
|
||||
</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<i className="ri-calendar-line mr-1"></i>
|
||||
{formatDate(backup.timestamp)}
|
||||
<span className="mx-2">•</span>
|
||||
<i className="ri-hard-drive-line mr-1"></i>
|
||||
{formatSize(backup.size)}
|
||||
</p>
|
||||
{backup.message && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
||||
{backup.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => downloadBackup(backup.filename)}
|
||||
title="Baixar backup"
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
<i className="ri-download-line text-xl"></i>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteBackup(backup.id)}
|
||||
title="Remover backup"
|
||||
className="p-2 hover:bg-red-100 dark:hover:bg-red-900/20 rounded-lg transition-colors text-red-600 dark:text-red-400"
|
||||
>
|
||||
<i className="ri-delete-bin-6-line text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Informações */}
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-4">
|
||||
<p className="text-sm text-amber-900 dark:text-amber-200">
|
||||
<i className="ri-information-line mr-2"></i>
|
||||
Os backups incluem banco de dados PostgreSQL e todos os arquivos do MinIO.
|
||||
Armazene-os em local seguro para recuperação em caso de necessidade.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -194,6 +194,70 @@
|
||||
"accept": "Accept",
|
||||
"decline": "Decline"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privacy Policy",
|
||||
"intro": "OCCTO Engineering values the privacy of its users and clients. This Privacy Policy describes how we collect, use and protect your personal information when using our website and services.",
|
||||
"section1": {
|
||||
"title": "1. Information Collection",
|
||||
"content": "We collect information that you provide directly to us, such as when you fill out our contact form, request a quote or subscribe to our newsletter. The information may include name, email, phone and details about your company or project."
|
||||
},
|
||||
"section2": {
|
||||
"title": "2. Use of Information",
|
||||
"intro": "We use the collected information to:",
|
||||
"items": [
|
||||
"Respond to your inquiries and quote requests;",
|
||||
"Provide information about our engineering services and technical reports;",
|
||||
"Improve the user experience on our website;",
|
||||
"Comply with legal and regulatory obligations."
|
||||
]
|
||||
},
|
||||
"section3": {
|
||||
"title": "3. Data Protection",
|
||||
"content": "We adopt appropriate technical and organizational security measures to protect your personal information against unauthorized access, alteration, disclosure or destruction."
|
||||
},
|
||||
"section4": {
|
||||
"title": "4. Sharing Information",
|
||||
"content": "We do not sell, trade or transfer your personal information to third parties, except when necessary for the provision of our services (eg: technical partners involved in a specific project) or when required by law."
|
||||
},
|
||||
"section5": {
|
||||
"title": "5. Cookies",
|
||||
"content": "Our website may use cookies to improve navigation and understand how visitors interact with our content. You can disable cookies in your browser settings if you prefer."
|
||||
},
|
||||
"section6": {
|
||||
"title": "6. Contact",
|
||||
"content": "If you have questions about this Privacy Policy, please contact us via email: contato@octto.com.br."
|
||||
},
|
||||
"lastUpdate": "Last updated: November 2025."
|
||||
},
|
||||
"terms": {
|
||||
"title": "Terms of Use",
|
||||
"intro": "Welcome to OCCTO Engineering's website. By accessing and using this website, you agree to comply with and be bound by the following Terms of Use. If you disagree with any part of these terms, please do not use our website.",
|
||||
"section1": {
|
||||
"title": "1. Website Use",
|
||||
"content": "The content of this website is for general informational purposes only about our mechanical engineering services, reports and projects. We reserve the right to modify or discontinue any aspect of the website at any time."
|
||||
},
|
||||
"section2": {
|
||||
"title": "2. Intellectual Property",
|
||||
"content": "All content on this website, including texts, graphics, logos, icons, images and software, is owned by OCCTO Engineering or its content providers and is protected by Brazilian and international copyright laws."
|
||||
},
|
||||
"section3": {
|
||||
"title": "3. Limitation of Liability",
|
||||
"content": "OCCTO Engineering is not responsible for any direct, indirect, incidental or consequential damages resulting from the use or inability to use this website or any information contained therein. The technical information provided on the website does not substitute professional consultation and the issuance of specific technical reports for each case."
|
||||
},
|
||||
"section4": {
|
||||
"title": "4. Links to Third Parties",
|
||||
"content": "Our website may contain links to third-party websites. These links are provided for your convenience only. OCCTO Engineering has no control over the content of these sites and assumes no responsibility for them."
|
||||
},
|
||||
"section5": {
|
||||
"title": "5. Changes to Terms",
|
||||
"content": "We may revise these Terms of Use at any time. By using this website, you agree to be bound by the current version of these Terms of Use."
|
||||
},
|
||||
"section6": {
|
||||
"title": "6. Applicable Law",
|
||||
"content": "These terms are governed by and interpreted in accordance with the laws of the Federative Republic of Brazil. Any dispute related to these terms will be submitted to the exclusive jurisdiction of the competent courts."
|
||||
},
|
||||
"lastUpdate": "Last updated: November 2025."
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
|
||||
@@ -194,6 +194,70 @@
|
||||
"accept": "Aceptar",
|
||||
"decline": "Rechazar"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Política de Privacidad",
|
||||
"intro": "OCCTO Ingeniería valora la privacidad de sus usuarios y clientes. Esta Política de Privacidad describe cómo recopilamos, utilizamos y protegemos su información personal al utilizar nuestro sitio web y servicios.",
|
||||
"section1": {
|
||||
"title": "1. Recopilación de Información",
|
||||
"content": "Recopilamos información que nos proporciona directamente, como cuando completa nuestro formulario de contacto, solicita un presupuesto o se suscribe a nuestro boletín informativo. La información puede incluir nombre, correo electrónico, teléfono y detalles sobre su empresa o proyecto."
|
||||
},
|
||||
"section2": {
|
||||
"title": "2. Uso de la Información",
|
||||
"intro": "Utilizamos la información recopilada para:",
|
||||
"items": [
|
||||
"Responder a sus consultas y solicitudes de presupuesto;",
|
||||
"Proporcionar información sobre nuestros servicios de ingeniería e informes técnicos;",
|
||||
"Mejorar la experiencia del usuario en nuestro sitio web;",
|
||||
"Cumplir con obligaciones legales y normativas."
|
||||
]
|
||||
},
|
||||
"section3": {
|
||||
"title": "3. Protección de Datos",
|
||||
"content": "Adoptamos medidas de seguridad técnicas y organizacionales apropiadas para proteger su información personal contra acceso no autorizado, alteración, divulgación o destrucción."
|
||||
},
|
||||
"section4": {
|
||||
"title": "4. Compartir Información",
|
||||
"content": "No vendemos, canjeamos ni transferimos su información personal a terceros, excepto cuando sea necesario para la prestación de nuestros servicios (ej: socios técnicos involucrados en un proyecto específico) o cuando lo exija la ley."
|
||||
},
|
||||
"section5": {
|
||||
"title": "5. Cookies",
|
||||
"content": "Nuestro sitio web puede utilizar cookies para mejorar la navegación y entender cómo los visitantes interactúan con nuestro contenido. Puede desactivar las cookies en la configuración de su navegador si lo prefiere."
|
||||
},
|
||||
"section6": {
|
||||
"title": "6. Contacto",
|
||||
"content": "Si tiene preguntas sobre esta Política de Privacidad, comuníquese con nosotros a través de correo electrónico: contato@octto.com.br."
|
||||
},
|
||||
"lastUpdate": "Última actualización: Noviembre de 2025."
|
||||
},
|
||||
"terms": {
|
||||
"title": "Términos de Uso",
|
||||
"intro": "Bienvenido al sitio web de OCCTO Ingeniería. Al acceder y utilizar este sitio web, usted acepta cumplir y estar vinculado a los siguientes Términos de Uso. Si no está de acuerdo con ninguna parte de estos términos, por favor no utilice nuestro sitio web.",
|
||||
"section1": {
|
||||
"title": "1. Uso del Sitio",
|
||||
"content": "El contenido de este sitio web es solo para propósitos informativos generales sobre nuestros servicios de ingeniería mecánica, informes y proyectos. Nos reservamos el derecho de alterar o descontinuar cualquier aspecto del sitio en cualquier momento."
|
||||
},
|
||||
"section2": {
|
||||
"title": "2. Propiedad Intelectual",
|
||||
"content": "Todo el contenido presente en este sitio web, incluidos textos, gráficos, logotipos, iconos, imágenes y software, es propiedad de OCCTO Ingeniería o de sus proveedores de contenido y está protegido por las leyes de derechos de autor de Brasil e internacionales."
|
||||
},
|
||||
"section3": {
|
||||
"title": "3. Limitación de Responsabilidad",
|
||||
"content": "OCCTO Ingeniería no se responsabiliza por ningún daño directo, indirecto, incidental o consecuente que resulte del uso o la incapacidad de usar este sitio web o cualquier información contenida en el mismo. La información técnica proporcionada en el sitio web no sustituye la consulta profesional y la emisión de informes técnicos específicos para cada caso."
|
||||
},
|
||||
"section4": {
|
||||
"title": "4. Enlaces a Terceros",
|
||||
"content": "Nuestro sitio web puede contener enlaces a sitios web de terceros. Estos enlaces se proporcionan solo para su conveniencia. OCCTO Ingeniería no tiene control sobre el contenido de estos sitios y no asume responsabilidad por ellos."
|
||||
},
|
||||
"section5": {
|
||||
"title": "5. Cambios en los Términos",
|
||||
"content": "Podemos revisar estos Términos de Uso en cualquier momento. Al utilizar este sitio web, usted acepta estar vinculado por la versión actual de estos Términos de Uso."
|
||||
},
|
||||
"section6": {
|
||||
"title": "6. Ley Aplicable",
|
||||
"content": "Estos términos se rigen e interpretan de acuerdo con las leyes de la República Federativa de Brasil. Cualquier disputa relacionada con estos términos será sometida a la jurisdicción exclusiva de los tribunales competentes."
|
||||
},
|
||||
"lastUpdate": "Última actualización: Noviembre de 2025."
|
||||
},
|
||||
"common": {
|
||||
"loading": "Cargando...",
|
||||
"error": "Error",
|
||||
|
||||
@@ -194,6 +194,70 @@
|
||||
"accept": "Aceitar",
|
||||
"decline": "Recusar"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Política de Privacidade",
|
||||
"intro": "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.",
|
||||
"section1": {
|
||||
"title": "1. Coleta de Informações",
|
||||
"content": "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."
|
||||
},
|
||||
"section2": {
|
||||
"title": "2. Uso das Informações",
|
||||
"intro": "Utilizamos as informações coletadas para:",
|
||||
"items": [
|
||||
"Responder a suas consultas e solicitações de orçamento;",
|
||||
"Fornecer informações sobre nossos serviços de engenharia e laudos técnicos;",
|
||||
"Melhorar a experiência do usuário em nosso site;",
|
||||
"Cumprir obrigações legais e regulatórias."
|
||||
]
|
||||
},
|
||||
"section3": {
|
||||
"title": "3. Proteção de Dados",
|
||||
"content": "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."
|
||||
},
|
||||
"section4": {
|
||||
"title": "4. Compartilhamento de Informações",
|
||||
"content": "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."
|
||||
},
|
||||
"section5": {
|
||||
"title": "5. Cookies",
|
||||
"content": "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."
|
||||
},
|
||||
"section6": {
|
||||
"title": "6. Contato",
|
||||
"content": "Se você tiver dúvidas sobre esta Política de Privacidade, entre em contato conosco através do e-mail: contato@octto.com.br."
|
||||
},
|
||||
"lastUpdate": "Última atualização: Novembro de 2025."
|
||||
},
|
||||
"terms": {
|
||||
"title": "Termos de Uso",
|
||||
"intro": "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.",
|
||||
"section1": {
|
||||
"title": "1. Uso do Site",
|
||||
"content": "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."
|
||||
},
|
||||
"section2": {
|
||||
"title": "2. Propriedade Intelectual",
|
||||
"content": "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."
|
||||
},
|
||||
"section3": {
|
||||
"title": "3. Limitação de Responsabilidade",
|
||||
"content": "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."
|
||||
},
|
||||
"section4": {
|
||||
"title": "4. Links para Terceiros",
|
||||
"content": "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."
|
||||
},
|
||||
"section5": {
|
||||
"title": "5. Alterações nos Termos",
|
||||
"content": "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."
|
||||
},
|
||||
"section6": {
|
||||
"title": "6. Legislação Aplicável",
|
||||
"content": "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."
|
||||
},
|
||||
"lastUpdate": "Última atualização: Novembro de 2025."
|
||||
},
|
||||
"common": {
|
||||
"loading": "Carregando...",
|
||||
"error": "Erro",
|
||||
|
||||
Reference in New Issue
Block a user