Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Internal: Add token validation system initial implementation - refs #5959 #5963

Merged
merged 2 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions src/CoreBundle/Controller/ValidationTokenController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

declare(strict_types=1);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a single space around assignment operators


/* 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')]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perl-style comments are not allowed. Use "// Comment." or "/* comment */" instead.

class ValidationTokenController extends AbstractController
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You must use "/**" style comments for a class comment

{
public function __construct(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing function doc comment

private readonly ValidationTokenHelper $validationTokenHelper,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line indented incorrectly; expected 4 spaces, found 8

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);
}
}
4 changes: 2 additions & 2 deletions src/CoreBundle/Entity/TrackEDefault.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public function getDefaultUserId()
return $this->defaultUserId;
}

public function setCId(int $cId): self
public function setCId(?int $cId): self
{
$this->cId = $cId;

Expand Down Expand Up @@ -153,7 +153,7 @@ public function getDefaultValue()
return $this->defaultValue;
}

public function setSessionId(int $sessionId): self
public function setSessionId(?int $sessionId): self
{
$this->sessionId = $sessionId;

Expand Down
96 changes: 96 additions & 0 deletions src/CoreBundle/Entity/ValidationToken.php
Original file line number Diff line number Diff line change
@@ -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();
}
}
42 changes: 42 additions & 0 deletions src/CoreBundle/Migrations/Schema/V200/Version20241211183300.php
Original file line number Diff line number Diff line change
@@ -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');
}
}
}
19 changes: 19 additions & 0 deletions src/CoreBundle/Repository/TrackEDefaultRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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;
Expand Down Expand Up @@ -65,4 +66,22 @@ public function getUserCourseRegistrationAt(int $courseId, int $userId, ?int $se

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();
}
}
37 changes: 37 additions & 0 deletions src/CoreBundle/Repository/ValidationTokenRepository.php
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
6 changes: 6 additions & 0 deletions src/CoreBundle/Resources/views/Validation/success.html.twig
Original file line number Diff line number Diff line change
@@ -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 %}
Loading
Loading