From 27f3bd88697c524e83c59e5796e1b138c74b5a00 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Dec 2025 16:45:08 +0100 Subject: [PATCH] feat: implementar Factory Reset completo com wizard e sistema de backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Api/AccountDeletionController.php | 526 ++++++++++++++++++ .../app/Mail/AccountDeletionConfirmation.php | 56 ++ .../account-deletion-confirmation.blade.php | 150 +++++ backend/routes/api.php | 13 + .../src/components/FactoryResetWizard.jsx | 450 +++++++++++++++ frontend/src/components/ImportBackupModal.jsx | 229 ++++++++ frontend/src/i18n/locales/en.json | 79 ++- frontend/src/i18n/locales/es.json | 79 ++- frontend/src/i18n/locales/pt-BR.json | 79 ++- frontend/src/pages/Profile.jsx | 88 +++ frontend/src/services/api.js | 45 ++ 11 files changed, 1791 insertions(+), 3 deletions(-) create mode 100644 backend/app/Http/Controllers/Api/AccountDeletionController.php create mode 100644 backend/app/Mail/AccountDeletionConfirmation.php create mode 100644 backend/resources/views/emails/account-deletion-confirmation.blade.php create mode 100644 frontend/src/components/FactoryResetWizard.jsx create mode 100644 frontend/src/components/ImportBackupModal.jsx diff --git a/backend/app/Http/Controllers/Api/AccountDeletionController.php b/backend/app/Http/Controllers/Api/AccountDeletionController.php new file mode 100644 index 0000000..07d51c4 --- /dev/null +++ b/backend/app/Http/Controllers/Api/AccountDeletionController.php @@ -0,0 +1,526 @@ +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); + } + } +} diff --git a/backend/app/Mail/AccountDeletionConfirmation.php b/backend/app/Mail/AccountDeletionConfirmation.php new file mode 100644 index 0000000..05eae5f --- /dev/null +++ b/backend/app/Mail/AccountDeletionConfirmation.php @@ -0,0 +1,56 @@ +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 + */ + public function attachments(): array + { + return []; + } +} diff --git a/backend/resources/views/emails/account-deletion-confirmation.blade.php b/backend/resources/views/emails/account-deletion-confirmation.blade.php new file mode 100644 index 0000000..39750a5 --- /dev/null +++ b/backend/resources/views/emails/account-deletion-confirmation.blade.php @@ -0,0 +1,150 @@ + + + + + + Código de Confirmação + + + +
+
+

⚠️ EXCLUSÃO PERMANENTE DE CONTA

+
+ +
+

Olá, {{ $userName }}

+ +

+ Você solicitou a exclusão PERMANENTE E IRREVERSÍVEL de sua conta no WebMoney. +

+ +
+ ⚠️ ATENÇÃO: 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. +
+ +
+ 🗑️ Serão deletados permanentemente: +
    +
  • Todas as transações
  • +
  • Todas as contas bancárias e de ativos
  • +
  • Todos os cartões de crédito e contas de passivos
  • +
  • Orçamentos e categorias personalizadas
  • +
  • Centros de custo e palavras-chave
  • +
  • Objetivos financeiros
  • +
  • Investimentos e histórico de preços
  • +
  • Configurações e preferências
  • +
  • Sua conta de usuário
  • +
+
+ +

+ Para confirmar esta ação, utilize o código abaixo: +

+ +
+
+ SEU CÓDIGO DE CONFIRMAÇÃO +
+
{{ $code }}
+
+ Válido por 10 minutos +
+
+ +

+ Não foi você? Se você não solicitou esta exclusão, ignore este email e sua conta permanecerá intacta. Recomendamos alterar sua senha imediatamente por segurança. +

+ +

+ Quer fazer backup? 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. +

+
+ + +
+ + diff --git a/backend/routes/api.php b/backend/routes/api.php index 21871df..d76d69c 100755 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -27,6 +27,7 @@ use App\Http\Controllers\Api\SubscriptionController; use App\Http\Controllers\Api\UserManagementController; use App\Http\Controllers\Api\SiteSettingsController; +use App\Http\Controllers\Api\AccountDeletionController; // Public routes with rate limiting Route::post('/register', [AuthController::class, 'register'])->middleware('throttle:register'); @@ -368,5 +369,17 @@ Route::get('/{key}', [SiteSettingsController::class, 'show']); 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'); + + diff --git a/frontend/src/components/FactoryResetWizard.jsx b/frontend/src/components/FactoryResetWizard.jsx new file mode 100644 index 0000000..68385a7 --- /dev/null +++ b/frontend/src/components/FactoryResetWizard.jsx @@ -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 ( +
+
+
+ + {/* Header */} +
+
+ + {t('factoryReset.title') || 'Factory Reset - Exclusão Permanente'} +
+ +
+ + {/* Body */} +
+ + {/* Step Indicator */} +
+ {[1, 2, 3, 4].map((s) => ( +
+ {s < step ? '✓' : s} +
+ ))} +
+ + {/* Step 1: Warning */} + {step === 1 && ( +
+
+
+ + {t('factoryReset.warningTitle') || '⚠️ ATENÇÃO: AÇÃO IRREVERSÍVEL'} +
+

+ {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.'} +

+
+ +
+
+
+ + {t('factoryReset.dataToDelete') || 'Dados que serão deletados:'} +
+
    +
  • {t('factoryReset.deleteItem1') || 'Todas as transações'}
  • +
  • {t('factoryReset.deleteItem2') || 'Todas as contas bancárias e de ativos'}
  • +
  • {t('factoryReset.deleteItem3') || 'Todos os cartões de crédito e passivos'}
  • +
  • {t('factoryReset.deleteItem4') || 'Orçamentos e categorias personalizadas'}
  • +
  • {t('factoryReset.deleteItem5') || 'Centros de custo'}
  • +
  • {t('factoryReset.deleteItem6') || 'Objetivos financeiros'}
  • +
  • {t('factoryReset.deleteItem7') || 'Investimentos'}
  • +
  • {t('factoryReset.deleteItem8') || 'Configurações e preferências'}
  • +
  • {t('factoryReset.deleteItem9') || 'Sua conta de usuário'}
  • +
+
+
+ +
+

+ + {t('factoryReset.backupRecommendation') || + 'Recomendamos fazer um backup dos seus dados antes de prosseguir. Você poderá importá-lo posteriormente.'} +

+
+
+ )} + + {/* Step 2: Backup */} + {step === 2 && ( +
+
+ +
+ {t('factoryReset.backupTitle') || 'Backup dos Dados'} +
+

+ {t('factoryReset.backupDescription') || + 'Você pode exportar todos os seus dados para um arquivo JSON e importá-los posteriormente em qualquer conta.'} +

+
+ + {!backupCreated ? ( +
+
+ +

+ {t('factoryReset.backupInfo') || + 'O backup será gerado em formato JSON e estará disponível por 24 horas'} +

+
+
+ ) : ( +
+
+ +
+ {t('factoryReset.backupReady') || 'Backup Criado com Sucesso!'} +
+ +

+ {t('factoryReset.backupExpires') || 'Disponível por 24 horas'} +

+
+
+ )} +
+ )} + + {/* Step 3: Code */} + {step === 3 && ( +
+
+ +
+ {t('factoryReset.codeTitle') || 'Código de Confirmação'} +
+

+ {t('factoryReset.codeDescription') || + 'Um código de 6 dígitos foi enviado para seu email. Digite-o abaixo para continuar.'} +

+
+ +
+
+ + 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 && ( + + + {t('factoryReset.codeExpires') || 'Expira em 10 minutos'} + + )} +
+
+ +
+

+ + {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.'} +

+
+
+ )} + + {/* Step 4: Final Confirmation */} + {step === 4 && ( +
+
+ +
+ {t('factoryReset.finalTitle') || 'Confirmação Final'} +
+

+ {t('factoryReset.finalDescription') || + 'Este é o último passo. Uma vez confirmado, não há como voltar atrás.'} +

+
+ +
+
+ {t('factoryReset.lastWarning') || 'ÚLTIMA CHANCE DE CANCELAR'} +
+

+ {t('factoryReset.lastWarningMessage') || + 'Após confirmar, todos os seus dados serão PERMANENTEMENTE deletados. Esta ação NÃO PODE SER DESFEITA.'} +

+
+ +
+
+ + setConfirmationText(e.target.value)} + style={{ + background: '#0f172a', + border: '2px solid #dc3545', + color: 'white' + }} + /> + + {t('factoryReset.confirmationHelp') || 'Digite exatamente: DELETAR (em maiúsculas)'} + +
+
+
+ )} + +
+ + {/* Footer */} +
+ + + {step === 1 && ( + + )} + + {step === 2 && ( + <> + + {backupCreated && ( + + )} + + )} + + {step === 3 && ( + + )} + + {step === 4 && ( + + )} +
+ +
+
+
+ ); +}; + +export default FactoryResetWizard; diff --git a/frontend/src/components/ImportBackupModal.jsx b/frontend/src/components/ImportBackupModal.jsx new file mode 100644 index 0000000..adaac9b --- /dev/null +++ b/frontend/src/components/ImportBackupModal.jsx @@ -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 ( +
+
+
+ + {/* Header */} +
+
+ + {t('importBackup.title') || 'Importar Backup'} +
+ +
+ + {/* Body */} +
+ +
+
+ + {t('importBackup.infoTitle') || 'Como funciona?'} +
+

+ {t('importBackup.infoMessage') || + 'Você pode importar um backup previamente exportado. Os dados serão adicionados à sua conta atual sem apagar os dados existentes.'} +

+
+ + {/* Drag & Drop Zone */} +
document.getElementById('backup-file-input').click()} + > +
+ {!selectedFile ? ( + <> + +
+ {t('importBackup.dragDrop') || 'Arraste e solte o arquivo aqui'} +
+

+ {t('importBackup.or') || 'ou'} +

+ +

+ {t('importBackup.fileRequirements') || 'Apenas arquivos JSON (máx. 50MB)'} +

+ + ) : ( + <> + +
{selectedFile.name}
+

+ {formatFileSize(selectedFile.size)} +

+ + + )} +
+
+ + + +
+

+ + {t('importBackup.warning') || + 'Atenção: A importação pode levar alguns minutos dependendo do tamanho do backup.'} +

+
+ +
+ + {/* Footer */} +
+ + +
+ +
+
+
+ ); +}; + +export default ImportBackupModal; diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 1b2cffa..7bc32a1 100755 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -57,6 +57,10 @@ "months": "months", "unclassified": "Unclassified", "viewAll": "View all", + "continue": "Continue", + "creating": "Creating...", + "deleting": "Deleting...", + "remove": "Remove", "today": "Today", "selectTransactions": "Select transactions", "selectAll": "Select All", @@ -2182,7 +2186,15 @@ "passwordChanged": "Password changed successfully!", "passwordError": "Error changing password", "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": { "title": "Plans & Pricing", @@ -2383,5 +2395,70 @@ "terms": "Terms of Use", "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" } } \ No newline at end of file diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index da01c1b..2b407cd 100755 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -58,6 +58,10 @@ "months": "meses", "unclassified": "Sin clasificar", "viewAll": "Ver todos", + "continue": "Continuar", + "creating": "Creando...", + "deleting": "Eliminando...", + "remove": "Eliminar", "today": "Hoy", "selectTransactions": "Seleccionar transacciones", "selectAll": "Seleccionar Todas", @@ -2174,7 +2178,15 @@ "passwordChanged": "¡Contraseña cambiada con éxito!", "passwordError": "Error al cambiar contraseña", "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": { "title": "Planes y Precios", @@ -2385,5 +2397,70 @@ "terms": "Términos de Uso", "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" } } \ No newline at end of file diff --git a/frontend/src/i18n/locales/pt-BR.json b/frontend/src/i18n/locales/pt-BR.json index 3fa100c..349a7cf 100755 --- a/frontend/src/i18n/locales/pt-BR.json +++ b/frontend/src/i18n/locales/pt-BR.json @@ -58,6 +58,10 @@ "months": "meses", "unclassified": "Sem classificar", "viewAll": "Ver todos", + "continue": "Continuar", + "creating": "Criando...", + "deleting": "Deletando...", + "remove": "Remover", "date": "Data", "today": "Hoje", "selectTransactions": "Selecionar transações", @@ -2192,7 +2196,15 @@ "passwordChanged": "Senha alterada com sucesso!", "passwordError": "Erro ao alterar senha", "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": { "title": "Planos e Preços", @@ -2403,5 +2415,70 @@ "terms": "Termos de Uso", "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" } } \ No newline at end of file diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx index 989fb70..5a45758 100755 --- a/frontend/src/pages/Profile.jsx +++ b/frontend/src/pages/Profile.jsx @@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useToast } from '../components/Toast'; 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) const COUNTRY_CODES = [ @@ -93,6 +95,10 @@ export default function Profile() { const [profileErrors, setProfileErrors] = useState({}); const [passwordErrors, setPasswordErrors] = useState({}); + // Modais + const [showFactoryReset, setShowFactoryReset] = useState(false); + const [showImportBackup, setShowImportBackup] = useState(false); + useEffect(() => { loadProfile(); }, []); @@ -567,8 +573,90 @@ export default function Profile() { + + {/* Seção: Backup e Factory Reset */} +
+
+
+
+ + {t('profile.dataManagement') || 'Gerenciamento de Dados'} +
+
+
+
+ {/* Importar Backup */} +
+
+ +
+
{t('profile.importBackup') || 'Importar Backup'}
+

+ {t('profile.importBackupDesc') || + 'Restaure dados de um backup anteriormente exportado. Os dados serão adicionados à sua conta.'} +

+ +
+
+
+ + {/* Factory Reset */} +
+
+ +
+
+ {t('profile.factoryReset') || 'Factory Reset'} +
+

+ {t('profile.factoryResetDesc') || + 'ATENÇÃO: Deleta permanentemente TODOS os seus dados. Esta ação não pode ser desfeita.'} +

+ +
+
+
+
+ +
+ + + {t('profile.dataManagementWarning') || + 'Antes de fazer Factory Reset, recomendamos criar um backup dos seus dados para recuperá-los posteriormente se necessário.'} + +
+
+
+
+ + {/* Modais */} + {showFactoryReset && ( + setShowFactoryReset(false)} /> + )} + {showImportBackup && ( + setShowImportBackup(false)} + onSuccess={() => { + setShowImportBackup(false); + // Recarregar dados se necessário + window.location.reload(); + }} + /> + )} ); } diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 57d5476..56dfe0f 100755 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -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; +