snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
This commit is contained in:
142
apps/live/src/controllers/pdf-export.controller.ts
Normal file
142
apps/live/src/controllers/pdf-export.controller.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { Effect, Schema, Cause } from "effect";
|
||||
import { Controller, Post } from "@plane/decorators";
|
||||
import { logger } from "@plane/logger";
|
||||
import { AppError } from "@/lib/errors";
|
||||
import { PdfExportRequestBody, PdfValidationError, PdfAuthenticationError } from "@/schema/pdf-export";
|
||||
import { PdfExportService, exportToPdf } from "@/services/pdf-export";
|
||||
import type { PdfExportInput } from "@/services/pdf-export";
|
||||
|
||||
@Controller("/pdf-export")
|
||||
export class PdfExportController {
|
||||
/**
|
||||
* Parses and validates the request, returning a typed input object
|
||||
*/
|
||||
private parseRequest(
|
||||
req: Request,
|
||||
requestId: string
|
||||
): Effect.Effect<PdfExportInput, PdfValidationError | PdfAuthenticationError> {
|
||||
return Effect.gen(function* () {
|
||||
const cookie = req.headers.cookie || "";
|
||||
if (!cookie) {
|
||||
return yield* Effect.fail(
|
||||
new PdfAuthenticationError({
|
||||
message: "Authentication required",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const body = yield* Schema.decodeUnknown(PdfExportRequestBody)(req.body).pipe(
|
||||
Effect.mapError(
|
||||
(cause) =>
|
||||
new PdfValidationError({
|
||||
message: "Invalid request body",
|
||||
cause,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
pageId: body.pageId,
|
||||
workspaceSlug: body.workspaceSlug,
|
||||
projectId: body.projectId,
|
||||
title: body.title,
|
||||
author: body.author,
|
||||
subject: body.subject,
|
||||
pageSize: body.pageSize,
|
||||
pageOrientation: body.pageOrientation,
|
||||
fileName: body.fileName,
|
||||
noAssets: body.noAssets,
|
||||
cookie,
|
||||
requestId,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps domain errors to HTTP responses
|
||||
*/
|
||||
private mapErrorToHttpResponse(error: unknown): { status: number; error: string } {
|
||||
if (error && typeof error === "object" && "_tag" in error) {
|
||||
const tag = (error as { _tag: string })._tag;
|
||||
const message = (error as { message?: string }).message || "Unknown error";
|
||||
|
||||
switch (tag) {
|
||||
case "PdfValidationError":
|
||||
return { status: 400, error: message };
|
||||
case "PdfAuthenticationError":
|
||||
return { status: 401, error: message };
|
||||
case "PdfContentFetchError":
|
||||
return {
|
||||
status: message.includes("not found") ? 404 : 502,
|
||||
error: message,
|
||||
};
|
||||
case "PdfTimeoutError":
|
||||
return { status: 504, error: message };
|
||||
case "PdfGenerationError":
|
||||
return { status: 500, error: message };
|
||||
case "PdfMetadataFetchError":
|
||||
case "PdfImageProcessingError":
|
||||
return { status: 502, error: message };
|
||||
default:
|
||||
return { status: 500, error: message };
|
||||
}
|
||||
}
|
||||
return { status: 500, error: "Failed to generate PDF" };
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
async exportToPdf(req: Request, res: Response) {
|
||||
const requestId = crypto.randomUUID();
|
||||
|
||||
const effect = Effect.gen(this, function* () {
|
||||
// Parse request
|
||||
const input = yield* this.parseRequest(req, requestId);
|
||||
|
||||
// Delegate to service
|
||||
return yield* exportToPdf(input);
|
||||
}).pipe(
|
||||
// Log errors before catching them
|
||||
Effect.tapError((error) => Effect.logError("PDF_EXPORT: Export failed", { requestId, error })),
|
||||
// Map all tagged errors to HTTP responses
|
||||
Effect.catchAll((error) => Effect.succeed(this.mapErrorToHttpResponse(error))),
|
||||
// Handle unexpected defects
|
||||
Effect.catchAllDefect((defect) => {
|
||||
const appError = new AppError(Cause.pretty(Cause.die(defect)), {
|
||||
context: { requestId, operation: "exportToPdf" },
|
||||
});
|
||||
logger.error("PDF_EXPORT: Unexpected failure", appError);
|
||||
return Effect.succeed({ status: 500, error: "Failed to generate PDF" });
|
||||
})
|
||||
);
|
||||
|
||||
const result = await Effect.runPromise(Effect.provide(effect, PdfExportService.Default));
|
||||
|
||||
// Check if result is an error response
|
||||
if ("error" in result && "status" in result) {
|
||||
return res.status(result.status).json({ message: result.error });
|
||||
}
|
||||
|
||||
// Success - send PDF
|
||||
const { pdfBuffer, outputFileName } = result;
|
||||
|
||||
// Sanitize filename for Content-Disposition header to prevent header injection
|
||||
const sanitizedFileName = outputFileName
|
||||
.replace(/["\\\r\n]/g, "") // Remove quotes, backslashes, and CRLF
|
||||
.replace(/[^\x20-\x7E]/g, "_"); // Replace non-ASCII with underscore
|
||||
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${sanitizedFileName}"; filename*=UTF-8''${encodeURIComponent(outputFileName)}`
|
||||
);
|
||||
res.setHeader("Content-Length", pdfBuffer.length);
|
||||
return res.send(pdfBuffer);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user