refactor : simplify codebase and fix critical issues
Backend: - Add MCP Serializer to centralize entity-to-array conversion (~300 lines deduped) - Fix race condition in task/ticket number generation (SELECT FOR UPDATE + transaction) - Add unique constraint on task (project_id, number) with migration - Fix MIME type validation: use server-detected finfo instead of client-supplied type - Add allowlist of permitted MIME types for uploads - Fix TaskDocumentDownloadController: allow ROLE_CLIENT access, add priority:1 - Fix notification sent even when ticket status unchanged - Remove redundant exception constructors - Simplify services (BookStackApi double fetch, TokenEncryptor, GiteaApi) - Consolidate duplicate checks in processors Frontend: - Fix useApi isHandlingUnauthorized scope (module-level to prevent double 401 redirect) - Fix client-tickets toast key copy-paste bug - Merge duplicated tasks service methods (getByProject + getByProjectArchived) - Extract shared uploadWithRelation helper in task-documents service - Extract formatFileSize utility from duplicated component code - Extract status transition logic into useClientTicketHelpers composable - Remove dead code (unused router, handleLogout, empty script blocks) - Merge duplicate watchers and onMounted calls - Normalize arrow functions to function declarations per convention Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,35 @@ final readonly class TaskDocumentProcessor implements ProcessorInterface
|
||||
{
|
||||
private const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB
|
||||
|
||||
private const ALLOWED_MIME_TYPES = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'text/plain', 'text/csv',
|
||||
'application/zip', 'application/x-rar-compressed', 'application/gzip',
|
||||
'application/json', 'application/xml', 'text/xml',
|
||||
];
|
||||
|
||||
private const MIME_TO_EXTENSION = [
|
||||
'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif',
|
||||
'image/webp' => 'webp', 'image/svg+xml' => 'svg',
|
||||
'application/pdf' => 'pdf',
|
||||
'application/msword' => 'doc',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
|
||||
'application/vnd.ms-excel' => 'xls',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
|
||||
'application/vnd.ms-powerpoint' => 'ppt',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
|
||||
'text/plain' => 'txt', 'text/csv' => 'csv',
|
||||
'application/zip' => 'zip', 'application/x-rar-compressed' => 'rar', 'application/gzip' => 'gz',
|
||||
'application/json' => 'json', 'application/xml' => 'xml', 'text/xml' => 'xml',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private Security $security,
|
||||
@@ -52,50 +81,48 @@ final readonly class TaskDocumentProcessor implements ProcessorInterface
|
||||
throw new BadRequestHttpException('File size exceeds 50 MB limit.');
|
||||
}
|
||||
|
||||
$taskIri = $request->request->get('task');
|
||||
$clientTicketIri = $request->request->get('clientTicket');
|
||||
$taskIri = $request->request->get('task', '');
|
||||
$clientTicketIri = $request->request->get('clientTicket', '');
|
||||
|
||||
if ((null === $taskIri || '' === $taskIri) && (null === $clientTicketIri || '' === $clientTicketIri)) {
|
||||
if ('' === $taskIri && '' === $clientTicketIri) {
|
||||
throw new BadRequestHttpException('Either task or clientTicket IRI is required.');
|
||||
}
|
||||
|
||||
$task = null;
|
||||
$clientTicket = null;
|
||||
|
||||
if (null !== $taskIri && '' !== $taskIri) {
|
||||
// Extract task ID from IRI (e.g., "/api/tasks/42" -> 42)
|
||||
$taskId = (int) basename((string) $taskIri);
|
||||
$task = $this->entityManager->getRepository(Task::class)->find($taskId);
|
||||
if ('' !== $taskIri) {
|
||||
$task = $this->entityManager->getRepository(Task::class)->find((int) basename($taskIri));
|
||||
|
||||
if (null === $task) {
|
||||
throw new BadRequestHttpException('Task not found.');
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $clientTicketIri && '' !== $clientTicketIri) {
|
||||
$clientTicketId = (int) basename((string) $clientTicketIri);
|
||||
$clientTicket = $this->entityManager->getRepository(ClientTicket::class)->find($clientTicketId);
|
||||
if ('' !== $clientTicketIri) {
|
||||
$clientTicket = $this->entityManager->getRepository(ClientTicket::class)->find((int) basename($clientTicketIri));
|
||||
|
||||
if (null === $clientTicket) {
|
||||
throw new BadRequestHttpException('Client ticket not found.');
|
||||
}
|
||||
|
||||
// Ownership validation for ROLE_CLIENT
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
$currentUser = $this->security->getUser();
|
||||
if ($clientTicket->getSubmittedBy() !== $currentUser) {
|
||||
throw new AccessDeniedHttpException('You can only upload documents to your own tickets.');
|
||||
}
|
||||
if (!$this->security->isGranted('ROLE_ADMIN') && $clientTicket->getSubmittedBy() !== $this->security->getUser()) {
|
||||
throw new AccessDeniedHttpException('You can only upload documents to your own tickets.');
|
||||
}
|
||||
}
|
||||
|
||||
// Capture file metadata BEFORE move() — move invalidates the temp file
|
||||
// Use server-detected MIME type (finfo), not the client-supplied one
|
||||
$originalName = $file->getClientOriginalName();
|
||||
$extension = $file->getClientOriginalExtension() ?: 'bin';
|
||||
$mimeType = $file->getClientMimeType() ?? 'application/octet-stream';
|
||||
$mimeType = $file->getMimeType() ?: 'application/octet-stream';
|
||||
$fileSize = $file->getSize();
|
||||
$uuid = Uuid::v4()->toRfc4122();
|
||||
$fileName = $uuid.'.'.$extension;
|
||||
|
||||
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)) {
|
||||
throw new BadRequestHttpException(sprintf('File type "%s" is not allowed.', $mimeType));
|
||||
}
|
||||
|
||||
$extension = self::MIME_TO_EXTENSION[$mimeType] ?? 'bin';
|
||||
$uuid = Uuid::v4()->toRfc4122();
|
||||
$fileName = $uuid.'.'.$extension;
|
||||
|
||||
if (!is_dir($this->uploadDir)) {
|
||||
mkdir($this->uploadDir, 0o775, true);
|
||||
|
||||
Reference in New Issue
Block a user