Skip to content

Conversation

@mfts
Copy link
Owner

@mfts mfts commented Jun 12, 2025

Summary by CodeRabbit

  • New Features

    • Introduced granular file and folder permissions for dataroom links, enabling precise control over viewing and downloading.
    • Added a dedicated UI for managing and editing file permissions within the dataroom link interface.
    • Enabled creation and management of permission groups for datarooms to support detailed access control.
    • Enhanced link creation and success flows with detailed permission summaries and improved sharing options.
    • Added status toggles to archive or reactivate links directly from the links table.
  • Improvements

    • Updated dataroom link APIs and data models to support permission groups alongside legacy group permissions.
    • Expanded UI panels for better organization and easier navigation of security and advanced controls.
    • Improved handling and display of access controls and permissions throughout the dataroom and link management experience.
    • Unified display of archived and active links with visual distinctions and per-link loading states.
  • Bug Fixes

    • Fixed inconsistencies in permission checks for downloads and document access by supporting both legacy and new permission models.

@vercel
Copy link

vercel bot commented Jun 12, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
papermark ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 12, 2025 4:10pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 12, 2025

Walkthrough

This update introduces a new granular permission system for dataroom links, centered around "Permission Groups" and their associated access controls. It adds new database tables, API endpoints, React components, and backend logic to support assigning, editing, and enforcing file-level permissions for dataroom links, alongside legacy group-based permissions. The UI and API are updated to handle both permission group and legacy group models.

Changes

File(s) / Path(s) Change Summary
prisma/migrations/20250612000000_add_permissions_to_link/migration.sql
prisma/schema/dataroom.prisma
prisma/schema/link.prisma
prisma/schema/schema.prisma
Added permissionGroupId to Link model; introduced PermissionGroup and PermissionGroupAccessControls models/tables; established relations and indexes; updated Team and Dataroom models to relate to permission groups.
lib/types.ts Extended LinkWithDataroom type to include an optional accessControls property supporting both legacy and new permission group access controls.
lib/api/links/link-data.ts Updated fetchDataroomLinkData and fetchDataroomDocumentLinkData to support both groupId and permissionGroupId, querying the appropriate access control tables and returning merged access controls.
app/api/views-dataroom/route.ts
pages/api/links/download/bulk.ts
pages/api/links/download/dataroom-document.ts
pages/api/links/download/dataroom-folder.ts
Updated download and permission logic to support both legacy and new permission groups, determining the effective group and querying the corresponding access controls for permission checks.
pages/api/links/[id]/documents/[documentId].ts
pages/api/links/[id]/index.ts
pages/api/links/domains/[...domainSlug].ts
Extended API data selection and fetching to include permissionGroupId and access controls, passing them to backend data functions and including them in API responses.
pages/api/teams/[teamId]/datarooms/[id]/permission-groups/index.ts
pages/api/teams/[teamId]/datarooms/[id]/permission-groups/[permissionGroupId].ts
Added new API routes for creating and updating permission groups and their access controls, including transactional updates and permission diffing logic.
lib/api/views/send-webhook-event.ts
lib/webhook/triggers/link-created.ts
Added permissionGroupId to webhook payloads for link events.
components/links/link-sheet/dataroom-link-sheet.tsx
components/links/link-sheet/permissions-sheet.tsx
ee/features/permissions/components/dataroom-link-sheet.tsx
ee/features/permissions/components/permissions-sheet.tsx
Introduced new React client components for dataroom link sheets and permission management, including wrappers and enterprise implementations for permission group assignment and editing.
components/links/link-sheet/index.tsx Added permissions and permissionGroupId properties to link type and default props; widened sheet content UI.
components/links/link-sheet/link-options.tsx Introduced CollapsibleSection component and reorganized link option UI into collapsible panels for better grouping.
components/links/link-sheet/link-success-sheet.tsx Added new success sheet UI component for displaying link creation summaries, including permission group and access control details.
components/links/link-sheet/og-section.tsx Adjusted tooltip and title text for custom link preview section; removed unused effect.
components/links/link-sheet/upload-section/index.tsx Updated plan requirements and upgrade action text for upload section.
components/links/links-table.tsx Added "Edit File Permissions" action, state management, and handlers for permission group editing; integrated new permission sheet UI and backend update logic for dataroom links; improved archive toggle with per-link loading state and error handling; unified archived and active links display.
components/datarooms/dataroom-header.tsx Swapped link sheet component for datarooms to new named import; updated tooltip import paths.
components/view/dataroom/dataroom-view.tsx
components/view/viewer/dataroom-viewer.tsx
Updated access control prop precedence and typing to support both legacy and new permission group access controls.

Sequence Diagram(s)

Dataroom Link Creation and Permission Group Assignment

sequenceDiagram
  participant User
  participant DataroomLinkSheet (FE)
  participant API /teams/[teamId]/datarooms/[id]/permission-groups/index.ts
  participant DB

  User->>DataroomLinkSheet (FE): Fill link form, set permissions
  DataroomLinkSheet (FE)->>API /teams/.../permission-groups/index.ts: POST create permission group (with permissions)
  API /teams/.../permission-groups/index.ts->>DB: Create PermissionGroup, PermissionGroupAccessControls
  API /teams/.../permission-groups/index.ts-->>DataroomLinkSheet (FE): Return created group and controls
  DataroomLinkSheet (FE)->>API /links/[id]/index.ts: PUT update link with permissionGroupId
  API /links/[id]/index.ts->>DB: Update Link.permissionGroupId
  API /links/[id]/index.ts-->>DataroomLinkSheet (FE): Return updated link
  DataroomLinkSheet (FE)-->>User: Show success sheet with permissions summary
Loading

Dataroom Link Access and Permission Check

sequenceDiagram
  participant Viewer
  participant Frontend
  participant API /links/domains/[...domainSlug].ts
  participant lib/api/links/link-data.ts
  participant DB

  Viewer->>Frontend: Access dataroom link
  Frontend->>API /links/domains/[...domainSlug].ts: GET link data
  API /links/domains/[...domainSlug].ts->>lib/api/links/link-data.ts: fetchDataroomLinkData({ groupId, permissionGroupId })
  lib/api/links/link-data.ts->>DB: Query Link, PermissionGroup, AccessControls
  lib/api/links/link-data.ts-->>API /links/domains/[...domainSlug].ts: Return link data with accessControls
  API /links/domains/[...domainSlug].ts-->>Frontend: Return link data
  Frontend-->>Viewer: Render dataroom view with permissions enforced
Loading

Editing File Permissions for a Dataroom Link

sequenceDiagram
  participant User
  participant LinksTable (FE)
  participant PermissionsSheet (FE)
  participant API /teams/[teamId]/datarooms/[id]/permission-groups/[permissionGroupId].ts
  participant DB

  User->>LinksTable (FE): Click "Edit File Permissions"
  LinksTable (FE)->>PermissionsSheet (FE): Open permission sheet for link
  User->>PermissionsSheet (FE): Modify permissions, save
  PermissionsSheet (FE)->>API /teams/.../permission-groups/[permissionGroupId].ts: PUT update access controls
  API /teams/.../permission-groups/[permissionGroupId].ts->>DB: Update PermissionGroupAccessControls (batch)
  API /teams/.../permission-groups/[permissionGroupId].ts-->>PermissionsSheet (FE): Return updated controls
  PermissionsSheet (FE)-->>LinksTable (FE): Notify permissions updated
  LinksTable (FE)-->>User: Show success toast, refresh link list
Loading

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

npm error Exit handler never called!
npm error This is an error with npm itself. Please report this error at:
npm error https://github.com/npm/cli/issues
npm error A complete log of this run can be found in: /.npm/_logs/2025-06-12T16_07_45_527Z-debug-0.log

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🔭 Outside diff range comments (4)
pages/api/links/[id]/documents/[documentId].ts (1)

94-103: 🛠️ Refactor suggestion

Potential precedence clash between groupId and permissionGroupId

fetchDataroomDocumentLinkData may now receive both groupId and permissionGroupId simultaneously (if the link was migrated and still has both ids).
Ensure that function defines a clear precedence rule; otherwise ambiguous access-control evaluation is possible.

app/api/views-dataroom/route.ts (1)

858-909: ⚠️ Potential issue

Composite key supplied incorrectly – may throw Prisma validation error

findUnique must receive exactly the unique/composite key object – extra fields are not allowed.
If the model has
@@unique([groupId, itemId, itemType]), the correct call is:

-await prisma.viewerGroupAccessControls.findUnique({
-  where: {
-    groupId_itemId: {
-      groupId: link.groupId,
-      itemId: dataroomDocument.id,
-    },
-    itemType: ItemType.DATAROOM_DOCUMENT,
-  },
+await prisma.viewerGroupAccessControls.findUnique({
+  where: {
+    groupId_itemId_itemType: {
+      groupId: link.groupId,
+      itemId: dataroomDocument.id,
+      itemType: ItemType.DATAROOM_DOCUMENT,
+    },
   },
   select: { canDownload: true },
});

The same applies to permissionGroupAccessControls. As written, Prisma will throw
Unknown arg 'itemType' at runtime.

pages/api/links/[id]/index.ts (1)

328-337: ⚠️ Potential issue

Validate permissionGroupId before persisting – potential privilege-escalation

permissionGroupId coming from the client is written straight to the DB without confirming that

  1. the group exists,
  2. it belongs to the same team and dataroom as the link, and
  3. the current user has access to it.

An attacker could point a link to an arbitrary permission group and silently gain (or revoke) access to items outside their scope.

+// ✱ Before calling tx.link.update
+if (linkData.permissionGroupId) {
+  const pg = await tx.permissionGroup.findUnique({
+    where: { id: linkData.permissionGroupId, teamId },
+    select: { dataroomId: true },
+  });
+  if (!pg || (!dataroomLink || pg.dataroomId !== targetId)) {
+    throw new Error("Invalid permissionGroupId for this team/dataroom");
+  }
+}
pages/api/links/download/dataroom-document.ts (1)

118-149: ⚠️ Potential issue

Guard against empty downloadDocuments

After filtering by group permissions the array may be empty, but later code dereferences downloadDocuments[0], causing a 500. Return 403 when no permitted docs remain.

♻️ Duplicate comments (1)
lib/webhook/triggers/link-created.ts (1)

96-99: Same compatibility caveat applies here

permissionGroupId is added to the link.created payload as well – be sure to keep the payload contract in sync with documentation / consumers (see comment above).

🧹 Nitpick comments (19)
components/links/link-sheet/upload-section/index.tsx (1)

199-205: Hard-coded plan strings – easy to drift

requiredPlan="data rooms plus" (lower-case) and plan: "Data Rooms Plus" (title-case) are now duplicated literals.
Consider centralising plan identifiers/labels in a shared constant enum to avoid typo-driven bugs and simplify future renames:

-import { PLAN_DATA_ROOMS_PLUS_ID, PLAN_DATA_ROOMS_PLUS_LABEL } from "@/lib/plans";
+import { PLAN_DR_PLUS_ID, PLAN_DR_PLUS_LABEL } from "@/lib/plans";

-        requiredPlan="data rooms plus"
+        requiredPlan={PLAN_DR_PLUS_ID}

-            plan: "Data Rooms Plus",
+            plan: PLAN_DR_PLUS_LABEL,
components/links/link-sheet/dataroom-link-sheet.tsx (1)

1-7: Avoid any and the extra wrapper – re-export with correct typing

The wrapper introduces an any leak and an extra render layer. You can preserve type-safety and tree-shaking by re-exporting the EE component directly (or at least typing the props).

-"use client";
-
-import { DataroomLinkSheet as DataroomLinkSheetEE } from "@/ee/features/permissions/components/dataroom-link-sheet";
-
-export function DataroomLinkSheet(props: any) {
-  return <DataroomLinkSheetEE {...props} />;
-}
+// “use client”  (keep if required)
+import { DataroomLinkSheet as DataroomLinkSheetEE } from "@/ee/features/permissions/components/dataroom-link-sheet";
+import { ComponentProps } from "react";
+
+export type DataroomLinkSheetProps = ComponentProps<typeof DataroomLinkSheetEE>;
+
+export function DataroomLinkSheet(props: DataroomLinkSheetProps) {
+  return <DataroomLinkSheetEE {...props} />;
+}
+
+// or simply:
+// export { DataroomLinkSheetEE as DataroomLinkSheet };
lib/types.ts (1)

156-159: Prefer a union inside the array for easier consumption

ViewerGroupAccessControls[] | PermissionGroupAccessControls[] forces callers to narrow
the whole array first.
Using an element-level union makes common operations (e.g. .map) type-safe without extra guards.

-  accessControls?:
-    | ViewerGroupAccessControls[]
-    | PermissionGroupAccessControls[];
+  accessControls?: (
+    | ViewerGroupAccessControls
+    | PermissionGroupAccessControls
+  )[];
components/datarooms/dataroom-header.tsx (1)

9-17: Minor typings / API nits

  1. linkType={"DATAROOM_LINK"} – if linkType is a string-literal union consider exporting a constant/enum to avoid typos.
  2. The surrounding <Button key={1}> doesn’t need a key prop; keys are only meaningful in arrays.

No blockers, just polish.

Also applies to: 72-77

components/links/link-sheet/permissions-sheet.tsx (1)

5-7: Avoid any; preserve the strong typing the EE component already exposes

Leaking any here drops all compile-time safety for callers and defeats the purpose of the thin wrapper. Re-export the existing prop type instead:

-import { PermissionsSheet as PermissionsSheetEE } from "@/ee/features/permissions/components/permissions-sheet";
-
-export function PermissionsSheet(props: any) {
-  return <PermissionsSheetEE {...props} />;
-}
+import { PermissionsSheet as PermissionsSheetEE } from "@/ee/features/permissions/components/permissions-sheet";
+
+type PermissionsSheetProps = React.ComponentProps<typeof PermissionsSheetEE>;
+
+export function PermissionsSheet(props: PermissionsSheetProps) {
+  return <PermissionsSheetEE {...props} />;
+}

This keeps the public API of the wrapper in lock-step with the enterprise component and gives consumers full IntelliSense.

components/view/viewer/dataroom-viewer.tsx (1)

105-106: Use an array of union instead of a union of arrays

ViewerGroupAccessControls[] | PermissionGroupAccessControls[] forbids mixed elements and forces downstream code to perform narrowing.
If mixed controls are ever returned (e.g. future migration period) you’ll get
Property 'itemId' does not exist on type ….

-accessControls: ViewerGroupAccessControls[] | PermissionGroupAccessControls[];
+accessControls: (ViewerGroupAccessControls | PermissionGroupAccessControls)[];

Even if today only one flavour is sent, the broader type costs nothing and removes the need for casts later.

pages/api/links/domains/[...domainSlug].ts (1)

176-178: Avoid mutating linkData in-place

linkData.accessControls = … breaks immutability assumptions and makes it harder to reason about the provenance of fields.

-          linkData = data.linkData;
-          brand = data.brand;
-          // Include access controls in the link data for the frontend
-          linkData.accessControls = data.accessControls;
+          ({ linkData, brand } = data);
+          linkData = { ...linkData, accessControls: data.accessControls };

A fresh object keeps side-effects local and avoids accidental bleed-through if data.linkData is reused elsewhere.

app/api/views-dataroom/route.ts (1)

858-866: Duplicate logic – extract a helper

The if/else blocks perform identical work against two tables.
A small helper reduces branching and future drift:

const canDownloadForGroup = async (
  table: "viewer" | "permission",
  groupId: string,
  itemId: string,
) => {
  const repo =
    table === "viewer"
      ? prisma.viewerGroupAccessControls
      : prisma.permissionGroupAccessControls;

  const rec = await repo.findUnique({
    where: {
      groupId_itemId_itemType: {
        groupId,
        itemId,
        itemType: ItemType.DATAROOM_DOCUMENT,
      },
    },
    select: { canDownload: true },
  });
  return rec?.canDownload ?? false;
};

Then:

canDownload = await canDownloadForGroup(
  link.groupId ? "viewer" : "permission",
  effectiveGroupId,
  dataroomDocument.id,
);
pages/api/links/[id]/index.ts (1)

127-129: linkData.accessControls mutates fetched data in-place

fetchDataroomLinkData most likely returns an object reused elsewhere; adding a new property in-place (linkData.accessControls = …) risks side-effects or React state-mutation warnings on the client.

Safer: create a fresh object.

-        linkData.accessControls = data.accessControls;
+        linkData = { ...linkData, accessControls: data.accessControls };
pages/api/links/download/dataroom-folder.ts (1)

134-150: Duplicate branching & unused effectiveGroupId

effectiveGroupId is computed but never consumed.
At the same time the two separate queries differ only by table name.
You can remove the duplication and the dead variable:

-const effectiveGroupId = view.groupId || view.link.permissionGroupId;
-
-if (effectiveGroupId) {
-  let groupPermissions: any[] = [];
-  if (view.groupId) {
-    groupPermissions = await prisma.viewerGroupAccessControls.findMany({ … });
-  } else if (view.link.permissionGroupId) {
-    groupPermissions = await prisma.permissionGroupAccessControls.findMany({ … });
-  }
+const groupPermissions =
+  !view.groupId && !view.link.permissionGroupId
+    ? []
+    : await (view.groupId
+        ? prisma.viewerGroupAccessControls
+        : prisma.permissionGroupAccessControls).findMany({
+        where: {
+          groupId: view.groupId ?? view.link.permissionGroupId,
+          canDownload: true,
+        },
+      });

Cleaner and easier to keep in sync.

pages/api/links/download/bulk.ts (1)

119-139: Same branching duplication as in dataroom-folder.ts

Consider extracting the common “fetch permissions by group” logic to avoid drift between the two endpoints.

pages/api/teams/[teamId]/datarooms/[id]/permission-groups/[permissionGroupId].ts (1)

211-229: N × updateMany can be replaced with a single bulk call

Running one updateMany per item scales poorly. Use a single createMany({ skipDuplicates:… }) + deleteMany or Prisma upsert to cut query count.

pages/api/links/download/dataroom-document.ts (2)

126-138: Duplicate code path for legacy vs new groups

The two queries only differ by table name. Consider a helper that selects the correct model based on group type to keep this block DRY.


140-144: Lose the any

groupPermissions and permission are typed as any. Add proper Prisma‐generated types to catch enum/value mismatches at compile time.

components/links/link-sheet/link-success-sheet.tsx (1)

50-54: domainSlug may be undefined

link.domainSlug isn’t guaranteed on LinkWithViews. Fallback to link.domain?.slug or guard to avoid undefined in URL.

components/links/link-sheet/link-options.tsx (1)

60-73: Minor UX: keyboard accessibility for collapsible headers

CollapsibleTrigger lacks role="button" / aria-expanded attributes, making it less accessible to screen-reader users.

ee/features/permissions/components/permissions-sheet.tsx (2)

190-190: Simplify the boolean expression.

The ternary operator is unnecessary here.

Apply this diff to simplify the expression:

-      view: permission ? permission.canView : hasPermissionData ? false : true,
+      view: permission ? permission.canView : !hasPermissionData,
🧰 Tools
🪛 Biome (1.9.4)

[error] 190-190: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with

(lint/complexity/noUselessTernary)


256-261: Simplify the document filtering condition.

The current condition is redundant and can be simplified for better readability.

Apply this diff to simplify the logic:

-  items
-    .filter(
-      (item) =>
-        (item.parentId === parentId && item.document) ||
-        (parentId === null && item.folderId === null && item.document),
-    )
+  items
+    .filter((item) => {
+      if (item.document) {
+        return parentId === null ? item.folderId === null : item.parentId === parentId;
+      }
+      return false;
+    })
ee/features/permissions/components/dataroom-link-sheet.tsx (1)

243-263: Use optional chaining for cleaner code.

Replace logical AND operators with optional chaining for better readability.

Apply this diff to use optional chaining:

-    let blobUrl: string | null =
-      linkData.metaImage && linkData.metaImage.startsWith("data:")
-        ? null
-        : linkData.metaImage;
-    if (linkData.metaImage && linkData.metaImage.startsWith("data:")) {
+    let blobUrl: string | null =
+      linkData.metaImage?.startsWith("data:")
+        ? null
+        : linkData.metaImage;
+    if (linkData.metaImage?.startsWith("data:")) {
       // Convert the data URL to a blob
       const blob = convertDataUrlToFile({ dataUrl: linkData.metaImage });
       // Upload the blob to vercel storage
       blobUrl = await uploadImage(blob);
     }

     // Upload meta favicon if it's a data URL
-    let blobUrlFavicon: string | null =
-      linkData.metaFavicon && linkData.metaFavicon.startsWith("data:")
-        ? null
-        : linkData.metaFavicon;
-    if (linkData.metaFavicon && linkData.metaFavicon.startsWith("data:")) {
+    let blobUrlFavicon: string | null =
+      linkData.metaFavicon?.startsWith("data:")
+        ? null
+        : linkData.metaFavicon;
+    if (linkData.metaFavicon?.startsWith("data:")) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 243-243: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 246-246: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 255-255: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 258-258: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79946b5 and 3ebcd96.

📒 Files selected for processing (30)
  • app/api/views-dataroom/route.ts (3 hunks)
  • components/datarooms/dataroom-header.tsx (2 hunks)
  • components/links/link-sheet/dataroom-link-sheet.tsx (1 hunks)
  • components/links/link-sheet/index.tsx (4 hunks)
  • components/links/link-sheet/link-options.tsx (8 hunks)
  • components/links/link-sheet/link-success-sheet.tsx (1 hunks)
  • components/links/link-sheet/og-section.tsx (1 hunks)
  • components/links/link-sheet/permissions-sheet.tsx (1 hunks)
  • components/links/link-sheet/upload-section/index.tsx (1 hunks)
  • components/links/links-table.tsx (8 hunks)
  • components/view/dataroom/dataroom-view.tsx (1 hunks)
  • components/view/viewer/dataroom-viewer.tsx (2 hunks)
  • ee/features/permissions/components/dataroom-link-sheet.tsx (1 hunks)
  • ee/features/permissions/components/permissions-sheet.tsx (1 hunks)
  • lib/api/links/link-data.ts (6 hunks)
  • lib/api/views/send-webhook-event.ts (1 hunks)
  • lib/types.ts (2 hunks)
  • lib/webhook/triggers/link-created.ts (1 hunks)
  • pages/api/links/[id]/documents/[documentId].ts (2 hunks)
  • pages/api/links/[id]/index.ts (3 hunks)
  • pages/api/links/domains/[...domainSlug].ts (3 hunks)
  • pages/api/links/download/bulk.ts (2 hunks)
  • pages/api/links/download/dataroom-document.ts (2 hunks)
  • pages/api/links/download/dataroom-folder.ts (2 hunks)
  • pages/api/teams/[teamId]/datarooms/[id]/permission-groups/[permissionGroupId].ts (1 hunks)
  • pages/api/teams/[teamId]/datarooms/[id]/permission-groups/index.ts (1 hunks)
  • prisma/migrations/20250612000000_add_permissions_to_link/migration.sql (1 hunks)
  • prisma/schema/dataroom.prisma (2 hunks)
  • prisma/schema/link.prisma (1 hunks)
  • prisma/schema/schema.prisma (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
components/datarooms/dataroom-header.tsx (2)
ee/features/permissions/components/dataroom-link-sheet.tsx (1)
  • DataroomLinkSheet (76-900)
components/links/link-sheet/dataroom-link-sheet.tsx (1)
  • DataroomLinkSheet (5-7)
components/links/link-sheet/permissions-sheet.tsx (1)
ee/features/permissions/components/permissions-sheet.tsx (1)
  • PermissionsSheet (336-1028)
🪛 Biome (1.9.4)
ee/features/permissions/components/permissions-sheet.tsx

[error] 190-190: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with

(lint/complexity/noUselessTernary)

ee/features/permissions/components/dataroom-link-sheet.tsx

[error] 243-243: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 246-246: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 255-255: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 258-258: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (15)
lib/api/views/send-webhook-event.ts (1)

97-100: Adding a new field in a public webhook payload can be breaking – consider versioning / announcing

permissionGroupId is now always included in the link object that is delivered to third-party webhooks.
For consumers performing strict schema validation this is a breaking change unless they explicitly ignore unknown properties.

At minimum:

  1. Announce the change in the changelog / developer docs.
  2. If you provide webhook “versions”, bump the version and gate the new field behind it.

No code change required if you’re fine with a silent rollout, but please verify downstream impact.

prisma/schema/schema.prisma (1)

82-83: Relation looks good – double-check onDelete behaviour

Team.permissionGroups PermissionGroup[] creates a one-to-many link but the corresponding PermissionGroup.team relation (defined in the permission-group model file) should specify an onDelete: Cascade (or the desired rule).
If that optic is missing, dangling permission groups could remain after a team is deleted.

 team   Team   @relation(fields: [teamId], references: [id], -onDelete: Cascade?)

Please confirm the setting matches your data-retention policy.

pages/api/links/[id]/documents/[documentId].ts (1)

55-57: 👍 Permission-group field correctly included in the select

Selecting permissionGroupId early avoids an extra query later – change looks good.

components/view/dataroom/dataroom-view.tsx (1)

268-270: ⚠️ Potential issue

|| will treat an intentionally empty array as “falsy” – use nullish-coalescing instead

If link.accessControls is an empty array (meaning “no explicit per-link overrides”), the current expression falls back to group?.accessControls, which is probably not what we want.
Switching to ?? preserves an empty array while still falling back on the group or [] when the value is truly null/undefined.

-          accessControls={link.accessControls || group?.accessControls || []}
+          accessControls={
+            link.accessControls ?? group?.accessControls ?? []
+          }

Likely an incorrect or invalid review comment.

components/links/link-sheet/og-section.tsx (1)

140-142: Text tweak looks good

Tooltip/title wording change is clear and user-friendly; no functional impact.

components/view/viewer/dataroom-viewer.tsx (1)

5-9: Import ordering looks fine
No concerns here.

components/links/link-sheet/index.tsx (2)

258-259: Preset application unintentionally resets notification flag to false

enableNotification is now set with !!preset.enableNotification, meaning:

  • undefinedfalse (overrides existing user choice)
  • nullfalse

Previously it kept the previous value. To restore that behaviour:

- enableNotification: !!preset.enableNotification,
+ enableNotification:
+   preset.enableNotification === undefined
+     ? prev.enableNotification
+     : preset.enableNotification,

448-448: Sheet width bump – check mobile break-points

Increasing the small-screen width from 600 px to 800 px will overflow many phones (< 414 px).
Confirm via responsive testing or add max-w-full at sm breakpoint.

prisma/schema/dataroom.prisma (1)

177-195: Duplicate item IDs across types not allowed

@@unique([groupId,itemId]) prevents storing separate rules for a document and a folder sharing the same ID (IDs are globally unique only by convention). If you ever support hierarchical reuse, include itemType in the compound key.

ee/features/permissions/components/permissions-sheet.tsx (2)

745-746: Good use of ref pattern to avoid stale closures.

The empty dependency array combined with dataRef is a good pattern here to prevent excessive re-renders while maintaining access to current data.


336-1028: Well-structured permissions management component.

The component demonstrates excellent architecture with:

  • Clear separation of tree building, permission calculation, and UI logic
  • Proper handling of hierarchical permissions with partial states
  • Good UX with the "Share Entire Dataroom" toggle
  • Appropriate use of React patterns and performance optimizations
components/links/links-table.tsx (1)

796-828: Clean integration of permission management components.

Good separation of concerns with conditional rendering of DataroomLinkSheet for datarooms and standard LinkSheet for documents. The PermissionsSheet integration is well-handled with proper state management and callbacks.

ee/features/permissions/components/dataroom-link-sheet.tsx (1)

76-900: Well-architected dataroom link management component.

The component demonstrates excellent patterns:

  • Clear separation of link creation/update logic from permission management
  • Proper handling of async operations with loading states
  • Good error handling and user feedback
  • Clean integration with the PermissionsSheet component
🧰 Tools
🪛 Biome (1.9.4)

[error] 243-243: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 246-246: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 255-255: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 258-258: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

prisma/migrations/20250612000000_add_permissions_to_link/migration.sql (1)

1-55: Well-designed database migration for granular permissions.

The migration demonstrates good practices:

  • Nullable permissionGroupId ensures backward compatibility
  • Composite unique constraint prevents duplicate access controls
  • CASCADE deletes maintain referential integrity
  • Appropriate indexes for query performance
lib/api/links/link-data.ts (1)

169-174: Clean handling of dual permission models.

Good approach to extract access controls from either ViewerGroup or PermissionGroup, maintaining backward compatibility while supporting the new permission system.

Comment on lines +53 to +56
// granular permissions
permissionGroupId String?
permissionGroup PermissionGroup? @relation(fields: [permissionGroupId], references: [id], onDelete: SetNull)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add an index & enforce exclusivity for permissionGroupId

  1. Queries on Link.permissionGroupId (present in multiple API routes) will scan the whole table.
    @@ model Link
      permissionGroupId String?
      permissionGroup   PermissionGroup? @relation(fields: [permissionGroupId], references: [id], onDelete: SetNull)
  • @@index([permissionGroupId])
  1. Only one of groupId or permissionGroupId should be set. Prisma can’t express a
    CHECK constraint yet, but adding a runtime guard in link-creation/update code would prevent inconsistent rows.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// granular permissions
permissionGroupId String?
permissionGroup PermissionGroup? @relation(fields: [permissionGroupId], references: [id], onDelete: SetNull)
// granular permissions
permissionGroupId String?
permissionGroup PermissionGroup? @relation(fields: [permissionGroupId], references: [id], onDelete: SetNull)
@@index([permissionGroupId])
🤖 Prompt for AI Agents
In prisma/schema/link.prisma around lines 53 to 56, add an index on the
permissionGroupId field to optimize queries filtering by this column.
Additionally, enforce exclusivity between groupId and permissionGroupId by
implementing a runtime check in the link creation and update logic to ensure
only one of these fields is set, since Prisma does not support CHECK constraints
for this yet.

Comment on lines 140 to 180
if (permissions && typeof permissions === "object") {
// Get existing access controls to determine what needs to be updated vs created
const existingControls =
await tx.permissionGroupAccessControls.findMany({
where: {
groupId: permissionGroupId,
},
select: {
itemId: true,
itemType: true,
canView: true,
canDownload: true,
canDownloadOriginal: true,
},
});

const existingMap = new Map(
existingControls.map((control) => [
`${control.itemId}-${control.itemType}`,
control,
]),
);

const toUpdate: Array<{
itemId: string;
itemType: any;
canView: boolean;
canDownload: boolean;
canDownloadOriginal: boolean;
}> = [];

const toCreate: Array<{
groupId: string;
itemId: string;
itemType: any;
canView: boolean;
canDownload: boolean;
canDownloadOriginal: boolean;
}> = [];

// Categorize permissions into updates vs creates
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate & sanitize incoming permissions payload

typeof permissions === "object" is too loose—malformed keys or unexpected booleans will silently pass through and write to the DB.
Add schema validation (e.g. Zod) and reject items that do not belong to the current dataroom to prevent privilege-escalation via arbitrary itemIds.

🤖 Prompt for AI Agents
In
pages/api/teams/[teamId]/datarooms/[id]/permission-groups/[permissionGroupId].ts
around lines 140 to 180, the current check for permissions using typeof
permissions === "object" is too broad and allows malformed or unauthorized data
to pass through. To fix this, implement schema validation using a library like
Zod to strictly validate the shape and types of the permissions payload.
Additionally, add logic to verify that each permission item belongs to the
current dataroom by checking itemIds against the dataroom context, and reject
any items that do not match to prevent privilege escalation.

Comment on lines 302 to 341
const handlePermissionsSave = async (permissions: any) => {
if (!editPermissionLink) return;

try {
// Update the permissions for the existing link
const res = await fetch(
`/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${targetId}/permission-groups/${editPermissionLink.permissionGroupId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
permissions: permissions,
linkId: editPermissionLink.id,
}),
},
);

if (!res.ok) {
const { error } = await res.json();
throw new Error(error ?? "Failed to update permissions");
}

// Refresh the links cache
const endpointTargetType = `${targetType.toLowerCase()}s`;
mutate(
`/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}/${encodeURIComponent(
targetId,
)}/links`,
);

setShowPermissionsSheet(false);
setEditPermissionLink(null);
toast.success("File permissions updated successfully");
} catch (error) {
console.error("Error updating file permissions:", error);
toast.error("Failed to update file permissions");
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add validation for team context before API call.

The function should validate that teamInfo?.currentTeam?.id exists before making the API call to prevent potential runtime errors.

Apply this diff to add proper validation:

 const handlePermissionsSave = async (permissions: any) => {
   if (!editPermissionLink) return;
+  if (!teamInfo?.currentTeam?.id) {
+    toast.error("Team information not available");
+    return;
+  }

   try {
     // Update the permissions for the existing link
     const res = await fetch(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handlePermissionsSave = async (permissions: any) => {
if (!editPermissionLink) return;
try {
// Update the permissions for the existing link
const res = await fetch(
`/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${targetId}/permission-groups/${editPermissionLink.permissionGroupId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
permissions: permissions,
linkId: editPermissionLink.id,
}),
},
);
if (!res.ok) {
const { error } = await res.json();
throw new Error(error ?? "Failed to update permissions");
}
// Refresh the links cache
const endpointTargetType = `${targetType.toLowerCase()}s`;
mutate(
`/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}/${encodeURIComponent(
targetId,
)}/links`,
);
setShowPermissionsSheet(false);
setEditPermissionLink(null);
toast.success("File permissions updated successfully");
} catch (error) {
console.error("Error updating file permissions:", error);
toast.error("Failed to update file permissions");
}
};
const handlePermissionsSave = async (permissions: any) => {
if (!editPermissionLink) return;
if (!teamInfo?.currentTeam?.id) {
toast.error("Team information not available");
return;
}
try {
// Update the permissions for the existing link
const res = await fetch(
`/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${targetId}/permission-groups/${editPermissionLink.permissionGroupId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
permissions: permissions,
linkId: editPermissionLink.id,
}),
},
);
if (!res.ok) {
const { error } = await res.json();
throw new Error(error ?? "Failed to update permissions");
}
// Refresh the links cache
const endpointTargetType = `${targetType.toLowerCase()}s`;
mutate(
`/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}/${encodeURIComponent(
targetId,
)}/links`,
);
setShowPermissionsSheet(false);
setEditPermissionLink(null);
toast.success("File permissions updated successfully");
} catch (error) {
console.error("Error updating file permissions:", error);
toast.error("Failed to update file permissions");
}
};
🤖 Prompt for AI Agents
In components/links/links-table.tsx around lines 302 to 341, the function
handlePermissionsSave does not validate the existence of
teamInfo?.currentTeam?.id before making the API call, which can cause runtime
errors. Add a check at the start of the function to ensure
teamInfo?.currentTeam?.id is defined; if not, exit early or handle the error
appropriately to prevent the API call from proceeding without a valid team
context.

Comment on lines +27 to +48
const effectiveGroupId = groupId || permissionGroupId;

if (effectiveGroupId) {
// Check if this is a ViewerGroup (legacy) or PermissionGroup
// First try to find ViewerGroup permissions (for backwards compatibility)
if (groupId) {
// This is a ViewerGroup (legacy behavior)
groupPermissions = await prisma.viewerGroupAccessControls.findMany({
where: {
groupId: groupId,
OR: [{ canView: true }, { canDownload: true }],
},
});
} else if (permissionGroupId) {
// This is a PermissionGroup (new behavior)
groupPermissions = await prisma.permissionGroupAccessControls.findMany({
where: {
groupId: permissionGroupId,
OR: [{ canView: true }, { canDownload: true }],
},
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Extract duplicated permission fetching logic into a helper function.

The permission fetching logic is duplicated between fetchDataroomLinkData and fetchDataroomDocumentLinkData. Consider extracting this into a reusable helper function.

Create a helper function to reduce duplication:

async function fetchGroupPermissions(
  groupId?: string,
  permissionGroupId?: string,
  itemId?: string,
  itemType?: ItemType
): Promise<ViewerGroupAccessControls[] | PermissionGroupAccessControls[]> {
  const effectiveGroupId = groupId || permissionGroupId;
  
  if (!effectiveGroupId) return [];
  
  const whereClause: any = {
    groupId: effectiveGroupId,
    OR: [{ canView: true }, { canDownload: true }],
  };
  
  if (itemId && itemType) {
    whereClause.itemId = itemId;
    whereClause.itemType = itemType;
  }
  
  if (groupId) {
    return await prisma.viewerGroupAccessControls.findMany({ where: whereClause });
  } else if (permissionGroupId) {
    return await prisma.permissionGroupAccessControls.findMany({ where: whereClause });
  }
  
  return [];
}

Also applies to: 194-221

🤖 Prompt for AI Agents
In lib/api/links/link-data.ts around lines 27 to 48 and also lines 194 to 221,
the permission fetching logic is duplicated in fetchDataroomLinkData and
fetchDataroomDocumentLinkData. Extract this logic into a single helper function
named fetchGroupPermissions that accepts groupId, permissionGroupId, itemId, and
itemType as parameters. This function should build a where clause with groupId
or permissionGroupId, include canView or canDownload conditions, and optionally
filter by itemId and itemType. Then it should query the appropriate prisma table
(viewerGroupAccessControls or permissionGroupAccessControls) based on which
groupId is provided, returning the results or an empty array if no effective
groupId is given. Replace the duplicated code in both functions with calls to
this new helper function.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
components/links/links-table.tsx (1)

304-327: ⚠️ Potential issue

Team / permission-group validation still missing before PUT request

This is the same issue flagged in a previous review: the handlePermissionsSave API call proceeds even if teamInfo?.currentTeam?.id or editPermissionLink.permissionGroupId are undefined, resulting in a malformed route and 404/500s.

   const handlePermissionsSave = async (permissions: any) => {
-    if (!editPermissionLink) return;
+    if (
+      !editPermissionLink ||
+      !teamInfo?.currentTeam?.id ||
+      !editPermissionLink.permissionGroupId
+    ) {
+      toast.error("Missing team or permission-group context");
+      return;
+    }
🧹 Nitpick comments (2)
components/links/links-table.tsx (2)

283-291: Remove redundant double negation

Static-analysis warning: if (!!groupId) can be simplified to if (groupId).

-    if (!!groupId) {
+    if (groupId) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 283-283: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation

(lint/complexity/noExtraBooleanCast)


698-709: Disable handler while the switch is loading

The switch is visually disabled via the disabled prop, yet onCheckedChange will still fire if the user toggles it before the previous request finishes (depends on implementation of the custom Switch).
Safest option is to early-return in the handler when loadingLinks.has(link.id) is true.

 onCheckedChange={(checked) => {
-  handleArchiveLink(
+  if (loadingLinks.has(link.id)) return;
+  handleArchiveLink(
     link.id,
     link.documentId ?? link.dataroomId ?? "",
     checked,
   );
 }}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ebcd96 and 7b0c5be.

📒 Files selected for processing (1)
  • components/links/links-table.tsx (12 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
components/links/links-table.tsx

[error] 396-396: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation

(lint/complexity/noExtraBooleanCast)

Comment on lines +299 to +303
const handleEditPermissions = (link: LinkWithViews) => {
setEditPermissionLink(link);
setShowPermissionsSheet(true);
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against missing permissionGroupId before opening the sheet

handleEditPermissions opens the PermissionsSheet even when the link has no associated permissionGroupId, which will immediately break the sheet’s API calls. Add a defensive check so the user gets a toast instead of a runtime error.

 const handleEditPermissions = (link: LinkWithViews) => {
-  setEditPermissionLink(link);
-  setShowPermissionsSheet(true);
+  if (!link.permissionGroupId) {
+    toast.error("This link is not associated with a permission group.");
+    return;
+  }
+  setEditPermissionLink(link);
+  setShowPermissionsSheet(true);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleEditPermissions = (link: LinkWithViews) => {
setEditPermissionLink(link);
setShowPermissionsSheet(true);
};
const handleEditPermissions = (link: LinkWithViews) => {
if (!link.permissionGroupId) {
toast.error("This link is not associated with a permission group.");
return;
}
setEditPermissionLink(link);
setShowPermissionsSheet(true);
};
🤖 Prompt for AI Agents
In components/links/links-table.tsx around lines 299 to 303, the function
handleEditPermissions opens the PermissionsSheet without checking if the link
has a permissionGroupId, causing runtime errors. Add a conditional check to
verify that link.permissionGroupId exists before calling
setShowPermissionsSheet(true). If it does not exist, show a toast notification
to inform the user instead of opening the sheet.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
pages/api/teams/[teamId]/datarooms/[id]/permission-groups/[permissionGroupId].ts (2)

155-161: ⚠️ Potential issue

Still no authorization / ownership check on itemIds
Validation now ensures shape & types, but any arbitrary itemId that belongs to a different dataroom happily passes and is written to the DB → privilege-escalation vector.
Add a check that every itemId belongs to dataroomId (or at least to teamId) before proceeding.


211-217: ⚠️ Potential issue

canDownloadOriginal hard-coded to false again

Every create/update forces canDownloadOriginal to false, silently stripping the permission.
Either (a) include it in the payload & validation, or (b) leave it untouched when not provided.

-  canDownloadOriginal: false,
+  canDownloadOriginal: permission.original ?? existing?.canDownloadOriginal ?? false,
🧹 Nitpick comments (3)
pages/api/teams/[teamId]/datarooms/[id]/permission-groups/[permissionGroupId].ts (3)

190-205: Avoid any; leverage the generated ItemType enum

itemType: any loses type safety and defeats the purpose of the earlier Zod validation.

-  itemType: any;
+  itemType: ItemType;

Same for the toCreate structure.


236-254: N+1 update/delete queries – batch them

Looping updateMany / deleteMany per item spawns O(N) round-trips.
Consider:

  1. A single updateMany with OR in where, or
  2. Use a raw SQL INSERT … ON CONFLICT for upserts, or
  3. Replace with createMany + skipDuplicates and then a single deleteMany for the leftovers.

Improves latency noticeably on large permission sets.

Also applies to: 275-286


312-314: Comment contradicts the code

// We only allow GET requests is outdated – you now allow GET and PUT.
Update the comment to avoid confusion.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b0c5be and de2bc14.

📒 Files selected for processing (3)
  • components/links/link-sheet/index.tsx (4 hunks)
  • pages/api/teams/[teamId]/datarooms/[id]/permission-groups/[permissionGroupId].ts (1 hunks)
  • pages/api/teams/[teamId]/datarooms/[id]/permission-groups/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/links/link-sheet/index.tsx
  • pages/api/teams/[teamId]/datarooms/[id]/permission-groups/index.ts

Comment on lines +46 to +53
const team = await prisma.team.findUnique({
where: {
id: teamId,
users: {
some: { userId },
},
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

findUnique will throw – relational filters are not allowed

findUnique only accepts the fields that make up a unique or composite-unique constraint.
Passing users: { some … } (or any relational filter) violates this rule and will crash at runtime.
Use findFirst (or findUnique on a dedicated composite key) instead.

-const team = await prisma.team.findUnique({
+const team = await prisma.team.findFirst({
   where: {
     id: teamId,
     users: { some: { userId } },
   },
 });

Same pattern appears for dataroom & permissionGroup look-ups below – fix them too.

Also applies to: 111-118

🤖 Prompt for AI Agents
In
pages/api/teams/[teamId]/datarooms/[id]/permission-groups/[permissionGroupId].ts
around lines 46 to 53 and also lines 111 to 118, replace all uses of prisma's
findUnique that include relational filters like users: { some: { userId } } with
findFirst, since findUnique only accepts unique fields and will throw an error
with relational filters. Update the dataroom and permissionGroup look-ups
similarly to use findFirst when filtering by relations.

Comment on lines +221 to +224
if (
existing.canView !== permission.view ||
existing.canDownload !== permission.download
) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Change-detection ignores canDownloadOriginal

Even if you expose canDownloadOriginal, the diff only checks view/download.
Include the field in the comparison or you'll miss updates and accidentally overwrite on unrelated changes.

-  existing.canDownload !== permission.download
+  existing.canDownload !== permission.download ||
+  existing.canDownloadOriginal !== permission.original

Also applies to: 246-249

🤖 Prompt for AI Agents
In
pages/api/teams/[teamId]/datarooms/[id]/permission-groups/[permissionGroupId].ts
around lines 221-224 and 246-249, the change detection logic only compares
canView and canDownload fields but ignores canDownloadOriginal. Update the
conditional checks to also compare existing.canDownloadOriginal with
permission.canDownloadOriginal to ensure all relevant permission changes are
detected and handled correctly.

Comment on lines +60 to +66
const permissionGroup = await prisma.permissionGroup.findUnique({
where: {
id: permissionGroupId,
dataroomId: dataroomId,
teamId: teamId,
},
include: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Compound filters with findUnique are invalid

Each of these findUnique calls mixes the primary key id with extra fields (dataroomId, teamId).
Unless you created a composite unique on all of those columns, Prisma validation will fail.
Switch to findFirst or reduce the where clause to the unique key only.

Also applies to: 124-131, 137-143

🤖 Prompt for AI Agents
In
pages/api/teams/[teamId]/datarooms/[id]/permission-groups/[permissionGroupId].ts
around lines 60 to 66, the code uses prisma.permissionGroup.findUnique with a
where clause containing multiple fields (id, dataroomId, teamId), which is
invalid unless a composite unique constraint exists on all those fields. To fix
this, replace findUnique with findFirst to allow filtering by multiple fields,
or modify the where clause to only include the unique key field if no composite
unique constraint exists. Apply the same fix to similar calls around lines
124-131 and 137-143.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
ee/features/permissions/components/dataroom-link-sheet.tsx (1)

702-706: Implement the “add group” flow instead of early-returning.

When the user chooses add_group the handler just returns, leaving them stuck. Either navigate to the group-creation screen or open the group sheet as originally intended.

This was pointed out in a previous review; please address or justify.

components/links/links-table.tsx (2)

298-301: Guard against missing permissionGroupId before opening the sheet.

Opening PermissionsSheet with a link that lacks permissionGroupId will break internal fetches. Add:

if (!link.permissionGroupId) {
  toast.error("This link is not associated with a permission group.");
  return;
}

Same feedback was given earlier – please address.


303-344: Validate teamInfo.currentTeam.id before hitting the API.

handlePermissionsSave constructs URLs with ${teamInfo?.currentTeam?.id} but never checks that the value exists. If the team context hasn’t loaded yet you’ll fire fetch /api/teams/undefined/..., returning 404 and showing a generic error to the user.

Add an early guard:

if (!teamInfo?.currentTeam?.id) {
  toast.error("Team context not available.");
  return;
}
🧹 Nitpick comments (2)
ee/features/permissions/components/dataroom-link-sheet.tsx (1)

242-263: Deduplicate data-URL checks & favour optional chaining.

The four-way linkData.metaImage && linkData.metaImage.startsWith("data:") pattern is verbose. Consider:

const isDataUrl = (u?: string | null) => u?.startsWith("data:");
const metaImageUrl = isDataUrl(linkData.metaImage)
  ? await uploadImage(convertDataUrlToFile({ dataUrl: linkData.metaImage! }))
  : linkData.metaImage;

Same for favicon. Improves readability and matches the linter’s “useOptionalChain” suggestion.

🧰 Tools
🪛 Biome (1.9.4)

[error] 243-243: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 246-246: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 255-255: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 258-258: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

components/links/links-table.tsx (1)

436-447: Minor: drop redundant double negation.

if (!!groupId)if (groupId). Keeps codebase consistent and removes the linter warning.

🧰 Tools
🪛 Biome (1.9.4)

[error] 436-436: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation

(lint/complexity/noExtraBooleanCast)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de2bc14 and 1524271.

📒 Files selected for processing (4)
  • components/links/link-sheet/dataroom-link-sheet.tsx (1 hunks)
  • components/links/link-sheet/index.tsx (5 hunks)
  • components/links/links-table.tsx (12 hunks)
  • ee/features/permissions/components/dataroom-link-sheet.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/links/link-sheet/dataroom-link-sheet.tsx
  • components/links/link-sheet/index.tsx
🧰 Additional context used
🪛 Biome (1.9.4)
components/links/links-table.tsx

[error] 436-436: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation

(lint/complexity/noExtraBooleanCast)

ee/features/permissions/components/dataroom-link-sheet.tsx

[error] 243-243: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 246-246: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 255-255: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 258-258: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

Comment on lines +320 to +364
link: any,
permissions: ItemPermission | null,
isUpdating: boolean,
) => {
// If permissions are null, user wants full dataroom access - no permission group needed
if (permissions === null) {
// If updating and link had a permission group, we might want to remove it
// For now, we'll leave existing permission groups as they are
return;
}

// If permissions is an empty object, user wants no access - still create/update permission group
// If permissions has items, create/update permission group with those permissions
if (permissions && typeof permissions === "object") {
if (isUpdating && currentLink?.permissionGroupId) {
// Update existing permission group
await fetch(
`/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${targetId}/permission-groups/${currentLink.permissionGroupId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
permissions: permissions,
}),
},
);
} else {
// Create new permission group
await fetch(
`/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${targetId}/permission-groups`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
permissions: permissions,
linkId: link.id,
}),
},
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Stale permission-group possible when permissions are cleared.

If a link previously had a permissionGroupId and the user now sets permissions to “full access” (null), the old permission group is left intact, so the DB contains an unused record and the link still references it. Persist either a DELETE or PUT with empty permissions to keep data in sync.

🤖 Prompt for AI Agents
In ee/features/permissions/components/dataroom-link-sheet.tsx around lines 320
to 364, when permissions are set to null (full access) but the link previously
had a permissionGroupId, the old permission group is not removed or cleared,
causing stale data. To fix this, add logic to detect this case and either send a
DELETE request to remove the old permission group or a PUT request with empty
permissions to clear it, ensuring the database and link state stay synchronized.

Comment on lines +235 to +317
const createOrUpdateLinkWithPermissions = async (
linkData: DEFAULT_LINK_TYPE,
permissions: ItemPermission | null,
shouldPreview: boolean = false,
showSuccess: boolean = false,
) => {
// Upload the image if it's a data URL
let blobUrl: string | null =
linkData.metaImage && linkData.metaImage.startsWith("data:")
? null
: linkData.metaImage;
if (linkData.metaImage && linkData.metaImage.startsWith("data:")) {
// Convert the data URL to a blob
const blob = convertDataUrlToFile({ dataUrl: linkData.metaImage });
// Upload the blob to vercel storage
blobUrl = await uploadImage(blob);
}

// Upload meta favicon if it's a data URL
let blobUrlFavicon: string | null =
linkData.metaFavicon && linkData.metaFavicon.startsWith("data:")
? null
: linkData.metaFavicon;
if (linkData.metaFavicon && linkData.metaFavicon.startsWith("data:")) {
const blobFavicon = convertDataUrlToFile({
dataUrl: linkData.metaFavicon,
});
blobUrlFavicon = await uploadImage(blobFavicon);
}

let endpoint = "/api/links";
let method = "POST";
const isUpdating = !!currentLink?.id;

if (isUpdating) {
endpoint = `/api/links/${currentLink.id}`;
method = "PUT";
}

const response = await fetch(endpoint, {
method: method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...linkData,
metaImage: blobUrl,
metaFavicon: blobUrlFavicon,
targetId: targetId,
linkType: linkType,
teamId: teamInfo?.currentTeam?.id,
}),
});

if (!response.ok) {
// handle error with toast message
const { error } = await response.json();
toast.error(error);
setIsSaving(false);
return;
}

const returnedLink = await response.json();

// Handle permissions
await handlePermissionGroupOperations(
returnedLink,
permissions,
isUpdating,
);

// Handle UI updates and notifications
await handlePostSaveOperations(
returnedLink,
isUpdating,
showSuccess,
shouldPreview,
permissions,
);

setData(DEFAULT_LINK_PROPS(linkType, groupId));
setIsSaving(false);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Break up createOrUpdateLinkWithPermissions for maintainability.

At ~180 LoC the method mixes upload logic, API branching, permission-group handling and post-save UI work. Extract helpers (e.g. uploadMetaAssets, persistLink, syncPermissionGroup, updateCaches) to keep each concern < 50 LoC and easier to test.

🧰 Tools
🪛 Biome (1.9.4)

[error] 243-243: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 246-246: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 255-255: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 258-258: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🤖 Prompt for AI Agents
In ee/features/permissions/components/dataroom-link-sheet.tsx around lines 235
to 317, the createOrUpdateLinkWithPermissions function is too long and mixes
multiple concerns like image uploading, API calls, permission handling, and UI
updates. Refactor by extracting separate helper functions such as
uploadMetaAssets for handling image uploads, persistLink for the API request
logic, syncPermissionGroup for permission-related operations, and updateCaches
or handlePostSaveOperations for UI updates and notifications. This will keep
each function under 50 lines and improve maintainability and testability.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
ee/features/permissions/components/dataroom-link-sheet.tsx (1)

320-329: Stale permission group persists when permissions reset to “Full access”
Same issue raised in a previous review – the early return leaves the old permissionGroupId hanging around and the DB record orphaned.
Please implement a DELETE or a PUT with empty permissions when permissions === null.

components/links/links-table.tsx (2)

305-336: Still no team-context null-check before API call

Previous review already highlighted the missing guard for teamInfo?.currentTeam?.id.
If the context is undefined (e.g. immediately after a hard refresh) these two fetches will hit /api/teams/undefined/..., returning 404.


299-303: 🛠️ Refactor suggestion

Opening PermissionsSheet without a group may 404

handleEditPermissions shows the sheet even when link.permissionGroupId is null.
<PermissionsSheet> receives permissionGroupId={undefined} which will likely trigger a GET /…/permission-groups/undefined unless the sheet guards internally.

Recommended quick fix:

 const handleEditPermissions = (link: LinkWithViews) => {
-  setEditPermissionLink(link);
-  setShowPermissionsSheet(true);
+  if (!link.permissionGroupId) {
+    toast.error("Link does not have custom permissions yet.");
+    return;
+  }
+  setEditPermissionLink(link);
+  setShowPermissionsSheet(true);
 };
🧹 Nitpick comments (2)
ee/features/permissions/components/dataroom-link-sheet.tsx (1)

241-264: Factor out duplicated image-upload block

The favicon/upload logic is a copy-paste of the meta-image branch. Extracting a small helper (e.g. await maybeUploadDataUrl(dataUrl)) removes ~20 LoC, reduces cyclomatic complexity and makes the function easier to test.

🧰 Tools
🪛 Biome (1.9.4)

[error] 243-243: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 246-246: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 255-255: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 258-258: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

components/links/links-table.tsx (1)

740-752: Redundant double-negation & misleading param naming

checked={!link.isArchived} and later toast.success(!isArchived ? …) make it hard to reason about the current vs. desired state.
You can drop the ! on the toast line and rename the parameter to isActive for clarity:

- toast.success(
-   !isArchived ? "Link successfully archived" : "Link successfully reactivated",
- );
+ toast.success(
+   isActive ? "Link successfully reactivated" : "Link successfully archived",
+ );

(Static analysis flagged the redundant double negation.)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1524271 and 805b58a.

📒 Files selected for processing (3)
  • components/links/link-sheet/index.tsx (9 hunks)
  • components/links/links-table.tsx (13 hunks)
  • ee/features/permissions/components/dataroom-link-sheet.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/links/link-sheet/index.tsx
🧰 Additional context used
🪛 Biome (1.9.4)
components/links/links-table.tsx

[error] 437-437: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation

(lint/complexity/noExtraBooleanCast)

ee/features/permissions/components/dataroom-link-sheet.tsx

[error] 243-243: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 246-246: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 255-255: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 258-258: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (1)
ee/features/permissions/components/dataroom-link-sheet.tsx (1)

139-142: useEffect misses reactive deps – sheet can show stale defaults

linkType, groupId, and isDatarooms influence the computed default props, but only currentLink is listed in the dependency array.
Navigating to another dataroom (or toggling the product tier) while the sheet is open will leave the form in an inconsistent state.

-  useEffect(() => {
-    setData(currentLink || DEFAULT_LINK_PROPS(linkType, groupId, !isDatarooms));
-  }, [currentLink]);
+  useEffect(() => {
+    setData(
+      currentLink || DEFAULT_LINK_PROPS(linkType, groupId, !isDatarooms),
+    );
+  }, [currentLink, linkType, groupId, isDatarooms]);

@mfts mfts merged commit 54a8352 into main Jun 13, 2025
4 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Jun 13, 2025
@mfts mfts deleted the feat/improvements branch August 19, 2025 07:58
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants