Compare commits
1 commit
dev
...
feat/imple
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1abc8d639 |
4 changed files with 109 additions and 190 deletions
|
|
@ -652,7 +652,11 @@ return [
|
||||||
'profile_updated' => 'Profile updated successfully.',
|
'profile_updated' => 'Profile updated successfully.',
|
||||||
'preferences_updated' => 'Preferences saved.',
|
'preferences_updated' => 'Preferences saved.',
|
||||||
'password_updated' => 'Password changed successfully.',
|
'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_scheduled' => 'Account deletion scheduled. Check your email for options.',
|
||||||
'deletion_cancelled' => 'Account deletion has been cancelled.',
|
'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;
|
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 Flux\Flux;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
@ -12,11 +18,6 @@ use Illuminate\Validation\Rules\Password;
|
||||||
use Livewire\Attributes\Url;
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Component;
|
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;
|
use Website\Hub\Concerns\HasRateLimiting;
|
||||||
|
|
||||||
class Settings extends Component
|
class Settings extends Component
|
||||||
|
|
@ -96,11 +97,9 @@ class Settings extends Component
|
||||||
$this->time_format = (int) $this->getUserSetting('time_format', 12);
|
$this->time_format = (int) $this->getUserSetting('time_format', 12);
|
||||||
$this->week_starts_on = (int) $this->getUserSetting('week_starts_on', 1);
|
$this->week_starts_on = (int) $this->getUserSetting('week_starts_on', 1);
|
||||||
|
|
||||||
// Feature flags - 2FA disabled until native implementation
|
// Feature flags - 2FA enabled when the User model uses TwoFactorAuthenticatable
|
||||||
$this->isTwoFactorEnabled = config('social.features.two_factor_auth', false);
|
$this->isTwoFactorEnabled = $this->userSupportsTwoFactor($user);
|
||||||
$this->userHasTwoFactorEnabled = method_exists($user, 'hasTwoFactorAuthEnabled')
|
$this->userHasTwoFactorEnabled = $this->isTwoFactorEnabled && $user->hasTwoFactorAuthEnabled();
|
||||||
? $user->hasTwoFactorAuthEnabled()
|
|
||||||
: false;
|
|
||||||
|
|
||||||
// Check for pending deletion request
|
// Check for pending deletion request
|
||||||
$this->pendingDeletion = AccountDeletionRequest::where('user_id', $user->id)
|
$this->pendingDeletion = AccountDeletionRequest::where('user_id', $user->id)
|
||||||
|
|
@ -113,6 +112,14 @@ class Settings extends Component
|
||||||
$this->timezones = UserStatsService::getTimezoneList();
|
$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
|
protected function onRateLimited(string $action, string $key): void
|
||||||
{
|
{
|
||||||
$seconds = \Illuminate\Support\Facades\RateLimiter::availableIn($key);
|
$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
|
public function enableTwoFactor(): void
|
||||||
{
|
{
|
||||||
// TODO: Implement native 2FA - currently disabled
|
$user = User::findOrFail(Auth::id());
|
||||||
Flux::toast(text: __('hub::hub.settings.messages.two_factor_upgrading'), variant: 'warning');
|
|
||||||
|
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
|
public function confirmTwoFactor(): void
|
||||||
{
|
{
|
||||||
// TODO: Implement native 2FA - currently disabled
|
$this->validate([
|
||||||
Flux::toast(text: __('hub::hub.settings.messages.two_factor_upgrading'), variant: 'warning');
|
'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
|
public function showRecoveryCodesModal(): void
|
||||||
{
|
{
|
||||||
// TODO: Implement native 2FA - currently disabled
|
$user = User::findOrFail(Auth::id());
|
||||||
Flux::toast(text: __('hub::hub.settings.messages.two_factor_upgrading'), variant: 'warning');
|
|
||||||
|
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
|
public function regenerateRecoveryCodes(): void
|
||||||
{
|
{
|
||||||
// TODO: Implement native 2FA - currently disabled
|
$user = User::findOrFail(Auth::id());
|
||||||
Flux::toast(text: __('hub::hub.settings.messages.two_factor_upgrading'), variant: 'warning');
|
|
||||||
|
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
|
public function disableTwoFactor(): void
|
||||||
{
|
{
|
||||||
// TODO: Implement native 2FA - currently disabled
|
$user = User::findOrFail(Auth::id());
|
||||||
Flux::toast(text: __('hub::hub.settings.messages.two_factor_upgrading'), variant: 'warning');
|
|
||||||
|
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
|
public function requestAccountDeletion(): void
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue