<?php
namespace Products\NotificationsBundle\Entity;
use Cms\CoreBundle\Util\DateTimeUtils;
use Cms\TenantBundle\Entity\TenantedEntity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class PortalLoginAttempt
* @package Products\NotificationsBundle\Entity
*
* @ORM\Entity(
* repositoryClass = "Products\NotificationsBundle\Doctrine\Repository\PortalLoginAttemptRepository",
* )
* @ORM\Table(
* name = "notis__portal_login_attempt",
* )
*/
class PortalLoginAttempt extends TenantedEntity
{
const MAX_TRIES = 5;
const EXPIRATION = 10;// in minutes
/**
* Always log the incoming input.
* Even if a profile/recipient cannot be matched, we need to log all attempts from an IP in order to better block hacking attempts.
* NOTE: while PHP allows for NULLs, the ORM config should NOT be nullable.
*
* @var string|null
*
* @ORM\Column(
* type = "string",
* nullable = true,
* )
*/
protected ?string $input = null;
/**
* Always log the IP of the client trying to submit the request.
* We will use this for rate-limiting.
* NOTE: while PHP allows for NULLs, the ORM config should NOT be nullable.
*
* @var string|null
*
* @ORM\Column(
* type = "string",
* nullable = true,
* )
*/
protected ?string $ipAddress = null;
/**
* During the login process, if a recipient can be matched to the input, track it here.
* This is nullable because not all inputs will be matched to a db record.
*
* @var AbstractRecipient|null
*
* @ORM\ManyToOne(
* targetEntity = "Products\NotificationsBundle\Entity\AbstractRecipient",
* )
* @ORM\JoinColumn(
* name = "recipient",
* referencedColumnName = "id",
* nullable = true,
* onDelete = "SET NULL",
* )
*/
protected ?AbstractRecipient $recipient = null;
/**
* While we can get to the profile from the recipient, also track the profile here for quicker access.
* Also, future changes may result in needing this anyway (like removing the recipient entities and just throwing the phone numbers on the profiles themselves).
*
* @var Profile|null
*
* @ORM\ManyToOne(
* targetEntity = "Products\NotificationsBundle\Entity\Profile",
* )
* @ORM\JoinColumn(
* name = "profile",
* referencedColumnName = "id",
* nullable = true,
* onDelete = "SET NULL",
* )
*/
protected ?Profile $profile = null;
/**
* The randomized code that the user will need to input again to verify ownership of the account.
* While we are generating INTs now, store as a string in case we need to do more complex codes in the future.
*
* @var string|null
*
* @ORM\Column(
* type = "string",
* nullable = true,
* )
*/
protected ?string $code = null;
/**
* Tracks the ID from the remote system that sent the email, SMS, or call to the person with the code details.
* If that failed, this will be NULL.
*
* @var string|null
*
* @ORM\Column(
* type = "string",
* nullable = true,
* )
*/
protected ?string $remoteMessageId = null;
/**
* Increment this every time a person actually attempts a login with the code.
* At a certain point after some number of attempts we are going to block access.
* Realistically a human should not enter this code wrong more than a handful of times.
*
* @var int
*
* @ORM\Column(
* type = "integer",
* nullable = false,
* )
*/
protected int $attempts = 0;
/**
* @var bool
*
* @ORM\Column(
* type = "boolean",
* nullable = false,
* options = {
* "default" = false,
* },
* )
*/
protected bool $used = false;
/**
* @return string|null
*/
public function getInput(): ?string
{
return $this->input;
}
/**
* @param string|null $input
* @return $this
*/
public function setInput(?string $input): self
{
$this->input = $input;
return $this;
}
/**
* @return string|null
*/
public function getIpAddress(): ?string
{
return $this->ipAddress;
}
/**
* @param string|null $ipAddress
* @return $this
*/
public function setIpAddress(?string $ipAddress): self
{
$this->ipAddress = $ipAddress;
return $this;
}
/**
* @return AbstractRecipient|null
*/
public function getRecipient(): ?AbstractRecipient
{
return $this->recipient;
}
/**
* @param AbstractRecipient|null $recipient
* @return $this
*/
public function setRecipient(?AbstractRecipient $recipient): self
{
$this->recipient = $recipient;
return $this;
}
/**
* @return Profile|null
*/
public function getProfile(): ?Profile
{
return $this->profile;
}
/**
* @param Profile|null $profile
* @return $this
*/
public function setProfile(?Profile $profile): self
{
$this->profile = $profile;
return $this;
}
/**
* @return string|null
*/
public function getCode(): ?string
{
return $this->code;
}
/**
* @param string|null $code
* @return $this
*/
public function setCode(?string $code): self
{
$this->code = $code;
return $this;
}
/**
* @return string|null
*/
public function getRemoteMessageId(): ?string
{
return $this->remoteMessageId;
}
/**
* @param string|null $remoteMessageId
* @return $this
*/
public function setRemoteMessageId(?string $remoteMessageId): self
{
$this->remoteMessageId = $remoteMessageId;
return $this;
}
/**
* @return int
*/
public function getAttempts(): int
{
return $this->attempts;
}
/**
* @param int $attempts
* @return $this
*/
public function setAttempts(int $attempts): self
{
$this->attempts = $attempts;
return $this;
}
/**
* @return bool
*/
public function isUsed(): bool
{
return $this->used;
}
/**
* @param bool $used
* @return $this
*/
public function setUsed(bool $used): self
{
// if we are already used and trying to set as unused, that's a problem
if ($this->used && ( ! $used)) {
throw new \Exception();
}
$this->used = $used;
return $this;
}
/**
* @return int
*/
public function getExpiredMinutes(): int
{
return DateTimeUtils::diffMinutes(
$this->getCreatedAt(),
DateTimeUtils::now()
);
}
/**
* @return int
*/
public function getExpiredMinutesLeft(): int
{
return max(0, self::EXPIRATION - DateTimeUtils::diffMinutes(
$this->getCreatedAt(),
DateTimeUtils::now()
));
}
/**
* @return bool
*/
public function isExpired(): bool
{
return ($this->getExpiredMinutes() >= self::EXPIRATION);
}
/**
* @return bool
*/
public function hasAttemptsLeft(): bool
{
return $this->getAttempts() < self::MAX_TRIES;
}
/**
* @param bool $success
* @return $this
*/
public function useAttempt(bool $success): self
{
return $this
->setAttempts($this->getAttempts() + (( ! $success) ? 1 : 0))
->setUsed($success);
}
}