findUserOrFail($id); $this->assertCanManageAvatar($user); $file = $request->files->get('file'); if (null === $file || !$file->isValid()) { throw new BadRequestHttpException('No valid file uploaded.'); } if ($file->getSize() > self::MAX_FILE_SIZE) { throw new BadRequestHttpException('File size exceeds 5 MB limit.'); } $mimeType = $file->getMimeType(); if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)) { throw new BadRequestHttpException('Invalid file type. Allowed: JPEG, PNG, WebP, GIF.'); } // Delete previous avatar file if exists $this->deleteAvatarFile($user); $extension = $file->guessExtension() ?? 'bin'; $fileName = Uuid::v4()->toRfc4122().'.'.$extension; if (!is_dir($this->avatarUploadDir)) { mkdir($this->avatarUploadDir, 0o775, true); } $file->move($this->avatarUploadDir, $fileName); $user->setAvatarFileName($fileName); $this->entityManager->flush(); return new JsonResponse(['avatarUrl' => $user->getAvatarUrl()]); } #[Route('/api/users/{id}/avatar', name: 'user_avatar_serve', methods: ['GET'], priority: 1)] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function serve(int $id): BinaryFileResponse { $user = $this->findUserOrFail($id); if (null === $user->getAvatarFileName()) { throw new NotFoundHttpException('No avatar set.'); } $filePath = $this->avatarUploadDir.'/'.$user->getAvatarFileName(); if (!file_exists($filePath)) { throw new NotFoundHttpException('Avatar file not found on disk.'); } $response = new BinaryFileResponse($filePath); $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE, $user->getAvatarFileName()); $extension = pathinfo($user->getAvatarFileName(), PATHINFO_EXTENSION); $mimeMap = ['jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'gif' => 'image/gif']; $response->headers->set('Content-Type', $mimeMap[$extension] ?? 'application/octet-stream'); $response->headers->set('Cache-Control', 'public, max-age=86400'); return $response; } #[Route('/api/users/{id}/avatar', name: 'user_avatar_delete', methods: ['DELETE'], priority: 1)] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function delete(int $id): Response { $user = $this->findUserOrFail($id); $this->assertCanManageAvatar($user); $this->deleteAvatarFile($user); $user->setAvatarFileName(null); $this->entityManager->flush(); return new Response(null, Response::HTTP_NO_CONTENT); } private function findUserOrFail(int $id): User { $user = $this->entityManager->getRepository(User::class)->find($id); if (null === $user) { throw new NotFoundHttpException('User not found.'); } return $user; } private function assertCanManageAvatar(User $user): void { $currentUser = $this->getUser(); if ($currentUser !== $user && !$this->isGranted('ROLE_ADMIN')) { throw new AccessDeniedHttpException('You can only manage your own avatar.'); } } private function deleteAvatarFile(User $user): void { if (null === $user->getAvatarFileName()) { return; } $filePath = $this->avatarUploadDir.'/'.$user->getAvatarFileName(); if (file_exists($filePath)) { unlink($filePath); } } }