fix(avatar) : address review findings — security and UX fixes
- Use getMimeType() instead of getClientMimeType() to prevent MIME spoofing - Change IsGranted to IS_AUTHENTICATED_FULLY so ROLE_CLIENT can access avatars - Remove Groups from avatarFileName (only avatarUrl needed by frontend) - Disable aggressive caching to prevent stale avatar images - Add error handling to avatar upload in profile page - Use i18n for "Mon profil" button text Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
class="block w-full px-3 py-2 text-left hover:bg-neutral-100"
|
class="block w-full px-3 py-2 text-left hover:bg-neutral-100"
|
||||||
@click="navigateTo('/profile')"
|
@click="navigateTo('/profile')"
|
||||||
>
|
>
|
||||||
Mon profil
|
{{ $t('profile.title') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -67,8 +67,12 @@ async function onCrop(blob: Blob) {
|
|||||||
selectedFile.value = null
|
selectedFile.value = null
|
||||||
if (!auth.user) return
|
if (!auth.user) return
|
||||||
|
|
||||||
await upload(auth.user.id, blob)
|
try {
|
||||||
await auth.refreshUser()
|
await upload(auth.user.id, blob)
|
||||||
|
await auth.refreshUser()
|
||||||
|
} catch {
|
||||||
|
// Upload error — $fetch will throw on non-2xx
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRemove() {
|
async function onRemove() {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class UserAvatarController extends AbstractController
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('/api/users/{id}/avatar', name: 'user_avatar_upload', methods: ['POST'], priority: 1)]
|
#[Route('/api/users/{id}/avatar', name: 'user_avatar_upload', methods: ['POST'], priority: 1)]
|
||||||
#[IsGranted('ROLE_USER')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function upload(int $id, Request $request): JsonResponse
|
public function upload(int $id, Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$user = $this->findUserOrFail($id);
|
$user = $this->findUserOrFail($id);
|
||||||
@@ -46,7 +46,7 @@ class UserAvatarController extends AbstractController
|
|||||||
throw new BadRequestHttpException('File size exceeds 5 MB limit.');
|
throw new BadRequestHttpException('File size exceeds 5 MB limit.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$mimeType = $file->getClientMimeType();
|
$mimeType = $file->getMimeType();
|
||||||
|
|
||||||
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)) {
|
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)) {
|
||||||
throw new BadRequestHttpException('Invalid file type. Allowed: JPEG, PNG, WebP, GIF.');
|
throw new BadRequestHttpException('Invalid file type. Allowed: JPEG, PNG, WebP, GIF.');
|
||||||
@@ -71,7 +71,7 @@ class UserAvatarController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/users/{id}/avatar', name: 'user_avatar_serve', methods: ['GET'], priority: 1)]
|
#[Route('/api/users/{id}/avatar', name: 'user_avatar_serve', methods: ['GET'], priority: 1)]
|
||||||
#[IsGranted('ROLE_USER')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function serve(int $id): BinaryFileResponse
|
public function serve(int $id): BinaryFileResponse
|
||||||
{
|
{
|
||||||
$user = $this->findUserOrFail($id);
|
$user = $this->findUserOrFail($id);
|
||||||
@@ -91,13 +91,13 @@ class UserAvatarController extends AbstractController
|
|||||||
$extension = pathinfo($user->getAvatarFileName(), PATHINFO_EXTENSION);
|
$extension = pathinfo($user->getAvatarFileName(), PATHINFO_EXTENSION);
|
||||||
$mimeMap = ['jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'gif' => 'image/gif'];
|
$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('Content-Type', $mimeMap[$extension] ?? 'application/octet-stream');
|
||||||
$response->headers->set('Cache-Control', 'public, max-age=86400');
|
$response->headers->set('Cache-Control', 'no-cache, must-revalidate');
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/users/{id}/avatar', name: 'user_avatar_delete', methods: ['DELETE'], priority: 1)]
|
#[Route('/api/users/{id}/avatar', name: 'user_avatar_delete', methods: ['DELETE'], priority: 1)]
|
||||||
#[IsGranted('ROLE_USER')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
public function delete(int $id): Response
|
public function delete(int $id): Response
|
||||||
{
|
{
|
||||||
$user = $this->findUserOrFail($id);
|
$user = $this->findUserOrFail($id);
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
private ?string $apiToken = null;
|
private ?string $apiToken = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
#[Groups(['me:read', 'user:list'])]
|
|
||||||
private ?string $avatarFileName = null;
|
private ?string $avatarFileName = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Client::class)]
|
#[ORM\ManyToOne(targetEntity: Client::class)]
|
||||||
|
|||||||
Reference in New Issue
Block a user