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) <noreply@anthropic.com>
This commit is contained in:
parent
f90cd2c3ec
commit
b1abc8d639
4 changed files with 109 additions and 190 deletions
|
|
@ -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.',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
<div>
|
||||
<!-- Page header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl md:text-3xl text-gray-800 dark:text-gray-100 font-bold">{{ __('hub::hub.boosts.title') }}</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400 mt-1">{{ __('hub::hub.boosts.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
@if(count($boostOptions) > 0)
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
@foreach($boostOptions as $boost)
|
||||
<div class="bg-white dark:bg-gray-800 shadow-xs rounded-xl p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ $boost['feature_name'] }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ $boost['description'] }}
|
||||
</p>
|
||||
</div>
|
||||
@switch($boost['boost_type'])
|
||||
@case('add_limit')
|
||||
<core:badge color="blue">+{{ number_format($boost['limit_value']) }}</core:badge>
|
||||
@break
|
||||
@case('unlimited')
|
||||
<core:badge color="purple">{{ __('hub::hub.boosts.types.unlimited') }}</core:badge>
|
||||
@break
|
||||
@case('enable')
|
||||
<core:badge color="green">{{ __('hub::hub.boosts.types.enable') }}</core:badge>
|
||||
@break
|
||||
@endswitch
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-4 border-t border-gray-100 dark:border-gray-700/60">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
@switch($boost['duration_type'])
|
||||
@case('cycle_bound')
|
||||
<core:icon name="clock" class="size-4 mr-1" />
|
||||
{{ __('hub::hub.boosts.duration.cycle_bound') }}
|
||||
@break
|
||||
@case('duration')
|
||||
<core:icon name="calendar" class="size-4 mr-1" />
|
||||
{{ __('hub::hub.boosts.duration.limited') }}
|
||||
@break
|
||||
@case('permanent')
|
||||
<core:icon name="infinity" class="size-4 mr-1" />
|
||||
{{ __('hub::hub.boosts.duration.permanent') }}
|
||||
@break
|
||||
@endswitch
|
||||
</div>
|
||||
<core:button wire:click="purchaseBoost('{{ $boost['blesta_id'] }}')" size="sm" variant="primary">
|
||||
{{ __('hub::hub.boosts.actions.purchase') }}
|
||||
</core:button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-white dark:bg-gray-800 shadow-xs rounded-xl">
|
||||
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<core:icon name="rocket" class="size-8 mx-auto mb-2 opacity-50" />
|
||||
<p>{{ __('hub::hub.boosts.empty.title') }}</p>
|
||||
<p class="text-sm mt-1">{{ __('hub::hub.boosts.empty.hint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Info Section -->
|
||||
<div class="bg-blue-500/10 dark:bg-blue-500/20 rounded-xl p-6">
|
||||
<h3 class="font-semibold text-blue-900 dark:text-blue-100 mb-2">
|
||||
<core:icon name="circle-info" class="size-5 mr-2" />
|
||||
{{ __('hub::hub.boosts.info.title') }}
|
||||
</h3>
|
||||
<ul class="text-sm text-blue-800 dark:text-blue-200 space-y-2 ml-7">
|
||||
<li><strong>{{ __('hub::hub.boosts.labels.cycle_bound') }}</strong> {{ __('hub::hub.boosts.info.cycle_bound') }}</li>
|
||||
<li><strong>{{ __('hub::hub.boosts.labels.duration_based') }}</strong> {{ __('hub::hub.boosts.info.duration_based') }}</li>
|
||||
<li><strong>{{ __('hub::hub.boosts.labels.permanent') }}</strong> {{ __('hub::hub.boosts.info.permanent') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Back Link -->
|
||||
<div class="flex justify-start">
|
||||
<core:button href="{{ route('hub.usage') }}" variant="ghost">
|
||||
<core:icon name="arrow-left" class="mr-2" />
|
||||
{{ __('hub::hub.boosts.actions.back') }}
|
||||
</core:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Website\Hub\View\Modal\Admin;
|
||||
|
||||
use Core\Tenant\Models\Feature;
|
||||
use Livewire\Component;
|
||||
|
||||
class BoostPurchase extends Component
|
||||
{
|
||||
/**
|
||||
* Available boost options from config.
|
||||
*/
|
||||
public array $boostOptions = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
// Require authenticated user with a workspace
|
||||
if (! auth()->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']);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue