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:
parent
4ad7060323
commit
27f3bd8869
526
backend/app/Http/Controllers/Api/AccountDeletionController.php
Normal file
526
backend/app/Http/Controllers/Api/AccountDeletionController.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
backend/app/Mail/AccountDeletionConfirmation.php
Normal file
56
backend/app/Mail/AccountDeletionConfirmation.php
Normal 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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -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');
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
450
frontend/src/components/FactoryResetWizard.jsx
Normal file
450
frontend/src/components/FactoryResetWizard.jsx
Normal 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;
|
||||||
229
frontend/src/components/ImportBackupModal.jsx
Normal file
229
frontend/src/components/ImportBackupModal.jsx
Normal 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;
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user