mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Usde soft deletes so users can be restored
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\PurgeDeletedAccounts;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class NntmuxPurgeDeletedAccounts extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'nntmux:purge-deleted-accounts';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Permanently remove accounts that were soft deleted 6 months ago';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Starting to purge accounts that were soft deleted 6 months ago...');
|
||||
|
||||
PurgeDeletedAccounts::dispatch();
|
||||
|
||||
$this->info('Job dispatched to purge deleted accounts');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\BasePageController;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
||||
class DeletedUsersController extends BasePageController
|
||||
{
|
||||
/**
|
||||
* Display a listing of soft-deleted users.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->setAdminPrefs();
|
||||
|
||||
$username = $request->has('username') ? $request->input('username') : '';
|
||||
$email = $request->has('email') ? $request->input('email') : '';
|
||||
$host = $request->has('host') ? $request->input('host') : '';
|
||||
$orderBy = $request->has('ob') && ! empty($request->input('ob')) ? $request->input('ob') : 'deleted_at_desc';
|
||||
|
||||
$deletedUsers = User::onlyTrashed()
|
||||
->when($username !== '', function ($query) use ($username) {
|
||||
return $query->where('username', 'like', '%' . $username . '%');
|
||||
})
|
||||
->when($email !== '', function ($query) use ($email) {
|
||||
return $query->where('email', 'like', '%' . $email . '%');
|
||||
})
|
||||
->when($host !== '', function ($query) use ($host) {
|
||||
return $query->where('host', 'like', '%' . $host . '%');
|
||||
});
|
||||
|
||||
// Determine sort order
|
||||
[$orderField, $orderSort] = $this->getSortOrder($orderBy);
|
||||
$deletedUsers = $deletedUsers->orderBy($orderField, $orderSort)->paginate(25);
|
||||
|
||||
$this->smarty->assign([
|
||||
'deletedusers' => $deletedUsers,
|
||||
'username' => $username,
|
||||
'email' => $email,
|
||||
'host' => $host,
|
||||
'orderby' => $orderBy,
|
||||
]);
|
||||
|
||||
$meta_title = 'Deleted Users';
|
||||
$meta_keywords = 'view,deleted,users,softdeleted';
|
||||
$meta_description = 'View and restore soft-deleted user accounts';
|
||||
|
||||
$content = $this->smarty->fetch('deleted_users.tpl');
|
||||
$this->smarty->assign(compact('content', 'meta_title', 'meta_keywords', 'meta_description'));
|
||||
|
||||
$this->adminrender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a soft-deleted user.
|
||||
*/
|
||||
public function restore($id)
|
||||
{
|
||||
$user = User::onlyTrashed()->find($id);
|
||||
|
||||
if ($user) {
|
||||
$user->restore();
|
||||
return redirect()->route('admin.deleted.users.index')
|
||||
->with('success', "User '{$user->username}' has been restored successfully.");
|
||||
}
|
||||
|
||||
return redirect()->route('admin.deleted.users.index')
|
||||
->with('error', 'User not found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete a soft-deleted user.
|
||||
*/
|
||||
public function permanentDelete($id)
|
||||
{
|
||||
$user = User::onlyTrashed()->find($id);
|
||||
|
||||
if ($user) {
|
||||
$username = $user->username;
|
||||
$user->forceDelete();
|
||||
return redirect()->route('admin.deleted.users.index')
|
||||
->with('success', "User '{$username}' has been permanently deleted.");
|
||||
}
|
||||
|
||||
return redirect()->route('admin.deleted.users.index')
|
||||
->with('error', 'User not found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse sort order from the orderBy parameter.
|
||||
*/
|
||||
private function getSortOrder($orderBy): array
|
||||
{
|
||||
$orderArr = explode('_', $orderBy);
|
||||
$orderField = match ($orderArr[0]) {
|
||||
'email' => 'email',
|
||||
'host' => 'host',
|
||||
'createdat' => 'created_at',
|
||||
'deletedat' => 'deleted_at',
|
||||
'lastlogin' => 'lastlogin',
|
||||
'apiaccess' => 'apiaccess',
|
||||
'grabs' => 'grabs',
|
||||
'role' => 'roles_id',
|
||||
default => 'username',
|
||||
};
|
||||
$orderSort = (isset($orderArr[1]) && preg_match('/^asc|desc$/i', $orderArr[1])) ? $orderArr[1] : 'desc';
|
||||
|
||||
return [$orderField, $orderSort];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class PurgeDeletedAccounts implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* Find users that were soft-deleted 6 months ago and permanently delete them.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
// Find users that were soft-deleted 6 months ago
|
||||
User::onlyTrashed()
|
||||
->where('deleted_at', '<', now()->subMonths(6))
|
||||
->get()
|
||||
->each(function ($user) {
|
||||
$user->forceDelete(); // Permanently delete the user
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ class RemoveInactiveAccounts implements ShouldQueue
|
||||
$query->where('apiaccess', '<', now()->subDays($purgeDays))
|
||||
->orWhereNull('apiaccess');
|
||||
})
|
||||
->delete();
|
||||
->get()->each(function ($user) {
|
||||
$user->delete(); // Use soft delete instead of mass deletion
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ use Junaidnasir\Larainvite\Facades\Invite;
|
||||
use Junaidnasir\Larainvite\InviteTrait;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* App\Models\User.
|
||||
@@ -147,7 +148,7 @@ use Spatie\Permission\Traits\HasRoles;
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasRoles, InviteTrait, Notifiable, UserVerification;
|
||||
use HasRoles, InviteTrait, Notifiable, UserVerification, SoftDeletes;
|
||||
|
||||
public const ERR_SIGNUP_BADUNAME = -1;
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddSoftDeletesToUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropSoftDeletes();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -236,6 +236,9 @@
|
||||
<a href="{{url("/admin/role-add")}}" class="list-group-item list-group-item-action bg-dark text-white">
|
||||
<span class="menu-collapsed">Add User Roles</span>
|
||||
</a>
|
||||
<a href="{{url("/admin/deleted-users")}}" class="list-group-item list-group-item-action bg-dark text-white">
|
||||
<span class="menu-collapsed">Deleted Users</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Tmux -->
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Deleted Users Management</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
{if isset($smarty.session.success)}
|
||||
<div class="alert alert-success">{$smarty.session.success}</div>
|
||||
{/if}
|
||||
{if isset($smarty.session.error)}
|
||||
<div class="alert alert-error">{$smarty.session.error}</div>
|
||||
{/if}
|
||||
|
||||
<form name="deletedusersearch" method="get" action="{{url("/admin/deleted-users")}}" id="deleted-user-search-form" class="mb-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label for="username">Username:</label>
|
||||
<input id="username" type="text" name="username" value="{$username}" class="form-control" placeholder="Username">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="email">Email:</label>
|
||||
<input id="email" type="text" name="email" value="{$email}" class="form-control" placeholder="Email">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="host">Host:</label>
|
||||
<input id="host" type="text" name="host" value="{$host}" class="form-control" placeholder="Host">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<input type="submit" value="Search" class="btn btn-success">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{if $deletedusers|@count > 0}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Soft-Deleted Users</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="data table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=username_asc"><i class="fas fa-sort-alpha-up"></i></a>
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=username_desc"><i class="fas fa-sort-alpha-down"></i></a>
|
||||
</th>
|
||||
<th>Email
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=email_asc"><i class="fas fa-sort-alpha-up"></i></a>
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=email_desc"><i class="fas fa-sort-alpha-down"></i></a>
|
||||
</th>
|
||||
<th>Host
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=host_asc"><i class="fas fa-sort-alpha-up"></i></a>
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=host_desc"><i class="fas fa-sort-alpha-down"></i></a>
|
||||
</th>
|
||||
<th>Created Date
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=createdat_asc"><i class="fas fa-sort-alpha-up"></i></a>
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=createdat_desc"><i class="fas fa-sort-alpha-down"></i></a>
|
||||
</th>
|
||||
<th>Deleted Date
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=deletedat_asc"><i class="fas fa-sort-alpha-up"></i></a>
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=deletedat_desc"><i class="fas fa-sort-alpha-down"></i></a>
|
||||
</th>
|
||||
<th>Last Login
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=lastlogin_asc"><i class="fas fa-sort-alpha-up"></i></a>
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users?ob=lastlogin_desc"><i class="fas fa-sort-alpha-down"></i></a>
|
||||
</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach from=$deletedusers item=user}
|
||||
<tr>
|
||||
<td>{$user->username}</td>
|
||||
<td>{$user->email}</td>
|
||||
<td>{$user->host}</td>
|
||||
<td>{$user->created_at|date_format:"%Y-%m-%d %H:%M"}</td>
|
||||
<td>{$user->deleted_at|date_format:"%Y-%m-%d %H:%M"}</td>
|
||||
<td>{if $user->lastlogin != ""}{$user->lastlogin|date_format:"%Y-%m-%d %H:%M"}{else}Never{/if}</td>
|
||||
<td>
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users/restore/{$user->id}" class="btn btn-success btn-sm me-2" title="Restore User">
|
||||
<i class="fas fa-user-check"></i> Restore
|
||||
</a>
|
||||
<a href="{$smarty.const.WWW_TOP}/admin/deleted-users/permanent-delete/{$user->id}" class="btn btn-danger btn-sm" title="Permanently Delete User" onclick="return confirm('Are you sure you want to permanently delete this user? This action cannot be undone.');">
|
||||
<i class="fas fa-trash-alt"></i> Delete Permanently
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{if $deletedusers->hasPages()}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="d-flex justify-content-center">
|
||||
{$deletedusers->links()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{else}
|
||||
<div class="alert alert-info">No soft-deleted users found.</div>
|
||||
{/if}
|
||||
|
||||
</div> <!-- end of card-body -->
|
||||
</div> <!-- end of card -->
|
||||
@@ -121,8 +121,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-center">Status</th>
|
||||
<th class="text-center">Stats</th>
|
||||
<th>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span>Status</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -179,6 +182,22 @@
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="badge {if isset($user->deleted_at)}bg-danger{else}bg-success{/if} rounded-pill">
|
||||
{if isset($user->deleted_at)}
|
||||
<i class="fa fa-trash me-1"></i>Soft-Deleted
|
||||
{else}
|
||||
<i class="fa fa-check me-1"></i>Active
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{if isset($user->deleted_at)}
|
||||
<small class="d-block text-muted mt-1" title="Deletion date">
|
||||
<i class="fa fa-calendar me-1"></i>{$user->deleted_at}
|
||||
</small>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div class="d-flex justify-content-between">
|
||||
|
||||
@@ -36,6 +36,7 @@ use App\Http\Controllers\Admin\AdminShowsController;
|
||||
use App\Http\Controllers\Admin\AdminSiteController;
|
||||
use App\Http\Controllers\Admin\AdminTmuxController;
|
||||
use App\Http\Controllers\Admin\AdminUserController;
|
||||
use App\Http\Controllers\Admin\DeletedUsersController;
|
||||
use App\Http\Controllers\AdultController;
|
||||
use App\Http\Controllers\AjaxController;
|
||||
use App\Http\Controllers\AnimeController;
|
||||
@@ -214,6 +215,10 @@ Route::middleware('role:Admin', '2fa')->prefix('admin')->group(function () {
|
||||
Route::match(['GET', 'POST'], 'group-list-active', [AdminGroupController::class, 'active'])->name('admin.group-list-active');
|
||||
Route::match(['GET', 'POST'], 'group-list-inactive', [AdminGroupController::class, 'inactive'])->name('admin.group-list-inactive');
|
||||
|
||||
// Deleted Users Management Routes
|
||||
Route::match(['GET', 'POST'], 'deleted-users', [DeletedUsersController::class, 'index'])->name('admin.deleted.users.index');
|
||||
Route::match(['GET', 'POST'], 'deleted-users/restore/{id}', [DeletedUsersController::class, 'restore'])->name('admin.deleted.users.restore');
|
||||
Route::match(['GET', 'POST'], 'deleted-users/permanent-delete/{id}', [DeletedUsersController::class, 'permanentDelete'])->name('admin.deleted.users.permanent-delete');
|
||||
});
|
||||
|
||||
Route::middleware('role_or_permission:Admin|Moderator|edit release')->prefix('admin')->group(function () {
|
||||
|
||||
Reference in New Issue
Block a user