CluPilotCloud/app/Services/Devices/SessionRegistry.php

121 lines
4.4 KiB
PHP

<?php
namespace App\Services\Devices;
use App\Models\LoginSession;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
/**
* Where an account is signed in, and how to end one of them.
*
* Everything is scoped by guard as well as by id, in every method. Operators
* and customers are two tables and two guards (R21), so operator 1 and customer
* 1 share an integer and nothing else — an unscoped query here would let either
* of them sign the other out.
*
* A session counts as live only if the framework still has its row AND that row
* has been touched within the session lifetime. The join alone is not enough:
* Laravel collects expired sessions by lottery, so on a quiet installation rows
* for sessions nobody could still use sit there for days, and the list would
* offer somebody a laptop they closed last month as somewhere they are signed
* in right now.
*/
final class SessionRegistry
{
/**
* The live sessions for one identity, newest activity first.
*
* @return Collection<int, object>
*/
public function for(string $guard, int $id, ?string $currentUuid = null): Collection
{
return DB::table('login_sessions as ls')
->join('user_devices as d', 'd.id', '=', 'ls.device_id')
->join('sessions as s', 's.id', '=', 'ls.session_id')
->where('d.guard', $guard)
->where('d.authenticatable_id', $id)
->where('s.last_activity', '>=', now()->subMinutes((int) config('session.lifetime'))->getTimestamp())
->orderByDesc('ls.last_seen_at')
->get([
'ls.uuid as uuid',
'ls.ip_address as ip_address',
'ls.last_seen_at as last_seen_at',
'd.name as device',
'd.uuid as device_uuid',
])
->map(function (object $row) use ($currentUuid) {
$row->is_current = $currentUuid !== null && $row->uuid === $currentUuid;
$row->last_seen_at = $row->last_seen_at ? \Illuminate\Support\Carbon::parse($row->last_seen_at) : null;
return $row;
});
}
/** End one session. Returns false when it is not this identity's to end. */
public function end(string $guard, int $id, string $uuid): bool
{
$row = DB::table('login_sessions as ls')
->join('user_devices as d', 'd.id', '=', 'ls.device_id')
->where('d.guard', $guard)
->where('d.authenticatable_id', $id)
->where('ls.uuid', $uuid)
->first(['ls.id as id', 'ls.session_id as session_id']);
if ($row === null) {
return false;
}
$this->forget($row->session_id);
DB::table('login_sessions')->where('id', $row->id)->delete();
return true;
}
/**
* End everything except the session asking.
*
* The current one is excluded rather than ended and re-created: signing
* somebody out of the page they are using to sign other people out is not
* what the button says, and it leaves them unable to see whether it worked.
*
* @return int how many were ended
*/
public function endOthers(string $guard, int $id, ?string $currentUuid): int
{
$rows = DB::table('login_sessions as ls')
->join('user_devices as d', 'd.id', '=', 'ls.device_id')
->where('d.guard', $guard)
->where('d.authenticatable_id', $id)
->when($currentUuid !== null, fn ($q) => $q->where('ls.uuid', '!=', $currentUuid))
->get(['ls.id as id', 'ls.session_id as session_id']);
foreach ($rows as $row) {
$this->forget($row->session_id);
}
DB::table('login_sessions')->whereIn('id', $rows->pluck('id'))->delete();
return $rows->count();
}
/** The uuid of the session making this request, if it has one. */
public function currentUuid(): ?string
{
$uuid = session()->get(LoginSession::KEY);
return is_string($uuid) && $uuid !== '' ? $uuid : null;
}
/**
* Delete the framework's own row, which is what actually signs the browser
* out: its next request finds no session and starts an empty one.
*/
private function forget(?string $sessionId): void
{
if ($sessionId !== null && $sessionId !== '') {
DB::table('sessions')->where('id', $sessionId)->delete();
}
}
}