From 5f28892153c0d02a50cc728e71ac3211c11c02f2 Mon Sep 17 00:00:00 2001 From: Roardom Date: Sat, 17 Aug 2024 14:41:20 +0000 Subject: [PATCH] add: password reset history logging --- app/Actions/Fortify/ResetUserPassword.php | 2 + app/Actions/Fortify/UpdateUserPassword.php | 2 + .../Staff/PasswordResetHistoryController.php | 30 ++++++++ .../Controllers/User/PasswordController.php | 28 +++++--- .../Livewire/PasswordResetHistorySearch.php | 69 +++++++++++++++++++ app/Models/PasswordResetHistory.php | 46 +++++++++++++ app/Models/User.php | 10 +++ ...7_140412_create_password_reset_history.php | 35 ++++++++++ lang/en/user.php | 1 + .../views/Staff/dashboard/index.blade.php | 9 +++ .../password-reset-history/index.blade.php | 32 +++++++++ .../password-reset-history-search.blade.php | 69 +++++++++++++++++++ resources/views/user/password/edit.blade.php | 33 +++++++++ routes/web.php | 7 ++ 14 files changed, 363 insertions(+), 10 deletions(-) create mode 100644 app/Http/Controllers/Staff/PasswordResetHistoryController.php create mode 100644 app/Http/Livewire/PasswordResetHistorySearch.php create mode 100644 app/Models/PasswordResetHistory.php create mode 100644 database/migrations/2024_08_17_140412_create_password_reset_history.php create mode 100644 resources/views/Staff/password-reset-history/index.blade.php create mode 100644 resources/views/livewire/password-reset-history-search.blade.php diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php index fc47985cdd..58308b9a9f 100644 --- a/app/Actions/Fortify/ResetUserPassword.php +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -61,5 +61,7 @@ public function reset(User $user, array $input): void $user->active = true; $user->save(); + + $user->passwordResetHistories()->create(); } } diff --git a/app/Actions/Fortify/UpdateUserPassword.php b/app/Actions/Fortify/UpdateUserPassword.php index d6eb7a24b9..e913a4209b 100644 --- a/app/Actions/Fortify/UpdateUserPassword.php +++ b/app/Actions/Fortify/UpdateUserPassword.php @@ -42,5 +42,7 @@ public function update(User $user, array $input): void $user->forceFill([ 'password' => Hash::make($input['password']), ])->save(); + + $user->passwordResetHistories()->create(); } } diff --git a/app/Http/Controllers/Staff/PasswordResetHistoryController.php b/app/Http/Controllers/Staff/PasswordResetHistoryController.php new file mode 100644 index 0000000000..3022d15ec4 --- /dev/null +++ b/app/Http/Controllers/Staff/PasswordResetHistoryController.php @@ -0,0 +1,30 @@ + + * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 + */ + +namespace App\Http\Controllers\Staff; + +use App\Http\Controllers\Controller; + +class PasswordResetHistoryController extends Controller +{ + /** + * Display all user password reset histories. + */ + public function index(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View + { + return view('Staff.password-reset-history.index'); + } +} diff --git a/app/Http/Controllers/User/PasswordController.php b/app/Http/Controllers/User/PasswordController.php index 11c8359f4e..9af9544496 100644 --- a/app/Http/Controllers/User/PasswordController.php +++ b/app/Http/Controllers/User/PasswordController.php @@ -19,6 +19,7 @@ use App\Http\Controllers\Controller; use App\Models\User; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\Password; @@ -48,16 +49,20 @@ protected function update(Request $request, User $user): \Illuminate\Http\Redire ], ]); - $user->update([ - 'password' => Hash::make($request->new_password), - ]); + DB::transaction(function () use ($user, $request, $changedByStaff): void { + $user->update([ + 'password' => Hash::make($request->new_password), + ]); + + $user->passwordResetHistories()->create(); - if ($changedByStaff) { - $user->sendSystemNotification( - subject: 'ATTENTION - Your password has been changed', - message: "Your password has been changed by staff. You will need to update your password manager with the new password.\n\nFor more information, please create a helpdesk ticket.", - ); - } + if ($changedByStaff) { + $user->sendSystemNotification( + subject: 'ATTENTION - Your password has been changed', + message: "Your password has been changed by staff. You will need to update your password manager with the new password.\n\nFor more information, please create a helpdesk ticket.", + ); + } + }); return to_route('users.password.edit', ['user' => $user]) ->withSuccess('Your new password has been saved successfully.'); @@ -70,6 +75,9 @@ public function edit(Request $request, User $user): \Illuminate\Contracts\View\F { abort_unless($request->user()->is($user) || $request->user()->group->is_modo, 403); - return view('user.password.edit', ['user' => $user]); + return view('user.password.edit', [ + 'user' => $user, + 'passwordResetHistories' => $user->passwordResetHistories()->latest()->get(), + ]); } } diff --git a/app/Http/Livewire/PasswordResetHistorySearch.php b/app/Http/Livewire/PasswordResetHistorySearch.php new file mode 100644 index 0000000000..400883907a --- /dev/null +++ b/app/Http/Livewire/PasswordResetHistorySearch.php @@ -0,0 +1,69 @@ + + * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 + */ + +namespace App\Http\Livewire; + +use App\Models\PasswordResetHistory; +use App\Models\User; +use App\Traits\LivewireSort; +use Livewire\Attributes\Computed; +use Livewire\Attributes\Url; +use Livewire\Component; +use Livewire\WithPagination; + +/** + * @property \Illuminate\Contracts\Pagination\LengthAwarePaginator $passwordResetHistories + */ +class PasswordResetHistorySearch extends Component +{ + use LivewireSort; + use WithPagination; + + #TODO: Update URL attributes once Livewire 3 fixes upstream bug. See: https://github.com/livewire/livewire/discussions/7746 + + #[Url(history: true)] + public string $username = ''; + + #[Url(history: true)] + public string $sortField = 'created_at'; + + #[Url(history: true)] + public string $sortDirection = 'desc'; + + #[Url(history: true)] + public int $perPage = 25; + + /** + * @return \Illuminate\Pagination\LengthAwarePaginator + */ + #[Computed] + final public function passwordResetHistories(): \Illuminate\Pagination\LengthAwarePaginator + { + return PasswordResetHistory::with([ + 'user' => fn ($query) => $query->withTrashed()->with('group'), + ]) + ->when($this->username, fn ($query) => $query->whereIn('user_id', User::withTrashed()->select('id')->where('username', 'LIKE', '%'.$this->username.'%'))) + ->orderBy($this->sortField, $this->sortDirection) + ->paginate($this->perPage); + } + + final public function render(): \Illuminate\Contracts\View\View|\Illuminate\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\Foundation\Application + { + return view('livewire.password-reset-history-search', [ + 'passwordResetHistories' => $this->passwordResetHistories, + ]); + } +} diff --git a/app/Models/PasswordResetHistory.php b/app/Models/PasswordResetHistory.php new file mode 100644 index 0000000000..f13e1aa7ac --- /dev/null +++ b/app/Models/PasswordResetHistory.php @@ -0,0 +1,46 @@ + 'datetime', + ]; + } + + /** + * Has Many Torrents. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 0604835332..e2abf44130 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -924,6 +924,16 @@ public function emailUpdates(): \Illuminate\Database\Eloquent\Relations\HasMany return $this->hasMany(EmailUpdate::class); } + /** + * Has many password reset histories. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function passwordResetHistories(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(PasswordResetHistory::class); + } + /** * Has many torrent trumps. * diff --git a/database/migrations/2024_08_17_140412_create_password_reset_history.php b/database/migrations/2024_08_17_140412_create_password_reset_history.php new file mode 100644 index 0000000000..5b94fe08b6 --- /dev/null +++ b/database/migrations/2024_08_17_140412_create_password_reset_history.php @@ -0,0 +1,35 @@ + + * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 + */ + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class () extends Migration { + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('password_reset_histories', function (Blueprint $table): void { + $table->increments('id'); + $table->unsignedInteger('user_id'); + $table->timestamp('created_at')->nullable()->useCurrent(); + + $table->foreign('user_id')->references('id')->on('users')->cascadeOnUpdate(); + }); + } +}; diff --git a/lang/en/user.php b/lang/en/user.php index 7a7391b5cf..8939f45c0f 100644 --- a/lang/en/user.php +++ b/lang/en/user.php @@ -245,6 +245,7 @@ 'other-privacy-online' => 'Allow users to see you in the online users block', 'passkey' => 'Passkey', 'passkey-warning' => 'PID is like your password, you must keep it safe!', + 'password-resets' => 'Password Resets', 'pending-achievements' => 'Pending Achievements', 'per-torrent' => 'Per Torrent', 'posts' => 'Posts', diff --git a/resources/views/Staff/dashboard/index.blade.php b/resources/views/Staff/dashboard/index.blade.php index f3af711540..88545245df 100644 --- a/resources/views/Staff/dashboard/index.blade.php +++ b/resources/views/Staff/dashboard/index.blade.php @@ -432,6 +432,15 @@ class="form__button form__button--text" {{ __('user.email-updates') }}

+

+ + + {{ __('user.password-resets') }} + +

+ {{ __('common.user') }} {{ __('user.password-resets') }} - + {{ __('staff.staff-dashboard') }} - {{ config('other.title') }} + +@endsection + +@section('meta') + +@endsection + +@section('breadcrumbs') +

+ +@endsection + +@section('page', 'page__password-reset-history-log--index') + +@section('main') + @livewire('password-reset-history-search') +@endsection diff --git a/resources/views/livewire/password-reset-history-search.blade.php b/resources/views/livewire/password-reset-history-search.blade.php new file mode 100644 index 0000000000..7e0093f06b --- /dev/null +++ b/resources/views/livewire/password-reset-history-search.blade.php @@ -0,0 +1,69 @@ +
+
+

{{ __('user.password-resets') }}

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + + + + + + @forelse ($passwordResetHistories as $passwordResetHistory) + + + + + @empty + + + + @endforelse + +
+ {{ __('common.username') }} + @include('livewire.includes._sort-icon', ['field' => 'user_id']) + + {{ __('common.created_at') }} + @include('livewire.includes._sort-icon', ['field' => 'created_at']) +
+ + + +
No Password Resets
+
+ {{ $passwordResetHistories->links('partials.pagination') }} +
diff --git a/resources/views/user/password/edit.blade.php b/resources/views/user/password/edit.blade.php index 22949ba9ed..fe1e6383bf 100644 --- a/resources/views/user/password/edit.blade.php +++ b/resources/views/user/password/edit.blade.php @@ -73,3 +73,36 @@ class="form__text" @endsection + +@section('sidebar') +
+

{{ __('user.password-resets') }}

+
+ + + + + + + + @forelse ($passwordResetHistories as $passwordResetHistory) + + + + @empty + + + + @endforelse + +
{{ __('common.created_at') }}
+ +
No password reset history
+
+
+@endsection diff --git a/routes/web.php b/routes/web.php index f45327cb00..27d144fa50 100644 --- a/routes/web.php +++ b/routes/web.php @@ -974,6 +974,13 @@ }); }); + // Password Reset Histories + Route::prefix('password-reset-histories')->group(function (): void { + Route::name('password_reset_histories.')->group(function (): void { + Route::get('/', [App\Http\Controllers\Staff\PasswordResetHistoryController::class, 'index'])->name('index'); + }); + }); + // Peers Route::prefix('peers')->group(function (): void { Route::name('peers.')->group(function (): void {