Skip to content

Conversation

@AshishViradiya153
Copy link
Contributor

@AshishViradiya153 AshishViradiya153 commented Sep 4, 2025

Summary by CodeRabbit

  • New Features
    • Slack integration: connect from Settings to receive notifications for document views, dataroom access, and downloads.
    • Channel management: select up to five channels and configure per-channel notification types.
    • Dedicated Slack settings page with connect/disconnect, enable toggle, and loading states; new sidebar and breadcrumb entries plus Slack icon.
  • Bug Fixes
    • Improved watermark timestamps by safely falling back when view time is unavailable.
  • Chores
    • Added Slack feature flag to control availability.
    • Minor UI and formatting cleanups with no functional changes.

@vercel
Copy link

vercel bot commented Sep 4, 2025

@AshishViradiya153 is attempting to deploy a commit to the mftsio Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 4, 2025

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

📥 Commits

Reviewing files that changed from the base of the PR and between fcda408 and 977fee1.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (33)
  • app/api/integrations/slack/oauth/authorize/route.ts (1 hunks)
  • app/api/integrations/slack/oauth/callback/route.ts (1 hunks)
  • app/api/views-dataroom/route.ts (3 hunks)
  • app/api/views/route.ts (2 hunks)
  • components/emails/installed-integration-notification.tsx (1 hunks)
  • components/settings/settings-header.tsx (2 hunks)
  • lib/featureFlags/index.ts (2 hunks)
  • lib/integrations/install.ts (1 hunks)
  • lib/integrations/slack/client.ts (1 hunks)
  • lib/integrations/slack/env.ts (1 hunks)
  • lib/integrations/slack/events.ts (1 hunks)
  • lib/integrations/slack/install.ts (1 hunks)
  • lib/integrations/slack/templates.ts (1 hunks)
  • lib/integrations/slack/types.ts (1 hunks)
  • lib/integrations/slack/uninstall.ts (1 hunks)
  • lib/integrations/slack/utils.ts (1 hunks)
  • lib/swr/use-slack-channels.ts (1 hunks)
  • lib/swr/use-slack-integration.ts (1 hunks)
  • pages/api/links/download/bulk.ts (5 hunks)
  • pages/api/links/download/dataroom-document.ts (4 hunks)
  • pages/api/links/download/dataroom-folder.ts (3 hunks)
  • pages/api/links/download/index.ts (4 hunks)
  • pages/api/teams/[teamId]/datarooms/[id]/documents/index.ts (0 hunks)
  • pages/api/teams/[teamId]/integrations/slack/channels.ts (1 hunks)
  • pages/api/teams/[teamId]/integrations/slack/index.ts (1 hunks)
  • pages/settings/slack.tsx (1 hunks)
  • prisma/migrations/20250913000000_add_integrations/migration.sql (1 hunks)
  • prisma/schema/conversation.prisma (2 hunks)
  • prisma/schema/dataroom.prisma (2 hunks)
  • prisma/schema/document.prisma (1 hunks)
  • prisma/schema/integration.prisma (1 hunks)
  • prisma/schema/schema.prisma (2 hunks)
  • prisma/schema/team.prisma (1 hunks)
 _________________________
< No pants, fast results! >
 -------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

Walkthrough

Introduces Slack integration across backend and UI: OAuth install flow, DB models/migration, client and event templating, team APIs to manage integration and channels, email notification on install, settings UI (connect, disconnect, channel selection, toggles), and background Slack notifications on document views, dataroom access, and downloads. Adds token encryption utilities and nodemailer dependency.

Changes

Cohort / File(s) Summary
Slack core: client, events, templates, types, crypto
lib/slack/client.ts, lib/slack/events.ts, lib/slack/templates.ts, lib/types/slack.ts, lib/utils.ts
New Slack client (OAuth, channels, postMessage), event manager with helpers (document_view, dataroom_access, document_download), Block Kit message builders, expanded Slack types, and AES-based token encrypt/decrypt utilities.
Slack install flow APIs
pages/api/teams/[teamId]/slack/oauth/authorize.ts, pages/api/slack/oauth/callback.ts
Adds OAuth authorize endpoint to generate Slack install URL and callback to create SlackIntegration, fetch workspace/bot info, store encrypted token, and redirect with status.
Team Slack management APIs
pages/api/teams/[teamId]/slack/index.ts, pages/api/teams/[teamId]/slack/channels.ts
CRUD/read for Slack integration (GET/PUT/DELETE) with zod validation and enabled channels update; channels GET via Slack API and PUT to persist enabled channels; auth and membership checks.
Notify on views/access/downloads
app/api/views-dataroom/route.ts, app/api/views/route.ts, pages/api/links/download/index.ts, pages/api/links/download/bulk.ts, pages/api/links/download/dataroom-document.ts, pages/api/links/download/dataroom-folder.ts, pages/api/record_reaction.ts
Wires background Slack notifications post-event (non-preview) using teamId; adds data selections (ids) and viewer date/time fallbacks; includes bulk/folder download metadata.
Settings UI and navigation
pages/settings/slack.tsx, components/settings/slack-settings-skeleton.tsx, components/settings/settings-header.tsx, components/sidebar/app-sidebar.tsx, components/layouts/breadcrumb.tsx, components/shared/icons/slack-icon.tsx, components/ui/alert-dialog.tsx, components/ui/multi-select-v2.tsx
New Slack settings page with connect/disconnect, toggles, channel multi-select (new triggerIcon prop), skeleton loading, nav entries, breadcrumb, Slack icon, and a reusable confirm dialog component.
Email for install notification
components/emails/slack-integration-notification.tsx, lib/emails/send-slack-integration-notification.ts
New email template and sender to notify team users when Slack integration is added.
Database schema and migration
prisma/migrations/20250904040013_slack_integration/migration.sql, prisma/schema/schema.prisma, prisma/schema/team.prisma
Adds Brand model; adds SlackIntegration model and relation on Team; migration creates SlackIntegration table with indexes and FK.
SWR hooks
lib/swr/use-slack-integration.ts, lib/swr/use-slack-channels.ts
New hooks to fetch integration state and channels with conditional enablement and SWR tuning.
Minor refactors/no-op formatting
components/upload-zone.tsx, pages/api/teams/[teamId]/datarooms/[id]/documents/index.ts, pages/api/teams/[teamId]/index.ts
Reformatting/removal of unused import/comments; no behavior change.
Dependency
package.json
Adds dependency: nodemailer ^6.10.1.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Settings as Settings UI (/settings/slack)
  participant API as /api/teams/:teamId/slack/oauth/authorize
  participant Slack as Slack OAuth
  participant Callback as /api/slack/oauth/callback
  participant DB as Prisma (SlackIntegration, Team)
  participant Email as sendSlackIntegrationNotification

  User->>Settings: Click "Connect Slack"
  Settings->>API: GET authorize (teamId)
  API->>API: Validate session + team membership
  API->>Slack: Build OAuth URL
  API-->>Settings: { oauthUrl, state }
  Settings->>Slack: Redirect to install URL
  Slack->>Callback: Redirect with code,state
  Callback->>Callback: Validate session, state, team access
  Callback->>Slack: Exchange code for token
  Callback->>Slack: team.info, auth.test
  Callback->>DB: Create SlackIntegration (encrypt token)
  Callback->>Email: Notify team users (best-effort)
  Callback-->>User: Redirect /settings/slack?success=1
Loading
sequenceDiagram
  autonumber
  participant API as View/Download APIs
  participant Events as slackEventManager.processEvent
  participant DB as Prisma (SlackIntegration, config)
  participant Templates as createSlackMessage
  participant Slack as Slack API (chat.postMessage)

  API->>API: Record event (view/access/download)
  API->>Events: notify*({...}) (background)
  Events->>DB: Fetch SlackIntegration by teamId
  alt Integration enabled and event type enabled
    Events->>Templates: createSlackMessage(eventData)
    alt Message created
      loop Enabled channels
        Events->>Slack: chat.postMessage (channel, blocks)
        Slack-->>Events: ok/error
      end
    else Unknown/invalid
      Events-->>API: Skip
    end
  else No integration or disabled
    Events-->>API: Skip
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 27

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
pages/api/teams/[teamId]/index.ts (1)

215-239: Fix Promise.all misuse and ensure domain deletions are awaited.

  • domainPromises is built via map but returns void[] (no return), so deletions start but aren’t tracked.
  • team.domains && domainPromises passes a nested array into Promise.all, which is treated as a plain value and not awaited.
  • Net effect: domain removals may not finish before team deletion.

Apply:

-      // prepare a list of promises to delete domains
-      let domainPromises: void[] = [];
-      if (team.domains) {
-        domainPromises = team.domains.map((domain) => {
-          removeDomainFromVercelProject(domain.slug);
-        });
-      }
+      // prepare a list of promises to delete domains
+      const domainPromises: Promise<void>[] =
+        team.domains?.map((domain) =>
+          removeDomainFromVercelProject(domain.slug),
+        ) ?? [];

       await Promise.all([
-        // delete domains, if exists on team
-        team.domains && domainPromises,
+        // delete domains, if exists on team
+        ...domainPromises,
         // delete subscription, if exists on team
         team.stripeId &&
         cancelSubscription(team.stripeId, isOldAccount(team.plan)),
         // delete user from contact book
         unsubscribe((session.user as CustomUser).email ?? ""),
         // delete user, if no other teams
-        userTeams.length === 1 &&
-        prisma.user.delete({
-          where: {
-            id: (session.user as CustomUser).id,
-          },
-        }),
+        ...(userTeams.length === 1
+          ? [
+              prisma.user.delete({
+                where: { id: (session.user as CustomUser).id },
+              }),
+            ]
+          : []),
         // delete team branding from redis
         redis.del(`brand:logo:${teamId}`),

         // delete team
         prisma.team.delete({
           where: {
             id: teamId,
           },
         }),
       ]);
pages/api/links/download/dataroom-document.ts (2)

149-152: Guard against empty permitted document set to prevent runtime crashes.

After permission filtering, downloadDocuments can be empty; subsequent downloadDocuments[0]... will throw. Return 403/404 early.

Apply this diff after the filtering block:

       downloadDocuments = downloadDocuments.filter((doc) =>
         permittedDocumentIds.includes(doc.id),
       );
     }
 
+    // Ensure the requested document is still permitted/available
+    if (!downloadDocuments.length) {
+      return res.status(403).json({ error: "Error downloading" });
+    }

Also applies to: 187-193


219-224: Possible “Invalid Date” when viewedAt is null.

new Date(view.viewedAt) can throw on .toLocale* if viewedAt is null. Fallback to new Date().

Apply this diff:

-                date: new Date(view.viewedAt).toLocaleDateString(),
+                date: (view.viewedAt ? new Date(view.viewedAt) : new Date()).toLocaleDateString(),
                 ipAddress: getIpAddress(req.headers),
                 link: view.link.name,
-                time: new Date(view.viewedAt).toLocaleTimeString(),
+                time: (view.viewedAt ? new Date(view.viewedAt) : new Date()).toLocaleTimeString(),
pages/api/links/download/index.ts (1)

152-157: Fix potential crash: viewedAt can be null when building watermark viewerData.

Guard against null to avoid RangeError from .toLocale*.

Apply this diff:

-                date: new Date(view.viewedAt).toLocaleDateString(),
+                date: (view.viewedAt ? new Date(view.viewedAt) : new Date()).toLocaleDateString(),
                 ipAddress: getIpAddress(req.headers),
                 link: view.link.name,
-                time: new Date(view.viewedAt).toLocaleTimeString(),
+                time: (view.viewedAt ? new Date(view.viewedAt) : new Date()).toLocaleTimeString(),
app/api/views-dataroom/route.ts (1)

681-688: Duplicate identifier: dataroomViewId redeclared in the same scope.

You destructure dataroomViewId from the request body earlier, then redeclare const dataroomViewId = ..., which is a compile-time error. Rename the local variable.

-        const dataroomViewId =
+        const computedDataroomViewId =
           newDataroomView?.id ?? dataroomSession?.viewId ?? undefined;
@@
-          viewId: dataroomViewId,
+          viewId: computedDataroomViewId,
🧹 Nitpick comments (61)
components/upload-zone.tsx (4)

270-279: Make extension checks case-insensitive.

Handles .DWG, .DXF, .KML, .KMZ.

-        if (
-          uploadResult.fileName.endsWith(".dwg") ||
-          uploadResult.fileName.endsWith(".dxf")
-        ) {
+        const fileNameLower = uploadResult.fileName.toLowerCase();
+        if (fileNameLower.endsWith(".dwg") || fileNameLower.endsWith(".dxf")) {
           supportedFileType = "cad";
-          contentType = `image/vnd.${uploadResult.fileName.split(".").pop()}`;
+          contentType = `image/vnd.${fileNameLower.split(".").pop()}`;
         }
@@
-        if (
-          uploadResult.fileName.endsWith(".kml") ||
-          uploadResult.fileName.endsWith(".kmz")
-        ) {
+        if (fileNameLower.endsWith(".kml") || fileNameLower.endsWith(".kmz")) {
           supportedFileType = "map";
-          contentType = `application/vnd.google-earth.${uploadResult.fileName.endsWith(".kml") ? "kml+xml" : "kmz"}`;
+          contentType = `application/vnd.google-earth.${fileNameLower.endsWith(".kml") ? "kml+xml" : "kmz"}`;
         }

Also applies to: 283-288


676-676: Trim unused dependency from getFilesFromEvent hook.

endpointTargetType isn’t referenced; avoid needless re-creations.

-    [folderPathName, endpointTargetType, teamInfo],
+    [folderPathName, teamInfo],

70-76: Broaden dataroomDocumentId type for onUploadSuccess payload.

You pass null when dataroom add fails; reflect that in the type.

   onUploadSuccess?: (
     files: {
       fileName: string;
       documentId: string;
-      dataroomDocumentId: string;
+      dataroomDocumentId: string | null;
     }[],
   ) => void;

699-723: Keep the hint text in sync with accepted types.

Paid plan supports .msg (Outlook). Consider adding it to the displayed list.

-                    : `Only *.pdf, *.pptx, *.docx, *.xlsx, *.xls, *.csv, *.ods, *.ppt, *.odp, *.doc, *.odt, *.dwg, *.dxf, *.png, *.jpg, *.jpeg, *.mp4, *.mov, *.avi, *.webm, *.ogg`}
+                    : `Only *.pdf, *.pptx, *.docx, *.xlsx, *.xls, *.csv, *.ods, *.ppt, *.odp, *.doc, *.odt, *.dwg, *.dxf, *.png, *.jpg, *.jpeg, *.mp4, *.mov, *.avi, *.webm, *.ogg, *.msg`}
package.json (1)

106-106: Remove unused nodemailer dependency (package.json: line 106)
nodemailer isn’t imported or referenced anywhere in the codebase; remove it to reduce bundle weight and avoid potential cold-start impact.

components/sidebar/app-sidebar.tsx (1)

153-157: Nice addition of Slack to Settings menu.

Optional: if Slack is feature-flagged or plan-gated later, mirror the gating used for other items.

pages/api/record_reaction.ts (1)

30-41: Avoid unnecessary include to reduce DB work.

The created reaction isn’t used; including the related view adds extra query cost without benefit here.

Apply to inline-remove the include:

-      include: {
-        view: {
-          select: {
-            documentId: true,
-            dataroomId: true,
-            linkId: true,
-            viewerEmail: true,
-            viewerId: true,
-            teamId: true,
-          },
-        },
-      },
components/settings/settings-header.tsx (1)

70-74: Slack tab added — consistent with sidebar and breadcrumbs.

If you later introduce feature flags, consider disabled: !features?.slack for parity with other tabs.

components/shared/icons/slack-icon.tsx (1)

8-16: Add accessible title support and hide decorative icon from AT by default.

Expose an optional title prop and set aria-hidden when no title is provided. This keeps it decorative by default and accessible when a label is supplied.

-interface SlackIconProps {
-  className?: string;
-  size?: number;
-}
+interface SlackIconProps {
+  className?: string;
+  size?: number;
+  title?: string;
+}

-export function SlackIcon({ className = "", size = 24 }: SlackIconProps) {
+export function SlackIcon({ className = "", size = 24, title }: SlackIconProps) {
   return (
     <svg
       width={size}
       height={size}
       viewBox="0 0 24 24"
       fill="none"
       className={className}
+      role={title ? "img" : undefined}
+      aria-hidden={title ? undefined : true}
+      focusable="false"
     >
+      {title ? <title>{title}</title> : null}
components/ui/multi-select-v2.tsx (2)

234-236: Hide trigger icon from assistive tech to avoid redundant labels.

The icon is purely decorative in the trigger; hide it from AT.

-            {triggerIcon || (
-              <TagIcon className="!size-4 shrink-0 text-muted-foreground" />
-            )}
+            <span aria-hidden="true">
+              {triggerIcon || (
+                <TagIcon className="!size-4 shrink-0 text-muted-foreground" />
+              )}
+            </span>

259-260: Typographic choice acknowledged.

Switch to “+{n} more” (no space) is fine; just flagging in case of consistency with other counters.

components/emails/slack-integration-notification.tsx (1)

16-24: Remove unused userEmail prop (or use it).

userEmail is accepted but unused. Either remove it from props or incorporate it into the copy.

-export default function SlackIntegrationNotification({
-  teamName,
-  userEmail,
-  settingsUrl,
-}: {
-  teamName: string;
-  userEmail: string;
-  settingsUrl: string;
-}) {
+export default function SlackIntegrationNotification({
+  teamName,
+  settingsUrl,
+}: {
+  teamName: string;
+  settingsUrl: string;
+}) {
lib/utils.ts (1)

592-616: Token encryption works; consider minor clarity and future-proofing.

  • Error messages mention only NEXT_PRIVATE_SLACK_TOKEN_KEY though a fallback key is allowed; adjust for accuracy.
  • Add a small prefix marker (e.g., slk:) to reduce false positives when checking “already encrypted”.
-  const encryptionKey = process.env.NEXT_PRIVATE_SLACK_TOKEN_KEY || process.env.NEXT_PRIVATE_DOCUMENT_PASSWORD_KEY;
+  const encryptionKey =
+    process.env.NEXT_PRIVATE_SLACK_TOKEN_KEY ||
+    process.env.NEXT_PRIVATE_DOCUMENT_PASSWORD_KEY;
   if (!encryptionKey) {
-    throw new Error("NEXT_PRIVATE_SLACK_TOKEN_KEY environment variable is required for token encryption");
+    throw new Error(
+      "Missing encryption key: set NEXT_PRIVATE_SLACK_TOKEN_KEY (preferred) or NEXT_PRIVATE_DOCUMENT_PASSWORD_KEY.",
+    );
   }
@@
-  const textParts: string[] = token.split(":");
-  if (textParts.length === 2) {
+  const textParts: string[] = token.split(":");
+  if (textParts.length === 2) {
     return token;
   }
prisma/schema/schema.prisma (1)

71-81: Brand model looks good; consider color validation upstream

One-per-team via teamId @unique is correct. Minor: enforce hex/rgb via app-layer schema (Zod/Yup) to prevent invalid brandColor/accentColor.

lib/slack/types.ts (3)

44-51: Make channel optional to match existing usage

Existing code builds messages without setting channel when a default is applied downstream. Keep the property optional to avoid forcing callers to pass it prematurely.

-export interface SlackMessage {
-    channel: string;
+export interface SlackMessage {
+    channel?: string;
     text?: string;
     blocks?: any[];
     thread_ts?: string;
     unfurl_links?: boolean;
     unfurl_media?: boolean;
 }

53-57: Unify notificationTypes representation

Top-level config uses a keyed object (SlackNotificationConfig), while per-channel uses string[]. Prefer a single event type definition (union) used everywhere to reduce mapping/validation complexity.

+export type SlackEventType = "document_view" | "dataroom_access" | "document_download";
 
 export interface SlackNotificationConfig {
-    document_view: boolean;
-    dataroom_access: boolean;
-    document_download: boolean;
+    [k in SlackEventType]: boolean;
 }

And adjust SlackChannelConfig below (see next comment).


59-64: Align SlackChannelConfig.notificationTypes to typed events

Use the union to ensure only supported events are configured.

 export interface SlackChannelConfig {
     id: string;
     name: string;
     enabled: boolean;
-    notificationTypes: string[];
+    notificationTypes: SlackEventType[];
 }
app/api/views/route.ts (1)

640-652: Gate Slack notifications and keep them non-blocking

  • Respect link-level notification toggle if applicable.
  • You're already using waitUntil; keep it but avoid firing when notifications are disabled.
-        if (!isPreview) {
+        if (!isPreview && link.enableNotification) {
           waitUntil(
             notifyDocumentView({
               teamId: link.teamId!,
               documentId,
               linkId,
               viewerEmail: email ?? undefined,
               viewerId: viewer?.id ?? undefined,
             }).catch((error) => {
               console.error("Error sending Slack notification:", error);
             })
           );
         }

Question: Is PII (viewerEmail) intended to be sent to Slack by default, or should this be controlled via a privacy flag in SlackIntegration settings?

pages/api/links/download/dataroom-folder.ts (1)

299-317: Avoid blocking the download path with Slack notification

This awaits Slack dispatch on the hot path. Fire-and-forget to reduce latency, and optionally gate by a link/team-level flag.

-    if (view.link.teamId) {
-      try {
-        await notifyDocumentDownload({
+    if (view.link.teamId) {
+      try {
+        // fire-and-forget; log failures
+        // eslint-disable-next-line @typescript-eslint/no-floating-promises
+        notifyDocumentDownload({
           teamId: view.link.teamId,
           documentId: undefined,
           dataroomId,
           linkId,
           viewerEmail: view.viewerEmail ?? undefined,
           viewerId: undefined,
           metadata: {
             folderName: rootFolder.name,
             documentCount: allDocuments.length,
             isFolderDownload: true,
           },
-        });
+        }).catch((error) => {
+          console.error("Error sending Slack notification:", error);
+        });
       } catch (error) {
         console.error("Error sending Slack notification:", error);
       }
     }

Additionally (outside this hunk), to gate and enrich:

  • Include enableNotification in view.link select and guard the send.
  • Select and pass viewerId if available.

Example changes elsewhere in this file:

 // in the view select
-        link: {
+        link: {
           select: {
             teamId: true,
             allowDownload: true,
+            enableNotification: true,
             expiresAt: true,
             isArchived: true,
             enableWatermark: true,
             watermarkConfig: true,
             name: true,
             permissionGroupId: true,
           },
         },
+        viewerId: true,

Then conditionally send:

if (view.link.teamId && view.link.enableNotification) { /* send */ }
pages/api/links/download/dataroom-document.ts (1)

170-186: Fire-and-forget Slack notify + minimize PII exposure.

  • Consider not awaiting the event to avoid adding tail latency to downloads.
  • Confirm product/legal stance before sending viewerEmail into Slack.

Apply this diff to make it non-blocking while preserving error logging:

-        try {
-          await notifyDocumentDownload({
+        try {
+          void notifyDocumentDownload({
             teamId: view.link.teamId,
             documentId,
             dataroomId: view.dataroom.id,
             linkId,
             viewerEmail: view.viewerEmail ?? undefined,
             viewerId: view.viewerId ?? undefined,
-          });
+          }).catch((error) =>
+            console.error("Error sending Slack notification:", error),
+          );
         } catch (error) {
           console.error("Error sending Slack notification:", error);
         }
pages/api/links/download/bulk.ts (2)

306-323: Include viewerId (if available) and consider fire-and-forget; validate optional documentId.

  • Pass viewerId to keep parity with single-document flow.
  • Consider non-blocking notify to avoid delaying bulk ZIP generation.
  • Verify documentId is optional in SlackEventData.

Apply these diffs:

@@
-          await notifyDocumentDownload({
+          void notifyDocumentDownload({
             teamId: view.dataroom.teamId,
-            documentId: undefined, // Bulk download, no specific document
+            documentId: undefined, // Bulk download, no specific document
             dataroomId: view.dataroom.id,
             linkId,
             viewerEmail: view.viewerEmail ?? undefined,
-            viewerId: undefined,
+            viewerId: view.viewerId ?? undefined,
             metadata: {
               documentCount: downloadDocuments.length,
               isBulkDownload: true,
             },
-          });
+          }).catch((error) =>
+            console.error("Error sending Slack notification:", error),
+          );

And select viewerId from the view:

         select: {
           id: true,
           viewedAt: true,
           viewerEmail: true,
+          viewerId: true,

243-245: Avoid enqueueing empty file keys for ZIP generation.

When adding empty folders (addFileToStructure(folder.path, "", "")), fileKey is "" and gets pushed, potentially breaking the Lambda.

Apply this diff:

-        fileKeys.push(fileKey);
+        if (fileKey) {
+          fileKeys.push(fileKey);
+        }
pages/api/links/download/index.ts (1)

104-117: Add viewerId and consider non-blocking notify; confirm PII policy.

  • Include viewerId (align with dataroom-document flow).
  • Fire-and-forget to reduce latency.
  • Ensure sending viewerEmail to Slack is intended.

Apply these diffs:

@@
-      await prisma.view.update({
+      await prisma.view.update({
         where: { id: viewId },
         data: { downloadedAt: new Date() },
       });
 
-      if (view.document?.teamId) {
+      if (view.document?.teamId) {
         try {
-          await notifyDocumentDownload({
+          void notifyDocumentDownload({
             teamId: view.document.teamId,
             documentId: view.document.id,
             dataroomId: undefined,
             linkId,
             viewerEmail: view.viewerEmail ?? undefined,
-            viewerId: undefined,
-          });
+            viewerId: view.viewerId ?? undefined,
+          }).catch((error) =>
+            console.error("Error sending Slack notification:", error),
+          );
         } catch (error) {
           console.error("Error sending Slack notification:", error);
         }
       }

And select viewerId from the view:

         select: {
           id: true,
           viewedAt: true,
           viewerEmail: true,
+          viewerId: true,
lib/swr/use-slack-integration.ts (1)

25-31: Minor naming nit: return isLoading to mirror SWR, or document loading.

Not required, but aligning with SWR’s isLoading can reduce confusion across hooks.

-        loading: isLoading && !data,
+        isLoading: isLoading && !data,
components/settings/slack-settings-skeleton.tsx (1)

5-5: Add basic a11y attributes to the loading container.

Helps assistive tech announce loading state.

-    <div className="space-y-6">
+    <div className="space-y-6" role="status" aria-busy aria-live="polite" aria-label="Loading Slack settings">
lib/emails/send-slack-integration-notification.ts (1)

23-23: Gate real sends across non-production envs, not only NODE_ENV=development

Avoid sending real emails on Preview/Stage (e.g., Vercel preview).

-            test: process.env.NODE_ENV === "development",
+            test: (process.env.VERCEL_ENV ?? process.env.NODE_ENV) !== "production",
lib/swr/use-slack-channels.ts (1)

16-22: Reduce noisy retries and flicker

Optional: don’t retry on 4xx and keep previous data during refreshes.

         {
             revalidateOnFocus: false,
             revalidateOnReconnect: false,
             dedupingInterval: 30000,
             revalidateIfStale: false,
             errorRetryCount: 2,
             errorRetryInterval: 5000,
+            shouldRetryOnError: (err) => !err?.status || err.status >= 500,
+            keepPreviousData: true,
         }
pages/api/teams/[teamId]/slack/oauth/authorize.ts (2)

51-53: Make redirectUri robust when NEXTAUTH_URL is unset

Fallback to request host/proto to avoid broken OAuth in local/preview environments.

-        const redirectUri = `${process.env.NEXTAUTH_URL}/api/slack/oauth/callback`;
+        const proto = (req.headers["x-forwarded-proto"] as string) || "https";
+        const host = req.headers.host;
+        const baseUrl = process.env.NEXTAUTH_URL ?? `${proto}://${host}`;
+        const redirectUri = `${baseUrl}/api/slack/oauth/callback`;

49-49: Optionally sign the state to prevent tampering

Base64-encoded JSON is easy to forge. Add an HMAC and verify it in the callback.

Example (outside this hunk):

import crypto from "crypto";
const hmac = (s:string) => crypto.createHmac("sha256", process.env.STATE_SECRET!).update(s).digest("hex");

// here:
const payload = JSON.stringify({ teamId, userId, ts: Date.now() });
const state = Buffer.from(payload).toString('base64');
const sig = hmac(state);
// send both: { oauthUrl, state, sig }

// in callback: recompute hmac(state) === sig and check ts freshness
pages/api/teams/[teamId]/slack/channels.ts (2)

85-98: Return 404 when integration is missing on update (handle Prisma P2025).

If the team has no Slack integration, update throws and you return 500. Surface a 404 instead.

-            await prisma.slackIntegration.update({
+            try {
+              await prisma.slackIntegration.update({
                 where: { teamId },
                 data: { enabledChannels },
-            });
+              });
+            } catch (e: any) {
+              if (e?.code === 'P2025') {
+                return res.status(404).json({ error: 'Slack integration not found' });
+              }
+              throw e;
+            }

57-70: Avoid masking Slack API failures with 200 + empty list.

Returning { channels: [] } on unexpected Slack errors hides issues and complicates debugging. Prefer a 502/500 with a user-friendly message.

-                return res.status(200).json({ channels: [] });
+                return res.status(502).json({ error: 'Failed to fetch channels from Slack. Please try again.' });
app/api/views-dataroom/route.ts (1)

666-676: Avoid noisy error logs; add concise context.

Logging raw errors repeatedly can be noisy. Add minimal context and avoid duplicate stack traces (the event manager may already log).

-                } catch (error) {
-                  console.error("Error sending Slack notification:", error);
+                } catch (error) {
+                  console.error("Slack notification error (dataroom_access)", { linkId, dataroomId, teamId: link.teamId, error: (error as Error).message });

and similarly for the document_view block.

Also applies to: 795-805

pages/api/slack/oauth/callback.ts (3)

126-149: Don’t block the OAuth redirect on email sends.

Awaiting Promise.allSettled(emailPromises) can slow or time out the OAuth flow for large teams. Fire-and-forget (with logging), or queue these emails out-of-band.

-            await Promise.allSettled(emailPromises)
-                .then(results => {
-                    const rejected = results.filter(r => r.status === 'rejected');
-                    if (rejected.length) {
-                        console.error('Some Slack integration notification emails failed:', rejected);
-                    }
-                });
+            // Fire-and-forget to keep OAuth redirect snappy
+            void Promise.allSettled(emailPromises).then(results => {
+              const rejected = results.filter(r => r.status === 'rejected');
+              if (rejected.length) {
+                console.error('Some Slack integration notification emails failed:', rejected);
+              }
+            }).catch((e) => {
+              console.error('Slack integration notification fanout failed:', e);
+            });

24-35: Coerce query params to strings to avoid string[] edge cases.

Next.js can provide string | string[]. Coerce defensively before use.

-        const { code, state, error } = req.query as { code: string; state: string; error?: string };
+        const qp = req.query as { code?: string | string[]; state?: string | string[]; error?: string | string[] };
+        const code = Array.isArray(qp.code) ? qp.code[0] : qp.code;
+        const state = Array.isArray(qp.state) ? qp.state[0] : qp.state;
+        const error = Array.isArray(qp.error) ? qp.error[0] : qp.error;

66-74: Handle race on duplicate installations (unique constraint).

Simultaneous installs can slip past the existence check and hit a unique constraint on create. Catch Prisma P2002 and redirect gracefully.

Would you like me to add a guarded create with a try/catch for P2002 (and import Prisma to check instanceof Prisma.PrismaClientKnownRequestError)?

Also applies to: 84-103

components/ui/alert-dialog.tsx (1)

176-177: Trim extra whitespace inside title element.

Minor readability/spacing nit.

-          <AlertDialogTitle> {title} </AlertDialogTitle>
+          <AlertDialogTitle>{title}</AlertDialogTitle>
prisma/migrations/20250904040013_slack_integration/migration.sql (3)

21-26: Remove redundant non-unique index on teamId.

The unique index on teamId already provides an index. Keeping both adds write overhead without benefit.

--- CreateIndex
-CREATE INDEX "SlackIntegration_teamId_idx" ON "SlackIntegration"("teamId");

12-16: Add safe defaults for JSONB columns.

Defaults simplify inserts and reduce chances of NULL/shape errors.

-    "enabledChannels" JSONB NOT NULL,
+    "enabledChannels" JSONB NOT NULL DEFAULT '{}'::jsonb,
     "enabled" BOOLEAN NOT NULL DEFAULT true,
-    "notificationTypes" JSONB NOT NULL,
+    "notificationTypes" JSONB NOT NULL DEFAULT '{"document_view":true,"dataroom_access":true,"document_download":true}'::jsonb,

8-8: Confirm token-at-rest strategy.

accessToken is sensitive. Ensure application-level encryption is enforced and KMS rotation is documented; consider renaming to accessTokenEnc to signal ciphertext.

pages/settings/slack.tsx (4)

221-226: Avoid console noise in production.

Gate performance logs or remove.

-          console.log(`Channel update API call took ${endTime - startTime}ms`);
+          if (process.env.NODE_ENV !== "production") {
+            console.log(`Channel update API call took ${endTime - startTime}ms`);
+          }

277-282: Function name is misleading; no actual debouncing.

Either implement debouncing or rename to reflect behavior.

Example (basic debounce without dependencies creep):

-const debouncedChannelsUpdate = (selectedChannelIds: string[]) => {
-  setPendingChannelUpdate(true);
-  handleChannelsUpdate(selectedChannelIds).finally(() => {
-    setPendingChannelUpdate(false);
-  });
-};
+const debouncedChannelsUpdate = (() => {
+  let t: ReturnType<typeof setTimeout> | null = null;
+  return (selectedChannelIds: string[]) => {
+    setPendingChannelUpdate(true);
+    if (t) clearTimeout(t);
+    t = setTimeout(() => {
+      handleChannelsUpdate(selectedChannelIds).finally(() => {
+        setPendingChannelUpdate(false);
+      });
+    }, 300);
+  };
+})();

94-97: Remove unused helper.

handleIntegrationUpdate is defined but never used.

-  const handleIntegrationUpdate = (updatedIntegration: SlackIntegration) => {
-    mutateIntegration(updatedIntegration, false);
-  };

500-520: Reflect pending updates in control state.

Optional: pass pendingChannelUpdate to loading for better UX feedback while saving.

-                            <MultiSelect
-                              loading={false}
+                            <MultiSelect
+                              loading={pendingChannelUpdate}
lib/types/slack.ts (4)

1-6: Avoid type name collision with lib/slack/types.ts

There’s another SlackChannel declared in lib/slack/types.ts with a different shape (adds is_member). Having two interfaces with the same name but different fields is a footgun for accidental imports. Prefer a distinct public API name (e.g., ApiSlackChannel) or re-export the shared type from a single module.

Apply:

-export interface SlackChannel {
+export interface ApiSlackChannel {
     id: string;
     name: string;
     is_archived: boolean;
     is_private: boolean;
 }

And update references in this file (and consumers) accordingly.


15-32: Align server vs API shapes and reuse types

  • createdAt/updatedAt as string is fine for API responses, but ensure server-side uses Date and converts explicitly at the boundary.
  • enabledChannels: Record<string, SlackChannelConfig> is good, but consider typing the key as channel ID: Record<ApiSlackChannel['id'], SlackChannelConfig>.

Would you like a follow-up patch to add zod/io-ts schemas to validate API payloads?


34-40: Reuse the union for request typing

Tie UpdateSlackIntegrationRequest.notificationTypes and enabledChannels to the new union to prevent invalid values.

Apply:

 export interface UpdateSlackIntegrationRequest {
     enabled?: boolean;
-    notificationTypes?: SlackIntegration['notificationTypes'];
+    notificationTypes?: Record<SlackNotificationType, boolean>;
     defaultChannel?: string;
-    enabledChannels?: Record<string, SlackChannelConfig>;
+    enabledChannels?: Record<string, SlackChannelConfig>;
 }

42-47: Minor: Export consistent API response types

If you rename SlackChannel to ApiSlackChannel above, reflect it here to avoid drift.

Apply:

 export interface SlackChannelsResponse {
-    channels: SlackChannel[];
+    channels: ApiSlackChannel[];
 }
 
 export interface SlackIntegrationResponse extends SlackIntegration { }
lib/slack/client.ts (1)

166-194: Handle rate limiting and join-on-demand

  • Consider handling HTTP 429 (Retry-After) with backoff.
  • For errors like not_in_channel, optionally call conversations.join before retrying.

I can provide a small Slack API wrapper with retry/backoff if you want it in this PR.

lib/slack/events.ts (7)

2-3: Import and reuse the client singleton

Avoid multiple SlackClient instances. Import the exported singleton.

Apply:

-import { SlackClient } from "./client";
+import { slackClient, SlackClient } from "./client";

21-23: Use singleton instead of new SlackClient()

This removes redundant env checks and centralizes HTTP behavior.

Apply:

-    constructor() {
-        this.client = new SlackClient();
-    }
+    constructor() {
+        this.client = slackClient;
+    }

6-16: Narrow eventType to a union

Use a union type to prevent invalid event types at compile-time.

Apply (after adding SlackNotificationType in lib/types/slack.ts):

-export interface SlackEventData {
+export interface SlackEventData {
     teamId: string;
-    eventType: string;
+    eventType: import('@/lib/types/slack').SlackNotificationType;
     documentId?: string;
     dataroomId?: string;
     linkId?: string;
     viewerEmail?: string;
     viewerId?: string;
     userId?: string;
     metadata?: Record<string, any>;
 }

61-74: Avoid rebuilding the same message per channel

createSlackMessage doesn’t vary by channel; build once and reuse inside the loop.

Apply:

-            for (const channel of channels) {
-                try {
-                    const message = await createSlackMessage(eventData);
-                    if (message) {
-                        const slackMessage = {
-                            ...message,
-                            channel: channel.id
-                        };
-                        await this.client.sendMessage(integration.accessToken, slackMessage);
-                    }
-                } catch (channelError) {
+            const baseMessage = await createSlackMessage(eventData);
+            if (!baseMessage) return;
+            for (const channel of channels) {
+                try {
+                    const slackMessage = { ...baseMessage, channel: channel.id };
+                    await this.client.sendMessage(integration.accessToken, slackMessage);
+                } catch (channelError) {
                     console.error(`Error sending to channel ${channel.name || channel.id}:`, channelError);
                 }
             }

89-95: Type the integration parameter; avoid any

Give the compiler help to catch typos in notification type keys.

Apply:

-    private isEventTypeEnabled(
-        eventType: string,
-        integration: any
-    ): boolean {
+    private isEventTypeEnabled(
+        eventType: string,
+        integration: { notificationTypes?: Record<string, boolean> }
+    ): boolean {
         const notificationTypes = integration.notificationTypes || {};
         return notificationTypes[eventType] || false;
     }

97-105: Return typed channel configs

Tie channel configs to a concrete type from your public types module to prevent shape drift.

Apply:

-    private async getNotificationChannels(
+    private async getNotificationChannels(
         eventData: SlackEventData,
         integration: SlackIntegration
-    ): Promise<any[]> {
+    ): Promise<import('@/lib/types/slack').SlackChannelConfig[]> {
         const enabledChannels = integration.enabledChannels || {};
         return Object.values(enabledChannels)
-            .filter((channel: any) => channel.enabled)
-            .filter((channel: any) => channel.notificationTypes && channel.notificationTypes.includes(eventData.eventType));
+            .filter((channel: any) => channel.enabled)
+            .filter((channel: any) => channel.notificationTypes?.includes(eventData.eventType as any));
     }

56-58: Optional: fallback to defaultChannel

If no per-channel config matches, consider falling back to integration.defaultChannel when set.

I can draft a small change that builds a single-target list from defaultChannel if you want it here.

lib/slack/templates.ts (6)

45-47: Fetch related entities in parallel to reduce latency.

Three sequential DB calls are unnecessary and slow under load.

@@
-    const document = eventData.documentId ? await getDocumentInfo(eventData.documentId) : null;
-    const dataroom = eventData.dataroomId ? await getDataroomInfo(eventData.dataroomId) : null;
-    const link = eventData.linkId ? await getLinkInfo(eventData.linkId) : null;
+    const [document, dataroom, link] = await Promise.all([
+        eventData.documentId ? getDocumentInfo(eventData.documentId) : Promise.resolve(null),
+        eventData.dataroomId ? getDataroomInfo(eventData.dataroomId) : Promise.resolve(null),
+        eventData.linkId ? getLinkInfo(eventData.linkId) : Promise.resolve(null),
+    ]);
@@
-    const dataroom = eventData.dataroomId ? await getDataroomInfo(eventData.dataroomId) : null;
-    const link = eventData.linkId ? await getLinkInfo(eventData.linkId) : null;
+    const [dataroom, link] = await Promise.all([
+        eventData.dataroomId ? getDataroomInfo(eventData.dataroomId) : Promise.resolve(null),
+        eventData.linkId ? getLinkInfo(eventData.linkId) : Promise.resolve(null),
+    ]);
@@
-    const document = eventData.documentId ? await getDocumentInfo(eventData.documentId) : null;
-    const dataroom = eventData.dataroomId ? await getDataroomInfo(eventData.dataroomId) : null;
-    const link = eventData.linkId ? await getLinkInfo(eventData.linkId) : null;
+    const [document, dataroom, link] = await Promise.all([
+        eventData.documentId ? getDocumentInfo(eventData.documentId) : Promise.resolve(null),
+        eventData.dataroomId ? getDataroomInfo(eventData.dataroomId) : Promise.resolve(null),
+        eventData.linkId ? getLinkInfo(eventData.linkId) : Promise.resolve(null),
+    ]);

Also applies to: 140-142, 235-237


97-99: Prefer event timestamp over “now” for message time.

If the pipeline is delayed/retried, new Date() is misleading.

@@
+    const ts = eventData.metadata?.timestamp ? new Date(eventData.metadata.timestamp) : new Date();
@@
-                        text: `*Time:*\n${new Date().toLocaleString()}`
+                        text: `*Time:*\n${ts.toLocaleString()}`
@@
+    const ts = eventData.metadata?.timestamp ? new Date(eventData.metadata.timestamp) : new Date();
@@
-                        text: `*Time:*\n${new Date().toLocaleString()}`
+                        text: `*Time:*\n${ts.toLocaleString()}`
@@
+    const ts = eventData.metadata?.timestamp ? new Date(eventData.metadata.timestamp) : new Date();
@@
-                        text: `*Time:*\n${new Date().toLocaleString()}`
+                        text: `*Time:*\n${ts.toLocaleString()}`

Also applies to: 184-185, 304-305


11-18: Type Slack blocks with the official SDK types.

Avoid any[] and catch shape errors at compile time.

+import type { KnownBlock } from "@slack/web-api";
@@
-export interface SlackMessage {
+export interface SlackMessage {
     channel?: string;
     text?: string;
-    blocks?: any[];
+    blocks?: KnownBlock[];
     thread_ts?: string;
     unfurl_links?: boolean;
     unfurl_media?: boolean;
 }

356-371: Consider scoping lookups by team/workspace to prevent cross-tenant leakage.

If models carry teamId/workspaceId, add it to the where clause.

Would you like a follow-up patch once you confirm the schema field name?

Also applies to: 375-397, 400-414


126-129: Guard against missing NEXTAUTH_URL to avoid broken Slack buttons.

If the env var is unset, URLs will start with “undefined…”.

Minimal guard example:

-                        url: eventData.documentId
-                            ? `${process.env.NEXTAUTH_URL}/documents/${eventData.documentId}`
-                            : `${process.env.NEXTAUTH_URL}/dashboard`
+                        url: (() => {
+                            const base = process.env.NEXTAUTH_URL;
+                            if (!base) return "https://papermark.app/dashboard";
+                            return eventData.documentId ? `${base}/documents/${eventData.documentId}` : `${base}/dashboard`;
+                        })()
@@
-                        url: eventData.dataroomId
-                            ? `${process.env.NEXTAUTH_URL}/datarooms/${eventData.dataroomId}`
-                            : `${process.env.NEXTAUTH_URL}/dashboard`
+                        url: (() => {
+                            const base = process.env.NEXTAUTH_URL;
+                            if (!base) return "https://papermark.app/dashboard";
+                            return eventData.dataroomId ? `${base}/datarooms/${eventData.dataroomId}` : `${base}/dashboard`;
+                        })()
@@
-                        url: eventData.dataroomId
-                            ? `${process.env.NEXTAUTH_URL}/datarooms/${eventData.dataroomId}`
-                            : eventData.documentId
-                                ? `${process.env.NEXTAUTH_URL}/documents/${eventData.documentId}`
-                                : `${process.env.NEXTAUTH_URL}/dashboard`
+                        url: (() => {
+                            const base = process.env.NEXTAUTH_URL;
+                            if (!base) return "https://papermark.app/dashboard";
+                            if (eventData.dataroomId) return `${base}/datarooms/${eventData.dataroomId}`;
+                            if (eventData.documentId) return `${base}/documents/${eventData.documentId}`;
+                            return `${base}/dashboard`;
+                        })()

Also applies to: 219-222, 333-339


49-51: DRY up viewer/link formatting.

The viewer mention and link label logic is duplicated.

I can factor tiny helpers like:

const mention = (email?: string) => email ? `<mailto:${email}|${email}>` : "Anonymous";
const linkLabelOf = (link?: { id?: string; name?: string } | null) =>
  link?.name ?? (link?.id ? `Link ${link.id.slice(0, 5)}` : null);

Also applies to: 143-145, 239-241

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0db57a7 and 9a90173.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (36)
  • app/api/views-dataroom/route.ts (4 hunks)
  • app/api/views/route.ts (2 hunks)
  • components/emails/slack-integration-notification.tsx (1 hunks)
  • components/layouts/breadcrumb.tsx (1 hunks)
  • components/settings/settings-header.tsx (1 hunks)
  • components/settings/slack-settings-skeleton.tsx (1 hunks)
  • components/shared/icons/slack-icon.tsx (1 hunks)
  • components/sidebar/app-sidebar.tsx (1 hunks)
  • components/ui/alert-dialog.tsx (2 hunks)
  • components/ui/multi-select-v2.tsx (4 hunks)
  • components/upload-zone.tsx (1 hunks)
  • lib/emails/send-slack-integration-notification.ts (1 hunks)
  • lib/slack/client.ts (1 hunks)
  • lib/slack/events.ts (1 hunks)
  • lib/slack/templates.ts (1 hunks)
  • lib/slack/types.ts (1 hunks)
  • lib/swr/use-slack-channels.ts (1 hunks)
  • lib/swr/use-slack-integration.ts (1 hunks)
  • lib/types/slack.ts (1 hunks)
  • lib/utils.ts (1 hunks)
  • package.json (2 hunks)
  • pages/api/links/download/bulk.ts (5 hunks)
  • pages/api/links/download/dataroom-document.ts (3 hunks)
  • pages/api/links/download/dataroom-folder.ts (2 hunks)
  • pages/api/links/download/index.ts (3 hunks)
  • pages/api/record_reaction.ts (1 hunks)
  • pages/api/slack/oauth/callback.ts (1 hunks)
  • pages/api/teams/[teamId]/datarooms/[id]/documents/index.ts (0 hunks)
  • pages/api/teams/[teamId]/index.ts (2 hunks)
  • pages/api/teams/[teamId]/slack/channels.ts (1 hunks)
  • pages/api/teams/[teamId]/slack/index.ts (1 hunks)
  • pages/api/teams/[teamId]/slack/oauth/authorize.ts (1 hunks)
  • pages/settings/slack.tsx (1 hunks)
  • prisma/migrations/20250904040013_slack_integration/migration.sql (1 hunks)
  • prisma/schema/schema.prisma (2 hunks)
  • prisma/schema/team.prisma (1 hunks)
💤 Files with no reviewable changes (1)
  • pages/api/teams/[teamId]/datarooms/[id]/documents/index.ts
🧰 Additional context used
🧬 Code graph analysis (22)
lib/slack/types.ts (2)
lib/types/slack.ts (2)
  • SlackChannel (1-6)
  • SlackChannelConfig (8-13)
lib/slack/templates.ts (2)
  • SlackMessage (11-18)
  • SlackChannelConfig (4-9)
lib/slack/client.ts (2)
lib/utils.ts (1)
  • decryptSlackToken (619-645)
lib/slack/types.ts (4)
  • SlackOAuthResponse (22-42)
  • SlackWorkspaceInfo (1-6)
  • SlackChannel (14-20)
  • SlackMessage (44-51)
lib/emails/send-slack-integration-notification.ts (2)
lib/resend.ts (1)
  • sendEmail (12-95)
components/emails/slack-integration-notification.tsx (1)
  • SlackIntegrationNotification (16-84)
lib/swr/use-slack-channels.ts (2)
lib/types/slack.ts (1)
  • SlackChannelsResponse (43-45)
lib/utils.ts (1)
  • fetcher (48-62)
pages/api/teams/[teamId]/slack/oauth/authorize.ts (6)
pages/api/slack/oauth/callback.ts (1)
  • handler (11-165)
pages/api/teams/[teamId]/slack/index.ts (1)
  • handler (8-43)
pages/api/teams/[teamId]/slack/channels.ts (1)
  • handler (8-102)
pages/api/auth/[...nextauth].ts (1)
  • authOptions (35-204)
lib/types.ts (1)
  • CustomUser (17-17)
lib/slack/client.ts (1)
  • slackClient (198-198)
app/api/views/route.ts (1)
lib/slack/events.ts (1)
  • notifyDocumentView (111-113)
pages/api/links/download/dataroom-document.ts (1)
lib/slack/events.ts (1)
  • notifyDocumentDownload (120-122)
pages/api/teams/[teamId]/slack/index.ts (5)
pages/api/slack/oauth/callback.ts (1)
  • handler (11-165)
pages/api/teams/[teamId]/slack/channels.ts (1)
  • handler (8-102)
pages/api/teams/[teamId]/slack/oauth/authorize.ts (1)
  • handler (8-64)
pages/api/auth/[...nextauth].ts (1)
  • authOptions (35-204)
lib/types.ts (1)
  • CustomUser (17-17)
lib/types/slack.ts (2)
lib/slack/types.ts (2)
  • SlackChannel (14-20)
  • SlackChannelConfig (59-64)
lib/slack/templates.ts (1)
  • SlackChannelConfig (4-9)
pages/api/teams/[teamId]/slack/channels.ts (4)
pages/api/teams/[teamId]/slack/index.ts (1)
  • handler (8-43)
pages/api/auth/[...nextauth].ts (1)
  • authOptions (35-204)
lib/types.ts (1)
  • CustomUser (17-17)
lib/slack/client.ts (1)
  • slackClient (198-198)
lib/swr/use-slack-integration.ts (2)
lib/types/slack.ts (1)
  • SlackIntegrationResponse (47-47)
lib/utils.ts (1)
  • fetcher (48-62)
pages/api/slack/oauth/callback.ts (5)
pages/api/teams/[teamId]/slack/channels.ts (1)
  • handler (8-102)
pages/api/teams/[teamId]/slack/oauth/authorize.ts (1)
  • handler (8-64)
lib/slack/client.ts (1)
  • slackClient (198-198)
lib/utils.ts (1)
  • encryptSlackToken (593-616)
lib/emails/send-slack-integration-notification.ts (1)
  • sendSlackIntegrationNotification (5-29)
lib/slack/templates.ts (1)
lib/slack/events.ts (1)
  • SlackEventData (6-16)
components/upload-zone.tsx (9)
lib/constants.ts (3)
  • FREE_PLAN_ACCEPTED_FILE_TYPES (101-110)
  • FULL_PLAN_ACCEPTED_FILE_TYPES (112-144)
  • SUPPORTED_DOCUMENT_MIME_TYPES (67-98)
lib/swr/use-billing.ts (1)
  • usePlan (86-136)
context/team-context.tsx (1)
  • useTeam (85-85)
ee/limits/swr-handler.ts (1)
  • useLimits (19-49)
lib/utils/get-file-size-limits.ts (2)
  • getFileSizeLimits (10-43)
  • getFileSizeLimit (46-66)
lib/utils/get-page-number-count.ts (1)
  • getPagesCount (8-40)
lib/utils/get-content-type.ts (1)
  • getSupportedContentType (1-46)
lib/documents/create-document.ts (2)
  • DocumentData (4-13)
  • createDocument (15-59)
lib/folders/create-folder.ts (4)
  • isSystemFile (5-10)
  • createFolderInMainDocs (51-79)
  • determineFolderPaths (81-97)
  • createFolderInBoth (99-162)
pages/api/links/download/index.ts (1)
lib/slack/events.ts (1)
  • notifyDocumentDownload (120-122)
pages/api/links/download/dataroom-folder.ts (1)
lib/slack/events.ts (1)
  • notifyDocumentDownload (120-122)
pages/api/teams/[teamId]/index.ts (4)
ee/stripe/index.ts (1)
  • cancelSubscription (33-56)
ee/stripe/utils.ts (1)
  • isOldAccount (252-254)
lib/types.ts (1)
  • CustomUser (17-17)
lib/redis.ts (1)
  • redis (4-7)
lib/slack/events.ts (3)
lib/slack/client.ts (1)
  • SlackClient (4-195)
lib/types/slack.ts (1)
  • SlackIntegration (15-32)
lib/slack/templates.ts (1)
  • createSlackMessage (20-39)
components/ui/alert-dialog.tsx (1)
components/ui/button.tsx (1)
  • Button (71-71)
app/api/views-dataroom/route.ts (1)
lib/slack/events.ts (2)
  • notifyDataroomAccess (115-117)
  • notifyDocumentView (111-113)
pages/settings/slack.tsx (9)
context/team-context.tsx (1)
  • useTeam (85-85)
lib/swr/use-slack-integration.ts (1)
  • useSlackIntegration (11-31)
lib/swr/use-slack-channels.ts (1)
  • useSlackChannels (11-31)
lib/types/slack.ts (4)
  • SlackIntegration (15-32)
  • SlackChannelConfig (8-13)
  • UpdateSlackIntegrationRequest (35-40)
  • SlackIntegrationResponse (47-47)
components/layouts/app.tsx (1)
  • AppLayout (16-38)
components/settings/settings-header.tsx (1)
  • SettingsHeader (8-96)
components/settings/slack-settings-skeleton.tsx (1)
  • SlackSettingsSkeleton (3-82)
components/ui/tooltip.tsx (1)
  • BadgeTooltip (53-107)
components/ui/multi-select-v2.tsx (1)
  • MultiSelect (134-378)
pages/api/links/download/bulk.ts (2)
lib/slack/events.ts (1)
  • notifyDocumentDownload (120-122)
lib/utils/ip.ts (1)
  • getIpAddress (1-8)
🪛 Biome (2.1.2)
components/emails/slack-integration-notification.tsx

[error] 69-69: Avoid using target="_blank" without rel="noopener" or rel="noreferrer".

Opening external links in new tabs without rel="noopener" is a security risk. See the explanation for more details.
Safe fix: Add the rel="noopener" attribute.

(lint/security/noBlankTarget)

🔇 Additional comments (28)
components/upload-zone.tsx (2)

258-261: Verify ownerId and teamId are always defined at drop time.

If session/team context isn’t loaded, uploads may fail.

Would you like a guard that blocks the drop until both are available and shows a toast?


37-43: Nice: extracting accept maps to top scope.

This avoids per-render object identity changes and fixes folder-drop MIME inference.

components/layouts/breadcrumb.tsx (1)

226-227: Slack breadcrumb entry looks correct and consistent.

pages/api/teams/[teamId]/index.ts (3)

13-13: No action needed – whitespace.


220-221: Confirm plan field presence for isOldAccount().

isOldAccount(team.plan) assumes team.plan is loaded. With include used above, scalar fields should be present, but please double-check the Team Prisma model to ensure plan is not null for deleted/legacy teams. If nullable, guard it.


232-232: No action needed – whitespace.

components/ui/multi-select-v2.tsx (1)

121-122: Prop addition LGTM.

triggerIcon makes the control more flexible for branded use (e.g., Slack).

lib/utils.ts (1)

592-645: Helpers confirmed server-side only: encryptSlackToken and decryptSlackToken are used exclusively in pages/api/slack/oauth/callback.ts and lib/slack/client.ts, with no imports in any client-side code.

prisma/schema/schema.prisma (2)

70-70: No-op line change

Nothing to review here.


578-598: Verify SlackIntegration encryption and indexing

  • accessToken: annotate with @db.Text for ciphertext storage; confirm it’s encrypted before persistence and, if so, rename to accessTokenEncrypted for clarity.
  • enabledChannels & notificationTypes: add @default(dbgenerated("'{}'::jsonb")) to ensure non-null defaults.
  • Add @@index([workspaceId]); if a workspace truly maps one-to-one with a team, replace with @@unique([workspaceId]).
prisma/schema/team.prisma (1)

25-25: 1:1 relation wired correctly

Optional one-to-one via SlackIntegration unique(teamId) matches this side. Looks good.

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

21-21: Import is fine

No concerns.

pages/api/links/download/dataroom-folder.ts (1)

11-11: Import looks good

Consistent with other Slack event imports.

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

9-9: Slack event import is appropriate and isolated.

Importing from the central Slack events module keeps concerns separated and avoids coupling API code to transport details.


41-50: Selecting link.teamId is required for downstream Slack routing.

Good addition; it avoids an extra round-trip to resolve team context during notification.

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

10-10: Centralized Slack event import looks good.


50-52: Selecting dataroom.id enables proper Slack payloads and later storage ops.


282-282: Formatting-only change.

No functional impact.

pages/api/links/download/index.ts (2)

9-9: Slack event import added—good modularity.


42-43: Selecting document.id is necessary for notification payloads.

lib/swr/use-slack-integration.ts (1)

11-23: SWR setup and key gating look solid.

The key nulling with enabled && teamId and conservative revalidation config are appropriate for settings data.

components/settings/slack-settings-skeleton.tsx (1)

3-14: Skeleton structure LGTM.

Clean, composable placeholders that match likely UI density.

lib/swr/use-slack-channels.ts (1)

25-30: Remove SWR v1 compatibility fallback
package.json shows SWR ^2.3.6, so isLoading is always defined and boolean—no need for the v1 fallback.

pages/api/teams/[teamId]/slack/index.ts (1)

50-67: Good: excludes sensitive fields in GET

The selection omits the Slack access token and limits exposure to non-sensitive fields.

pages/api/teams/[teamId]/slack/channels.ts (1)

1-9: Manual decryption redundant
slackClient.getChannels already calls its internal decryptToken on the passed token, so you can continue passing integration.accessToken directly.

Likely an incorrect or invalid review comment.

pages/settings/slack.tsx (1)

98-117: Verify toast.warning availability in sonner.

Some versions don’t expose toast.warning; fallback to toast or toast.message to avoid runtime errors.

If unsupported, change:

-        toast.warning(`Warning: ${router.query.warning}`);
+        toast(`Warning: ${router.query.warning}`);
lib/slack/client.ts (2)

26-37: Verify Slack OAuth scopes; separate bot scopes from user scopes

users:read.email typically requires user_scope in OAuth v2. Either add user_scope or drop the user email scope if not needed.

Apply (if you need user emails):

     getOAuthUrl(state: string, redirectUri: string): string {
         const params = new URLSearchParams({
             client_id: this.clientId,
-            scope: 'channels:read,groups:read,mpim:read,im:read,chat:write,chat:write.public,team:read,users:read,users:read.email',
+            scope: 'channels:read,groups:read,mpim:read,im:read,chat:write,chat:write.public,team:read,users:read',
+            user_scope: 'users:read.email',
             redirect_uri: redirectUri,
             state: state,
         });

If you prefer, I can also add a small helper to build scopes from arrays to avoid typos.


197-198: Reuse singleton in consumers

Good exporting of a singleton. Ensure downstream (events.ts) uses this instead of constructing new instances.

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 (5)
pages/api/links/download/bulk.ts (1)

341-350: Null-safe watermark timestamps — addressed.

Fallback to current time avoids “Invalid time value.” Thanks for closing out the earlier note.

lib/types/slack.ts (1)

81-88: Good: strong typing for notification types is in place

Using SlackNotificationType[] for channel-level configs addresses the earlier safety concern.

lib/slack/client.ts (2)

137-147: Paginate with limit and add per-page timeout

Minor efficiency and resiliency improvement.

         do {
             const url = new URL(`${this.baseUrl}/conversations.list`);
             url.searchParams.set('types', 'public_channel,private_channel');
             url.searchParams.set('exclude_archived', 'true');
+            url.searchParams.set('limit', '200');
             if (cursor) url.searchParams.set('cursor', cursor);
-            const resp = await fetch(url.toString(), {
-                headers: {
-                    'Authorization': `Bearer ${decryptedToken}`,
-                    'Content-Type': 'application/json',
-                },
-            });
+            const ac = new AbortController();
+            const t = setTimeout(() => ac.abort(), 10000);
+            const resp = await fetch(url.toString(), {
+                headers: {
+                    'Authorization': `Bearer ${decryptedToken}`,
+                    'Content-Type': 'application/json',
+                },
+                signal: ac.signal,
+            }).finally(() => clearTimeout(t));

Also applies to: 148-161


19-21: Harden decryptToken: tolerate plaintext and missing key

Wrap decryptSlackToken to avoid crashes when NEXT_PRIVATE_SLACK_TOKEN_KEY is absent (common in dev) or token is already plaintext.

-    private decryptToken(accessToken: string): string {
-        return decryptSlackToken(accessToken);
-    }
+    private decryptToken(accessToken: string): string {
+        try {
+            return decryptSlackToken(accessToken);
+        } catch {
+            return accessToken;
+        }
+    }
lib/slack/templates.ts (1)

37-44: Fix unsafe link?.id.slice(...) and add “direct access” fallback

link?.id.slice(...) can throw when id is undefined; unify to a safe linkLabel and fall back to “direct access.”

@@
-    let accessContext = "";
-    if (eventData.dataroomId && dataroom) {
-        accessContext = `in dataroom "${dataroom.name}"`;
-    } else if (link?.name) {
-        accessContext = `via shared link "${link.name}"`;
-    } else {
-        accessContext = `via shared link "Link ${link?.id.slice(0, 5)}"`;
-    }
+    const linkLabel = link?.name ?? (typeof link?.id === 'string' ? `Link ${link.id.slice(0, 5)}` : null);
+    const accessContext =
+        dataroom?.name ? `in dataroom "${dataroom.name}"`
+        : linkLabel ? `via shared link "${linkLabel}"`
+        : "via direct access";
@@
-                        text: eventData.dataroomId
-                            ? `Viewed document in dataroom "${dataroom?.name || 'Unknown'}"`
-                            : link?.name
-                                ? `Viewed document via shared link "${link.name}"`
-                                : `Viewed document via shared link "Link ${link?.id.slice(0, 5)}"`
+                        text: eventData.dataroomId
+                            ? `Viewed document in dataroom "${dataroom?.name || 'Unknown'}"`
+                            : linkLabel
+                                ? `Viewed document via shared link "${linkLabel}"`
+                                : "Viewed document via direct access"
@@
-    const accessContext = link?.name ? `via shared link "${link.name}"` : `via shared link "Link ${link?.id.slice(0, 5)}"`;
+    const linkLabel = link?.name ?? (typeof link?.id === 'string' ? `Link ${link.id.slice(0, 5)}` : null);
+    const accessContext = linkLabel ? `via shared link "${linkLabel}"` : "via direct access";
@@
-                        text: link?.name
-                            ? `Dataroom accessed via shared link "${link.name}"`
-                            : `Dataroom accessed via shared link "Link ${link?.id.slice(0, 5)}"`
+                        text: linkLabel
+                            ? `Dataroom accessed via shared link "${linkLabel}"`
+                            : "Dataroom accessed via direct access"
@@
-    } else if (link?.name) {
-        downloadContext = `via shared link "${link.name}"`;
-    } else {
-        downloadContext = `via shared link "Link ${link?.id.slice(0, 5)}"`;
-    }
+    } else if (linkLabel) {
+        downloadContext = `via shared link "${linkLabel}"`;
+    } else {
+        downloadContext = "via direct access";
+    }
@@
-                                : link?.name
-                                    ? `Document download via shared link "${link.name}"`
-                                    : `Document download via shared link "Link ${link?.id.slice(0, 5)}"`
+                                : linkLabel
+                                    ? `Document download via shared link "${linkLabel}"`
+                                    : "Document download via direct access"

Also applies to: 92-97, 132-134, 187-190, 243-248, 301-305

🧹 Nitpick comments (9)
pages/api/links/download/bulk.ts (1)

306-324: Don’t block bulk download on Slack latency; optionally send after success.

Awaiting Slack here can slow user downloads and the event name suggests a successful download. Two options:

  • Non-blocking: fire-and-forget with local error handling.
  • Semantics: emit after the Lambda returns a URL (i.e., confirmed success), or include a status field.

Apply for non-blocking (minimal change):

-      if (view.dataroom?.teamId) {
-        try {
-          await notifyDocumentDownload({
+      if (view.dataroom?.teamId) {
+        notifyDocumentDownload({
             teamId: view.dataroom.teamId,
             documentId: undefined, // Bulk download, no specific document
             dataroomId: view.dataroom.id,
             linkId,
             viewerEmail: view.viewerEmail ?? undefined,
             viewerId: undefined,
             metadata: {
               documentCount: downloadDocuments.length,
               isBulkDownload: true,
             },
-          });
-        } catch (error) {
-          console.error("Error sending Slack notification:", error);
-        }
+          }).catch((error) => {
+            console.error("Error sending Slack notification:", error);
+          });
       }

Would you prefer we emit only after the download URL is generated? I can provide a follow-up diff to move this block into the 200-response branch with a metadata.status = "success".

lib/types/slack.ts (2)

17-21: Unify notification types shape across integration and channels

SlackIntegration.notificationTypes is an object of booleans while channels use a string union array. Standardize via a reusable map type.

@@
-    notificationTypes: {
-        document_view: boolean;
-        dataroom_access: boolean;
-        document_download: boolean;
-    };
+    notificationTypes: SlackNotificationSettings;
@@
-export type SlackNotificationType = 'document_view' | 'dataroom_access' | 'document_download';
+export type SlackNotificationType = 'document_view' | 'dataroom_access' | 'document_download';
+export type SlackNotificationSettings = Record<SlackNotificationType, boolean>;

Also applies to: 81-83


72-79: Type Slack blocks more strictly (optional)

Swap any[] for official Slack block types to catch template issues at compile time.

+import type { KnownBlock, Block } from '@slack/types';
@@
-export interface SlackMessage {
+export interface SlackMessage {
     channel?: string;
     text?: string;
-    blocks?: any[];
+    blocks?: Array<KnownBlock | Block>;
     thread_ts?: string;
     unfurl_links?: boolean;
     unfurl_media?: boolean;
 }
lib/slack/client.ts (4)

62-64: Surface Slack error body on OAuth failure

Include response body for faster debugging.

-        if (!response.ok) {
-            throw new Error(`Slack OAuth failed: ${response.statusText}`);
-        }
+        if (!response.ok) {
+            const text = await response.text().catch(() => '');
+            throw new Error(`Slack OAuth failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ''}`);
+        }
@@
-        if (!data.ok) {
-            throw new Error(`Slack OAuth error: ${data.error}`);
-        }
+        if (!data.ok) {
+            throw new Error(`Slack OAuth error: ${data.error}`);
+        }

Also applies to: 66-71


79-86: Add timeout to team.info and drop unnecessary Content-Type

Consistency with OAuth call; GET doesn’t need Content-Type.

     async getWorkspaceInfo(accessToken: string): Promise<SlackWorkspaceInfo> {
         const decryptedToken = this.decryptToken(accessToken);
-        const response = await fetch(`${this.baseUrl}/team.info`, {
-            headers: {
-                'Authorization': `Bearer ${decryptedToken}`,
-                'Content-Type': 'application/x-www-form-urlencoded',
-            },
-        });
+        const ac = new AbortController();
+        const t = setTimeout(() => ac.abort(), 10000);
+        const response = await fetch(`${this.baseUrl}/team.info`, {
+            headers: {
+                'Authorization': `Bearer ${decryptedToken}`,
+            },
+            signal: ac.signal,
+        }).finally(() => clearTimeout(t));

Also applies to: 87-90


109-116: Add timeout to auth.test and simplify headers

Same rationale as above.

-        const response = await fetch(`${this.baseUrl}/auth.test`, {
-            headers: {
-                'Authorization': `Bearer ${decryptedToken}`,
-                'Content-Type': 'application/json',
-            },
-        });
+        const ac = new AbortController();
+        const t = setTimeout(() => ac.abort(), 10000);
+        const response = await fetch(`${this.baseUrl}/auth.test`, {
+            headers: {
+                'Authorization': `Bearer ${decryptedToken}`,
+            },
+            signal: ac.signal,
+        }).finally(() => clearTimeout(t));

Also applies to: 117-121


169-178: Timeout + basic 429 retry for chat.postMessage

Avoid hanging calls; respect Retry-After for transient rate limits.

-        const response = await fetch(`${this.baseUrl}/chat.postMessage`, {
-            method: 'POST',
-            headers: {
-                'Authorization': `Bearer ${decryptedToken}`,
-                'Content-Type': 'application/json',
-            },
-            body: JSON.stringify(message),
-        });
+        let response: Response;
+        for (let attempt = 0; attempt < 2; attempt++) {
+            const ac = new AbortController();
+            const t = setTimeout(() => ac.abort(), 10000);
+            response = await fetch(`${this.baseUrl}/chat.postMessage`, {
+                method: 'POST',
+                headers: {
+                    'Authorization': `Bearer ${decryptedToken}`,
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify(message),
+                signal: ac.signal,
+            }).finally(() => clearTimeout(t));
+            if (response.status !== 429) break;
+            const retryAfter = parseInt(response.headers.get('retry-after') || '1', 10);
+            await new Promise(r => setTimeout(r, Math.min(retryAfter, 5) * 1000));
+        }

Also applies to: 180-183

lib/slack/templates.ts (2)

30-33: Parallelize DB fetches to cut latency

Fetch document/dataroom/link in parallel.

-    const document = eventData.documentId ? await getDocumentInfo(eventData.documentId) : null;
-    const dataroom = eventData.dataroomId ? await getDataroomInfo(eventData.dataroomId) : null;
-    const link = eventData.linkId ? await getLinkInfo(eventData.linkId) : null;
+    const [document, dataroom, link] = await Promise.all([
+        eventData.documentId ? getDocumentInfo(eventData.documentId) : Promise.resolve(null),
+        eventData.dataroomId ? getDataroomInfo(eventData.dataroomId) : Promise.resolve(null),
+        eventData.linkId ? getLinkInfo(eventData.linkId) : Promise.resolve(null),
+    ]);
@@
-    const document = eventData.documentId ? await getDocumentInfo(eventData.documentId) : null;
-    const dataroom = eventData.dataroomId ? await getDataroomInfo(eventData.dataroomId) : null;
-    const link = eventData.linkId ? await getLinkInfo(eventData.linkId) : null;
+    const [document, dataroom, link] = await Promise.all([
+        eventData.documentId ? getDocumentInfo(eventData.documentId) : Promise.resolve(null),
+        eventData.dataroomId ? getDataroomInfo(eventData.dataroomId) : Promise.resolve(null),
+        eventData.linkId ? getLinkInfo(eventData.linkId) : Promise.resolve(null),
+    ]);

Also applies to: 220-223


111-114: Ensure NEXTAUTH_URL is set (avoid broken action links)

If unset in some envs, buttons will point to “undefined/...”.

Also applies to: 204-207, 319-324

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9a90173 and fcda408.

📒 Files selected for processing (11)
  • components/emails/slack-integration-notification.tsx (1 hunks)
  • components/ui/alert-dialog.tsx (2 hunks)
  • lib/slack/client.ts (1 hunks)
  • lib/slack/templates.ts (1 hunks)
  • lib/types/slack.ts (1 hunks)
  • pages/api/links/download/bulk.ts (5 hunks)
  • pages/api/links/download/dataroom-document.ts (4 hunks)
  • pages/api/links/download/dataroom-folder.ts (3 hunks)
  • pages/api/links/download/index.ts (4 hunks)
  • pages/api/teams/[teamId]/slack/index.ts (1 hunks)
  • pages/settings/slack.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • pages/api/teams/[teamId]/slack/index.ts
  • components/ui/alert-dialog.tsx
  • pages/settings/slack.tsx
  • components/emails/slack-integration-notification.tsx
  • pages/api/links/download/index.ts
  • pages/api/links/download/dataroom-document.ts
🧰 Additional context used
🧬 Code graph analysis (4)
lib/slack/client.ts (2)
lib/utils.ts (1)
  • decryptSlackToken (619-645)
lib/types/slack.ts (4)
  • SlackOAuthResponse (50-70)
  • SlackWorkspaceInfo (43-48)
  • SlackChannel (1-7)
  • SlackMessage (72-79)
pages/api/links/download/dataroom-folder.ts (1)
lib/slack/events.ts (1)
  • notifyDocumentDownload (120-122)
lib/slack/templates.ts (2)
lib/slack/events.ts (1)
  • SlackEventData (6-16)
lib/types/slack.ts (1)
  • SlackMessage (72-79)
pages/api/links/download/bulk.ts (2)
lib/slack/events.ts (1)
  • notifyDocumentDownload (120-122)
lib/utils/ip.ts (1)
  • getIpAddress (1-8)
🔇 Additional comments (5)
pages/api/links/download/bulk.ts (3)

10-10: Importing Slack notifier is correct and scoped well.

No concerns here.


50-50: Selecting dataroom.id is necessary for Slack payload.

Good catch—this unblocks composing the event.


282-282: Formatting-only change.

No functional impact.

pages/api/links/download/dataroom-folder.ts (1)

11-11: LGTM: Slack events import is correct.

Import path and symbol match lib/slack/events.ts. No issues.

lib/slack/client.ts (1)

27-33: Verify scopes; trim to least privilege

Do you truly need mpim:read and im:read and chat:write.public? If not required for your UX, drop them.

Comment on lines +299 to +317
if (view.link.teamId) {
try {
await notifyDocumentDownload({
teamId: view.link.teamId,
documentId: undefined,
dataroomId,
linkId,
viewerEmail: view.viewerEmail ?? undefined,
viewerId: undefined,
metadata: {
folderName: rootFolder.name,
documentCount: allDocuments.length,
isFolderDownload: true,
},
});
} catch (error) {
console.error("Error sending Slack notification:", error);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Don’t block the download API on Slack; also avoid false-positive “download” notifications.

  • The awaited Slack call adds latency to a hot path. Make it fire-and-forget with a caught promise.
  • Consider moving the notification to after the Lambda succeeds (i.e., after downloadUrl is obtained) to avoid notifying on failed/aborted downloads. If business intent is “download requested,” keep here but clarify in Slack copy.
  • Improve error log context (teamId, dataroomId, linkId) for triage.

Apply this diff to make the call non-blocking and improve logging:

-    if (view.link.teamId) {
-      try {
-        await notifyDocumentDownload({
-          teamId: view.link.teamId,
-          documentId: undefined,
-          dataroomId,
-          linkId,
-          viewerEmail: view.viewerEmail ?? undefined,
-          viewerId: undefined,
-          metadata: {
-            folderName: rootFolder.name,
-            documentCount: allDocuments.length,
-            isFolderDownload: true,
-          },
-        });
-      } catch (error) {
-        console.error("Error sending Slack notification:", error);
-      }
-    }
+    if (view.link.teamId) {
+      void notifyDocumentDownload({
+        teamId: view.link.teamId,
+        documentId: undefined,
+        dataroomId,
+        linkId,
+        viewerEmail: view.viewerEmail ?? undefined,
+        viewerId: undefined,
+        metadata: {
+          folderName: rootFolder.name,
+          documentCount: allDocuments.length,
+          isFolderDownload: true,
+        },
+      }).catch((error) => {
+        console.error("Error sending Slack notification", {
+          teamId: view.link.teamId,
+          dataroomId,
+          linkId,
+          error,
+        });
+      });
+    }

Option (if you want “only on success” semantics): move the non-blocking call to just after you compute downloadUrl and before sending the 200 response.

Run to check other usages and ensure consistent event semantics across the codebase:


🏁 Script executed:

#!/bin/bash
rg -n --no-heading "notifyDocumentDownload\(|eventType:\s*['\"]document_download['\"]" -C2

Length of output: 1972


Make notifyDocumentDownload calls non-blocking in all download APIs
Apply the fire-and-forget pattern (void notifyDocumentDownload(...).catch(...)) instead of await in:

  • pages/api/links/download/index.ts
  • pages/api/links/download/dataroom-folder.ts
  • pages/api/links/download/dataroom-document.ts
  • pages/api/links/download/bulk.ts
    Include {teamId, dataroomId?, linkId?, documentId?} in error logs for triage. Optionally move calls to after downloadUrl generation for true “download succeeded” notifications.
🤖 Prompt for AI Agents
In pages/api/links/download/dataroom-folder.ts around lines 299 to 317, the
notifyDocumentDownload call is awaited which blocks the response; change it to a
fire-and-forget pattern by calling notifyDocumentDownload without await and
attach .catch(...) to log errors, and ensure the catch log includes { teamId:
view.link.teamId, dataroomId, linkId, documentId: undefined } for triage;
optionally move the non-blocking call to after downloadUrl generation if you
want to guarantee the notification only when the download URL is created.

Comment on lines 338 to 340
date: new Date(view.viewedAt ? view.viewedAt : new Date()).toLocaleDateString(),
time: new Date(view.viewedAt ? view.viewedAt : new Date()).toLocaleTimeString(),
link: view.link.name,
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

Compute once to prevent date/time skew; use a stable timezone.

Two new Date() evaluations can diverge across midnight boundaries and are locale/host-TZ dependent. Compute once and format with an explicit timezone.

Apply these changes:

@@
-    const params = {
+    const viewedAtForWatermark = view.viewedAt ?? new Date();
+    const params = {
@@
-                email: view.viewerEmail,
-                date: new Date(view.viewedAt ? view.viewedAt : new Date()).toLocaleDateString(),
-                time: new Date(view.viewedAt ? view.viewedAt : new Date()).toLocaleTimeString(),
+                email: view.viewerEmail,
+                date: viewedAtForWatermark.toLocaleDateString('en-US', { timeZone: 'UTC' }),
+                time: viewedAtForWatermark.toLocaleTimeString('en-US', { timeZone: 'UTC', hour12: false }),
                 link: view.link.name,
                 ipAddress: getIpAddress(req.headers),

If the watermark service supports it, consider also passing an ISO timestamp (e.g., timestamp: viewedAtForWatermark.toISOString()) for unambiguous auditing.

📝 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
date: new Date(view.viewedAt ? view.viewedAt : new Date()).toLocaleDateString(),
time: new Date(view.viewedAt ? view.viewedAt : new Date()).toLocaleTimeString(),
link: view.link.name,
// Compute once to avoid skew and use a stable timezone
const viewedAtForWatermark = view.viewedAt ?? new Date();
const params = {
email: view.viewerEmail,
date: viewedAtForWatermark.toLocaleDateString('en-US', { timeZone: 'UTC' }),
time: viewedAtForWatermark.toLocaleTimeString('en-US', { timeZone: 'UTC', hour12: false }),
link: view.link.name,
ipAddress: getIpAddress(req.headers),
// Optional for unambiguous auditing:
// timestamp: viewedAtForWatermark.toISOString(),
};
🤖 Prompt for AI Agents
In pages/api/links/download/dataroom-folder.ts around lines 338 to 340, the code
instantiates new Date() twice causing potential date/time skew and uses host
locale/ TZ; compute viewedAtForWatermark once (const viewedAtForWatermark =
view.viewedAt ? new Date(view.viewedAt) : new Date()), use that single Date
instance to derive date and time strings formatted with an explicit
timezone/locale (e.g., toLocaleDateString('en-US', { timeZone: 'UTC' }) and
toLocaleTimeString('en-US', { timeZone: 'UTC' })), and also pass an ISO
timestamp (e.g., timestamp: viewedAtForWatermark.toISOString()) to the watermark
service for unambiguous auditing.

@mfts
Copy link
Owner

mfts commented Sep 13, 2025

coderabbit full review

@mfts mfts changed the base branch from main to staging September 13, 2025 15:00
@vercel
Copy link

vercel bot commented Sep 13, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
papermark Error Error Sep 13, 2025 3:03pm

@mfts mfts merged commit 89e384a into mfts:staging Sep 13, 2025
5 of 6 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Sep 13, 2025
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: 27

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/featureFlags/index.ts (1)

31-36: Avoid enabling Slack by default when EDGE_CONFIG is missing

Auto-enabling all features in dev also turns on Slack, which can surface broken UI/API paths if Slack env isn’t configured. Keep dev ergonomics for other flags but default Slack to false here.

-  if (!process.env.EDGE_CONFIG) {
-    return Object.fromEntries(
-      Object.entries(teamFeatures).map(([key, _v]) => [key, true]),
-    );
-  } else if (!teamId) {
+  if (!process.env.EDGE_CONFIG) {
+    // Enable all for local/dev except Slack which requires external config
+    return Object.fromEntries(
+      Object.entries(teamFeatures).map(([key]) => [
+        key,
+        key === "slack" ? false : true,
+      ]),
+    );
+  } else if (!teamId) {
🧹 Nitpick comments (25)
lib/featureFlags/index.ts (1)

48-54: Validate edge-config payload keys to known BetaFeatures

Runtime casts allow unknown keys to toggle flags silently. Add a schema (z.enum of BetaFeatures) to validate and ignore unknown entries.

Would you like a small zod schema snippet to validate betaFeatures from Edge Config?

lib/integrations/slack/env.ts (2)

14-15: Add explicit return type to lock contract

Helps prevent accidental changes and improves DX.

-export const getSlackEnv = () => {
+export const getSlackEnv = (): SlackEnv => {

21-25: Surface which env vars are missing without leaking secrets

Printing the paths of failing keys speeds up setup while keeping values private.

-  if (!parsed.success) {
-    throw new Error(
-      "Slack app environment variables are not configured properly.",
-    );
-  }
+  if (!parsed.success) {
+    const missing = parsed.error.issues.map((i) => i.path.join(".")).join(", ");
+    throw new Error(
+      `Slack app environment variables are not configured properly. Missing/invalid: ${missing}`,
+    );
+  }
prisma/schema/conversation.prisma (1)

204-213: Consider composite indexes for primary read paths

Per-item indexes are good. If you commonly list FAQs by team/status/recency or by dataroom/status, add composites like:

  • @@index([teamId, status, createdAt])
  • @@index([dataroomId, status, createdAt])

This can materially reduce planning time and I/O on list endpoints.

If you share typical WHERE/ORDER BY patterns, I can propose targeted indexes.

components/emails/installed-integration-notification.tsx (1)

59-66: Guard settings link against missing base URL

If NEXT_PUBLIC_BASE_URL isn’t set in the email-rendering environment, the link becomes invalid. Add a fallback or fail early.

-                href={`${process.env.NEXT_PUBLIC_BASE_URL}/settings/integrations/${integration.slug}`}
+                href={`${process.env.NEXT_PUBLIC_BASE_URL ?? "https://app.papermark.io"}/settings/integrations/${integration.slug}`}
prisma/schema/integration.prisma (1)

21-40: Operational indexes and token hygiene metadata

  • Add @@index([teamId, enabled]) to speed up listing enabled integrations per team.
  • Consider encryptionVersion (Int) and tokenLastRotatedAt (DateTime?) to support crypto migrations/rotation without out-of-band knowledge.
  • If categories are finite, switch category to an enum for data quality.

Share your common read patterns and I’ll propose exact composite indexes.

lib/integrations/slack/uninstall.ts (1)

14-24: Add timeout and handle non-2xx responses explicitly

Avoid hanging requests and surface HTTP errors cleanly.

-  const response = await fetch("https://slack.com/api/apps.uninstall", {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/x-www-form-urlencoded",
-    },
-    body: new URLSearchParams({
-      token,
-      client_id: env.SLACK_CLIENT_ID,
-      client_secret: env.SLACK_CLIENT_SECRET,
-    }),
-  });
+  const controller = new AbortController();
+  const timeout = setTimeout(() => controller.abort(), 10_000);
+  const response = await fetch("https://slack.com/api/apps.uninstall", {
+    method: "POST",
+    headers: { "Content-Type": "application/x-www-form-urlencoded" },
+    body: new URLSearchParams({
+      token,
+      client_id: env.SLACK_CLIENT_ID,
+      client_secret: env.SLACK_CLIENT_SECRET,
+    }),
+    signal: controller.signal,
+  }).finally(() => clearTimeout(timeout));
@@
-  const data = await response.json();
+  if (!response.ok) {
+    throw new Error(`Slack uninstall HTTP error: ${response.status}`);
+  }
+  const data = await response.json();

Also applies to: 26-32

lib/swr/use-slack-integration.ts (1)

14-20: Extract shared SWR options to avoid duplication across hooks.

These options are duplicated in use-slack-channels. Centralize them (e.g., lib/swr/options.ts) and import here.

Apply locally:

-    {
-      revalidateOnFocus: false,
-      revalidateOnReconnect: false,
-      dedupingInterval: 30000,
-      revalidateIfStale: false,
-      errorRetryCount: 2,
-      errorRetryInterval: 5000,
-    },
+    SLACK_SWR_OPTS,

New file:

// lib/swr/options.ts
export const SLACK_SWR_OPTS = {
  revalidateOnFocus: false,
  revalidateOnReconnect: false,
  dedupingInterval: 30000,
  revalidateIfStale: false,
  errorRetryCount: 2,
  errorRetryInterval: 5000,
} as const;
app/api/integrations/slack/oauth/callback/route.ts (1)

27-28: Remove unused workspace variable.

It’s never read.

-  let workspace: Pick<Team, "id" | "plan"> | null = null;
+  // no-op
-    workspace = await prisma.team.findUniqueOrThrow({
+    await prisma.team.findUniqueOrThrow({
lib/integrations/install.ts (1)

1-1: Prefer Next’s unstable_after over @vercel/functions waitUntil for portability.

unstable_after works across runtimes in Next.js App Router.

-import { waitUntil } from "@vercel/functions";
+import { unstable_after as after } from "next/server";
-  waitUntil(
-    (async () => {
+  after(async () => {
       const team = await prisma.team.findUniqueOrThrow({
         where: {
           id: teamId,
         },
         select: {
           name: true,
           users: {
             where: { userId },
             select: {
               user: {
                 select: { email: true },
               },
             },
           },
           installedIntegrations: {
             where: { integrationId },
             select: {
               integration: {
                 select: {
                   name: true,
                   slug: true,
                 },
               },
             },
           },
         },
       });
       // ...
-    })(),
-  );
+  });
lib/swr/use-slack-channels.ts (2)

17-23: Extract shared SWR options to a constant (same as useSlackIntegration).

Avoid duplication and ensure consistency of retry/backoff.

-    {
-      revalidateOnFocus: false,
-      revalidateOnReconnect: false,
-      dedupingInterval: 30000,
-      revalidateIfStale: false,
-      errorRetryCount: 2,
-      errorRetryInterval: 5000,
-    },
+    SLACK_SWR_OPTS,

Additional change outside this hunk:

// import from your new options module
import { SLACK_SWR_OPTS } from "@/lib/swr/options";

27-27: Use nullish coalescing for array default.

Safer when empty arrays are valid values.

-    channels: data?.channels || [],
+    channels: data?.channels ?? [],
pages/api/teams/[teamId]/integrations/slack/index.ts (1)

82-86: Prefer structured logging with context.

Add teamId and userId to the log to aid debugging.

pages/api/teams/[teamId]/integrations/slack/channels.ts (1)

73-94: Consider surfacing Slack API failures instead of a 200 with empty list.

Returning 200 on unknown Slack errors hides outages. Prefer 5xx (e.g., 502) with a minimal error to allow the UI to display a retry.

lib/integrations/slack/client.ts (2)

19-21: Don’t hard-fail when OAuth env vars are missing.

Channel listing and messaging only need the bot token; throwing here breaks non-OAuth code paths and import-time.

Apply:

-    if (!this.clientId || !this.clientSecret) {
-      throw new Error("SLACK_CLIENT_ID and SLACK_CLIENT_SECRET must be set");
-    }
+    if (!this.clientId || !this.clientSecret) {
+      console.warn(
+        "Slack OAuth env not set; OAuth features disabled. Token-based APIs remain available.",
+      );
+    }

224-224: Singleton instantiation can still throw at import-time.

If you keep strict env checks in the future, consider lazy instantiation (factory) to avoid import-time failures.

lib/integrations/slack/events.ts (1)

61-80: Optional: parallelize per-channel posts with bounded concurrency.

If many channels are enabled, consider Promise.allSettled or a small pool to reduce latency while preserving isolation.

lib/integrations/slack/types.ts (1)

17-17: Remove stale comment.

The inline comment adds noise.

-// from lib/types/slack.ts
prisma/migrations/20250913000000_add_integrations/migration.sql (1)

11-11: Default configuration to an empty object to avoid null handling.

Set a NOT NULL default for configuration to simplify reads and avoid null checks in resolvers/UI.

-    "configuration" JSONB,
+    "configuration" JSONB NOT NULL DEFAULT '{}'::jsonb,
pages/settings/slack.tsx (2)

223-233: Remove unused timing variables.

startTime/endTime aren’t used; keep code lean.

-          const startTime = performance.now();
           const response = await fetch(
             `/api/teams/${teamId}/integrations/slack/channels`,
             {
               method: "PUT",
               headers: { "Content-Type": "application/json" },
               body: JSON.stringify(requestBody),
             },
           );
-          const endTime = performance.now();

286-291: “debouncedChannelsUpdate” isn’t debounced; implement actual debounce.

Frequent selection changes will fire many PUTs. Debounce client-side.

-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-  const debouncedChannelsUpdate = (selectedChannelIds: string[]) => {
-    setPendingChannelUpdate(true);
-    handleChannelsUpdate(selectedChannelIds).finally(() => {
-      setPendingChannelUpdate(false);
-    });
-  };
+  const channelsUpdateTimer = useRef<number | null>(null);
+  const debouncedChannelsUpdate = (selectedChannelIds: string[]) => {
+    setPendingChannelUpdate(true);
+    if (channelsUpdateTimer.current) window.clearTimeout(channelsUpdateTimer.current);
+    channelsUpdateTimer.current = window.setTimeout(() => {
+      handleChannelsUpdate(selectedChannelIds).finally(() => {
+        setPendingChannelUpdate(false);
+      });
+    }, 400);
+  };

Also applies to: 3-3

lib/integrations/slack/templates.ts (4)

119-123: Ensure absolute URLs exist; NEXTAUTH_URL may be unset.

If NEXTAUTH_URL isn’t configured, Slack links may be invalid. Either validate and throw early, or fall back to a known public base URL.

-              ? `${process.env.NEXTAUTH_URL}/documents/${eventData.documentId}`
-              : `${process.env.NEXTAUTH_URL}/dashboard`,
+              ? `${process.env.NEXTAUTH_URL ?? ""}/documents/${eventData.documentId}`
+              : `${process.env.NEXTAUTH_URL ?? ""}/dashboard`,

Consider failing fast if the base URL is empty.

Also applies to: 219-223, 339-344


91-93: Use a consistent, explicit time format.

Server-local toLocaleString leads to inconsistent timestamps. Prefer ISO 8601 or add timezone.

-            text: `*Time:*\n${new Date().toLocaleString()}`,
+            text: `*Time:*\n${new Date().toISOString()}`,

Also applies to: 184-186, 309-311


32-39: Parallelize independent fetches (minor).

Fetch document/dataroom/link in parallel to reduce latency.

Example:

const [document, dataroom, link] = await Promise.all([
  eventData.documentId ? getDocumentInfo(eventData.documentId, eventData.teamId) : Promise.resolve(null),
  eventData.dataroomId ? getDataroomInfo(eventData.dataroomId, eventData.teamId) : Promise.resolve(null),
  eventData.linkId ? getLinkInfo(eventData.linkId, eventData.teamId) : Promise.resolve(null),
]);

Also applies to: 135-139, 235-242


415-433: Unused helper.

getViewInfo isn’t used; remove or integrate to source metadata from viewId when present.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fcda408 and 977fee1.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (33)
  • app/api/integrations/slack/oauth/authorize/route.ts (1 hunks)
  • app/api/integrations/slack/oauth/callback/route.ts (1 hunks)
  • app/api/views-dataroom/route.ts (3 hunks)
  • app/api/views/route.ts (2 hunks)
  • components/emails/installed-integration-notification.tsx (1 hunks)
  • components/settings/settings-header.tsx (2 hunks)
  • lib/featureFlags/index.ts (2 hunks)
  • lib/integrations/install.ts (1 hunks)
  • lib/integrations/slack/client.ts (1 hunks)
  • lib/integrations/slack/env.ts (1 hunks)
  • lib/integrations/slack/events.ts (1 hunks)
  • lib/integrations/slack/install.ts (1 hunks)
  • lib/integrations/slack/templates.ts (1 hunks)
  • lib/integrations/slack/types.ts (1 hunks)
  • lib/integrations/slack/uninstall.ts (1 hunks)
  • lib/integrations/slack/utils.ts (1 hunks)
  • lib/swr/use-slack-channels.ts (1 hunks)
  • lib/swr/use-slack-integration.ts (1 hunks)
  • pages/api/links/download/bulk.ts (5 hunks)
  • pages/api/links/download/dataroom-document.ts (4 hunks)
  • pages/api/links/download/dataroom-folder.ts (3 hunks)
  • pages/api/links/download/index.ts (4 hunks)
  • pages/api/teams/[teamId]/datarooms/[id]/documents/index.ts (0 hunks)
  • pages/api/teams/[teamId]/integrations/slack/channels.ts (1 hunks)
  • pages/api/teams/[teamId]/integrations/slack/index.ts (1 hunks)
  • pages/settings/slack.tsx (1 hunks)
  • prisma/migrations/20250913000000_add_integrations/migration.sql (1 hunks)
  • prisma/schema/conversation.prisma (2 hunks)
  • prisma/schema/dataroom.prisma (2 hunks)
  • prisma/schema/document.prisma (1 hunks)
  • prisma/schema/integration.prisma (1 hunks)
  • prisma/schema/schema.prisma (2 hunks)
  • prisma/schema/team.prisma (1 hunks)
💤 Files with no reviewable changes (1)
  • pages/api/teams/[teamId]/datarooms/[id]/documents/index.ts
✅ Files skipped from review due to trivial changes (2)
  • prisma/schema/document.prisma
  • prisma/schema/dataroom.prisma
🚧 Files skipped from review as they are similar to previous changes (9)
  • prisma/schema/team.prisma
  • pages/api/links/download/dataroom-folder.ts
  • app/api/views-dataroom/route.ts
  • prisma/schema/schema.prisma
  • app/api/views/route.ts
  • pages/api/links/download/bulk.ts
  • pages/api/links/download/dataroom-document.ts
  • pages/api/links/download/index.ts
  • components/settings/settings-header.tsx
🧰 Additional context used
🧬 Code graph analysis (14)
lib/integrations/slack/uninstall.ts (2)
lib/integrations/slack/env.ts (1)
  • getSlackEnv (14-30)
lib/integrations/slack/types.ts (1)
  • SlackCredential (3-11)
app/api/integrations/slack/oauth/authorize/route.ts (5)
app/api/integrations/slack/oauth/callback/route.ts (1)
  • GET (24-95)
pages/api/auth/[...nextauth].ts (1)
  • authOptions (35-204)
lib/utils/get-search-params.ts (1)
  • getSearchParams (1-10)
lib/types.ts (1)
  • CustomUser (17-17)
lib/integrations/slack/install.ts (1)
  • getSlackInstallationUrl (8-26)
lib/integrations/slack/install.ts (2)
lib/integrations/slack/env.ts (1)
  • getSlackEnv (14-30)
lib/redis.ts (1)
  • redis (4-7)
pages/api/teams/[teamId]/integrations/slack/index.ts (4)
pages/api/teams/[teamId]/integrations/slack/channels.ts (1)
  • handler (12-131)
pages/api/auth/[...nextauth].ts (1)
  • authOptions (35-204)
lib/types.ts (1)
  • CustomUser (17-17)
lib/integrations/slack/env.ts (1)
  • getSlackEnv (14-30)
pages/api/teams/[teamId]/integrations/slack/channels.ts (6)
pages/api/teams/[teamId]/integrations/slack/index.ts (1)
  • handler (16-51)
pages/api/auth/[...nextauth].ts (1)
  • authOptions (35-204)
lib/types.ts (1)
  • CustomUser (17-17)
lib/integrations/slack/env.ts (1)
  • getSlackEnv (14-30)
lib/integrations/slack/client.ts (1)
  • slackClient (224-224)
lib/integrations/slack/types.ts (1)
  • SlackCredential (3-11)
lib/integrations/install.ts (1)
lib/resend.ts (1)
  • sendEmail (12-96)
app/api/integrations/slack/oauth/callback/route.ts (7)
lib/integrations/slack/env.ts (1)
  • getSlackEnv (14-30)
lib/types.ts (2)
  • Team (304-311)
  • CustomUser (17-17)
lib/utils/get-search-params.ts (1)
  • getSearchParams (1-10)
lib/redis.ts (1)
  • redis (4-7)
lib/integrations/slack/types.ts (1)
  • SlackCredential (3-11)
lib/integrations/slack/utils.ts (1)
  • encryptSlackToken (3-28)
lib/integrations/install.ts (1)
  • installIntegration (16-98)
pages/settings/slack.tsx (11)
context/team-context.tsx (1)
  • useTeam (85-85)
lib/hooks/use-feature-flags.ts (1)
  • useFeatureFlags (10-31)
lib/swr/use-slack-integration.ts (1)
  • useSlackIntegration (8-29)
lib/swr/use-slack-channels.ts (1)
  • useSlackChannels (7-32)
lib/integrations/slack/types.ts (2)
  • SlackChannelConfig (61-66)
  • SlackIntegration (26-32)
components/layouts/app.tsx (1)
  • AppLayout (16-38)
components/settings/settings-header.tsx (1)
  • SettingsHeader (8-98)
components/settings/slack-settings-skeleton.tsx (1)
  • SlackSettingsSkeleton (3-82)
components/shared/icons/slack-icon.tsx (1)
  • SlackIcon (8-36)
components/ui/tooltip.tsx (1)
  • BadgeTooltip (53-107)
components/ui/multi-select-v2.tsx (1)
  • MultiSelect (134-378)
lib/swr/use-slack-channels.ts (3)
context/team-context.tsx (1)
  • useTeam (85-85)
lib/integrations/slack/types.ts (1)
  • SlackChannel (18-24)
lib/utils.ts (1)
  • fetcher (48-62)
lib/integrations/slack/events.ts (4)
lib/integrations/slack/client.ts (1)
  • SlackClient (9-221)
lib/integrations/slack/types.ts (2)
  • SlackEventData (43-54)
  • SlackIntegration (26-32)
lib/integrations/slack/env.ts (1)
  • getSlackEnv (14-30)
lib/integrations/slack/templates.ts (1)
  • createSlackMessage (5-24)
lib/integrations/slack/templates.ts (1)
lib/integrations/slack/types.ts (2)
  • SlackEventData (43-54)
  • SlackMessage (34-41)
components/emails/installed-integration-notification.tsx (1)
components/emails/shared/footer.tsx (1)
  • Footer (3-27)
lib/swr/use-slack-integration.ts (3)
context/team-context.tsx (1)
  • useTeam (85-85)
lib/integrations/slack/types.ts (1)
  • SlackIntegration (26-32)
lib/utils.ts (1)
  • fetcher (48-62)
lib/integrations/slack/client.ts (2)
lib/integrations/slack/types.ts (2)
  • SlackChannel (18-24)
  • SlackMessage (34-41)
lib/integrations/slack/utils.ts (1)
  • decryptSlackToken (30-58)
🔇 Additional comments (8)
lib/integrations/slack/utils.ts (1)

1-1: Confirm Node.js runtime usage

This relies on Node’s crypto. Ensure these helpers aren’t called in the Edge runtime (Web Crypto API differs).

pages/api/teams/[teamId]/integrations/slack/index.ts (1)

1-51: Auth and team-guarding look good.

The session check, composite key membership check, and method switch are clean.

pages/api/teams/[teamId]/integrations/slack/channels.ts (1)

62-71: Channel mapping LGTM.

Filters archived, normalizes is_member, and sorts by name.

lib/integrations/slack/types.ts (1)

34-41: Types for SlackMessage are fine for now.

Given variability of Block Kit payloads, any[] is acceptable.

prisma/migrations/20250913000000_add_integrations/migration.sql (2)

44-44: Indexes/uniques look good.

Composite uniqueness on (teamId, integrationId) and unique slug are appropriate for preventing duplicates.

Also applies to: 47-47


10-10: Confirm: updatedAt is managed by Prisma (@updatedat). Integration.updatedAt (prisma/schema/integration.prisma:16) and InstalledIntegration.updatedAt (prisma/schema/integration.prisma:35) are declared DateTime @updatedat — DB default not required.

pages/settings/slack.tsx (1)

46-55: SWR usage and gating look solid.

Nice job gating channel fetches on integration availability and disabling revalidate-on-focus to reduce churn.

Also applies to: 56-64

lib/integrations/slack/templates.ts (1)

5-24: Top-level API looks good.

Graceful error handling with null returns is appropriate for the dispatcher.

Comment on lines +27 to +34
const userTeam = await prisma.userTeam.findUnique({
where: {
userId_teamId: {
userId,
teamId,
},
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm composite unique (userId, teamId) exists for userTeam.

findUnique on userId_teamId requires a matching @@unique/@@id. If absent, use findFirst.


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Model UserTeam definition:"
rg -nP -C3 'model\s+UserTeam\b' prisma

echo
echo "Looking for composite unique on [userId, teamId]:"
rg -nP '@@unique\(\s*\[userId\s*,\s*teamId\]\s*\)' prisma || echo "Composite unique NOT found"

Length of output: 420


Fix findUnique usage — add composite unique or use findFirst

Verified: prisma/schema/team.prisma model UserTeam does NOT define @@unique([userId, teamId]) (composite unique NOT found). findUnique({ where: { userId_teamId: { userId, teamId } } }) is invalid.

  • Option A (preferred): add @@unique([userId, teamId]) to model UserTeam in prisma/schema/team.prisma so the existing findUnique call is correct.
  • Option B: change app/api/integrations/slack/oauth/authorize/route.ts (lines ~27–34) to use prisma.userTeam.findFirst({ where: { userId, teamId } }) or query by a true unique field.
🤖 Prompt for AI Agents
In app/api/integrations/slack/oauth/authorize/route.ts around lines 27 to 34,
the code uses prisma.userTeam.findUnique with a composite key userId_teamId but
the UserTeam model in prisma/schema/team.prisma does not define
@@unique([userId, teamId]), so update either the schema or the query: preferred
fix — add @@unique([userId, teamId]) to the UserTeam model in
prisma/schema/team.prisma and run prisma migrate to make the composite unique
valid; alternative quick fix — change the call in route.ts to
prisma.userTeam.findFirst({ where: { userId, teamId } }) (or query by an actual
unique field) so the query matches the schema.

}

const oauthUrl = await getSlackInstallationUrl(teamId);
console.log("oauthUrl", oauthUrl);
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

Do not log the OAuth URL (leaks state).

oauthUrl includes the state param. Avoid logging it.

-    console.log("oauthUrl", oauthUrl);
+    // Avoid logging oauthUrl; it contains sensitive `state`.
📝 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
console.log("oauthUrl", oauthUrl);
// Avoid logging oauthUrl; it contains sensitive `state`.
🤖 Prompt for AI Agents
In app/api/integrations/slack/oauth/authorize/route.ts around line 41, remove
the console.log("oauthUrl", oauthUrl) which prints the OAuth URL containing the
state param; instead either remove the logging entirely or log a non-sensitive
placeholder (e.g., "oauthUrl generated" or only log safe parts like the provider
name) and if you must inspect the URL in development use a secure debug flag
that strips or redacts the state and query params before logging.

Comment on lines +46 to +52
} catch (error) {
console.error("Slack OAuth authorization error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
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

Return 400 on validation errors instead of 500.

Handle Zod parsing failures explicitly.

-  } catch (error) {
+  } catch (error) {
+    if (error instanceof z.ZodError) {
+      return NextResponse.json({ error: "Invalid request" }, { status: 400 });
+    }
     console.error("Slack OAuth authorization error:", error);
     return NextResponse.json(
       { error: "Internal server error" },
       { status: 500 },
     );
   }
📝 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
} catch (error) {
console.error("Slack OAuth authorization error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
}
console.error("Slack OAuth authorization error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
🤖 Prompt for AI Agents
In app/api/integrations/slack/oauth/authorize/route.ts around lines 46 to 52,
the catch block currently returns a 500 for all errors; update it to detect Zod
validation failures and return a 400 instead. Import ZodError from "zod" (or use
error.name === "ZodError"), and in the catch: if the error is a ZodError return
NextResponse.json({ error: "Invalid request", details: /* error issues or
message */ }, { status: 400 }); otherwise log the error and return the existing
500 response; ensure logs distinguish validation vs internal errors.

Comment on lines +5 to +6
import { getSession } from "next-auth/react";
import z from "zod";
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

Use getServerSession(authOptions) in App Router route.

getSession from next-auth/react is for client; it will not work reliably in a server route.

-import { getSession } from "next-auth/react";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/pages/api/auth/[...nextauth]";
-    const session = await getSession();
+    const session = await getServerSession(authOptions);
📝 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
import { getSession } from "next-auth/react";
import z from "zod";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import z from "zod";
🤖 Prompt for AI Agents
In app/api/integrations/slack/oauth/callback/route.ts around lines 5-6, the code
imports getSession from next-auth/react which is client-side; replace it with
getServerSession from next-auth and call getServerSession(authOptions) in this
server route. Import or reference your authOptions (e.g., from your
[...nextauth] config) and use await getServerSession(req, res, authOptions) or
the appropriate signature for your Next.js version, remove the getSession
import/usages, and ensure the route handler runs on the server so the
server-side session is retrieved reliably.

Comment on lines +70 to +82
const data = await response.json();

console.log("data", data);

const credentials: SlackCredential = {
appId: data.app_id,
botUserId: data.bot_user_id,
scope: data.scope,
accessToken: encryptSlackToken(data.access_token),
tokenType: data.token_type,
authUser: data.authed_user,
team: data.team,
};
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

⚠️ Potential issue

Validate Slack response and avoid logging secrets.

Check data.ok, surface Slack errors, and stop logging the full payload which contains tokens.

-    const data = await response.json();
-
-    console.log("data", data);
-
-    const credentials: SlackCredential = {
-      appId: data.app_id,
-      botUserId: data.bot_user_id,
-      scope: data.scope,
-      accessToken: encryptSlackToken(data.access_token),
-      tokenType: data.token_type,
-      authUser: data.authed_user,
-      team: data.team,
-    };
+    const data = await response.json();
+    if (!data?.ok) {
+      throw new Error(`Slack OAuth failed: ${data?.error ?? "unknown_error"}`);
+    }
+    const {
+      app_id,
+      bot_user_id,
+      scope,
+      access_token,
+      token_type,
+      authed_user,
+      team,
+    } = data;
+    const credentials: SlackCredential = {
+      appId: app_id,
+      botUserId: bot_user_id,
+      scope,
+      accessToken: encryptSlackToken(access_token),
+      tokenType: token_type,
+      authUser: authed_user,
+      team,
+    };
📝 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 data = await response.json();
console.log("data", data);
const credentials: SlackCredential = {
appId: data.app_id,
botUserId: data.bot_user_id,
scope: data.scope,
accessToken: encryptSlackToken(data.access_token),
tokenType: data.token_type,
authUser: data.authed_user,
team: data.team,
};
const data = await response.json();
if (!data?.ok) {
throw new Error(`Slack OAuth failed: ${data?.error ?? "unknown_error"}`);
}
const {
app_id,
bot_user_id,
scope,
access_token,
token_type,
authed_user,
team,
} = data;
const credentials: SlackCredential = {
appId: app_id,
botUserId: bot_user_id,
scope,
accessToken: encryptSlackToken(access_token),
tokenType: token_type,
authUser: authed_user,
team,
};
🤖 Prompt for AI Agents
In app/api/integrations/slack/oauth/callback/route.ts around lines 70 to 82, the
code currently parses and logs the full Slack response and assumes success;
instead check if data.ok is true and if not throw or return an error that
surfaces data.error (and any useful error_description) to the caller; remove the
console.log("data", data) call so tokens and secrets are not printed; validate
required fields (access_token, app_id, team, etc.) before building
SlackCredential and return a clear error if any are missing; keep
encryptSlackToken for the token but never log the raw or encrypted token.

Comment on lines +96 to +156
const validationResult = slackIntegrationUpdateSchema.safeParse(req.body);

if (!validationResult.success) {
return res.status(400).json({
error: "Invalid request payload",
details: validationResult.error.errors,
});
}

const { enabled, enabledChannels } = validationResult.data;

if (enabledChannels && Object.keys(validationResult.data).length === 1) {
await prisma.installedIntegration.update({
where: {
teamId_integrationId: {
teamId,
integrationId: env.SLACK_INTEGRATION_ID,
},
},
data: { configuration: { enabledChannels } },
});

return res.status(200).json({
success: true,
enabledChannels,
updatedAt: new Date().toISOString(),
});
}

const updateData: {
enabled?: boolean;
enabledChannels?: Record<string, boolean>;
} = {};

if (enabled !== undefined) updateData.enabled = enabled;

if (enabledChannels) updateData.enabledChannels = enabledChannels;

if (Object.keys(updateData).length === 0) {
return res.status(400).json({ error: "No fields to update" });
}

const updatedIntegration = await prisma.installedIntegration.update({
where: {
teamId_integrationId: {
teamId,
integrationId: env.SLACK_INTEGRATION_ID,
},
},
data: updateData,
select: {
id: true,
credentials: true,
configuration: true,
enabled: true,
createdAt: true,
updatedAt: true,
},
});

return res.status(200).json(updatedIntegration);
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

Fix update logic: writes to a non-existent column and can clobber configuration; also no 404 when missing.

  • Bug: updateData.enabledChannels targets a top-level column that doesn't exist. enabledChannels lives under configuration.
  • Inconsistent branches: special-case path updates JSON, generic path writes invalid top-level field.
  • No existence check: update will throw, returning 500 instead of 404.

Replace the whole update section with a merge-into-JSON update and proper 404:

   const env = getSlackEnv();
   try {
-    const validationResult = slackIntegrationUpdateSchema.safeParse(req.body);
+    const validationResult = slackIntegrationUpdateSchema.safeParse(req.body);
 
     if (!validationResult.success) {
       return res.status(400).json({
         error: "Invalid request payload",
         details: validationResult.error.errors,
       });
     }
 
-    const { enabled, enabledChannels } = validationResult.data;
-
-    if (enabledChannels && Object.keys(validationResult.data).length === 1) {
-      await prisma.installedIntegration.update({
-        where: {
-          teamId_integrationId: {
-            teamId,
-            integrationId: env.SLACK_INTEGRATION_ID,
-          },
-        },
-        data: { configuration: { enabledChannels } },
-      });
-
-      return res.status(200).json({
-        success: true,
-        enabledChannels,
-        updatedAt: new Date().toISOString(),
-      });
-    }
-
-    const updateData: {
-      enabled?: boolean;
-      enabledChannels?: Record<string, boolean>;
-    } = {};
-
-    if (enabled !== undefined) updateData.enabled = enabled;
-
-    if (enabledChannels) updateData.enabledChannels = enabledChannels;
-
-    if (Object.keys(updateData).length === 0) {
-      return res.status(400).json({ error: "No fields to update" });
-    }
-
-    const updatedIntegration = await prisma.installedIntegration.update({
+    const { enabled, enabledChannels } = validationResult.data;
+
+    const existing = await prisma.installedIntegration.findUnique({
+      where: {
+        teamId_integrationId: {
+          teamId,
+          integrationId: env.SLACK_INTEGRATION_ID,
+        },
+      },
+      select: { configuration: true },
+    });
+    if (!existing) {
+      return res.status(404).json({ error: "Slack integration not found" });
+    }
+
+    const nextConfig =
+      enabledChannels != null
+        ? { ...(existing.configuration || {}), enabledChannels }
+        : undefined;
+
+    if (enabled === undefined && nextConfig === undefined) {
+      return res.status(400).json({ error: "No fields to update" });
+    }
+
+    const updatedIntegration = await prisma.installedIntegration.update({
       where: {
         teamId_integrationId: {
           teamId,
           integrationId: env.SLACK_INTEGRATION_ID,
         },
       },
-      data: updateData,
+      data: {
+        ...(enabled !== undefined ? { enabled } : {}),
+        ...(nextConfig ? { configuration: { set: nextConfig } } : {}),
+      },
       select: {
         id: true,
-        credentials: true,
         configuration: true,
         enabled: true,
         createdAt: true,
         updatedAt: true,
       },
     });
 
     return res.status(200).json(updatedIntegration);
📝 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 validationResult = slackIntegrationUpdateSchema.safeParse(req.body);
if (!validationResult.success) {
return res.status(400).json({
error: "Invalid request payload",
details: validationResult.error.errors,
});
}
const { enabled, enabledChannels } = validationResult.data;
if (enabledChannels && Object.keys(validationResult.data).length === 1) {
await prisma.installedIntegration.update({
where: {
teamId_integrationId: {
teamId,
integrationId: env.SLACK_INTEGRATION_ID,
},
},
data: { configuration: { enabledChannels } },
});
return res.status(200).json({
success: true,
enabledChannels,
updatedAt: new Date().toISOString(),
});
}
const updateData: {
enabled?: boolean;
enabledChannels?: Record<string, boolean>;
} = {};
if (enabled !== undefined) updateData.enabled = enabled;
if (enabledChannels) updateData.enabledChannels = enabledChannels;
if (Object.keys(updateData).length === 0) {
return res.status(400).json({ error: "No fields to update" });
}
const updatedIntegration = await prisma.installedIntegration.update({
where: {
teamId_integrationId: {
teamId,
integrationId: env.SLACK_INTEGRATION_ID,
},
},
data: updateData,
select: {
id: true,
credentials: true,
configuration: true,
enabled: true,
createdAt: true,
updatedAt: true,
},
});
return res.status(200).json(updatedIntegration);
const validationResult = slackIntegrationUpdateSchema.safeParse(req.body);
if (!validationResult.success) {
return res.status(400).json({
error: "Invalid request payload",
details: validationResult.error.errors,
});
}
const { enabled, enabledChannels } = validationResult.data;
const existing = await prisma.installedIntegration.findUnique({
where: {
teamId_integrationId: {
teamId,
integrationId: env.SLACK_INTEGRATION_ID,
},
},
select: { configuration: true },
});
if (!existing) {
return res.status(404).json({ error: "Slack integration not found" });
}
const nextConfig =
enabledChannels != null
? { ...(existing.configuration || {}), enabledChannels }
: undefined;
if (enabled === undefined && nextConfig === undefined) {
return res.status(400).json({ error: "No fields to update" });
}
const updatedIntegration = await prisma.installedIntegration.update({
where: {
teamId_integrationId: {
teamId,
integrationId: env.SLACK_INTEGRATION_ID,
},
},
data: {
...(enabled !== undefined ? { enabled } : {}),
...(nextConfig ? { configuration: { set: nextConfig } } : {}),
},
select: {
id: true,
configuration: true,
enabled: true,
createdAt: true,
updatedAt: true,
},
});
return res.status(200).json(updatedIntegration);

import { useCallback, useEffect, useMemo, useState } from "react";

import { useTeam } from "@/context/team-context";
import { CircleHelpIcon, Hash, Settings, XCircleIcon } from "lucide-react";
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

Fix lucide-react imports and components; current names don’t exist.

lucide-react exports icons without the “Icon” suffix. Use CircleHelp and XCircle.

-import { CircleHelpIcon, Hash, Settings, XCircleIcon } from "lucide-react";
+import { CircleHelp, Hash, Settings, XCircle } from "lucide-react";
-                                <CircleHelpIcon className="h-4 w-4 shrink-0 text-muted-foreground hover:text-foreground" />
+                                <CircleHelp className="h-4 w-4 shrink-0 text-muted-foreground hover:text-foreground" />
-                                <XCircleIcon className="h-4 w-4" />
+                                <XCircle className="h-4 w-4" />

Also applies to: 466-467, 483-483

🤖 Prompt for AI Agents
In pages/settings/slack.tsx around line 6 (and also check lines 466-467 and
483), the code imports and uses lucide-react icons with an "Icon" suffix that
doesn't exist; update the import to use the correct exported names (e.g., import
CircleHelp and XCircle instead of CircleHelpIcon and XCircleIcon) and replace
any JSX usages of <CircleHelpIcon /> and <XCircleIcon /> with <CircleHelp /> and
<XCircle /> respectively, ensuring any props passed remain unchanged.

Comment on lines +121 to +127
// Redirect if Slack feature is not enabled
useEffect(() => {
if (!isFeatureEnabled("slack")) {
router.push("/settings/general");
toast.error("This feature is not available for your team");
}
}, [isFeatureEnabled, router]);
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

Avoid false redirects: gate feature-flag redirect on loading state.

isFeatureEnabled("slack") returns false while flags load, causing unwanted redirect. Wait for flags to load and use replace to avoid back-button loops.

-  const { isFeatureEnabled } = useFeatureFlags();
+  const { isFeatureEnabled, isLoading } = useFeatureFlags();
...
-  useEffect(() => {
-    if (!isFeatureEnabled("slack")) {
-      router.push("/settings/general");
-      toast.error("This feature is not available for your team");
-    }
-  }, [isFeatureEnabled, router]);
+  useEffect(() => {
+    if (isLoading) return;
+    if (!isFeatureEnabled("slack")) {
+      toast.error("This feature is not available for your team");
+      router.replace("/settings/general");
+    }
+  }, [isLoading, isFeatureEnabled, router]);

Also applies to: 9-13

🤖 Prompt for AI Agents
In pages/settings/slack.tsx around lines 121-127, the redirect runs while
feature flags are still loading causing false redirects; update the effect to
wait for the feature-flag loading state (e.g., isLoading or flagsLoaded) and
only redirect when flags have finished loading and isFeatureEnabled("slack") is
false, use router.replace("/settings/general") instead of push to avoid
back-button loops, and include the loading state in the dependency array (e.g.,
[isFeatureEnabled, router, isFeatureFlagsLoaded]).

Comment on lines +153 to +172
const handleDisconnect = async () => {
const disconnectPromise = async () => {
const response = await fetch(`/api/teams/${teamId}/integrations/slack`, {
method: "DELETE",
});

if (response.ok) {
mutateIntegration(undefined, false);
} else {
const data = await response.json();
throw new Error(data.error || "Failed to disconnect Slack");
}
};

toast.promise(disconnectPromise(), {
loading: "Disconnecting Slack integration...",
success: "Slack integration disconnected successfully",
error: "Failed to disconnect Slack integration. Please try again.",
});
};
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

Guard against missing teamId in disconnect flow.

Prevent “undefined” API path and provide user feedback.

-  const handleDisconnect = async () => {
+  const handleDisconnect = async () => {
+    if (!teamId) {
+      toast.error("No team selected");
+      return;
+    }
     const disconnectPromise = async () => {
       const response = await fetch(`/api/teams/${teamId}/integrations/slack`, {
         method: "DELETE",
       });
       if (response.ok) {
         mutateIntegration(undefined, false);
+        // Optional: clear channels cache as well
+        // mutateChannels({ channels: [] }, false);
       } else {
         const data = await response.json();
         throw new Error(data.error || "Failed to disconnect Slack");
       }
     };
     toast.promise(disconnectPromise(), {
📝 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 handleDisconnect = async () => {
const disconnectPromise = async () => {
const response = await fetch(`/api/teams/${teamId}/integrations/slack`, {
method: "DELETE",
});
if (response.ok) {
mutateIntegration(undefined, false);
} else {
const data = await response.json();
throw new Error(data.error || "Failed to disconnect Slack");
}
};
toast.promise(disconnectPromise(), {
loading: "Disconnecting Slack integration...",
success: "Slack integration disconnected successfully",
error: "Failed to disconnect Slack integration. Please try again.",
});
};
const handleDisconnect = async () => {
if (!teamId) {
toast.error("No team selected");
return;
}
const disconnectPromise = async () => {
const response = await fetch(`/api/teams/${teamId}/integrations/slack`, {
method: "DELETE",
});
if (response.ok) {
mutateIntegration(undefined, false);
// Optional: clear channels cache as well
// mutateChannels({ channels: [] }, false);
} else {
const data = await response.json();
throw new Error(data.error || "Failed to disconnect Slack");
}
};
toast.promise(disconnectPromise(), {
loading: "Disconnecting Slack integration...",
success: "Slack integration disconnected successfully",
error: "Failed to disconnect Slack integration. Please try again.",
});
};
🤖 Prompt for AI Agents
In pages/settings/slack.tsx around lines 153 to 172, the disconnect flow can
call the API with an undefined teamId which produces an invalid URL and poor UX;
guard by checking teamId before making the request and aborting with user
feedback. Implement an early check that if (!teamId) then call
toast.error("Unable to disconnect: missing team ID") (or similar) and return
without invoking disconnectPromise; alternatively incorporate the check inside
disconnectPromise to throw a descriptive error before calling fetch, and ensure
toast.promise receives that rejection so the user sees the message instead of
attempting a DELETE to /api/teams/undefined/....

-- CreateTable
CREATE TABLE "InstalledIntegration" (
"id" TEXT NOT NULL,
"credentials" JSONB,
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

⚠️ Potential issue

Do not store OAuth credentials in plaintext JSON. Encrypt or segregate secrets.

Storing tokens in JSONB is a high-impact security risk. Prefer application-level envelope encryption (KMS/HSM) and store ciphertext+nonce, or move credentials to a dedicated secrets table with stricter access. At minimum, ensure tokens are encrypted before persisting.

I can draft a follow-up migration and helper to write/read encrypted blobs if helpful.

Also applies to: 11-11

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