src/Products/NotificationsBundle/Entity/PortalLoginAttempt.php line 20

Open in your IDE?
  1. <?php
  2. namespace Products\NotificationsBundle\Entity;
  3. use Cms\CoreBundle\Util\DateTimeUtils;
  4. use Cms\TenantBundle\Entity\TenantedEntity;
  5. use Doctrine\ORM\Mapping as ORM;
  6. /**
  7.  * Class PortalLoginAttempt
  8.  * @package Products\NotificationsBundle\Entity
  9.  *
  10.  * @ORM\Entity(
  11.  *     repositoryClass = "Products\NotificationsBundle\Doctrine\Repository\PortalLoginAttemptRepository",
  12.  * )
  13.  * @ORM\Table(
  14.  *     name = "notis__portal_login_attempt",
  15.  * )
  16.  */
  17. class PortalLoginAttempt extends TenantedEntity
  18. {
  19.     const MAX_TRIES 5;
  20.     const EXPIRATION 10;// in minutes
  21.     /**
  22.      * Always log the incoming input.
  23.      * Even if a profile/recipient cannot be matched, we need to log all attempts from an IP in order to better block hacking attempts.
  24.      * NOTE: while PHP allows for NULLs, the ORM config should NOT be nullable.
  25.      *
  26.      * @var string|null
  27.      *
  28.      * @ORM\Column(
  29.      *     type = "string",
  30.      *     nullable = true,
  31.      * )
  32.      */
  33.     protected ?string $input null;
  34.     /**
  35.      * Always log the IP of the client trying to submit the request.
  36.      * We will use this for rate-limiting.
  37.      * NOTE: while PHP allows for NULLs, the ORM config should NOT be nullable.
  38.      *
  39.      * @var string|null
  40.      *
  41.      * @ORM\Column(
  42.      *     type = "string",
  43.      *     nullable = true,
  44.      * )
  45.      */
  46.     protected ?string $ipAddress null;
  47.     /**
  48.      * During the login process, if a recipient can be matched to the input, track it here.
  49.      * This is nullable because not all inputs will be matched to a db record.
  50.      *
  51.      * @var AbstractRecipient|null
  52.      *
  53.      * @ORM\ManyToOne(
  54.      *     targetEntity = "Products\NotificationsBundle\Entity\AbstractRecipient",
  55.      * )
  56.      * @ORM\JoinColumn(
  57.      *     name = "recipient",
  58.      *     referencedColumnName = "id",
  59.      *     nullable = true,
  60.      *     onDelete = "SET NULL",
  61.      * )
  62.      */
  63.     protected ?AbstractRecipient $recipient null;
  64.     /**
  65.      * While we can get to the profile from the recipient, also track the profile here for quicker access.
  66.      * Also, future changes may result in needing this anyway (like removing the recipient entities and just throwing the phone numbers on the profiles themselves).
  67.      *
  68.      * @var Profile|null
  69.      *
  70.      * @ORM\ManyToOne(
  71.      *     targetEntity = "Products\NotificationsBundle\Entity\Profile",
  72.      * )
  73.      * @ORM\JoinColumn(
  74.      *     name = "profile",
  75.      *     referencedColumnName = "id",
  76.      *     nullable = true,
  77.      *     onDelete = "SET NULL",
  78.      * )
  79.      */
  80.     protected ?Profile $profile null;
  81.     /**
  82.      * The randomized code that the user will need to input again to verify ownership of the account.
  83.      * While we are generating INTs now, store as a string in case we need to do more complex codes in the future.
  84.      *
  85.      * @var string|null
  86.      *
  87.      * @ORM\Column(
  88.      *     type = "string",
  89.      *     nullable = true,
  90.      * )
  91.      */
  92.     protected ?string $code null;
  93.     /**
  94.      * Tracks the ID from the remote system that sent the email, SMS, or call to the person with the code details.
  95.      * If that failed, this will be NULL.
  96.      *
  97.      * @var string|null
  98.      *
  99.      * @ORM\Column(
  100.      *     type = "string",
  101.      *     nullable = true,
  102.      * )
  103.      */
  104.     protected ?string $remoteMessageId null;
  105.     /**
  106.      * Increment this every time a person actually attempts a login with the code.
  107.      * At a certain point after some number of attempts we are going to block access.
  108.      * Realistically a human should not enter this code wrong more than a handful of times.
  109.      *
  110.      * @var int
  111.      *
  112.      * @ORM\Column(
  113.      *     type = "integer",
  114.      *     nullable = false,
  115.      * )
  116.      */
  117.     protected int $attempts 0;
  118.     /**
  119.      * @var bool
  120.      *
  121.      * @ORM\Column(
  122.      *     type = "boolean",
  123.      *     nullable = false,
  124.      *     options = {
  125.      *         "default" = false,
  126.      *     },
  127.      * )
  128.      */
  129.     protected bool $used false;
  130.     /**
  131.      * @return string|null
  132.      */
  133.     public function getInput(): ?string
  134.     {
  135.         return $this->input;
  136.     }
  137.     /**
  138.      * @param string|null $input
  139.      * @return $this
  140.      */
  141.     public function setInput(?string $input): self
  142.     {
  143.         $this->input $input;
  144.         return $this;
  145.     }
  146.     /**
  147.      * @return string|null
  148.      */
  149.     public function getIpAddress(): ?string
  150.     {
  151.         return $this->ipAddress;
  152.     }
  153.     /**
  154.      * @param string|null $ipAddress
  155.      * @return $this
  156.      */
  157.     public function setIpAddress(?string $ipAddress): self
  158.     {
  159.         $this->ipAddress $ipAddress;
  160.         return $this;
  161.     }
  162.     /**
  163.      * @return AbstractRecipient|null
  164.      */
  165.     public function getRecipient(): ?AbstractRecipient
  166.     {
  167.         return $this->recipient;
  168.     }
  169.     /**
  170.      * @param AbstractRecipient|null $recipient
  171.      * @return $this
  172.      */
  173.     public function setRecipient(?AbstractRecipient $recipient): self
  174.     {
  175.         $this->recipient $recipient;
  176.         return $this;
  177.     }
  178.     /**
  179.      * @return Profile|null
  180.      */
  181.     public function getProfile(): ?Profile
  182.     {
  183.         return $this->profile;
  184.     }
  185.     /**
  186.      * @param Profile|null $profile
  187.      * @return $this
  188.      */
  189.     public function setProfile(?Profile $profile): self
  190.     {
  191.         $this->profile $profile;
  192.         return $this;
  193.     }
  194.     /**
  195.      * @return string|null
  196.      */
  197.     public function getCode(): ?string
  198.     {
  199.         return $this->code;
  200.     }
  201.     /**
  202.      * @param string|null $code
  203.      * @return $this
  204.      */
  205.     public function setCode(?string $code): self
  206.     {
  207.         $this->code $code;
  208.         return $this;
  209.     }
  210.     /**
  211.      * @return string|null
  212.      */
  213.     public function getRemoteMessageId(): ?string
  214.     {
  215.         return $this->remoteMessageId;
  216.     }
  217.     /**
  218.      * @param string|null $remoteMessageId
  219.      * @return $this
  220.      */
  221.     public function setRemoteMessageId(?string $remoteMessageId): self
  222.     {
  223.         $this->remoteMessageId $remoteMessageId;
  224.         return $this;
  225.     }
  226.     /**
  227.      * @return int
  228.      */
  229.     public function getAttempts(): int
  230.     {
  231.         return $this->attempts;
  232.     }
  233.     /**
  234.      * @param int $attempts
  235.      * @return $this
  236.      */
  237.     public function setAttempts(int $attempts): self
  238.     {
  239.         $this->attempts $attempts;
  240.         return $this;
  241.     }
  242.     /**
  243.      * @return bool
  244.      */
  245.     public function isUsed(): bool
  246.     {
  247.         return $this->used;
  248.     }
  249.     /**
  250.      * @param bool $used
  251.      * @return $this
  252.      */
  253.     public function setUsed(bool $used): self
  254.     {
  255.         // if we are already used and trying to set as unused, that's a problem
  256.         if ($this->used && ( ! $used)) {
  257.             throw new \Exception();
  258.         }
  259.         $this->used $used;
  260.         return $this;
  261.     }
  262.     /**
  263.      * @return int
  264.      */
  265.     public function getExpiredMinutes(): int
  266.     {
  267.         return DateTimeUtils::diffMinutes(
  268.             $this->getCreatedAt(),
  269.             DateTimeUtils::now()
  270.         );
  271.     }
  272.     /**
  273.      * @return int
  274.      */
  275.     public function getExpiredMinutesLeft(): int
  276.     {
  277.         return max(0self::EXPIRATION DateTimeUtils::diffMinutes(
  278.             $this->getCreatedAt(),
  279.             DateTimeUtils::now()
  280.         ));
  281.     }
  282.     /**
  283.      * @return bool
  284.      */
  285.     public function isExpired(): bool
  286.     {
  287.         return ($this->getExpiredMinutes() >= self::EXPIRATION);
  288.     }
  289.     /**
  290.      * @return bool
  291.      */
  292.     public function hasAttemptsLeft(): bool
  293.     {
  294.         return $this->getAttempts() < self::MAX_TRIES;
  295.     }
  296.     /**
  297.      * @param bool $success
  298.      * @return $this
  299.      */
  300.     public function useAttempt(bool $success): self
  301.     {
  302.         return $this
  303.             ->setAttempts($this->getAttempts() + (( ! $success) ? 0))
  304.             ->setUsed($success);
  305.     }
  306. }