feat: Provisionner automatiquement un nouvel établissement
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

Lorsqu'un super-admin crée un établissement via l'interface, le système
doit automatiquement créer la base tenant, exécuter les migrations,
créer le premier utilisateur admin et envoyer l'invitation — le tout
de manière asynchrone pour ne pas bloquer la réponse HTTP.

Ce mécanisme rend chaque établissement opérationnel dès sa création
sans intervention manuelle sur l'infrastructure.
This commit is contained in:
2026-04-08 13:55:41 +02:00
parent bec211ebf0
commit e72867932d
107 changed files with 9709 additions and 383 deletions

View File

@@ -6,6 +6,7 @@ namespace App\Scolarite\Infrastructure\Api\Controller;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Scolarite\Application\Port\ClassStudentsReader;
use App\Scolarite\Application\Port\FileStorage;
use App\Scolarite\Domain\Model\Homework\HomeworkId;
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmission;
use App\Scolarite\Domain\Model\HomeworkSubmission\HomeworkSubmissionId;
@@ -24,15 +25,15 @@ use function count;
use DateTimeImmutable;
use function fclose;
use function fpassthru;
use function in_array;
use function realpath;
use function str_starts_with;
use RuntimeException;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Attribute\Route;
@@ -45,8 +46,7 @@ final readonly class TeacherSubmissionController
private HomeworkSubmissionRepository $submissionRepository,
private SubmissionAttachmentRepository $attachmentRepository,
private ClassStudentsReader $classStudentsReader,
#[Autowire('%kernel.project_dir%/var/storage')]
private string $storageDir,
private FileStorage $fileStorage,
) {
}
@@ -240,7 +240,7 @@ final readonly class TeacherSubmissionController
}
#[Route('/api/homework/{homeworkId}/submissions/{submissionId}/attachments/{attachmentId}', name: 'api_teacher_submission_attachment_download', methods: ['GET'])]
public function downloadAttachment(string $homeworkId, string $submissionId, string $attachmentId): BinaryFileResponse
public function downloadAttachment(string $homeworkId, string $submissionId, string $attachmentId): StreamedResponse
{
$user = $this->getSecurityUser();
$tenantId = TenantId::fromString($user->tenantId());
@@ -268,20 +268,29 @@ final readonly class TeacherSubmissionController
foreach ($attachments as $attachment) {
if ((string) $attachment->id === $attachmentId) {
$fullPath = $this->storageDir . '/' . $attachment->filePath;
$realPath = realpath($fullPath);
$realStorageDir = realpath($this->storageDir);
if ($realPath === false || $realStorageDir === false || !str_starts_with($realPath, $realStorageDir)) {
try {
$stream = $this->fileStorage->readStream($attachment->filePath);
} catch (RuntimeException) {
throw new NotFoundHttpException('Pièce jointe non trouvée.');
}
$response = new BinaryFileResponse($realPath);
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_INLINE,
$response = new StreamedResponse(static function () use ($stream): void {
try {
fpassthru($stream);
} finally {
fclose($stream);
}
});
$disposition = HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_INLINE,
$attachment->filename,
);
$response->headers->set('Content-Type', $attachment->mimeType);
$response->headers->set('Content-Disposition', $disposition);
$response->headers->set('Content-Length', (string) $attachment->fileSize);
return $response;
}
}