Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ const nextConfig: NextConfig = {
// Prefer AVIF (~20-30% smaller than WebP), falling back to WebP, for any
// next/image output.
formats: ["image/avif", "image/webp"],
// Permit optimizing Supabase Storage signed URLs (private document/image
// previews) through next/image. Scoped to this app's exact production and
// (when configured) staging project hostnames, not the wildcard *.supabase.co.
// Private signed document/image previews opt out of the optimizer at the
// component level (`SignedImage` sets `unoptimized`). Do not rely on
// `minimumCacheTTL` as an expiry cap for bearer URLs: it is a lower bound,
// and stale-while-revalidate can keep serving private bytes past the
// signed-URL lifetime without re-entering the authenticated signed-URL route.
// Permit optimizing other Supabase Storage URLs through next/image when a
// caller intentionally uses the optimizer. Scoped to this app's exact
// production and (when configured) staging project hostnames, not the
// wildcard *.supabase.co.
remotePatterns: (() => {
const allowedHostnames = [expectedSupabaseProject.ref + ".supabase.co"];
const stagingRef = process.env.SUPABASE_STAGING_PROJECT_REF?.trim();
Expand Down
16 changes: 10 additions & 6 deletions src/components/clinical-dashboard/signed-image.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

/* eslint-disable @next/next/no-img-element */

import Image from "next/image";
import { memo, useEffect, useRef, useState } from "react";
import { CircleAlert, Loader2, Maximize2 } from "lucide-react";

Expand Down Expand Up @@ -129,15 +128,20 @@ export const SignedImage = memo(function SignedImage({
)}
>
{url ? (
<img
// Keep next/image for fill/layout/sizes, but mark private signed previews
// `unoptimized` so bearer URLs never enter the unauthenticated
// `/_next/image` optimizer cache (stale-while-revalidate can outlive the
// signed token). Authorization stays on `/api/.../signed-url` issuance.
<Image
src={url}
alt={alt}
loading="lazy"
decoding="async"
fill
sizes="(max-width: 768px) 92vw, 320px"
Comment thread
BigSimmo marked this conversation as resolved.
Comment thread
BigSimmo marked this conversation as resolved.
Comment thread
BigSimmo marked this conversation as resolved.
unoptimized
onLoad={() => setLoaded(true)}
onError={handleImageError}
className={cn(
"absolute inset-0 h-full w-full rounded-lg object-contain transition-opacity duration-300 motion-reduce:transition-none",
"rounded-lg object-contain transition-opacity duration-300 motion-reduce:transition-none",
loaded ? "opacity-100" : "opacity-0",
)}
/>
Expand Down
6 changes: 5 additions & 1 deletion tests/sheet.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ beforeEach(() => {
});

afterEach(() => {
document.body.style.overflow = "";
// Guard teardown: unhandled rAF/setTimeout from Sheet focus scheduling can
// fire after Vitest tears down the jsdom environment under coverage workers.
if (typeof document !== "undefined" && document.body) {
document.body.style.overflow = "";
}
});

function Stacked({
Expand Down
6 changes: 5 additions & 1 deletion tests/signed-image.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ describe("SignedImage failure/retry (jsdom)", () => {
await user.click(retry);

const img = await screen.findByRole("img", { name: "Airway diagram" });
expect(img).toHaveAttribute("src", "/demo/airway.png");
// Private previews use unoptimized next/image, so src stays the direct URL
// (jsdom may absolutize it) and must not be rewritten through `/_next/image`.
const src = img.getAttribute("src") ?? "";
expect(src.endsWith("/demo/airway.png")).toBe(true);
expect(src).not.toContain("/_next/image");
expect(screen.queryByText("Image preview failed.")).not.toBeInTheDocument();
expect(fetchMock).toHaveBeenCalledTimes(2);
});
Expand Down
3 changes: 3 additions & 0 deletions tests/signed-image.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ describe("SignedImage", () => {
const markup = renderToStaticMarkup(createElement(SignedImage, { endpoint: ENDPOINT, alt: "Airway diagram" }));

expect(markup).toContain("<img");
// Private previews stay on a direct src (unoptimized) — never rewritten
// through the public `/_next/image` optimizer cache.
expect(markup).toContain('src="/demo-documents/airway.png"');
expect(markup).not.toContain("/_next/image");
expect(markup).toContain('alt="Airway diagram"');
// A seeded frame is active, not deferred.
expect(markup).not.toContain("Image preview will load when visible");
Expand Down
Loading