mirror of
https://github.com/HDInnovations/UNIT3D-Community-Edition.git
synced 2026-04-30 07:20:25 -05:00
Merge pull request #4063 from Roardom/password-reset-history
(Add) Password reset history logging
This commit is contained in:
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user