mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(client): proxy external images through cover proxy for CSP
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
<img v-if="imageUrl" :src="imageUrl" :alt="`${author.name} portrait`" class="h-full w-full object-cover" />
|
||||
<img v-if="displayImageUrl" :src="displayImageUrl" :alt="`${author.name} portrait`" class="h-full w-full object-cover" />
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full w-full items-center justify-center bg-gradient-to-br from-primary/20 to-primary/5 text-3xl font-semibold text-primary"
|
||||
@@ -162,7 +167,7 @@ watch(resolvedBio, () => {
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="imageLightboxOpen && imageUrl"
|
||||
v-if="imageLightboxOpen && displayImageUrl"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
|
||||
@click="imageLightboxOpen = false"
|
||||
>
|
||||
@@ -172,7 +177,12 @@ watch(resolvedBio, () => {
|
||||
>
|
||||
<X class="size-5" />
|
||||
</button>
|
||||
<img :src="imageUrl" :alt="`${author.name} portrait`" class="max-h-[90vh] max-w-[90vw] rounded-lg object-contain shadow-2xl" @click.stop />
|
||||
<img
|
||||
:src="displayImageUrl"
|
||||
:alt="`${author.name} portrait`"
|
||||
class="max-h-[90vh] max-w-[90vw] rounded-lg object-contain shadow-2xl"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
<div class="h-8 w-8 shrink-0 overflow-hidden rounded-md border border-border/60 bg-muted/30">
|
||||
<img
|
||||
v-if="author.imageUrl && !imageFailed"
|
||||
:src="author.imageUrl"
|
||||
v-if="imageSrc && !imageFailed"
|
||||
:src="imageSrc"
|
||||
:alt="`${author.name} portrait`"
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { onUnmounted, ref, type Ref } from 'vue'
|
||||
import { api } from '@/lib/api'
|
||||
import { useCoverVersions } from './useCoverVersions'
|
||||
import { toDisplayCoverUrl } from '../lib/metadata-fetch'
|
||||
|
||||
export function useCoverEditor(bookId: Ref<number>) {
|
||||
const uploading = ref(false)
|
||||
@@ -19,9 +20,10 @@ export function useCoverEditor(bookId: Ref<number>) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user