presseportale/app/Console/Commands/ResyncAutomaticMemberships.php
Kevin Adametz af72810296 fix: Abgleich der Firmenrechte entzieht Bestandskonten ohne E-Mail-Bestätigung nicht mehr pauschal alle Rechte — memberships:resync-auto mit Probelauf und Notbremse
Auf Live meldete memberships:resync-auto nach v1.4.0 „vorher 19607, nachher 9":
syncMembershipsForEmail() entzog Konten ohne email_verified_at alle
automatischen Rechte, und 19.597 davon gehören Bestandskonten aus dem
Altsystem. Jetzt wird nur entzogen, was keine E-Mail-Grundlage mehr hat —
unabhängig vom Kontostatus (S-03 bleibt wirksam); neue Rechte weiterhin nur
für aktive, verifizierte Konten (F01).

memberships:resync-auto läuft in einer Transaktion, kennt --dry-run und rollt
zurück, wenn mehr als 5 % (mindestens 50) der Rechte wegfallen würden
(--force zum Erzwingen). Probelauf gegen die Live-Kopie: 19.607 → 19.316.
Wiederherstellung: storage/app/private/restore-company-user-auto-2026-10-07.sql
(nicht versioniert, INSERT IGNORE, idempotent).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:22:25 +00:00

107 lines
3.8 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\User;
use App\Services\Auth\ContactAccessService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* Bewertet automatisch vergebene Firmenrechte (`granted_via` = auto bzw.
* auto-upgrade) aller betroffenen User neu (Sicherheitsaudit 07.10.2026,
* S-03). Der CompanyMembershipObserver erledigt das bei jeder Änderung über
* Eloquent; dieses Kommando bereinigt den Altbestand und repariert nach
* Massen-Updates per Query-Builder.
*
* Manuelle (`manual`) und API-Mitgliedschaften (`api`) bleiben unberührt.
*
* Sicherung (nach dem Abgleich vom 07.10.2026, v1.4.1): Der Lauf geschieht in
* einer Transaktion. `--dry-run` rollt immer zurück und zeigt nur die Zahlen.
* Würde mehr als MAX_REVOKE_SHARE der Rechte (mindestens MAX_REVOKE_ABSOLUTE)
* wegfallen, rollt das Kommando zurück, solange nicht `--force` gesetzt ist —
* ein solcher Einbruch deutet auf einen Fehler, nicht auf veraltete Rechte.
*/
class ResyncAutomaticMemberships extends Command
{
private const MAX_REVOKE_SHARE = 0.05;
private const MAX_REVOKE_ABSOLUTE = 50;
protected $signature = 'memberships:resync-auto
{--user= : Nur diesen User (ID) abgleichen}
{--dry-run : Nur zählen, nichts speichern}
{--force : Auch bei auffällig vielen Entzügen speichern}';
protected $description = 'Gleicht automatisch vergebene Firmenrechte mit den aktuellen Kontakt- und Firmenadressen ab.';
public function handle(ContactAccessService $contactAccess): int
{
$before = $this->countAutomaticGrants();
$userIds = DB::table('company_user')
->whereIn('granted_via', ['auto', 'auto-upgrade'])
->when($this->option('user'), fn ($query, $userId) => $query->where('user_id', (int) $userId))
->distinct()
->pluck('user_id');
if ($userIds->isEmpty()) {
$this->info('Keine automatisch vergebenen Firmenrechte gefunden — nichts zu tun.');
return self::SUCCESS;
}
DB::beginTransaction();
try {
$bar = $this->output->createProgressBar($userIds->count());
foreach ($userIds->chunk(200) as $chunk) {
User::query()->whereIn('id', $chunk->all())->get()->each(function (User $user) use ($contactAccess, $bar): void {
$contactAccess->syncMembershipsForEmail($user);
$bar->advance();
});
}
$bar->finish();
$this->newLine();
$after = $this->countAutomaticGrants();
$revoked = max(0, $before - $after);
$summary = "Abgeglichen: {$userIds->count()} User. Automatische Rechte vorher {$before}, nachher {$after}.";
if ($this->option('dry-run')) {
DB::rollBack();
$this->info($summary.' Probelauf — nichts gespeichert.');
return self::SUCCESS;
}
$limit = max(self::MAX_REVOKE_ABSOLUTE, (int) ceil($before * self::MAX_REVOKE_SHARE));
if ($revoked > $limit && ! $this->option('force')) {
DB::rollBack();
$this->error($summary." Es würden {$revoked} Rechte wegfallen (Grenze {$limit}) — zurückgerollt. Ursache prüfen, nur bei Absicht mit --force wiederholen.");
return self::FAILURE;
}
DB::commit();
$this->info($summary);
return self::SUCCESS;
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
}
private function countAutomaticGrants(): int
{
return DB::table('company_user')
->where('role', 'responsible')
->whereIn('granted_via', ['auto', 'auto-upgrade'])
->count();
}
}