webmoney/backend/app/Http/Controllers/Api/AccountDeletionController.php
marco 27f3bd8869 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
2025-12-19 16:45:08 +01:00

527 lines
22 KiB
PHP

<?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);
}
}
}