show image preview + single file downloads
This commit is contained in:
12
backend/Cargo.lock
generated
12
backend/Cargo.lock
generated
@@ -144,6 +144,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cookie",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
@@ -292,6 +293,17 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ path = "src/bin/backend/main.rs"
|
|||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
axum = { version = "0.8.4", features = ["multipart", "query"] }
|
axum = { version = "0.8.4", features = ["multipart", "query"] }
|
||||||
axum-extra = { version = "0.10.1", features = ["multipart"] }
|
axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use axum::{
|
|||||||
extract::FromRequestParts,
|
extract::FromRequestParts,
|
||||||
http::{header::AUTHORIZATION, request::Parts},
|
http::{header::AUTHORIZATION, request::Parts},
|
||||||
};
|
};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
domain::warren::models::auth_session::AuthSessionIdWithType, inbound::http::responses::ApiError,
|
domain::warren::models::auth_session::AuthSessionIdWithType, inbound::http::responses::ApiError,
|
||||||
@@ -15,7 +16,23 @@ where
|
|||||||
{
|
{
|
||||||
type Rejection = ApiError;
|
type Rejection = ApiError;
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
// Release build
|
||||||
|
if !cfg!(debug_assertions) {
|
||||||
|
let jar = CookieJar::from_request_parts(parts, state).await.unwrap();
|
||||||
|
|
||||||
|
let Some(cookie) = jar.get(AUTHORIZATION.as_str()) else {
|
||||||
|
return Err(ApiError::Unauthorized(
|
||||||
|
"Missing authorization cookie".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
AuthSessionIdWithType::from_str(cookie.value())
|
||||||
|
.map(|session_id| SessionIdHeader(session_id))
|
||||||
|
.map_err(|_| ApiError::BadRequest("Invalid session id".to_string()))
|
||||||
|
}
|
||||||
|
// Debug build
|
||||||
|
else {
|
||||||
let Some(header) = parts.headers.get(AUTHORIZATION) else {
|
let Some(header) = parts.headers.get(AUTHORIZATION) else {
|
||||||
return Err(ApiError::Unauthorized(
|
return Err(ApiError::Unauthorized(
|
||||||
"Missing authorization header".to_string(),
|
"Missing authorization header".to_string(),
|
||||||
@@ -32,4 +49,5 @@ where
|
|||||||
.map(|session_id| SessionIdHeader(session_id))
|
.map(|session_id| SessionIdHeader(session_id))
|
||||||
.map_err(|_| ApiError::BadRequest("Invalid session id".to_string()))
|
.map_err(|_| ApiError::BadRequest("Invalid session id".to_string()))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ services:
|
|||||||
- 'SERVE_DIRECTORY=/serve'
|
- 'SERVE_DIRECTORY=/serve'
|
||||||
- 'CORS_ALLOW_ORIGIN=http://localhost:8081'
|
- 'CORS_ALLOW_ORIGIN=http://localhost:8081'
|
||||||
- 'LOG_LEVEL=debug'
|
- 'LOG_LEVEL=debug'
|
||||||
|
- 'MAX_FILE_FETCH_BYTES=10737418240'
|
||||||
volumes:
|
volumes:
|
||||||
- './backend/serve:/serve:rw'
|
- './backend/serve:/serve:rw'
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# this file is ignored when the app is built since we're using SSG
|
# 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_PUBLIC_API_BASE="http://127.0.0.1:8080/api"
|
||||||
NUXT_COOKIES_SECURE="false"
|
NUXT_PUBLIC_COOKIES_SECURE="false"
|
||||||
NUXT_COOKIES_SAME_SITE="strict"
|
NUXT_PUBLIC_COOKIES_SAME_SITE="strict"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
fetchFile,
|
fetchFile,
|
||||||
} from '~/lib/api/warrens';
|
} from '~/lib/api/warrens';
|
||||||
import type { DirectoryEntry } from '#shared/types';
|
import type { DirectoryEntry } from '#shared/types';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
|
||||||
const warrenStore = useWarrenStore();
|
const warrenStore = useWarrenStore();
|
||||||
const copyStore = useCopyStore();
|
const copyStore = useCopyStore();
|
||||||
@@ -110,6 +111,30 @@ function onCopy() {
|
|||||||
entry.name
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -128,6 +153,11 @@ function onCopy() {
|
|||||||
>
|
>
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
<Icon
|
<Icon
|
||||||
|
v-if="
|
||||||
|
entry.fileType !== 'file' ||
|
||||||
|
entry.mimeType == null ||
|
||||||
|
!entry.mimeType.startsWith('image/')
|
||||||
|
"
|
||||||
class="size-6"
|
class="size-6"
|
||||||
:name="
|
:name="
|
||||||
entry.fileType === 'file'
|
entry.fileType === 'file'
|
||||||
@@ -135,6 +165,23 @@ function onCopy() {
|
|||||||
: 'lucide:folder'
|
: '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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -159,6 +206,13 @@ function onCopy() {
|
|||||||
<Icon name="lucide:copy" />
|
<Icon name="lucide:copy" />
|
||||||
Copy
|
Copy
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
:disabled="entry.fileType !== 'file'"
|
||||||
|
@select="onDownload"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:download" />
|
||||||
|
Download
|
||||||
|
</ContextMenuItem>
|
||||||
|
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
const SAME_SITE_SETTINGS = ['strict', 'lax', 'none'] as const;
|
||||||
|
|
||||||
@@ -12,9 +12,36 @@ export function useAuthSession() {
|
|||||||
|
|
||||||
return useCookie('auth_session', {
|
return useCookie('auth_session', {
|
||||||
default: () => null as AuthSession | null,
|
default: () => null as AuthSession | null,
|
||||||
secure: config.cookiesSecure.toLowerCase() === 'true',
|
secure: config.cookiesSecure,
|
||||||
sameSite,
|
sameSite,
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: false,
|
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 = '';
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ export async function loginUser(
|
|||||||
const token = data.value.data.token;
|
const token = data.value.data.token;
|
||||||
const { user, expiresAt } = data.value.data;
|
const { user, expiresAt } = data.value.data;
|
||||||
|
|
||||||
useAuthSession().value = {
|
setAuthSession({
|
||||||
type: 'WarrenAuth',
|
type: 'WarrenAuth',
|
||||||
id: token,
|
id: token,
|
||||||
user,
|
user,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
};
|
});
|
||||||
|
|
||||||
toast.success('Login', {
|
toast.success('Login', {
|
||||||
description: `Successfully logged in`,
|
description: `Successfully logged in`,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export async function logout() {
|
export async function logout() {
|
||||||
useAuthSession().value = null;
|
clearAuthSession();
|
||||||
useAdminStore().$reset();
|
useAdminStore().$reset();
|
||||||
useWarrenStore().$reset();
|
useWarrenStore().$reset();
|
||||||
await navigateTo({
|
await navigateTo({
|
||||||
|
|||||||
@@ -57,12 +57,12 @@ export async function oidcLoginUser(
|
|||||||
const token = data.value.data.token;
|
const token = data.value.data.token;
|
||||||
const { user, expiresAt } = data.value.data;
|
const { user, expiresAt } = data.value.data;
|
||||||
|
|
||||||
useAuthSession().value = {
|
setAuthSession({
|
||||||
type: 'WarrenAuth',
|
type: 'WarrenAuth',
|
||||||
id: token,
|
id: token,
|
||||||
user,
|
user,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
};
|
});
|
||||||
|
|
||||||
toast.success('OpenID Connect', {
|
toast.success('OpenID Connect', {
|
||||||
description: `Successfully logged in`,
|
description: `Successfully logged in`,
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ export function getAuthHeader(): ['authorization', string] | null {
|
|||||||
export function getApiHeaders(
|
export function getApiHeaders(
|
||||||
includeAuth: boolean = true
|
includeAuth: boolean = true
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {
|
||||||
|
cookie: document.cookie,
|
||||||
|
};
|
||||||
|
|
||||||
if (includeAuth) {
|
if (includeAuth) {
|
||||||
const header = getAuthHeader();
|
const header = getAuthHeader();
|
||||||
|
|||||||
@@ -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(
|
export async function moveFile(
|
||||||
warrenId: string,
|
warrenId: string,
|
||||||
currentPath: string,
|
currentPath: string,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default defineNuxtConfig({
|
|||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
apiBase: '/api',
|
apiBase: '/api',
|
||||||
cookiesSecure: 'false',
|
cookiesSecure: false,
|
||||||
cookiesSameSite: 'strict',
|
cookiesSameSite: 'strict',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user