diff --git a/paperless-frontend/src/components/DocumentEditModal.tsx b/paperless-frontend/src/components/DocumentEditModal.tsx
index ea3b728..7943695 100644
--- a/paperless-frontend/src/components/DocumentEditModal.tsx
+++ b/paperless-frontend/src/components/DocumentEditModal.tsx
@@ -9,6 +9,7 @@ import type { Client } from '../api/inbox';
import { paperlessApi } from '../api/paperless';
import type { PaperlessDocType, PaperlessCorrespondent } from '../api/paperless';
import { getEnv } from '../utils/env';
+import { AuthIframe, openAuthUrl } from '../utils/auth-resource';
import DocumentSearchModal from './DocumentSearchModal';
const { Option } = Select;
@@ -468,7 +469,7 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
icon={}
onClick={(e) => {
e.stopPropagation();
- window.open(`${getEnv('VITE_API_URL')}/api/paperless/inbox/pdf/${currentId}`, '_blank');
+ openAuthUrl(`${getEnv('VITE_API_URL')}/api/paperless/inbox/pdf/${currentId}`);
}}
/>
)}
@@ -508,9 +509,9 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
-
diff --git a/paperless-frontend/src/components/DocumentSearchModal.tsx b/paperless-frontend/src/components/DocumentSearchModal.tsx
index a2cc0ee..3a5573b 100644
--- a/paperless-frontend/src/components/DocumentSearchModal.tsx
+++ b/paperless-frontend/src/components/DocumentSearchModal.tsx
@@ -1,8 +1,9 @@
import { useState, useCallback, useEffect } from 'react';
-import { Modal, Input, List, Image, Typography, Space, Pagination, Spin, Button } from 'antd';
+import { Modal, Input, List, Typography, Space, Pagination, Spin, Button } from 'antd';
import { SearchOutlined, CheckOutlined } from '@ant-design/icons';
import { paperlessApi } from '../api/paperless';
import { getEnv } from '../utils/env';
+import { AuthImage } from '../utils/auth-resource';
import dayjs from 'dayjs';
const { Text } = Typography;
@@ -88,12 +89,11 @@ export default function DocumentSearchModal({ open, onCancel, onSelect }: Props)
>
}
title={doc.title}
diff --git a/paperless-frontend/src/pages/ManuellBearbeitenPage.tsx b/paperless-frontend/src/pages/ManuellBearbeitenPage.tsx
index 7dd9ac5..56bce0a 100644
--- a/paperless-frontend/src/pages/ManuellBearbeitenPage.tsx
+++ b/paperless-frontend/src/pages/ManuellBearbeitenPage.tsx
@@ -1,11 +1,12 @@
import { useEffect, useState } from 'react';
-import { Table, Popover, Image, Button, Space, message, ConfigProvider, Tooltip } from 'antd';
+import { Table, Popover, Button, Space, message, ConfigProvider, Tooltip } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { posteingangApi } from '../api/posteingang';
import type { PosteingangDocument } from '../api/posteingang';
import DocumentEditModal from '../components/DocumentEditModal';
import { getEnv } from '../utils/env';
+import { AuthImage } from '../utils/auth-resource';
export default function ManuellBearbeitenPage() {
const [data, setData] = useState([]);
@@ -55,10 +56,9 @@ export default function ManuellBearbeitenPage() {
};
const popoverContent = (id: number) => (
-
);
@@ -69,10 +69,9 @@ export default function ManuellBearbeitenPage() {
width: 150,
render: (_: any, record: PosteingangDocument) => (
-
diff --git a/paperless-frontend/src/pages/PosteingangPage.tsx b/paperless-frontend/src/pages/PosteingangPage.tsx
index 42c8741..b612c1a 100644
--- a/paperless-frontend/src/pages/PosteingangPage.tsx
+++ b/paperless-frontend/src/pages/PosteingangPage.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
-import { Table, Popover, Image, Button, Space, message, ConfigProvider, Tooltip } from 'antd';
+import { Table, Popover, Button, Space, message, ConfigProvider, Tooltip } from 'antd';
+import { AuthImage } from '../utils/auth-resource';
import { ReloadOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { posteingangApi } from '../api/posteingang';
@@ -55,10 +56,9 @@ export default function PosteingangPage() {
};
const popoverContent = (id: number) => (
-
);
@@ -69,10 +69,9 @@ export default function PosteingangPage() {
width: 150,
render: (_: any, record: PosteingangDocument) => (
-
diff --git a/paperless-frontend/src/utils/auth-resource.tsx b/paperless-frontend/src/utils/auth-resource.tsx
new file mode 100644
index 0000000..ac5b3d5
--- /dev/null
+++ b/paperless-frontend/src/utils/auth-resource.tsx
@@ -0,0 +1,107 @@
+import { useState, useEffect } from 'react';
+import { Spin } from 'antd';
+import type { CSSProperties } from 'react';
+import { getAccessToken } from '../auth/oidc';
+
+export function useAuthUrl(url: string | null | undefined): string | null {
+ const [blobUrl, setBlobUrl] = useState(null);
+
+ useEffect(() => {
+ if (!url) {
+ setBlobUrl(null);
+ return;
+ }
+ let cancelled = false;
+ let objectUrl: string | undefined;
+
+ (async () => {
+ try {
+ const token = await getAccessToken();
+ if (cancelled) return;
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
+ if (cancelled || !res.ok) return;
+ const blob = await res.blob();
+ if (cancelled) return;
+ objectUrl = URL.createObjectURL(blob);
+ setBlobUrl(objectUrl);
+ } catch {
+ // image/resource just won't show
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ setBlobUrl(null);
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
+ };
+ }, [url]);
+
+ return blobUrl;
+}
+
+interface AuthImageProps {
+ src: string;
+ width?: number | string;
+ height?: number | string;
+ style?: CSSProperties;
+ alt?: string;
+}
+
+export function AuthImage({ src, width, height, style, alt = '' }: AuthImageProps) {
+ const blobUrl = useAuthUrl(src);
+ if (!blobUrl) {
+ return (
+
+
+
+ );
+ }
+ return
;
+}
+
+interface AuthIframeProps {
+ src: string;
+ style?: CSSProperties;
+ title?: string;
+}
+
+export function AuthIframe({ src, style, title }: AuthIframeProps) {
+ const hashIdx = src.indexOf('#');
+ const cleanUrl = hashIdx >= 0 ? src.slice(0, hashIdx) : src;
+ const hash = hashIdx >= 0 ? src.slice(hashIdx) : '';
+
+ const blobUrl = useAuthUrl(cleanUrl);
+
+ if (!blobUrl) {
+ return (
+
+
+
+ );
+ }
+ return ;
+}
+
+export async function openAuthUrl(url: string): Promise {
+ try {
+ const token = await getAccessToken();
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
+ if (!res.ok) return;
+ const blob = await res.blob();
+ const blobUrl = URL.createObjectURL(blob);
+ window.open(blobUrl, '_blank');
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
+ } catch {
+ // silently fail
+ }
+}