From b1abc8d639ffa21491bb0a10418d4149bd1ed36e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 16:44:20 +0000 Subject: [PATCH] refactor: implement 2FA stubs and remove dead BoostPurchase component Replace the five 2FA stub methods in Settings with real implementations that delegate to the TwoFactorAuthenticatable trait from php-tenant. The 2FA tab now auto-enables when the User model uses the trait, and handles the full lifecycle: enable, verify, confirm, view/regenerate recovery codes, and disable. Remove the orphaned BoostPurchase Livewire component and its blade template. The /boosts route already redirects to the account usage page's boosts tab, making this component dead code. Update language strings: remove the old "upgrading" stub message and add proper 2FA success/error messages. Fixes #15 Fixes #16 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Mod/Hub/Lang/en_GB/hub.php | 6 +- .../View/Blade/admin/boost-purchase.blade.php | 90 ------------- .../Hub/View/Modal/Admin/BoostPurchase.php | 79 ----------- src/Website/Hub/View/Modal/Admin/Settings.php | 124 +++++++++++++++--- 4 files changed, 109 insertions(+), 190 deletions(-) delete mode 100644 src/Website/Hub/View/Blade/admin/boost-purchase.blade.php delete mode 100644 src/Website/Hub/View/Modal/Admin/BoostPurchase.php diff --git a/src/Mod/Hub/Lang/en_GB/hub.php b/src/Mod/Hub/Lang/en_GB/hub.php index fd3278c..65e6dbf 100644 --- a/src/Mod/Hub/Lang/en_GB/hub.php +++ b/src/Mod/Hub/Lang/en_GB/hub.php @@ -652,7 +652,11 @@ return [ 'profile_updated' => 'Profile updated successfully.', 'preferences_updated' => 'Preferences saved.', 'password_updated' => 'Password changed successfully.', - 'two_factor_upgrading' => 'Two-factor authentication is currently being upgraded. Please try again later.', + 'two_factor_enabled' => 'Two-factor authentication has been enabled.', + 'two_factor_disabled' => 'Two-factor authentication has been disabled.', + 'two_factor_invalid_code' => 'The verification code is invalid.', + 'two_factor_codes_regenerated' => 'Recovery codes have been regenerated.', + 'two_factor_unavailable' => 'Two-factor authentication is not available for your account.', 'deletion_scheduled' => 'Account deletion scheduled. Check your email for options.', 'deletion_cancelled' => 'Account deletion has been cancelled.', ], diff --git a/src/Website/Hub/View/Blade/admin/boost-purchase.blade.php b/src/Website/Hub/View/Blade/admin/boost-purchase.blade.php deleted file mode 100644 index e6fb1ea..0000000 --- a/src/Website/Hub/View/Blade/admin/boost-purchase.blade.php +++ /dev/null @@ -1,90 +0,0 @@ -
- -
-

{{ __('hub::hub.boosts.title') }}

-

{{ __('hub::hub.boosts.subtitle') }}

-
- -
- @if(count($boostOptions) > 0) -
- @foreach($boostOptions as $boost) -
-
-
-

- {{ $boost['feature_name'] }} -

-

- {{ $boost['description'] }} -

-
- @switch($boost['boost_type']) - @case('add_limit') - +{{ number_format($boost['limit_value']) }} - @break - @case('unlimited') - {{ __('hub::hub.boosts.types.unlimited') }} - @break - @case('enable') - {{ __('hub::hub.boosts.types.enable') }} - @break - @endswitch -
- -
-
- @switch($boost['duration_type']) - @case('cycle_bound') - - {{ __('hub::hub.boosts.duration.cycle_bound') }} - @break - @case('duration') - - {{ __('hub::hub.boosts.duration.limited') }} - @break - @case('permanent') - - {{ __('hub::hub.boosts.duration.permanent') }} - @break - @endswitch -
- - {{ __('hub::hub.boosts.actions.purchase') }} - -
-
- @endforeach -
- @else -
-
- -

{{ __('hub::hub.boosts.empty.title') }}

-

{{ __('hub::hub.boosts.empty.hint') }}

-
-
- @endif - - -
-

- - {{ __('hub::hub.boosts.info.title') }} -

-
    -
  • {{ __('hub::hub.boosts.labels.cycle_bound') }} {{ __('hub::hub.boosts.info.cycle_bound') }}
  • -
  • {{ __('hub::hub.boosts.labels.duration_based') }} {{ __('hub::hub.boosts.info.duration_based') }}
  • -
  • {{ __('hub::hub.boosts.labels.permanent') }} {{ __('hub::hub.boosts.info.permanent') }}
  • -
-
- - -
- - - {{ __('hub::hub.boosts.actions.back') }} - -
-
-
diff --git a/src/Website/Hub/View/Modal/Admin/BoostPurchase.php b/src/Website/Hub/View/Modal/Admin/BoostPurchase.php deleted file mode 100644 index d85b5ce..0000000 --- a/src/Website/Hub/View/Modal/Admin/BoostPurchase.php +++ /dev/null @@ -1,79 +0,0 @@ -check()) { - abort(403, 'Authentication required.'); - } - - // Get boost options from config - $addonMapping = config('services.blesta.addon_mapping', []); - - $this->boostOptions = collect($addonMapping)->map(function ($config, $blestaId) { - $feature = Feature::where('code', $config['feature_code'])->first(); - - return [ - 'blesta_id' => $blestaId, - 'feature_code' => $config['feature_code'], - 'feature_name' => $feature?->name ?? $config['feature_code'], - 'boost_type' => $config['boost_type'], - 'limit_value' => $config['limit_value'] ?? null, - 'duration_type' => $config['duration_type'], - 'description' => $this->getBoostDescription($config), - ]; - })->values()->toArray(); - } - - protected function getBoostDescription(array $config): string - { - $type = $config['boost_type']; - $value = $config['limit_value'] ?? null; - $duration = $config['duration_type']; - - $description = match ($type) { - 'add_limit' => "+{$value} additional", - 'unlimited' => 'Unlimited access', - 'enable' => 'Feature enabled', - default => 'Boost', - }; - - $durationText = match ($duration) { - 'cycle_bound' => 'until billing cycle ends', - 'duration' => 'for limited time', - 'permanent' => 'permanently', - default => '', - }; - - return trim("{$description} {$durationText}"); - } - - public function purchaseBoost(string $blestaId): void - { - // Redirect to Blesta for purchase - // TODO: Implement when Blesta is configured - $blestaUrl = config('services.blesta.url', 'https://billing.host.uk.com'); - - $this->redirect("{$blestaUrl}/order/addon/{$blestaId}"); - } - - public function render() - { - return view('hub::admin.boost-purchase') - ->layout('hub::admin.layouts.app', ['title' => 'Purchase Boost']); - } -} diff --git a/src/Website/Hub/View/Modal/Admin/Settings.php b/src/Website/Hub/View/Modal/Admin/Settings.php index 7902c65..3dccf4d 100644 --- a/src/Website/Hub/View/Modal/Admin/Settings.php +++ b/src/Website/Hub/View/Modal/Admin/Settings.php @@ -4,6 +4,12 @@ declare(strict_types=1); namespace Website\Hub\View\Modal\Admin; +use Core\Mod\Social\Models\Setting; +use Core\Tenant\Concerns\TwoFactorAuthenticatable; +use Core\Tenant\Mail\AccountDeletionRequested; +use Core\Tenant\Models\AccountDeletionRequest; +use Core\Tenant\Models\User; +use Core\Tenant\Services\UserStatsService; use Flux\Flux; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -12,11 +18,6 @@ use Illuminate\Validation\Rules\Password; use Livewire\Attributes\Url; use Livewire\Attributes\Validate; use Livewire\Component; -use Core\Mod\Social\Models\Setting; -use Core\Tenant\Mail\AccountDeletionRequested; -use Core\Tenant\Models\AccountDeletionRequest; -use Core\Tenant\Models\User; -use Core\Tenant\Services\UserStatsService; use Website\Hub\Concerns\HasRateLimiting; class Settings extends Component @@ -96,11 +97,9 @@ class Settings extends Component $this->time_format = (int) $this->getUserSetting('time_format', 12); $this->week_starts_on = (int) $this->getUserSetting('week_starts_on', 1); - // Feature flags - 2FA disabled until native implementation - $this->isTwoFactorEnabled = config('social.features.two_factor_auth', false); - $this->userHasTwoFactorEnabled = method_exists($user, 'hasTwoFactorAuthEnabled') - ? $user->hasTwoFactorAuthEnabled() - : false; + // Feature flags - 2FA enabled when the User model uses TwoFactorAuthenticatable + $this->isTwoFactorEnabled = $this->userSupportsTwoFactor($user); + $this->userHasTwoFactorEnabled = $this->isTwoFactorEnabled && $user->hasTwoFactorAuthEnabled(); // Check for pending deletion request $this->pendingDeletion = AccountDeletionRequest::where('user_id', $user->id) @@ -113,6 +112,14 @@ class Settings extends Component $this->timezones = UserStatsService::getTimezoneList(); } + /** + * Check whether the User model uses the TwoFactorAuthenticatable trait. + */ + protected function userSupportsTwoFactor(User $user): bool + { + return in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user), true); + } + protected function onRateLimited(string $action, string $key): void { $seconds = \Illuminate\Support\Facades\RateLimiter::availableIn($key); @@ -196,34 +203,111 @@ class Settings extends Component }); } + /** + * Begin 2FA setup: generate a secret and display the QR code. + */ public function enableTwoFactor(): void { - // TODO: Implement native 2FA - currently disabled - Flux::toast(text: __('hub::hub.settings.messages.two_factor_upgrading'), variant: 'warning'); + $user = User::findOrFail(Auth::id()); + + if (! $this->userSupportsTwoFactor($user)) { + Flux::toast(text: __('hub::hub.settings.messages.two_factor_unavailable'), variant: 'warning'); + + return; + } + + $secret = $user->enableTwoFactorAuth(); + + $this->twoFactorSecretKey = $secret; + $this->twoFactorQrCode = $user->twoFactorQrCodeSvg(); + $this->showTwoFactorSetup = true; + $this->twoFactorCode = ''; } + /** + * Verify the TOTP code and confirm 2FA activation. + */ public function confirmTwoFactor(): void { - // TODO: Implement native 2FA - currently disabled - Flux::toast(text: __('hub::hub.settings.messages.two_factor_upgrading'), variant: 'warning'); + $this->validate([ + 'twoFactorCode' => ['required', 'string', 'size:6'], + ]); + + $user = User::findOrFail(Auth::id()); + + if (! $this->userSupportsTwoFactor($user)) { + Flux::toast(text: __('hub::hub.settings.messages.two_factor_unavailable'), variant: 'warning'); + + return; + } + + if (! $user->verifyTwoFactorCode($this->twoFactorCode)) { + $this->addError('twoFactorCode', __('hub::hub.settings.messages.two_factor_invalid_code')); + + return; + } + + $this->recoveryCodes = $user->confirmTwoFactorAuth(); + $this->showTwoFactorSetup = false; + $this->showRecoveryCodes = true; + $this->userHasTwoFactorEnabled = true; + $this->twoFactorCode = ''; + $this->twoFactorQrCode = null; + $this->twoFactorSecretKey = null; + + Flux::toast(text: __('hub::hub.settings.messages.two_factor_enabled'), variant: 'success'); } + /** + * Display the user's existing recovery codes. + */ public function showRecoveryCodesModal(): void { - // TODO: Implement native 2FA - currently disabled - Flux::toast(text: __('hub::hub.settings.messages.two_factor_upgrading'), variant: 'warning'); + $user = User::findOrFail(Auth::id()); + + if (! $this->userSupportsTwoFactor($user) || ! $user->hasTwoFactorAuthEnabled()) { + return; + } + + $this->recoveryCodes = $user->twoFactorRecoveryCodes(); + $this->showRecoveryCodes = true; } + /** + * Generate a fresh set of recovery codes. + */ public function regenerateRecoveryCodes(): void { - // TODO: Implement native 2FA - currently disabled - Flux::toast(text: __('hub::hub.settings.messages.two_factor_upgrading'), variant: 'warning'); + $user = User::findOrFail(Auth::id()); + + if (! $this->userSupportsTwoFactor($user) || ! $user->hasTwoFactorAuthEnabled()) { + return; + } + + $this->recoveryCodes = $user->regenerateTwoFactorRecoveryCodes(); + $this->showRecoveryCodes = true; + + Flux::toast(text: __('hub::hub.settings.messages.two_factor_codes_regenerated'), variant: 'success'); } + /** + * Disable 2FA for the current user. + */ public function disableTwoFactor(): void { - // TODO: Implement native 2FA - currently disabled - Flux::toast(text: __('hub::hub.settings.messages.two_factor_upgrading'), variant: 'warning'); + $user = User::findOrFail(Auth::id()); + + if (! $this->userSupportsTwoFactor($user)) { + return; + } + + $user->disableTwoFactorAuth(); + + $this->userHasTwoFactorEnabled = false; + $this->showRecoveryCodes = false; + $this->recoveryCodes = []; + + Flux::toast(text: __('hub::hub.settings.messages.two_factor_disabled'), variant: 'success'); } public function requestAccountDeletion(): void