mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
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:
@@ -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') }}
|
||||||
|
|||||||
@@ -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 }"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user