Files
2025-11-25 14:21:36 +01:00

1014 lines
30 KiB
TypeScript

import { NextApiRequest, NextApiResponse } from "next";
import { LinkPreset } from "@prisma/client";
import slugify from "@sindresorhus/slugify";
import { put } from "@vercel/blob";
import { waitUntil } from "@vercel/functions";
import { z } from "zod";
import { hashToken } from "@/lib/api/auth/token";
import { createDocument } from "@/lib/documents/create-document";
import { putFileServer } from "@/lib/files/put-file-server";
import { newId } from "@/lib/id-helper";
import { extractTeamId, isValidWebhookId } from "@/lib/incoming-webhooks";
import prisma from "@/lib/prisma";
import { ratelimit } from "@/lib/redis";
import {
convertDataUrlToBuffer,
generateEncrpytedPassword,
isDataUrl,
uploadImage,
} from "@/lib/utils";
import {
getExtensionFromContentType,
getSupportedContentType,
} from "@/lib/utils/get-content-type";
import { sendLinkCreatedWebhook } from "@/lib/webhook/triggers/link-created";
import { webhookFileUrlSchema } from "@/lib/zod/url-validation";
export const config = {
// in order to enable `waitUntil` function
supportsResponseStreaming: true,
maxDuration: 120,
};
// Define a common link schema to reuse
const LinkSchema = z.object({
name: z.string().optional(),
domain: z.string().optional(),
slug: z.string().optional(),
password: z.string().optional(),
expiresAt: z.string().optional(), // ISO string date
emailProtected: z.boolean().optional(),
emailAuthenticated: z.boolean().optional(),
allowDownload: z.boolean().optional(),
enableNotification: z.boolean().optional(),
enableFeedback: z.boolean().optional(),
enableScreenshotProtection: z.boolean().optional(),
showBanner: z.boolean().optional(),
audienceType: z.enum(["GENERAL", "GROUP", "TEAM"]).optional(),
groupId: z.string().optional(),
allowList: z.array(z.string()).optional(),
denyList: z.array(z.string()).optional(),
presetId: z.string().optional(),
});
// Define validation schemas for different resource types
const BaseSchema = z.object({
resourceType: z.enum(["document.create", "link.create", "dataroom.create"]),
});
const DocumentCreateSchema = BaseSchema.extend({
resourceType: z.literal("document.create"),
fileUrl: webhookFileUrlSchema,
name: z.string(),
contentType: z.string(),
dataroomId: z.string().optional(),
folderId: z.string().nullable().optional(),
dataroomFolderId: z.string().nullable().optional(),
createLink: z.boolean().optional().default(false),
link: LinkSchema.optional(),
});
const LinkCreateSchema = BaseSchema.extend({
resourceType: z.literal("link.create"),
targetId: z.string(),
linkType: z.enum(["DOCUMENT_LINK", "DATAROOM_LINK"]),
link: LinkSchema,
});
// Schema for dataroom folder structure
const DataroomFolderSchema: z.ZodType<any> = z.lazy(() =>
z.object({
name: z.string(),
subfolders: z.array(DataroomFolderSchema).optional(),
}),
);
const DataroomCreateSchema = BaseSchema.extend({
resourceType: z.literal("dataroom.create"),
name: z.string(),
description: z.string().optional(),
folders: z.array(DataroomFolderSchema).optional(), // Create folders with hierarchy
createLink: z.boolean().optional().default(false),
link: LinkSchema.optional(),
});
const RequestBodySchema = z.discriminatedUnion("resourceType", [
DocumentCreateSchema,
LinkCreateSchema,
DataroomCreateSchema,
]);
export default async function incomingWebhookHandler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
// Get the full webhook ID from the path
const { path } = req.query;
const webhookId = Array.isArray(path) ? path.join("/") : path;
if (!webhookId || !isValidWebhookId(webhookId)) {
return res.status(400).json({ error: "Invalid webhook format" });
}
// Check for API token
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res
.status(401)
.json({ error: "Missing or invalid authorization header" });
}
const token = authHeader.replace("Bearer ", "");
const hashedToken = hashToken(token);
// Look up token in database
const restrictedToken = await prisma.restrictedToken.findUnique({
where: { hashedKey: hashedToken },
select: { teamId: true, rateLimit: true },
});
if (!restrictedToken) {
return res.status(401).json({ error: "Invalid token" });
}
// Rate limit checks for API tokens
const rateLimit = restrictedToken.rateLimit || 60; // Default rate limit of 60 requests per minute
const { success, limit, reset, remaining } = await ratelimit(
rateLimit,
"1 m",
).limit(hashedToken);
// Set rate limit headers
res.setHeader("Retry-After", reset.toString());
res.setHeader("X-RateLimit-Limit", limit.toString());
res.setHeader("X-RateLimit-Remaining", remaining.toString());
res.setHeader("X-RateLimit-Reset", reset.toString());
if (!success) {
return res.status(429).json({ error: "Too many requests" });
}
// Update last used timestamp for the token
waitUntil(
prisma.restrictedToken.update({
where: {
hashedKey: hashedToken,
},
data: {
lastUsed: new Date(),
},
}),
);
const teamId = extractTeamId(webhookId);
if (!teamId) {
return res.status(400).json({ error: "Invalid team ID in webhook" });
}
if (restrictedToken.teamId !== teamId) {
return res.status(401).json({ error: "Unauthorized" });
}
try {
// 1. Find the webhook integration
const incomingWebhook = await prisma.incomingWebhook.findUnique({
where: {
externalId: webhookId,
teamId: teamId,
},
include: { team: true },
});
if (!incomingWebhook) {
return res.status(404).json({ error: "Webhook not found" });
}
// Validate request body against the schema
const validationResult = RequestBodySchema.safeParse(req.body);
if (!validationResult.success) {
return res.status(400).json({
error: "Invalid request body",
details: validationResult.error.format(),
});
}
const validatedData = validationResult.data;
// Handle different resource types
if (validatedData.resourceType === "document.create") {
return await handleDocumentCreate(
validatedData,
incomingWebhook.teamId,
token,
res,
);
} else if (validatedData.resourceType === "link.create") {
return await handleLinkCreate(
validatedData,
incomingWebhook.teamId,
token,
res,
);
} else if (validatedData.resourceType === "dataroom.create") {
return await handleDataroomCreate(
validatedData,
incomingWebhook.teamId,
token,
res,
);
}
// This shouldn't be reached due to the validation schema, but just in case
return res.status(400).json({ error: "Invalid resource type" });
} catch (error) {
console.error("Webhook error:", error);
return res.status(500).json({ error: "Internal server error" });
}
}
/**
* Handle document.create resource type
*/
async function handleDocumentCreate(
data: z.infer<typeof DocumentCreateSchema>,
teamId: string,
token: string,
res: NextApiResponse,
) {
const {
fileUrl,
name,
contentType,
dataroomId,
createLink,
link,
folderId,
dataroomFolderId,
} = data;
// Check if the content type is supported
const supportedContentType = getSupportedContentType(contentType);
if (!supportedContentType) {
return res.status(400).json({ error: "Unsupported content type" });
}
if (dataroomId) {
// Verify dataroom exists and belongs to team
const dataroom = await prisma.dataroom.findUnique({
where: {
id: dataroomId,
teamId: teamId,
},
});
if (!dataroom) {
return res.status(400).json({ error: "Invalid dataroom ID" });
}
}
// If custom domain and slug are provided, validate them
if (createLink && link?.domain && link?.slug) {
// Check if domain exists
const domain = await prisma.domain.findUnique({
where: {
slug: link.domain,
teamId: teamId,
},
});
if (!domain) {
return res
.status(400)
.json({ error: "Domain not found or not associated with this team" });
}
// Check if the slug is already in use with this domain
const existingLink = await prisma.link.findUnique({
where: {
domainSlug_slug: {
slug: link.slug,
domainSlug: link.domain,
},
},
});
if (existingLink) {
return res
.status(400)
.json({ error: "The link with this domain and slug already exists" });
}
}
// 4. Fetch file from URL
const response = await fetch(fileUrl);
if (!response.ok) {
return res.status(400).json({ error: "Failed to fetch file from URL" });
}
// 5. Validate response content type matches expected
const responseContentType = response.headers.get("content-type");
if (!responseContentType || responseContentType.startsWith("text/html")) {
return res
.status(400)
.json({ error: "Remote resource is not a supported file type" });
}
if (!responseContentType.startsWith(contentType)) {
console.warn(
`Content type mismatch: expected ${contentType}, got ${responseContentType}`,
);
// Log but don't fail - some services return generic types
}
// 6. Convert to buffer
const fileBuffer = Buffer.from(await response.arrayBuffer());
// Ensure filename has proper extension, based on the actual response content-type when available
let fileName = name?.trim();
const actualContentType = (
responseContentType?.split(";")[0] ?? contentType
).trim();
const expectedExtension = getExtensionFromContentType(actualContentType);
if (expectedExtension) {
const lower = fileName.toLowerCase();
const dotIdx = lower.lastIndexOf(".");
const currentExt = dotIdx !== -1 ? lower.slice(dotIdx + 1) : null;
// Minimal alias map to avoid double extensions (e.g., jpg vs jpeg)
const alias: Record<string, string[]> = {
jpeg: ["jpeg", "jpg"],
jpg: ["jpg", "jpeg"],
tiff: ["tiff", "tif"],
};
const matches =
!!currentExt &&
(currentExt === expectedExtension ||
(alias[expectedExtension]?.includes(currentExt) ?? false));
if (!matches) {
fileName = `${fileName}.${expectedExtension}`;
}
}
console.log("Uploading file to storage", teamId, fileName, contentType);
// 7. Upload the file to storage
const { type: storageType, data: fileData } = await putFileServer({
file: {
name: fileName,
type: contentType,
buffer: fileBuffer,
},
teamId: teamId,
restricted: false, // allows all supported file types
});
if (!fileData || !storageType) {
return res.status(500).json({ error: "Failed to save file to storage" });
}
// 8. Create document using our service
// Note: The createDocument function doesn't accept linkData in its parameters
// so we will just pass createLink flag
const documentCreationResponse = await createDocument({
documentData: {
name: fileName,
key: fileData,
storageType: storageType,
contentType: contentType,
supportedFileType: supportedContentType,
fileSize: fileBuffer.byteLength,
},
teamId: teamId,
numPages: 1,
token: token,
createLink: createLink, // INFO: creatLink=true will not trigger a link.created webhook
});
if (!documentCreationResponse.ok) {
return res.status(500).json({ error: "Failed to create document" });
}
const document = await documentCreationResponse.json();
let newLink: any;
// If the document is added to a folder, update the folderId
if (folderId) {
const folder = await prisma.folder.findUnique({
where: { id: folderId, teamId: teamId },
select: {
id: true,
},
});
if (!folder) {
return res.status(400).json({ error: "Invalid folder ID" });
}
await prisma.document.update({
where: { id: document.id, teamId: teamId },
data: {
folderId: folder.id,
},
});
}
// If we need to customize the link, update it after creation
if (createLink && document.links && document.links.length > 0 && link) {
const linkId = document.links[0].id;
// If preset is provided, validate it
let preset: LinkPreset | null = null;
let metaImage: string | null = null;
let metaFavicon: string | null = null;
if (link?.presetId) {
preset = await prisma.linkPreset.findUnique({
where: { pId: link.presetId, teamId: teamId },
});
if (!preset) {
return res.status(400).json({
error: "Link preset not found or not associated with this team",
});
}
// Handle image files for custom meta tag (if enabled)
if (preset.enableCustomMetaTag) {
// Process meta image if present
if (preset.metaImage && isDataUrl(preset.metaImage)) {
const { buffer, mimeType, filename } = convertDataUrlToBuffer(
preset.metaImage,
);
const blob = await put(filename, buffer, {
access: "public",
addRandomSuffix: true,
});
metaImage = blob.url;
}
// Process favicon if present
if (preset.metaFavicon && isDataUrl(preset.metaFavicon)) {
const { buffer, mimeType, filename } = convertDataUrlToBuffer(
preset.metaFavicon,
);
const blob = await put(filename, buffer, {
access: "public",
addRandomSuffix: true,
});
metaFavicon = blob.url;
}
}
}
// Process fields for link update
const hashedPassword = link.password
? await generateEncrpytedPassword(link.password)
: preset?.password
? await generateEncrpytedPassword(preset.password)
: null;
const expiresAtDate = link.expiresAt
? new Date(link.expiresAt)
: preset?.expiresAt
? new Date(preset.expiresAt)
: null;
const isGroupAudience = link.audienceType === "GROUP";
let domainId = null;
if (link.domain) {
const domain = await prisma.domain.findUnique({
where: {
slug: link.domain,
teamId: teamId,
},
select: { id: true },
});
domainId = domain?.id || null;
}
// Update the link with custom settings
newLink = await prisma.link.update({
where: { id: linkId, teamId: teamId },
data: {
name: link.name,
password: hashedPassword,
expiresAt: expiresAtDate,
domainId: domainId,
domainSlug: link.domain || null,
slug: link.slug || null,
emailProtected: link.emailProtected || preset?.emailProtected || false,
emailAuthenticated:
link.emailAuthenticated || preset?.emailAuthenticated || false,
allowDownload: link.allowDownload || preset?.allowDownload,
enableNotification:
link.enableNotification ?? preset?.enableNotification ?? false,
enableFeedback: link.enableFeedback,
enableScreenshotProtection: link.enableScreenshotProtection,
showBanner: link.showBanner ?? preset?.showBanner ?? false,
audienceType: link.audienceType,
groupId: isGroupAudience ? link.groupId : null,
// For group links, ignore allow/deny lists from presets as access is controlled by group membership
allowList: isGroupAudience
? link.allowList
: (link.allowList ?? preset?.allowList),
denyList: isGroupAudience
? link.denyList
: (link.denyList ?? preset?.denyList),
...(preset?.enableCustomMetaTag && {
enableCustomMetatag: preset?.enableCustomMetaTag,
metaTitle: preset?.metaTitle,
metaDescription: preset?.metaDescription,
metaImage: metaImage,
metaFavicon: metaFavicon,
}),
},
});
waitUntil(
sendLinkCreatedWebhook({
teamId,
data: {
document_id: document.id,
link_id: newLink.id,
},
}),
);
}
// If dataroomId was provided, create the relationship
if (dataroomId) {
// If dataroomFolderId is provided, validate it belongs to the dataroom
if (dataroomFolderId) {
const dataroomFolder = await prisma.dataroomFolder.findUnique({
where: {
id: dataroomFolderId,
dataroomId: dataroomId,
},
});
if (!dataroomFolder) {
return res.status(400).json({
error:
"Invalid dataroom folder ID or folder does not belong to the specified dataroom",
});
}
}
await prisma.dataroomDocument.create({
data: {
dataroomId,
documentId: document.id,
folderId: dataroomFolderId || null,
},
});
}
return res.status(200).json({
message: `Document created successfully${
dataroomId ? ` and added to dataroom` : ""
}`,
documentId: document.id,
dataroomId: dataroomId ?? undefined,
linkId: newLink?.id ?? undefined,
linkUrl: createLink
? newLink?.domainSlug && newLink?.slug
? `https://${newLink.domainSlug}/${newLink.slug}`
: `${process.env.NEXT_PUBLIC_MARKETING_URL}/view/${newLink?.id}`
: undefined,
});
}
/**
* Handle link.create resource type
*/
async function handleLinkCreate(
data: z.infer<typeof LinkCreateSchema>,
teamId: string,
token: string,
res: NextApiResponse,
) {
const { targetId, linkType, link } = data;
// Validate target exists and belongs to the team
if (linkType === "DOCUMENT_LINK") {
const document = await prisma.document.findUnique({
where: {
id: targetId,
teamId: teamId,
},
});
if (!document) {
return res
.status(400)
.json({ error: "Document not found or not associated with this team" });
}
} else if (linkType === "DATAROOM_LINK") {
const dataroom = await prisma.dataroom.findUnique({
where: {
id: targetId,
teamId: teamId,
},
});
if (!dataroom) {
return res
.status(400)
.json({ error: "Dataroom not found or not associated with this team" });
}
}
// If domain and slug are provided, validate them
let domainId = null;
if (link.domain && link.slug) {
// Check if domain exists
const domain = await prisma.domain.findUnique({
where: {
slug: link.domain,
teamId: teamId,
},
select: { id: true },
});
if (!domain) {
return res
.status(400)
.json({ error: "Domain not found or not associated with this team" });
}
domainId = domain.id;
// Check if the slug is already in use with this domain
const existingLink = await prisma.link.findUnique({
where: {
domainSlug_slug: {
slug: link.slug,
domainSlug: link.domain,
},
},
});
if (existingLink) {
return res
.status(400)
.json({ error: "The link with this domain and slug already exists" });
}
}
// If preset is provided, validate it
let preset: LinkPreset | null = null;
let metaImage: string | null = null;
let metaFavicon: string | null = null;
if (link.presetId) {
preset = await prisma.linkPreset.findUnique({
where: { pId: link.presetId, teamId: teamId },
});
if (!preset) {
return res.status(400).json({
error: "Link preset not found or not associated with this team",
});
}
// 4. Handle image files for custom meta tag (if enabled)
if (preset.enableCustomMetaTag) {
// Process meta image if present
if (preset.metaImage && isDataUrl(preset.metaImage)) {
const { buffer, mimeType, filename } = convertDataUrlToBuffer(
preset.metaImage,
);
const blob = await put(filename, buffer, {
access: "public",
addRandomSuffix: true,
});
metaImage = blob.url;
}
// Process favicon if present
if (preset.metaFavicon && isDataUrl(preset.metaFavicon)) {
const { buffer, mimeType, filename } = convertDataUrlToBuffer(
preset.metaFavicon,
);
const blob = await put(filename, buffer, {
access: "public",
addRandomSuffix: true,
});
metaFavicon = blob.url;
}
}
}
// Create the link
try {
// Hash password if provided
const hashedPassword = link.password
? await generateEncrpytedPassword(link.password)
: preset?.password
? await generateEncrpytedPassword(preset.password)
: null;
const expiresAtDate = link.expiresAt
? new Date(link.expiresAt)
: preset?.expiresAt
? new Date(preset.expiresAt)
: null;
const isGroupAudience = link.audienceType === "GROUP";
const newLink = await prisma.link.create({
data: {
documentId: linkType === "DOCUMENT_LINK" ? targetId : null,
dataroomId: linkType === "DATAROOM_LINK" ? targetId : null,
linkType,
teamId,
name: link.name,
password: hashedPassword,
domainId: domainId,
domainSlug: link.domain || null,
slug: link.slug || null,
expiresAt: expiresAtDate,
emailProtected: link.emailProtected || preset?.emailProtected || false,
emailAuthenticated:
link.emailAuthenticated || preset?.emailAuthenticated || false,
allowDownload: link.allowDownload || preset?.allowDownload,
enableNotification:
link.enableNotification ?? preset?.enableNotification ?? false,
enableFeedback: link.enableFeedback,
enableScreenshotProtection: link.enableScreenshotProtection,
showBanner: link.showBanner ?? preset?.showBanner ?? false,
audienceType: link.audienceType,
groupId: isGroupAudience ? link.groupId : null,
// For group links, ignore allow/deny lists from presets as access is controlled by group membership
allowList: isGroupAudience
? link.allowList
: link.allowList || preset?.allowList,
denyList: isGroupAudience
? link.denyList
: link.denyList || preset?.denyList,
...(preset?.enableCustomMetaTag && {
enableCustomMetatag: preset?.enableCustomMetaTag,
metaTitle: preset?.metaTitle,
metaDescription: preset?.metaDescription,
metaImage: metaImage,
metaFavicon: metaFavicon,
}),
},
});
waitUntil(
sendLinkCreatedWebhook({
teamId,
data: {
document_id: linkType === "DOCUMENT_LINK" ? targetId : null,
dataroom_id: linkType === "DATAROOM_LINK" ? targetId : null,
link_id: newLink.id,
},
}),
);
return res.status(200).json({
message: "Link created successfully",
linkId: newLink.id,
targetId,
linkType,
linkUrl:
domainId && link.domain && link.slug
? `https://${newLink.domainSlug}/${newLink.slug}`
: `${process.env.NEXT_PUBLIC_MARKETING_URL}/view/${newLink.id}`,
});
} catch (error) {
console.error("Link creation error:", error);
return res.status(500).json({ error: "Failed to create link" });
}
}
/**
* Helper function to create dataroom folders recursively
*/
async function createDataroomFoldersRecursive(
dataroomId: string,
folders: Array<{ name: string; subfolders?: any[] }>,
parentPath: string = "",
parentId: string | null = null,
): Promise<void> {
for (const folder of folders) {
const folderPath = parentPath + "/" + slugify(folder.name);
// Create the folder
const createdFolder = await prisma.dataroomFolder.create({
data: {
name: folder.name,
path: folderPath,
parentId: parentId,
dataroomId: dataroomId,
},
});
// If the folder has subfolders, create them recursively
if (folder.subfolders && folder.subfolders.length > 0) {
await createDataroomFoldersRecursive(
dataroomId,
folder.subfolders,
folderPath,
createdFolder.id,
);
}
}
}
/**
* Handle dataroom.create resource type
*/
async function handleDataroomCreate(
data: z.infer<typeof DataroomCreateSchema>,
teamId: string,
token: string,
res: NextApiResponse,
) {
const { name, description, createLink, link, folders } = data;
// If custom domain and slug are provided for link, validate them
let domainId = null;
if (createLink && link?.domain && link?.slug) {
// Check if domain exists
const domain = await prisma.domain.findUnique({
where: {
slug: link.domain,
teamId: teamId,
},
});
if (!domain) {
return res
.status(400)
.json({ error: "Domain not found or not associated with this team" });
}
domainId = domain.id;
// Check if the slug is already in use with this domain
const existingLink = await prisma.link.findUnique({
where: {
domainSlug_slug: {
slug: link.slug,
domainSlug: link.domain,
},
},
});
if (existingLink) {
return res
.status(400)
.json({ error: "The link with this domain and slug already exists" });
}
}
// If preset is provided, validate it
let preset: LinkPreset | null = null;
let metaImage: string | null = null;
let metaFavicon: string | null = null;
if (createLink && link?.presetId) {
preset = await prisma.linkPreset.findUnique({
where: { pId: link.presetId, teamId: teamId },
});
if (!preset) {
return res.status(400).json({
error: "Link preset not found or not associated with this team",
});
}
// Handle image files for custom meta tag (if enabled)
if (preset.enableCustomMetaTag) {
// Process meta image if present
if (preset.metaImage && isDataUrl(preset.metaImage)) {
const { buffer, mimeType, filename } = convertDataUrlToBuffer(
preset.metaImage,
);
const blob = await put(filename, buffer, {
access: "public",
addRandomSuffix: true,
});
metaImage = blob.url;
}
// Process favicon if present
if (preset.metaFavicon && isDataUrl(preset.metaFavicon)) {
const { buffer, mimeType, filename } = convertDataUrlToBuffer(
preset.metaFavicon,
);
const blob = await put(filename, buffer, {
access: "public",
addRandomSuffix: true,
});
metaFavicon = blob.url;
}
}
}
// Create the dataroom
try {
// Generate unique public ID for the dataroom
const pId = newId("dataroom");
// Create dataroom with link if requested
let createData: any = {
name,
description,
teamId,
pId,
};
if (createLink && link) {
const isGroupAudience = link.audienceType === "GROUP";
const hashedPassword = link.password
? await generateEncrpytedPassword(link.password)
: preset?.password
? await generateEncrpytedPassword(preset.password)
: null;
const expiresAtDate = link.expiresAt
? new Date(link.expiresAt)
: preset?.expiresAt
? new Date(preset?.expiresAt)
: null;
createData.links = {
create: {
name: link.name,
teamId,
linkType: "DATAROOM_LINK",
domainId: domainId,
domainSlug: link.domain || null,
slug: link.slug || null,
password: hashedPassword,
expiresAt: expiresAtDate,
emailProtected:
link.emailProtected || preset?.emailProtected || false,
emailAuthenticated:
link.emailAuthenticated || preset?.emailAuthenticated || false,
allowDownload: link.allowDownload || preset?.allowDownload,
enableNotification:
link.enableNotification ?? preset?.enableNotification ?? false,
enableFeedback: link.enableFeedback,
enableScreenshotProtection: link.enableScreenshotProtection,
showBanner: link.showBanner ?? preset?.showBanner ?? false,
audienceType: link.audienceType,
groupId: isGroupAudience ? link.groupId : null,
allowList: link.allowList || preset?.allowList,
denyList: link.denyList || preset?.denyList,
...(preset?.enableCustomMetaTag && {
enableCustomMetatag: preset?.enableCustomMetaTag,
metaTitle: preset?.metaTitle,
metaDescription: preset?.metaDescription,
metaImage: metaImage,
metaFavicon: metaFavicon,
}),
},
};
}
const dataroom = await prisma.dataroom.create({
data: createData,
include: {
links: createLink, // Only include links if we're creating one
},
});
// Create folders if provided
if (folders && folders.length > 0) {
await createDataroomFoldersRecursive(dataroom.id, folders);
}
if (createLink) {
waitUntil(
sendLinkCreatedWebhook({
teamId,
data: {
dataroom_id: dataroom.id,
link_id: dataroom.links?.[0]?.id,
},
}),
);
}
return res.status(200).json({
message: "Dataroom created successfully",
dataroomId: dataroom.id,
linkId: createLink ? dataroom.links?.[0]?.id : undefined,
linkUrl: createLink
? dataroom.links?.[0]?.domainSlug && dataroom.links?.[0]?.slug
? `https://${dataroom.links?.[0]?.domainSlug}/${dataroom.links?.[0]?.slug}`
: `${process.env.NEXT_PUBLIC_MARKETING_URL}/view/${dataroom.links?.[0]?.id}`
: undefined,
});
} catch (error) {
console.error("Dataroom creation error:", error);
return res.status(500).json({ error: "Failed to create dataroom" });
}
}