Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers;
use Laravel\Jetstream\Jetstream;
class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules;
/**
* Validate and create a newly registered user.
*
* @param array $input
* @return \App\Models\User
*/
public function create(array $input)
{
Validator::make($input, [
'name' => ['required', 'string','max:25', 'unique:users'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => $this->passwordRules(),
// 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
])->validate();
$user = User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
// Always move this section to the final registration.
Session([
'activeProfileType' => User::class,
'activeProfileId' => Auth::guard('web')->user()->id,
'activeProfileName'=> Auth::guard('web')->user()->name,
'activeProfilePhoto'=> Auth::guard('web')->user()->profile_photo_path,
'firstLogin' => true
]);
//TODO: Welcome and introduction with Session('firstLogin') on rest of site views
return $user;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Actions\Fortify;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
class EnableTwoFactorAuthentication
{
/**
* The two factor authentication provider.
*
* @var \Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider
*/
protected $provider;
/**
* Create a new action instance.
*
* @param \Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider $provider
* @return void
*/
public function __construct(TwoFactorAuthenticationProvider $provider)
{
$this->provider = $provider;
}
/**
* Enable two factor authentication for the user by generating secrets
* and storing them temporarily in the session.
*
* @param mixed $user
* @return void
*/
public function __invoke($user)
{
$secretKey = $this->provider->generateSecretKey();
$recoveryCodes = collect(range(1, 8))
->map(fn () => Str::random(10).'-'.Str::random(10))
->all();
$qrCodeSvg = $this->provider->qrCodeSvg(
config('app.name'),
$user->email,
$secretKey
);
// Store the generated data in the session
session([
'2fa_setup_secret' => $secretKey, // Unencrypted secret for display and confirmation
'2fa_setup_qr_svg' => $qrCodeSvg,
'2fa_setup_recovery_codes' => encrypt(json_encode($recoveryCodes)), // Encrypt for storage in session
]);
// IMPORTANT: This custom action does NOT save anything to the user model in the database.
// That will be handled by the custom ConfirmTwoFactorAuthentication action.
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Actions\Fortify;
use Laravel\Fortify\Rules\Password;
trait PasswordValidationRules
{
/**
* Get the validation rules used to validate passwords.
*
* @return array
*/
protected function passwordRules()
{
// Dynamically get the password validation rules from the config
return timebank_config('rules.profile_user.password', ['required', 'string', 'min:8', 'confirmed']);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Actions\Fortify;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;
class ResetUserPassword implements ResetsUserPasswords
{
use PasswordValidationRules;
/**
* Validate and reset the user's forgotten password.
*
* @param mixed $user
* @param array $input
* @return void
*/
public function reset($user, array $input)
{
Validator::make($input, [
'password' => $this->passwordRules(),
])->validate();
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Actions\Fortify;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
class UpdateUserPassword implements UpdatesUserPasswords
{
use PasswordValidationRules;
/**
* Validate and update the user's password.
*
* @param mixed $user
* @param array $input
* @return void
*/
public function update($user, array $input)
{
Validator::make($input, [
'current_password' => ['required', 'string'],
'password' => $this->passwordRules(),
])->after(function ($validator) use ($user, $input) {
if (! isset($input['current_password']) || ! Hash::check($input['current_password'], $user->password)) {
$validator->errors()->add('current_password', __('The provided password does not match your current password.'));
}
})->validateWithBag('updatePassword');
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
activity()
->useLog('User')
->performedOn($user)
->causedBy(Auth::guard('web')->user())
->event('password_changed')
->log('Password changed for ' . $user->name);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Actions\Fortify;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{
/**
* Validate and update the given user's profile information.
*
* @param mixed $user
* @param array $input
* @return void
*/
public function update($user, array $input)
{
Validator::make($input, [
'name' => ['required', 'string', 'min:3', 'max:40', Rule::unique('users')->ignore($user->id)],
'email' => ['required', 'email', 'max:40', Rule::unique('users')->ignore($user->id)],
'photo' => ['nullable', 'mimes:jpg,jpeg,png,svg', 'max:1024'],
])->validateWithBag('updateProfileInformation');
if (isset($input['photo'])) {
$user->updateProfilePhoto($input['photo']);
} else {
$user->forcefill(['profile_photo_path' => timebank_config('profiles.user.profile_photo_path_default')])->save();
}
if ($input['email'] !== $user->email &&
$user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
'email' => $input['email'],
])->save();
// Also update session with new name and profile_photo_path
Session([
'activeProfileName' => Auth::user()->name,
'activeProfilePhoto' => Auth::user()->profile_photo_path
]);
return redirect()->route('profile.show_by_type_and_id');
}
}
/**
* Update the given verified user's profile information.
*
* @param mixed $user
* @param array $input
* @return void
*/
protected function updateVerifiedUser($user, array $input)
{
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
])->save();
$user->sendEmailVerificationNotification();
}
}

View File

@@ -0,0 +1,566 @@
<?php
namespace App\Actions\Jetstream;
use App\Models\User;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Facades\DB;
use Laravel\Jetstream\Contracts\DeletesUsers;
use Throwable;
class DeleteUser implements DeletesUsers
{
/**
* Delete the given user.
*
* @param mixed $user
* @param string $balanceHandlingOption
* @param int|null $donationAccountId
* @param bool $isAutoDeleted
* @param string|null $deletedByUsername
* @return void
*/
public function delete($user, $balanceHandlingOption = 'delete', $donationAccountId = null, $isAutoDeleted = false, $deletedByUsername = null)
{
try {
// Use a transaction for deleting the user
// START
DB::transaction(function () use ($user, $balanceHandlingOption, $donationAccountId, $isAutoDeleted, $deletedByUsername): void {
// Check for negative balances before proceeding with deletion
$userAccounts = $user->accounts()->active()->notRemoved()->get();
foreach ($userAccounts as $account) {
\Cache::forget("account_balance_{$account->id}");
if ($account->balance < 0) {
\Log::error('Profile deletion blocked: negative balance detected', [
'user_id' => $user->id,
'account_id' => $account->id,
'account_name' => $account->name,
'balance' => $account->balance
]);
throw new \Exception('Cannot delete profile with negative balance. Please settle all debts before deleting your profile.');
}
}
// Store balance handling preferences in cache for later use
// This will be used by permanentlyDelete() after grace period
// Fallback: if cache is lost, currency will be destroyed (transferred to debit account)
$balanceHandlingData = [
'option' => $balanceHandlingOption,
'donation_account_id' => $donationAccountId,
'stored_at' => now()->toDateTimeString(),
];
// Store in cache with TTL = grace period + 7 days buffer
$gracePeriodDays = timebank_config('delete_profile.grace_period_days', 30);
$cacheKey = 'balance_handling_' . get_class($user) . '_' . $user->id;
\Cache::put($cacheKey, $balanceHandlingData, now()->addDays($gracePeriodDays + 7));
// Set human-readable comment (always in English for database storage)
if ($isAutoDeleted) {
// Auto-deletion due to inactivity
$daysInactive = timebank_config('profile_inactive.days_not_logged_in') + timebank_config('delete_profile.days_after_inactive.run_delete');
$user->comment = 'Profile automatically deleted after ' . $daysInactive . ' days of inactivity.';
} elseif ($deletedByUsername) {
// Admin/manager deletion
$user->comment = 'Profile deleted by ' . $deletedByUsername;
} else {
// Self-deletion by profile owner
$user->comment = 'Profile deleted by self-deletion';
}
// Mark profile as deleted (soft delete with grace period)
// Balances will be handled after grace period by scheduled command
// Accounts remain active during grace period to allow restoration
$user->deleted_at = now();
$user->save();
// Delete tokens to force logout
if ($user instanceof \App\Models\User) {
$user->tokens->each->delete();
}
});
// STOP
// End of transaction
return ['status' => 'success'];
} catch (Throwable $e) {
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
/**
* Donate user's account balances to an organization.
*
* @param mixed $user
* @param int $donationAccountId
* @return void
*/
protected function donateBalancesToOrganization($user, $donationAccountId)
{
\Log::info('Starting balance donation', [
'user_id' => $user->id,
'donation_account_id' => $donationAccountId
]);
// Get the donation target account
$toAccount = \App\Models\Account::find($donationAccountId);
if (!$toAccount) {
\Log::error('Donation account not found', ['donation_account_id' => $donationAccountId]);
throw new \Exception('Donation account not found.');
}
\Log::info('Donation target account found', [
'account_id' => $toAccount->id,
'account_name' => $toAccount->name,
'accountable_type' => $toAccount->accountable_type
]);
// Verify the target account is an organization
if ($toAccount->accountable_type !== 'App\\Models\\Organization') {
\Log::error('Target account is not an organization', [
'accountable_type' => $toAccount->accountable_type
]);
throw new \Exception('The selected account is not an organization account.');
}
// Get all active accounts belonging to the user with positive balances
$userAccounts = $user->accounts()
->active()
->notRemoved()
->get();
// Clear balance cache for all accounts to ensure we get current values
foreach ($userAccounts as $account) {
\Cache::forget("account_balance_{$account->id}");
}
\Log::info('User accounts found', [
'count' => $userAccounts->count(),
'accounts' => $userAccounts->map(function($acc) {
return [
'id' => $acc->id,
'name' => $acc->name,
'balance' => $acc->balance
];
})
]);
$totalTransferred = 0;
$transactionsCreated = 0;
foreach ($userAccounts as $fromAccount) {
// Calculate the current balance
$balance = $fromAccount->balance;
\Log::info('Processing account', [
'account_id' => $fromAccount->id,
'balance' => $balance
]);
// Only create transaction if there's a positive balance
if ($balance > 0) {
try {
// Create a donation transaction
$transaction = \App\Models\Transaction::create([
'from_account_id' => $fromAccount->id,
'to_account_id' => $toAccount->id,
'transaction_type_id' => 3, // Donation type
'amount' => $balance,
'description' => 'Balance donation from deleted profile ' . $user->name,
'created_at' => now(),
'updated_at' => now(),
]);
$totalTransferred += $balance;
$transactionsCreated++;
\Log::info('Transaction created successfully', [
'transaction_id' => $transaction->id,
'amount' => $balance
]);
} catch (\Exception $e) {
\Log::error('Failed to create transaction', [
'from_account_id' => $fromAccount->id,
'to_account_id' => $toAccount->id,
'amount' => $balance,
'error' => $e->getMessage()
]);
throw new \Exception('Failed to create donation transaction: ' . $e->getMessage());
}
}
}
\Log::info('Balance donation completed', [
'transactions_created' => $transactionsCreated,
'total_transferred' => $totalTransferred
]);
// Mark associated accounts inactive
$user->accounts()->update(['inactive_at' => now(), 'deleted_at' => now()]);
}
/**
* Transfer user's account balances to a bank the profile was a client of.
*
* @param mixed $user
* @return void
*/
protected function transferBalancesToBankClient($user)
{
// Get all active accounts belonging to the user with positive balances
$userAccounts = $user->accounts()
->active()
->notRemoved()
->get();
// Find banks the user was a client of
// This would need to be implemented based on your bank-client relationship structure
// For now, this is a placeholder for the implementation
foreach ($userAccounts as $fromAccount) {
$balance = $fromAccount->balance;
if ($balance > 0) {
// Transfer to bank account logic would go here
// You would need to determine which bank account to use
}
}
}
/**
* Transfer user's account balances to a specific account ID.
*
* @param mixed $user
* @param int $accountId
* @return void
*/
protected function transferBalancesToSpecificAccount($user, $accountId)
{
// Get the target account
$toAccount = \App\Models\Account::find($accountId);
if (!$toAccount) {
throw new \Exception('Target account not found.');
}
// Get all active accounts belonging to the user with positive balances
$userAccounts = $user->accounts()
->active()
->notRemoved()
->get();
foreach ($userAccounts as $fromAccount) {
$balance = $fromAccount->balance;
if ($balance > 0) {
// Create a donation transaction to the specified account
\App\Models\Transaction::create([
'from_account_id' => $fromAccount->id,
'to_account_id' => $toAccount->id,
'transaction_type_id' => 3, // Donation type
'amount' => $balance,
'description' => 'Balance transfer from deleted profile ' . $user->name,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
// Mark associated accounts inactive
$user->accounts()->update(['inactive_at' => now(), 'deleted_at' => now()]);
}
/**
* Transfer user's account balances to debit account (remove currency from circulation).
*
* @param mixed $user
* @return void
*/
protected function transferBalancesToDebitAccount($user)
{
// Get all active accounts belonging to the user with positive balances
$userAccounts = $user->accounts()
->active()
->notRemoved()
->get();
// Find the debit account (typically a system account for currency removal)
$debitAccount = \App\Models\Account::where('name', 'debit')
->whereHasMorph('accountable', [\App\Models\Bank::class])
->first();
if (!$debitAccount) {
throw new \Exception('Debit account not found for currency removal.');
}
foreach ($userAccounts as $fromAccount) {
$balance = $fromAccount->balance;
if ($balance > 0) {
// Create a currency removal transaction
\App\Models\Transaction::create([
'from_account_id' => $fromAccount->id,
'to_account_id' => $debitAccount->id,
'transaction_type_id' => 5, // Currency removal type
'amount' => $balance,
'description' => 'Currency removal from deleted profile ' . $user->name,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
// Mark associated accounts inactive
$user->accounts()->update(['inactive_at' => now(), 'deleted_at' => now()]);
}
/**
* Permanently delete a profile by handling balances and anonymizing all data.
* Called by scheduled command after grace period expires.
*
* @param mixed $user
* @return array
*/
public function permanentlyDelete($user)
{
try {
DB::transaction(function () use ($user): void {
$profileType = get_class($user);
$profileTypeName = class_basename($profileType);
// Retrieve balance handling preferences from cache
$cacheKey = 'balance_handling_' . get_class($user) . '_' . $user->id;
$balanceHandlingData = \Cache::get($cacheKey);
// Fallback: try to parse from comment field if it's JSON (old format compatibility)
if (!$balanceHandlingData && $user->comment && str_starts_with($user->comment, '{')) {
$balanceHandlingData = json_decode($user->comment, true);
}
// Handle balances before anonymization
if ($balanceHandlingData && isset($balanceHandlingData['option'])) {
$option = $balanceHandlingData['option'];
$donationAccountId = $balanceHandlingData['donation_account_id'] ?? null;
// Execute balance handling based on stored option
if ($option === 'donate' && $donationAccountId) {
$this->donateBalancesToOrganization($user, $donationAccountId);
} elseif ($option === 'delete') {
// User chose to delete balance - destroy currency
$this->transferBalancesToDebitAccount($user);
}
} else {
// FALLBACK: Cache lost or no data stored
// Destroy currency (transfer to debit account) as safe default
\Log::warning('Balance handling cache lost for profile deletion', [
'user_id' => $user->id,
'user_name' => $user->name,
'fallback' => 'destroying_currency'
]);
$this->transferBalancesToDebitAccount($user);
}
// Handle WireChat kept messages to prevent orphaned data
// This is ALWAYS done when profile is permanently deleted (not optional)
if (timebank_config('wirechat.profile_deletion.release_kept_messages', true)) {
$releasedCount = \DB::table('wirechat_messages')
->where('sendable_id', $user->id)
->where('sendable_type', get_class($user))
->whereNotNull('kept_at')
->update([
'kept_at' => null,
'updated_at' => now()
]);
if ($releasedCount > 0) {
\Log::info('WireChat kept messages released for deleted profile', [
'profile_id' => $user->id,
'profile_type' => get_class($user),
'profile_name' => $user->name,
'messages_released' => $releasedCount
]);
}
}
// Detach all relationships that do not need any historic record
// User-specific relationships
if ($user instanceof \App\Models\User) {
if (method_exists($user, 'locations')) {
$user->locations()->delete();
}
if (method_exists($user, 'languages')) {
$user->languages()->detach();
}
if (method_exists($user, 'socials')) {
$user->socials()->detach();
}
if (method_exists($user, 'organizations')) {
$user->organizations()->detach();
}
if (method_exists($user, 'bankClients')) {
$user->bankClients()->detach();
}
if (method_exists($user, 'banksManaged')) {
$user->banksManaged()->detach();
}
if (method_exists($user, 'admins')) {
$user->admins()->detach();
}
}
// Organization/Bank/Admin specific relationships
if ($user instanceof \App\Models\Organization) {
if (method_exists($user, 'users')) {
$user->users()->detach();
}
}
if ($user instanceof \App\Models\Bank) {
if (method_exists($user, 'managers')) {
$user->managers()->detach();
}
}
if ($user instanceof \App\Models\Admin) {
if (method_exists($user, 'users')) {
$user->users()->detach();
}
}
// Common relationships for all profile types
if (method_exists($user, 'locations')) {
$user->locations()->delete();
}
// Anonymize profile
$anonymousId = $this->generateAnonymousId($profileType);
$user->name = 'Removed ' . strtolower($profileTypeName) . ' ' . $anonymousId;
$user->full_name = 'Removed ' . strtolower($profileTypeName) . ' ' . $anonymousId;
$user->email = 'removed-' . $anonymousId . '@remove.ed';
$user->email_verified_at = null;
$user->password = "";
if (property_exists($user, 'two_factor_secret')) {
$user->two_factor_secret = null;
}
if (property_exists($user, 'two_factor_recovery_codes')) {
$user->two_factor_recovery_codes = null;
}
if (property_exists($user, 'two_factor_confirmed_at')) {
$user->two_factor_confirmed_at = null;
}
$user->deleteProfilePhoto();
$user->profile_photo_path = 'app-images/profile-user-removed.svg';
$user->about = null;
$user->about_short = null;
$user->motivation = null;
if (property_exists($user, 'date_of_birth')) {
$user->date_of_birth = null;
}
$user->website = null;
$user->phone = null;
$user->phone_public = 0;
if (property_exists($user, 'remember_token')) {
$user->remember_token = null;
}
if (property_exists($user, 'current_team_id')) {
$user->current_team_id = null;
}
if (property_exists($user, 'cyclos_id')) {
$user->cyclos_id = null;
}
if (property_exists($user, 'cyclos_salt')) {
$user->cyclos_salt = null;
}
if (property_exists($user, 'cyclos_skills')) {
$user->cyclos_skills = null;
}
$user->limit_min = 0;
$user->limit_max = 0;
$user->comment = null;
$user->lang_preference = null;
if (property_exists($user, 'principles_terms_accepted')) {
$user->principles_terms_accepted = null;
}
$user->last_login_ip = null;
$user->save();
// Unreact all Laravel-love reactions
if (!($user instanceof \App\Models\Admin)) {
$reacterFacade = $user->getloveReacter();
$reactions = $reacterFacade->getReactions()->load(['reactant', 'type']);
foreach ($reactions as $reaction) {
if ($reaction->reactant && $reaction->type) {
$reacterFacade->unReactTo($reaction->reactant, $reaction->type);
}
}
$reactantFacade = $user->getloveReactant();
$receivedReactions = $reactantFacade->getReactions()->load(['reacter', 'type']);
foreach ($receivedReactions as $reaction) {
if ($reaction->reacter && $reaction->type) {
$reaction->reacter->unReactTo($reaction->reactant, $reaction->type);
}
}
}
// Remove all taggable skills
if (!($user instanceof \App\Models\Admin)) {
$user->detag();
}
// Clear the balance handling cache
$cacheKey = 'balance_handling_' . get_class($user) . '_' . $user->id;
\Cache::forget($cacheKey);
});
return ['status' => 'success'];
} catch (Throwable $e) {
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
/**
* Generate a short, anonymous, unique identifier for deleted profiles.
*
* @param string $profileType The profile model class name
* @return string 8-character alphanumeric ID
*/
protected function generateAnonymousId($profileType)
{
$attempts = 0;
$maxAttempts = 100;
do {
// Generate 8-character random alphanumeric string (lowercase for consistency)
$anonymousId = strtolower(substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, 8));
// Check if this ID is already used in name or email fields
$nameExists = $profileType::where('name', 'like', '%' . $anonymousId . '%')->exists();
$emailExists = $profileType::where('email', 'like', '%' . $anonymousId . '%')->exists();
$attempts++;
if ($attempts >= $maxAttempts) {
// Fallback to timestamp-based ID if we can't find a unique random one
$anonymousId = strtolower(substr(md5(microtime()), 0, 8));
break;
}
} while ($nameExists || $emailExists);
return $anonymousId;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Actions\Jetstream;
use Illuminate\Support\Facades\Log;
use Throwable;
class RestoreProfile
{
/**
* Restore a deleted profile if within grace period and not yet anonymized.
*
* @param mixed $profile
* @return array
*/
public function restore($profile)
{
try {
// Check if profile is actually deleted
if (!$profile->deleted_at) {
return [
'status' => 'error',
'message' => 'Profile is not deleted.'
];
}
// Check if profile has been anonymized (email is the indicator)
if (str_starts_with($profile->email, 'removed-') && str_ends_with($profile->email, '@remove.ed')) {
return [
'status' => 'error',
'message' => 'Profile has been permanently deleted and cannot be restored.'
];
}
// Check if grace period has expired
$gracePeriodDays = timebank_config('delete_profile.grace_period_days', 30);
$gracePeriodExpiry = $profile->deleted_at->addDays($gracePeriodDays);
if (now()->isAfter($gracePeriodExpiry)) {
return [
'status' => 'error',
'message' => 'Grace period has expired. Profile cannot be restored.'
];
}
// Restore the profile by clearing deleted_at and balance handling data
$profile->deleted_at = null;
$profile->comment = null; // Clear stored balance handling preferences
$profile->save();
// Clear balance handling cache
$cacheKey = 'balance_handling_' . get_class($profile) . '_' . $profile->id;
\Cache::forget($cacheKey);
// Restore associated accounts (they were never marked deleted during grace period)
// No need to update accounts as they remain active during grace period
Log::info('Profile restored', [
'profile_type' => get_class($profile),
'profile_id' => $profile->id,
'profile_name' => $profile->name,
]);
return [
'status' => 'success',
'message' => 'Profile has been successfully restored.'
];
} catch (Throwable $e) {
Log::error('Profile restoration failed', [
'profile_id' => $profile->id ?? null,
'error' => $e->getMessage()
]);
return [
'status' => 'error',
'message' => $e->getMessage()
];
}
}
}