Fix: rollenlose aktive Nutzer -> Backfill-Command + Import-Nachlauf
Problem: Ein eingeloggter Nutzer ohne admin/editor/customer-Rolle wird per redirectUsersTo auf die Startseite geleitet und haengt dort fest. Ursache sind Alt-Konten, deren Rollen-Zuweisung nie lief (frueher Import bzw. ein nach dem User-Insert fehlgeschlagener Rollen-Sync). - Neues Command users:backfill-portal-role (--role, --dry-run): weist aktiven, komplett rollenlosen Nutzern die customer-Rolle zu. api-only-Konten haben bereits eine Rolle und bleiben unangetastet; inaktive werden uebersprungen. - legacy:import ruft den Backfill nach dem User-Schritt automatisch auf (self-heal, da updateOrCreate + Rollen-Sync nicht atomar sind). Analyse-Ergebnis nebenbei: In der aktuellen DB war nur genau 1 echter Fall (bereits gefixt); die zuvor genannten ~167 waren fast alle api-only (gewollt ohne Portal-Zugriff). Command laeuft daher jetzt sauber auf 0. Test: BackfillPortalRoleTest (customer-Zuweisung, Rollen-Erhalt bei customer/api-only, Ueberspringen Inaktiver, dry-run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9fadfe6458
commit
1503d3a7ed
3 changed files with 118 additions and 0 deletions
62
app/Console/Commands/BackfillPortalRole.php
Normal file
62
app/Console/Commands/BackfillPortalRole.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\UserRolePermissionSyncService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Weist aktiven, komplett rollenlosen Nutzern eine Portal-Rolle zu.
|
||||
*
|
||||
* Hintergrund: Guest-/Auth-Redirects lenken einen eingeloggten Nutzer ohne
|
||||
* admin/editor/customer-Rolle auf die Startseite (bootstrap/app.php →
|
||||
* redirectUsersTo). Legacy-Importe oder ein fehlgeschlagener Rollen-Sync nach
|
||||
* dem User-Insert können solche „hängenden" Konten erzeugen. Dieser Nachlauf
|
||||
* heilt sie. api-only-Konten besitzen bereits eine Rolle und werden bewusst
|
||||
* NICHT angefasst (kein Portal-Zugriff gewollt).
|
||||
*/
|
||||
class BackfillPortalRole extends Command
|
||||
{
|
||||
protected $signature = 'users:backfill-portal-role
|
||||
{--role=customer : Zuzuweisende Rolle}
|
||||
{--dry-run : Nur zählen, nichts schreiben}';
|
||||
|
||||
protected $description = 'Weist aktiven, rollenlosen Nutzern eine Portal-Rolle (Standard: customer) zu.';
|
||||
|
||||
public function handle(UserRolePermissionSyncService $roleSync): int
|
||||
{
|
||||
$role = (string) $this->option('role');
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
|
||||
$ids = User::query()
|
||||
->where('is_active', true)
|
||||
->whereDoesntHave('roles')
|
||||
->pluck('id');
|
||||
|
||||
if ($ids->isEmpty()) {
|
||||
$this->info('Keine aktiven rollenlosen Nutzer gefunden – nichts zu tun.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn("[DRY-RUN] {$ids->count()} aktive rollenlose Nutzer würden die Rolle '{$role}' erhalten.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$assigned = 0;
|
||||
|
||||
foreach ($ids->chunk(500) as $chunk) {
|
||||
User::query()->whereIn('id', $chunk)->get()->each(function (User $user) use ($roleSync, $role, &$assigned): void {
|
||||
$roleSync->assignRoleAndSyncPermissions($user, $role);
|
||||
$assigned++;
|
||||
});
|
||||
}
|
||||
|
||||
$this->info("{$assigned} aktiven rollenlosen Nutzern die Rolle '{$role}' zugewiesen.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -120,6 +120,16 @@ class ImportLegacyData extends Command
|
|||
}
|
||||
}
|
||||
|
||||
// Selbstheilung: Ein User-Insert (updateOrCreate) und die anschließende
|
||||
// Rollen-Zuweisung sind nicht atomar – schlägt der Rollen-Sync nach dem
|
||||
// Insert fehl, bliebe ein aktives, rollenloses Konto zurück (hängt beim
|
||||
// Login auf der Startseite). Nach einem User-Import daher absichern.
|
||||
if (! $isDryRun && in_array('users', $steps, true)) {
|
||||
$this->newLine();
|
||||
$this->info('▶ Nachlauf: aktive rollenlose Nutzer absichern');
|
||||
$this->call('users:backfill-portal-role');
|
||||
}
|
||||
|
||||
$total = round(microtime(true) - $totalStart, 1);
|
||||
$this->newLine();
|
||||
$this->info("Import abgeschlossen in {$total}s.");
|
||||
|
|
|
|||
46
tests/Feature/BackfillPortalRoleTest.php
Normal file
46
tests/Feature/BackfillPortalRoleTest.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Tests\TestCase;
|
||||
|
||||
beforeEach(function (): void {
|
||||
/** @var TestCase $this */
|
||||
$this->seed(RolesAndPermissionsSeeder::class);
|
||||
});
|
||||
|
||||
test('assigns customer role to active roleless users', function () {
|
||||
$stuck = User::factory()->create(['is_active' => true]);
|
||||
|
||||
$this->artisan('users:backfill-portal-role')->assertSuccessful();
|
||||
|
||||
expect($stuck->fresh()->hasRole('customer'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('leaves users that already have a role untouched', function () {
|
||||
$customer = User::factory()->create(['is_active' => true]);
|
||||
$customer->assignRole('customer');
|
||||
$apiOnly = User::factory()->create(['is_active' => true]);
|
||||
$apiOnly->assignRole('api-only');
|
||||
|
||||
$this->artisan('users:backfill-portal-role')->assertSuccessful();
|
||||
|
||||
expect($customer->fresh()->getRoleNames()->all())->toBe(['customer'])
|
||||
->and($apiOnly->fresh()->getRoleNames()->all())->toBe(['api-only']);
|
||||
});
|
||||
|
||||
test('skips inactive roleless users', function () {
|
||||
$inactive = User::factory()->create(['is_active' => false]);
|
||||
|
||||
$this->artisan('users:backfill-portal-role')->assertSuccessful();
|
||||
|
||||
expect($inactive->fresh()->roles()->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('dry-run assigns nothing', function () {
|
||||
$stuck = User::factory()->create(['is_active' => true]);
|
||||
|
||||
$this->artisan('users:backfill-portal-role', ['--dry-run' => true])->assertSuccessful();
|
||||
|
||||
expect($stuck->fresh()->roles()->exists())->toBeFalse();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue