Chore: Upgrade to Laravel 12 + remove Mary and Jetstream dependencies (#141)
* docs: remove requirement for setting APP_KEY manually * optimize date picker * clean up modals * spot light working * reorganization * add lazy load * wip * remove filament * styling
This commit is contained in:
@@ -1,3 +1,76 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
/**
|
||||
* Indicates if user deletion is being confirmed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $confirmingUserDeletion = false;
|
||||
|
||||
/**
|
||||
* The user's current password.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $password = '';
|
||||
|
||||
/**
|
||||
* Confirm that the user would like to delete their account.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function confirmUserDeletion()
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
|
||||
$this->password = '';
|
||||
|
||||
$this->dispatch('confirming-delete-user');
|
||||
|
||||
$this->confirmingUserDeletion = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current user.
|
||||
*
|
||||
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function deleteUser(Request $request, StatefulGuard $auth)
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
|
||||
if (! Hash::check($this->password, Auth::user()->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => [__('This password does not match our records.')],
|
||||
]);
|
||||
}
|
||||
|
||||
$user = Auth::user()->fresh();
|
||||
|
||||
$user->deleteProfilePhoto();
|
||||
$user->tokens->each->delete();
|
||||
$user->delete();
|
||||
|
||||
$auth->logout();
|
||||
|
||||
if ($request->hasSession()) {
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
}
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<x-forms.action-section>
|
||||
<x-slot name="title">
|
||||
{{ __('Delete Account') }}
|
||||
@@ -13,13 +86,13 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<x-button class="btn-error text-white" wire:click="confirmUserDeletion" wire:loading.attr="disabled">
|
||||
<x-ui.button class="btn-error text-white" wire:click="confirmUserDeletion" wire:loading.attr="disabled">
|
||||
{{ __('Delete Account') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
</div>
|
||||
|
||||
<!-- Delete User Confirmation Modal -->
|
||||
<x-dialog-modal wire:model.live="confirmingUserDeletion">
|
||||
{{-- Delete User Confirmation Modal --}}
|
||||
<x-ui.dialog-modal wire:model.live="confirmingUserDeletion">
|
||||
<x-slot name="title">
|
||||
{{ __('Delete Account') }}
|
||||
</x-slot>
|
||||
@@ -28,7 +101,7 @@
|
||||
{{ __('Are you sure you want to delete your account? Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
|
||||
|
||||
<div class="mt-4" x-data="{}" x-on:confirming-delete-user.window="setTimeout(() => $refs.password.focus(), 250)">
|
||||
<x-input type="password" class="mt-1 block w-3/4"
|
||||
<x-ui.input type="password" class="mt-1 block w-3/4"
|
||||
autocomplete="current-password"
|
||||
placeholder="{{ __('Password') }}"
|
||||
x-ref="password"
|
||||
@@ -39,14 +112,14 @@
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-button class="btn-outline" wire:click="$toggle('confirmingUserDeletion')" wire:loading.attr="disabled">
|
||||
<x-ui.button class="btn-outline" wire:click="$toggle('confirmingUserDeletion')" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
|
||||
<x-button class="ms-3 btn-error text-white" wire:click="deleteUser" wire:loading.attr="disabled">
|
||||
<x-ui.button class="ms-3 btn-error text-white" wire:click="deleteUser" wire:loading.attr="disabled">
|
||||
{{ __('Delete Account') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
</x-slot>
|
||||
</x-dialog-modal>
|
||||
</x-ui.dialog-modal>
|
||||
</x-slot>
|
||||
</x-forms.action-section>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
use Mary\Traits\Toast;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Exports\BackupExport;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
||||
new class extends Component {
|
||||
use Toast;
|
||||
|
||||
// props
|
||||
|
||||
// methods
|
||||
public function export()
|
||||
{
|
||||
if (!RateLimiter::attempt('export:'.auth()->user()->id, $perMinute = 3, fn()=>null)) {
|
||||
|
||||
$this->error(__('Hang on! You\'re doing that too much.'));
|
||||
return;
|
||||
}
|
||||
|
||||
return Excel::download(new BackupExport, now()->format('Y_m_d') . '_investbrain_backup.xlsx');
|
||||
}
|
||||
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<x-button type="submit" @click="$wire.export" spinner="export">
|
||||
{{ __('Download Export') }}
|
||||
</x-button>
|
||||
</div>
|
||||
@@ -1,149 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Exports\BackupExport;
|
||||
use App\Models\BackupImport as BackupImportModel;
|
||||
use Livewire\Attributes\Rule;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Mary\Traits\Toast;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
use Toast;
|
||||
use WithFileUploads;
|
||||
|
||||
// props
|
||||
#[Rule('required|extensions:xlsx|mimes:xlsx|max:2048')]
|
||||
public $file;
|
||||
|
||||
public bool $importStatusDialog = false;
|
||||
|
||||
public ?BackupImportModel $backupImport = null;
|
||||
|
||||
public int $percent = 10;
|
||||
|
||||
// methods
|
||||
public function import()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
if (! RateLimiter::attempt('import:'.auth()->user()->id, $perMinute = 3, fn () => null)) {
|
||||
|
||||
$this->error(__('Hang on! You\'re doing that too much.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->backupImport = BackupImportModel::create([
|
||||
'user_id' => auth()->user()->id,
|
||||
'path' => $this->file->getPathname(),
|
||||
]);
|
||||
|
||||
$this->importStatusDialog = true;
|
||||
|
||||
}
|
||||
|
||||
public function checkImportStatus()
|
||||
{
|
||||
if (Str::contains($this->backupImport?->message, 'portfolios')) {
|
||||
|
||||
$this->percent = (1 / 2) * 100;
|
||||
}
|
||||
|
||||
if (Str::contains($this->backupImport?->message, 'transactions')) {
|
||||
|
||||
$this->percent = (3 / 4) * 100;
|
||||
}
|
||||
|
||||
if (Str::contains($this->backupImport?->message, 'daily changes')) {
|
||||
|
||||
$this->percent = (7 / 8) * 100;
|
||||
}
|
||||
|
||||
if ($this->backupImport?->status == 'failed') {
|
||||
|
||||
unset($this->file);
|
||||
$this->percent = 100;
|
||||
}
|
||||
|
||||
if ($this->backupImport?->status == 'success') {
|
||||
|
||||
$this->importStatusDialog = false;
|
||||
$this->backupImport = null;
|
||||
|
||||
$this->success(__('Successfully imported!'), redirectTo: route('dashboard'));
|
||||
}
|
||||
}
|
||||
|
||||
public function downloadTemplate()
|
||||
{
|
||||
return Excel::download(new BackupExport(empty: true), now()->format('Y_m_d').'_investbrain_template.xlsx');
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<x-forms.form-section submit="import">
|
||||
<x-slot name="title">
|
||||
{{ __('Import') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="description">
|
||||
{{ __('Upload or recover your Investbrain portfolio and holdings.') }}
|
||||
<span class="text-xs text-secondary"><a href="#" title="{{ __('Click to download import template.') }}" @click="$wire.downloadTemplate()"> {{ __('Download import template.') }}</a></span>
|
||||
</x-slot>
|
||||
|
||||
<x-slot:form>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-file wire:model="file" label="{{ __('Select a file') }}" hint="" accept=".xlsx" required />
|
||||
</div>
|
||||
|
||||
<x-dialog-modal wire:model.live="importStatusDialog" persistent>
|
||||
<x-slot name="title">
|
||||
|
||||
@if($backupImport?->status)
|
||||
<div
|
||||
class="{{ $backupImport?->status == 'failed' ? 'text-error' : '' }}"
|
||||
>
|
||||
{{ $backupImport?->message }}
|
||||
</div>
|
||||
@endif
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
@if($backupImport?->status != 'failed')
|
||||
<x-progress
|
||||
:indeterminate="$backupImport?->status == 'pending'"
|
||||
class="progress-primary h-3"
|
||||
value="{{ $percent }}"
|
||||
max="100"
|
||||
/>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
@if($backupImport?->status == 'failed')
|
||||
|
||||
<x-button wire:click="$toggle('importStatusDialog')"> {{ __('Try again') }} </x-button>
|
||||
@else
|
||||
<div wire:poll="checkImportStatus" class="text-gray-400 text-sm">{{ __('Your import will continue in the background') }}</div>
|
||||
<x-ib-flex-spacer />
|
||||
<x-button wire:click="$toggle('importStatusDialog')"> {{ __('Close') }} </x-button>
|
||||
@endif
|
||||
</x-slot>
|
||||
</x-dialog-modal>
|
||||
|
||||
</x-slot:form>
|
||||
|
||||
<x-slot name="actions">
|
||||
|
||||
<x-forms.action-message class="me-3" on="saved">
|
||||
{{ __('Saved.') }}
|
||||
</x-forms.action-message>
|
||||
|
||||
<x-button type="submit" wire:loading.attr="disabled" spinner="import">
|
||||
{{ __('Import') }}
|
||||
</x-button>
|
||||
</x-slot>
|
||||
</x-forms.form-section>
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ new class extends Component
|
||||
// $this->js('window.location.reload();');
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<x-forms.form-section submit="updateProfileInformation">
|
||||
<x-slot name="title">
|
||||
{{ __('Locale Options') }}
|
||||
@@ -66,7 +67,7 @@ new class extends Component
|
||||
<x-slot name="form">
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-select
|
||||
<x-ui.select
|
||||
label="{{ __('Locale') }}"
|
||||
class="select block mt-1 w-full"
|
||||
:options="config('app.available_locales')"
|
||||
@@ -75,12 +76,13 @@ new class extends Component
|
||||
placeholder="Choose a locale"
|
||||
wire:model="locale"
|
||||
id="locale"
|
||||
required
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-select
|
||||
<x-ui.select
|
||||
label="{{ __('Display Currency') }}"
|
||||
class="select block mt-1 w-full"
|
||||
:options="$currencies"
|
||||
@@ -89,6 +91,7 @@ new class extends Component
|
||||
placeholder="Choose a display currency"
|
||||
wire:model="display_currency"
|
||||
id="display_currency"
|
||||
required
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -100,8 +103,8 @@ new class extends Component
|
||||
{{ __('Saved.') }}
|
||||
</x-forms.action-message>
|
||||
|
||||
<x-button type="submit">
|
||||
<x-ui.button type="submit">
|
||||
{{ __('Save') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
</x-slot>
|
||||
</x-forms.form-section>
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<x-forms.action-section>
|
||||
<x-slot name="title">
|
||||
{{ __('Browser Sessions') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="description">
|
||||
{{ __('Manage and log out your active sessions on other browsers and devices.') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="max-w-xl text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __('If necessary, you may log out of all of your other browser sessions across all of your devices. Some of your recent sessions are listed below; however, this list may not be exhaustive. If you feel your account has been compromised, you should also update your password.') }}
|
||||
</div>
|
||||
|
||||
@if (count($this->sessions) > 0)
|
||||
<div class="mt-5 space-y-6">
|
||||
<!-- Other Browser Sessions -->
|
||||
@foreach ($this->sessions as $session)
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
@if ($session->agent->isDesktop())
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 text-gray-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
|
||||
</svg>
|
||||
@else
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 text-gray-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="ms-3">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ $session->agent->platform() ? $session->agent->platform() : __('Unknown') }} - {{ $session->agent->browser() ? $session->agent->browser() : __('Unknown') }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ $session->ip_address }},
|
||||
|
||||
@if ($session->is_current_device)
|
||||
<span class="text-green-500 font-semibold">{{ __('This device') }}</span>
|
||||
@else
|
||||
{{ __('Last active') }} {{ $session->last_active }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center mt-5">
|
||||
<x-button type="submit" wire:click="confirmLogout" wire:loading.attr="disabled">
|
||||
{{ __('Log Out Other Browser Sessions') }}
|
||||
</x-button>
|
||||
|
||||
<x-forms.action-message class="ms-3" on="loggedOut">
|
||||
{{ __('Done.') }}
|
||||
</x-forms.action-message>
|
||||
</div>
|
||||
|
||||
<!-- Log Out Other Devices Confirmation Modal -->
|
||||
<x-dialog-modal wire:model.live="confirmingLogout">
|
||||
<x-slot name="title">
|
||||
{{ __('Log Out Other Browser Sessions') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ __('Please enter your password to confirm you would like to log out of your other browser sessions across all of your devices.') }}
|
||||
|
||||
<div class="mt-4" x-data="{}" x-on:confirming-logout-other-browser-sessions.window="setTimeout(() => $refs.password.focus(), 250)">
|
||||
<x-input type="password" class="mt-1 block w-3/4"
|
||||
autocomplete="current-password"
|
||||
placeholder="{{ __('Password') }}"
|
||||
x-ref="password"
|
||||
wire:model="password"
|
||||
wire:keydown.enter="logoutOtherBrowserSessions" />
|
||||
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-button class="btn-outline" wire:click="$toggle('confirmingLogout')" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-button>
|
||||
|
||||
<x-button type="submit" class="ms-3"
|
||||
wire:click="logoutOtherBrowserSessions"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Log Out Other Browser Sessions') }}
|
||||
</x-button>
|
||||
</x-slot>
|
||||
</x-dialog-modal>
|
||||
</x-slot>
|
||||
</x-forms.action-section>
|
||||
@@ -1,46 +1,39 @@
|
||||
<x-app-layout>
|
||||
<x-layouts.app>
|
||||
|
||||
<div>
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
@if (Laravel\Fortify\Features::canUpdateProfileInformation())
|
||||
@livewire('profile.update-profile-information-form')
|
||||
|
||||
<x-section-border hide-on-mobile />
|
||||
@endif
|
||||
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('localization-form')
|
||||
</div>
|
||||
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
||||
@if (Laravel\Fortify\Features::canUpdateProfileInformation())
|
||||
@livewire('update-profile-information-form')
|
||||
|
||||
<x-section-border hide-on-mobile />
|
||||
|
||||
@if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::updatePasswords()))
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.update-password-form')
|
||||
</div>
|
||||
|
||||
<x-section-border hide-on-mobile />
|
||||
@endif
|
||||
|
||||
@if (Laravel\Fortify\Features::canManageTwoFactorAuthentication())
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.two-factor-authentication-form')
|
||||
</div>
|
||||
|
||||
<x-section-border hide-on-mobile />
|
||||
@endif
|
||||
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.logout-other-browser-sessions-form')
|
||||
</div>
|
||||
|
||||
@if (Laravel\Jetstream\Jetstream::hasAccountDeletionFeatures())
|
||||
<x-section-border hide-on-mobile />
|
||||
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.delete-user-form')
|
||||
</div>
|
||||
@endif
|
||||
<x-ui.section-border hide-on-mobile />
|
||||
@endif
|
||||
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('localization-form')
|
||||
</div>
|
||||
|
||||
<x-ui.section-border hide-on-mobile />
|
||||
|
||||
@if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::updatePasswords()))
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('update-password-form')
|
||||
</div>
|
||||
|
||||
<x-ui.section-border hide-on-mobile />
|
||||
@endif
|
||||
|
||||
@if (Laravel\Fortify\Features::canManageTwoFactorAuthentication())
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('two-factor-authentication-form')
|
||||
</div>
|
||||
|
||||
<x-ui.section-border hide-on-mobile />
|
||||
@endif
|
||||
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('delete-user-form')
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
</x-layouts.app>
|
||||
|
||||
@@ -1,3 +1,168 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
|
||||
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
|
||||
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
|
||||
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
|
||||
use Livewire\Volt\Component;
|
||||
use Laravel\Fortify\Features;
|
||||
use App\Traits\ConfirmsPasswords;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
use ConfirmsPasswords;
|
||||
|
||||
/**
|
||||
* Indicates if two factor authentication QR code is being displayed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $showingQrCode = false;
|
||||
|
||||
/**
|
||||
* Indicates if the two factor authentication confirmation input and button are being displayed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $showingConfirmation = false;
|
||||
|
||||
/**
|
||||
* Indicates if two factor authentication recovery codes are being displayed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $showingRecoveryCodes = false;
|
||||
|
||||
/**
|
||||
* The OTP code for confirming two factor authentication.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $code;
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function mount()
|
||||
{
|
||||
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm') &&
|
||||
is_null(Auth::user()->two_factor_confirmed_at)) {
|
||||
app(DisableTwoFactorAuthentication::class)(Auth::user());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable two factor authentication for the user.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $enable)
|
||||
{
|
||||
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
|
||||
$this->ensurePasswordIsConfirmed();
|
||||
}
|
||||
|
||||
$enable(Auth::user());
|
||||
|
||||
$this->showingQrCode = true;
|
||||
|
||||
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm')) {
|
||||
$this->showingConfirmation = true;
|
||||
} else {
|
||||
$this->showingRecoveryCodes = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm two factor authentication for the user.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function confirmTwoFactorAuthentication(ConfirmTwoFactorAuthentication $confirm)
|
||||
{
|
||||
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
|
||||
$this->ensurePasswordIsConfirmed();
|
||||
}
|
||||
|
||||
$confirm(Auth::user(), $this->code);
|
||||
|
||||
$this->showingQrCode = false;
|
||||
$this->showingConfirmation = false;
|
||||
$this->showingRecoveryCodes = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the user's recovery codes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function showRecoveryCodes()
|
||||
{
|
||||
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
|
||||
$this->ensurePasswordIsConfirmed();
|
||||
}
|
||||
|
||||
$this->showingRecoveryCodes = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate new recovery codes for the user.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generate)
|
||||
{
|
||||
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
|
||||
$this->ensurePasswordIsConfirmed();
|
||||
}
|
||||
|
||||
$generate(Auth::user());
|
||||
|
||||
$this->showingRecoveryCodes = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable two factor authentication for the user.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disableTwoFactorAuthentication(DisableTwoFactorAuthentication $disable)
|
||||
{
|
||||
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
|
||||
$this->ensurePasswordIsConfirmed();
|
||||
}
|
||||
|
||||
$disable(Auth::user());
|
||||
|
||||
$this->showingQrCode = false;
|
||||
$this->showingConfirmation = false;
|
||||
$this->showingRecoveryCodes = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user of the application.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUserProperty()
|
||||
{
|
||||
return Auth::user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if two factor authentication is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getEnabledProperty()
|
||||
{
|
||||
return ! empty($this->user->two_factor_secret);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<x-forms.action-section>
|
||||
<x-slot name="title">
|
||||
{{ __('Two Factor Authentication') }}
|
||||
@@ -8,7 +173,7 @@
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
<h3 class="text-lg font-medium">
|
||||
@if ($this->enabled)
|
||||
@if ($showingConfirmation)
|
||||
{{ __('Finish enabling two factor authentication.') }}
|
||||
@@ -20,7 +185,7 @@
|
||||
@endif
|
||||
</h3>
|
||||
|
||||
<div class="mt-3 max-w-xl text-sm text-gray-600 dark:text-gray-400">
|
||||
<div class="mt-3 max-w-xl text-sm">
|
||||
<p>
|
||||
{{ __('When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone\'s Google Authenticator application.') }}
|
||||
</p>
|
||||
@@ -52,7 +217,7 @@
|
||||
<div class="mt-4">
|
||||
|
||||
|
||||
<x-input id="code" label="{{ __('Code') }}" type="text" name="code" class="block mt-1 w-1/2" inputmode="numeric" autofocus autocomplete="one-time-code"
|
||||
<x-ui.input id="code" label="{{ __('Code') }}" type="text" name="code" class="block mt-1 w-1/2" inputmode="numeric" autofocus autocomplete="one-time-code"
|
||||
wire:model="code"
|
||||
wire:keydown.enter="confirmTwoFactorAuthentication" />
|
||||
|
||||
@@ -67,7 +232,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 dark:bg-gray-900 dark:text-gray-100 rounded-lg">
|
||||
<div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-base-100 dark:text-gray-100 rounded-lg">
|
||||
@foreach (json_decode(decrypt($this->user->two_factor_recovery_codes), true) as $code)
|
||||
<div>{{ $code }}</div>
|
||||
@endforeach
|
||||
@@ -78,42 +243,42 @@
|
||||
<div class="mt-5">
|
||||
@if (! $this->enabled)
|
||||
<x-forms.confirms-password wire:then="enableTwoFactorAuthentication">
|
||||
<x-button type="button" wire:loading.attr="disabled">
|
||||
<x-ui.button type="button" wire:loading.attr="disabled">
|
||||
{{ __('Enable') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
</x-forms.confirms-password>
|
||||
@else
|
||||
@if ($showingRecoveryCodes)
|
||||
<x-forms.confirms-password wire:then="regenerateRecoveryCodes">
|
||||
<x-button class="btn-outline" class="me-3">
|
||||
<x-ui.button class="btn-outline" class="me-3">
|
||||
{{ __('Regenerate Recovery Codes') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
</x-forms.confirms-password>
|
||||
@elseif ($showingConfirmation)
|
||||
<x-forms.confirms-password wire:then="confirmTwoFactorAuthentication">
|
||||
<x-button type="button" class="me-3" wire:loading.attr="disabled">
|
||||
<x-ui.button type="button" class="me-3" wire:loading.attr="disabled">
|
||||
{{ __('Confirm') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
</x-forms.confirms-password>
|
||||
@else
|
||||
<x-forms.confirms-password wire:then="showRecoveryCodes">
|
||||
<x-button class="btn-outline" class="me-3">
|
||||
<x-ui.button class="btn-outline" class="me-3">
|
||||
{{ __('Show Recovery Codes') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
</x-forms.confirms-password>
|
||||
@endif
|
||||
|
||||
@if ($showingConfirmation)
|
||||
<x-forms.confirms-password wire:then="disableTwoFactorAuthentication">
|
||||
<x-button class="btn-outline" wire:loading.attr="disabled">
|
||||
<x-ui.button class="btn-outline" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
</x-forms.confirms-password>
|
||||
@else
|
||||
<x-forms.confirms-password wire:then="disableTwoFactorAuthentication">
|
||||
<x-button class="btn-error text-white" wire:loading.attr="disabled">
|
||||
<x-ui.button class="btn-error text-white" wire:loading.attr="disabled">
|
||||
{{ __('Disable') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
</x-forms.confirms-password>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -1,3 +1,59 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
/**
|
||||
* The component's state.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $state = [
|
||||
'current_password' => '',
|
||||
'password' => '',
|
||||
'password_confirmation' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updatePassword(UpdatesUserPasswords $updater)
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
|
||||
$updater->update(Auth::user(), $this->state);
|
||||
|
||||
if (request()->hasSession()) {
|
||||
request()->session()->put([
|
||||
'password_hash_'.Auth::getDefaultDriver() => Auth::user()->getAuthPassword(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->state = [
|
||||
'current_password' => '',
|
||||
'password' => '',
|
||||
'password_confirmation' => '',
|
||||
];
|
||||
|
||||
$this->dispatch('saved');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user of the application.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUserProperty()
|
||||
{
|
||||
return Auth::user();
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<x-forms.form-section submit="updatePassword">
|
||||
<x-slot name="title">
|
||||
{{ __('Update Password') }}
|
||||
@@ -11,19 +67,19 @@
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
|
||||
<x-input id="current_password" label="{{ __('Current Password') }}" type="password" class="mt-1 block w-full" wire:model="state.current_password" error-field="current_password" autocomplete="current-password" />
|
||||
<x-ui.input id="current_password" label="{{ __('Current Password') }}" type="password" class="mt-1 block w-full" wire:model="state.current_password" error-field="current_password" autocomplete="current-password" />
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
|
||||
<x-input id="password" label="{{ __('New Password') }}" type="password" class="mt-1 block w-full" wire:model="state.password" error-field="password" autocomplete="new-password" />
|
||||
<x-ui.input id="password" label="{{ __('New Password') }}" type="password" class="mt-1 block w-full" wire:model="state.password" error-field="password" autocomplete="new-password" />
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
|
||||
<x-input id="password_confirmation" label="{{ __('Confirm Password') }}" type="password" class="mt-1 block w-full" wire:model="state.password_confirmation" error-field="password_confirmation" autocomplete="new-password" />
|
||||
<x-ui.input id="password_confirmation" label="{{ __('Confirm Password') }}" type="password" class="mt-1 block w-full" wire:model="state.password_confirmation" error-field="password_confirmation" autocomplete="new-password" />
|
||||
|
||||
</div>
|
||||
</x-slot>
|
||||
@@ -33,8 +89,8 @@
|
||||
{{ __('Saved.') }}
|
||||
</x-forms.action-message>
|
||||
|
||||
<x-button type="submit">
|
||||
<x-ui.button type="submit">
|
||||
{{ __('Save') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
</x-slot>
|
||||
</x-forms.form-section>
|
||||
|
||||
@@ -1,3 +1,109 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
/**
|
||||
* The component's state.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $state = [];
|
||||
|
||||
/**
|
||||
* The new avatar for the user.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $photo;
|
||||
|
||||
/**
|
||||
* Determine if the verification email was sent.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $verificationLinkSent = false;
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function mount()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$this->state = array_merge([
|
||||
'email' => $user->email,
|
||||
], $user->withoutRelations()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse|null
|
||||
*/
|
||||
public function updateProfileInformation(UpdatesUserProfileInformation $updater)
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
|
||||
$updater->update(
|
||||
Auth::user(),
|
||||
$this->photo
|
||||
? array_merge($this->state, ['photo' => $this->photo])
|
||||
: $this->state
|
||||
);
|
||||
|
||||
if (isset($this->photo)) {
|
||||
return redirect()->route('profile.show');
|
||||
}
|
||||
|
||||
$this->dispatch('saved');
|
||||
|
||||
$this->dispatch('refresh-navigation-menu');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user's profile photo.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteProfilePhoto()
|
||||
{
|
||||
Auth::user()->deleteProfilePhoto();
|
||||
|
||||
$this->dispatch('refresh-navigation-menu');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sent the email verification.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function sendEmailVerification()
|
||||
{
|
||||
Auth::user()->sendEmailVerificationNotification();
|
||||
|
||||
$this->verificationLinkSent = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user of the application.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUserProperty()
|
||||
{
|
||||
return Auth::user();
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<x-forms.form-section submit="updateProfileInformation">
|
||||
<x-slot name="title">
|
||||
{{ __('Profile Information') }}
|
||||
@@ -9,63 +115,61 @@
|
||||
|
||||
<x-slot name="form">
|
||||
|
||||
<!-- Profile Photo -->
|
||||
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
|
||||
<div x-data="{photoName: null, photoPreview: null}" class="col-span-6 sm:col-span-4">
|
||||
{{-- Profile Photo --}}
|
||||
<div x-data="{photoName: null, photoPreview: null}" class="col-span-6 sm:col-span-4">
|
||||
|
||||
<!-- Profile Photo File Input -->
|
||||
<input type="file" id="photo" class="hidden"
|
||||
wire:model.live="photo"
|
||||
x-ref="photo"
|
||||
x-on:change="
|
||||
photoName = $refs.photo.files[0].name;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
photoPreview = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL($refs.photo.files[0]);
|
||||
" />
|
||||
{{-- Profile Photo File Input --}}
|
||||
<input type="file" id="photo" class="hidden"
|
||||
wire:model.live="photo"
|
||||
x-ref="photo"
|
||||
x-on:change="
|
||||
photoName = $refs.photo.files[0].name;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
photoPreview = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL($refs.photo.files[0]);
|
||||
" />
|
||||
|
||||
<label for="photo" class="pt-0 label label-text font-semibold">
|
||||
<span>{{ __('Photo') }} </span>
|
||||
</label>
|
||||
<label for="photo" class="pt-0 label label-text font-semibold">
|
||||
<span>{{ __('Photo') }} </span>
|
||||
</label>
|
||||
|
||||
<!-- Current Profile Photo -->
|
||||
<div class="mt-2" x-show="! photoPreview">
|
||||
<img src="{{ $this->user->profile_photo_url }}" alt="{{ $this->user->name }}" class="rounded-full h-20 w-20 object-cover">
|
||||
</div>
|
||||
|
||||
<!-- New Profile Photo Preview -->
|
||||
<div class="mt-2" x-show="photoPreview" style="display: none;">
|
||||
<span class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
|
||||
x-bind:style="'background-image: url(\'' + photoPreview + '\');'">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<x-button class="btn-outline" class="mt-2 me-2" type="button" x-on:click.prevent="$refs.photo.click()">
|
||||
{{ __('Select A New Photo') }}
|
||||
</x-button>
|
||||
|
||||
@if ($this->user->profile_photo_path)
|
||||
<x-button class="btn-outline" type="button" class="mt-2" wire:click="deleteProfilePhoto">
|
||||
{{ __('Remove Photo') }}
|
||||
</x-button>
|
||||
@endif
|
||||
|
||||
@if ($errors->has('photo') && is_array($errors->get('photo')))
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{{ $errors->get('photo')[0] }}</p>
|
||||
@endif
|
||||
{{-- Current Profile Photo --}}
|
||||
<div class="mt-2" x-show="! photoPreview">
|
||||
<img src="{{ $this->user->profile_photo_url }}" alt="{{ $this->user->name }}" class="rounded-full h-20 w-20 object-cover">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Name -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-input id="name" label="{{ __('Name') }}" type="text" class="mt-1 block w-full" wire:model="state.name" error-field="name" required autocomplete="name" />
|
||||
{{-- New Profile Photo Preview --}}
|
||||
<div class="mt-2" x-show="photoPreview" style="display: none;">
|
||||
<span class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
|
||||
x-bind:style="'background-image: url(\'' + photoPreview + '\');'">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<x-ui.button class="btn-outline" class="mt-2 me-2" type="button" x-on:click.prevent="$refs.photo.click()">
|
||||
{{ __('Select A New Photo') }}
|
||||
</x-ui.button>
|
||||
|
||||
@if ($this->user->profile_photo_path)
|
||||
<x-ui.button class="btn-outline" type="button" class="mt-2" wire:click="deleteProfilePhoto">
|
||||
{{ __('Remove Photo') }}
|
||||
</x-ui.button>
|
||||
@endif
|
||||
|
||||
@if ($errors->has('photo') && is_array($errors->get('photo')))
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{{ $errors->get('photo')[0] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
{{-- Name --}}
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-input id="email" label="{{ __('Email') }}" type="email" class="mt-1 block w-full" wire:model="state.email" error-field="email" required autocomplete="username" />
|
||||
<x-ui.input id="name" label="{{ __('Name') }}" type="text" class="mt-1 block w-full" wire:model="state.name" error-field="name" required autocomplete="name" />
|
||||
</div>
|
||||
|
||||
{{-- Email --}}
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-ui.input id="email" label="{{ __('Email') }}" type="email" class="mt-1 block w-full" wire:model="state.email" error-field="email" required autocomplete="username" />
|
||||
|
||||
@if (
|
||||
! config('investbrain.self_hosted')
|
||||
@@ -94,8 +198,8 @@
|
||||
{{ __('Saved.') }}
|
||||
</x-forms.action-message>
|
||||
|
||||
<x-button type="submit" spinner="photo" wire:loading.attr="disabled" wire:target="photo">
|
||||
<x-ui.button type="submit" spinner="photo" wire:loading.attr="disabled" wire:target="photo">
|
||||
{{ __('Save') }}
|
||||
</x-button>
|
||||
</x-ui.button>
|
||||
</x-slot>
|
||||
</x-forms.form-section>
|
||||
|
||||
Reference in New Issue
Block a user