From d1a650642f3a41d2a232163a8a1a6d99828b98dc Mon Sep 17 00:00:00 2001 From: Arjan Kapteijn Date: Thu, 22 Oct 2026 15:30:23 +0200 Subject: [PATCH 1/2] fix: stop users in timeout from starting orgs through the api The web org.store route refuses users in timeout, but its api-org.store twin did not, so a timed-out user with an approved device could still create orgs and put the org name in members' inboxes. Co-Authored-By: Claude Opus 5.5 --- routes/api.php | 2 +- tests/Feature/Controllers/Api/OrgControllerTest.php | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/routes/api.php b/routes/api.php index 915587c51..0d5c9ed5c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -245,7 +245,7 @@ // Remotes: finding people to push to, and starting an org, as the web's New remote and New org do... Route::middleware('throttle:conversations')->group(function (): void { Route::get('remotes/candidates', [RemoteCandidateController::class, 'index'])->name('api-remote-candidate.index'); - Route::post('remotes/orgs', [OrgController::class, 'store'])->middleware('throttle:messages')->name('api-org.store'); + Route::post('remotes/orgs', [OrgController::class, 'store'])->middleware([PreventPostingInTimeout::class, 'throttle:messages'])->name('api-org.store'); }); }); }); diff --git a/tests/Feature/Controllers/Api/OrgControllerTest.php b/tests/Feature/Controllers/Api/OrgControllerTest.php index fe8d78ef7..9ac38fa64 100644 --- a/tests/Feature/Controllers/Api/OrgControllerTest.php +++ b/tests/Feature/Controllers/Api/OrgControllerTest.php @@ -6,6 +6,7 @@ use App\Models\Conversation; use App\Models\Message; use App\Models\User; +use App\Models\UserTimeout; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Str; @@ -34,6 +35,18 @@ ->and(Message::query()->sole()->event)->toBe(OrgEvent::Created); }); +it('refuses to start an org for a user in timeout, as the web does', function (): void { + [$nuno] = followersOf($this->user, 1); + UserTimeout::factory()->for($this->user)->create(); + + signedApiRequest($this->keyPair, 'POST', route('api-org.store'), ['name' => 'buy my course', 'members' => [$nuno->username]]) + ->assertForbidden() + ->assertJsonPath('code', 'timed_out'); + + expect(Conversation::query()->exists())->toBeFalse() + ->and(Message::query()->exists())->toBeFalse(); +}); + it('leaves an org without a name when none is given', function (?string $name): void { [$nuno] = followersOf($this->user, 1); From 1292c462c67365efada1a5ea62758d0a13773762 Mon Sep 17 00:00:00 2001 From: Arjan Kapteijn Date: Thu, 29 Oct 2026 14:17:29 +0100 Subject: [PATCH 2/2] test: send a real member handle in the api org timeout test Co-Authored-By: Claude Opus 5.5 --- tests/Feature/Controllers/Api/OrgControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Controllers/Api/OrgControllerTest.php b/tests/Feature/Controllers/Api/OrgControllerTest.php index ddaf0db62..b9d0b5b3f 100644 --- a/tests/Feature/Controllers/Api/OrgControllerTest.php +++ b/tests/Feature/Controllers/Api/OrgControllerTest.php @@ -39,7 +39,7 @@ [$nuno] = followersOf($this->user, 1); UserTimeout::factory()->for($this->user)->create(); - signedApiRequest($this->keyPair, 'POST', route('api-org.store'), ['name' => 'buy my course', 'members' => [$nuno->username]]) + signedApiRequest($this->keyPair, 'POST', route('api-org.store'), ['name' => 'buy my course', 'members' => [$nuno->handle]]) ->assertForbidden() ->assertJsonPath('code', 'timed_out');