Merge pull request #5963 from christianbeeznest/GH-5959

Internal: Add token validation system initial implementation - refs #5959
pull/5987/head
christianbeeznest 9 months ago committed by GitHub
commit e717c78f87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 119
      src/CoreBundle/Controller/ValidationTokenController.php
  2. 4
      src/CoreBundle/Entity/TrackEDefault.php
  3. 96
      src/CoreBundle/Entity/ValidationToken.php
  4. 42
      src/CoreBundle/Migrations/Schema/V200/Version20241211183300.php
  5. 19
      src/CoreBundle/Repository/TrackEDefaultRepository.php
  6. 37
      src/CoreBundle/Repository/ValidationTokenRepository.php
  7. 6
      src/CoreBundle/Resources/views/Validation/success.html.twig
  8. 48
      src/CoreBundle/ServiceHelper/ValidationTokenHelper.php

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Entity\ValidationToken;
use Chamilo\CoreBundle\Repository\TrackEDefaultRepository;
use Chamilo\CoreBundle\Repository\ValidationTokenRepository;
use Chamilo\CoreBundle\ServiceHelper\ValidationTokenHelper;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
#[Route('/validate')]
class ValidationTokenController extends AbstractController
{
public function __construct(
private readonly ValidationTokenHelper $validationTokenHelper,
private readonly ValidationTokenRepository $tokenRepository,
private readonly TrackEDefaultRepository $trackEDefaultRepository,
private readonly Security $security
) {}
#[Route('/{type}/{hash}', name: 'validate_token')]
public function validate(string $type, string $hash): Response
{
$token = $this->tokenRepository->findOneBy([
'type' => $this->validationTokenHelper->getTypeId($type),
'hash' => $hash
]);
if (!$token) {
throw $this->createNotFoundException('Invalid token.');
}
// Process the action related to the token type
$this->processAction($token);
// Remove the used token
$this->tokenRepository->remove($token, true);
// Register the token usage event
$this->registerTokenUsedEvent($token);
return $this->render('@ChamiloCore/Validation/success.html.twig', [
'type' => $type,
]);
}
#[Route('/test/generate-token/{type}/{resourceId}', name: 'test_generate_token')]
public function testGenerateToken(string $type, int $resourceId): Response
{
$typeId = $this->validationTokenHelper->getTypeId($type);
$token = new ValidationToken($typeId, $resourceId);
$this->tokenRepository->save($token, true);
$validationLink = $this->generateUrl('validate_token', [
'type' => $type,
'hash' => $token->getHash(),
], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
return new Response("Generated token: {$token->getHash()}<br>Validation link: <a href='{$validationLink}'>{$validationLink}</a>");
}
private function processAction(ValidationToken $token): void
{
switch ($token->getType()) {
case 1: // Assuming 1 is for 'ticket'
$this->processTicketValidation($token);
break;
case 2: // Assuming 2 is for 'user'
// Implement user validation logic here
break;
default:
throw new \InvalidArgumentException('Unrecognized token type');
}
}
private function processTicketValidation(ValidationToken $token): void
{
$ticketId = $token->getResourceId();
// Simulate ticket validation logic
// Here you would typically check if the ticket exists and is valid
// For now, we'll just print a message to simulate this
// Replace this with your actual ticket validation logic
$ticketValid = $this->validateTicket($ticketId);
if (!$ticketValid) {
throw new \RuntimeException('Invalid ticket.');
}
// If the ticket is valid, you can mark it as used or perform other actions
// For example, update the ticket status in the database
// $this->ticketRepository->markAsUsed($ticketId);
}
private function validateTicket(int $ticketId): bool
{
// Here you would implement the logic to check if the ticket is valid.
// This is a placeholder function to simulate validation.
// For testing purposes, let's assume all tickets are valid.
// In a real implementation, you would query your database or service.
return true; // Assume the ticket is valid for now
}
private function registerTokenUsedEvent(ValidationToken $token): void
{
$user = $this->security->getUser();
$userId = $user?->getId();
$this->trackEDefaultRepository->registerTokenUsedEvent($token, $userId);
}
}

@ -68,7 +68,7 @@ class TrackEDefault
return $this->defaultUserId;
}
public function setCId(int $cId): self
public function setCId(?int $cId): self
{
$this->cId = $cId;
@ -153,7 +153,7 @@ class TrackEDefault
return $this->defaultValue;
}
public function setSessionId(int $sessionId): self
public function setSessionId(?int $sessionId): self
{
$this->sessionId = $sessionId;

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Entity;
use Chamilo\CoreBundle\Repository\ValidationTokenRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* ValidationToken entity.
*/
#[ORM\Table(name: 'validation_token')]
#[ORM\Index(columns: ['type', 'hash'], name: 'idx_type_hash')]
#[ORM\Entity(repositoryClass: ValidationTokenRepository::class)]
class ValidationToken
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(type: 'integer')]
protected ?int $id = null;
#[ORM\Column(type: 'integer')]
protected int $type;
#[ORM\Column(type: 'bigint')]
protected int $resourceId;
#[ORM\Column(type: 'string', length: 64)]
protected string $hash;
#[ORM\Column(type: 'datetime')]
protected \DateTime $createdAt;
public function __construct(int $type, int $resourceId)
{
$this->type = $type;
$this->resourceId = $resourceId;
$this->hash = hash('sha256', uniqid((string) rand(), true));
$this->createdAt = new \DateTime();
}
public function getId(): ?int
{
return $this->id;
}
public function getType(): int
{
return $this->type;
}
public function setType(int $type): self
{
$this->type = $type;
return $this;
}
public function getResourceId(): int
{
return $this->resourceId;
}
public function setResourceId(int $resourceId): self
{
$this->resourceId = $resourceId;
return $this;
}
public function getHash(): string
{
return $this->hash;
}
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
public function setCreatedAt(\DateTime $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Generates a validation link.
*/
public static function generateLink(int $type, int $resourceId): string
{
$token = new self($type, $resourceId);
return '/validate/' . $type . '/' . $token->getHash();
}
}

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
final class Version20241211183300 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Migration for creating the validation_token table';
}
public function up(Schema $schema): void
{
if (!$schema->hasTable('validation_token')) {
$this->addSql("
CREATE TABLE validation_token (
id INT AUTO_INCREMENT NOT NULL,
type INT NOT NULL,
resource_id BIGINT NOT NULL,
hash CHAR(64) NOT NULL,
created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime)',
INDEX idx_type_hash (type, hash),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB ROW_FORMAT = DYNAMIC
");
}
}
public function down(Schema $schema): void
{
if ($schema->hasTable('validation_token')) {
$this->addSql('DROP TABLE validation_token');
}
}
}

@ -7,6 +7,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\TrackEDefault;
use Chamilo\CoreBundle\Entity\ValidationToken;
use DateTime;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
@ -65,4 +66,22 @@ class TrackEDefaultRepository extends ServiceEntityRepository
return null;
}
/**
* Registers an event when a validation token is used.
*/
public function registerTokenUsedEvent(ValidationToken $token, ?int $userId = null): void
{
$event = new TrackEDefault();
$event->setDefaultUserId($userId ?? 0);
$event->setCId(null);
$event->setDefaultDate(new \DateTime());
$event->setDefaultEventType('VALIDATION_TOKEN_USED');
$event->setDefaultValueType('validation_token');
$event->setDefaultValue(\json_encode(['hash' => $token->getHash()]));
$event->setSessionId(null);
$this->_em->persist($event);
$this->_em->flush();
}
}

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\ValidationToken;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class ValidationTokenRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ValidationToken::class);
}
public function save(ValidationToken $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(ValidationToken $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
}

@ -0,0 +1,6 @@
{% extends "@ChamiloCore/Layout/layout_one_col.html.twig" %}
{% block content %}
<h1>Validation Successful</h1>
<p>The token for {{ type }} has been successfully validated.</p>
{% endblock %}

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\ServiceHelper;
use Chamilo\CoreBundle\Entity\ValidationToken;
use Chamilo\CoreBundle\Repository\ValidationTokenRepository;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class ValidationTokenHelper
{
public function __construct(
private readonly ValidationTokenRepository $tokenRepository,
private readonly UrlGeneratorInterface $urlGenerator,
) {}
public function generateLink(int $type, int $resourceId): string
{
$token = new ValidationToken($type, $resourceId);
$this->tokenRepository->save($token, true);
return $this->urlGenerator->generate('validate_token', [
'type' => $this->getTypeString($type),
'hash' => $token->getHash(),
], UrlGeneratorInterface::ABSOLUTE_URL);
}
public function getTypeId(string $type): int
{
return match ($type) {
'ticket' => 1,
'user' => 2,
default => throw new \InvalidArgumentException('Unrecognized validation type'),
};
}
private function getTypeString(int $type): string
{
return match ($type) {
1 => 'ticket',
2 => 'user',
default => throw new \InvalidArgumentException('Unrecognized validation type'),
};
}
}
Loading…
Cancel
Save