Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unsubscribe link to email update #321

Merged
merged 4 commits into from
Feb 4, 2025
Merged

Conversation

elie222
Copy link
Owner

@elie222 elie222 commented Feb 4, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a secure, token-based unsubscribe option to summary emails for streamlined email preference management.
    • Added an API endpoint for handling email unsubscriptions, enhancing user control over email preferences.
    • Implemented a new function for generating unsubscribe tokens.
  • Refactor

    • Streamlined the email sending process to enhance clarity and improve security by modularizing email-related logic.
  • Chores

    • Disabled legacy weekly statistics email notifications.

Copy link

vercel bot commented Feb 4, 2025

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

Name Status Preview Updated (UTC)
inbox-zero ✅ Ready (Inspect) Visit Preview Feb 4, 2025 7:17pm

Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This pull request updates multiple modules related to email and token management. It disables the weekly statistics email by commenting out its send call and modularizes summary email sending with a new function. A new API endpoint is added to handle email unsubscriptions with input validation and concurrent database updates. Additionally, the Prisma schema and migration now support an EmailToken model, and utility functions for API key generation have been renamed to use a new secure token generator. Finally, unsubscribe tokens are integrated into email components, ensuring dynamic unsubscribe links.

Changes

File(s) Change Summary
apps/web/app/api/resend/route.ts
apps/web/app/api/resend/summary/route.ts
In the resend routes, the weekly stats email sending has been disabled (code commented out) and the inline summary email logic has been replaced with a new, modular sendEmail(userId: string) function.
apps/web/app/api/unsubscribe/route.ts A new API endpoint for handling email unsubscriptions is implemented. It validates input using Zod, updates user preferences, and deletes the token using Prisma.
apps/web/prisma/migrations/.../migration.sql
apps/web/prisma/schema.prisma
Introduces an EmailToken table/model with unique constraints and foreign key relationships; adds a new unique constraint on Account.userId and a new unsubscribeTokens field in the User model.
apps/web/utils/actions/api-key.ts
apps/web/utils/api-key.ts
The API key generation function is updated: generateSecureApiKey is renamed to generateSecureToken, changing the mechanism for generating the token.
apps/web/utils/unsubscribe.ts A new asynchronous function createUnsubscribeToken(userId: string) is added to generate a secure token and store it in the database for unsubscribe actions.
packages/resend/emails/summary.tsx
packages/resend/src/send.tsx
The email components are enhanced to integrate unsubscribe functionality by requiring an unsubscribeToken. Email headers and function signatures are updated accordingly.

Possibly related issues

Possibly related PRs

  • Summary update email #187: Modifies similar email sending and token handling functionalities in the resend route, specifically impacting the handling of the sendEmail function.

Poem

I'm a rabbit, skipping through code all day,
Finding tokens and tweaks along my playful way.
Weekly stats now rest while summaries gleam,
Unsubscribing with ease—a coder's dream!
In lines of light and binary hop, I cheer,
Celebrating changes that make our code dear! 🐰

Tip

🌐 Web search-backed reviews and chat
  • We have enabled web search-based reviews and chat for all users. This feature allows CodeRabbit to access the latest documentation and information on the web.
  • You can disable this feature by setting web_search: false in the knowledge_base settings.
  • Please share any feedback in the Discord discussion.

📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8020a33 and ca36311.

📒 Files selected for processing (2)
  • apps/web/prisma/schema.prisma (2 hunks)
  • apps/web/utils/unsubscribe.ts (1 hunks)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

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

CodeRabbit Commands (Invoked using PR comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Documentation and Community

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (5)
apps/web/app/api/unsubscribe/route.ts (2)

12-14: Enforce consistent error structure for better maintainability.

Currently, different errors are returned with somewhat inconsistent JSON structures (e.g., { error: "Invalid token" }, { error: "Token expired" }). Consider centralizing these responses into a consistent format (e.g., returning { error: { message: "..." } } or another common schema) to improve maintainability and clarity for API consumers.


22-28: Validate error codes for security and user clarity.

Sending a 400 status code is correct for invalid/expired tokens. However, you might consider returning different error codes (e.g., 404 for a nonexistent token or 410 for an expired token) if it helps clarify the cause for the client.

apps/web/utils/api-key.ts (1)

4-6: Consider URL-safe tokens for broader usage.

While Base64-encoded tokens are acceptable, if this token might be used in query parameters or non-encoded URLs, you might benefit from a URL-safe variant (e.g., replacing special characters like “+” and “/”). Doing so can prevent potential issues when passing tokens around in different parts of the application stack.

apps/web/utils/unsubscribe.ts (1)

5-18: Potential collision handling for token creation.

Although unlikely, random token collisions can occur (albeit rarely). If you wish to handle that edge case, you might implement a retry-on-collision approach or ensure the database enforces uniqueness and handle the conflict.

apps/web/app/api/resend/summary/route.ts (1)

181-199: Refactor nested function for better maintainability.

Consider moving the sendEmail function outside its parent function to:

  1. Improve testability
  2. Make the code more modular
  3. Reduce complexity
-  async function sendEmail(userId: string) {
-    const token = await createUnsubscribeToken(userId);
-
-    return sendSummaryEmail({
-      to: email,
-      emailProps: {
-        baseUrl: env.NEXT_PUBLIC_BASE_URL,
-        coldEmailers,
-        pendingCount,
-        needsReplyCount: typeCounts[ThreadTrackerType.NEEDS_REPLY],
-        awaitingReplyCount: typeCounts[ThreadTrackerType.AWAITING],
-        needsActionCount: typeCounts[ThreadTrackerType.NEEDS_ACTION],
-        needsReply: recentNeedsReply,
-        awaitingReply: recentAwaitingReply,
-        needsAction: recentNeedsAction,
-        unsubscribeToken: token,
-      },
-    });
-  }

Move it to a separate function with explicit parameters:

async function sendSummaryEmailWithUnsubscribe({
  userId,
  email,
  baseUrl,
  stats,
}: {
  userId: string;
  email: string;
  baseUrl: string;
  stats: {
    coldEmailers: any[];
    pendingCount: number;
    typeCounts: Record<ThreadTrackerType, number>;
    recentNeedsReply: any[];
    recentAwaitingReply: any[];
    recentNeedsAction: any[];
  };
}) {
  const token = await createUnsubscribeToken(userId);

  return sendSummaryEmail({
    to: email,
    emailProps: {
      baseUrl,
      coldEmailers: stats.coldEmailers,
      pendingCount: stats.pendingCount,
      needsReplyCount: stats.typeCounts[ThreadTrackerType.NEEDS_REPLY],
      awaitingReplyCount: stats.typeCounts[ThreadTrackerType.AWAITING],
      needsActionCount: stats.typeCounts[ThreadTrackerType.NEEDS_ACTION],
      needsReply: stats.recentNeedsReply,
      awaitingReply: stats.recentAwaitingReply,
      needsAction: stats.recentNeedsAction,
      unsubscribeToken: token,
    },
  });
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4e462c4 and 99573bc.

📒 Files selected for processing (10)
  • apps/web/app/api/resend/route.ts (1 hunks)
  • apps/web/app/api/resend/summary/route.ts (2 hunks)
  • apps/web/app/api/unsubscribe/route.ts (1 hunks)
  • apps/web/prisma/migrations/20250204162638_email_token/migration.sql (1 hunks)
  • apps/web/prisma/schema.prisma (1 hunks)
  • apps/web/utils/actions/api-key.ts (2 hunks)
  • apps/web/utils/api-key.ts (1 hunks)
  • apps/web/utils/unsubscribe.ts (1 hunks)
  • packages/resend/emails/summary.tsx (6 hunks)
  • packages/resend/src/send.tsx (4 hunks)
🔇 Additional comments (10)
apps/web/app/api/unsubscribe/route.ts (1)

30-44: Assess partial failure risk with Promise.allSettled.

Using Promise.allSettled means one operation can fail while the other succeeds, yet the endpoint still returns success. This may create a discrepancy if the user update fails but the token is deleted, removing the chance to retry unsubscribing. Consider using Promise.all if you’d like both operations to succeed or revert changes upon failure, or handle partial failures more explicitly (such as returning an error to the user).

apps/web/utils/unsubscribe.ts (1)

1-1: Confirm the correctness of subpath imports in date-fns.

You are importing addDays from "date-fns/addDays". Though valid in many setups, ensure your bundler/tooling fully supports subpath imports to avoid runtime errors or bundling issues. If not required for tree shaking, consider importing from "date-fns" directly for consistency.

apps/web/utils/actions/api-key.ts (1)

14-14: Verify token generation security for different use cases.

The generateSecureToken function is now used for both API keys and unsubscribe tokens. While reusing secure token generation is good practice, ensure that:

  1. The token length (32 bytes) provides sufficient security for both use cases
  2. Token storage and validation are appropriately scoped for each use case

Also applies to: 32-32

✅ Verification successful

Token Generation Security Appears Sufficient

  • The generateSecureToken function uses Node.js’s crypto.randomBytes(32) combined with base64 encoding, providing 256 bits of entropy. This level of randomness is generally secure for both API keys and unsubscribe tokens.
  • Ensure that downstream handling (storage, validation, and scoping) distinguishes between API key usage and unsubscribe token usage to avoid potential mix-ups.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check token generation implementation
ast-grep --pattern 'function generateSecureToken() {
  $$$
}'

Length of output: 63


Script:

#!/bin/bash
# Search for any reference to generateSecureToken in the repository,
# including its declaration, export, or usage.
rg -n "generateSecureToken"

Length of output: 441


Script:

#!/bin/bash
# Display the content of the generateSecureToken function implementation along with its surrounding context.
sed -n '1,100p' apps/web/utils/api-key.ts

Length of output: 471

packages/resend/src/send.tsx (1)

38-41: LGTM! Proper implementation of one-click unsubscribe headers.

The implementation follows Google's bulk sender requirements by including both List-Unsubscribe and List-Unsubscribe-Post headers.

packages/resend/emails/summary.tsx (1)

35-35: LGTM! Good addition of the required unsubscribeToken field.

The type safety ensures the token is always provided when using the component.

apps/web/prisma/migrations/20250204162638_email_token/migration.sql (3)

1-11: LGTM! Well-designed table schema with proper constraints.

The table design includes all necessary fields and constraints for secure token management:

  • Primary key for unique identification
  • Creation timestamp with default
  • Required fields for token and action
  • Expiration timestamp for security

13-14: LGTM! Good addition of unique index on token.

The unique constraint on the token field prevents token collisions and enhances security.


16-17: LGTM! Proper foreign key constraint with cascade.

The foreign key with cascade delete/update maintains referential integrity with the User table.

apps/web/prisma/schema.prisma (2)

101-110: LGTM! Well-structured EmailToken model.

The model includes all necessary fields and relationships:

  • Unique token field for security
  • Action field with clear documentation
  • Proper user relation with cascade delete
  • Expiration timestamp for token lifecycle

16-16: Verify the impact of adding unique constraint to userId.

The comment suggests this constraint might be removed in the future. Consider if this change could affect existing data or cause issues with multiple accounts per user.

✅ Verification successful

Unique Constraint on Account.userId – Confirmed with Schema Changes

The review change adds a unique constraint to the userId field in the Account model, enforcing a one-to-one relationship between Account and User. The retrieved shell output confirms that the code in apps/web/prisma/schema.prisma includes the constraint along with a comment noting its experimental nature.

  • The unique constraint will prevent a single user from associating with multiple accounts.
  • This change has implications only if the current or future business logic requires supporting multiple accounts per user.
  • The inline comment already indicates a potential reversion if needed.

Overall, the schema change is consistent, and the impact is as described—ensure that production data conforms to this constraint or update the schema if multiple accounts are needed.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if there are any users with multiple accounts that would violate the new constraint
rg -A 5 "model Account" | grep -A 10 "userId"

Length of output: 266

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/resend/emails/summary.tsx (1)

162-162: Consider using a more realistic token format in preview props.

The current preview token "123" is overly simplistic. Consider using a more realistic format to better represent production data.

-  unsubscribeToken: "123",
+  unsubscribeToken: "usr_123e4567-e89b-12d3-a456-426614174000",
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 99573bc and 8020a33.

📒 Files selected for processing (2)
  • apps/web/app/api/unsubscribe/route.ts (1 hunks)
  • packages/resend/emails/summary.tsx (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/api/unsubscribe/route.ts
🔇 Additional comments (2)
packages/resend/emails/summary.tsx (2)

338-344: LGTM! Well-implemented unsubscribe functionality.

The Footer component correctly implements the unsubscribe link with proper URL encoding and clear user messaging.

Also applies to: 359-362


35-35: LGTM! Clean interface extension.

The addition of the unsubscribe token to the props interface and its usage in the component is clean and follows the existing patterns.

Also applies to: 49-49

@elie222 elie222 merged commit 5f27807 into main Feb 4, 2025
2 of 4 checks passed
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.

1 participant