Harden published curriculum and student flows

This commit is contained in:
Hamza-Ayed
2026-09-30 08:22:17 +03:00
parent e8163fe265
commit ce7b0fcf14
53 changed files with 4723 additions and 3317 deletions
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Core\RedisClient;
/** Short-lived, asset-scoped browser grant. Never place a session JWT in a URL. */
final class AssetDownloadTicketService
{
private const TTL_SECONDS = 120;
public static function issue(int $studentId, string $assetUuid): array
{
if ($studentId <= 0 || $assetUuid === '') throw new \InvalidArgumentException('Invalid download scope');
$ticket = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
$key = 'asset_download:' . hash('sha256', $ticket);
$saved = RedisClient::getInstance()->setex($key, self::TTL_SECONDS, json_encode([
'student_id' => $studentId,
'asset_uuid' => $assetUuid,
], JSON_THROW_ON_ERROR));
if (!$saved) throw new \RuntimeException('Download grant unavailable');
return ['ticket' => $ticket, 'expires_in_seconds' => self::TTL_SECONDS];
}
public static function resolve(string $ticket, string $assetUuid): ?int
{
if (!preg_match('/^[A-Za-z0-9_-]{43}$/', $ticket)) return null;
$payload = RedisClient::getInstance()->get('asset_download:' . hash('sha256', $ticket));
if (!is_string($payload)) return null;
$grant = json_decode($payload, true);
if (!is_array($grant) || !hash_equals((string)($grant['asset_uuid'] ?? ''), $assetUuid)) return null;
$studentId = (int)($grant['student_id'] ?? 0);
return $studentId > 0 ? $studentId : null;
}
}