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 @@ + + + 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 @@ +
| 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 + + | +