onConnection('provisioning'); $this->onQueue('provisioning'); } /** Auch was an handle() vorbeifliegt, muss am Sitz sichtbar werden. */ public function failed(?Throwable $e): void { Seat::query()->where('uuid', $this->seatUuid)->update([ 'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'unexpected', ]); } public function handle(NextcloudUsers $users): void { $seat = Seat::query()->with('customer')->where('uuid', $this->seatUuid)->first(); if ($seat === null) { return; } // Erneut geprüft, nicht der Seite geglaubt: eine geschlossene Instanz // kann ihre VMID auf demselben Host weiterverliehen haben — dieselbe // Falle, die in IssueInstanceAdminAccess schon beschrieben steht. $instance = $seat->customer?->instances() ->whereIn('status', ['active', 'cancellation_scheduled']) ->latest('id')->first(); if ($instance === null || $instance->host === null || blank($instance->vmid)) { $this->record($seat, false, 'no_instance'); return; } $ok = match ($this->action) { 'invite' => $this->invite($users, $instance, $seat), 'role' => $users->applyRole($instance, $seat), 'disable' => $users->disable($instance, $seat), 'enable' => $users->enable($instance, $seat), // Der Wiederholen-Knopf an einer OFFENEN Zeile. Er weiss nicht, // welcher der beiden Befehle beim letzten Mal scheiterte — das // stünde nirgends am Sitz, und ein Feld dafür wäre eine // weitere Behauptung über die Cloud des Kunden, die irgendwann // nicht mehr stimmt. Beide zu schicken ist billiger und ehrlicher // als es zu raten; `user:enable` auf ein offenes Konto ist // folgenlos, und das Setzen der Rolle ist es ohnehin. Siehe // Users::retry(). 'restore' => $users->enable($instance, $seat) && $users->applyRole($instance, $seat), default => false, }; $this->record($seat, $ok, $ok ? null : 'guest_failed'); } /** * Einladen setzt in einem Zug auch die Gruppe: ein Benutzer, der angelegt * ist und in keiner Rolle steckt, sieht in seiner neuen Cloud nichts und * meldet sich am ersten Tag beim Inhaber. * * Das Anlegen wird dabei SOFORT vermerkt, noch bevor die Gruppe gesetzt * wird. `nc_synced_at` beantwortet die Frage „gibt es dieses Konto dort * überhaupt?", und ab dem geglückten `user:add` lautet die Antwort ja — * auch wenn der nächste Befehl scheitert. Die Seite entscheidet genau * daran, ob ein späteres Sperren etwas zu sperren hätte; ohne diese Zeile * bliebe der gefährlichste Fall offen: Konto angelegt, Rolle gescheitert, * Zugang entzogen — und nie eine Sperre losgeschickt. * * `nc_state` bleibt davon unberührt: der Versuch ist erst gelungen, wenn * auch die Rolle sitzt. */ private function invite(NextcloudUsers $users, Instance $instance, Seat $seat): bool { // VOR dem Vermerk gelesen: war dieser Sitz schon einmal in der // Nextcloud, ist dies eine Wiederaufnahme — und sein Konto dort steht // gesperrt, weil `revoke()` ein `disable` geschickt hat. `user:welcome` // macht eine Sperre nicht rückgängig; ohne diese Zeile bekäme der // Wiederaufgenommene eine Willkommensmail zu einer Tür, die zu bleibt. // Bei einem frisch angelegten Konto entfällt der Befehl ganz, statt // sich auf ein folgenloses `user:enable` zu verlassen. $wiederaufnahme = $seat->nc_synced_at !== null; if (! $users->invite($instance, $seat)) { return false; } $seat->forceFill(['nc_synced_at' => now()])->save(); /* * Zwischen Absenden und Ausführen kann sich die Absicht geändert * haben — dieselbe Begründung wie beim Vermerk oben. * * Der Arbeiter teilt sich die Warteschlange mit der bezahlten * Bereitstellung; das Fenster ist MINUTEN lang, nicht Millisekunden. * In dieser Zeit merkt ein Inhaber seinen Fehlgriff und entzieht. Ohne * diese Zeilen führe der Auftrag danach blind aus, was beim Absenden * galt: die Zeile zeichnete „Entfernt" und in der Cloud stände ein * aktives, freigeschaltetes Konto. * * Frisch aus der Datenbank, nicht am mitgeschleppten Modell: dieses * Objekt stammt vom Anfang des Auftrags und trägt den Status von * damals. */ $gesperrtGewollt = in_array( Seat::query()->where('uuid', $seat->uuid)->value('status'), ['revoked', 'suspended'], true, ); // Eine Tür aufzumachen, um sie im selben Auftrag wieder zuzuziehen, // wäre im besten Fall folgenlos und im schlechteren ein offenes // Zeitfenster. if ($wiederaufnahme && ! $gesperrtGewollt && ! $users->enable($instance, $seat)) { return false; } $rolleOk = $users->applyRole($instance, $seat); if (! $gesperrtGewollt) { return $rolleOk; } // Die Sperre geht auch dann noch hinaus, wenn die Rolle scheiterte: // an ihr hängt ein beendeter Zugang, und eine misslungene Gruppe darf // ihn nicht offen lassen. Gemeldet wird trotzdem beides — die Zeile // stimmt nur, wenn sie den Fehlschlag zeigt. return $users->disable($instance, $seat) && $rolleOk; } /** * `nc_synced_at` bleibt beim Fehlschlag stehen, statt geleert zu werden: * wann dieser Sitz zuletzt WIRKLICH stimmte, ist genau die Angabe, die * einen Fehlschlag einordnet — heute Morgen oder vor drei Wochen. */ private function record(Seat $seat, bool $ok, ?string $grund): void { $seat->forceFill([ 'nc_state' => $ok ? Seat::STATE_SYNCED : Seat::STATE_FAILED, 'nc_error' => $grund, 'nc_synced_at' => $ok ? now() : $seat->nc_synced_at, ])->save(); } }