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