fix(web): attach JWT token to file download URL to fix auth error

Browser <a> tag downloads don't send Authorization headers, causing
"missing or malformed jwt" errors. Pass token via query param which
the backend JWT middleware already supports.
This commit is contained in:
Acbox
2026-03-29 17:43:42 +08:00
parent 6c2da4b2f5
commit d133a85fe3
2 changed files with 18 additions and 26 deletions
@@ -2,7 +2,7 @@
import { ref, watch, computed, onBeforeUnmount } from 'vue' import { ref, watch, computed, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { File, Download, X } from 'lucide-vue-next'
import { Button, Spinner } from '@memohai/ui' import { Button, Spinner } from '@memohai/ui'
import { import {
getBotsByBotIdContainerFsRead, getBotsByBotIdContainerFsRead,
@@ -93,7 +93,8 @@ async function handleSave() {
} }
function handleDownload() { function handleDownload() {
const url = `/api/bots/${props.botId}/container/fs/download?path=${encodeURIComponent(filePath.value)}` const token = localStorage.getItem('token') ?? ''
const url = `/api/bots/${props.botId}/container/fs/download?path=${encodeURIComponent(filePath.value)}&token=${encodeURIComponent(token)}`
const a = document.createElement('a') const a = document.createElement('a')
a.href = url a.href = url
a.download = filename.value a.download = filename.value
@@ -128,8 +129,7 @@ onBeforeUnmount(() => {
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between border-b border-border px-4 py-2"> <div class="flex items-center justify-between border-b border-border px-4 py-2">
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-2 min-w-0">
<FontAwesomeIcon <File
:icon="['fas', 'file']"
class="size-3.5 shrink-0 text-muted-foreground" class="size-3.5 shrink-0 text-muted-foreground"
/> />
<span class="truncate text-xs font-medium">{{ filename }}</span> <span class="truncate text-xs font-medium">{{ filename }}</span>
@@ -157,8 +157,7 @@ onBeforeUnmount(() => {
size="sm" size="sm"
@click="handleDownload" @click="handleDownload"
> >
<FontAwesomeIcon <Download
:icon="['fas', 'download']"
class="mr-1 size-3" class="mr-1 size-3"
/> />
{{ t('bots.files.download') }} {{ t('bots.files.download') }}
@@ -169,8 +168,7 @@ onBeforeUnmount(() => {
class="size-7 p-0" class="size-7 p-0"
@click="emit('close')" @click="emit('close')"
> >
<FontAwesomeIcon <X
:icon="['fas', 'xmark']"
class="size-4" class="size-4"
/> />
</Button> </Button>
@@ -209,8 +207,7 @@ onBeforeUnmount(() => {
v-else v-else
class="flex h-full flex-col items-center justify-center gap-3 text-muted-foreground" class="flex h-full flex-col items-center justify-center gap-3 text-muted-foreground"
> >
<FontAwesomeIcon <File
:icon="['fas', 'file']"
class="size-12 opacity-30" class="size-12 opacity-30"
/> />
<p class="text-xs"> <p class="text-xs">
@@ -221,8 +218,7 @@ onBeforeUnmount(() => {
size="sm" size="sm"
@click="handleDownload" @click="handleDownload"
> >
<FontAwesomeIcon <Download
:icon="['fas', 'download']"
class="mr-1.5 size-3" class="mr-1.5 size-3"
/> />
{{ t('bots.files.download') }} {{ t('bots.files.download') }}
+10 -14
View File
@@ -3,7 +3,7 @@ import { ref, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { ChevronRight, Folder, Upload, FolderPlus, RefreshCw } from 'lucide-vue-next'
import { useSyncedQueryParam } from '@/composables/useSyncedQueryParam' import { useSyncedQueryParam } from '@/composables/useSyncedQueryParam'
import { import {
Button, Button,
@@ -238,7 +238,8 @@ async function handleDelete() {
// Download // Download
function handleDownload(entry: HandlersFsFileInfo) { function handleDownload(entry: HandlersFsFileInfo) {
const url = `/api/bots/${props.botId}/container/fs/download?path=${encodeURIComponent(entry.path ?? '')}` const token = localStorage.getItem('token') ?? ''
const url = `/api/bots/${props.botId}/container/fs/download?path=${encodeURIComponent(entry.path ?? '')}&token=${encodeURIComponent(token)}`
const a = document.createElement('a') const a = document.createElement('a')
a.href = url a.href = url
a.download = entry.name ?? 'file' a.download = entry.name ?? 'file'
@@ -282,20 +283,18 @@ defineExpose({ navigateTo, openFileByPath })
v-for="(seg, idx) in pathSegments(currentPath)" v-for="(seg, idx) in pathSegments(currentPath)"
:key="seg.path" :key="seg.path"
> >
<FontAwesomeIcon <ChevronRight
v-if="idx > 0" v-if="idx > 0"
:icon="['fas', 'chevron-right']"
class="size-2.5 shrink-0 text-muted-foreground" class="size-2.5 shrink-0 text-muted-foreground"
/> />
<button <button
class="truncate rounded px-1.5 py-0.5 hover:bg-muted transition-colors" class="inline-flex items-center truncate rounded px-1.5 py-0.5 hover:bg-muted transition-colors"
:class="idx === pathSegments(currentPath).length - 1 ? 'font-medium text-foreground' : 'text-muted-foreground'" :class="idx === pathSegments(currentPath).length - 1 ? 'font-medium text-foreground' : 'text-muted-foreground'"
@click="handleNavigate(seg.path)" @click="handleNavigate(seg.path)"
> >
<FontAwesomeIcon <Folder
v-if="idx === 0" v-if="idx === 0"
:icon="['fas', 'folder']" class="mr-1 size-3 shrink-0"
class="mr-1 size-3"
/> />
{{ seg.name }} {{ seg.name }}
</button> </button>
@@ -315,8 +314,7 @@ defineExpose({ navigateTo, openFileByPath })
size="sm" size="sm"
@click="triggerUpload" @click="triggerUpload"
> >
<FontAwesomeIcon <Upload
:icon="['fas', 'upload']"
class="mr-1.5 size-3" class="mr-1.5 size-3"
/> />
{{ t('bots.files.upload') }} {{ t('bots.files.upload') }}
@@ -326,8 +324,7 @@ defineExpose({ navigateTo, openFileByPath })
size="sm" size="sm"
@click="openMkdirDialog" @click="openMkdirDialog"
> >
<FontAwesomeIcon <FolderPlus
:icon="['fas', 'folder-plus']"
class="mr-1.5 size-3" class="mr-1.5 size-3"
/> />
{{ t('bots.files.newFolder') }} {{ t('bots.files.newFolder') }}
@@ -339,8 +336,7 @@ defineExpose({ navigateTo, openFileByPath })
:disabled="listLoading" :disabled="listLoading"
@click="() => loadDirectory(currentPath)" @click="() => loadDirectory(currentPath)"
> >
<FontAwesomeIcon <RefreshCw
:icon="['fas', 'rotate']"
class="size-3.5" class="size-3.5"
:class="{ 'animate-spin': listLoading }" :class="{ 'animate-spin': listLoading }"
/> />