Die Adresse ist die Person, sobald es das Konto im Gast gibt
W1. `addressEditable` hing an status === 'invited' — und nichts im Bestand setzt seats.status je von 'invited' auf 'active'. Ein eingeladener Sitz blieb damit fuer immer 'invited' und seine Adresse dauerhaft aenderbar, entgegen dem eigenen Kopfkommentar dieser Klasse. Der Ablauf: einladen fuer a@x, spaeter im Modal auf b@y aendern. seats.email wird b@y, nc_username bleibt a@x (richtig, Nextcloud kann nicht umbenennen), und kein Befehl traegt die neue Adresse in den Gast. Danach schickt "Erneut senden" ein user:welcome --reset-password 'a@x': das Portal fuehrt b@y als Inhaberin des Sitzes, tatsaechlich haelt a@x den Zugang und bekommt jederzeit einen neuen Zuruecksetzen-Link. Massgeblich ist jetzt blank($seat->nc_username) — genau die Bedingung, die der Kommentar ohnehin meinte: einmal gesetzt, danach unveraenderlich. Der Kommentar ist berichtigt. Beide Stellen, das Anzeigen und die erneute Pruefung in save(), lesen dieselbe Angabe; die hydrierte Eigenschaft bleibt ungeglaubt. Die Adresse nachtraeglich in den Gast zu fahren ist ausdruecklich nicht Teil dieses Durchgangs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/versandtakt
parent
1fd5d5f8d2
commit
f476ee6bd1
|
|
@ -34,10 +34,23 @@ class EditSeat extends ModalComponent
|
|||
/**
|
||||
* Whether the address may still be corrected.
|
||||
*
|
||||
* Only while the invitation is in flight. Once someone has accepted, the
|
||||
* address IS the person: editing it would hand one employee's access to
|
||||
* another with nobody told — a transfer of access wearing the clothes of a
|
||||
* rename.
|
||||
* Only until the seat has an account in the customer's cloud. From that
|
||||
* point the address IS the person: editing it would hand one employee's
|
||||
* access to another with nobody told — a transfer of access wearing the
|
||||
* clothes of a rename.
|
||||
*
|
||||
* Die Bedingung hing an `status === 'invited'`, und das trug nicht: NICHTS
|
||||
* im Bestand setzt `seats.status` je von 'invited' auf 'active'. Ein
|
||||
* eingeladener Sitz blieb damit für immer 'invited' und seine Adresse
|
||||
* dauerhaft änderbar — entgegen genau diesem Kommentar. `seats.email`
|
||||
* wurde `b@y`, `nc_username` blieb `a@x` (richtig: Nextcloud kann Benutzer
|
||||
* nicht umbenennen), und kein Befehl trug die neue Adresse in den Gast.
|
||||
* „Erneut senden" schickte danach `user:welcome --reset-password 'a@x'`:
|
||||
* das Portal führte `b@y` als Inhaberin des Sitzes, tatsächlich hielt
|
||||
* `a@x` den Zugang und bekam jederzeit einen neuen Zurücksetzen-Link.
|
||||
*
|
||||
* `nc_username` ist die Angabe, die der Kommentar ohnehin meint — genau
|
||||
* einmal gesetzt, danach unveränderlich.
|
||||
*/
|
||||
public bool $addressEditable = false;
|
||||
|
||||
|
|
@ -51,7 +64,7 @@ class EditSeat extends ModalComponent
|
|||
$this->name = (string) $seat->name;
|
||||
$this->email = (string) $seat->email;
|
||||
$this->isOwner = $seat->role === 'owner';
|
||||
$this->addressEditable = $seat->status === 'invited';
|
||||
$this->addressEditable = blank($seat->nc_username);
|
||||
}
|
||||
|
||||
public function save()
|
||||
|
|
@ -66,8 +79,9 @@ class EditSeat extends ModalComponent
|
|||
$rules = ['name' => 'nullable|string|max:255'];
|
||||
|
||||
// Re-read from the record, never from the hydrated property: a forged
|
||||
// addressEditable would otherwise open the address of an accepted seat.
|
||||
if ($seat->status === 'invited') {
|
||||
// addressEditable would otherwise open the address of a seat that
|
||||
// already has an account in the cloud.
|
||||
if (blank($seat->nc_username)) {
|
||||
$rules['email'] = 'required|email|max:255';
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +89,7 @@ class EditSeat extends ModalComponent
|
|||
|
||||
$changes = ['name' => trim($data['name'] ?? '') ?: null];
|
||||
|
||||
if ($seat->status === 'invited') {
|
||||
if (blank($seat->nc_username)) {
|
||||
$address = trim($data['email']);
|
||||
|
||||
if ($address !== $seat->email) {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ it('creates an owner seat on first visit', function () {
|
|||
// invite() heisst seit dem Nextcloud-Anschluss addSeat(), weil sie genau das
|
||||
// tut und nichts weiter: eine Zeile anlegen. Verschickt wird erst in
|
||||
// sendInvite() — siehe SyncSeatToNextcloudTest. Die Sitzplatzgrenze und die
|
||||
// Sperre gegen doppelte Adressen sind dabei unveraendert geblieben.
|
||||
// Sperre gegen doppelte Adressen sind dabei unverändert geblieben.
|
||||
|
||||
it('adds a seat and blocks duplicates', function () {
|
||||
['user' => $user, 'customer' => $customer] = seatSetup();
|
||||
|
|
@ -65,8 +65,8 @@ it('will not remove or demote the last owner', function () {
|
|||
$owner = $customer->seats()->where('role', 'owner')->first();
|
||||
|
||||
Livewire::actingAs($user)->test(Users::class)->call('revoke', $owner->uuid);
|
||||
// Auf den STATUS geprueft, nicht mehr nur auf die Zeile: seit revoke()
|
||||
// grundsaetzlich nicht mehr loescht, bewiese eine noch vorhandene Zeile
|
||||
// Auf den STATUS geprüft, nicht mehr nur auf die Zeile: seit revoke()
|
||||
// grundsätzlich nicht mehr löscht, bewiese eine noch vorhandene Zeile
|
||||
// gar nichts — sie bliebe auch dann stehen, wenn die Inhaber-Sperre
|
||||
// versagte. Der Status ist die Zusicherung, die hier gemeint war.
|
||||
expect($customer->seats()->whereKey($owner->id)->exists())->toBeTrue()
|
||||
|
|
@ -77,11 +77,11 @@ it('will not remove or demote the last owner', function () {
|
|||
});
|
||||
|
||||
it('revokes a non-owner seat without deleting the row', function () {
|
||||
// Frueher wurde die Zeile geloescht. Das faellt weg: ein Fehlgriff im
|
||||
// Früher wurde die Zeile gelöscht. Das fällt weg: ein Fehlgriff im
|
||||
// Userpanel darf die Arbeit eines Menschen nicht vernichten, und in einem
|
||||
// Produkt, das mit Nachvollziehbarkeit verkauft wird, gaebe es danach
|
||||
// nichts mehr zu zeigen, wer welchen Zugang hatte. Geloescht wird, wenn
|
||||
// ueberhaupt, in der Nextcloud selbst — dort fragt Nextcloud nach, was mit
|
||||
// Produkt, das mit Nachvollziehbarkeit verkauft wird, gäbe es danach
|
||||
// nichts mehr zu zeigen, wer welchen Zugang hatte. Gelöscht wird, wenn
|
||||
// überhaupt, in der Nextcloud selbst — dort fragt Nextcloud nach, was mit
|
||||
// den Dateien geschehen soll.
|
||||
['user' => $user, 'customer' => $customer] = seatSetup();
|
||||
$seat = Seat::factory()->create(['customer_id' => $customer->id, 'role' => 'member']);
|
||||
|
|
@ -129,7 +129,9 @@ it('renames any seat, the owner included', function () {
|
|||
expect($owner->fresh()->name)->toBe('Dr. M. Muster');
|
||||
});
|
||||
|
||||
it('corrects the address of an invitation still in flight', function () {
|
||||
it('corrects the address of an invitation nobody has sent yet', function () {
|
||||
// Angelegt, noch nicht eingeladen: in der Cloud gibt es dazu nichts, und
|
||||
// ein Tippfehler ist an dieser Stelle folgenlos zu berichtigen.
|
||||
$user = User::factory()->create();
|
||||
$customer = Customer::factory()->create(['user_id' => $user->id, 'email' => $user->email]);
|
||||
$seat = $customer->seats()->create([
|
||||
|
|
@ -146,15 +148,25 @@ it('corrects the address of an invitation still in flight', function () {
|
|||
expect($seat->fresh()->email)->toBe('typo@example.test');
|
||||
});
|
||||
|
||||
it('will not move an accepted seat to a different address', function () {
|
||||
// The address IS the person once they are in. Editing it would hand one
|
||||
// employee's access to another with nobody told — a transfer of access
|
||||
// wearing the clothes of a rename. Driven past the form on purpose: the
|
||||
// property is hydrated from the browser and must not be trusted.
|
||||
it('will not move a seat that already has an account in the cloud', function () {
|
||||
// Die Bedingung hing an `status === 'invited'` — und NICHTS im Bestand
|
||||
// setzt `seats.status` je von 'invited' auf 'active'. Ein eingeladener
|
||||
// Sitz blieb damit für immer 'invited', und die Adresse dauerhaft
|
||||
// änderbar: `seats.email` wurde `b@y`, `nc_username` blieb `a@x`
|
||||
// (richtig — Nextcloud kann nicht umbenennen), und kein Befehl trug die
|
||||
// neue Adresse in den Gast. „Erneut senden" schickte danach
|
||||
// `user:welcome --reset-password 'a@x'`: das Portal führte `b@y` als
|
||||
// Inhaberin des Sitzes, tatsächlich hielt `a@x` den Zugang und bekam
|
||||
// jederzeit einen neuen Zurücksetzen-Link.
|
||||
//
|
||||
// Massgeblich ist deshalb `nc_username`: ab da IST die Adresse die Person.
|
||||
// Am Formular vorbei gefahren, mit Absicht — die Eigenschaft kommt aus
|
||||
// dem Browser und darf nicht geglaubt werden.
|
||||
$user = User::factory()->create();
|
||||
$customer = Customer::factory()->create(['user_id' => $user->id, 'email' => $user->email]);
|
||||
$seat = $customer->seats()->create([
|
||||
'email' => 'in@example.test', 'name' => 'A', 'role' => 'member', 'status' => 'active',
|
||||
'email' => 'in@example.test', 'name' => 'A', 'role' => 'member', 'status' => 'invited',
|
||||
'nc_username' => 'in@example.test',
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
|
|
@ -166,6 +178,7 @@ it('will not move an accepted seat to a different address', function () {
|
|||
->call('save');
|
||||
|
||||
expect($seat->fresh()->email)->toBe('in@example.test')
|
||||
// Umbenennen bleibt erlaubt — es ist dieselbe Person.
|
||||
->and($seat->fresh()->name)->toBe('B');
|
||||
});
|
||||
|
||||
|
|
@ -262,8 +275,8 @@ it('revokes the seat once the page receives the confirmed event', function () {
|
|||
|
||||
Livewire::actingAs($user)->test(Users::class)->call('onRevokeConfirmed', $seat->uuid);
|
||||
|
||||
// Wieder der Status statt der Existenz: revoke() loescht nicht mehr (siehe
|
||||
// die Notiz weiter oben), also waere „die Zeile ist weg" jetzt eine
|
||||
// Wieder der Status statt der Existenz: revoke() löscht nicht mehr (siehe
|
||||
// die Notiz weiter oben), also wäre „die Zeile ist weg" jetzt eine
|
||||
// Zusicherung, die das Produkt bewusst nicht mehr gibt.
|
||||
expect($seat->fresh()->status)->toBe('revoked');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue