mirror of
https://github.com/docmost/docmost.git
synced 2026-05-06 22:03:06 +08:00
feat(ee): server side PDF export
This commit is contained in:
@@ -43,6 +43,9 @@ POSTMARK_TOKEN=
|
||||
# for custom drawio server
|
||||
DRAWIO_URL=
|
||||
|
||||
# Gotenberg URL for server-side PDF export
|
||||
GOTENBERG_URL=
|
||||
|
||||
DISABLE_TELEMETRY=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 { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.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 ShareLayout from "@/features/share/components/share-layout.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>
|
||||
|
||||
<Route path={"/pdf-render/:pageId"} element={<PdfRenderPage />} />
|
||||
<Route path={"/share/:shareId"} element={<ShareRedirect />} />
|
||||
<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',
|
||||
VIEWER_COMMENTS: 'comment:viewer',
|
||||
TEMPLATES: 'templates',
|
||||
PDF_EXPORT: 'export:pdf',
|
||||
} as const;
|
||||
|
||||
export type FeatureKey = (typeof Feature)[keyof typeof Feature];
|
||||
|
||||
@@ -5,6 +5,7 @@ export enum JwtType {
|
||||
ATTACHMENT = 'attachment',
|
||||
MFA_TOKEN = 'mfa_token',
|
||||
API_KEY = 'api_key',
|
||||
PDF_RENDER = 'pdf_render',
|
||||
}
|
||||
export type JwtPayload = {
|
||||
sub: string;
|
||||
@@ -45,3 +46,9 @@ export type JwtApiKeyPayload = {
|
||||
apiKeyId: string;
|
||||
type: 'api_key';
|
||||
};
|
||||
|
||||
export type JwtPdfRenderPayload = {
|
||||
pageId: string;
|
||||
workspaceId: string;
|
||||
type: 'pdf_render';
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
JwtExchangePayload,
|
||||
JwtMfaTokenPayload,
|
||||
JwtPayload,
|
||||
JwtPdfRenderPayload,
|
||||
JwtType,
|
||||
} from '../dto/jwt-payload';
|
||||
import { User } from '@docmost/db/types/entity.types';
|
||||
@@ -115,6 +116,18 @@ export class TokenService {
|
||||
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) {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: this.environmentService.getAppSecret(),
|
||||
|
||||
@@ -75,6 +75,10 @@ export class EnvironmentService {
|
||||
return new Date(Date.now() + msUntilExpiry);
|
||||
}
|
||||
|
||||
getGotenbergUrl(): string | undefined {
|
||||
return this.configService.get<string>('GOTENBERG_URL');
|
||||
}
|
||||
|
||||
getStorageDriver(): string {
|
||||
return this.configService.get<string>('STORAGE_DRIVER', 'local');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user