Skip to content

Conversation

@shnai0
Copy link
Collaborator

@shnai0 shnai0 commented Nov 26, 2025

  • nda certificate
  • email
  • nda certifciate for textual nda

Summary by CodeRabbit

  • New Features
    • NDA completion emails are sent to admins and CC’d team members when agreements are signed, including signer and agreement details and a link to view/download the certificate.
    • Users can download NDA certificate PDFs directly from views/visitors tables.
    • Location and device info captured and included in NDA notifications.

✏️ Tip: You can customize this high-level summary in your review settings.

@shnai0 shnai0 requested a review from mfts as a code owner November 26, 2025 19:08
@vercel
Copy link

vercel bot commented Nov 26, 2025

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

Project Deployment Preview Comments Updated (UTC)
papermark Ready Ready Preview Comment Nov 29, 2025 7:35pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

Walkthrough

Adds NDA completion emails, a SignedNDA email template and sender, NDA certificate PDF generation endpoint, and UI actions to download certificates; updates view creation flows to collect team/admin emails and conditionally trigger NDA notification; makes link creation explicitly set agreement fields.

Changes

Cohort / File(s) Summary
NDA Email template & sender
components/emails/signed-nda.tsx, lib/emails/send-signed-nda.ts
Adds a SignedNDA React email template and sendSignedNDAEmail function that composes and dispatches NDA-signed notifications (to owner, CC team members) with viewer and location details; validates ownerEmail and returns structured success/error.
View creation flows (email trigger)
app/api/views-dataroom/route.ts, app/api/views/route.ts
Extends dataroom and document view handlers to include nested team user retrieval, derive an owner/admin email, compute a location string (Vercel geolocation or localhost fallback), and conditionally call sendSignedNDAEmail asynchronously when agreement/notification flags and confirmed agreement are present.
NDA certificate API
pages/api/views/[viewId]/nda-certificate.ts
Adds a GET API route that authenticates, authorizes team access, fetches agreement/response, builds a PDF (text or linked PDF fallback), appends a certificate page with recipient/location/device/system info, and returns a downloadable PDF attachment.
UI: certificate download actions
components/analytics/views-table.tsx, components/visitors/dataroom-visitors-table.tsx, components/visitors/visitors-table.tsx
Renders a new certificate download badge/button (FileTextIcon) alongside agreement indicators; button fetches /api/views/{id}/nda-certificate, converts to blob, triggers programmatic PDF download, and shows error toasts on failure while preventing row-click propagation.
Link creation shape
pages/api/links/index.ts
Changes link creation payload to always include enableAgreement (default false) and set agreementId to the provided id only when enableAgreement is true (otherwise null), ensuring consistent data shape.

Possibly related PRs

Pre-merge checks

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Nda certificate' is vague and generic, using non-descriptive terminology that doesn't convey meaningful information about the substantial changes across multiple files. Revise the title to be more specific and descriptive, such as 'Add NDA certificate generation and signed NDA email notifications' or 'Implement NDA certificate PDF export with email notifications to admins and team members'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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

🧹 Nitpick comments (7)
components/visitors/visitors-table.tsx (1)

299-350: Extract duplicate NDA certificate download logic into a shared utility.

This download handler is duplicated across visitors-table.tsx, views-table.tsx, and dataroom-visitors-table.tsx. Consider extracting it into a reusable utility function to improve maintainability.

// lib/utils/download-nda-certificate.ts
export async function downloadNdaCertificate(
  viewId: string,
  viewerName?: string | null,
  viewerEmail?: string | null,
): Promise<void> {
  const response = await fetch(`/api/views/${viewId}/nda-certificate`);
  if (!response.ok) {
    throw new Error("Failed to generate certificate");
  }
  const blob = await response.blob();
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `NDA-Certificate-${viewerName || viewerEmail || "Anonymous"}-${Date.now()}.pdf`;
  document.body.appendChild(a);
  a.click();
  window.URL.revokeObjectURL(url);
  document.body.removeChild(a);
}
components/emails/signed-nda.tsx (1)

37-41: Consider using an environment variable for the base URL.

The base URL https://app.papermark.com is hardcoded. For flexibility across environments (staging, self-hosted instances), consider using an environment variable.

+  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://app.papermark.com";
   const resourceUrl = dataroomId
-    ? `https://app.papermark.com/datarooms/${dataroomId}`
+    ? `${baseUrl}/datarooms/${dataroomId}`
     : documentId
-      ? `https://app.papermark.com/documents/${documentId}`
-      : `https://app.papermark.com`;
+      ? `${baseUrl}/documents/${documentId}`
+      : baseUrl;
pages/api/views/[viewId]/nda-certificate.ts (1)

241-248: Add timeout to external PDF fetch requests.

These external HTTP requests have no timeout, which could cause the API to hang if the remote server is slow or unresponsive.

+          const controller = new AbortController();
+          const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
           const response = await fetch(agreement.content, {
             headers: { Accept: "application/pdf" },
+            signal: controller.signal,
           });
+          clearTimeout(timeoutId);
           if (!response.ok) {
             throw new Error("Failed to fetch agreement document");
           }

Apply similarly to the fetch at line 297.

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

707-763: Approve, but consider extracting duplicated NDA email logic.

The NDA email sending logic is correct and properly handles the async context. However, this block is nearly identical to lines 894-951 in the DOCUMENT_VIEW path. Consider extracting a helper function to reduce duplication.

// Example helper (could be placed outside POST or in a separate module)
async function sendNDACompletionEmail({
  request,
  viewId,
  dataroomId,
  documentId,
  link,
  email,
  name,
}: {
  request: NextRequest;
  viewId: string;
  dataroomId?: string;
  documentId?: string;
  link: /* appropriate type */;
  email: string | null;
  name: string | null;
}) {
  const geo = process.env.VERCEL === "1" ? geolocation(request) : LOCALHOST_GEO_DATA;
  const locationString = geo.city && geo.country
    ? geo.region && geo.region !== geo.city
      ? `${geo.city}, ${geo.region}, ${geo.country}`
      : `${geo.city}, ${geo.country}`
    : undefined;

  const adminUser = link.team.users.find((u) => u.role === "ADMIN");
  const adminEmail = adminUser?.user.email || null;
  const teamMembers = link.team.users
    .map((u) => u.user.email)
    .filter((e): e is string => !!e && e !== adminEmail);

  await sendSignedNDAEmail({
    ownerEmail: adminEmail,
    viewId,
    dataroomId,
    documentId,
    agreementName: link.agreement.name,
    linkName: link.name || `Link #${link.id.slice(-5)}`,
    viewerEmail: email,
    viewerName: name,
    teamMembers: teamMembers.length > 0 ? teamMembers : undefined,
    locationString,
  });
}

894-951: Duplicate NDA email logic – see earlier refactor suggestion.

This block duplicates the DATAROOM_VIEW path logic. The same helper extraction suggested above would eliminate this duplication.

lib/emails/send-signed-nda.ts (2)

28-41: Move validation before template creation for efficiency.

The ownerEmail validation happens after template creation. While harmless, validating first avoids unnecessary work when the check fails.

+}) => {
+  if (!ownerEmail) {
+    return { success: false, error: new Error("Document Owner not found") };
+  }
+
   const emailTemplate = SignedNDA({
     viewId,
     documentId,
     dataroomId,
     agreementName,
     linkName,
     viewerEmail,
     viewerName,
     locationString,
   });
   try {
-    if (!ownerEmail) {
-      throw new Error("Document Owner not found");
-    }

43-47: Subject line logic can be simplified.

The conditional reassignment can be expressed more concisely.

-    let subjectLine: string = `NDA Agreement Signed: ${agreementName}`;
     const displayName = viewerName || viewerEmail || "Someone";
-    if (displayName !== "Someone") {
-      subjectLine = `${displayName} signed the NDA: ${agreementName}`;
-    }
+    const subjectLine = displayName !== "Someone"
+      ? `${displayName} signed the NDA: ${agreementName}`
+      : `NDA Agreement Signed: ${agreementName}`;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f13e8d and 382450e.

📒 Files selected for processing (9)
  • app/api/views-dataroom/route.ts (5 hunks)
  • app/api/views/route.ts (4 hunks)
  • components/analytics/views-table.tsx (2 hunks)
  • components/emails/signed-nda.tsx (1 hunks)
  • components/visitors/dataroom-visitors-table.tsx (2 hunks)
  • components/visitors/visitors-table.tsx (2 hunks)
  • lib/emails/send-signed-nda.ts (1 hunks)
  • pages/api/links/index.ts (1 hunks)
  • pages/api/views/[viewId]/nda-certificate.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
components/emails/signed-nda.tsx (1)
components/emails/shared/footer.tsx (1)
  • Footer (3-27)
components/visitors/dataroom-visitors-table.tsx (1)
components/ui/tooltip.tsx (1)
  • BadgeTooltip (53-107)
app/api/views-dataroom/route.ts (2)
lib/utils/geo.ts (1)
  • LOCALHOST_GEO_DATA (25-32)
lib/emails/send-signed-nda.ts (1)
  • sendSignedNDAEmail (5-62)
components/analytics/views-table.tsx (1)
components/ui/tooltip.tsx (1)
  • BadgeTooltip (53-107)
lib/emails/send-signed-nda.ts (2)
components/emails/signed-nda.tsx (1)
  • SignedNDA (17-96)
lib/resend.ts (1)
  • sendEmail (12-96)
app/api/views/route.ts (3)
lib/utils/geo.ts (1)
  • LOCALHOST_GEO_DATA (25-32)
lib/emails/send-signed-nda.ts (1)
  • sendSignedNDAEmail (5-62)
lib/utils.ts (1)
  • log (64-124)
components/visitors/visitors-table.tsx (1)
components/ui/tooltip.tsx (1)
  • BadgeTooltip (53-107)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
pages/api/links/index.ts (1)

196-200: LGTM!

The explicit field assignment is cleaner than a conditional spread. The logic correctly ensures agreementId is null when agreement is disabled, which aligns with the validation at lines 111-115.

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

691-700: Well-handled geolocation with smart region deduplication.

The logic correctly falls back to localhost data in development and avoids redundant region display when it matches the city name. The undefined return for missing data is appropriate.


680-686: LGTM - comprehensive condition checks.

The conditions correctly verify all required data is present before attempting to send the NDA email: agreement must be enabled with a valid ID, notifications must be enabled, and the agreement data must exist.


703-712: Multiple admins are consistently handled using first admin as primary owner—this is intentional, not a bug.

The codebase design treats the first admin as the document owner (primary recipient) and includes other team members (including additional admins) on CC. This same pattern is used in pages/api/jobs/send-notification.ts line 192, showing it's a consistent architectural choice. The data model does allow multiple admins per team, so the scenario you identified is valid. However, the current implementation addresses this by including all team members on CC, meaning other admins receive the notification. If the intent is to notify all admins as primary recipients, that would require refactoring the sendSignedNDAEmail API to accept an array of owner emails.

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

137-152: LGTM – Necessary data expansion for NDA notifications.

The additional users and agreement selections appropriately support the new NDA email feature by providing team member emails for CC and the agreement name for the subject line.

lib/emails/send-signed-nda.ts (1)

5-62: LGTM – Clean email sending abstraction.

The function provides a well-structured wrapper around sendEmail with proper error handling. The typed parameter object and consistent return shape make it easy to use from callers.

Comment on lines +116 to +139
<button
onClick={async (e) => {
e.stopPropagation();
try {
const response = await fetch(
`/api/views/${row.original.id}/nda-certificate`,
);
if (!response.ok) {
throw new Error(
"Failed to generate certificate",
);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `NDA-Certificate-${row.original.viewerEmail || "Anonymous"}-${Date.now()}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
toast.error("Failed to download certificate");
}
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 | 🟡 Minor

Inconsistent behavior compared to other download implementations.

Two inconsistencies with visitors-table.tsx and dataroom-visitors-table.tsx:

  1. Line 132: Missing viewerName in filename - other files use viewerName || viewerEmail
  2. Missing success toast after download (other files show "NDA certificate downloaded successfully!")
                              a.href = url;
-                              a.download = `NDA-Certificate-${row.original.viewerEmail || "Anonymous"}-${Date.now()}.pdf`;
+                              a.download = `NDA-Certificate-${row.original.viewerName || row.original.viewerEmail || "Anonymous"}-${Date.now()}.pdf`;
                              document.body.appendChild(a);
                              a.click();
                              window.URL.revokeObjectURL(url);
                              document.body.removeChild(a);
+                              toast.success("NDA certificate downloaded successfully!");
                            } catch (error) {

Note: The View interface at line 61-79 may need a viewerName field added if not already present.

📝 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
<button
onClick={async (e) => {
e.stopPropagation();
try {
const response = await fetch(
`/api/views/${row.original.id}/nda-certificate`,
);
if (!response.ok) {
throw new Error(
"Failed to generate certificate",
);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `NDA-Certificate-${row.original.viewerEmail || "Anonymous"}-${Date.now()}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
toast.error("Failed to download certificate");
}
<button
onClick={async (e) => {
e.stopPropagation();
try {
const response = await fetch(
`/api/views/${row.original.id}/nda-certificate`,
);
if (!response.ok) {
throw new Error(
"Failed to generate certificate",
);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `NDA-Certificate-${row.original.viewerName || row.original.viewerEmail || "Anonymous"}-${Date.now()}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success("NDA certificate downloaded successfully!");
} catch (error) {
toast.error("Failed to download certificate");
}
🤖 Prompt for AI Agents
In components/analytics/views-table.tsx around lines 116 to 139, the download
handler is inconsistent with other tables: update the generated filename to
prefer row.original.viewerName and fall back to viewerEmail (i.e. use viewerName
|| viewerEmail) and add a success toast after the download completes (e.g.,
toast.success("NDA certificate downloaded successfully!")); if the View
interface (lines ~61-79) does not include viewerName, add that optional field to
the interface so TypeScript knows the property exists.

Comment on lines +144 to +173
<button
onClick={async (e) => {
e.stopPropagation();
try {
const response = await fetch(
`/api/views/${view.id}/nda-certificate`,
);
if (!response.ok) {
throw new Error(
"Failed to generate certificate",
);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(
blob,
);
const a = document.createElement(
"a",
);
a.href = url;
a.download = `NDA-Certificate-${view.viewerName || view.viewerEmail || "Anonymous"}-${Date.now()}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
toast.error(
"Failed to download certificate",
);
}
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 | 🟡 Minor

Missing success toast for consistency.

Unlike visitors-table.tsx, this implementation doesn't show a success toast after download. Consider adding it for consistent UX.

                                window.URL.revokeObjectURL(url);
                                document.body.removeChild(a);
+                               toast.success("NDA certificate downloaded successfully!");
                              } catch (error) {
📝 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
<button
onClick={async (e) => {
e.stopPropagation();
try {
const response = await fetch(
`/api/views/${view.id}/nda-certificate`,
);
if (!response.ok) {
throw new Error(
"Failed to generate certificate",
);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(
blob,
);
const a = document.createElement(
"a",
);
a.href = url;
a.download = `NDA-Certificate-${view.viewerName || view.viewerEmail || "Anonymous"}-${Date.now()}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
toast.error(
"Failed to download certificate",
);
}
<button
onClick={async (e) => {
e.stopPropagation();
try {
const response = await fetch(
`/api/views/${view.id}/nda-certificate`,
);
if (!response.ok) {
throw new Error(
"Failed to generate certificate",
);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(
blob,
);
const a = document.createElement(
"a",
);
a.href = url;
a.download = `NDA-Certificate-${view.viewerName || view.viewerEmail || "Anonymous"}-${Date.now()}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success("NDA certificate downloaded successfully!");
} catch (error) {
toast.error(
"Failed to download certificate",
);
}
🤖 Prompt for AI Agents
In components/visitors/dataroom-visitors-table.tsx around lines 144 to 173, the
download flow lacks a success toast for consistency with visitors-table.tsx;
after the download is successfully prepared and triggered (i.e., after a.click()
and before/after revoking the object URL), call toast.success with a brief
message like "Certificate downloaded" (optionally include
viewerName/viewerEmail), and ensure the toast utility is imported in this file
if not already; keep the try/catch structure so the success toast only fires on
successful response/blob creation.

Comment on lines +537 to +539
const viewedDate = view.viewedAt
? format(new Date(view.viewedAt), "yyyy-MM-dd h:mm a '(UTC)'")
: "Not available";
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 | 🟡 Minor

Date format label "(UTC)" is misleading.

The format string includes '(UTC)' as a literal suffix, but format() from date-fns doesn't convert the date to UTC—it uses the local timezone. This could display incorrect information.

Either format in UTC or remove the misleading suffix:

-    const viewedDate = view.viewedAt
-      ? format(new Date(view.viewedAt), "yyyy-MM-dd h:mm a '(UTC)'")
-      : "Not available";
+    const viewedDate = view.viewedAt
+      ? format(new Date(view.viewedAt), "yyyy-MM-dd h:mm a")
+      : "Not available";

Or use formatInTimeZone from date-fns-tz for actual UTC formatting:

import { formatInTimeZone } from 'date-fns-tz';
// ...
const viewedDate = view.viewedAt
  ? formatInTimeZone(new Date(view.viewedAt), 'UTC', "yyyy-MM-dd h:mm a '(UTC)'")
  : "Not available";
🤖 Prompt for AI Agents
In pages/api/views/[viewId]/nda-certificate.ts around lines 537-539, the
formatted viewedDate appends the literal "(UTC)" but uses date-fns format which
formats in local time; either remove the misleading "(UTC)" literal or perform a
true UTC format by switching to formatInTimeZone from date-fns-tz. To fix:
choose one — (A) remove " '(UTC)'" from the format string so it reflects local
time correctly, or (B) import formatInTimeZone from 'date-fns-tz', replace
format(...) with formatInTimeZone(new Date(view.viewedAt), 'UTC', "yyyy-MM-dd
h:mm a '(UTC)'"), and update imports accordingly.

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 (1)
pages/api/views/[viewId]/nda-certificate.ts (1)

558-583: “(UTC)” suffix is still misleading; format is in local server time (also for completedDate).

date-fns’s format does not convert to UTC; it formats the Date in the server’s local timezone. Appending "(UTC)" to both viewedDate and completedDate therefore mislabels the time shown on the certificate.

Either:

  • Drop the literal "(UTC)" from the format strings so they reflect server/local time, or
  • Switch to a true UTC formatter (e.g., formatInTimeZone from date-fns-tz) and actually format in UTC.

Also apply the same treatment to completedDate, not just viewedDate.

Does `date-fns` `format()` use the local timezone by default, and is `formatInTimeZone` from `date-fns-tz` the recommended way to format a `Date` in UTC while appending a literal "(UTC)" label?
🧹 Nitpick comments (3)
app/api/views/route.ts (1)

113-128: Based on my verification of the codebase, the review comment identifies valid concerns. Here's what I confirmed:

  1. Role semantics verified: The Prisma schema (prisma/schema/team.prisma) defines enum Role { ADMIN, MANAGER, MEMBER }, confirming "ADMIN" is the correct discriminator.

  2. Null handling confirmed: When no ADMIN user exists or the admin lacks an email, adminEmail becomes null. The sendSignedNDAEmail function does check for this (if (!ownerEmail) { throw new Error(...) }) and catches the error, but the call site wraps it in waitUntil() for background execution. The error IS logged via the log() function, but since it's async/background, it may appear silent to the user.

  3. Multiple admins issue verified: The code uses find() to get the first admin with no explicit "primary" admin concept in the schema, making the selection arbitrary.


Admin email fallback needed for robustness in NDA notifications

When a team has no ADMIN user or the admin lacks an email, adminEmail becomes null and sendSignedNDAEmail throws "Document Owner not found". The error is logged but executes asynchronously via waitUntil(), so notification failures may go unnoticed. Consider implementing a fallback (e.g., first team user, document owner, or a designated fallback email) to ensure NDA completion notifications reach someone. If multiple admins exist, the arbitrary find() selection could be replaced with an explicit "primary admin" field to ensure consistent owner assignment.

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

134-147: Team users selection is correct but might be heavier than needed.

Selecting all team.users (and nested user.email) per view works for deriving admin + CCs, but it’s now part of every link fetch, even when NDA notifications are off. If teams can be large, consider narrowing this to just what you need (e.g., only admins, or querying users lazily inside the NDA email block) to avoid unnecessary load on the view creation hot path.

Also applies to: 149-153

pages/api/views/[viewId]/nda-certificate.ts (1)

173-375: Agreement PDF creation is generally robust; consider making the contentType handling explicit.

The TEXT branch’s simple word‑wrap and multipage logic, and the LINK branch’s attempts to (a) resolve Papermark links, then (b) fall back to direct fetch, then (c) fall back to a minimal reference PDF, give you a nice progressive‑enhancement ladder.

If Agreement.contentType might ever gain new values beyond "TEXT" / "LINK", it’d be safer to make this conditional exhaustive (e.g., a switch that throws or falls back explicitly for unknown types) so future schema changes don’t silently end up in the LINK code path.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 382450e and 69f8129.

📒 Files selected for processing (3)
  • app/api/views-dataroom/route.ts (5 hunks)
  • app/api/views/route.ts (4 hunks)
  • pages/api/views/[viewId]/nda-certificate.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
pages/api/views/[viewId]/nda-certificate.ts (5)
pages/api/auth/[...nextauth].ts (1)
  • authOptions (38-194)
lib/types.ts (1)
  • CustomUser (17-17)
lib/utils.ts (1)
  • log (64-124)
lib/tinybird/pipes.ts (2)
  • getViewUserAgent (95-107)
  • getViewUserAgent_v2 (79-93)
lib/files/get-file.ts (1)
  • getFile (11-28)
app/api/views/route.ts (2)
lib/utils/geo.ts (1)
  • LOCALHOST_GEO_DATA (25-32)
lib/emails/send-signed-nda.ts (1)
  • sendSignedNDAEmail (5-62)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (3)
app/api/views/route.ts (1)

14-29: Based on my verification, I can now provide the final rewritten review comment:


Imports and geo constants verified; no action needed

The imports for sendSignedNDAEmail, geolocation, and LOCALHOST_GEO_DATA/LOCALHOST_IP are correctly implemented:

  • LOCALHOST_IP is properly exported from lib/utils/geo.ts (line 34)
  • geolocation(request) usage is safely guarded: (1) only called when process.env.VERCEL === "1", falling back to LOCALHOST_GEO_DATA otherwise, and (2) properties are accessed only after explicit null checks (geo.city && geo.country)

No changes required.

pages/api/views/[viewId]/nda-certificate.ts (2)

35-81: Access control and agreement scoping look correct.

You correctly:

  • Require an authenticated session.
  • Restrict access to users belonging to the view’s team.
  • Base the certificate on agreementResponse.agreement, ensuring you tie it to the actual agreement version the user accepted.

This keeps the route appropriately locked down and avoids coupling to any current link agreement settings.


87-103: Good handling of agreement version drift and optional user‑agent/location metadata.

Logging when agreement.updatedAt > agreementResponse.createdAt makes the known “no snapshot” limitation explicit and debuggable without breaking certificate generation, and the user‑agent fetching logic degrades gracefully to sensible defaults. The locationText / deviceType derivations give useful context while staying robust to missing Tinybird data.

Also applies to: 156-168

Comment on lines +708 to +763
// Send NDA completion email if agreement was signed and notifications are enabled
if (
hasConfirmedAgreement &&
link.enableAgreement &&
link.agreementId &&
link.enableNotification &&
link.agreement
) {
waitUntil(
(async () => {
try {
// Get location data
const geo =
process.env.VERCEL === "1"
? geolocation(request)
: LOCALHOST_GEO_DATA;
const locationString =
geo.city && geo.country
? geo.region && geo.region !== geo.city
? `${geo.city}, ${geo.region}, ${geo.country}`
: `${geo.city}, ${geo.country}`
: undefined;

// Get team members for CC
const adminUser = link.team?.users.find(
(u) => u.role === "ADMIN",
);
const adminEmail = adminUser?.user.email || null;
const teamMembers = link.team?.users
.map((u) => u.user.email)
.filter(
(email): email is string =>
!!email && email !== adminEmail,
) || [];

// Send NDA completion email
await sendSignedNDAEmail({
ownerEmail: adminEmail,
viewId: newDataroomView.id,
dataroomId: dataroomId,
agreementName: link.agreement?.name || "NDA",
linkName: link.name || `Link #${linkId.slice(-5)}`,
viewerEmail: email ?? null,
viewerName: name ?? null,
teamMembers: teamMembers.length > 0 ? teamMembers : undefined,
locationString,
});
} catch (error) {
log({
message: `Failed to send NDA completion email for dataroom view: ${newDataroomView.id}. \n\n ${error}`,
type: "error",
});
}
})(),
);
}
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 | 🟡 Minor

NDA completion email logic is sound; consider DRYing geo/admin lookup and guarding on missing admin email.

The conditions around hasConfirmedAgreement, enableAgreement, agreementId, enableNotification, and agreement look correct, and running sendSignedNDAEmail via waitUntil keeps the main response non‑blocking. Two follow‑ups:

  1. The geo lookup + locationString construction and adminEmail / teamMembers derivation are duplicated across DATAROOM and DOCUMENT views. A small helper (e.g., buildNdaEmailContext({ link, request, viewId, dataroomId, viewer })) would reduce copy‑paste and keep future changes in sync.
  2. If no ADMIN user exists, adminEmail will be null and still passed as ownerEmail. If sendSignedNDAEmail assumes a non‑null owner address, this will just log errors on every call. Either short‑circuit when !adminEmail or make sendSignedNDAEmail explicitly tolerate a missing owner (e.g., skip sending or fall back to another role).

Given the error handling and waitUntil, this is non‑blocking but worth tightening up.

Also applies to: 895-951

Comment on lines +679 to +735
// Send NDA completion email if agreement was signed and notifications are enabled
if (
hasConfirmedAgreement &&
link.enableAgreement &&
link.agreementId &&
link.enableNotification &&
link.agreement
) {
waitUntil(
(async () => {
try {
// Get location data
const geo =
process.env.VERCEL === "1"
? geolocation(request)
: LOCALHOST_GEO_DATA;
const locationString =
geo.city && geo.country
? geo.region && geo.region !== geo.city
? `${geo.city}, ${geo.region}, ${geo.country}`
: `${geo.city}, ${geo.country}`
: undefined;

// Get team members for CC
const adminUser = link.team?.users.find(
(u) => u.role === "ADMIN",
);
const adminEmail = adminUser?.user.email || null;
const teamMembers = link.team?.users
.map((u) => u.user.email)
.filter(
(email): email is string =>
!!email && email !== adminEmail,
) || [];

// Send NDA completion email
await sendSignedNDAEmail({
ownerEmail: adminEmail,
viewId: newView.id,
documentId: documentId,
dataroomId: undefined, // Document view, not dataroom
agreementName: link.agreement?.name || "NDA",
linkName: link.name || `Link #${linkId.slice(-5)}`,
viewerEmail: email ?? null,
viewerName: name ?? null,
teamMembers: teamMembers.length > 0 ? teamMembers : undefined,
locationString,
});
} catch (error) {
log({
message: `Failed to send NDA completion email for view: ${newView.id}. \n\n ${error}`,
type: "error",
});
}
})(),
);
}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the sendSignedNDAEmail function definition to verify its return type
rg -n "sendSignedNDAEmail\s*=" -A 15 --type=ts

Repository: mfts/papermark

Length of output: 864


🏁 Script executed:

# Search for all usages of sendSignedNDAEmail to understand error handling patterns
rg -n "sendSignedNDAEmail" -B 3 -A 5 --type=ts

Repository: mfts/papermark

Length of output: 4161


🏁 Script executed:

# Check how agreementResponse is used/stored in the route
rg -n "agreementResponse" -B 3 -A 3 --type=ts

Repository: mfts/papermark

Length of output: 14233


🏁 Script executed:

# Read the full sendSignedNDAEmail implementation
cat lib/emails/send-signed-nda.ts

Repository: mfts/papermark

Length of output: 1438


🏁 Script executed:

# Check the sendEmail function to understand error handling
rg -n "export const sendEmail" -A 20 --type=ts

Repository: mfts/papermark

Length of output: 4414


🏁 Script executed:

# Look at the context around the NDA email sending in app/api/views/route.ts
sed -n '500,740p' app/api/views/route.ts | head -250

Repository: mfts/papermark

Length of output: 8081


Perfect. I've now verified all the claims in the review comment. Let me confirm the findings:

Verification Results:

  1. sendSignedNDAEmail return type: Confirmed it returns { success: true, data } or { success: false, error } (lib/emails/send-signed-nda.ts)

  2. Silent failures: Confirmed all three call sites (app/api/views/route.ts:715, app/api/views-dataroom/route.ts:744 & 932) do await sendSignedNDAEmail(...) without capturing the return value, meaning success: false cases are silently ignored.

  3. De-duplication via agreementResponse: Confirmed valid. The agreementResponse is created during view creation (lines 537-540) before email sending (lines 679-735). It can be gated on: if multiple POSTs are sent with hasConfirmedAgreement=true for the same viewer/link, multiple views + multiple emails would be created. Gating on newView.agreementResponse existence would prevent this.

Both suggestions are valid, actionable, and practical. The review comment is accurate.


NDA completion email block is solid; capture email send failures and consider de‑duplication

The NDA completion email logic (agreement + notifications gate, geo enrichment, admin + CC derivation, and waitUntil wrapping) looks good and non‑blocking. Two refinements you should consider:

  • Log on email send failure: sendSignedNDAEmail returns { success, error } and internally catches errors. Since you're not inspecting the return value, non‑throwing failures will be completely silent. Capturing and logging !result.success would help observability.

  •            await sendSignedNDAEmail({
    
  •            const result = await sendSignedNDAEmail({
                 ownerEmail: adminEmail,
                 viewId: newView.id,
                 documentId: documentId,
                 dataroomId: undefined, // Document view, not dataroom
                 agreementName: link.agreement?.name || "NDA",
                 linkName: link.name || `Link #${linkId.slice(-5)}`,
                 viewerEmail: email ?? null,
                 viewerName: name ?? null,
                 teamMembers: teamMembers.length > 0 ? teamMembers : undefined,
                 locationString,
    
  •            });
    
  •            });
    
  •            if (!result?.success) {
    
  •              log({
    
  •                message: `sendSignedNDAEmail failed for view ${newView.id}: ${String(
    
  •                  result?.error ?? "unknown error",
    
  •                )}`,
    
  •                type: "error",
    
  •              });
    
  •            }
    
    
    
  • De‑duplicate via agreementResponse: If the frontend sends multiple POSTs with hasConfirmedAgreement=true for the same viewer/link, you'll create multiple views and send multiple NDA emails. Gate on newView.agreementResponse existence instead to ensure one email per agreement response:

  •    if (
    
  •      hasConfirmedAgreement &&
    
  •      link.enableAgreement &&
    
  •      link.agreementId &&
    
  •      link.enableNotification &&
    
  •      link.agreement
    
  •    ) {
    
  •    if (
    
  •      newView.agreementResponse &&
    
  •      link.enableNotification &&
    
  •      link.agreement
    
  •    ) {
    
    
    
🤖 Prompt for AI Agents
In app/api/views/route.ts around lines 679 to 735, the NDA email send is
currently fire-and-forget and ignores the sendSignedNDAEmail return value and
can be invoked multiple times for duplicate POSTs; capture the result of
sendSignedNDAEmail, check if result.success is false and call log(...) with the
returned error details inside the try block (or in a small if after the await)
so non-throwing failures are recorded, and before entering the
waitUntil/email-send block add a guard that only proceeds if
newView.agreementResponse exists (or was just created) to de-duplicate emails
for repeated agreement confirmations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants