mirror of
https://github.com/hanzoai/dataroom.git
synced 2026-11-02 22:22:45 +00:00
108 files: README, marketing copy, page titles, meta tags, footer text, email templates, .env.example, UI strings. URLs swapped from papermark.com/papermark.io to dataroom.hanzo.ai. Internal identifiers left intact per repo policy: localStorage keys (papermark.email/papermark.name/last_papermark_login — changing them logs users out), type field names (papermarkUserId), internal helper symbols (PAPERMARK_HEADERS, PapermarkSparkle component, isPapermarkUrl helper), internal-only filenames. LLM.md retains 'Upstream: Papermark' attribution line. Per-locale .po translations regenerate from source on the next lingui-extract CI run. DB migrations untouched (append-only history; rewriting them breaks installed schemas). LICENSE stays AGPL-3.0.
258 lines
8.0 KiB
TypeScript
258 lines
8.0 KiB
TypeScript
import { NextApiRequest, NextApiResponse } from "next";
|
|
|
|
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
|
import { getServerSession } from "next-auth/next";
|
|
|
|
import { errorhandler } from "@/lib/errorHandler";
|
|
import { getFile } from "@/lib/files/get-file";
|
|
import prisma from "@/lib/prisma";
|
|
import { CustomUser } from "@/lib/types";
|
|
|
|
export default async function handle(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse,
|
|
) {
|
|
if (req.method === "POST") {
|
|
// POST /api/teams/:teamId/agreements/:agreementId/download
|
|
const session = await getServerSession(req, res, authOptions);
|
|
if (!session) {
|
|
res.status(401).end("Unauthorized");
|
|
return;
|
|
}
|
|
|
|
const userId = (session.user as CustomUser).id;
|
|
const { teamId, agreementId } = req.query as {
|
|
teamId: string;
|
|
agreementId: string;
|
|
};
|
|
|
|
if (!teamId || !agreementId) {
|
|
return res.status(400).json("Missing required parameters");
|
|
}
|
|
|
|
try {
|
|
// Check if user belongs to the team and get the agreement
|
|
const team = await prisma.team.findUnique({
|
|
where: {
|
|
id: teamId,
|
|
users: {
|
|
some: {
|
|
userId,
|
|
},
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
agreements: {
|
|
where: {
|
|
id: agreementId,
|
|
deletedAt: null, // Only allow downloading non-deleted agreements
|
|
},
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
content: true,
|
|
requireName: true,
|
|
createdAt: true,
|
|
updatedAt: true,
|
|
_count: {
|
|
select: {
|
|
links: true,
|
|
responses: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!team || team.agreements.length === 0) {
|
|
return res.status(404).json("Agreement not found or unauthorized");
|
|
}
|
|
|
|
const agreement = team.agreements[0];
|
|
|
|
// Check if the content is a Hanzo Dataroom URL
|
|
const isPapermarkUrl =
|
|
agreement.content.includes("dataroom.hanzo.ai/view/") ||
|
|
agreement.content.includes("dataroom.hanzo.ai/view/");
|
|
|
|
let fileContent: string;
|
|
let filename: string;
|
|
let link: any = null;
|
|
|
|
if (isPapermarkUrl) {
|
|
// Extract linkId from Hanzo Dataroom URL
|
|
const urlParts = agreement.content.split("/view/");
|
|
if (urlParts.length < 2) {
|
|
return res.status(400).json("Invalid Hanzo Dataroom URL format");
|
|
}
|
|
|
|
const linkId = urlParts[1].split(/[/?#]/)[0]; // Get linkId, remove any query params or fragments
|
|
|
|
// Fetch the link and its document
|
|
link = await prisma.link.findUnique({
|
|
where: { id: linkId },
|
|
include: {
|
|
document: {
|
|
select: {
|
|
name: true,
|
|
file: true,
|
|
originalFile: true,
|
|
storageType: true,
|
|
type: true,
|
|
contentType: true,
|
|
versions: {
|
|
where: { isPrimary: true },
|
|
select: {
|
|
file: true,
|
|
originalFile: true,
|
|
storageType: true,
|
|
type: true,
|
|
contentType: true,
|
|
},
|
|
take: 1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!link || !link.document) {
|
|
return res
|
|
.status(404)
|
|
.json("Document not found for the provided Hanzo Dataroom URL");
|
|
}
|
|
|
|
// Use the primary version if available, otherwise use the document file
|
|
const documentVersion = link.document.versions[0];
|
|
const fileKey = documentVersion
|
|
? documentVersion.originalFile || documentVersion.file
|
|
: link.document.originalFile || link.document.file;
|
|
const storageType = documentVersion
|
|
? documentVersion.storageType
|
|
: link.document.storageType;
|
|
const fileType = documentVersion
|
|
? documentVersion.type
|
|
: link.document.type;
|
|
|
|
// Get the file URL from storage
|
|
const fileUrl = await getFile({
|
|
type: storageType,
|
|
data: fileKey,
|
|
isDownload: true,
|
|
});
|
|
|
|
// Fetch the actual file content with safety measures
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
|
|
|
|
const fileResponse = await fetch(fileUrl, {
|
|
signal: controller.signal,
|
|
// Add size limit check in the response handling
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!fileResponse.ok) {
|
|
throw new Error("Failed to fetch document content");
|
|
}
|
|
|
|
// Use the document name for filename
|
|
const docName = link.document.name.replace(/\.[^/.]+$/, ""); // Remove extension
|
|
let extension = "txt";
|
|
if (fileType === "pdf") extension = "pdf";
|
|
else if (fileType === "docs") extension = "docx";
|
|
else if (fileType === "slides") extension = "pptx";
|
|
else if (fileType === "sheet") extension = "xlsx";
|
|
|
|
filename = `${docName
|
|
.replace(/[^a-z0-9\-_]/gi, "_")
|
|
.toLowerCase()
|
|
.substring(0, 50)}_agreement.${extension}`;
|
|
|
|
// Handle different file types appropriately
|
|
const isPdf =
|
|
fileType === "pdf" || link.document.contentType?.includes("pdf");
|
|
const isDocx =
|
|
fileType === "docs" ||
|
|
link.document.contentType?.includes("wordprocessingml");
|
|
|
|
if (isPdf || isDocx) {
|
|
// Handle binary files (PDFs, Word docs)
|
|
const buffer = await fileResponse.arrayBuffer();
|
|
let contentType =
|
|
link.document.contentType || "application/octet-stream";
|
|
|
|
if (isPdf) contentType = "application/pdf";
|
|
else if (isDocx)
|
|
contentType =
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
|
|
res.setHeader("Content-Type", contentType);
|
|
res.setHeader(
|
|
"Content-Disposition",
|
|
`attachment; filename="${filename}"`,
|
|
);
|
|
res.setHeader("Content-Length", buffer.byteLength.toString());
|
|
return res.send(Buffer.from(buffer));
|
|
} else {
|
|
// Handle text-based files
|
|
fileContent = await fileResponse.text();
|
|
}
|
|
} else {
|
|
// Regular URL - return formatted metadata as before
|
|
fileContent = `
|
|
AGREEMENT DETAILS
|
|
================
|
|
|
|
Name: ${agreement.name}
|
|
URL: ${agreement.content}
|
|
Requires Name: ${agreement.requireName ? "Yes" : "No"}
|
|
Created: ${agreement.createdAt.toLocaleDateString()} at ${agreement.createdAt.toLocaleTimeString()}
|
|
Last Updated: ${agreement.updatedAt.toLocaleDateString()} at ${agreement.updatedAt.toLocaleTimeString()}
|
|
Team: ${team.name}
|
|
|
|
USAGE STATISTICS
|
|
===============
|
|
|
|
Used in ${agreement._count.links} link${agreement._count.links === 1 ? "" : "s"}
|
|
Total responses: ${agreement._count.responses}
|
|
|
|
AGREEMENT URL
|
|
=============
|
|
|
|
${agreement.content}
|
|
|
|
---
|
|
Downloaded on: ${new Date().toLocaleDateString()} at ${new Date().toLocaleTimeString()}
|
|
Agreement ID: ${agreement.id}
|
|
`.trim();
|
|
|
|
filename = `${agreement.name
|
|
.replace(/[^a-z0-9\-_]/gi, "_")
|
|
.toLowerCase()
|
|
.substring(0, 50)}_agreement.txt`;
|
|
}
|
|
|
|
// Set headers for file download (only for text files - PDFs are handled above)
|
|
if (fileContent) {
|
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
res.setHeader(
|
|
"Content-Disposition",
|
|
`attachment; filename="${filename}"`,
|
|
);
|
|
res.setHeader("Content-Length", Buffer.byteLength(fileContent, "utf8"));
|
|
return res.send(fileContent);
|
|
}
|
|
} catch (error) {
|
|
errorhandler(error, res);
|
|
}
|
|
} else {
|
|
// We only allow POST requests
|
|
res.setHeader("Allow", ["POST"]);
|
|
return res.status(405).end(`Method ${req.method} Not Allowed`);
|
|
}
|
|
}
|