forked from trent_larson/crowd-funder-for-time-pwa
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:
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user