🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
952 lines
26 KiB
PHP
Executable File
952 lines
26 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\RSpade\Core\Session;
|
|
|
|
use RuntimeException;
|
|
use App\RSpade\Core\Database\Models\Rsx_System_Model_Abstract;
|
|
use App\RSpade\Core\Manifest\Manifest;
|
|
use App\RSpade\Core\Models\Login_User_Model;
|
|
use App\RSpade\Core\Models\Site_Model;
|
|
use App\RSpade\Core\Models\User_Model;
|
|
use App\RSpade\Core\Session\User_Agent;
|
|
|
|
/**
|
|
* Session model - handles both authentication sessions and static session management
|
|
*
|
|
* This class serves dual purposes:
|
|
* 1. As a Laravel Eloquent model for the sessions table
|
|
* 2. As a static interface for session management (similar to RS3 design)
|
|
*
|
|
* The session represents a unique browser session with persistent authentication.
|
|
* Sessions are always persistent (365 days) - no "remember me" option.
|
|
*
|
|
* @FILE-SUBCLASS-01-EXCEPTION Class intentionally named Session instead of Session_Model
|
|
* to maintain compatibility with static method calls like Session::init(). The developer will
|
|
* almost never be interacting with a session orm record directly, so the term Session_Model
|
|
* doesnt have much meaning.
|
|
*
|
|
* @property int $id
|
|
* @property bool $active
|
|
* @property int $site_id
|
|
* @property int $login_user_id
|
|
* @property int $experience_id
|
|
* @property string $session_token
|
|
* @property string $csrf_token
|
|
* @property string $ip_address
|
|
* @property string $user_agent
|
|
* @property \Carbon\Carbon $last_active
|
|
* @property int $version
|
|
* @property \Carbon\Carbon $created_at
|
|
* @property \Carbon\Carbon $updated_at
|
|
*/
|
|
/**
|
|
* _AUTO_GENERATED_ Database type hints - do not edit manually
|
|
* Generated on: 2025-12-10 02:59:33
|
|
* Table: _sessions
|
|
*
|
|
* @property int $id
|
|
* @property bool $active
|
|
* @property int $site_id
|
|
* @property int $login_user_id
|
|
* @property mixed $session_token
|
|
* @property mixed $csrf_token
|
|
* @property mixed $ip_address
|
|
* @property mixed $user_agent
|
|
* @property string $last_active
|
|
* @property int $version
|
|
* @property string $created_at
|
|
* @property string $updated_at
|
|
* @property int $created_by
|
|
* @property int $updated_by
|
|
* @property int $experience_id
|
|
*
|
|
* @mixin \Eloquent
|
|
*/
|
|
class Session extends Rsx_System_Model_Abstract
|
|
{
|
|
// Enum definitions (required by abstract parent)
|
|
public static $enums = [];
|
|
|
|
// Static session management properties
|
|
private static $_session = null;
|
|
|
|
private static $_site = null;
|
|
|
|
private static $_login_user = null; // Authentication identity (Login_User_Model)
|
|
|
|
private static $_user = null; // Site-specific user (User_Model)
|
|
|
|
private static $_session_token = null;
|
|
|
|
private static $_has_init = false;
|
|
|
|
private static $_has_activate = false;
|
|
|
|
private static $_has_set_cookie = false;
|
|
|
|
// Experience and request-scoped overrides
|
|
private static $_experience_id = 0;
|
|
|
|
private static $_request_site_id_override = null;
|
|
|
|
// CLI mode properties (static-only, no database)
|
|
private static $_cli_site_id = null;
|
|
|
|
private static $_cli_login_user_id = null; // Authentication identity ID
|
|
|
|
private static $_cli_user_id = null; // Site-specific user ID
|
|
|
|
private static $_cli_experience_id = 0;
|
|
|
|
/**
|
|
* The table associated with the model
|
|
* @var string
|
|
*/
|
|
protected $table = '_sessions';
|
|
|
|
/**
|
|
* The attributes that should be cast
|
|
* @var array
|
|
*/
|
|
protected $casts = [
|
|
'active' => 'boolean',
|
|
'site_id' => 'integer',
|
|
'login_user_id' => 'integer',
|
|
'experience_id' => 'integer',
|
|
'version' => 'integer',
|
|
'last_active' => 'datetime',
|
|
];
|
|
|
|
/**
|
|
* Columns that should never be exported to JavaScript
|
|
* @var array
|
|
*/
|
|
protected $neverExport = [
|
|
'session_token',
|
|
'csrf_token',
|
|
'ip_address',
|
|
];
|
|
|
|
/**
|
|
* Check if running in CLI mode
|
|
* @return bool
|
|
*/
|
|
private static function __is_cli(): bool
|
|
{
|
|
return php_sapi_name() === 'cli';
|
|
}
|
|
|
|
/**
|
|
* Initialize session from cookie or request
|
|
* Loads existing session but does not create new one
|
|
* Filters by current experience_id to support multiple authentication realms
|
|
* In CLI mode: does nothing
|
|
* @return void
|
|
*/
|
|
public static function init(): void
|
|
{
|
|
if (self::$_has_init) {
|
|
return;
|
|
}
|
|
self::$_has_init = true;
|
|
|
|
// CLI mode: do nothing
|
|
if (self::__is_cli()) {
|
|
return;
|
|
}
|
|
|
|
Manifest::init();
|
|
|
|
// Try to get session token from cookie or request
|
|
$session_token = $_COOKIE['rsx'] ?? null;
|
|
|
|
if (empty($session_token)) {
|
|
self::$_session = null;
|
|
|
|
return;
|
|
}
|
|
|
|
// Load session for CURRENT EXPERIENCE only
|
|
// This allows same cookie to have different sessions per experience
|
|
$session = static::where('session_token', $session_token)
|
|
->where('active', true)
|
|
->where('experience_id', self::$_experience_id)
|
|
->first();
|
|
|
|
if (!$session) {
|
|
self::$_session = null;
|
|
|
|
return;
|
|
}
|
|
|
|
// Update last activity (but don't save immediately to avoid version conflicts)
|
|
$session->last_active = now();
|
|
|
|
// We'll let the session save happen later if needed (e.g. in set_user)
|
|
// For simple page loads, we can update last_active in a separate query
|
|
static::where('id', $session->id)->update(['last_active' => now()]);
|
|
|
|
// Reload the session to ensure we have the latest version
|
|
$session = static::find($session->id);
|
|
|
|
self::$_session_token = $session_token;
|
|
self::$_session = $session;
|
|
|
|
self::_set_cookie();
|
|
}
|
|
|
|
/**
|
|
* Activate session - creates new one if needed
|
|
* In CLI mode: does nothing
|
|
* @return void
|
|
*/
|
|
private static function __activate(): void
|
|
{
|
|
if (self::$_has_activate) {
|
|
return;
|
|
}
|
|
self::$_has_activate = true;
|
|
|
|
// CLI mode: do nothing
|
|
if (self::__is_cli()) {
|
|
return;
|
|
}
|
|
|
|
self::init();
|
|
|
|
// If no session exists, create one
|
|
if (empty(self::$_session)) {
|
|
// Generate cryptographically secure token
|
|
self::$_session_token = bin2hex(random_bytes(32));
|
|
|
|
// Generate CSRF token
|
|
$csrf_token = bin2hex(random_bytes(32));
|
|
|
|
$session = new static();
|
|
$session->session_token = self::$_session_token;
|
|
$session->csrf_token = $csrf_token;
|
|
$session->ip_address = self::__get_client_ip();
|
|
$session->user_agent = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255);
|
|
$session->last_active = now();
|
|
$session->active = true;
|
|
$session->site_id = 0;
|
|
$session->experience_id = self::$_experience_id;
|
|
$session->version = 1;
|
|
|
|
$session->save();
|
|
|
|
self::$_session = $session;
|
|
self::_set_cookie();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the session cookie with security flags
|
|
* In CLI mode: does nothing
|
|
* @return void
|
|
*/
|
|
private static function _set_cookie(): void
|
|
{
|
|
if (self::$_has_set_cookie) {
|
|
return;
|
|
}
|
|
self::$_has_set_cookie = true;
|
|
|
|
// CLI mode: do nothing
|
|
if (self::__is_cli()) {
|
|
return;
|
|
}
|
|
|
|
// Set cookie with security flags
|
|
setcookie('rsx', self::$_session_token, [
|
|
'expires' => time() + (365 * 86400), // 1 year
|
|
'path' => '/',
|
|
'domain' => '', // Current domain only
|
|
'secure' => true, // HTTPS only
|
|
'httponly' => true, // No JavaScript access
|
|
'samesite' => 'Lax', // CSRF protection
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get client IP address, handling proxies
|
|
* In CLI mode: returns "CLI"
|
|
* @return string
|
|
*/
|
|
private static function __get_client_ip(): string
|
|
{
|
|
// CLI mode: return "CLI"
|
|
if (self::__is_cli()) {
|
|
return 'CLI';
|
|
}
|
|
|
|
// Check for forwarded IP (when behind proxy/CDN)
|
|
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
|
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
|
|
|
return trim($ips[0]);
|
|
}
|
|
|
|
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
|
return $_SERVER['HTTP_X_REAL_IP'];
|
|
}
|
|
|
|
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
|
}
|
|
|
|
/**
|
|
* Reset/logout the current session
|
|
* @return void
|
|
*/
|
|
public static function reset(): void
|
|
{
|
|
self::init();
|
|
|
|
if (!empty(self::$_session)) {
|
|
self::$_session->active = false;
|
|
self::$_session->save();
|
|
}
|
|
|
|
self::$_session = null;
|
|
self::$_site = null;
|
|
self::$_user = null;
|
|
self::$_has_init = false;
|
|
self::$_has_activate = false;
|
|
self::$_has_set_cookie = false;
|
|
|
|
// Clear cookie
|
|
setcookie('rsx', '', [
|
|
'expires' => time() - 3600,
|
|
'path' => '/',
|
|
'domain' => '',
|
|
'secure' => true,
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get site ID for current session
|
|
* Respects request-scoped override first (for subdomain enforcement)
|
|
* In CLI mode: returns static CLI property
|
|
* @return int
|
|
*/
|
|
public static function get_site_id(): int
|
|
{
|
|
// Request override takes precedence (subdomain enforcement)
|
|
if (self::$_request_site_id_override !== null) {
|
|
return self::$_request_site_id_override;
|
|
}
|
|
|
|
// CLI mode: return static property
|
|
if (self::__is_cli()) {
|
|
return self::$_cli_site_id ?? 0;
|
|
}
|
|
|
|
self::init();
|
|
|
|
if (empty(self::$_session)) {
|
|
return 0;
|
|
}
|
|
|
|
return self::$_session->site_id ?? 0;
|
|
}
|
|
|
|
/**
|
|
* Get site model for current session
|
|
* @return Site_Model|null
|
|
*/
|
|
public static function get_site()
|
|
{
|
|
$site_id = self::get_site_id();
|
|
|
|
if ($site_id === 0) {
|
|
return null;
|
|
}
|
|
|
|
if (empty(self::$_site)) {
|
|
self::$_site = Site_Model::find($site_id);
|
|
}
|
|
|
|
return self::$_site;
|
|
}
|
|
|
|
/**
|
|
* Get login user ID (authentication identity) for current session
|
|
* In CLI mode: returns static CLI property
|
|
* @return int|null
|
|
*/
|
|
public static function get_login_user_id()
|
|
{
|
|
// CLI mode: return static property
|
|
if (self::__is_cli()) {
|
|
return self::$_cli_login_user_id;
|
|
}
|
|
|
|
self::init();
|
|
|
|
if (empty(self::$_session)) {
|
|
return null;
|
|
}
|
|
|
|
return self::$_session->login_user_id;
|
|
}
|
|
|
|
/**
|
|
* Get site-specific user ID for current session
|
|
* This is the users.id (site-specific), not login_users.id
|
|
* In CLI mode: returns static CLI property
|
|
* @return int|null
|
|
*/
|
|
public static function get_user_id()
|
|
{
|
|
// CLI mode: return static property
|
|
if (self::__is_cli()) {
|
|
return self::$_cli_user_id;
|
|
}
|
|
|
|
$user = self::get_user();
|
|
|
|
return $user ? $user->id : null;
|
|
}
|
|
|
|
/**
|
|
* Check if user is logged in
|
|
* @return bool
|
|
*/
|
|
public static function is_logged_in(): bool
|
|
{
|
|
return !empty(self::get_login_user_id());
|
|
}
|
|
|
|
/**
|
|
* Get login user model (authentication identity) for current session
|
|
* @return Login_User_Model|null
|
|
*/
|
|
public static function get_login_user()
|
|
{
|
|
$login_user_id = self::get_login_user_id();
|
|
|
|
if (empty($login_user_id)) {
|
|
return null;
|
|
}
|
|
|
|
if (empty(self::$_login_user)) {
|
|
self::$_login_user = Login_User_Model::find($login_user_id);
|
|
}
|
|
|
|
return self::$_login_user;
|
|
}
|
|
|
|
/**
|
|
* Get site-specific user model for current session
|
|
* @return User_Model|null
|
|
*/
|
|
public static function get_user()
|
|
{
|
|
$login_user_id = self::get_login_user_id();
|
|
$site_id = self::get_site_id();
|
|
|
|
if (empty($login_user_id) || empty($site_id)) {
|
|
return null;
|
|
}
|
|
|
|
if (empty(self::$_user)) {
|
|
self::$_user = User_Model::where('login_user_id', $login_user_id)
|
|
->where('site_id', $site_id)
|
|
->first();
|
|
}
|
|
|
|
return self::$_user;
|
|
}
|
|
|
|
/**
|
|
* Get current session model (creates if needed)
|
|
* @return Session
|
|
*/
|
|
public static function get_session(): Session
|
|
{
|
|
self::__activate();
|
|
|
|
return self::$_session;
|
|
}
|
|
|
|
/**
|
|
* Get current session ID (creates session if needed)
|
|
* @return int
|
|
*/
|
|
public static function get_session_id(): int
|
|
{
|
|
self::__activate();
|
|
|
|
return self::$_session->id;
|
|
}
|
|
|
|
/**
|
|
* Get CSRF token for current session
|
|
* @return string|null
|
|
*/
|
|
public static function get_csrf_token(): ?string
|
|
{
|
|
self::init();
|
|
|
|
if (empty(self::$_session)) {
|
|
return null;
|
|
}
|
|
|
|
return self::$_session->csrf_token;
|
|
}
|
|
|
|
/**
|
|
* Verify CSRF token
|
|
* @param string $token
|
|
* @return bool
|
|
*/
|
|
public static function verify_csrf_token(string $token): bool
|
|
{
|
|
self::init();
|
|
|
|
if (empty(self::$_session)) {
|
|
return false;
|
|
}
|
|
|
|
// Use constant-time comparison
|
|
return hash_equals(self::$_session->csrf_token, $token);
|
|
}
|
|
|
|
/**
|
|
* Logout current user
|
|
* @return void
|
|
*/
|
|
public static function logout(): void
|
|
{
|
|
self::set_login_user_id(null);
|
|
}
|
|
|
|
/**
|
|
* Set login user ID for current session (login/logout)
|
|
* In CLI mode: sets static CLI property only, no database
|
|
* @param int|null $login_user_id Login user ID, or null to logout
|
|
* @return void
|
|
*/
|
|
public static function set_login_user_id(?int $login_user_id): void
|
|
{
|
|
// Logout if null/0
|
|
if (empty($login_user_id)) {
|
|
// CLI mode: clear static property only
|
|
if (self::__is_cli()) {
|
|
self::$_cli_login_user_id = null;
|
|
self::$_cli_user_id = null;
|
|
self::$_login_user = null;
|
|
self::$_user = null;
|
|
self::$_site = null;
|
|
|
|
return;
|
|
}
|
|
|
|
self::__activate();
|
|
self::$_session->login_user_id = null;
|
|
self::$_session->save();
|
|
|
|
self::$_login_user = null;
|
|
self::$_user = null;
|
|
self::$_site = null;
|
|
|
|
return;
|
|
}
|
|
|
|
// CLI mode: set static property only
|
|
if (self::__is_cli()) {
|
|
self::$_cli_login_user_id = $login_user_id;
|
|
self::$_cli_user_id = null;
|
|
self::$_login_user = null;
|
|
self::$_user = null;
|
|
self::$_site = null;
|
|
|
|
return;
|
|
}
|
|
|
|
self::__activate();
|
|
|
|
// Regenerate session token and CSRF token on login (prevent session fixation)
|
|
$new_token = bin2hex(random_bytes(32));
|
|
$new_csrf = bin2hex(random_bytes(32));
|
|
self::$_session->session_token = $new_token;
|
|
self::$_session->csrf_token = $new_csrf;
|
|
self::$_session->login_user_id = $login_user_id;
|
|
self::$_session->version++;
|
|
self::$_session->save();
|
|
|
|
self::$_session_token = $new_token;
|
|
self::$_has_set_cookie = false; // Force new cookie
|
|
self::_set_cookie();
|
|
|
|
// Clear cached login_user/user/site
|
|
self::$_login_user = null;
|
|
self::$_user = null;
|
|
self::$_site = null;
|
|
|
|
// Update login user's last login timestamp
|
|
$login_user_record = self::get_login_user();
|
|
if ($login_user_record) {
|
|
$login_user_record->last_login = now();
|
|
$login_user_record->save();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set site for current session
|
|
* In CLI mode: sets static CLI property only, no database
|
|
* @param Site_Model|int $site Site model or site ID
|
|
* @return void
|
|
*/
|
|
/**
|
|
* Set site ID
|
|
* In CLI mode: sets static CLI property only
|
|
* @param int $site_id
|
|
* @return void
|
|
*/
|
|
public static function set_site_id(int $site_id): void
|
|
{
|
|
// CLI mode: set static property only
|
|
if (self::__is_cli()) {
|
|
self::$_cli_site_id = $site_id;
|
|
self::$_site = null;
|
|
|
|
return;
|
|
}
|
|
|
|
self::__activate();
|
|
|
|
// Skip if already set
|
|
if (self::get_site_id() === $site_id) {
|
|
return;
|
|
}
|
|
|
|
self::$_session->site_id = $site_id;
|
|
self::$_session->version++;
|
|
self::$_session->save();
|
|
|
|
// Clear cached site
|
|
self::$_site = null;
|
|
}
|
|
|
|
/**
|
|
* Check if a session exists
|
|
* In CLI mode: returns true if site_id or user_id is set
|
|
* In web mode: returns true if session record exists
|
|
* @return bool
|
|
*/
|
|
public static function has_session(): bool
|
|
{
|
|
// CLI mode: check if site_id or user_id is set
|
|
if (self::__is_cli()) {
|
|
return self::$_cli_site_id !== null || self::$_cli_user_id !== null;
|
|
}
|
|
|
|
// Web mode: init and check if session exists
|
|
self::init();
|
|
|
|
return !empty(self::$_session);
|
|
}
|
|
|
|
/**
|
|
* Get session by token (for API/external access)
|
|
* @param string $token
|
|
* @return Session|null
|
|
*/
|
|
public static function find_by_token(string $token)
|
|
{
|
|
return static::where('session_token', $token)
|
|
->where('active', true)
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* Clean up expired sessions (garbage collection)
|
|
* @param int $days_until_expiry
|
|
* @return int Number of sessions deleted
|
|
*/
|
|
public static function cleanup_expired(int $days_until_expiry = 365): int
|
|
{
|
|
return static::where('last_active', '<', now()->subDays($days_until_expiry))
|
|
->delete();
|
|
}
|
|
|
|
/**
|
|
* Override save to increment version on updates
|
|
* @param array $options
|
|
* @return bool
|
|
*/
|
|
public function save(array $options = []): bool
|
|
{
|
|
// Increment version on updates (but don't check for conflicts since sessions
|
|
// are single-user and shouldn't have real concurrent modifications)
|
|
if ($this->exists && $this->isDirty()) {
|
|
$this->version = ($this->version ?? 1) + 1;
|
|
}
|
|
|
|
return parent::save($options);
|
|
}
|
|
|
|
/**
|
|
* Relationship: Login User (authentication identity)
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
*/
|
|
public function login_user()
|
|
{
|
|
return $this->belongsTo(Login_User_Model::class, 'login_user_id');
|
|
}
|
|
|
|
/**
|
|
* Relationship: Site
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
*/
|
|
public function site()
|
|
{
|
|
return $this->belongsTo(Site_Model::class, 'site_id');
|
|
}
|
|
|
|
/**
|
|
* Relationship: Site-specific User (composite key relationship)
|
|
* Returns the User_Model that matches both login_user_id and site_id
|
|
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
|
*/
|
|
public function user()
|
|
{
|
|
return $this->hasOne(User_Model::class, 'login_user_id', 'login_user_id')
|
|
->where('users.site_id', '=', $this->site_id);
|
|
}
|
|
|
|
/**
|
|
* Set experience context for current request
|
|
* Determines which authentication realm we're in (default=0, staff=1, customer=2, etc.)
|
|
* Does NOT persist to database - request-scoped only
|
|
* Clears cached session/user/site since experience changed
|
|
* In CLI mode: sets static CLI property
|
|
* @param int $experience_id
|
|
* @return void
|
|
*/
|
|
public static function set_experience_id(int $experience_id): void
|
|
{
|
|
if (self::__is_cli()) {
|
|
self::$_cli_experience_id = $experience_id;
|
|
|
|
return;
|
|
}
|
|
|
|
self::$_experience_id = $experience_id;
|
|
|
|
// Clear cached data since experience changed
|
|
self::$_user = null;
|
|
self::$_site = null;
|
|
self::$_session = null;
|
|
|
|
// Force re-init with new experience context
|
|
self::$_has_init = false;
|
|
self::init();
|
|
}
|
|
|
|
/**
|
|
* Get current experience ID
|
|
* In CLI mode: returns static CLI property
|
|
* @return int
|
|
*/
|
|
public static function get_experience_id(): int
|
|
{
|
|
if (self::__is_cli()) {
|
|
return self::$_cli_experience_id;
|
|
}
|
|
|
|
return self::$_experience_id;
|
|
}
|
|
|
|
/**
|
|
* Set site_id override for current request (subdomain enforcement)
|
|
* This overrides the database session site_id for THIS REQUEST ONLY
|
|
* Does NOT modify the session record
|
|
* Use case: User visits subdomain assigned to a tenant, enforce that tenant
|
|
* @param int $site_id
|
|
* @return void
|
|
*/
|
|
public static function set_request_site_id_override(int $site_id): void
|
|
{
|
|
self::$_request_site_id_override = $site_id;
|
|
self::$_site = null; // Clear cached site
|
|
}
|
|
|
|
/**
|
|
* Clear site_id override (return to normal session-based site_id)
|
|
* @return void
|
|
*/
|
|
public static function clear_request_site_id_override(): void
|
|
{
|
|
self::$_request_site_id_override = null;
|
|
self::$_site = null; // Clear cached site
|
|
}
|
|
|
|
/**
|
|
* Check if request has site_id override active
|
|
* @return bool
|
|
*/
|
|
public static function has_request_site_id_override(): bool
|
|
{
|
|
return self::$_request_site_id_override !== null;
|
|
}
|
|
|
|
// =========================================================================
|
|
// SESSION MANAGEMENT METHODS
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Get all active sessions for a login user
|
|
* Returns formatted session info including device/location parsing
|
|
*
|
|
* @param int|null $login_user_id If null, uses current logged-in user
|
|
* @return array Array of session info
|
|
*/
|
|
public static function get_sessions_for_user(?int $login_user_id = null): array
|
|
{
|
|
if ($login_user_id === null) {
|
|
$login_user_id = self::get_login_user_id();
|
|
}
|
|
|
|
if (empty($login_user_id)) {
|
|
return [];
|
|
}
|
|
|
|
$current_session_id = self::has_session() ? self::$_session?->id : null;
|
|
|
|
return static::where('login_user_id', $login_user_id)
|
|
->where('active', true)
|
|
->orderBy('last_active', 'desc')
|
|
->get()
|
|
->map(function ($session) use ($current_session_id) {
|
|
$parsed_ua = User_Agent::parse($session->user_agent);
|
|
|
|
return [
|
|
'id' => $session->id,
|
|
'ip_address' => $session->ip_address,
|
|
'user_agent' => $session->user_agent,
|
|
'user_agent_parsed' => $parsed_ua,
|
|
'device_summary' => $parsed_ua['summary'],
|
|
'location' => self::_format_session_location($session),
|
|
'last_active' => $session->last_active,
|
|
'created_at' => $session->created_at,
|
|
'is_current' => $session->id === $current_session_id,
|
|
];
|
|
})
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* Terminate a specific session by ID
|
|
* Cannot terminate the current session (use logout() instead)
|
|
*
|
|
* @param int $session_id
|
|
* @return bool True if session was terminated, false if not found or is current
|
|
*/
|
|
public static function terminate_session(int $session_id): bool
|
|
{
|
|
self::init();
|
|
|
|
// Don't allow terminating current session
|
|
if (self::$_session && self::$_session->id === $session_id) {
|
|
return false;
|
|
}
|
|
|
|
$affected = static::where('id', $session_id)
|
|
->where('active', true)
|
|
->update(['active' => false]);
|
|
|
|
return $affected > 0;
|
|
}
|
|
|
|
/**
|
|
* Terminate all sessions for the current user except the current one
|
|
*
|
|
* @return int Number of sessions terminated
|
|
*/
|
|
public static function terminate_all_other_sessions(): int
|
|
{
|
|
self::init();
|
|
|
|
$login_user_id = self::get_login_user_id();
|
|
if (empty($login_user_id)) {
|
|
return 0;
|
|
}
|
|
|
|
$query = static::where('login_user_id', $login_user_id)
|
|
->where('active', true);
|
|
|
|
// Exclude current session if we have one
|
|
if (self::$_session) {
|
|
$query->where('id', '!=', self::$_session->id);
|
|
}
|
|
|
|
return $query->update(['active' => false]);
|
|
}
|
|
|
|
/**
|
|
* Terminate all sessions for a specific login user
|
|
* Useful for admin actions or password changes
|
|
*
|
|
* @param int $login_user_id
|
|
* @param int|null $except_session_id Optional session ID to exclude
|
|
* @return int Number of sessions terminated
|
|
*/
|
|
public static function terminate_all_sessions_for_user(int $login_user_id, ?int $except_session_id = null): int
|
|
{
|
|
$query = static::where('login_user_id', $login_user_id)
|
|
->where('active', true);
|
|
|
|
if ($except_session_id !== null) {
|
|
$query->where('id', '!=', $except_session_id);
|
|
}
|
|
|
|
return $query->update(['active' => false]);
|
|
}
|
|
|
|
/**
|
|
* Get information about the current session
|
|
* Returns formatted info with device/location parsing
|
|
*
|
|
* @return array|null Session info or null if not logged in
|
|
*/
|
|
public static function get_current_session_info(): ?array
|
|
{
|
|
self::init();
|
|
|
|
if (empty(self::$_session)) {
|
|
return null;
|
|
}
|
|
|
|
$parsed_ua = User_Agent::parse(self::$_session->user_agent);
|
|
|
|
return [
|
|
'id' => self::$_session->id,
|
|
'ip_address' => self::$_session->ip_address,
|
|
'user_agent' => self::$_session->user_agent,
|
|
'user_agent_parsed' => $parsed_ua,
|
|
'device_summary' => $parsed_ua['summary'],
|
|
'location' => self::_format_session_location(self::$_session),
|
|
'last_active' => self::$_session->last_active,
|
|
'created_at' => self::$_session->created_at,
|
|
'is_current' => true,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Format location string from session (placeholder for future geo lookup)
|
|
*
|
|
* @param Session $session
|
|
* @return string|null
|
|
*/
|
|
private static function _format_session_location($session): ?string
|
|
{
|
|
// Future: Implement geo lookup based on IP
|
|
// For now, return null (location not available)
|
|
return null;
|
|
}
|
|
}
|