mirror of
https://github.com/hanzoai/dataroom.git
synced 2026-11-02 22:22:45 +00:00
571 lines
16 KiB
TypeScript
571 lines
16 KiB
TypeScript
import { NextApiRequest, NextApiResponse } from "next";
|
|
|
|
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
|
import { ItemType } from "@prisma/client";
|
|
import { getServerSession } from "next-auth/next";
|
|
import { z } from "zod";
|
|
|
|
import { errorhandler } from "@/lib/errorHandler";
|
|
import prisma from "@/lib/prisma";
|
|
import { CustomUser } from "@/lib/types";
|
|
|
|
// Zod schema for validating permissions
|
|
const itemPermissionSchema = z.object({
|
|
view: z.boolean(),
|
|
download: z.boolean(),
|
|
itemType: z.nativeEnum(ItemType),
|
|
});
|
|
|
|
const permissionsSchema = z.record(z.string(), itemPermissionSchema);
|
|
|
|
const patchPermissionGroupSchema = z.object({
|
|
name: z.string().optional(),
|
|
description: z.string().optional(),
|
|
});
|
|
|
|
// Helper function to get parent folder IDs for a given document in a dataroom
|
|
async function getParentFolderIds(
|
|
dataroomId: string,
|
|
documentId: string,
|
|
): Promise<string[]> {
|
|
// Get the dataroom document to find which folder it's in
|
|
const dataroomDocument = await prisma.dataroomDocument.findUnique({
|
|
where: { id: documentId },
|
|
select: { folderId: true },
|
|
});
|
|
|
|
if (!dataroomDocument?.folderId) {
|
|
return []; // Document is at root level
|
|
}
|
|
|
|
// Get the folder and walk up the hierarchy
|
|
const parentFolders: string[] = [];
|
|
let currentFolderId: string | null = dataroomDocument.folderId;
|
|
|
|
while (currentFolderId) {
|
|
parentFolders.push(currentFolderId);
|
|
|
|
const folder: { parentId: string | null } | null =
|
|
await prisma.dataroomFolder.findUnique({
|
|
where: { id: currentFolderId },
|
|
select: { parentId: true },
|
|
});
|
|
|
|
currentFolderId = folder?.parentId || null;
|
|
}
|
|
|
|
return parentFolders;
|
|
}
|
|
|
|
// Helper function to ensure parent folders are visible when child documents are made visible
|
|
async function ensureParentFoldersVisible(
|
|
dataroomId: string,
|
|
groupId: string,
|
|
permissions: Record<
|
|
string,
|
|
{ itemType: ItemType; view: boolean; download: boolean }
|
|
>,
|
|
): Promise<void> {
|
|
const foldersToMakeVisible = new Set<string>();
|
|
|
|
// Find all documents that are being made visible
|
|
const visibleDocuments = Object.entries(permissions)
|
|
.filter(
|
|
([_, perm]) => perm.itemType === ItemType.DATAROOM_DOCUMENT && perm.view,
|
|
)
|
|
.map(([itemId, _]) => itemId);
|
|
|
|
// Get parent folder IDs for all visible documents
|
|
for (const documentId of visibleDocuments) {
|
|
const parentFolders = await getParentFolderIds(dataroomId, documentId);
|
|
parentFolders.forEach((folderId) => foldersToMakeVisible.add(folderId));
|
|
}
|
|
|
|
// Also handle folders that are being made visible - ensure their parent folders are visible too
|
|
const visibleFolders = Object.entries(permissions)
|
|
.filter(
|
|
([_, perm]) => perm.itemType === ItemType.DATAROOM_FOLDER && perm.view,
|
|
)
|
|
.map(([itemId, _]) => itemId);
|
|
|
|
for (const folderId of visibleFolders) {
|
|
let currentFolderId: string | null = folderId;
|
|
|
|
while (currentFolderId) {
|
|
const folder: { parentId: string | null } | null =
|
|
await prisma.dataroomFolder.findUnique({
|
|
where: { id: currentFolderId },
|
|
select: { parentId: true },
|
|
});
|
|
|
|
if (folder?.parentId) {
|
|
foldersToMakeVisible.add(folder.parentId);
|
|
currentFolderId = folder.parentId;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update or create permissions for parent folders to make them visible
|
|
if (foldersToMakeVisible.size > 0) {
|
|
const foldersToUpdate = Array.from(foldersToMakeVisible);
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
for (const folderId of foldersToUpdate) {
|
|
await tx.permissionGroupAccessControls.upsert({
|
|
where: {
|
|
groupId_itemId: {
|
|
groupId: groupId,
|
|
itemId: folderId,
|
|
},
|
|
},
|
|
create: {
|
|
groupId: groupId,
|
|
itemId: folderId,
|
|
itemType: ItemType.DATAROOM_FOLDER,
|
|
canView: true,
|
|
canDownload: false,
|
|
canDownloadOriginal: false,
|
|
},
|
|
update: {
|
|
canView: true, // Always ensure parent folders are visible
|
|
// Don't change canDownload - preserve existing setting
|
|
},
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
export default async function handle(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse,
|
|
) {
|
|
if (req.method === "GET") {
|
|
// GET /api/teams/:teamId/datarooms/:id/permission-groups/:permissionGroupId
|
|
const session = await getServerSession(req, res, authOptions);
|
|
if (!session) {
|
|
return res.status(401).end("Unauthorized");
|
|
}
|
|
|
|
const {
|
|
teamId,
|
|
id: dataroomId,
|
|
permissionGroupId,
|
|
} = req.query as {
|
|
teamId: string;
|
|
id: string;
|
|
permissionGroupId: string;
|
|
};
|
|
|
|
const userId = (session.user as CustomUser).id;
|
|
|
|
try {
|
|
// Verify team membership
|
|
const team = await prisma.team.findUnique({
|
|
where: {
|
|
id: teamId,
|
|
users: {
|
|
some: { userId },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!team) {
|
|
return res.status(401).end("Unauthorized");
|
|
}
|
|
|
|
// Verify dataroom exists and belongs to team
|
|
const dataroom = await prisma.dataroom.findUnique({
|
|
where: {
|
|
id: dataroomId,
|
|
teamId: teamId,
|
|
},
|
|
});
|
|
|
|
if (!dataroom) {
|
|
return res.status(404).json({ error: "Dataroom not found" });
|
|
}
|
|
|
|
// Fetch permission group and its access controls
|
|
const permissionGroup = await prisma.permissionGroup.findUnique({
|
|
where: {
|
|
id: permissionGroupId,
|
|
dataroomId: dataroomId,
|
|
teamId: teamId,
|
|
},
|
|
include: {
|
|
accessControls: true,
|
|
},
|
|
});
|
|
|
|
if (!permissionGroup) {
|
|
return res.status(404).json({ error: "Permission group not found" });
|
|
}
|
|
|
|
return res.status(200).json({ permissionGroup });
|
|
} catch (error) {
|
|
return errorhandler(error, res);
|
|
}
|
|
} else if (req.method === "PATCH") {
|
|
// PATCH /api/teams/:teamId/datarooms/:id/permission-groups/:permissionGroupId
|
|
const session = await getServerSession(req, res, authOptions);
|
|
if (!session) {
|
|
return res.status(401).end("Unauthorized");
|
|
}
|
|
|
|
const {
|
|
teamId,
|
|
id: dataroomId,
|
|
permissionGroupId,
|
|
} = req.query as {
|
|
teamId: string;
|
|
id: string;
|
|
permissionGroupId: string;
|
|
};
|
|
|
|
const userId = (session.user as CustomUser).id;
|
|
|
|
try {
|
|
// Verify team membership
|
|
const team = await prisma.team.findUnique({
|
|
where: {
|
|
id: teamId,
|
|
users: {
|
|
some: { userId },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!team) {
|
|
return res.status(401).end("Unauthorized");
|
|
}
|
|
|
|
// Verify dataroom exists and belongs to team
|
|
const dataroom = await prisma.dataroom.findUnique({
|
|
where: {
|
|
id: dataroomId,
|
|
teamId: teamId,
|
|
},
|
|
});
|
|
|
|
if (!dataroom) {
|
|
return res.status(404).json({ error: "Dataroom not found" });
|
|
}
|
|
|
|
// Verify permission group exists and belongs to dataroom
|
|
const permissionGroup = await prisma.permissionGroup.findUnique({
|
|
where: {
|
|
id: permissionGroupId,
|
|
dataroomId: dataroomId,
|
|
teamId: teamId,
|
|
},
|
|
});
|
|
|
|
if (!permissionGroup) {
|
|
return res.status(404).json({ error: "Permission group not found" });
|
|
}
|
|
|
|
// Validate and update permission group
|
|
const validationResult = patchPermissionGroupSchema.safeParse(req.body);
|
|
if (!validationResult.success) {
|
|
return res.status(400).json({
|
|
error: "Invalid request body",
|
|
details: validationResult.error.issues,
|
|
});
|
|
}
|
|
|
|
const { name, description } = validationResult.data;
|
|
|
|
const updatedPermissionGroup = await prisma.permissionGroup.update({
|
|
where: {
|
|
id: permissionGroupId,
|
|
},
|
|
data: {
|
|
...(name && { name }),
|
|
...(description !== undefined && { description }),
|
|
},
|
|
});
|
|
|
|
return res.status(200).json({ permissionGroup: updatedPermissionGroup });
|
|
} catch (error) {
|
|
return errorhandler(error, res);
|
|
}
|
|
} else if (req.method === "PUT") {
|
|
// PUT /api/teams/:teamId/datarooms/:id/permission-groups/:permissionGroupId
|
|
const session = await getServerSession(req, res, authOptions);
|
|
if (!session) {
|
|
return res.status(401).end("Unauthorized");
|
|
}
|
|
|
|
const {
|
|
teamId,
|
|
id: dataroomId,
|
|
permissionGroupId,
|
|
} = req.query as {
|
|
teamId: string;
|
|
id: string;
|
|
permissionGroupId: string;
|
|
};
|
|
|
|
const { permissions } = req.body;
|
|
|
|
const userId = (session.user as CustomUser).id;
|
|
|
|
try {
|
|
// Verify team membership
|
|
const team = await prisma.team.findUnique({
|
|
where: {
|
|
id: teamId,
|
|
users: {
|
|
some: { userId },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!team) {
|
|
return res.status(401).end("Unauthorized");
|
|
}
|
|
|
|
// Verify dataroom exists and belongs to team
|
|
const dataroom = await prisma.dataroom.findUnique({
|
|
where: {
|
|
id: dataroomId,
|
|
teamId: teamId,
|
|
},
|
|
});
|
|
|
|
if (!dataroom) {
|
|
return res.status(404).json({ error: "Dataroom not found" });
|
|
}
|
|
|
|
// Verify permission group exists and belongs to dataroom
|
|
const permissionGroup = await prisma.permissionGroup.findUnique({
|
|
where: {
|
|
id: permissionGroupId,
|
|
dataroomId: dataroomId,
|
|
teamId: teamId,
|
|
},
|
|
});
|
|
|
|
if (!permissionGroup) {
|
|
return res.status(404).json({ error: "Permission group not found" });
|
|
}
|
|
|
|
// Validate permissions payload using Zod
|
|
if (!permissions) {
|
|
return res.status(400).json({ error: "Permissions are required" });
|
|
}
|
|
|
|
// Validate schema structure
|
|
const validationResult = permissionsSchema.safeParse(permissions);
|
|
if (!validationResult.success) {
|
|
return res.status(400).json({
|
|
error: "Invalid permissions format",
|
|
details: validationResult.error.issues,
|
|
});
|
|
}
|
|
|
|
const validatedPermissions = validationResult.data;
|
|
|
|
const updatedPermissionGroup = await prisma.$transaction(async (tx) => {
|
|
// Get existing permissions for comparison
|
|
const existingPermissions =
|
|
await tx.permissionGroupAccessControls.findMany({
|
|
where: {
|
|
groupId: permissionGroupId,
|
|
},
|
|
});
|
|
|
|
// Create maps for efficient lookup
|
|
const existingMap = new Map(
|
|
existingPermissions.map((p) => [`${p.itemId}-${p.itemType}`, p]),
|
|
);
|
|
|
|
// Categorize permissions into updates vs creates
|
|
const toUpdate: Array<{
|
|
itemId: string;
|
|
itemType: ItemType;
|
|
canView: boolean;
|
|
canDownload: boolean;
|
|
canDownloadOriginal: boolean;
|
|
}> = [];
|
|
|
|
const toCreate: Array<{
|
|
groupId: string;
|
|
itemId: string;
|
|
itemType: ItemType;
|
|
canView: boolean;
|
|
canDownload: boolean;
|
|
canDownloadOriginal: boolean;
|
|
}> = [];
|
|
|
|
// Categorize permissions into updates vs creates
|
|
Object.entries(validatedPermissions).forEach(([itemId, permission]) => {
|
|
const key = `${itemId}-${permission.itemType}`;
|
|
const existing = existingMap.get(key);
|
|
|
|
const permissionData = {
|
|
itemId,
|
|
itemType: permission.itemType,
|
|
canView: permission.view,
|
|
canDownload: permission.download,
|
|
canDownloadOriginal: false,
|
|
};
|
|
|
|
if (existing) {
|
|
// Check if anything actually changed
|
|
if (
|
|
existing.canView !== permission.view ||
|
|
existing.canDownload !== permission.download
|
|
) {
|
|
toUpdate.push(permissionData);
|
|
}
|
|
} else {
|
|
toCreate.push({
|
|
groupId: permissionGroupId,
|
|
...permissionData,
|
|
});
|
|
}
|
|
});
|
|
|
|
// Perform updates
|
|
if (toUpdate.length > 0) {
|
|
await Promise.all(
|
|
toUpdate.map((item) =>
|
|
tx.permissionGroupAccessControls.updateMany({
|
|
where: {
|
|
groupId: permissionGroupId,
|
|
itemId: item.itemId,
|
|
itemType: item.itemType,
|
|
},
|
|
data: {
|
|
canView: item.canView,
|
|
canDownload: item.canDownload,
|
|
canDownloadOriginal: item.canDownloadOriginal,
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Perform creates
|
|
if (toCreate.length > 0) {
|
|
await tx.permissionGroupAccessControls.createMany({
|
|
data: toCreate,
|
|
});
|
|
}
|
|
|
|
// Remove permissions that are no longer in the new set
|
|
const newItemKeys = new Set(
|
|
Object.entries(validatedPermissions).map(
|
|
([itemId, permission]) => `${itemId}-${permission.itemType}`,
|
|
),
|
|
);
|
|
|
|
const toDelete = existingPermissions.filter(
|
|
(p) => !newItemKeys.has(`${p.itemId}-${p.itemType}`),
|
|
);
|
|
|
|
if (toDelete.length > 0) {
|
|
await tx.permissionGroupAccessControls.deleteMany({
|
|
where: {
|
|
id: {
|
|
in: toDelete.map((p) => p.id),
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
return permissionGroup;
|
|
});
|
|
|
|
// After saving permissions, ensure parent folders are visible for any items that were made visible
|
|
await ensureParentFoldersVisible(
|
|
dataroomId,
|
|
permissionGroupId,
|
|
validatedPermissions,
|
|
);
|
|
|
|
return res.status(200).json({ permissionGroup: updatedPermissionGroup });
|
|
} catch (error) {
|
|
return errorhandler(error, res);
|
|
}
|
|
} else if (req.method === "DELETE") {
|
|
// DELETE /api/teams/:teamId/datarooms/:id/permission-groups/:permissionGroupId
|
|
const session = await getServerSession(req, res, authOptions);
|
|
if (!session) {
|
|
return res.status(401).end("Unauthorized");
|
|
}
|
|
|
|
const {
|
|
teamId,
|
|
id: dataroomId,
|
|
permissionGroupId,
|
|
} = req.query as {
|
|
teamId: string;
|
|
id: string;
|
|
permissionGroupId: string;
|
|
};
|
|
|
|
const userId = (session.user as CustomUser).id;
|
|
|
|
try {
|
|
// Verify team membership
|
|
const team = await prisma.team.findUnique({
|
|
where: {
|
|
id: teamId,
|
|
users: {
|
|
some: { userId },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!team) {
|
|
return res.status(401).end("Unauthorized");
|
|
}
|
|
|
|
// Verify dataroom exists and belongs to team
|
|
const dataroom = await prisma.dataroom.findUnique({
|
|
where: {
|
|
id: dataroomId,
|
|
teamId: teamId,
|
|
},
|
|
});
|
|
|
|
if (!dataroom) {
|
|
return res.status(404).json({ error: "Dataroom not found" });
|
|
}
|
|
|
|
// Verify permission group exists and belongs to dataroom
|
|
const permissionGroup = await prisma.permissionGroup.findUnique({
|
|
where: {
|
|
id: permissionGroupId,
|
|
dataroomId: dataroomId,
|
|
teamId: teamId,
|
|
},
|
|
});
|
|
|
|
if (!permissionGroup) {
|
|
return res.status(404).json({ error: "Permission group not found" });
|
|
}
|
|
|
|
// Delete the permission group (this will cascade delete access controls)
|
|
await prisma.permissionGroup.delete({
|
|
where: {
|
|
id: permissionGroupId,
|
|
},
|
|
});
|
|
|
|
return res.status(200).json({ message: "Permission group deleted" });
|
|
} catch (error) {
|
|
return errorhandler(error, res);
|
|
}
|
|
}
|
|
|
|
// We only allow GET, PATCH, PUT, and DELETE requests
|
|
res.setHeader("Allow", ["GET", "PATCH", "PUT", "DELETE"]);
|
|
return res.status(405).end(`Method ${req.method} Not Allowed`);
|
|
}
|