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>
107 lines
3.8 KiB
PHP
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();
|
|
}
|
|
}
|