Merge pull request #4063 from Roardom/password-reset-history

(Add) Password reset history logging
This commit is contained in:
HDVinnie
2024-08-17 12:02:11 -04:00
committed by GitHub
14 changed files with 363 additions and 10 deletions
@@ -61,5 +61,7 @@ class ResetUserPassword implements ResetsUserPasswords
$user->active = true;
$user->save();
$user->passwordResetHistories()->create();
}
}
@@ -42,5 +42,7 @@ class UpdateUserPassword implements UpdatesUserPasswords
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
$user->passwordResetHistories()->create();
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author Roardom <roardom@protonmail.com>
* @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');
}
}
@@ -19,6 +19,7 @@ namespace App\Http\Controllers\User;
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 @@ class PasswordController extends Controller
],
]);
$user->update([
'password' => Hash::make($request->new_password),
]);
DB::transaction(function () use ($user, $request, $changedByStaff): void {
$user->update([
'password' => Hash::make($request->new_password),
]);
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.",
);
}
$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.",
);
}
});
return to_route('users.password.edit', ['user' => $user])
->withSuccess('Your new password has been saved successfully.');
@@ -70,6 +75,9 @@ class PasswordController extends Controller
{
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(),
]);
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author Roardom <roardom@protonmail.com>
* @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<PasswordResetHistory> $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<PasswordResetHistory>
*/
#[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,
]);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PasswordResetHistory extends Model
{
/**
* Indicates If The Model Should Be Timestamped.
*
* @var bool
*/
public $timestamps = false;
/**
* The attributes that aren't mass assignable.
*
* @var string[]
*/
protected $guarded = ['id'];
/**
* Get the attributes that should be cast.
*
* @return array{created_at: 'datetime'}
*/
protected function casts(): array
{
return [
'created_at' => 'datetime',
];
}
/**
* Has Many Torrents.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<User, $this>
*/
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class);
}
}
+10
View File
@@ -924,6 +924,16 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->hasMany(EmailUpdate::class);
}
/**
* Has many password reset histories.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<PasswordResetHistory, $this>
*/
public function passwordResetHistories(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(PasswordResetHistory::class);
}
/**
* Has many torrent trumps.
*
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author HDVinnie <hdinnovations@protonmail.com>
* @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();
});
}
};
+1
View File
@@ -245,6 +245,7 @@ return [
'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',
@@ -432,6 +432,15 @@
{{ __('user.email-updates') }}
</a>
</p>
<p class="form__group form__group--horizontal">
<a
class="form__button form__button--text"
href="{{ route('staff.password_reset_histories.index') }}"
>
<i class="{{ config('other.font-awesome') }} fa-key"></i>
{{ __('user.password-resets') }}
</a>
</p>
<p class="form__group form__group--horizontal">
<a
class="form__button form__button--text"
@@ -0,0 +1,32 @@
@extends('layout.default')
@section('title')
<title>
{{ __('common.user') }} {{ __('user.password-resets') }} -
{{ __('staff.staff-dashboard') }} - {{ config('other.title') }}
</title>
@endsection
@section('meta')
<meta
name="description"
content="{{ __('user.password-resets') }} - {{ __('staff.staff-dashboard') }}"
/>
@endsection
@section('breadcrumbs')
<li class="breadcrumbV2">
<a href="{{ route('staff.dashboard.index') }}" class="breadcrumb__link">
{{ __('staff.staff-dashboard') }}
</a>
</li>
<li class="breadcrumb--active">
{{ __('user.password-resets') }}
</li>
@endsection
@section('page', 'page__password-reset-history-log--index')
@section('main')
@livewire('password-reset-history-search')
@endsection
@@ -0,0 +1,69 @@
<section class="panelV2">
<header class="panel__header">
<h2 class="panel__heading">{{ __('user.password-resets') }}</h2>
<div class="panel__actions">
<div class="panel__action">
<div class="form__group">
<input
id="username"
class="form__text"
type="text"
wire:model.live="username"
placeholder=" "
/>
<label class="form__label form__label--floating" for="username">
{{ __('common.username') }}
</label>
</div>
</div>
<div class="panel__action">
<div class="form__group">
<select id="quantity" class="form__select" wire:model="perPage" required>
<option>25</option>
<option>50</option>
<option>100</option>
</select>
<label class="form__label form__label--floating" for="quantity">
{{ __('common.quantity') }}
</label>
</div>
</div>
</div>
</header>
<div class="data-table-wrapper">
<table class="data-table">
<tbody>
<tr>
<th wire:click="sortBy('user_id')" role="columnheader button">
{{ __('common.username') }}
@include('livewire.includes._sort-icon', ['field' => 'user_id'])
</th>
<th wire:click="sortBy('created_at')" role="columnheader button">
{{ __('common.created_at') }}
@include('livewire.includes._sort-icon', ['field' => 'created_at'])
</th>
</tr>
@forelse ($passwordResetHistories as $passwordResetHistory)
<tr>
<td>
<x-user_tag :user="$passwordResetHistory->user" :anon="false" />
</td>
<td>
<time
datetime="{{ $passwordResetHistory->created_at }}"
title="{{ $passwordResetHistory->created_at }}"
>
{{ $passwordResetHistory->created_at }}
</time>
</td>
</tr>
@empty
<tr>
<td colspan="4">No Password Resets</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{ $passwordResetHistories->links('partials.pagination') }}
</section>
@@ -73,3 +73,36 @@
</div>
</section>
@endsection
@section('sidebar')
<section class="panelV2">
<h2 class="panel__heading">{{ __('user.password-resets') }}</h2>
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>{{ __('common.created_at') }}</th>
</tr>
</thead>
<tbody>
@forelse ($passwordResetHistories as $passwordResetHistory)
<tr>
<td>
<time
datetime="{{ $passwordResetHistory->created_at }}"
title="{{ $passwordResetHistory->created_at }}"
>
{{ $passwordResetHistory->created_at }}
</time>
</td>
</tr>
@empty
<tr>
<td>No password reset history</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</section>
@endsection
+7
View File
@@ -974,6 +974,13 @@ Route::middleware('language')->group(function (): void {
});
});
// 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 {