121 lines
4.4 KiB
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();
|
|
}
|
|
}
|
|
}
|