mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 01:01:24 +00:00
Update bulk user actions
This commit is contained in:
@@ -13,6 +13,7 @@ use App\Models\UserDownload;
|
||||
use App\Models\UserRequest;
|
||||
use App\Services\AdminDashboardSnapshotService;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -317,7 +318,7 @@ class AdminUserController extends BasePageController
|
||||
public function bulkAction(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'action' => ['required', 'string', 'in:delete,verify'],
|
||||
'action' => ['required', 'string', 'in:delete,verify,resend_verification'],
|
||||
'user_ids' => ['required', 'array', 'min:1'],
|
||||
'user_ids.*' => ['integer', 'exists:users,id'],
|
||||
]);
|
||||
@@ -325,36 +326,57 @@ class AdminUserController extends BasePageController
|
||||
$userIds = array_values(array_unique(array_map('intval', $validated['user_ids'])));
|
||||
|
||||
if ($validated['action'] === 'delete') {
|
||||
$count = User::query()
|
||||
->whereIn('id', $userIds)
|
||||
->where('roles_id', '!=', UserRole::ADMIN->value)
|
||||
->whereDoesntHave('role', fn ($query) => $query->where('name', UserRole::ADMIN->label()))
|
||||
->whereDoesntHave('roles', fn ($query) => $query->where('name', UserRole::ADMIN->label()))
|
||||
->delete();
|
||||
$count = $this->bulkActionUsersQuery($userIds)->delete();
|
||||
|
||||
Cache::forget(AdminDashboardSnapshotService::CACHE_KEY);
|
||||
|
||||
return redirect()->back()->with('success', $count.' user(s) soft-deleted successfully.');
|
||||
}
|
||||
|
||||
if ($validated['action'] === 'verify') {
|
||||
$count = 0;
|
||||
$this->bulkActionUsersQuery($userIds)
|
||||
->where('verified', false)
|
||||
->whereNull('email_verified_at')
|
||||
->get()
|
||||
->each(function (User $user) use (&$count): void {
|
||||
if ($user->markEmailAsVerified()) {
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
|
||||
return redirect()->back()->with('success', $count.' user(s) marked as verified successfully.');
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
User::query()
|
||||
->whereIn('id', $userIds)
|
||||
->where('roles_id', '!=', UserRole::ADMIN->value)
|
||||
->whereDoesntHave('role', fn ($query) => $query->where('name', UserRole::ADMIN->label()))
|
||||
->whereDoesntHave('roles', fn ($query) => $query->where('name', UserRole::ADMIN->label()))
|
||||
->where(function ($query): void {
|
||||
$query->where('verified', false)
|
||||
->orWhereNull('email_verified_at');
|
||||
})
|
||||
$this->bulkActionUsersQuery($userIds)
|
||||
->where('verified', false)
|
||||
->whereNull('email_verified_at')
|
||||
->get()
|
||||
->each(function (User $user) use (&$count): void {
|
||||
if ($user->markEmailAsVerified()) {
|
||||
$count++;
|
||||
}
|
||||
$user->sendEmailVerificationNotification();
|
||||
$count++;
|
||||
});
|
||||
|
||||
return redirect()->back()->with('success', $count.' user(s) marked as verified successfully.');
|
||||
return redirect()->back()->with('success', $count.' verification email(s) queued successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $userIds
|
||||
* @return Builder<User>
|
||||
*/
|
||||
private function bulkActionUsersQuery(array $userIds): Builder
|
||||
{
|
||||
$adminRoleIds = Role::query()
|
||||
->where('name', UserRole::ADMIN->label())
|
||||
->pluck('id')
|
||||
->all();
|
||||
|
||||
return User::query()
|
||||
->whereIn('id', $userIds)
|
||||
->when($adminRoleIds !== [], fn (Builder $query): Builder => $query->whereNotIn('roles_id', $adminRoleIds))
|
||||
->whereDoesntHave('role', fn (Builder $query): Builder => $query->where('name', UserRole::ADMIN->label()))
|
||||
->whereDoesntHave('roles', fn (Builder $query): Builder => $query->where('name', UserRole::ADMIN->label()));
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
|
||||
@@ -257,6 +257,10 @@ Alpine.data('adminUserList', () => ({
|
||||
return this._checkboxes().filter(cb => cb.checked);
|
||||
},
|
||||
|
||||
_checkedUnverifiedBoxes() {
|
||||
return this._checkedBoxes().filter(cb => cb.dataset.isVerified === '0');
|
||||
},
|
||||
|
||||
_setValidationError(message) {
|
||||
const errEl = document.getElementById('validationError');
|
||||
const errMsg = document.getElementById('validationErrorMessage');
|
||||
@@ -295,17 +299,61 @@ Alpine.data('adminUserList', () => ({
|
||||
|
||||
const form = this.$el.querySelector('#bulkUserActionForm');
|
||||
const action = this.$el.querySelector('#bulkUserAction')?.value;
|
||||
const checkedCount = this._checkedBoxes().length;
|
||||
const checked = this._checkedBoxes();
|
||||
const checkedCount = checked.length;
|
||||
|
||||
if (!action) { this._setValidationError('Please select an action.'); return; }
|
||||
if (checkedCount === 0) { this._setValidationError('Please select at least one non-admin active user.'); return; }
|
||||
|
||||
const isDelete = action === 'delete';
|
||||
const actions = {
|
||||
delete: {
|
||||
title: 'Soft Delete Users',
|
||||
actionText: 'soft-delete',
|
||||
type: 'danger',
|
||||
confirmText: 'Soft Delete'
|
||||
},
|
||||
verify: {
|
||||
title: 'Verify Users',
|
||||
actionText: 'mark as verified',
|
||||
type: 'success',
|
||||
confirmText: 'Verify'
|
||||
},
|
||||
resend_verification: {
|
||||
title: 'Resend Verification Emails',
|
||||
actionText: 'send account verification emails to',
|
||||
type: 'warning',
|
||||
confirmText: 'Send Emails'
|
||||
}
|
||||
};
|
||||
|
||||
const config = actions[action];
|
||||
if (!config) { this._setValidationError('Please select a valid action.'); return; }
|
||||
|
||||
let targetCount = checkedCount;
|
||||
let message = 'Are you sure you want to ' + config.actionText + ' ' + targetCount + ' user(s)?';
|
||||
|
||||
if (action === 'resend_verification') {
|
||||
const unverifiedCount = this._checkedUnverifiedBoxes().length;
|
||||
const skippedCount = checkedCount - unverifiedCount;
|
||||
|
||||
if (unverifiedCount === 0) {
|
||||
this._setValidationError('Please select at least one not verified user to resend verification email.');
|
||||
return;
|
||||
}
|
||||
|
||||
targetCount = unverifiedCount;
|
||||
message = 'Are you sure you want to send account verification emails to ' + targetCount + ' not verified user(s)?';
|
||||
|
||||
if (skippedCount > 0) {
|
||||
message += ' ' + skippedCount + ' already verified user(s) will be skipped.';
|
||||
}
|
||||
}
|
||||
|
||||
showConfirm({
|
||||
title: isDelete ? 'Soft Delete Users' : 'Verify Users',
|
||||
message: 'Are you sure you want to ' + (isDelete ? 'soft-delete' : 'mark as verified') + ' ' + checkedCount + ' user(s)?',
|
||||
type: isDelete ? 'danger' : 'success',
|
||||
confirmText: isDelete ? 'Soft Delete' : 'Verify',
|
||||
title: config.title,
|
||||
message: message,
|
||||
type: config.type,
|
||||
confirmText: config.confirmText,
|
||||
onConfirm: function() { if (form) form.submit(); }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
<select name="action" id="bulkUserAction" class="w-full md:w-auto px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Select Action</option>
|
||||
<option value="verify">Mark Selected Verified</option>
|
||||
<option value="resend_verification">Resend Verification Email</option>
|
||||
<option value="delete">Soft Delete Selected</option>
|
||||
</select>
|
||||
<button type="submit"
|
||||
@@ -243,11 +244,12 @@
|
||||
$isAdminUser = (int) $user->roles_id === \App\Enums\UserRole::ADMIN->value
|
||||
|| ($user->rolename ?? null) === \App\Enums\UserRole::ADMIN->label()
|
||||
|| $user->roles->contains('name', \App\Enums\UserRole::ADMIN->label());
|
||||
$isVerified = $user->hasVerifiedEmail();
|
||||
@endphp
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if(!$user->deleted_at && !$isAdminUser)
|
||||
<input type="checkbox" name="user_ids[]" value="{{ $user->id }}" form="bulkUserActionForm" class="user-list-checkbox h-4 w-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-600 rounded">
|
||||
<input type="checkbox" name="user_ids[]" value="{{ $user->id }}" form="bulkUserActionForm" data-is-verified="{{ $isVerified ? '1' : '0' }}" class="user-list-checkbox h-4 w-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-600 rounded">
|
||||
@elseif($isAdminUser)
|
||||
<input type="checkbox" disabled class="h-4 w-4 text-gray-300 dark:text-gray-600 border-gray-300 dark:border-gray-600 rounded cursor-not-allowed" title="Admin users cannot be selected for bulk actions">
|
||||
@else
|
||||
@@ -331,7 +333,6 @@
|
||||
N/A
|
||||
@endif
|
||||
</td>
|
||||
@php($isVerified = $user->hasVerifiedEmail())
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if($isVerified)
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
|
||||
@@ -393,11 +394,16 @@
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</button>
|
||||
</form>
|
||||
<a href="{{ url('admin/resendverification?id=' . $user->id) }}"
|
||||
class="text-yellow-600 dark:text-yellow-400 hover:text-yellow-900 dark:hover:text-yellow-300"
|
||||
title="Resend Verification">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ route('admin.resend-verification') }}" class="inline">
|
||||
@csrf
|
||||
<input type="hidden" name="id" value="{{ $user->id }}">
|
||||
<button type="submit"
|
||||
class="text-yellow-600 dark:text-yellow-400 hover:text-yellow-900 dark:hover:text-yellow-300 border-0 bg-transparent cursor-pointer p-0"
|
||||
title="Resend Verification"
|
||||
data-confirm="Resend account verification email to '{{ $user->username }}'?">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
<form action="{{ url('admin/user-delete') }}" method="POST" class="inline-form">
|
||||
@csrf
|
||||
|
||||
@@ -6,8 +6,10 @@ namespace Tests\Feature\Admin;
|
||||
|
||||
use App\Http\Middleware\Google2FAMiddleware;
|
||||
use App\Models\User;
|
||||
use App\Notifications\VerifyEmailBranded;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\Permission\Models\Role;
|
||||
@@ -146,6 +148,31 @@ class AdminUserControllerTest extends TestCase
|
||||
$this->assertNull($unverifiedUser->verification_token);
|
||||
}
|
||||
|
||||
public function test_admin_can_bulk_resend_verification_email_to_not_verified_users(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$admin = $this->createUserWithRole('Admin', false);
|
||||
$unverifiedUser = $this->createUserWithRole('User', true);
|
||||
$alreadyVerifiedUser = $this->createUserWithRole('User', true);
|
||||
|
||||
$unverifiedUser->forceFill([
|
||||
'verified' => false,
|
||||
'email_verified_at' => null,
|
||||
])->save();
|
||||
|
||||
$response = $this->actingAs($admin)->post(route('admin.user-list.bulk'), [
|
||||
'action' => 'resend_verification',
|
||||
'user_ids' => [$unverifiedUser->id, $alreadyVerifiedUser->id],
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', '1 verification email(s) queued successfully.');
|
||||
|
||||
Notification::assertSentTo($unverifiedUser, VerifyEmailBranded::class);
|
||||
Notification::assertNotSentTo($alreadyVerifiedUser, VerifyEmailBranded::class);
|
||||
}
|
||||
|
||||
public function test_bulk_user_action_rejects_role_updates(): void
|
||||
{
|
||||
$admin = $this->createUserWithRole('Admin', false);
|
||||
|
||||
Reference in New Issue
Block a user