'array', 'allowed_ips' => 'array', 'last_used_at' => 'datetime', 'expires_at' => 'datetime', 'revoked_at' => 'datetime', ]; protected array $hidden = [ 'token', ]; public function tokenable(): MorphTo { return $this->morphTo(); } public static function findByPlainToken(string $plain): ?self { return static::query()->where('token', hash('sha256', $plain))->first(); } public function isExpired(): bool { return $this->expires_at !== null && $this->expires_at->isPast(); } public function isRevoked(): bool { return $this->revoked_at !== null; } public function isActive(): bool { return ! $this->isExpired() && ! $this->isRevoked(); } public function can(string $ability): bool { $abilities = $this->abilities ?? []; if (in_array(TokenAbilities::WILDCARD, $abilities, true)) { return true; } return in_array($ability, $abilities, true); } public function ipAllowed(string $ip): bool { $list = $this->allowed_ips ?? []; if (empty($list)) { return true; } foreach ($list as $entry) { if ($this->ipMatches($ip, $entry)) { return true; } } return false; } private function ipMatches(string $ip, string $entry): bool { $entry = trim($entry); if ($entry === '') { return false; } if (! str_contains($entry, '/')) { return $ip === $entry; } [$subnet, $bits] = explode('/', $entry, 2); $bits = (int) $bits; $ipBin = @inet_pton($ip); $subnetBin = @inet_pton($subnet); if ($ipBin === false || $subnetBin === false || strlen($ipBin) !== strlen($subnetBin)) { return false; } $bytes = intdiv($bits, 8); $remainder = $bits % 8; if (substr($ipBin, 0, $bytes) !== substr($subnetBin, 0, $bytes)) { return false; } if ($remainder === 0) { return true; } $mask = chr(0xff << (8 - $remainder) & 0xff); return (ord($ipBin[$bytes]) & ord($mask)) === (ord($subnetBin[$bytes]) & ord($mask)); } }