Skip to content

Commit

Permalink
add: password reset history logging
Browse files Browse the repository at this point in the history
  • Loading branch information
Roardom committed Aug 17, 2024
1 parent 498c4ab commit b5537fa
Show file tree
Hide file tree
Showing 14 changed files with 356 additions and 10 deletions.
2 changes: 2 additions & 0 deletions app/Actions/Fortify/ResetUserPassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,7 @@ public function reset(User $user, array $input): void

$user->active = true;
$user->save();

$user->passwordResetHistories()->create();
}
}
2 changes: 2 additions & 0 deletions app/Actions/Fortify/UpdateUserPassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,7 @@ public function update(User $user, array $input): void
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();

$user->passwordResetHistories()->create();
}
}
30 changes: 30 additions & 0 deletions app/Http/Controllers/Staff/PasswordResetHistoryController.php
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
* @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');
}
}
29 changes: 19 additions & 10 deletions app/Http/Controllers/User/PasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,16 +49,21 @@ 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.');
Expand All @@ -70,6 +76,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(),
]);
}
}
66 changes: 66 additions & 0 deletions app/Http/Livewire/PasswordResetHistorySearch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
/**
* 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 <[email protected]>
* @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,
]);
}
}
44 changes: 44 additions & 0 deletions app/Models/PasswordResetHistory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

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, self>
*/
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class);

Check failure on line 42 in app/Models/PasswordResetHistory.php

View workflow job for this annotation

GitHub Actions / php 8.3 on ubuntu-22.04

Method App\Models\PasswordResetHistory::user() should return Illuminate\Database\Eloquent\Relations\BelongsTo<App\Models\User, App\Models\PasswordResetHistory> but returns Illuminate\Database\Eloquent\Relations\BelongsTo<App\Models\User, $this(App\Models\PasswordResetHistory)>.
}
}
10 changes: 10 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<PasswordResetHistory, $this>
*/
public function passwordResetHistories(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(PasswordResetHistory::class);
}

/**
* Has many torrent trumps.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
/**
* 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 <[email protected]>
* @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 change: 1 addition & 0 deletions lang/en/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
9 changes: 9 additions & 0 deletions resources/views/Staff/dashboard/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,15 @@ class="form__button form__button--text"
{{ __('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"
Expand Down
32 changes: 32 additions & 0 deletions resources/views/Staff/password-reset-history/index.blade.php
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions resources/views/livewire/password-reset-history-search.blade.php
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit b5537fa

Please sign in to comment.