feat: implementar Factory Reset completo com wizard e sistema de backup

Backend:
- AccountDeletionController com 4 endpoints principais
- requestDeletionCode: Envia código de 6 dígitos por email (válido 10min)
- exportBackup: Exporta todos os dados do usuário em JSON
- executeHardDelete: Deleta permanentemente conta e dados com validação de código
- importBackup: Importa backup completo com mapeamento de IDs

Frontend:
- FactoryResetWizard: Wizard de 4 etapas (Warning → Backup → Code → Confirmation)
- ImportBackupModal: Drag & drop para importar backup JSON
- Integração na página Profile com seção de Gerenciamento de Dados
- accountDeletionService: Serviços API completos

Email:
- Template HTML para código de confirmação
- Avisos visuais sobre irreversibilidade da ação

i18n:
- Traduções completas em pt-BR, es, en
- 50+ strings de tradução adicionadas
- Avisos e mensagens de erro traduzidos

Funcionalidades:
 Hard delete com confirmação dupla (código + texto DELETAR)
 Backup completo em JSON (transações, contas, categorias, etc)
 Importação de backup com mapeamento inteligente de IDs
 Email com código de segurança
 Wizard responsivo com 4 etapas
 Validação de arquivos e tamanho (max 50MB)
 Drag & drop para upload
 Estatísticas de importação
 Logout automático após delete
This commit is contained in:
marco 2025-12-19 16:45:08 +01:00
parent 4ad7060323
commit 27f3bd8869
11 changed files with 1791 additions and 3 deletions

View File

@ -0,0 +1,526 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Mail\AccountDeletionConfirmation;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class AccountDeletionController extends Controller
{
/**
* Solicitar código de confirmação por email
*/
public function requestDeletionCode(Request $request)
{
try {
$user = Auth::user();
// Gerar código de 6 dígitos
$code = str_pad(random_int(100000, 999999), 6, '0', STR_PAD_LEFT);
// Armazenar no cache por 10 minutos
$cacheKey = "account_deletion_code_{$user->id}";
Cache::put($cacheKey, $code, now()->addMinutes(10));
// Enviar email
Mail::to($user->email)->send(new AccountDeletionConfirmation($code, $user->name));
return response()->json([
'success' => true,
'message' => 'Código de confirmação enviado para seu email',
'expires_at' => now()->addMinutes(10)->toISOString()
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Erro ao enviar código de confirmação',
'error' => $e->getMessage()
], 500);
}
}
/**
* Exportar backup completo dos dados do usuário
*/
public function exportBackup(Request $request)
{
try {
$user = Auth::user();
// Coletar todos os dados do usuário
$backup = [
'version' => '1.0',
'exported_at' => now()->toISOString(),
'user' => [
'name' => $user->name,
'email' => $user->email,
],
'accounts' => $user->accounts()->with('currency')->get(),
'asset_accounts' => $user->assetAccounts()->with('assetType')->get(),
'categories' => $user->categories()->get(),
'cost_centers' => $user->costCenters()->with('keywords')->where('is_system', false)->get(),
'credit_cards' => $user->creditCards()->with('account')->get(),
'liability_accounts' => $user->liabilityAccounts()->get(),
'budgets' => $user->budgets()->with(['category', 'subcategory'])->get(),
'goals' => $user->goals()->get(),
'investments' => $user->investments()->with(['assetAccount', 'investmentType', 'priceHistories'])->get(),
'transactions' => $user->transactions()
->with([
'account',
'category',
'subcategory',
'costCenter',
'creditCard',
'liabilityAccount'
])
->get(),
];
// Gerar nome do arquivo
$fileName = 'webmoney_backup_' . $user->id . '_' . now()->format('Y-m-d_His') . '.json';
// Salvar temporariamente
$filePath = 'backups/' . $fileName;
Storage::disk('local')->put($filePath, json_encode($backup, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// Retornar URL para download
return response()->json([
'success' => true,
'message' => 'Backup criado com sucesso',
'download_url' => route('download.backup', ['file' => $fileName]),
'file_size' => Storage::disk('local')->size($filePath),
'expires_in' => '24 horas'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Erro ao criar backup',
'error' => $e->getMessage()
], 500);
}
}
/**
* Download do arquivo de backup
*/
public function downloadBackup($file)
{
try {
$filePath = 'backups/' . $file;
if (!Storage::disk('local')->exists($filePath)) {
abort(404, 'Arquivo não encontrado');
}
return Storage::disk('local')->download($filePath);
} catch (\Exception $e) {
abort(500, 'Erro ao baixar backup');
}
}
/**
* Validar código e executar hard delete
*/
public function executeHardDelete(Request $request)
{
$request->validate([
'code' => 'required|string|size:6',
'confirmation_text' => 'required|string|in:DELETAR'
]);
try {
$user = Auth::user();
$cacheKey = "account_deletion_code_{$user->id}";
// Verificar código
$storedCode = Cache::get($cacheKey);
if (!$storedCode) {
return response()->json([
'success' => false,
'message' => 'Código expirado ou inválido'
], 400);
}
if ($storedCode !== $request->code) {
return response()->json([
'success' => false,
'message' => 'Código incorreto'
], 400);
}
// Limpar cache
Cache::forget($cacheKey);
// Executar hard delete em transação
DB::beginTransaction();
try {
// Deletar em ordem (respeitando foreign keys)
// 1. Transações
$user->transactions()->delete();
// 2. Orçamentos
$user->budgets()->delete();
// 3. Histórico de preços de investimentos
DB::table('investment_price_histories')
->whereIn('investment_id', $user->investments()->pluck('id'))
->delete();
// 4. Investimentos
$user->investments()->delete();
// 5. Cartões de crédito
$user->creditCards()->delete();
// 6. Contas de passivos
$user->liabilityAccounts()->delete();
// 7. Contas de ativos
$user->assetAccounts()->delete();
// 8. Contas bancárias
$user->accounts()->delete();
// 9. Palavras-chave de centros de custo
$costCenterIds = $user->costCenters()->pluck('id');
DB::table('cost_center_keywords')->whereIn('cost_center_id', $costCenterIds)->delete();
// 10. Centros de custo (não-sistema)
$user->costCenters()->where('is_system', false)->delete();
// 11. Categorias customizadas
$user->categories()->where('is_custom', true)->delete();
// 12. Objetivos
$user->goals()->delete();
// 13. Tokens de autenticação
$user->tokens()->delete();
// 14. Usuário
$userName = $user->name;
$userEmail = $user->email;
$user->delete();
DB::commit();
return response()->json([
'success' => true,
'message' => 'Conta deletada permanentemente',
'deleted_user' => [
'name' => $userName,
'email' => $userEmail
]
]);
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Erro ao executar exclusão',
'error' => $e->getMessage()
], 500);
}
}
/**
* Importar backup para conta atual
*/
public function importBackup(Request $request)
{
$request->validate([
'backup_file' => 'required|file|mimes:json|max:51200' // Max 50MB
]);
try {
$user = Auth::user();
// Ler arquivo
$content = file_get_contents($request->file('backup_file')->getRealPath());
$backup = json_decode($content, true);
if (!$backup || !isset($backup['version'])) {
return response()->json([
'success' => false,
'message' => 'Arquivo de backup inválido'
], 400);
}
DB::beginTransaction();
try {
$stats = [
'accounts' => 0,
'asset_accounts' => 0,
'categories' => 0,
'cost_centers' => 0,
'credit_cards' => 0,
'liability_accounts' => 0,
'budgets' => 0,
'goals' => 0,
'investments' => 0,
'transactions' => 0,
];
// Mapear IDs antigos -> novos IDs
$accountMap = [];
$categoryMap = [];
$subcategoryMap = [];
$costCenterMap = [];
$creditCardMap = [];
$liabilityAccountMap = [];
$assetAccountMap = [];
// 1. Importar contas bancárias
if (isset($backup['accounts'])) {
foreach ($backup['accounts'] as $account) {
$oldId = $account['id'];
unset($account['id'], $account['user_id'], $account['created_at'], $account['updated_at']);
$newAccount = $user->accounts()->create($account);
$accountMap[$oldId] = $newAccount->id;
$stats['accounts']++;
}
}
// 2. Importar contas de ativos
if (isset($backup['asset_accounts'])) {
foreach ($backup['asset_accounts'] as $assetAccount) {
$oldId = $assetAccount['id'];
unset($assetAccount['id'], $assetAccount['user_id'], $assetAccount['created_at'], $assetAccount['updated_at'], $assetAccount['asset_type']);
$newAssetAccount = $user->assetAccounts()->create($assetAccount);
$assetAccountMap[$oldId] = $newAssetAccount->id;
$stats['asset_accounts']++;
}
}
// 3. Importar categorias customizadas
if (isset($backup['categories'])) {
foreach ($backup['categories'] as $category) {
if (!$category['is_custom']) continue;
$oldId = $category['id'];
$oldParentId = $category['parent_id'] ?? null;
unset($category['id'], $category['user_id'], $category['created_at'], $category['updated_at']);
// Se tem parent_id, precisa esperar para mapear depois
if ($oldParentId) {
$category['parent_id'] = null; // Temporário
}
$newCategory = $user->categories()->create($category);
if ($oldParentId) {
$subcategoryMap[$oldId] = ['new_id' => $newCategory->id, 'old_parent_id' => $oldParentId];
} else {
$categoryMap[$oldId] = $newCategory->id;
}
$stats['categories']++;
}
}
// Atualizar parent_id das subcategorias
foreach ($subcategoryMap as $oldId => $data) {
if (isset($categoryMap[$data['old_parent_id']])) {
DB::table('categories')
->where('id', $data['new_id'])
->update(['parent_id' => $categoryMap[$data['old_parent_id']]]);
$categoryMap[$oldId] = $data['new_id']; // Adicionar ao mapa principal
}
}
// 4. Importar centros de custo
if (isset($backup['cost_centers'])) {
foreach ($backup['cost_centers'] as $costCenter) {
$oldId = $costCenter['id'];
$keywords = $costCenter['keywords'] ?? [];
unset($costCenter['id'], $costCenter['user_id'], $costCenter['created_at'], $costCenter['updated_at'], $costCenter['keywords']);
$newCostCenter = $user->costCenters()->create($costCenter);
$costCenterMap[$oldId] = $newCostCenter->id;
// Importar keywords
foreach ($keywords as $keyword) {
unset($keyword['id'], $keyword['cost_center_id'], $keyword['created_at'], $keyword['updated_at']);
$newCostCenter->keywords()->create($keyword);
}
$stats['cost_centers']++;
}
}
// 5. Importar cartões de crédito
if (isset($backup['credit_cards'])) {
foreach ($backup['credit_cards'] as $creditCard) {
$oldId = $creditCard['id'];
$oldAccountId = $creditCard['account_id'];
unset($creditCard['id'], $creditCard['user_id'], $creditCard['created_at'], $creditCard['updated_at'], $creditCard['account']);
// Mapear account_id
if (isset($accountMap[$oldAccountId])) {
$creditCard['account_id'] = $accountMap[$oldAccountId];
} else {
continue; // Pular se conta não foi importada
}
$newCreditCard = $user->creditCards()->create($creditCard);
$creditCardMap[$oldId] = $newCreditCard->id;
$stats['credit_cards']++;
}
}
// 6. Importar contas de passivos
if (isset($backup['liability_accounts'])) {
foreach ($backup['liability_accounts'] as $liabilityAccount) {
$oldId = $liabilityAccount['id'];
unset($liabilityAccount['id'], $liabilityAccount['user_id'], $liabilityAccount['created_at'], $liabilityAccount['updated_at']);
$newLiabilityAccount = $user->liabilityAccounts()->create($liabilityAccount);
$liabilityAccountMap[$oldId] = $newLiabilityAccount->id;
$stats['liability_accounts']++;
}
}
// 7. Importar orçamentos
if (isset($backup['budgets'])) {
foreach ($backup['budgets'] as $budget) {
$oldCategoryId = $budget['category_id'];
$oldSubcategoryId = $budget['subcategory_id'] ?? null;
unset($budget['id'], $budget['user_id'], $budget['created_at'], $budget['updated_at'], $budget['category'], $budget['subcategory']);
// Mapear IDs
if (isset($categoryMap[$oldCategoryId])) {
$budget['category_id'] = $categoryMap[$oldCategoryId];
} else {
continue;
}
if ($oldSubcategoryId && isset($categoryMap[$oldSubcategoryId])) {
$budget['subcategory_id'] = $categoryMap[$oldSubcategoryId];
}
$user->budgets()->create($budget);
$stats['budgets']++;
}
}
// 8. Importar objetivos
if (isset($backup['goals'])) {
foreach ($backup['goals'] as $goal) {
unset($goal['id'], $goal['user_id'], $goal['created_at'], $goal['updated_at']);
$user->goals()->create($goal);
$stats['goals']++;
}
}
// 9. Importar investimentos
if (isset($backup['investments'])) {
foreach ($backup['investments'] as $investment) {
$oldId = $investment['id'];
$oldAssetAccountId = $investment['asset_account_id'];
$priceHistories = $investment['price_histories'] ?? [];
unset($investment['id'], $investment['user_id'], $investment['created_at'], $investment['updated_at'], $investment['asset_account'], $investment['investment_type'], $investment['price_histories']);
// Mapear asset_account_id
if (isset($assetAccountMap[$oldAssetAccountId])) {
$investment['asset_account_id'] = $assetAccountMap[$oldAssetAccountId];
} else {
continue;
}
$newInvestment = $user->investments()->create($investment);
// Importar histórico de preços
foreach ($priceHistories as $history) {
unset($history['id'], $history['investment_id'], $history['created_at'], $history['updated_at']);
$newInvestment->priceHistories()->create($history);
}
$stats['investments']++;
}
}
// 10. Importar transações
if (isset($backup['transactions'])) {
foreach ($backup['transactions'] as $transaction) {
$oldAccountId = $transaction['account_id'] ?? null;
$oldCategoryId = $transaction['category_id'] ?? null;
$oldSubcategoryId = $transaction['subcategory_id'] ?? null;
$oldCostCenterId = $transaction['cost_center_id'] ?? null;
$oldCreditCardId = $transaction['credit_card_id'] ?? null;
$oldLiabilityAccountId = $transaction['liability_account_id'] ?? null;
unset($transaction['id'], $transaction['user_id'], $transaction['created_at'], $transaction['updated_at'], $transaction['account'], $transaction['category'], $transaction['subcategory'], $transaction['cost_center'], $transaction['credit_card'], $transaction['liability_account']);
// Mapear IDs
if ($oldAccountId && isset($accountMap[$oldAccountId])) {
$transaction['account_id'] = $accountMap[$oldAccountId];
} elseif ($oldAccountId) {
continue;
}
if ($oldCategoryId && isset($categoryMap[$oldCategoryId])) {
$transaction['category_id'] = $categoryMap[$oldCategoryId];
}
if ($oldSubcategoryId && isset($categoryMap[$oldSubcategoryId])) {
$transaction['subcategory_id'] = $categoryMap[$oldSubcategoryId];
}
if ($oldCostCenterId && isset($costCenterMap[$oldCostCenterId])) {
$transaction['cost_center_id'] = $costCenterMap[$oldCostCenterId];
}
if ($oldCreditCardId && isset($creditCardMap[$oldCreditCardId])) {
$transaction['credit_card_id'] = $creditCardMap[$oldCreditCardId];
}
if ($oldLiabilityAccountId && isset($liabilityAccountMap[$oldLiabilityAccountId])) {
$transaction['liability_account_id'] = $liabilityAccountMap[$oldLiabilityAccountId];
}
$user->transactions()->create($transaction);
$stats['transactions']++;
}
}
DB::commit();
return response()->json([
'success' => true,
'message' => 'Backup importado com sucesso',
'stats' => $stats
]);
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Erro ao importar backup',
'error' => $e->getMessage()
], 500);
}
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class AccountDeletionConfirmation extends Mailable
{
use Queueable, SerializesModels;
public $code;
public $userName;
/**
* Create a new message instance.
*/
public function __construct($code, $userName)
{
$this->code = $code;
$this->userName = $userName;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Código de Confirmação - Exclusão de Conta',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'emails.account-deletion-confirmation',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Código de Confirmação</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 40px auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 24px;
}
.content {
padding: 40px 30px;
}
.warning-box {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}
.warning-box strong {
color: #856404;
}
.code-box {
background-color: #f8f9fa;
border: 2px solid #dc3545;
border-radius: 8px;
padding: 20px;
text-align: center;
margin: 30px 0;
}
.code {
font-size: 36px;
font-weight: bold;
color: #dc3545;
letter-spacing: 8px;
margin: 10px 0;
}
.info-text {
color: #6c757d;
font-size: 14px;
line-height: 1.6;
}
.footer {
background-color: #f8f9fa;
padding: 20px;
text-align: center;
color: #6c757d;
font-size: 12px;
}
.danger-list {
background-color: #f8d7da;
border-left: 4px solid #dc3545;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}
.danger-list ul {
margin: 10px 0;
padding-left: 20px;
}
.danger-list li {
color: #721c24;
margin: 5px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⚠️ EXCLUSÃO PERMANENTE DE CONTA</h1>
</div>
<div class="content">
<p>Olá, <strong>{{ $userName }}</strong></p>
<p class="info-text">
Você solicitou a exclusão <strong>PERMANENTE E IRREVERSÍVEL</strong> de sua conta no WebMoney.
</p>
<div class="warning-box">
<strong>⚠️ ATENÇÃO:</strong> Esta ação NÃO pode ser desfeita. Uma vez confirmada, todos os seus dados serão permanentemente deletados dos nossos servidores sem possibilidade de recuperação.
</div>
<div class="danger-list">
<strong>🗑️ Serão deletados permanentemente:</strong>
<ul>
<li>Todas as transações</li>
<li>Todas as contas bancárias e de ativos</li>
<li>Todos os cartões de crédito e contas de passivos</li>
<li>Orçamentos e categorias personalizadas</li>
<li>Centros de custo e palavras-chave</li>
<li>Objetivos financeiros</li>
<li>Investimentos e histórico de preços</li>
<li>Configurações e preferências</li>
<li>Sua conta de usuário</li>
</ul>
</div>
<p class="info-text">
Para confirmar esta ação, utilize o código abaixo:
</p>
<div class="code-box">
<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px;">
SEU CÓDIGO DE CONFIRMAÇÃO
</div>
<div class="code">{{ $code }}</div>
<div style="font-size: 12px; color: #6c757d; margin-top: 10px;">
Válido por 10 minutos
</div>
</div>
<p class="info-text">
<strong>Não foi você?</strong> Se você não solicitou esta exclusão, ignore este email e sua conta permanecerá intacta. Recomendamos alterar sua senha imediatamente por segurança.
</p>
<p class="info-text">
<strong>Quer fazer backup?</strong> Antes de confirmar a exclusão, você pode exportar todos os seus dados para um arquivo JSON e importá-los posteriormente em uma nova conta.
</p>
</div>
<div class="footer">
<p>Este é um email automático do <strong>WebMoney</strong></p>
<p>Por favor, não responda a este email.</p>
</div>
</div>
</body>
</html>

View File

@ -27,6 +27,7 @@
use App\Http\Controllers\Api\SubscriptionController; use App\Http\Controllers\Api\SubscriptionController;
use App\Http\Controllers\Api\UserManagementController; use App\Http\Controllers\Api\UserManagementController;
use App\Http\Controllers\Api\SiteSettingsController; use App\Http\Controllers\Api\SiteSettingsController;
use App\Http\Controllers\Api\AccountDeletionController;
// Public routes with rate limiting // Public routes with rate limiting
Route::post('/register', [AuthController::class, 'register'])->middleware('throttle:register'); Route::post('/register', [AuthController::class, 'register'])->middleware('throttle:register');
@ -368,5 +369,17 @@
Route::get('/{key}', [SiteSettingsController::class, 'show']); Route::get('/{key}', [SiteSettingsController::class, 'show']);
Route::put('/{key}', [SiteSettingsController::class, 'update']); Route::put('/{key}', [SiteSettingsController::class, 'update']);
}); });
// ============================================
// Account Deletion & Factory Reset
// ============================================
Route::post('account-deletion/request-code', [AccountDeletionController::class, 'requestDeletionCode']);
Route::post('account-deletion/export-backup', [AccountDeletionController::class, 'exportBackup']);
Route::post('account-deletion/execute', [AccountDeletionController::class, 'executeHardDelete']);
Route::post('account-deletion/import-backup', [AccountDeletionController::class, 'importBackup']);
}); });
// Download de backup (público com segurança de nome de arquivo)
Route::get('download-backup/{file}', [AccountDeletionController::class, 'downloadBackup'])->name('download.backup');

View File

@ -0,0 +1,450 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { accountDeletionService } from '../services/api';
import { useToast } from './Toast';
const FactoryResetWizard = ({ onClose }) => {
const { t } = useTranslation();
const toast = useToast();
const navigate = useNavigate();
// Estados do wizard
const [step, setStep] = useState(1); // 1: Warning, 2: Backup, 3: Code, 4: Confirmation
const [loading, setLoading] = useState(false);
const [backupCreated, setBackupCreated] = useState(false);
const [backupUrl, setBackupUrl] = useState(null);
const [code, setCode] = useState('');
const [confirmationText, setConfirmationText] = useState('');
const [codeExpiresAt, setCodeExpiresAt] = useState(null);
// Step 1: Aviso inicial
const handleContinueToBackup = () => {
setStep(2);
};
// Step 2: Criar backup
const handleCreateBackup = async () => {
setLoading(true);
try {
const response = await accountDeletionService.exportBackup();
if (response.success) {
setBackupCreated(true);
setBackupUrl(response.download_url);
toast.success(t('factoryReset.backupCreated') || 'Backup criado com sucesso!');
}
} catch (error) {
console.error('Error creating backup:', error);
toast.error(t('factoryReset.backupError') || 'Erro ao criar backup');
} finally {
setLoading(false);
}
};
const handleDownloadBackup = () => {
if (backupUrl) {
window.open(backupUrl, '_blank');
}
};
const handleSkipBackup = () => {
setStep(3);
handleRequestCode();
};
const handleContinueWithBackup = () => {
setStep(3);
handleRequestCode();
};
// Step 3: Solicitar código
const handleRequestCode = async () => {
setLoading(true);
try {
const response = await accountDeletionService.requestDeletionCode();
if (response.success) {
setCodeExpiresAt(response.expires_at);
toast.success(t('factoryReset.codeSent') || 'Código enviado para seu email');
}
} catch (error) {
console.error('Error requesting code:', error);
toast.error(t('factoryReset.codeError') || 'Erro ao enviar código');
} finally {
setLoading(false);
}
};
const handleContinueToFinalStep = () => {
if (!code || code.length !== 6) {
toast.error(t('factoryReset.invalidCode') || 'Digite o código de 6 dígitos');
return;
}
setStep(4);
};
// Step 4: Confirmação final
const handleExecuteDelete = async () => {
if (confirmationText.toUpperCase() !== 'DELETAR') {
toast.error(t('factoryReset.invalidConfirmation') || 'Digite DELETAR para confirmar');
return;
}
setLoading(true);
try {
const response = await accountDeletionService.executeHardDelete(code, confirmationText.toUpperCase());
if (response.success) {
toast.success(t('factoryReset.accountDeleted') || 'Conta deletada permanentemente');
// Logout
localStorage.removeItem('token');
localStorage.removeItem('user');
// Redirecionar para landing page
setTimeout(() => {
window.location.href = '/';
}, 2000);
}
} catch (error) {
console.error('Error deleting account:', error);
if (error.response?.status === 400) {
toast.error(error.response.data.message || t('factoryReset.deleteError'));
} else {
toast.error(t('factoryReset.deleteError') || 'Erro ao deletar conta');
}
} finally {
setLoading(false);
}
};
return (
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.9)' }}>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content border-0" style={{ background: '#1e293b' }}>
{/* Header */}
<div className="modal-header border-0 pb-2">
<h5 className="modal-title text-white d-flex align-items-center">
<i className="bi bi-exclamation-triangle-fill me-2 text-danger"></i>
{t('factoryReset.title') || 'Factory Reset - Exclusão Permanente'}
</h5>
<button
className="btn-close btn-close-white"
onClick={onClose}
disabled={loading}
></button>
</div>
{/* Body */}
<div className="modal-body pt-3" style={{ maxHeight: '70vh', overflowY: 'auto' }}>
{/* Step Indicator */}
<div className="d-flex justify-content-center mb-4">
{[1, 2, 3, 4].map((s) => (
<div
key={s}
className={`rounded-circle d-flex align-items-center justify-content-center me-2 ${
s === step ? 'bg-danger text-white' : s < step ? 'bg-success text-white' : 'bg-secondary text-white'
}`}
style={{ width: '40px', height: '40px', fontSize: '14px', fontWeight: 'bold' }}
>
{s < step ? '✓' : s}
</div>
))}
</div>
{/* Step 1: Warning */}
{step === 1 && (
<div>
<div className="alert alert-danger border-0 mb-3">
<h6 className="alert-heading">
<i className="bi bi-exclamation-octagon me-2"></i>
{t('factoryReset.warningTitle') || '⚠️ ATENÇÃO: AÇÃO IRREVERSÍVEL'}
</h6>
<p className="mb-0">
{t('factoryReset.warningMessage') ||
'Esta ação irá DELETAR PERMANENTEMENTE todos os seus dados dos nossos servidores. Não há como recuperar após a confirmação.'}
</p>
</div>
<div className="card bg-dark border-danger mb-3">
<div className="card-body">
<h6 className="text-danger mb-3">
<i className="bi bi-trash me-2"></i>
{t('factoryReset.dataToDelete') || 'Dados que serão deletados:'}
</h6>
<ul className="text-white-50 mb-0" style={{ fontSize: '14px' }}>
<li>{t('factoryReset.deleteItem1') || 'Todas as transações'}</li>
<li>{t('factoryReset.deleteItem2') || 'Todas as contas bancárias e de ativos'}</li>
<li>{t('factoryReset.deleteItem3') || 'Todos os cartões de crédito e passivos'}</li>
<li>{t('factoryReset.deleteItem4') || 'Orçamentos e categorias personalizadas'}</li>
<li>{t('factoryReset.deleteItem5') || 'Centros de custo'}</li>
<li>{t('factoryReset.deleteItem6') || 'Objetivos financeiros'}</li>
<li>{t('factoryReset.deleteItem7') || 'Investimentos'}</li>
<li>{t('factoryReset.deleteItem8') || 'Configurações e preferências'}</li>
<li className="text-danger fw-bold">{t('factoryReset.deleteItem9') || 'Sua conta de usuário'}</li>
</ul>
</div>
</div>
<div className="alert alert-warning border-0">
<p className="mb-0" style={{ fontSize: '14px' }}>
<i className="bi bi-lightbulb me-2"></i>
{t('factoryReset.backupRecommendation') ||
'Recomendamos fazer um backup dos seus dados antes de prosseguir. Você poderá importá-lo posteriormente.'}
</p>
</div>
</div>
)}
{/* Step 2: Backup */}
{step === 2 && (
<div>
<div className="text-center mb-4">
<i className="bi bi-cloud-download display-1 text-primary"></i>
<h5 className="text-white mt-3 mb-2">
{t('factoryReset.backupTitle') || 'Backup dos Dados'}
</h5>
<p className="text-slate-400" style={{ fontSize: '14px' }}>
{t('factoryReset.backupDescription') ||
'Você pode exportar todos os seus dados para um arquivo JSON e importá-los posteriormente em qualquer conta.'}
</p>
</div>
{!backupCreated ? (
<div className="card bg-dark border-0 mb-3">
<div className="card-body text-center py-5">
<button
className="btn btn-primary btn-lg mb-3"
onClick={handleCreateBackup}
disabled={loading}
>
{loading ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
{t('common.creating') || 'Criando...'}
</>
) : (
<>
<i className="bi bi-download me-2"></i>
{t('factoryReset.createBackup') || 'Criar Backup Agora'}
</>
)}
</button>
<p className="text-slate-400 mb-0" style={{ fontSize: '13px' }}>
{t('factoryReset.backupInfo') ||
'O backup será gerado em formato JSON e estará disponível por 24 horas'}
</p>
</div>
</div>
) : (
<div className="card bg-success bg-opacity-10 border-success mb-3">
<div className="card-body text-center">
<i className="bi bi-check-circle display-4 text-success"></i>
<h6 className="text-white mt-3 mb-3">
{t('factoryReset.backupReady') || 'Backup Criado com Sucesso!'}
</h6>
<button
className="btn btn-success"
onClick={handleDownloadBackup}
>
<i className="bi bi-download me-2"></i>
{t('factoryReset.downloadBackup') || 'Baixar Backup'}
</button>
<p className="text-slate-400 mt-3 mb-0" style={{ fontSize: '12px' }}>
{t('factoryReset.backupExpires') || 'Disponível por 24 horas'}
</p>
</div>
</div>
)}
</div>
)}
{/* Step 3: Code */}
{step === 3 && (
<div>
<div className="text-center mb-4">
<i className="bi bi-envelope-check display-1 text-info"></i>
<h5 className="text-white mt-3 mb-2">
{t('factoryReset.codeTitle') || 'Código de Confirmação'}
</h5>
<p className="text-slate-400" style={{ fontSize: '14px' }}>
{t('factoryReset.codeDescription') ||
'Um código de 6 dígitos foi enviado para seu email. Digite-o abaixo para continuar.'}
</p>
</div>
<div className="card bg-dark border-0 mb-3">
<div className="card-body">
<label className="form-label text-white">
{t('factoryReset.codeLabel') || 'Código de Confirmação'}
</label>
<input
type="text"
className="form-control form-control-lg text-center"
placeholder="000000"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
maxLength={6}
style={{
background: '#0f172a',
border: '1px solid #475569',
color: 'white',
letterSpacing: '0.5em',
fontSize: '24px'
}}
/>
{codeExpiresAt && (
<small className="text-slate-400 mt-2 d-block">
<i className="bi bi-clock me-1"></i>
{t('factoryReset.codeExpires') || 'Expira em 10 minutos'}
</small>
)}
</div>
</div>
<div className="alert alert-info border-0">
<p className="mb-0" style={{ fontSize: '13px' }}>
<i className="bi bi-info-circle me-2"></i>
{t('factoryReset.codeNotReceived') ||
'Não recebeu o código? Verifique sua caixa de spam ou solicite um novo código fechando e reabrindo este wizard.'}
</p>
</div>
</div>
)}
{/* Step 4: Final Confirmation */}
{step === 4 && (
<div>
<div className="text-center mb-4">
<i className="bi bi-exclamation-diamond display-1 text-danger"></i>
<h5 className="text-white mt-3 mb-2">
{t('factoryReset.finalTitle') || 'Confirmação Final'}
</h5>
<p className="text-slate-400" style={{ fontSize: '14px' }}>
{t('factoryReset.finalDescription') ||
'Este é o último passo. Uma vez confirmado, não há como voltar atrás.'}
</p>
</div>
<div className="alert alert-danger border-0 mb-3">
<h6 className="alert-heading">
{t('factoryReset.lastWarning') || 'ÚLTIMA CHANCE DE CANCELAR'}
</h6>
<p className="mb-0">
{t('factoryReset.lastWarningMessage') ||
'Após confirmar, todos os seus dados serão PERMANENTEMENTE deletados. Esta ação NÃO PODE SER DESFEITA.'}
</p>
</div>
<div className="card bg-dark border-danger mb-3">
<div className="card-body">
<label className="form-label text-white">
{t('factoryReset.typeToConfirm') || 'Digite DELETAR para confirmar:'}
</label>
<input
type="text"
className="form-control form-control-lg"
placeholder="DELETAR"
value={confirmationText}
onChange={(e) => setConfirmationText(e.target.value)}
style={{
background: '#0f172a',
border: '2px solid #dc3545',
color: 'white'
}}
/>
<small className="text-slate-400 mt-2 d-block">
{t('factoryReset.confirmationHelp') || 'Digite exatamente: DELETAR (em maiúsculas)'}
</small>
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="modal-footer border-0">
<button
className="btn btn-outline-secondary px-4"
onClick={onClose}
disabled={loading}
>
{t('common.cancel') || 'Cancelar'}
</button>
{step === 1 && (
<button
className="btn btn-danger px-4"
onClick={handleContinueToBackup}
>
{t('common.continue') || 'Continuar'}
<i className="bi bi-arrow-right ms-2"></i>
</button>
)}
{step === 2 && (
<>
<button
className="btn btn-outline-warning px-4"
onClick={handleSkipBackup}
disabled={loading}
>
{t('factoryReset.skipBackup') || 'Pular Backup'}
</button>
{backupCreated && (
<button
className="btn btn-danger px-4"
onClick={handleContinueWithBackup}
disabled={loading}
>
{t('common.continue') || 'Continuar'}
<i className="bi bi-arrow-right ms-2"></i>
</button>
)}
</>
)}
{step === 3 && (
<button
className="btn btn-danger px-4"
onClick={handleContinueToFinalStep}
disabled={!code || code.length !== 6}
>
{t('common.continue') || 'Continuar'}
<i className="bi bi-arrow-right ms-2"></i>
</button>
)}
{step === 4 && (
<button
className="btn btn-danger px-4"
onClick={handleExecuteDelete}
disabled={loading || confirmationText.toUpperCase() !== 'DELETAR'}
>
{loading ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
{t('common.deleting') || 'Deletando...'}
</>
) : (
<>
<i className="bi bi-trash me-2"></i>
{t('factoryReset.deleteAccount') || 'DELETAR CONTA'}
</>
)}
</button>
)}
</div>
</div>
</div>
</div>
);
};
export default FactoryResetWizard;

View File

@ -0,0 +1,229 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { accountDeletionService } from '../services/api';
import { useToast } from './Toast';
const ImportBackupModal = ({ onClose, onSuccess }) => {
const { t } = useTranslation();
const toast = useToast();
const [loading, setLoading] = useState(false);
const [selectedFile, setSelectedFile] = useState(null);
const [dragActive, setDragActive] = useState(false);
const handleDrag = (e) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
const file = e.dataTransfer.files[0];
if (file.type === 'application/json' || file.name.endsWith('.json')) {
setSelectedFile(file);
} else {
toast.error(t('importBackup.invalidFileType') || 'Apenas arquivos JSON são aceitos');
}
}
};
const handleFileSelect = (e) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
if (file.type === 'application/json' || file.name.endsWith('.json')) {
setSelectedFile(file);
} else {
toast.error(t('importBackup.invalidFileType') || 'Apenas arquivos JSON são aceitos');
}
}
};
const handleImport = async () => {
if (!selectedFile) {
toast.error(t('importBackup.noFileSelected') || 'Selecione um arquivo de backup');
return;
}
setLoading(true);
try {
const response = await accountDeletionService.importBackup(selectedFile);
if (response.success) {
toast.success(t('importBackup.success') || 'Backup importado com sucesso!');
// Mostrar estatísticas
const stats = response.stats;
let message = `${t('importBackup.imported') || 'Importado'}:\n`;
if (stats.accounts > 0) message += `${stats.accounts} ${t('importBackup.accounts') || 'contas'}\n`;
if (stats.transactions > 0) message += `${stats.transactions} ${t('importBackup.transactions') || 'transações'}\n`;
if (stats.categories > 0) message += `${stats.categories} ${t('importBackup.categories') || 'categorias'}\n`;
if (stats.budgets > 0) message += `${stats.budgets} ${t('importBackup.budgets') || 'orçamentos'}\n`;
if (stats.goals > 0) message += `${stats.goals} ${t('importBackup.goals') || 'objetivos'}\n`;
toast.success(message);
if (onSuccess) {
setTimeout(() => onSuccess(), 2000);
}
}
} catch (error) {
console.error('Error importing backup:', error);
const errorMsg = error.response?.data?.message || t('importBackup.error') || 'Erro ao importar backup';
toast.error(errorMsg);
} finally {
setLoading(false);
}
};
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', '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];
};
return (
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.8)' }}>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content border-0" style={{ background: '#1e293b' }}>
{/* Header */}
<div className="modal-header border-0 pb-2">
<h5 className="modal-title text-white">
<i className="bi bi-cloud-upload me-2 text-primary"></i>
{t('importBackup.title') || 'Importar Backup'}
</h5>
<button
className="btn-close btn-close-white"
onClick={onClose}
disabled={loading}
></button>
</div>
{/* Body */}
<div className="modal-body pt-3">
<div className="alert alert-info border-0 mb-4">
<h6 className="alert-heading">
<i className="bi bi-info-circle me-2"></i>
{t('importBackup.infoTitle') || 'Como funciona?'}
</h6>
<p className="mb-0" style={{ fontSize: '14px' }}>
{t('importBackup.infoMessage') ||
'Você pode importar um backup previamente exportado. Os dados serão adicionados à sua conta atual sem apagar os dados existentes.'}
</p>
</div>
{/* Drag & Drop Zone */}
<div
className={`card border-2 border-dashed mb-3 ${dragActive ? 'border-primary bg-primary bg-opacity-10' : 'border-secondary'}`}
style={{ background: '#0f172a', cursor: 'pointer' }}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
onClick={() => document.getElementById('backup-file-input').click()}
>
<div className="card-body text-center py-5">
{!selectedFile ? (
<>
<i className="bi bi-cloud-upload display-1 text-primary mb-3"></i>
<h6 className="text-white mb-2">
{t('importBackup.dragDrop') || 'Arraste e solte o arquivo aqui'}
</h6>
<p className="text-slate-400 mb-3" style={{ fontSize: '14px' }}>
{t('importBackup.or') || 'ou'}
</p>
<button className="btn btn-primary">
<i className="bi bi-folder2-open me-2"></i>
{t('importBackup.selectFile') || 'Selecionar Arquivo'}
</button>
<p className="text-slate-400 mt-3 mb-0" style={{ fontSize: '12px' }}>
{t('importBackup.fileRequirements') || 'Apenas arquivos JSON (máx. 50MB)'}
</p>
</>
) : (
<>
<i className="bi bi-file-earmark-check display-1 text-success mb-3"></i>
<h6 className="text-white mb-2">{selectedFile.name}</h6>
<p className="text-slate-400 mb-3">
{formatFileSize(selectedFile.size)}
</p>
<button
className="btn btn-outline-secondary btn-sm"
onClick={(e) => {
e.stopPropagation();
setSelectedFile(null);
}}
>
<i className="bi bi-x-circle me-1"></i>
{t('common.remove') || 'Remover'}
</button>
</>
)}
</div>
</div>
<input
id="backup-file-input"
type="file"
accept=".json,application/json"
onChange={handleFileSelect}
style={{ display: 'none' }}
/>
<div className="alert alert-warning border-0">
<p className="mb-0" style={{ fontSize: '13px' }}>
<i className="bi bi-exclamation-triangle me-2"></i>
{t('importBackup.warning') ||
'Atenção: A importação pode levar alguns minutos dependendo do tamanho do backup.'}
</p>
</div>
</div>
{/* Footer */}
<div className="modal-footer border-0">
<button
className="btn btn-outline-secondary px-4"
onClick={onClose}
disabled={loading}
>
{t('common.cancel') || 'Cancelar'}
</button>
<button
className="btn btn-primary px-4"
onClick={handleImport}
disabled={!selectedFile || loading}
>
{loading ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
{t('common.importing') || 'Importando...'}
</>
) : (
<>
<i className="bi bi-upload me-2"></i>
{t('importBackup.import') || 'Importar Backup'}
</>
)}
</button>
</div>
</div>
</div>
</div>
);
};
export default ImportBackupModal;

View File

@ -57,6 +57,10 @@
"months": "months", "months": "months",
"unclassified": "Unclassified", "unclassified": "Unclassified",
"viewAll": "View all", "viewAll": "View all",
"continue": "Continue",
"creating": "Creating...",
"deleting": "Deleting...",
"remove": "Remove",
"today": "Today", "today": "Today",
"selectTransactions": "Select transactions", "selectTransactions": "Select transactions",
"selectAll": "Select All", "selectAll": "Select All",
@ -2182,7 +2186,15 @@
"passwordChanged": "Password changed successfully!", "passwordChanged": "Password changed successfully!",
"passwordError": "Error changing password", "passwordError": "Error changing password",
"passwordMismatch": "Passwords do not match", "passwordMismatch": "Passwords do not match",
"passwordTooShort": "Password must be at least 8 characters" "passwordTooShort": "Password must be at least 8 characters",
"dataManagement": "Data Management",
"importBackup": "Import Backup",
"importBackupDesc": "Restore data from a previously exported backup. Data will be added to your account.",
"importBackupBtn": "Import Now",
"factoryReset": "Factory Reset",
"factoryResetDesc": "WARNING: Permanently deletes ALL your data. This action cannot be undone.",
"factoryResetBtn": "Start Factory Reset",
"dataManagementWarning": "Before doing a Factory Reset, we recommend creating a backup of your data to recover it later if needed."
}, },
"pricing": { "pricing": {
"title": "Plans & Pricing", "title": "Plans & Pricing",
@ -2383,5 +2395,70 @@
"terms": "Terms of Use", "terms": "Terms of Use",
"contact": "Contact" "contact": "Contact"
} }
},
"factoryReset": {
"title": "Factory Reset - Permanent Deletion",
"warningTitle": "⚠️ WARNING: IRREVERSIBLE ACTION",
"warningMessage": "This action will PERMANENTLY DELETE all your data from our servers. There is no way to recover it after confirmation.",
"dataToDelete": "Data that will be deleted:",
"deleteItem1": "All transactions",
"deleteItem2": "All bank and asset accounts",
"deleteItem3": "All credit cards and liabilities",
"deleteItem4": "Budgets and custom categories",
"deleteItem5": "Cost centers",
"deleteItem6": "Financial goals",
"deleteItem7": "Investments",
"deleteItem8": "Settings and preferences",
"deleteItem9": "Your user account",
"backupRecommendation": "We recommend backing up your data before proceeding. You can import it later.",
"backupTitle": "Data Backup",
"backupDescription": "You can export all your data to a JSON file and import it later into any account.",
"createBackup": "Create Backup Now",
"backupInfo": "The backup will be generated in JSON format and will be available for 24 hours",
"backupReady": "Backup Created Successfully!",
"downloadBackup": "Download Backup",
"backupExpires": "Available for 24 hours",
"skipBackup": "Skip Backup",
"codeTitle": "Confirmation Code",
"codeDescription": "A 6-digit code has been sent to your email. Enter it below to continue.",
"codeLabel": "Confirmation Code",
"codeExpires": "Expires in 10 minutes",
"codeNotReceived": "Didn't receive the code? Check your spam folder or request a new code by closing and reopening this wizard.",
"finalTitle": "Final Confirmation",
"finalDescription": "This is the last step. Once confirmed, there is no going back.",
"lastWarning": "LAST CHANCE TO CANCEL",
"lastWarningMessage": "After confirming, all your data will be PERMANENTLY deleted. This action CANNOT BE UNDONE.",
"typeToConfirm": "Type DELETE to confirm:",
"confirmationHelp": "Type exactly: DELETE (in uppercase)",
"deleteAccount": "DELETE ACCOUNT",
"backupCreated": "Backup created successfully!",
"backupError": "Error creating backup",
"codeSent": "Code sent to your email",
"codeError": "Error sending code",
"invalidCode": "Enter the 6-digit code",
"invalidConfirmation": "Type DELETE to confirm",
"accountDeleted": "Account permanently deleted",
"deleteError": "Error executing deletion"
},
"importBackup": {
"title": "Import Backup",
"infoTitle": "How does it work?",
"infoMessage": "You can import a previously exported backup. The data will be added to your current account without deleting existing data.",
"dragDrop": "Drag and drop the file here",
"or": "or",
"selectFile": "Select File",
"fileRequirements": "JSON files only (max. 50MB)",
"invalidFileType": "Only JSON files are accepted",
"noFileSelected": "Select a backup file",
"success": "Backup imported successfully!",
"imported": "Imported",
"accounts": "accounts",
"transactions": "transactions",
"categories": "categories",
"budgets": "budgets",
"goals": "goals",
"error": "Error importing backup",
"warning": "Warning: Import may take a few minutes depending on backup size.",
"import": "Import Backup"
} }
} }

View File

@ -58,6 +58,10 @@
"months": "meses", "months": "meses",
"unclassified": "Sin clasificar", "unclassified": "Sin clasificar",
"viewAll": "Ver todos", "viewAll": "Ver todos",
"continue": "Continuar",
"creating": "Creando...",
"deleting": "Eliminando...",
"remove": "Eliminar",
"today": "Hoy", "today": "Hoy",
"selectTransactions": "Seleccionar transacciones", "selectTransactions": "Seleccionar transacciones",
"selectAll": "Seleccionar Todas", "selectAll": "Seleccionar Todas",
@ -2174,7 +2178,15 @@
"passwordChanged": "¡Contraseña cambiada con éxito!", "passwordChanged": "¡Contraseña cambiada con éxito!",
"passwordError": "Error al cambiar contraseña", "passwordError": "Error al cambiar contraseña",
"passwordMismatch": "Las contraseñas no coinciden", "passwordMismatch": "Las contraseñas no coinciden",
"passwordTooShort": "La contraseña debe tener al menos 8 caracteres" "passwordTooShort": "La contraseña debe tener al menos 8 caracteres",
"dataManagement": "Gestión de Datos",
"importBackup": "Importar Copia de Seguridad",
"importBackupDesc": "Restaure datos de una copia de seguridad exportada previamente. Los datos se agregarán a su cuenta.",
"importBackupBtn": "Importar Ahora",
"factoryReset": "Factory Reset",
"factoryResetDesc": "ATENCIÓN: Elimina permanentemente TODOS sus datos. Esta acción no se puede deshacer.",
"factoryResetBtn": "Iniciar Factory Reset",
"dataManagementWarning": "Antes de hacer Factory Reset, recomendamos crear una copia de seguridad de sus datos para recuperarlos posteriormente si es necesario."
}, },
"pricing": { "pricing": {
"title": "Planes y Precios", "title": "Planes y Precios",
@ -2385,5 +2397,70 @@
"terms": "Términos de Uso", "terms": "Términos de Uso",
"contact": "Contacto" "contact": "Contacto"
} }
},
"factoryReset": {
"title": "Factory Reset - Eliminación Permanente",
"warningTitle": "⚠️ ATENCIÓN: ACCIÓN IRREVERSIBLE",
"warningMessage": "Esta acción ELIMINARÁ PERMANENTEMENTE todos sus datos de nuestros servidores. No hay forma de recuperarlos después de la confirmación.",
"dataToDelete": "Datos que serán eliminados:",
"deleteItem1": "Todas las transacciones",
"deleteItem2": "Todas las cuentas bancarias y de activos",
"deleteItem3": "Todas las tarjetas de crédito y pasivos",
"deleteItem4": "Presupuestos y categorías personalizadas",
"deleteItem5": "Centros de costo",
"deleteItem6": "Objetivos financieros",
"deleteItem7": "Inversiones",
"deleteItem8": "Configuraciones y preferencias",
"deleteItem9": "Su cuenta de usuario",
"backupRecommendation": "Recomendamos hacer una copia de seguridad de sus datos antes de continuar. Podrá importarla posteriormente.",
"backupTitle": "Copia de Seguridad de Datos",
"backupDescription": "Puede exportar todos sus datos a un archivo JSON e importarlos posteriormente en cualquier cuenta.",
"createBackup": "Crear Copia de Seguridad Ahora",
"backupInfo": "La copia de seguridad se generará en formato JSON y estará disponible durante 24 horas",
"backupReady": "¡Copia de Seguridad Creada con Éxito!",
"downloadBackup": "Descargar Copia de Seguridad",
"backupExpires": "Disponible durante 24 horas",
"skipBackup": "Omitir Copia de Seguridad",
"codeTitle": "Código de Confirmación",
"codeDescription": "Se ha enviado un código de 6 dígitos a su correo electrónico. Introdúzcalo a continuación para continuar.",
"codeLabel": "Código de Confirmación",
"codeExpires": "Expira en 10 minutos",
"codeNotReceived": "¿No recibió el código? Verifique su carpeta de spam o solicite un nuevo código cerrando y reabriendo este asistente.",
"finalTitle": "Confirmación Final",
"finalDescription": "Este es el último paso. Una vez confirmado, no hay vuelta atrás.",
"lastWarning": "ÚLTIMA OPORTUNIDAD DE CANCELAR",
"lastWarningMessage": "Después de confirmar, todos sus datos serán PERMANENTEMENTE eliminados. Esta acción NO PUEDE SER DESHECHA.",
"typeToConfirm": "Escriba ELIMINAR para confirmar:",
"confirmationHelp": "Escriba exactamente: ELIMINAR (en mayúsculas)",
"deleteAccount": "ELIMINAR CUENTA",
"backupCreated": "¡Copia de seguridad creada con éxito!",
"backupError": "Error al crear copia de seguridad",
"codeSent": "Código enviado a su correo electrónico",
"codeError": "Error al enviar código",
"invalidCode": "Introduzca el código de 6 dígitos",
"invalidConfirmation": "Escriba ELIMINAR para confirmar",
"accountDeleted": "Cuenta eliminada permanentemente",
"deleteError": "Error al ejecutar eliminación"
},
"importBackup": {
"title": "Importar Copia de Seguridad",
"infoTitle": "¿Cómo funciona?",
"infoMessage": "Puede importar una copia de seguridad exportada previamente. Los datos se agregarán a su cuenta actual sin borrar los datos existentes.",
"dragDrop": "Arrastre y suelte el archivo aquí",
"or": "o",
"selectFile": "Seleccionar Archivo",
"fileRequirements": "Solo archivos JSON (máx. 50MB)",
"invalidFileType": "Solo se aceptan archivos JSON",
"noFileSelected": "Seleccione un archivo de copia de seguridad",
"success": "¡Copia de seguridad importada con éxito!",
"imported": "Importado",
"accounts": "cuentas",
"transactions": "transacciones",
"categories": "categorías",
"budgets": "presupuestos",
"goals": "objetivos",
"error": "Error al importar copia de seguridad",
"warning": "Atención: La importación puede tardar unos minutos dependiendo del tamaño de la copia de seguridad.",
"import": "Importar Copia de Seguridad"
} }
} }

View File

@ -58,6 +58,10 @@
"months": "meses", "months": "meses",
"unclassified": "Sem classificar", "unclassified": "Sem classificar",
"viewAll": "Ver todos", "viewAll": "Ver todos",
"continue": "Continuar",
"creating": "Criando...",
"deleting": "Deletando...",
"remove": "Remover",
"date": "Data", "date": "Data",
"today": "Hoje", "today": "Hoje",
"selectTransactions": "Selecionar transações", "selectTransactions": "Selecionar transações",
@ -2192,7 +2196,15 @@
"passwordChanged": "Senha alterada com sucesso!", "passwordChanged": "Senha alterada com sucesso!",
"passwordError": "Erro ao alterar senha", "passwordError": "Erro ao alterar senha",
"passwordMismatch": "As senhas não coincidem", "passwordMismatch": "As senhas não coincidem",
"passwordTooShort": "A senha deve ter pelo menos 8 caracteres" "passwordTooShort": "A senha deve ter pelo menos 8 caracteres",
"dataManagement": "Gerenciamento de Dados",
"importBackup": "Importar Backup",
"importBackupDesc": "Restaure dados de um backup anteriormente exportado. Os dados serão adicionados à sua conta.",
"importBackupBtn": "Importar Agora",
"factoryReset": "Factory Reset",
"factoryResetDesc": "ATENÇÃO: Deleta permanentemente TODOS os seus dados. Esta ação não pode ser desfeita.",
"factoryResetBtn": "Iniciar Factory Reset",
"dataManagementWarning": "Antes de fazer Factory Reset, recomendamos criar um backup dos seus dados para recuperá-los posteriormente se necessário."
}, },
"pricing": { "pricing": {
"title": "Planos e Preços", "title": "Planos e Preços",
@ -2403,5 +2415,70 @@
"terms": "Termos de Uso", "terms": "Termos de Uso",
"contact": "Contato" "contact": "Contato"
} }
},
"factoryReset": {
"title": "Factory Reset - Exclusão Permanente",
"warningTitle": "⚠️ ATENÇÃO: AÇÃO IRREVERSÍVEL",
"warningMessage": "Esta ação irá DELETAR PERMANENTEMENTE todos os seus dados dos nossos servidores. Não há como recuperar após a confirmação.",
"dataToDelete": "Dados que serão deletados:",
"deleteItem1": "Todas as transações",
"deleteItem2": "Todas as contas bancárias e de ativos",
"deleteItem3": "Todos os cartões de crédito e passivos",
"deleteItem4": "Orçamentos e categorias personalizadas",
"deleteItem5": "Centros de custo",
"deleteItem6": "Objetivos financeiros",
"deleteItem7": "Investimentos",
"deleteItem8": "Configurações e preferências",
"deleteItem9": "Sua conta de usuário",
"backupRecommendation": "Recomendamos fazer um backup dos seus dados antes de prosseguir. Você poderá importá-lo posteriormente.",
"backupTitle": "Backup dos Dados",
"backupDescription": "Você pode exportar todos os seus dados para um arquivo JSON e importá-los posteriormente em qualquer conta.",
"createBackup": "Criar Backup Agora",
"backupInfo": "O backup será gerado em formato JSON e estará disponível por 24 horas",
"backupReady": "Backup Criado com Sucesso!",
"downloadBackup": "Baixar Backup",
"backupExpires": "Disponível por 24 horas",
"skipBackup": "Pular Backup",
"codeTitle": "Código de Confirmação",
"codeDescription": "Um código de 6 dígitos foi enviado para seu email. Digite-o abaixo para continuar.",
"codeLabel": "Código de Confirmação",
"codeExpires": "Expira em 10 minutos",
"codeNotReceived": "Não recebeu o código? Verifique sua caixa de spam ou solicite um novo código fechando e reabrindo este wizard.",
"finalTitle": "Confirmação Final",
"finalDescription": "Este é o último passo. Uma vez confirmado, não há como voltar atrás.",
"lastWarning": "ÚLTIMA CHANCE DE CANCELAR",
"lastWarningMessage": "Após confirmar, todos os seus dados serão PERMANENTEMENTE deletados. Esta ação NÃO PODE SER DESFEITA.",
"typeToConfirm": "Digite DELETAR para confirmar:",
"confirmationHelp": "Digite exatamente: DELETAR (em maiúsculas)",
"deleteAccount": "DELETAR CONTA",
"backupCreated": "Backup criado com sucesso!",
"backupError": "Erro ao criar backup",
"codeSent": "Código enviado para seu email",
"codeError": "Erro ao enviar código",
"invalidCode": "Digite o código de 6 dígitos",
"invalidConfirmation": "Digite DELETAR para confirmar",
"accountDeleted": "Conta deletada permanentemente",
"deleteError": "Erro ao executar exclusão"
},
"importBackup": {
"title": "Importar Backup",
"infoTitle": "Como funciona?",
"infoMessage": "Você pode importar um backup previamente exportado. Os dados serão adicionados à sua conta atual sem apagar os dados existentes.",
"dragDrop": "Arraste e solte o arquivo aqui",
"or": "ou",
"selectFile": "Selecionar Arquivo",
"fileRequirements": "Apenas arquivos JSON (máx. 50MB)",
"invalidFileType": "Apenas arquivos JSON são aceitos",
"noFileSelected": "Selecione um arquivo de backup",
"success": "Backup importado com sucesso!",
"imported": "Importado",
"accounts": "contas",
"transactions": "transações",
"categories": "categorias",
"budgets": "orçamentos",
"goals": "objetivos",
"error": "Erro ao importar backup",
"warning": "Atenção: A importação pode levar alguns minutos dependendo do tamanho do backup.",
"import": "Importar Backup"
} }
} }

View File

@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToast } from '../components/Toast'; import { useToast } from '../components/Toast';
import { profileService } from '../services/api'; import { profileService } from '../services/api';
import FactoryResetWizard from '../components/FactoryResetWizard';
import ImportBackupModal from '../components/ImportBackupModal';
// Lista de países com código de telefone (foco: ES, BR, US) // Lista de países com código de telefone (foco: ES, BR, US)
const COUNTRY_CODES = [ const COUNTRY_CODES = [
@ -93,6 +95,10 @@ export default function Profile() {
const [profileErrors, setProfileErrors] = useState({}); const [profileErrors, setProfileErrors] = useState({});
const [passwordErrors, setPasswordErrors] = useState({}); const [passwordErrors, setPasswordErrors] = useState({});
// Modais
const [showFactoryReset, setShowFactoryReset] = useState(false);
const [showImportBackup, setShowImportBackup] = useState(false);
useEffect(() => { useEffect(() => {
loadProfile(); loadProfile();
}, []); }, []);
@ -567,8 +573,90 @@ export default function Profile() {
</form> </form>
</div> </div>
</div> </div>
{/* Seção: Backup e Factory Reset */}
<div className="col-12 mt-4">
<div className="card shadow-sm">
<div className="card-header bg-danger text-white">
<h5 className="mb-0">
<i className="bi bi-shield-exclamation me-2"></i>
{t('profile.dataManagement') || 'Gerenciamento de Dados'}
</h5>
</div>
<div className="card-body">
<div className="row">
{/* Importar Backup */}
<div className="col-md-6 mb-3 mb-md-0">
<div className="d-flex align-items-start">
<i className="bi bi-cloud-upload text-primary fs-3 me-3"></i>
<div className="flex-grow-1">
<h6 className="fw-bold">{t('profile.importBackup') || 'Importar Backup'}</h6>
<p className="text-muted small mb-2">
{t('profile.importBackupDesc') ||
'Restaure dados de um backup anteriormente exportado. Os dados serão adicionados à sua conta.'}
</p>
<button
className="btn btn-sm btn-outline-primary"
onClick={() => setShowImportBackup(true)}
>
<i className="bi bi-upload me-1"></i>
{t('profile.importBackupBtn') || 'Importar Agora'}
</button>
</div>
</div>
</div>
{/* Factory Reset */}
<div className="col-md-6">
<div className="d-flex align-items-start">
<i className="bi bi-exclamation-triangle text-danger fs-3 me-3"></i>
<div className="flex-grow-1">
<h6 className="fw-bold text-danger">
{t('profile.factoryReset') || 'Factory Reset'}
</h6>
<p className="text-muted small mb-2">
{t('profile.factoryResetDesc') ||
'ATENÇÃO: Deleta permanentemente TODOS os seus dados. Esta ação não pode ser desfeita.'}
</p>
<button
className="btn btn-sm btn-outline-danger"
onClick={() => setShowFactoryReset(true)}
>
<i className="bi bi-trash me-1"></i>
{t('profile.factoryResetBtn') || 'Iniciar Factory Reset'}
</button>
</div>
</div>
</div>
</div>
<div className="alert alert-warning mt-3 mb-0">
<small>
<i className="bi bi-info-circle me-2"></i>
{t('profile.dataManagementWarning') ||
'Antes de fazer Factory Reset, recomendamos criar um backup dos seus dados para recuperá-los posteriormente se necessário.'}
</small>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
{/* Modais */}
{showFactoryReset && (
<FactoryResetWizard onClose={() => setShowFactoryReset(false)} />
)}
{showImportBackup && (
<ImportBackupModal
onClose={() => setShowImportBackup(false)}
onSuccess={() => {
setShowImportBackup(false);
// Recarregar dados se necessário
window.location.reload();
}}
/>
)}
</div> </div>
); );
} }

View File

@ -1657,4 +1657,49 @@ export const profileService = {
}, },
}; };
// ============================================
// Account Deletion Service (Factory Reset)
// ============================================
export const accountDeletionService = {
// Solicitar código de confirmação
requestDeletionCode: async () => {
const response = await api.post('/account-deletion/request-code');
return response.data;
},
// Exportar backup completo
exportBackup: async () => {
const response = await api.post('/account-deletion/export-backup');
return response.data;
},
// Executar hard delete
executeHardDelete: async (code, confirmationText) => {
const response = await api.post('/account-deletion/execute', {
code,
confirmation_text: confirmationText
});
return response.data;
},
// Importar backup
importBackup: async (file) => {
const formData = new FormData();
formData.append('backup_file', file);
const response = await api.post('/account-deletion/import-backup', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
},
// Download de backup (retorna URL para abrir em nova aba)
getBackupDownloadUrl: (fileName) => {
return `${API_BASE_URL.replace('/api', '')}/api/download-backup/${fileName}`;
},
};
export default api; export default api;