Merge pull request #5963 from christianbeeznest/GH-5959
Internal: Add token validation system initial implementation - refs #5959pull/5987/head
commit
e717c78f87
@ -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); |
||||
} |
||||
} |
@ -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'); |
||||
} |
||||
} |
||||
} |
@ -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…
Reference in new issue