mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
fix(teams): keep switched team when concurrent requests save the session
Closes #11733
This commit is contained in:
@@ -110,3 +110,6 @@
|
||||
|
||||
## Keep browser form restore away from app state
|
||||
- Firefox session restore writes saved input values back and fires a trusted `input` event. Add `autocomplete="off"` to inputs that only mirror client state (for example an Alpine-bound color picker), or their handlers run without a user action.
|
||||
|
||||
## Session writes are last-writer-wins
|
||||
- Laravel saves the whole session payload at the end of every request, and pages run concurrent `wire:poll` requests. A value that one request changes can be overwritten by a request that started earlier. `KeepConcurrentTeamSwitch` protects `currentTeam`; protect any new session value that changes mid-session the same way, or keep it out of the session.
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Http\Middleware\EncryptCookies;
|
||||
use App\Http\Middleware\EnsureMcpEnabled;
|
||||
use App\Http\Middleware\EnsureTeamMcpEnabled;
|
||||
use App\Http\Middleware\EnsureTokenBelongsToCurrentTeamMember;
|
||||
use App\Http\Middleware\KeepConcurrentTeamSwitch;
|
||||
use App\Http\Middleware\PreventRequestsDuringMaintenance;
|
||||
use App\Http\Middleware\RedirectIfAuthenticated;
|
||||
use App\Http\Middleware\RequirePassword;
|
||||
@@ -72,6 +73,7 @@ class Kernel extends HttpKernel
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
KeepConcurrentTeamSwitch::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Laravel writes the whole session payload at the end of every request. A request
|
||||
* that started before a team switch (for example a wire:poll update) would write the
|
||||
* previous team back into the session, and the next request would make it permanent.
|
||||
* When this request did not change the team itself, keep the team that is stored now.
|
||||
*/
|
||||
class KeepConcurrentTeamSwitch
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$teamIdAtStart = data_get($request->session()->get('currentTeam'), 'id');
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
$session = $request->session();
|
||||
if (data_get($session->get('currentTeam'), 'id') !== $teamIdAtStart) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$storedSession = clone $session;
|
||||
$storedSession->start();
|
||||
$storedTeam = $storedSession->get('currentTeam');
|
||||
if ($storedTeam && data_get($storedTeam, 'id') !== $teamIdAtStart) {
|
||||
$session->put('currentTeam', $storedTeam);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Routing\RouteCollection;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function registerTeamSwitchTestRoutesBeforeCatchAll(array $actions): void
|
||||
{
|
||||
$routes = new RouteCollection;
|
||||
foreach ($actions as $uri => $action) {
|
||||
$routes->add(Route::middleware(['web', 'auth'])->get($uri, $action));
|
||||
}
|
||||
foreach (app('router')->getRoutes() as $route) {
|
||||
$routes->add($route);
|
||||
}
|
||||
app('router')->setRoutes($routes);
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
$this->firstTeam = $this->user->teams()->first();
|
||||
$this->secondTeam = Team::factory()->create();
|
||||
$this->secondTeam->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
$this->thirdTeam = Team::factory()->create();
|
||||
$this->thirdTeam->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
Team::query()->update(['show_boarding' => false]);
|
||||
$this->user->forceFill(['current_team_id' => $this->firstTeam->id])->saveQuietly();
|
||||
|
||||
registerTeamSwitchTestRoutesBeforeCatchAll([
|
||||
'_test/team-switch-during-request' => function () {
|
||||
// The team switcher saves the session while this request still runs.
|
||||
$switchRequestSession = clone session()->driver();
|
||||
$switchRequestSession->start();
|
||||
$switchRequestSession->put('currentTeam', $this->secondTeam);
|
||||
$switchRequestSession->save();
|
||||
$this->user->forceFill(['current_team_id' => $this->secondTeam->id])->saveQuietly();
|
||||
|
||||
return 'ok';
|
||||
},
|
||||
'_test/switch-to-second-team' => function () {
|
||||
$otherRequestSession = clone session()->driver();
|
||||
$otherRequestSession->start();
|
||||
$otherRequestSession->put('currentTeam', $this->thirdTeam);
|
||||
$otherRequestSession->save();
|
||||
|
||||
refreshSession($this->secondTeam);
|
||||
|
||||
return 'ok';
|
||||
},
|
||||
'_test/current-team' => fn () => 'team:'.currentTeam()->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->firstTeam]);
|
||||
});
|
||||
|
||||
test('a request that was in flight during a team switch does not restore the previous team', function () {
|
||||
$this->get('/_test/team-switch-during-request')->assertOk();
|
||||
|
||||
$this->get('/_test/current-team')->assertOk()->assertSee('team:'.$this->secondTeam->id);
|
||||
expect($this->user->fresh()->current_team_id)->toBe($this->secondTeam->id);
|
||||
});
|
||||
|
||||
test('a request that switches the team keeps the team it selected over a concurrent change', function () {
|
||||
$this->get('/_test/switch-to-second-team')->assertOk();
|
||||
|
||||
$this->get('/_test/current-team')->assertOk()->assertSee('team:'.$this->secondTeam->id);
|
||||
expect($this->user->fresh()->current_team_id)->toBe($this->secondTeam->id);
|
||||
});
|
||||
Reference in New Issue
Block a user