From cdb9d999d865f1999ca340db982c14e926fbc303 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Sun, 10 May 2026 15:30:47 -0600 Subject: [PATCH] fix(client): proxy external images through cover proxy for CSP --- .../features/author/components/AuthorCard.vue | 6 +++++- .../author/components/AuthorHeader.vue | 18 ++++++++++++++---- .../author/components/AuthorListRow.vue | 9 +++++++-- .../book/composables/useCoverEditor.ts | 6 ++++-- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/client/src/features/author/components/AuthorCard.vue b/client/src/features/author/components/AuthorCard.vue index 2b88e382e..52f64a517 100644 --- a/client/src/features/author/components/AuthorCard.vue +++ b/client/src/features/author/components/AuthorCard.vue @@ -4,6 +4,7 @@ import type { AuthorSummary } from '@bookorbit/types' import { BookCopy, Check, ExternalLink, Loader2, MoreHorizontal, RefreshCw, Trash2 } from 'lucide-vue-next' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { bookCoverStyle } from '@/features/book/lib/book-cover' +import { toDisplayCoverUrl } from '@/features/book/lib/metadata-fetch' const props = defineProps<{ author: AuthorSummary @@ -25,7 +26,10 @@ const emit = defineEmits<{ const initial = computed(() => props.author.name.trim().charAt(0).toUpperCase() || '?') const fallbackStyle = computed(() => bookCoverStyle(props.author.name || String(props.author.id))) -const imageSrc = computed(() => props.author.imageUrl ?? undefined) +const imageSrc = computed(() => { + const display = toDisplayCoverUrl(props.author.imageUrl) + return display || undefined +}) const hasImage = computed(() => Boolean(imageSrc.value) && !imageFailed.value) const imageFailed = ref(false) diff --git a/client/src/features/author/components/AuthorHeader.vue b/client/src/features/author/components/AuthorHeader.vue index cc907a7b6..9ee596093 100644 --- a/client/src/features/author/components/AuthorHeader.vue +++ b/client/src/features/author/components/AuthorHeader.vue @@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue' import type { AuthorDetail } from '@bookorbit/types' import { MoreHorizontal, Pencil, RefreshCcw, Trash2, UsersRound, X } from 'lucide-vue-next' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { toDisplayCoverUrl } from '@/features/book/lib/metadata-fetch' const props = defineProps<{ author: AuthorDetail @@ -54,7 +55,11 @@ const previewProviderLabel = computed(() => { const showMenu = computed(() => props.canUpdate || props.canMerge || props.canDelete) const imageLightboxOpen = ref(false) -const canOpenImageLightbox = computed(() => Boolean(props.imageUrl)) +const displayImageUrl = computed(() => { + const display = toDisplayCoverUrl(props.imageUrl) + return display || null +}) +const canOpenImageLightbox = computed(() => Boolean(displayImageUrl.value)) const bioExpanded = ref(false) watch(resolvedBio, () => { @@ -71,7 +76,7 @@ watch(resolvedBio, () => { :class="canOpenImageLightbox ? 'cursor-zoom-in' : ''" @click="canOpenImageLightbox && (imageLightboxOpen = true)" > - +
{
@@ -172,7 +177,12 @@ watch(resolvedBio, () => { > - +
diff --git a/client/src/features/author/components/AuthorListRow.vue b/client/src/features/author/components/AuthorListRow.vue index 4f8f99076..fa183c228 100644 --- a/client/src/features/author/components/AuthorListRow.vue +++ b/client/src/features/author/components/AuthorListRow.vue @@ -2,6 +2,7 @@ import { computed, ref, watch } from 'vue' import type { AuthorSummary } from '@bookorbit/types' import { ArrowRight, BookCopy, Check, Clock3, Loader2 } from 'lucide-vue-next' +import { toDisplayCoverUrl } from '@/features/book/lib/metadata-fetch' const props = defineProps<{ author: AuthorSummary @@ -23,6 +24,10 @@ const lastAddedLabel = computed(() => { const imageFailed = ref(false) const initial = computed(() => props.author.name.trim().charAt(0).toUpperCase() || '?') +const imageSrc = computed(() => { + const display = toDisplayCoverUrl(props.author.imageUrl) + return display || undefined +}) watch( () => props.author.imageUrl, () => { @@ -56,8 +61,8 @@ function handleClick(event: MouseEvent) {
) { const uploading = ref(false) @@ -19,9 +20,10 @@ export function useCoverEditor(bookId: Ref) { function setUrl(url: string) { if (previewSrc.value?.startsWith('blob:')) URL.revokeObjectURL(previewSrc.value) + const normalizedUrl = url.trim() pendingFile.value = null - pendingUrl.value = url || null - previewSrc.value = url || null + pendingUrl.value = normalizedUrl || null + previewSrc.value = normalizedUrl ? toDisplayCoverUrl(normalizedUrl) : null error.value = null }