diff --git a/app/Console/Commands/NntmuxPurgeDeletedAccounts.php b/app/Console/Commands/NntmuxPurgeDeletedAccounts.php new file mode 100644 index 000000000..d6ab76f2f --- /dev/null +++ b/app/Console/Commands/NntmuxPurgeDeletedAccounts.php @@ -0,0 +1,36 @@ +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; + } +} diff --git a/app/Http/Controllers/Admin/DeletedUsersController.php b/app/Http/Controllers/Admin/DeletedUsersController.php new file mode 100644 index 000000000..6956e77de --- /dev/null +++ b/app/Http/Controllers/Admin/DeletedUsersController.php @@ -0,0 +1,113 @@ +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]; + } +} diff --git a/app/Jobs/PurgeDeletedAccounts.php b/app/Jobs/PurgeDeletedAccounts.php new file mode 100644 index 000000000..e3e102204 --- /dev/null +++ b/app/Jobs/PurgeDeletedAccounts.php @@ -0,0 +1,41 @@ +where('deleted_at', '<', now()->subMonths(6)) + ->get() + ->each(function ($user) { + $user->forceDelete(); // Permanently delete the user + }); + } +} diff --git a/app/Jobs/RemoveInactiveAccounts.php b/app/Jobs/RemoveInactiveAccounts.php index 2e250f1e1..cd27ccabe 100644 --- a/app/Jobs/RemoveInactiveAccounts.php +++ b/app/Jobs/RemoveInactiveAccounts.php @@ -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 + }); } } diff --git a/app/Models/User.php b/app/Models/User.php index a7ef99507..47b546800 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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; diff --git a/database/migrations/2025_06_06_000000_add_soft_deletes_to_users_table.php b/database/migrations/2025_06_06_000000_add_soft_deletes_to_users_table.php new file mode 100644 index 000000000..c9aef33c6 --- /dev/null +++ b/database/migrations/2025_06_06_000000_add_soft_deletes_to_users_table.php @@ -0,0 +1,32 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +} diff --git a/resources/views/themes/admin/adminmenu.tpl b/resources/views/themes/admin/adminmenu.tpl index ccfc29b5c..0440c3907 100644 --- a/resources/views/themes/admin/adminmenu.tpl +++ b/resources/views/themes/admin/adminmenu.tpl @@ -236,6 +236,9 @@ Add User Roles + + Deleted Users + diff --git a/resources/views/themes/admin/deleted_users.tpl b/resources/views/themes/admin/deleted_users.tpl new file mode 100644 index 000000000..7dc564173 --- /dev/null +++ b/resources/views/themes/admin/deleted_users.tpl @@ -0,0 +1,124 @@ +
+
+
+

Deleted Users Management

+
+
+ +
+ +{if isset($smarty.session.success)} +
{$smarty.session.success}
+{/if} +{if isset($smarty.session.error)} +
{$smarty.session.error}
+{/if} + +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ +{if $deletedusers|@count > 0} +
+
+
+
+

Soft-Deleted Users

+
+
+
+
+ + + + + + + + + + + + + + {foreach from=$deletedusers item=user} + + + + + + + + + + {/foreach} + +
Username + + + Email + + + Host + + + Created Date + + + Deleted Date + + + Last Login + + + Options
{$user->username}{$user->email}{$user->host}{$user->created_at|date_format:"%Y-%m-%d %H:%M"}{$user->deleted_at|date_format:"%Y-%m-%d %H:%M"}{if $user->lastlogin != ""}{$user->lastlogin|date_format:"%Y-%m-%d %H:%M"}{else}Never{/if} + + Restore + + + Delete Permanently + +
+
+
+
+
+
+
+ +{if $deletedusers->hasPages()} +
+
+
+ {$deletedusers->links()} +
+
+
+{/if} + +{else} +
No soft-deleted users found.
+{/if} + +
+
diff --git a/resources/views/themes/admin/user-list.tpl b/resources/views/themes/admin/user-list.tpl index a9784f4b6..97f9d507d 100644 --- a/resources/views/themes/admin/user-list.tpl +++ b/resources/views/themes/admin/user-list.tpl @@ -121,8 +121,11 @@ - Status - Stats + +
+ Status +
+ Actions @@ -179,6 +182,22 @@ {/if} + +
+ + {if isset($user->deleted_at)} + Soft-Deleted + {else} + Active + {/if} + +
+ {if isset($user->deleted_at)} + + {$user->deleted_at} + + {/if} +
diff --git a/routes/web.php b/routes/web.php index 3050a6b19..3d514e4ac 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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 () {