Stop a suspended customer's sign-out from killing a shared operator session
EnsureCustomerActive used session()->invalidate() to sign a suspended or closed customer out. In shared-host mode the console and the portal keep their login key in one session, so invalidate() (flush() + migrate(true)) silently signed out an operator elsewhere in that same browser too. Same fix as the console's own /logout route: regenerate() gets a fresh session id without flushing attributes belonging to another guard.feat/operator-identity
parent
a5db914696
commit
055c6228f4
|
|
@ -27,7 +27,20 @@ class EnsureCustomerActive
|
||||||
|
|
||||||
if ($customer !== null && ($customer->status === 'suspended' || $customer->status === 'closed' || $customer->closed_at !== null)) {
|
if ($customer !== null && ($customer->status === 'suspended' || $customer->status === 'closed' || $customer->closed_at !== null)) {
|
||||||
Auth::logout();
|
Auth::logout();
|
||||||
$request->session()->invalidate();
|
|
||||||
|
// NOT session()->invalidate(): where the console and the portal
|
||||||
|
// share a host (shared/fallback mode — this VM's own mode), both
|
||||||
|
// guards keep their login key in the SAME session. invalidate()
|
||||||
|
// is flush() + migrate(true) — it discards every attribute in the
|
||||||
|
// session, not just the 'web' guard's, so it would silently sign
|
||||||
|
// an operator elsewhere in that same browser out of the console
|
||||||
|
// too. regenerate() issues a new session id (no fixation risk)
|
||||||
|
// without touching the other attributes, so the 'operator'
|
||||||
|
// guard's own login key — untouched by the Auth::logout() call
|
||||||
|
// above, which only logs out the default ('web') guard —
|
||||||
|
// survives untouched. Same fix as routes/admin.php's own
|
||||||
|
// /logout route, mirrored the other way round.
|
||||||
|
$request->session()->regenerate();
|
||||||
$request->session()->regenerateToken();
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
$key = $customer->closed_at !== null || $customer->status === 'closed' ? 'auth.account_closed' : 'auth.account_suspended';
|
$key = $customer->closed_at !== null || $customer->status === 'closed' ? 'auth.account_closed' : 'auth.account_suspended';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Customer;
|
||||||
|
use App\Models\Operator;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same shape as tests/Feature/Admin/OperatorLogoutTest.php, the other side of
|
||||||
|
* the same bug family: in shared-host mode (this suite's default, ADMIN_HOSTS=""
|
||||||
|
* — also this VM's own mode) the portal and console guards keep their login
|
||||||
|
* key in ONE session. EnsureCustomerActive used session()->invalidate() to
|
||||||
|
* sign a suspended or closed customer out — flush() + migrate(true) — which
|
||||||
|
* discarded every attribute in the session, not only the 'web' guard's, so it
|
||||||
|
* silently signed an operator elsewhere in that same browser out of the
|
||||||
|
* console too.
|
||||||
|
*/
|
||||||
|
it("suspending a portal customer does not sign an operator out of a console session sharing the same browser", function () {
|
||||||
|
$operator = Operator::factory()->role('Owner')->create();
|
||||||
|
$user = User::factory()->create();
|
||||||
|
Customer::factory()->create(['email' => $user->email, 'user_id' => $user->id, 'status' => 'suspended']);
|
||||||
|
|
||||||
|
// Auth::guard(...)->login() rather than actingAs(): actingAs() only sets
|
||||||
|
// an in-memory user on the guard object, it never writes to the session —
|
||||||
|
// so it cannot tell session()->invalidate() and session()->regenerate()
|
||||||
|
// apart. login() does the same session write a real sign-in performs.
|
||||||
|
Auth::guard('operator')->login($operator);
|
||||||
|
Auth::guard('web')->login($user);
|
||||||
|
|
||||||
|
$this->get(route('dashboard'))->assertRedirect(route('login'));
|
||||||
|
|
||||||
|
// Auth::guard('operator') is still the SAME cached guard instance from
|
||||||
|
// the login() call above — its check() would say "authenticated" from
|
||||||
|
// its own in-memory $user property regardless of what happened to the
|
||||||
|
// session, which is exactly the blind spot that would make this
|
||||||
|
// assertion pass whether the fix is there or not. forgetGuards() drops
|
||||||
|
// that cache, forcing the next guard() call to rebuild from the CURRENT
|
||||||
|
// session data — the same thing a genuinely new request would do.
|
||||||
|
Auth::forgetGuards();
|
||||||
|
|
||||||
|
expect(Auth::guard('web')->check())->toBeFalse()
|
||||||
|
->and(Auth::guard('operator')->check())->toBeTrue()
|
||||||
|
->and(Auth::guard('operator')->id())->toBe($operator->id);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue