mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat(ee): server side PDF export
This commit is contained in:
@@ -43,6 +43,9 @@ POSTMARK_TOKEN=
|
|||||||
# for custom drawio server
|
# for custom drawio server
|
||||||
DRAWIO_URL=
|
DRAWIO_URL=
|
||||||
|
|
||||||
|
# Gotenberg URL for server-side PDF export
|
||||||
|
GOTENBERG_URL=
|
||||||
|
|
||||||
DISABLE_TELEMETRY=false
|
DISABLE_TELEMETRY=false
|
||||||
|
|
||||||
# Enable debug logging in production (default: false)
|
# Enable debug logging in production (default: false)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import Security from "@/ee/security/pages/security.tsx";
|
|||||||
import License from "@/ee/licence/pages/license.tsx";
|
import License from "@/ee/licence/pages/license.tsx";
|
||||||
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
|
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
|
||||||
import SharedPage from "@/pages/share/shared-page.tsx";
|
import SharedPage from "@/pages/share/shared-page.tsx";
|
||||||
|
import PdfRenderPage from "@/ee/pdf-export/pdf-render-page.tsx";
|
||||||
import Shares from "@/pages/settings/shares/shares.tsx";
|
import Shares from "@/pages/settings/shares/shares.tsx";
|
||||||
import ShareLayout from "@/features/share/components/share-layout.tsx";
|
import ShareLayout from "@/features/share/components/share-layout.tsx";
|
||||||
import ShareRedirect from "@/pages/share/share-redirect.tsx";
|
import ShareRedirect from "@/pages/share/share-redirect.tsx";
|
||||||
@@ -81,6 +82,7 @@ export default function App() {
|
|||||||
<Route path={"/share/p/:pageSlug"} element={<SharedPage />} />
|
<Route path={"/share/p/:pageSlug"} element={<SharedPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path={"/pdf-render/:pageId"} element={<PdfRenderPage />} />
|
||||||
<Route path={"/share/:shareId"} element={<ShareRedirect />} />
|
<Route path={"/share/:shareId"} element={<ShareRedirect />} />
|
||||||
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
|
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import "@/features/editor/styles/index.css";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams, useSearchParams } from "react-router-dom";
|
||||||
|
import ReadonlyPageEditor from "@/features/editor/readonly-page-editor";
|
||||||
|
import { Container } from "@mantine/core";
|
||||||
|
|
||||||
|
type PdfRenderData = {
|
||||||
|
pageId: string;
|
||||||
|
title: string;
|
||||||
|
content: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PdfRenderPage() {
|
||||||
|
const { pageId } = useParams<{ pageId: string }>();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
|
||||||
|
const [data, setData] = useState<PdfRenderData | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pageId || !token) {
|
||||||
|
setError("Missing page ID or token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/pdf/render', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ pageId, token }),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((result) => setData(result.data))
|
||||||
|
.catch((err) => setError(err.message));
|
||||||
|
}, [pageId, token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.title) {
|
||||||
|
document.title = data.title;
|
||||||
|
}
|
||||||
|
}, [data?.title]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size={900} p={0}>
|
||||||
|
<ReadonlyPageEditor
|
||||||
|
key={data.pageId}
|
||||||
|
title={data.title}
|
||||||
|
content={data.content}
|
||||||
|
pageId={data.pageId}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ export const Feature = {
|
|||||||
SHARING_CONTROLS: 'sharing:controls',
|
SHARING_CONTROLS: 'sharing:controls',
|
||||||
VIEWER_COMMENTS: 'comment:viewer',
|
VIEWER_COMMENTS: 'comment:viewer',
|
||||||
TEMPLATES: 'templates',
|
TEMPLATES: 'templates',
|
||||||
|
PDF_EXPORT: 'export:pdf',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type FeatureKey = (typeof Feature)[keyof typeof Feature];
|
export type FeatureKey = (typeof Feature)[keyof typeof Feature];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export enum JwtType {
|
|||||||
ATTACHMENT = 'attachment',
|
ATTACHMENT = 'attachment',
|
||||||
MFA_TOKEN = 'mfa_token',
|
MFA_TOKEN = 'mfa_token',
|
||||||
API_KEY = 'api_key',
|
API_KEY = 'api_key',
|
||||||
|
PDF_RENDER = 'pdf_render',
|
||||||
}
|
}
|
||||||
export type JwtPayload = {
|
export type JwtPayload = {
|
||||||
sub: string;
|
sub: string;
|
||||||
@@ -45,3 +46,9 @@ export type JwtApiKeyPayload = {
|
|||||||
apiKeyId: string;
|
apiKeyId: string;
|
||||||
type: 'api_key';
|
type: 'api_key';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type JwtPdfRenderPayload = {
|
||||||
|
pageId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
type: 'pdf_render';
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
JwtExchangePayload,
|
JwtExchangePayload,
|
||||||
JwtMfaTokenPayload,
|
JwtMfaTokenPayload,
|
||||||
JwtPayload,
|
JwtPayload,
|
||||||
|
JwtPdfRenderPayload,
|
||||||
JwtType,
|
JwtType,
|
||||||
} from '../dto/jwt-payload';
|
} from '../dto/jwt-payload';
|
||||||
import { User } from '@docmost/db/types/entity.types';
|
import { User } from '@docmost/db/types/entity.types';
|
||||||
@@ -115,6 +116,18 @@ export class TokenService {
|
|||||||
return this.jwtService.sign(payload, expiresIn ? { expiresIn } : {});
|
return this.jwtService.sign(payload, expiresIn ? { expiresIn } : {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generatePdfRenderToken(
|
||||||
|
pageId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const payload: JwtPdfRenderPayload = {
|
||||||
|
pageId,
|
||||||
|
workspaceId,
|
||||||
|
type: JwtType.PDF_RENDER,
|
||||||
|
};
|
||||||
|
return this.jwtService.sign(payload, { expiresIn: '60s' });
|
||||||
|
}
|
||||||
|
|
||||||
async verifyJwt(token: string, tokenType: string) {
|
async verifyJwt(token: string, tokenType: string) {
|
||||||
const payload = await this.jwtService.verifyAsync(token, {
|
const payload = await this.jwtService.verifyAsync(token, {
|
||||||
secret: this.environmentService.getAppSecret(),
|
secret: this.environmentService.getAppSecret(),
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ export class EnvironmentService {
|
|||||||
return new Date(Date.now() + msUntilExpiry);
|
return new Date(Date.now() + msUntilExpiry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGotenbergUrl(): string | undefined {
|
||||||
|
return this.configService.get<string>('GOTENBERG_URL');
|
||||||
|
}
|
||||||
|
|
||||||
getStorageDriver(): string {
|
getStorageDriver(): string {
|
||||||
return this.configService.get<string>('STORAGE_DRIVER', 'local');
|
return this.configService.get<string>('STORAGE_DRIVER', 'local');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user