75 lines
2.7 KiB
PHP
75 lines
2.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use App\Models\LoginSession;
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
/**
|
|
* Keeps a session row pointing at the session it describes.
|
|
*
|
|
* Laravel rotates the session id on every sign-in and on every regenerate().
|
|
* A row that stored the id once stops matching within a request or two — and
|
|
* the list would then show sessions that cannot be ended and hide the one the
|
|
* reader is sitting in. The uuid inside the payload is what survives; the id is
|
|
* refreshed from it here.
|
|
*
|
|
* Also the only thing that keeps "last seen" honest. Without it the list
|
|
* reports the moment somebody signed in, which for a browser left open for a
|
|
* fortnight is the least useful timestamp available.
|
|
*
|
|
* Written at most once a minute. This runs on every authenticated request, and
|
|
* a write per request would turn a page of thumbnails into forty updates of the
|
|
* same row.
|
|
*/
|
|
class TouchLoginSession
|
|
{
|
|
private const THROTTLE_SECONDS = 60;
|
|
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
$response = $next($request);
|
|
|
|
if (! $request->hasSession()) {
|
|
return $response;
|
|
}
|
|
|
|
$uuid = $request->session()->get(LoginSession::KEY);
|
|
|
|
if (! is_string($uuid) || $uuid === '') {
|
|
return $response;
|
|
}
|
|
|
|
$current = $request->session()->getId();
|
|
|
|
// A bare update rather than a model round-trip: this is on the path of
|
|
// every authenticated request, and the row is not needed, only moved.
|
|
DB::table('login_sessions')
|
|
->where('uuid', $uuid)
|
|
->where(function ($q) use ($current) {
|
|
// A CHANGED id is never throttled. Both sign-in paths raise the
|
|
// Login event before calling session()->regenerate(), so the row
|
|
// is written against an id that is dead a moment later. Held
|
|
// back by the throttle, the row would point at nothing for the
|
|
// first minute of every session — the one it describes missing
|
|
// from the list, and "end this one" deleting a session that no
|
|
// longer exists.
|
|
$q->where('session_id', '!=', $current)
|
|
->orWhereNull('session_id')
|
|
->orWhereNull('last_seen_at')
|
|
->orWhere('last_seen_at', '<=', now()->subSeconds(self::THROTTLE_SECONDS));
|
|
})
|
|
->update([
|
|
'session_id' => $current,
|
|
'ip_address' => $request->ip(),
|
|
'last_seen_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
return $response;
|
|
}
|
|
}
|