Load API images via authenticated fetch instead of bare img src

img tags don't send Authorization headers; AuthImage component fetches
via axios (with Bearer token) and renders a blob URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-11 14:01:33 +00:00
parent b471c7c500
commit e9e19fbd9f
4 changed files with 30 additions and 6 deletions

View file

@ -0,0 +1,24 @@
import { useEffect, useState, ImgHTMLAttributes } from "react";
import client from "../api/client";
interface AuthImageProps extends ImgHTMLAttributes<HTMLImageElement> {
src: string;
}
export function AuthImage({ src, ...props }: AuthImageProps) {
const [objectUrl, setObjectUrl] = useState<string | null>(null);
useEffect(() => {
let url: string | null = null;
client.get(src, { responseType: "blob" }).then((res) => {
url = URL.createObjectURL(res.data);
setObjectUrl(url);
});
return () => {
if (url) URL.revokeObjectURL(url);
};
}, [src]);
if (!objectUrl) return null;
return <img src={objectUrl} {...props} />;
}

View file

@ -3,6 +3,7 @@ import { useParams, useNavigate } from "react-router-dom";
import { useAnalysis, useDeleteAnalysis } from "../hooks/useAnalysis";
import { useAnalysisStore, type AnalysisTab } from "../stores/analysisStore";
import { getAnalysisImageUrl, checkAIInsightsAvailable, generateAIInsights } from "../api/analysis";
import { AuthImage } from "../components/AuthImage";
import type { Insight } from "../types/analysis";
function getScoreInfo(score: number) {
@ -76,7 +77,7 @@ function ZoomableImage({ src, alt }: { src: string; alt: string }) {
const zoom = useAnalysisStore((s) => s.zoom);
return (
<div className="overflow-auto">
<img
<AuthImage
src={src}
alt={alt}
className="object-contain"

View file

@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useProject } from "../hooks/useProjects";
import { getAnalysisImageUrl } from "../api/analysis";
import { AuthImage } from "../components/AuthImage";
import Card from "../components/common/Card";
import Button from "../components/common/Button";
import LoadingSpinner from "../components/common/LoadingSpinner";
@ -98,7 +99,7 @@ export default function ComparisonView() {
<p className="text-xs text-gray-400">{analysis.model}</p>
</div>
<div className="aspect-video bg-gray-100">
<img
<AuthImage
src={getAnalysisImageUrl(analysis.id, "heatmap")}
alt={`Heatmap: ${analysis.name}`}
className="w-full h-full object-contain"

View file

@ -3,6 +3,7 @@ import { useParams, Link, useNavigate } from "react-router-dom";
import { useProject, useDeleteProject } from "../hooks/useProjects";
import { useDeleteAnalysis } from "../hooks/useAnalysis";
import { getAnalysisImageUrl } from "../api/analysis";
import { AuthImage } from "../components/AuthImage";
import Card from "../components/common/Card";
import Button from "../components/common/Button";
import LoadingSpinner from "../components/common/LoadingSpinner";
@ -123,16 +124,13 @@ export default function ProjectDetail() {
className="hover:shadow-md transition-shadow cursor-pointer overflow-hidden"
>
<div className="aspect-video bg-gray-100 relative">
<img
<AuthImage
src={
analysis.thumbnail_url ||
getAnalysisImageUrl(analysis.id, "heatmap")
}
alt={analysis.name}
className="w-full h-full object-cover"
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
<span
className={`absolute top-2 right-2 text-xs px-2 py-0.5 rounded-full font-medium ${