show image preview + single file downloads

This commit is contained in:
2025-08-10 23:16:18 +02:00
parent bfe73eefb9
commit c8b52a5b3b
13 changed files with 174 additions and 26 deletions

View File

@@ -1,5 +1,5 @@
# this file is ignored when the app is built since we're using SSG
NUXT_PUBLIC_API_BASE="http://127.0.0.1:8080/api"
NUXT_COOKIES_SECURE="false"
NUXT_COOKIES_SAME_SITE="strict"
NUXT_PUBLIC_COOKIES_SECURE="false"
NUXT_PUBLIC_COOKIES_SAME_SITE="strict"

View File

@@ -12,6 +12,7 @@ import {
fetchFile,
} from '~/lib/api/warrens';
import type { DirectoryEntry } from '#shared/types';
import { toast } from 'vue-sonner';
const warrenStore = useWarrenStore();
const copyStore = useCopyStore();
@@ -110,6 +111,30 @@ function onCopy() {
entry.name
);
}
async function onDownload() {
if (warrenStore.current == null) {
return;
}
if (entry.fileType !== 'file') {
toast.warning('Download', {
description: 'Directory downloads are not supported yet',
});
return;
}
const anchor = document.createElement('a');
anchor.download = entry.name;
anchor.href = getApiUrl(
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&path=${joinPaths(warrenStore.current.path, entry.name)}`
);
anchor.rel = 'noopener';
anchor.target = '_blank';
anchor.click();
}
</script>
<template>
@@ -128,6 +153,11 @@ function onCopy() {
>
<div class="flex flex-row items-center">
<Icon
v-if="
entry.fileType !== 'file' ||
entry.mimeType == null ||
!entry.mimeType.startsWith('image/')
"
class="size-6"
:name="
entry.fileType === 'file'
@@ -135,6 +165,23 @@ function onCopy() {
: 'lucide:folder'
"
/>
<object
v-else
:type="entry.mimeType"
class="size-6 object-cover"
width="24"
height="24"
:data="
getApiUrl(
`warrens/files/cat?warrenId=${warrenStore.current!.warrenId}&path=${joinPaths(warrenStore.current!.path, entry.name)}`
)
"
>
<Icon
class="size-6"
:name="getFileIcon(entry.mimeType)"
/>
</object>
</div>
<div
@@ -159,6 +206,13 @@ function onCopy() {
<Icon name="lucide:copy" />
Copy
</ContextMenuItem>
<ContextMenuItem
:disabled="entry.fileType !== 'file'"
@select="onDownload"
>
<Icon name="lucide:download" />
Download
</ContextMenuItem>
<ContextMenuSeparator />

View File

@@ -1,4 +1,4 @@
import type { AuthSession } from '#shared/types/auth';
import type { AuthSession, AuthUser } from '#shared/types/auth';
const SAME_SITE_SETTINGS = ['strict', 'lax', 'none'] as const;
@@ -12,9 +12,36 @@ export function useAuthSession() {
return useCookie('auth_session', {
default: () => null as AuthSession | null,
secure: config.cookiesSecure.toLowerCase() === 'true',
secure: config.cookiesSecure,
sameSite,
path: '/',
httpOnly: false,
});
}
export function setAuthSession(value: {
type: 'WarrenAuth';
id: string;
user: AuthUser;
expiresAt: number;
}) {
useAuthSession().value = value;
let cookie = `authorization=WarrenAuth ${value.id}; path=/; SameSite=Lax; Secure;`;
const config = useRuntimeConfig().public;
console.log('config', config);
const cookieDomain = config.authCookieDomain;
if (cookieDomain != null && cookieDomain.length > 0) {
cookie += ` domain=${cookieDomain}`;
}
console.log(cookie);
document.cookie = cookie;
}
export function clearAuthSession() {
useAuthSession().value = null;
document.cookie = '';
}

View File

@@ -32,12 +32,12 @@ export async function loginUser(
const token = data.value.data.token;
const { user, expiresAt } = data.value.data;
useAuthSession().value = {
setAuthSession({
type: 'WarrenAuth',
id: token,
user,
expiresAt,
};
});
toast.success('Login', {
description: `Successfully logged in`,

View File

@@ -1,5 +1,5 @@
export async function logout() {
useAuthSession().value = null;
clearAuthSession();
useAdminStore().$reset();
useWarrenStore().$reset();
await navigateTo({

View File

@@ -57,12 +57,12 @@ export async function oidcLoginUser(
const token = data.value.data.token;
const { user, expiresAt } = data.value.data;
useAuthSession().value = {
setAuthSession({
type: 'WarrenAuth',
id: token,
user,
expiresAt,
};
});
toast.success('OpenID Connect', {
description: `Successfully logged in`,

View File

@@ -9,7 +9,9 @@ export function getAuthHeader(): ['authorization', string] | null {
export function getApiHeaders(
includeAuth: boolean = true
): Record<string, string> {
const headers: Record<string, string> = {};
const headers: Record<string, string> = {
cookie: document.cookie,
};
if (includeAuth) {
const header = getAuthHeader();

View File

@@ -301,6 +301,40 @@ export async function fetchFile(
};
}
export async function fetchFileStream(
warrenId: string,
path: string,
fileName: string
): Promise<
{ success: true; stream: ReadableStream<Uint8Array> } | { success: false }
> {
if (!path.endsWith('/')) {
path += '/';
}
path += fileName;
const { data, error } = await useFetch<ReadableStream<Uint8Array>>(
getApiUrl(`warrens/files/cat?warrenId=${warrenId}&path=${path}`),
{
method: 'GET',
headers: getApiHeaders(),
responseType: 'stream',
cache: 'default',
}
);
if (data.value == null || error.value != null) {
return {
success: false,
};
}
return {
success: true,
stream: data.value,
};
}
export async function moveFile(
warrenId: string,
currentPath: string,

View File

@@ -58,7 +58,7 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
apiBase: '/api',
cookiesSecure: 'false',
cookiesSecure: false,
cookiesSameSite: 'strict',
},
},