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:
parent
b471c7c500
commit
e9e19fbd9f
4 changed files with 30 additions and 6 deletions
24
frontend/src/components/AuthImage.tsx
Normal file
24
frontend/src/components/AuthImage.tsx
Normal 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} />;
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 ${
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue