Fix image CORS violations with comprehensive proxy and component updates

- Applied transformImageUrlForCors to all image-displaying components
- Added followRedirects: true to image proxy to serve actual content
- Proxy now returns 200 OK with image data instead of 301 redirects
- Maintains CORS headers required for SharedArrayBuffer support
- Added debug logging for proxy response monitoring

Resolves all image loading failures in development environment.
This commit is contained in:
Matthew Raymer
2025-07-03 04:15:26 +00:00
parent be87d38d29
commit 797db7069c
11 changed files with 304 additions and 85 deletions

View File

@@ -63,7 +63,7 @@
<div
v-if="record.image"
class="bg-cover mb-2 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
:style="`background-image: url(${record.image});`"
:style="`background-image: url(${transformImageUrlForCors(record.image)});`"
>
<a
class="block bg-slate-100/50 backdrop-blur-md px-6 py-4 cursor-pointer"
@@ -71,7 +71,7 @@
>
<img
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
:src="record.image"
:src="transformImageUrlForCors(record.image)"
alt="Activity image"
@load="cacheImage(record.image)"
/>
@@ -251,7 +251,11 @@
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "../types";
import EntityIcon from "./EntityIcon.vue";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
import {
isGiveClaimType,
notifyWhyCannotConfirm,
transformImageUrlForCors,
} from "../libs/util";
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue";
import { NotificationIface } from "../constants/app";
@@ -360,5 +364,9 @@ export default class ActivityListItem extends Vue {
day: "numeric",
});
}
transformImageUrlForCors(imageUrl: string): string {
return transformImageUrlForCors(imageUrl);
}
}
</script>

View File

@@ -7,6 +7,7 @@ import { createAvatar, StyleOptions } from "@dicebear/core";
import { avataaars } from "@dicebear/collection";
import { Vue, Component, Prop } from "vue-facing-decorator";
import { Contact } from "../db/tables/contacts";
import { transformImageUrlForCors } from "../libs/util";
@Component
export default class EntityIcon extends Vue {
@@ -18,7 +19,7 @@ export default class EntityIcon extends Vue {
generateIcon() {
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
if (imageUrl) {
return `<img src="${imageUrl}" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
return `<img src="${transformImageUrlForCors(imageUrl)}" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
} else {
const identifier = this.contact?.did || this.entityId;
if (!identifier) {

View File

@@ -25,7 +25,7 @@
<div class="flex-1 flex items-center justify-center p-2">
<div class="w-full h-full flex items-center justify-center">
<img
:src="imageUrl"
:src="transformedImageUrl"
class="max-h-[calc(100vh-5rem)] w-full h-full object-contain"
alt="expanded shared content"
@click="close"
@@ -41,6 +41,7 @@
import { Component, Vue, Prop } from "vue-facing-decorator";
import { UAParser } from "ua-parser-js";
import { logger } from "../utils/logger";
import { transformImageUrlForCors } from "../libs/util";
@Component({ emits: ["update:isOpen"] })
export default class ImageViewer extends Vue {
@@ -79,6 +80,10 @@ export default class ImageViewer extends Vue {
window.open(this.imageUrl, "_blank");
}
}
get transformedImageUrl() {
return transformImageUrlForCors(this.imageUrl);
}
}
</script>

View File

@@ -13,6 +13,7 @@
<script lang="ts">
import { toSvg } from "jdenticon";
import { Vue, Component, Prop } from "vue-facing-decorator";
import { transformImageUrlForCors } from "../libs/util";
const BLANK_CONFIG = {
lightness: {
@@ -35,7 +36,7 @@ export default class ProjectIcon extends Vue {
generateIcon() {
if (this.imageUrl) {
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
return `<img src="${transformImageUrlForCors(this.imageUrl)}" class="w-full h-full object-contain" />`;
} else {
const config = this.entityId ? undefined : BLANK_CONFIG;
const svgString = toSvg(this.entityId, this.iconSize, config);

View File

@@ -11,15 +11,15 @@ export enum AppString {
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
LOCAL_ENDORSER_API_SERVER = "/api",
PROD_IMAGE_API_SERVER = "https://image-api.timesafari.app",
TEST_IMAGE_API_SERVER = "https://test-image-api.timesafari.app",
LOCAL_IMAGE_API_SERVER = "http://localhost:3001",
LOCAL_IMAGE_API_SERVER = "/image-api",
PROD_PARTNER_API_SERVER = "https://partner-api.endorser.ch",
TEST_PARTNER_API_SERVER = "https://test-partner-api.endorser.ch",
LOCAL_PARTNER_API_SERVER = LOCAL_ENDORSER_API_SERVER,
LOCAL_PARTNER_API_SERVER = "/partner-api",
PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app",

View File

@@ -926,3 +926,40 @@ export async function importFromMnemonic(
// Save the new identity
await saveNewIdentity(newId, mne, derivationPath);
}
/**
* Transforms direct image URLs to use proxy endpoints in development to avoid CORS issues
* with restrictive headers required for SharedArrayBuffer support.
*
* @param imageUrl - The original image URL
* @returns Transformed URL that uses proxy in development, original URL otherwise
*
* @example
* transformImageUrlForCors('https://image.timesafari.app/abc123.jpg')
* // Returns: '/image-proxy/abc123.jpg' in development
* // Returns: 'https://image.timesafari.app/abc123.jpg' in production
*/
export function transformImageUrlForCors(
imageUrl: string | undefined | null,
): string {
if (!imageUrl) return "";
// Only transform in development mode
if (process.env.NODE_ENV === "production") {
return imageUrl;
}
// Transform direct image.timesafari.app URLs to use proxy
if (imageUrl.startsWith("https://image.timesafari.app/")) {
const imagePath = imageUrl.replace("https://image.timesafari.app/", "");
return `/image-proxy/${imagePath}`;
}
// Transform other timesafari.app subdomains if needed
if (imageUrl.includes(".timesafari.app/")) {
const imagePath = imageUrl.split(".timesafari.app/")[1];
return `/image-proxy/${imagePath}`;
}
return imageUrl;
}

View File

@@ -34,7 +34,10 @@
<div class="flex justify-center mt-4">
<span v-if="imageUrl" class="flex justify-between">
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
<img :src="imageUrl" class="h-24 rounded-xl" />
<img
:src="transformImageUrlForCors(imageUrl)"
class="h-24 rounded-xl"
/>
</a>
<font-awesome
icon="trash-can"
@@ -243,6 +246,7 @@ import {
import {
retrieveAccountCount,
retrieveFullyDecryptedAccount,
transformImageUrlForCors,
} from "../libs/util";
import {
@@ -828,5 +832,12 @@ export default class NewEditProjectView extends Vue {
this.latitude = event.latlng.lat;
this.longitude = event.latlng.lng;
}
/**
* Transforms image URLs to avoid CORS issues in development
* @param imageUrl - Original image URL
* @returns Transformed URL for proxy or original URL
*/
transformImageUrlForCors = transformImageUrlForCors;
}
</script>

View File

@@ -608,7 +608,10 @@
</div>
<div v-if="give.fullClaim.image" class="flex justify-center">
<a :href="give.fullClaim.image" target="_blank">
<img :src="give.fullClaim.image" class="h-24 mt-2 rounded-xl" />
<img
:src="transformImageUrlForCors(give.fullClaim.image)"
class="h-24 mt-2 rounded-xl"
/>
</a>
</div>
</li>
@@ -653,6 +656,7 @@ import HiddenDidDialog from "../components/HiddenDidDialog.vue";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { useClipboard } from "@vueuse/core";
import { transformImageUrlForCors } from "../libs/util";
/**
* Project View Component
* @author Matthew Raymer
@@ -1552,5 +1556,12 @@ export default class ProjectViewView extends Vue {
this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0
);
}
/**
* Transforms image URLs to avoid CORS issues in development
* @param imageUrl - Original image URL
* @returns Transformed URL for proxy or original URL
*/
transformImageUrlForCors = transformImageUrlForCors;
}
</script>