Skip to content

Conversation

@Nil2000
Copy link
Contributor

@Nil2000 Nil2000 commented Oct 6, 2024

✨ Codu Pull Request 💻

Fixes #1049

Pull Request details

  • Implemented email verification part if the user want's to change email.

Any Breaking changes

  • None

Associated Screenshots

image
image
image
image

@Nil2000 Nil2000 requested a review from a team as a code owner October 6, 2024 05:35
@vercel
Copy link

vercel bot commented Oct 6, 2024

@Nil2000 is attempting to deploy a commit to the Codú Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 6, 2024

Walkthrough

The changes in this pull request enhance the email management functionality within the application. Key modifications include the introduction of new components and API endpoints for updating and verifying user email addresses. The _client.tsx file now supports email updates with state management for input fields and verification processes. The database schema is updated to accommodate email verification tokens, and new utility functions are added for token management. The overall structure ensures that users can update their email addresses and verify them through an email link.

Changes

File Change Summary
app/(app)/settings/_client.tsx Added new state variables for email updates, modified form structure, and introduced handleNewEmailUpdate function.
app/api/update_email/route.ts Introduced a new API endpoint for updating user email with validation and token generation.
app/api/verify-email/route.ts Added a new API endpoint for verifying email with token validation and user email update functionality.
app/verify-email/_client.tsx Created a new React component for handling email verification process and displaying status messages.
app/verify-email/page.tsx Introduced a server-side component for managing email verification sessions and rendering the verification UI.
drizzle/0007_added_emailtokenverification_schema.sql Added EmailVerificationToken table to the database schema with necessary constraints and fields.
drizzle/0009_email-verification.sql Created EmailVerificationToken table with unique constraints and foreign key relationship.
server/api/router/profile.ts Added a new procedure updateEmail to handle email updates securely via token verification.
utils/emailToken.ts Created utility functions for managing email verification tokens, including generation, storage, and updates.
config/constants.ts Introduced a new constant TOKEN_EXPIRATION_TIME for token expiration duration.

Assessment against linked issues

Objective Addressed Explanation
Allow user to update email (1049)
Display current email and input for new email (1049)
Save new email as temporary until verified (1049)
Send verification email upon update (1049)

Possibly related PRs

  • Fix/date to string #990: The changes in this PR involve modifications to the UserDetails type and its handling, which may relate to the user profile updates in the main PR, particularly regarding the management of user data structures.

Poem

🐰 In the meadow where emails bloom,
A new address dispels the gloom.
With a click, the link is sent,
To verify, the user’s intent.
Hops of joy, let’s celebrate,
A new email, oh, what a fate! 🌼


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 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.

@github-actions
Copy link

github-actions bot commented Oct 6, 2024

Hello @Nil2000, thanks for opening your first Pull Request. The maintainers will review this Pull Request and provide feedback as soon as possible. Keep up the great work!

@Nil2000
Copy link
Contributor Author

Nil2000 commented Oct 6, 2024

@NiallJoeMaher @John-Paul-Larkin Let me know what you think about this.

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

🧹 Outside diff range and nitpick comments (10)
schema/token.ts (2)

3-9: TokenSchema structure is good, but consider enhancing security and data integrity.

The overall structure of the TokenSchema is appropriate for a token system. However, consider the following enhancements:

  1. Add a minimum length requirement for the token field to ensure security.
  2. Specify maximum lengths for userId and token to prevent potential storage issues.
  3. Add validation to ensure expiresAt is always in the future.

Here's a suggested improvement:

export const TokenSchema = z.object({
  id: z.string().uuid(),
  userId: z.string().max(255), // Adjust max length as needed
  token: z.string().min(32).max(255), // Adjust min and max as per your security requirements
  createdAt: z.date().default(() => new Date()),
  expiresAt: z.date().refine(val => val > new Date(), {
    message: "Expiration date must be in the future"
  }),
});

1-11: Consider adding email-specific fields to align with PR objectives.

The token schema is well-structured for general token management, which is crucial for the email verification system mentioned in the PR objectives. However, to more closely align with the specific use case of email verification, consider adding the following fields:

  1. email: To store the email address being verified.
  2. type: To distinguish between different token uses (e.g., 'email_verification', 'password_reset', etc.).

Here's a suggested enhancement:

export const TokenSchema = z.object({
  id: z.string().uuid(),
  userId: z.string().max(255),
  token: z.string().min(32).max(255),
  type: z.enum(['email_verification', 'password_reset']), // Add other types as needed
  email: z.string().email().optional(), // Optional to allow for non-email related tokens
  createdAt: z.date().default(() => new Date()),
  expiresAt: z.date().refine(val => val > new Date(), {
    message: "Expiration date must be in the future"
  }),
});

This enhancement would make the token schema more specific to the email verification use case while maintaining flexibility for other potential token uses.

drizzle/0007_shocking_rachel_grey.sql (1)

1-9: LGTM! Consider adding an index on the userId column.

The EmailVerificationToken table structure is well-designed and aligns with the PR objectives. It includes all necessary fields for email verification, with appropriate constraints for data integrity and security.

Consider adding an index on the userId column to optimize queries that look up tokens for a specific user:

CREATE INDEX IF NOT EXISTS "EmailVerificationToken_userId_idx" ON "EmailVerificationToken" ("userId");

This can improve performance, especially if you expect to query tokens by user frequently.

app/verify_email/page.tsx (2)

11-30: Improve user experience with more appropriate redirects

Redirecting to "/not-found" might be confusing for users who are not authenticated or whose session has expired. Consider redirecting unauthenticated users to the login page and handling the case when a user is not found differently.

Apply this diff to update the redirects:

   if (!session || !session.user) {
-    redirect("/not-found");
+    redirect("/login");
   }

   const existingUser = await db.query.user.findUnique({
     // ...
   });

   if (!existingUser) {
-    redirect("/not-found");
+    redirect("/error?message=User%20not%20found");
   }

This change improves navigation flow and provides clearer feedback to the user.


3-3: Remove unnecessary import of React

In Next.js with the App Router (app directory), importing React is not necessary, as it's automatically handled. Removing unused imports keeps the code clean.

Apply this diff to remove the import:

-import React from "react";
app/api/verify_email/route.ts (1)

17-17: Remove console.log statements from production code

Using console.log in production code can expose sensitive information and clutter the logs. It's better to remove it or use a proper logging mechanism with configurable log levels.

Apply this diff to remove the console log:

-console.log("tokenFromDb", tokenFromDb);
app/verify_email/_client.tsx (1)

44-44: Remove console.log statements in production code

The console.log statement is useful for debugging but should be removed to avoid cluttering the console and potential exposure of sensitive information in production environments.

Apply this diff to remove the console.log statement:

-        console.log("data", data);
utils/emailToken.ts (1)

34-103: Update the email content to reflect email address updates

The email template is currently tailored for new user registrations. Since this feature is for updating a user's email address, consider adjusting the content to reflect this context. This will provide clarity to the user about why they are receiving the email.

Apply the following changes to the email content:

<h1>Confirm Your Email Address</h1>
<p>Hello,</p>
- <p>Thank you for registering with us! To complete your registration, please verify your email address by clicking the button below:</p>
+ <p>You have requested to update your email address. Please confirm your new email by clicking the button below:</p>
<a href="${verificationLink}" class="btn">Verify Email</a>
<p>Please note that this link is valid for 1 hour only. If it expires, you will need to request a new one.</p>
<p>If you did not request this change, please ignore this email.</p>
app/(app)/settings/_client.tsx (2)

425-425: Ensure consistent loader sizes for visual consistency.

The Loader components have different sizes (h-4 w-4 and h-8 w-8). Unless there's a specific reason for this difference, consider using consistent sizes to maintain a uniform user interface.

If you wish to standardize the loader sizes, apply this diff (choose the appropriate size):

-                              <Loader className="text-primary h-4 w-4 animate-spin" />
+                              <Loader className="text-primary h-6 w-6 animate-spin" />

And:

-                                <Loader className="text-primary h-8 w-8 animate-spin" />
+                                <Loader className="text-primary h-6 w-6 animate-spin" />

Also applies to: 441-441


401-404: Consider adding a 'readOnly' attribute instead of 'disabled' for the current email input.

Using readOnly allows the input to be focusable, letting users copy the current email if needed, while still preventing modifications.

Apply this diff to update the input:

                              {...register("email")}
-                             disabled
+                             readOnly
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 7cc3d4f and 27f1a76.

⛔ Files ignored due to path filters (3)
  • drizzle/meta/0007_snapshot.json is excluded by !**/*.json
  • drizzle/meta/_journal.json is excluded by !**/*.json
  • package-lock.json is excluded by !**/package-lock.json, !**/*.json
📒 Files selected for processing (11)
  • app/(app)/settings/_client.tsx (6 hunks)
  • app/(app)/settings/page.tsx (4 hunks)
  • app/api/update_email/route.ts (1 hunks)
  • app/api/verify_email/route.ts (1 hunks)
  • app/verify_email/_client.tsx (1 hunks)
  • app/verify_email/page.tsx (1 hunks)
  • drizzle/0007_shocking_rachel_grey.sql (1 hunks)
  • schema/profile.ts (1 hunks)
  • schema/token.ts (1 hunks)
  • server/db/schema.ts (1 hunks)
  • utils/emailToken.ts (1 hunks)
🧰 Additional context used
🪛 Biome
app/(app)/settings/_client.tsx

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

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

(lint/complexity/noUselessTernary)


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

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

(lint/complexity/noUselessTernary)

🔇 Additional comments (22)
schema/token.ts (2)

1-1: LGTM: Zod import is correct.

The import statement for Zod is correct and follows common practices.


11-11: LGTM: TokenInput type is well-defined.

The TokenInput type is correctly derived from the TokenSchema using Zod's TypeOf utility. This is a good practice as it ensures type consistency between the schema validation and TypeScript types. Exporting this type will make it easy to use in other parts of the application where token data needs to be handled.

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

1-5: LGTM: Appropriate utility functions imported.

The imported utility functions from "@/utils/emailToken" are relevant to the email update process. The use of a path alias (@) is a good practice for maintaining clean import statements.


6-6: LGTM: Correct Next.js server imports.

The import of NextRequest and NextResponse from "next/server" is correct for handling server-side API routes in Next.js. This ensures proper typing for the request and response objects.


8-25: Overall implementation aligns well with PR objectives

The POST function successfully implements the core functionality described in the PR objectives. It provides a mechanism for users to update their email addresses, which was the main goal of the linked issue #1049. The implementation includes:

  1. Receiving a new email address from the user
  2. Generating a verification token
  3. Storing the token and new email temporarily
  4. Sending a verification email to the new address

This approach ensures that the old email remains associated with the account until the verification process is completed, as specified in the objectives.

To further improve the implementation and fully meet the objectives:

  1. Ensure that the verification link sent in the email correctly handles the token verification process.
  2. Implement the final step of updating the user's email in the database after successful verification.
  3. Consider adding a mechanism to handle token expiration and cleanup of unverified email changes.

These additions will complete the email update process and provide a robust solution for users to change their email addresses.

schema/profile.ts (2)

Line range hint 1-38: Overall schema structure remains consistent

The addition of the email field to saveSettingsSchema is the only change in this file. The overall structure and other schemas remain unchanged, which is good for maintaining consistency. However, ensure that any components or functions using saveSettingsSchema are updated to handle the new email field appropriately.

To ensure all usages of saveSettingsSchema are updated, run the following script:

#!/bin/bash
# Search for usages of saveSettingsSchema
rg --type typescript 'saveSettingsSchema'

Review the results to identify any areas that might need updates due to the new email field.


21-21: 🛠️ Refactor suggestion

Refine email field definition for better alignment with PR objectives

The addition of the email field is a step towards implementing the email update feature. However, there are a few considerations:

  1. The current definition allows for an empty string, which might not be ideal for email updates. Consider removing the or(z.literal("")) part to ensure a valid email is always provided.

  2. To better align with the PR objectives, consider separating the current email and the new email to be verified. This could be achieved by using two fields:

    • currentEmail: The user's verified email address
    • newEmail: The email address pending verification
  3. The schema doesn't enforce the temporary storage of the new email until verification. This logic might need to be handled in the application code.

Here's a suggested refinement:

email: z.object({
  current: z.string().email(),
  new: z.string().email().optional(),
}),

This structure clearly separates the current and new email addresses, aligning better with the PR objectives and the email verification process.

To ensure this change doesn't break existing functionality, let's check how the email field is currently used:

Please review the results of this search to ensure the proposed changes are compatible with existing code.

app/(app)/settings/page.tsx (4)

Line range hint 26-35: LGTM: Email field added to user query

The addition of the 'email' field to the user query is appropriate and aligns with the PR objective of implementing an email verification system. This change ensures that the user's email is available for the new email update functionality.


Line range hint 55-64: LGTM: Email field added to user update return

The inclusion of the 'email' field in the returning clause of the user update operation is consistent with the earlier change and ensures that the email data is available for the Content component. This modification supports the new email verification functionality.


Line range hint 71-83: Verify newsletter subscription with new email system

While the newsletter subscription check remains unchanged, it's crucial to ensure that the new email verification system doesn't negatively impact this functionality.

Please verify that:

  1. The newsletter subscription status is correctly maintained when a user updates their email.
  2. The process of subscribing/unsubscribing to the newsletter works correctly with both verified and unverified email addresses.

Consider adding tests to cover these scenarios if they don't already exist.

To assist in verifying this functionality, you can run the following script:

#!/bin/bash
# Description: Check for newsletter-related functionality in other files

# Test: Search for newsletter-related operations
rg --type typescript -i 'newsletter|subscribe' -g '!app/(app)/settings/page.tsx'

Line range hint 67-69: Consider revising the email presence check

While checking for the presence of an email is generally good practice, the current implementation might conflict with the PR objective of allowing users to add an email if they don't have one. Additionally, its placement after the new user creation logic could prevent new users from setting their email.

Consider the following suggestions:

  1. Move this check before the new user creation logic.
  2. Instead of returning a "not found" response, redirect the user to an "add email" page or section if the email is missing.

This approach would better align with the PR objective of implementing an email verification system and allowing users to update their email addresses.

To verify the impact of this change, please run the following script:

drizzle/0007_shocking_rachel_grey.sql (5)

1-53: Overall, the migration script looks good and aligns well with the PR objectives.

This migration script successfully implements the necessary changes for the email verification system, including:

  1. Creating the EmailVerificationToken table
  2. Updating updatedAt column defaults
  3. Adding appropriate constraints and indexes

The script maintains data integrity and should provide good query performance. However, please consider the following recommendations:

  1. Add an index on the userId column in the EmailVerificationToken table
  2. Verify the impact of dropping and recreating indexes, especially on large tables
  3. Check for any unexpected effects on existing data after altering the updatedAt columns
  4. Confirm that the new constraints are correctly applied and don't conflict with existing data
  5. Monitor index usage after deployment to ensure optimal performance

By addressing these points, you'll ensure a smooth deployment and optimal performance of the new email verification system.


26-31: LGTM! Verify the impact on existing data.

Setting the default value of updatedAt columns to CURRENT_TIMESTAMP is a good practice. It ensures that the last-modified timestamp is automatically updated when a row changes.

To ensure this change doesn't affect existing data unexpectedly, run the following script:

#!/bin/bash
# Description: Check for null or unexpected values in updatedAt columns after the migration.

# Test: Check for null or unexpected updatedAt values
psql -c "SELECT table_name, COUNT(*) as null_or_unexpected_count
         FROM (
           SELECT 'BannedUsers' as table_name, updatedAt FROM \"BannedUsers\"
           UNION ALL SELECT 'Comment', updatedAt FROM \"Comment\"
           UNION ALL SELECT 'Flagged', updatedAt FROM \"Flagged\"
           UNION ALL SELECT 'Notification', updatedAt FROM \"Notification\"
           UNION ALL SELECT 'Post', updatedAt FROM \"Post\"
           UNION ALL SELECT 'user', updatedAt FROM \"user\"
         ) as combined
         WHERE updatedAt IS NULL OR updatedAt < '2000-01-01'
         GROUP BY table_name;"

This will help identify any rows where updatedAt might have unexpected values after the migration.


39-53: LGTM! Consider verifying index usage after deployment.

The recreation of indexes, including unique constraints and the additional non-unique index on the user table, is well-structured and should maintain data integrity and query performance.

After deploying these changes, it's important to verify that the indexes are being used effectively. Run the following script periodically in your production environment to monitor index usage:

#!/bin/bash
# Description: Check index usage statistics for the newly created indexes.

# Test: Retrieve index usage statistics
psql -c "SELECT schemaname, relname, indexrelname, idx_scan, idx_tup_read, idx_tup_fetch
         FROM pg_stat_user_indexes
         WHERE indexrelname IN (
           'BannedUsers_userId_key', 'Bookmark_userId_postId_key', 'Community_id_key', 
           'Community_slug_key', 'Event_id_key', 'Event_slug_key', 'Like_userId_commentId_key', 
           'Like_userId_postId_key', 'Post_id_key', 'Post_slug_key', 'PostTag_tagId_postId_key', 
           'Tag_title_key', 'User_username_key', 'User_email_key', 'User_username_id_idx'
         )
         ORDER BY idx_scan DESC;"

This will help you identify any unused or underutilized indexes, allowing for further optimization if needed.


32-37: LGTM! Verify the newly added constraints.

The addition of primary and foreign key constraints enhances data integrity and properly enforces relationships between tables. The composite primary key on the account table is appropriate for handling multiple authentication providers.

To ensure the constraints are correctly applied and don't conflict with existing data, run the following script:

#!/bin/bash
# Description: Verify the newly added constraints on account and EmailVerificationToken tables.

# Test 1: Check if there are any rows in the account table that violate the new primary key constraint
psql -c "SELECT provider, providerAccountId, COUNT(*) 
         FROM account 
         GROUP BY provider, providerAccountId 
         HAVING COUNT(*) > 1;"

# Test 2: Check if there are any rows in the EmailVerificationToken table that violate the new foreign key constraint
psql -c "SELECT COUNT(*) 
         FROM \"EmailVerificationToken\" evt 
         LEFT JOIN \"user\" u ON evt.\"userId\" = u.id 
         WHERE u.id IS NULL;"

# Test 3: Verify the constraints are properly created
psql -c "\d+ account"
psql -c "\d+ \"EmailVerificationToken\""

This script will help identify any potential issues with the new constraints and confirm they are correctly applied.


11-25: Verify the impact of dropping and recreating indexes.

The script drops several indexes before recreating them later. While this approach works, it may lead to temporary performance degradation during the migration process, especially on large tables.

Consider the following:

  1. Verify that this migration won't significantly impact production performance.
  2. For future migrations, consider using CREATE OR REPLACE INDEX instead of dropping and recreating indexes, where possible.

To assess the potential impact, run the following script to check the size of affected tables:

This will help estimate the time and resources required for index recreation.

server/db/schema.ts (1)

116-125: Summary: New emailVerificationToken table aligns well with PR objectives

The addition of the emailVerificationToken table is a solid step towards implementing the email verification system as outlined in the PR objectives. This table provides a structure to securely manage temporary tokens for email verification, allowing users to update their email addresses safely.

Key points:

  1. The table design supports the temporary storage of new email addresses until verified.
  2. It maintains a link to the user account via the userId field.
  3. The inclusion of createdAt and expiresAt fields allows for proper token management and expiration.

These changes lay a good foundation for the email verification feature. Ensure that the corresponding application logic is implemented to utilize this table effectively in the email update and verification process.

app/verify_email/page.tsx (2)

29-29: Ensure Content component handles user data securely

When passing userId to the Content component, ensure that the component doesn't expose sensitive user information or render data that should remain confidential.

Review the Content component to confirm it adheres to security best practices.


14-16: ⚠️ Potential issue

Ensure type safety by guarding session.user without non-null assertion

You've already checked that session and session.user exist, so the non-null assertion session.user!.id isn't necessary. Removing it improves type safety and avoids potential runtime errors.

Apply this diff to remove the non-null assertion:

   if (!session || !session.user) {
     redirect("/not-found");
   }
-  const existingUser = await db.query.user.findFirst({
+  const existingUser = await db.query.user.findFirst({
     // ...
-    where: (users, { eq }) => eq(users.id, session.user!.id),
+    where: (users, { eq }) => eq(users.id, session.user.id),
   });

Likely invalid or redundant comment.

app/verify_email/_client.tsx (1)

62-104: LGTM!

The rendering logic is well-structured, and the component appropriately handles different verification statuses.

utils/emailToken.ts (2)

31-31: Ensure the NEXT_PUBLIC_BACKEND_URL is correctly configured

The NEXT_PUBLIC_BACKEND_URL environment variable is used to construct the verification link. Please verify that this variable is correctly set to the front-end application URL that will handle the email verification process. Misconfiguration could lead to incorrect or insecure links being sent to users.


31-31: Verify user identification during email verification

The verification link only includes the token as a query parameter, but the getTokenFromDb function requires both the token and userId. Ensure that the userId can be securely obtained when processing the verification request, such as from the authenticated session or by including it in the verification link in a secure manner (e.g., encoding or hashing).

Consider whether it is appropriate to include the user ID in the verification link or adjust the getTokenFromDb function to retrieve the token using only the token value. Including sensitive information like userId in URLs should be handled carefully to prevent security risks.

Comment on lines 8 to 25
export async function POST(req: NextRequest, res: NextResponse) {
const { userId, newEmail } = await req.json();

if (!newEmail || !userId)
return NextResponse.json({ message: "Invalid request" }, { status: 400 });

const token = generateEmailToken();
const expiresAt = new Date(Date.now() + 1000 * 60 * 60); //1 hour

await storeTokenInDb(userId, token, expiresAt, newEmail);

await sendVerificationEmail(newEmail, token);

return NextResponse.json(
{ message: "Verification email sent" },
{ status: 200 },
);
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 6, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Enhance error handling and input validation

While the overall structure of the POST function is good, there are several areas where it can be improved:

  1. Error handling: Add try-catch blocks around database operations and email sending to handle potential errors gracefully.
  2. Input validation:
    • Check if the new email is different from the current one to avoid unnecessary processing.
    • Validate the email format before proceeding.
    • Verify that the user exists in the database before updating their email.
  3. Response consistency: Ensure all responses follow the same structure for consistency.

Here's a suggested improvement:

import { isValidEmail, getUserById } from "@/utils/validators"; // Assume these exist

export async function POST(req: NextRequest, res: NextResponse) {
  try {
    const { userId, newEmail } = await req.json();

    if (!newEmail || !userId) {
      return NextResponse.json({ error: "Invalid request" }, { status: 400 });
    }

    if (!isValidEmail(newEmail)) {
      return NextResponse.json({ error: "Invalid email format" }, { status: 400 });
    }

    const user = await getUserById(userId);
    if (!user) {
      return NextResponse.json({ error: "User not found" }, { status: 404 });
    }

    if (user.email === newEmail) {
      return NextResponse.json({ message: "New email is the same as the current email" }, { status: 200 });
    }

    const token = generateEmailToken();
    const expiresAt = new Date(Date.now() + 1000 * 60 * 60); // 1 hour

    await storeTokenInDb(userId, token, expiresAt, newEmail);
    await sendVerificationEmail(newEmail, token);

    return NextResponse.json({ message: "Verification email sent" }, { status: 200 });
  } catch (error) {
    console.error("Error updating email:", error);
    return NextResponse.json({ error: "Internal server error" }, { status: 500 });
  }
}

This implementation adds error handling, more comprehensive input validation, and consistent response structures.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed! A try catch is useful here.

Copy link
Contributor

Choose a reason for hiding this comment

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

@NiallJoeMaher, glad to hear that you're implementing the try-catch block for improved error handling!

(^_^/)


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Comment on lines 14 to 15
const token = generateEmailToken();
const expiresAt = new Date(Date.now() + 1000 * 60 * 60); //1 hour
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

Consider making token expiration configurable

The token expiration is currently hardcoded to 1 hour. Consider making this configurable to allow for flexibility in different environments or use cases.

Here's a suggested improvement:

import { TOKEN_EXPIRATION_TIME } from "@/config/constants"; // Assume this exists

// ...

const expiresAt = new Date(Date.now() + TOKEN_EXPIRATION_TIME);

This change allows you to easily adjust the token expiration time across your application by modifying a single constant.

Comment on lines 116 to 125
export const emailVerificationToken = pgTable("EmailVerificationToken", {
id: serial("id").primaryKey(),
token: text("token").unique().notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
expiresAt: timestamp("expiresAt").notNull(),
email: text("email").notNull(),
userId: text("userId")
.notNull()
.references(() => user.id),
});
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

Verify user table setup and add relation to emailVerificationToken

The new emailVerificationToken table correctly references the user table. To complete this relationship:

  1. Verify that the user table is properly set up to handle this new relationship.
  2. Add a relation in the userRelations definition to include the emailVerificationToken.

Add the following relation to the userRelations definition:

 export const userRelations = relations(user, ({ one, many }) => ({
   // ... existing relations ...
+  emailVerificationTokens: many(emailVerificationToken),
 }));

This will establish a one-to-many relationship between users and email verification tokens, allowing for easier querying and management of these tokens per user.

Committable suggestion was skipped due to low confidence.


🛠️ Refactor suggestion

Consider enhancing the emailVerificationToken table definition

The new table structure aligns well with the PR objectives. However, consider the following improvements:

  1. Add a length constraint to the token field for consistency and potential performance benefits.
  2. Create an index on the userId and/or email fields to improve query performance, especially if you'll be querying by these fields frequently.
  3. Add a constraint or default value to ensure the expiresAt field is always set in the future.

Here's a suggested modification:

 export const emailVerificationToken = pgTable("EmailVerificationToken", {
   id: serial("id").primaryKey(),
-  token: text("token").unique().notNull(),
+  token: varchar("token", { length: 64 }).unique().notNull(),
   createdAt: timestamp("createdAt").defaultNow().notNull(),
-  expiresAt: timestamp("expiresAt").notNull(),
+  expiresAt: timestamp("expiresAt").notNull().$check(sql`"expiresAt" > CURRENT_TIMESTAMP`),
   email: text("email").notNull(),
   userId: text("userId")
     .notNull()
     .references(() => user.id),
+ }, (table) => ({
+   userIdIdx: index("EmailVerificationToken_userId_idx").on(table.userId),
+   emailIdx: index("EmailVerificationToken_email_idx").on(table.email),
 });

These changes will:

  1. Limit the token length to 64 characters (adjust as needed).
  2. Add a check constraint to ensure expiresAt is always in the future.
  3. Create indexes on userId and email for better query performance.
📝 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
export const emailVerificationToken = pgTable("EmailVerificationToken", {
id: serial("id").primaryKey(),
token: text("token").unique().notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
expiresAt: timestamp("expiresAt").notNull(),
email: text("email").notNull(),
userId: text("userId")
.notNull()
.references(() => user.id),
});
export const emailVerificationToken = pgTable("EmailVerificationToken", {
id: serial("id").primaryKey(),
token: varchar("token", { length: 64 }).unique().notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
expiresAt: timestamp("expiresAt").notNull().$check(sql`"expiresAt" > CURRENT_TIMESTAMP`),
email: text("email").notNull(),
userId: text("userId")
.notNull()
.references(() => user.id),
}, (table) => ({
userIdIdx: index("EmailVerificationToken_userId_idx").on(table.userId),
emailIdx: index("EmailVerificationToken_email_idx").on(table.email),
});


export default async function Page() {
const session = await getServerAuthSession();
console.log("session", session);
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 6, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove console.log to prevent logging sensitive information

The console.log("session", session); statement logs the entire session object, which may contain sensitive user information. Logging such data can lead to security vulnerabilities if logs are exposed.

Apply this diff to remove the debug statement:

   const session = await getServerAuthSession();
-  console.log("session", session);
   if (!session || !session.user) {
📝 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("session", session);
const session = await getServerAuthSession();
if (!session || !session.user) {

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Contributor

Choose a reason for hiding this comment

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

@NiallJoeMaher, thanks for addressing this!

((^_^)/


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Comment on lines 18 to 24
const existingUser = await db.query.user.findFirst({
columns: {
id: true,
},
where: (users, { eq }) => eq(users.id, session.user!.id),
});

Copy link
Contributor

@coderabbitai coderabbitai bot Oct 6, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify user retrieval by using findUnique

Since you're querying by a unique identifier (id), you can use findUnique instead of findFirst for clarity and potential performance benefits.

Apply this diff to use findUnique:

-  const existingUser = await db.query.user.findFirst({
-    columns: {
-      id: true,
-    },
-    where: (users, { eq }) => eq(users.id, session.user!.id),
-  });
+  const existingUser = await db.query.user.findUnique({
+    where: { id: session.user.id },
+    select: { id: true },
+  });
📝 Committable suggestion

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

Suggested change
const existingUser = await db.query.user.findFirst({
columns: {
id: true,
},
where: (users, { eq }) => eq(users.id, session.user!.id),
});
const existingUser = await db.query.user.findUnique({
where: { id: session.user.id },
select: { id: true },
});

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Contributor

Choose a reason for hiding this comment

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

@NiallJoeMaher, glad you agree!

(_/)
(^.^)
/🎉\

import { zodResolver } from "@hookform/resolvers/zod";
import type { SubmitHandler } from "react-hook-form";
import { useForm } from "react-hook-form";
import { set, useForm } from "react-hook-form";
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

Remove unnecessary import 'set' from 'react-hook-form'.

The set function is not used anywhere in the code. Removing unused imports helps keep the code clean and maintainable.

Apply this diff to fix the issue:

-import { set, useForm } from "react-hook-form";
+import { useForm } from "react-hook-form";
📝 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 { set, useForm } from "react-hook-form";
import { useForm } from "react-hook-form";

{!sendForVerification ? (
<Button
className="w-[200px]"
disabled={newEmail || loading ? false : true}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Simplify the 'disabled' prop expression in the 'Send verification link' button.

The use of a ternary operator with boolean literals is unnecessary. You can simplify the expression by using the logical NOT operator for clarity.

Apply this diff to fix the issue:

-                            disabled={newEmail || loading ? false : true}
+                            disabled={!(newEmail || loading)}
📝 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
disabled={newEmail || loading ? false : true}
disabled={!(newEmail || loading)}
🧰 Tools
🪛 Biome

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

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

(lint/complexity/noUselessTernary)

</h2>
<Button
className="w-[200px]"
disabled={newEmail || loading ? false : true}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Simplify the 'disabled' prop expression in the 'Resend verification link' button.

Similarly, simplify the ternary expression to make the code more readable.

Apply this diff to fix the issue:

-                              disabled={newEmail || loading ? false : true}
+                              disabled={!(newEmail || loading)}
📝 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
disabled={newEmail || loading ? false : true}
disabled={!(newEmail || loading)}
🧰 Tools
🪛 Biome

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

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

(lint/complexity/noUselessTernary)

Comment on lines 141 to 177
const handleNewEmailUpdate = async () => {
setLoading(true);
try {
await fetch("/api/update_email", {
method: "POST",
body: JSON.stringify({ userId: profile.id, newEmail }),
headers: {
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((res) => {
console.log(res);
if (res.status === 400) {
setLoading(false);
toast.error(
"Something went wrong while sending the verification link",
);
} else {
toast.success(
"Verification link has been sent to your new email address.",
);
setLoading(false);
setSendForVerification(true);
}
})
.catch((error) => {
setLoading(false);
toast.error(
"Something went wrong while sending the verification link.",
);
});
} catch (error) {
setLoading(false);
toast.error("Something went wrong while sending the verification link.");
}
};
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 incorrect response handling in 'handleNewEmailUpdate' function.

The response handling in your fetch call is incorrect. After calling res.json(), you lose access to the res.status property, as res now contains the parsed JSON body. Additionally, mixing async/await with .then() and .catch() can lead to confusion and is not considered best practice.

Here's how you can refactor the function:

  • Use async/await consistently for better readability.
  • Access the response status code before parsing the JSON.
  • Improve error handling to accurately reflect the response.

Apply this diff to fix the issue:

 const handleNewEmailUpdate = async () => {
   setLoading(true);
   try {
-    await fetch("/api/update_email", {
+    const res = await fetch("/api/update_email", {
       method: "POST",
       body: JSON.stringify({ userId: profile.id, newEmail }),
       headers: {
         "Content-Type": "application/json",
       },
-    })
-      .then((res) => res.json())
-      .then((res) => {
-        console.log(res);
-        if (res.status === 400) {
-          setLoading(false);
-          toast.error(
-            "Something went wrong while sending the verification link",
-          );
-        } else {
-          toast.success(
-            "Verification link has been sent to your new email address.",
-          );
-          setLoading(false);
-          setSendForVerification(true);
-        }
-      })
-      .catch((error) => {
-        setLoading(false);
-        toast.error(
-          "Something went wrong while sending the verification link.",
-        );
-      });
+    });
+    if (res.status === 400) {
+      toast.error(
+        "Something went wrong while sending the verification link"
+      );
+    } else if (res.ok) {
+      toast.success(
+        "Verification link has been sent to your new email address."
+      );
+      setSendForVerification(true);
+    } else {
+      toast.error(
+        "Unexpected error occurred while sending the verification link."
+      );
+    }
+  } catch (error) {
+    toast.error(
+      "Something went wrong while sending the verification link."
+    );
   } finally {
     setLoading(false);
   }
 };
📝 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 handleNewEmailUpdate = async () => {
setLoading(true);
try {
await fetch("/api/update_email", {
method: "POST",
body: JSON.stringify({ userId: profile.id, newEmail }),
headers: {
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((res) => {
console.log(res);
if (res.status === 400) {
setLoading(false);
toast.error(
"Something went wrong while sending the verification link",
);
} else {
toast.success(
"Verification link has been sent to your new email address.",
);
setLoading(false);
setSendForVerification(true);
}
})
.catch((error) => {
setLoading(false);
toast.error(
"Something went wrong while sending the verification link.",
);
});
} catch (error) {
setLoading(false);
toast.error("Something went wrong while sending the verification link.");
}
};
const handleNewEmailUpdate = async () => {
setLoading(true);
try {
const res = await fetch("/api/update_email", {
method: "POST",
body: JSON.stringify({ userId: profile.id, newEmail }),
headers: {
"Content-Type": "application/json",
},
});
if (res.status === 400) {
toast.error(
"Something went wrong while sending the verification link"
);
} else if (res.ok) {
toast.success(
"Verification link has been sent to your new email address."
);
setSendForVerification(true);
} else {
toast.error(
"Unexpected error occurred while sending the verification link."
);
}
} catch (error) {
toast.error(
"Something went wrong while sending the verification link."
);
} finally {
setLoading(false);
}
};

Copy link
Contributor

@NiallJoeMaher NiallJoeMaher left a comment

Choose a reason for hiding this comment

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

Wowza! Lots of code on this one so great work so far. I added a good few comments but nothing mental. You have the guts of everything here. Well done!

import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest, res: NextResponse) {
const { userId, newEmail } = await req.json();
Copy link
Contributor

Choose a reason for hiding this comment

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

This could let someone change someone elses email. You would need to grab the user/userId from the current users session. Also for mutating requests like this we use trpc. You could add this here: https://github.com/codu-code/codu/blob/develop/server/api/router/profile.ts

And you'll see the pattern we have for cleaning data with schemas and getting the user from ctx.session.user

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes followed this way now.

Comment on lines 8 to 25
export async function POST(req: NextRequest, res: NextResponse) {
const { userId, newEmail } = await req.json();

if (!newEmail || !userId)
return NextResponse.json({ message: "Invalid request" }, { status: 400 });

const token = generateEmailToken();
const expiresAt = new Date(Date.now() + 1000 * 60 * 60); //1 hour

await storeTokenInDb(userId, token, expiresAt, newEmail);

await sendVerificationEmail(newEmail, token);

return NextResponse.json(
{ message: "Verification email sent" },
{ status: 200 },
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed! A try catch is useful here.

@@ -0,0 +1,33 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

On the verify email route we could probably create a GET call which takes the token ID and redirects the user to a success or an error page depending on what they do (that's the normal pattern)

So have a route that might be /verify-email?token=eyJhbGciOiJIUzI1NiIsInR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done ✅

Comment on lines 8 to 33
export async function POST(req: NextRequest, res: NextResponse) {
const token = req.nextUrl.searchParams.get("token");
const { userId } = await req.json();

if (!token)
return NextResponse.json({ message: "Invalid request" }, { status: 400 });

const tokenFromDb = await getTokenFromDb(token, userId);

console.log("tokenFromDb", tokenFromDb);
if (!tokenFromDb.length)
return NextResponse.json({ message: "Invalid token" }, { status: 400 });

const { expiresAt, email } = tokenFromDb[0];
if (expiresAt < new Date())
return NextResponse.json({ message: "Token expired" }, { status: 400 });

await updateEmail(userId, email);

await deleteTokenFromDb(token);

return NextResponse.json(
{ message: "Email successfully verified" },
{ status: 200 },
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Good suggestions here!

import { useRouter, useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react";

function Content({ userId }: { userId: string }) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Good comment again. Nitpick, but the url would look better if it was verify-email.

Comment on lines 36 to 42
const res = await fetch(`/api/verify_email?token=${token}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userId }),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

The user should be able to click a link to a GET route which will auto-magically validate the token (instead of having to post a request). This ensures that the user clicked their email always.


export default async function Page() {
const session = await getServerAuthSession();
console.log("session", session);
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Comment on lines 18 to 24
const existingUser = await db.query.user.findFirst({
columns: {
id: true,
},
where: (users, { eq }) => eq(users.id, session.user!.id),
});

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

"userId" text NOT NULL,
CONSTRAINT "EmailVerificationToken_token_unique" UNIQUE("token")
);
--> statement-breakpoint
Copy link
Contributor

Choose a reason for hiding this comment

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

This migration seems to drop a lot of indexes that we might need. Also the file name is different than the ones we usually have which are descriptive.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah saw that. But it's also creating those later.

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

🧹 Outside diff range and nitpick comments (9)
schema/token.ts (1)

1-15: Great job: File aligns well with PR objectives and follows best practices.

This file successfully introduces the necessary schemas and types for the email verification system:

  1. The emailTokenReqSchema validates the new email input.
  2. The TokenSchema defines the structure for storing verification tokens.
  3. The TokenInput type alias ensures type safety in TypeScript.

These components directly support the PR objective of implementing an email verification system, allowing users to update their email addresses securely. The use of Zod for schema validation is a great choice, providing both runtime checks and TypeScript type inference.

One minor suggestion to consider:

Consider adding a brief comment at the top of the file explaining its purpose in the context of the email verification system. This would enhance code readability and maintainability.

drizzle/0007_added-emailtokenverification-schema.sql (3)

1-9: LGTM! Consider adding an index on the userId column.

The structure of the EmailVerificationToken table looks good and includes all necessary fields for managing email verification tokens. The unique constraint on the token field is a good security measure.

Consider adding an index on the userId column to improve query performance when looking up tokens for a specific user:

CREATE INDEX IF NOT EXISTS "EmailVerificationToken_userId_idx" ON "EmailVerificationToken" ("userId");

33-37: Consider CASCADE on delete for the foreign key constraint.

The addition of the foreign key constraint is good for maintaining referential integrity. The use of a DO block with exception handling is a nice touch to make the migration more robust.

However, consider changing the ON DELETE behavior to CASCADE. This would automatically delete the verification tokens when a user is deleted, which is likely the desired behavior:

ALTER TABLE "EmailVerificationToken" ADD CONSTRAINT "EmailVerificationToken_userId_user_id_fk" 
FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE CASCADE ON UPDATE NO ACTION;

This change would help maintain data consistency and avoid orphaned records in the EmailVerificationToken table.


39-53: LGTM! Consider using consistent IF NOT EXISTS clauses.

The recreation of the previously dropped indexes is good. This addresses the earlier concern about potential performance impacts from removing these indexes.

For consistency, consider using the IF NOT EXISTS clause for all index creations, not just some of them. This makes the migration more idempotent and robust. For example:

CREATE UNIQUE INDEX IF NOT EXISTS "BannedUsers_userId_key" ON "BannedUsers" USING btree ("userId");
CREATE UNIQUE INDEX IF NOT EXISTS "Bookmark_userId_postId_key" ON "Bookmark" USING btree ("postId","userId");
-- Apply this pattern to all index creations

This change would make the migration more consistent and easier to maintain.

utils/emailToken.ts (1)

34-100: LGTM: Well-structured and styled email template.

The email template is well-designed, responsive, and follows good practices for email HTML. It includes all necessary information and a clear call-to-action button.

Minor suggestion: Consider adding alt text to the verification button for improved accessibility:

- <a href="${verificationLink}" class="btn">Verify Email</a>
+ <a href="${verificationLink}" class="btn" role="button" aria-label="Verify your email address">Verify Email</a>

This change will improve the experience for users relying on screen readers.

app/api/verify-email/route.ts (1)

38-43: Log errors in the catch block for debugging

Currently, errors are not logged, which can make debugging difficult. Consider logging the error details:

 } catch (error) {
+  console.error("Error during email verification:", error);
   return NextResponse.json(
     { error: "Internal server error" },
     { status: 500 },
   );
 }

Ensure that sensitive information is not exposed in logs.

app/verify-email/_client.tsx (1)

63-93: Enhance user experience on error state

When the verification fails, consider providing a retry option or guidance on the next steps the user can take. This improves the user experience by giving them a clear path forward.

For example, you could add a "Retry" button or a link to contact support.

server/api/router/profile.ts (1)

136-164: Add unit tests for the updateEmail procedure

To ensure the new email update functionality works correctly and to prevent future regressions, consider adding unit tests for the updateEmail procedure.

app/(app)/settings/_client.tsx (1)

407-407: Keep loader icon sizes consistent for uniform UI.

The Loader icons used in the "Send verification link" and "Resend verification link" buttons have different sizes. In line 407, the icon has h-4 w-4, while in line 423, it uses h-8 w-8. For a consistent user experience, consider standardizing the icon sizes.

Apply this diff to unify the loader icon sizes:

// In the "Resend verification link" button
 {loading && (
-  <Loader className="text-primary h-8 w-8 animate-spin" />
+  <Loader className="text-primary h-4 w-4 animate-spin" />
 )}

Also applies to: 423-423

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 27f1a76 and 29a344a.

⛔ Files ignored due to path filters (2)
  • drizzle/meta/0007_snapshot.json is excluded by !**/*.json
  • drizzle/meta/_journal.json is excluded by !**/*.json
📒 Files selected for processing (9)
  • app/(app)/settings/_client.tsx (7 hunks)
  • app/api/verify-email/route.ts (1 hunks)
  • app/verify-email/_client.tsx (1 hunks)
  • app/verify-email/page.tsx (1 hunks)
  • config/contants.ts (1 hunks)
  • drizzle/0007_added-emailtokenverification-schema.sql (1 hunks)
  • schema/token.ts (1 hunks)
  • server/api/router/profile.ts (2 hunks)
  • utils/emailToken.ts (1 hunks)
🧰 Additional context used
🔇 Additional comments (15)
config/contants.ts (1)

1-3: ```shell

#!/bin/bash

Description: Search for time-related constants or configurations related to email verification in TypeScript and JavaScript files

Search for time-related constants or configurations

rg --type ts --type js -i '(expir|timeout|duration|ttl).(\d+\s(*\s60\s){2}|\d+\s*hours?|3600000)'


</blockquote></details>
<details>
<summary>schema/token.ts (4)</summary><blockquote>

`1-1`: **LGTM: Zod import looks good.**

The import statement for Zod is correct and follows the common convention of importing it as 'z'.

---

`3-5`: **LGTM: Email token request schema is well-defined.**

The `emailTokenReqSchema` correctly validates the new email input, ensuring it's a string in email format. This aligns well with the PR objective of implementing an email verification system.

---

`7-13`: **LGTM: Token schema is comprehensive and well-structured.**

The `TokenSchema` includes all necessary fields for a token object:
- `id`: Correctly uses UUID format.
- `userId`: Stored as a string.
- `token`: Stored as a string.
- `createdAt`: Uses a date with a default value of the current date.
- `expiresAt`: Uses a date for token expiration.

This structure supports the email verification process outlined in the PR objectives.

---

`15-15`: **LGTM: TokenInput type alias is correctly defined.**

The `TokenInput` type alias is properly inferred from the `TokenSchema` using Zod's `TypeOf` utility. This ensures type safety and consistency between the schema validation and TypeScript types.

</blockquote></details>
<details>
<summary>app/verify-email/page.tsx (4)</summary><blockquote>

`1-5`: **LGTM: Imports are well-organized and relevant.**

The imports are logically ordered and include all necessary modules for the component's functionality.

---

`7-9`: **LGTM: Metadata export is correct and descriptive.**

The metadata export follows Next.js best practices and provides an appropriate title for the page.

---

`1-30`: **Overall implementation looks good, but verify complete email verification flow.**

The `page.tsx` file for the "Verify Email" page is well-structured and follows Next.js and TypeScript best practices. It correctly handles authentication and user verification before rendering the main content.

However, this file doesn't directly implement email update or verification functionality. To ensure the complete email verification system is working as intended:

1. Verify that the `Content` component (imported from `./_client`) implements the necessary UI and logic for email updates and verification.
2. Check that the API endpoints for updating and verifying email addresses are correctly implemented and integrated.
3. Ensure that the email verification token generation and validation are securely implemented in the backend.



To help verify the complete email verification flow, you can run the following script to check for related implementations:

```shell
#!/bin/bash
# Description: Check for email verification related implementations

# Test 1: Search for email update functionality in the Content component
echo "Checking Content component for email update functionality:"
rg --type typescript -A 10 'updateEmail|verifyEmail' app/verify-email/_client.tsx

# Test 2: Search for API endpoints related to email verification
echo "Checking for API endpoints related to email verification:"
rg --type typescript -A 5 'router\.(post|put|patch)\s*\([\'"]\/api\/.*email' app/api

# Test 3: Search for email verification token handling in the backend
echo "Checking for email verification token handling:"
rg --type typescript -A 10 'generateEmailVerificationToken|validateEmailVerificationToken' server/

11-30: LGTM: Page function handles authentication and rendering correctly.

The Page function effectively manages authentication, user verification, and rendering. It follows good practices such as early returns for error cases and uses type-safe database queries.

Could you verify if the Content component requires any props? Currently, it's rendered without passing any data:

return <Content />;

If the Content component needs access to user data or session information, consider passing relevant props. Here's a script to check the Content component's implementation:

drizzle/0007_added-emailtokenverification-schema.sql (2)

26-32: LGTM! Good improvements to data integrity and tracking.

The changes to set CURRENT_TIMESTAMP as the default for updatedAt columns in various tables is a good practice. It ensures that the last modification time is automatically tracked without requiring explicit updates in application code.

Adding a primary key constraint to the account table using the provider and providerAccountId columns is also a good improvement for data integrity.


11-25: Please clarify the reason for dropping these indexes.

The migration is dropping several indexes from various tables. While this might be part of a larger schema restructuring, it's important to understand the reasoning behind these changes and their potential impact on query performance.

Could you provide more context on why these indexes are being removed? Are they being replaced with different indexes, or has the query pattern changed to make them unnecessary?

To assess the impact, let's check if these indexes are recreated later in the migration:

✅ Verification successful

Index Dropping and Recreation Verified

All dropped indexes are being consistently recreated within this migration, ensuring no disruption to query performance.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if dropped indexes are recreated
echo "Indexes recreated in this migration:"
grep -n "CREATE.*INDEX" drizzle/0007_added-emailtokenverification-schema.sql

echo "\nIndexes not recreated:"
for index in "BannedUsers_userId_key" "Bookmark_userId_postId_key" "Community_id_key" "Community_slug_key" "Event_id_key" "Event_slug_key" "Like_userId_commentId_key" "Like_userId_postId_key" "Post_id_key" "Post_slug_key" "PostTag_tagId_postId_key" "Tag_title_key" "User_username_key" "User_email_key" "User_username_id_idx"
do
    if ! grep -q "CREATE.*INDEX.*$index" drizzle/0007_added-emailtokenverification-schema.sql; then
        echo $index
    fi
done

Length of output: 8365

utils/emailToken.ts (3)

1-5: LGTM: Import statements are appropriate and concise.

The import statements are well-organized and include all necessary modules for the file's functionality.


7-9: LGTM: Secure token generation implemented.

The generateEmailToken function uses a cryptographically secure method to generate a random token, which is appropriate for email verification purposes.


1-126: Overall assessment: Good implementation with room for improvements.

This file provides a comprehensive set of utilities for managing email verification tokens. The functions are well-structured and cover the necessary operations (generate, store, send, retrieve, update, and delete). The email template is also well-designed and responsive.

However, there are several areas where the code can be improved:

  1. Error handling should be added to all database operations.
  2. The getTokenFromDb function should check for token expiration.
  3. The updateEmail and deleteTokenFromDb functions should provide confirmation of successful operations.
  4. The company name in the email template should be updated for consistency.

Implementing the suggested changes will significantly enhance the robustness, security, and maintainability of this module.

app/verify-email/_client.tsx (1)

8-97: Well-structured implementation of the email verification component

The Content component effectively implements the email verification process, handling different states and providing appropriate feedback to the user. The code is clean, readable, and aligns with the PR objectives.

Comment on lines 1 to 3
const TOKEN_EXPIRATION_TIME = 1000 * 60 * 60; // 1 hour

export { TOKEN_EXPIRATION_TIME };
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Filename Typo Detected

The file config/contants.ts appears to contain a typo. Consider renaming it to constants.ts to maintain consistency and avoid confusion.

🔗 Analysis chain

Potential typo in file name

The file name appears to be "contants.ts" which might be a typo. Did you mean "constants.ts"?

Let's verify the file name:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if there's a typo in the file name

# List files with names similar to "constants.ts" or "contants.ts"
fd -t f -g '*on?tants.ts' -d 1 config

Length of output: 268


Script:

#!/bin/bash
# Description: Correctly check for "constants.ts" or "contants.ts" in the config directory

# List files named "constants.ts" or "contants.ts" in the config directory
fd -t f -e ts -g 'constants.ts' config || fd -t f -e ts -g 'contants.ts' config

Length of output: 179

Comment on lines 11 to 28
export const storeTokenInDb = async (
userId: string,
token: string,
expiresAt: Date,
email: string,
) => {
const newToken = await db
.insert(emailVerificationToken)
.values({
userId,
token,
expiresAt,
email,
})
.returning();

return newToken[0];
};
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

Consider adding error handling to storeTokenInDb.

While the function correctly stores the token in the database, it lacks error handling. Consider wrapping the database operation in a try-catch block to handle potential errors gracefully.

Here's a suggested improvement:

export const storeTokenInDb = async (
  userId: string,
  token: string,
  expiresAt: Date,
  email: string,
) => {
  try {
    const newToken = await db
      .insert(emailVerificationToken)
      .values({
        userId,
        token,
        expiresAt,
        email,
      })
      .returning();

    return newToken[0];
  } catch (error) {
    console.error('Error storing token in database:', error);
    throw new Error('Failed to store email verification token');
  }
};

This change will provide better error handling and logging, making it easier to debug issues in production.

Comment on lines 105 to 116
export const getTokenFromDb = async (token: string, userId: string) => {
const tokenFromDb = await db
.select()
.from(emailVerificationToken)
.where(
and(
eq(emailVerificationToken.token, token),
eq(emailVerificationToken.userId, userId),
),
);
return tokenFromDb;
};
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

Enhance getTokenFromDb for better security and consistency.

The current implementation has two areas for improvement:

  1. It doesn't check if the token has expired, which could be a security risk.
  2. It returns an array of tokens instead of a single token, which might be unexpected for the caller.

Consider applying these changes:

import { gt } from "drizzle-orm";

export const getTokenFromDb = async (token: string, userId: string) => {
  const tokenFromDb = await db
    .select()
    .from(emailVerificationToken)
    .where(
      and(
        eq(emailVerificationToken.token, token),
        eq(emailVerificationToken.userId, userId),
        gt(emailVerificationToken.expiresAt, new Date()) // Check if token is not expired
      ),
    )
    .limit(1); // Ensure we only get one result

  return tokenFromDb[0] || null; // Return null if no token found
};

These changes will ensure that only valid, non-expired tokens are returned, and the function consistently returns a single token object or null.

Comment on lines 118 to 120
export const updateEmail = async (userId: string, newEmail: string) => {
await db.update(user).set({ email: newEmail }).where(eq(user.id, userId));
};
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

Improve updateEmail with error handling and success confirmation.

The current implementation lacks error handling and doesn't provide confirmation of a successful update. This could lead to silent failures and make debugging difficult.

Consider applying these changes:

export const updateEmail = async (userId: string, newEmail: string): Promise<boolean> => {
  try {
    const result = await db
      .update(user)
      .set({ email: newEmail })
      .where(eq(user.id, userId))
      .returning({ updatedId: user.id });
    
    return result.length > 0;
  } catch (error) {
    console.error('Error updating email:', error);
    throw new Error('Failed to update email');
  }
};

These changes will:

  1. Add error handling to catch and log any database errors.
  2. Return a boolean indicating whether the update was successful.
  3. Use the returning clause to confirm that a row was actually updated.

This will make the function more robust and easier to use in the calling code.

Comment on lines 122 to 126
export const deleteTokenFromDb = async (token: string) => {
await db
.delete(emailVerificationToken)
.where(eq(emailVerificationToken.token, token));
};
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

Enhance deleteTokenFromDb for improved security and error handling.

The current implementation has two main areas for improvement:

  1. It doesn't include the user ID in the deletion criteria, which could be a security risk.
  2. It lacks error handling and doesn't confirm if a deletion actually occurred.

Consider applying these changes:

export const deleteTokenFromDb = async (token: string, userId: string): Promise<boolean> => {
  try {
    const result = await db
      .delete(emailVerificationToken)
      .where(
        and(
          eq(emailVerificationToken.token, token),
          eq(emailVerificationToken.userId, userId)
        )
      )
      .returning({ deletedId: emailVerificationToken.id });
    
    return result.length > 0;
  } catch (error) {
    console.error('Error deleting token from database:', error);
    throw new Error('Failed to delete email verification token');
  }
};

These changes will:

  1. Include the user ID in the deletion criteria for improved security.
  2. Add error handling to catch and log any database errors.
  3. Return a boolean indicating whether a token was actually deleted.
  4. Use the returning clause to confirm that a row was deleted.

This will make the function more secure, robust, and informative for the calling code.

Comment on lines 158 to 163
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Internal server 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

Log the error before throwing INTERNAL_SERVER_ERROR

Currently, the caught error is not logged, which can make debugging difficult. Consider logging the error to aid in troubleshooting, ensuring that no sensitive information is exposed.

sendVerificationEmail,
storeTokenInDb,
} from "@/utils/emailToken";
import { TOKEN_EXPIRATION_TIME } from "@/config/contants";
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 the typo in the import path for TOKEN_EXPIRATION_TIME

The import path has a typo; 'contants' should be 'constants'.

Apply this diff to correct the import path:

-import { TOKEN_EXPIRATION_TIME } from "@/config/contants";
+import { TOKEN_EXPIRATION_TIME } from "@/config/constants";
📝 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 { TOKEN_EXPIRATION_TIME } from "@/config/contants";
import { TOKEN_EXPIRATION_TIME } from "@/config/constants";

Comment on lines +142 to +147
if (!newEmail) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid request",
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add email format validation for newEmail

Currently, the code checks if newEmail is provided but does not validate its format. It's important to ensure that newEmail is a valid email address to prevent invalid data from being processed.

Apply this diff to add email format validation:

            if (!newEmail) {
              throw new TRPCError({
                code: "BAD_REQUEST",
                message: "Invalid request",
              });
            }
+           const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+           if (!emailRegex.test(newEmail)) {
+             throw new TRPCError({
+               code: "BAD_REQUEST",
+               message: "Invalid email format",
+             });
+           }
📝 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
if (!newEmail) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid request",
});
}
if (!newEmail) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid request",
});
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(newEmail)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid email format",
});
}

Comment on lines +149 to +151
const userId = ctx.session.user.id;

const token = generateEmailToken();
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 6, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Check if newEmail is already in use

To prevent users from changing their email to one that is already associated with another account, you should check if newEmail exists in the database.

Apply this diff to add the check:

+           const existingUser = await ctx.db
+             .select()
+             .from(user)
+             .where(eq(user.email, newEmail))
+             .limit(1);
+
+           if (existingUser.length > 0) {
+             throw new TRPCError({
+               code: "BAD_REQUEST",
+               message: "Email is already in use",
+             });
+           }
📝 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 userId = ctx.session.user.id;
const token = generateEmailToken();
const userId = ctx.session.user.id;
const token = generateEmailToken();
const existingUser = await ctx.db
.select()
.from(user)
.where(eq(user.email, newEmail))
.limit(1);
if (existingUser.length > 0) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Email is already in use",
});
}

Copy link
Contributor

Choose a reason for hiding this comment

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

This is definitely needed!

Copy link
Contributor

Choose a reason for hiding this comment

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

@NiallJoeMaher, glad you find it helpful!

(=^・^=)

Comment on lines 141 to 159
const handleNewEmailUpdate = async () => {
setLoading(true);
try {
await updateEmail({ newEmail });

toast.success("Verification link sent to your email.");
setSendForVerification(true);
} catch (error: any) {
if (
error.data.code === "BAD_REQUEST" ||
error.data?.code === "INTERNAL_SERVER_ERROR"
) {
toast.error(error.data.message);
} else {
toast.error("Something went wrong");
}
}
setLoading(false);
};
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

Ensure robust error handling in handleNewEmailUpdate function.

In the catch block, accessing error.data.code and error.data.message without confirming that error.data exists may lead to runtime errors if error.data is undefined. To prevent potential issues, use optional chaining when accessing these properties.

Apply this diff to enhance error handling:

 const handleNewEmailUpdate = async () => {
   setLoading(true);
   try {
     await updateEmail({ newEmail });

     toast.success("Verification link sent to your email.");
     setSendForVerification(true);
   } catch (error: any) {
-    if (
-      error.data.code === "BAD_REQUEST" ||
-      error.data?.code === "INTERNAL_SERVER_ERROR"
-    ) {
-      toast.error(error.data.message);
+    const errorCode = error.data?.code;
+    if (
+      errorCode === "BAD_REQUEST" ||
+      errorCode === "INTERNAL_SERVER_ERROR"
+    ) {
+      toast.error(error.data?.message || "An error occurred.");
     } else {
       toast.error("Something went wrong");
     }
   } finally {
     setLoading(false);
   }
 };
📝 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 handleNewEmailUpdate = async () => {
setLoading(true);
try {
await updateEmail({ newEmail });
toast.success("Verification link sent to your email.");
setSendForVerification(true);
} catch (error: any) {
if (
error.data.code === "BAD_REQUEST" ||
error.data?.code === "INTERNAL_SERVER_ERROR"
) {
toast.error(error.data.message);
} else {
toast.error("Something went wrong");
}
}
setLoading(false);
};
const handleNewEmailUpdate = async () => {
setLoading(true);
try {
await updateEmail({ newEmail });
toast.success("Verification link sent to your email.");
setSendForVerification(true);
} catch (error: any) {
const errorCode = error.data?.code;
if (
errorCode === "BAD_REQUEST" ||
errorCode === "INTERNAL_SERVER_ERROR"
) {
toast.error(error.data?.message || "An error occurred.");
} else {
toast.error("Something went wrong");
}
} finally {
setLoading(false);
}
};

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

🧹 Outside diff range and nitpick comments (1)
config/constants.ts (1)

1-3: LGTM! Consider adding a purpose comment.

The implementation of TOKEN_EXPIRATION_TIME is clear and correct. The constant is well-named, accurately calculated, and properly exported.

Consider adding a brief comment explaining the purpose of this constant. For example:

// Expiration time for email verification tokens
const TOKEN_EXPIRATION_TIME = 1000 * 60 * 60; // 1 hour

This would provide more context for other developers working with this constant.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 29a344a and 8279ff8.

📒 Files selected for processing (1)
  • config/constants.ts (1 hunks)
🧰 Additional context used
🔇 Additional comments (1)
config/constants.ts (1)

1-1: Verify the token expiration time with the team.

The current expiration time of 1 hour (3,600,000 milliseconds) seems reasonable for a token. However, it's important to ensure this aligns with the project's security requirements and user experience expectations.

Please confirm with the team if this duration is appropriate for the email verification process. Consider factors such as:

  1. How long users typically take to verify their email.
  2. The balance between security (shorter expiration) and user convenience (longer expiration).
  3. Any compliance requirements that might affect token lifetimes.

If adjustments are needed, this constant can be easily modified.

@Nil2000
Copy link
Contributor Author

Nil2000 commented Oct 7, 2024

@NiallJoeMaher Can you check if any more changes require?

Key changes

  1. Changed Migration file name (Removing snapshot and migration file and then created new with proper name)
  2. Removed api/update_email/route.ts and introduced trpc method for updating email.
  3. Introduced few trycatch block
  4. Changed POST to GET for token verification (verifying userId from getServerAut())

Copy link
Contributor

@NiallJoeMaher NiallJoeMaher left a comment

Choose a reason for hiding this comment

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

A few errors to catch anywhere you are doing an async/await operation. But nearly there! Well done! Tonne of work.

CONSTRAINT "EmailVerificationToken_token_unique" UNIQUE("token")
);
--> statement-breakpoint
DROP INDEX IF EXISTS "BannedUsers_userId_key";--> statement-breakpoint
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure what is going on here because we seem to be deleting and re-adding indexes (I know this is autocreated so I need to test this locally).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah even I was not sure why just creating a new schema allows to have a migration which deletes prev indexes and again adding them later

<a href="${verificationLink}" class="btn">Verify Email</a>
<p>Please note that this link is valid for 1 hour only. If it expires, you will need to request a new one.</p>
<p>If you did not create an account, please ignore this email.</p>
<p>Best regards, <br>The Codu Team</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

Spellcheck - Codú (we need the accent)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh my bad 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done ✅

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

🧹 Outside diff range and nitpick comments (1)
utils/emailToken.ts (1)

1-146: Overall assessment: Good implementation with room for improvement

The utils/emailToken.ts file provides a comprehensive set of functions for managing email verification tokens. While the overall implementation is solid, there are several areas where improvements can be made:

  1. Update the company name in the email template for consistency.
  2. Enhance the getTokenFromDb function to check for token expiration and return a single token.
  3. Improve the updateEmail function to confirm successful updates.
  4. Strengthen the deleteTokenFromDb function by including the user ID in the deletion criteria and confirming deletions.

These improvements will enhance the security, consistency, and reliability of the email verification system.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 2f49be7 and 36e63db.

📒 Files selected for processing (2)
  • app/api/verify-email/route.ts (1 hunks)
  • utils/emailToken.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/api/verify-email/route.ts
🧰 Additional context used
🔇 Additional comments (7)
utils/emailToken.ts (7)

1-5: LGTM: Import statements are well-organized and relevant.

The import statements are correctly structured and include all necessary dependencies for the module's functionality.


7-9: LGTM: Secure token generation implementation.

The generateEmailToken function uses crypto.randomBytes to generate a secure random token, which is a best practice for creating unpredictable tokens.


11-33: LGTM: Robust implementation of token storage with error handling.

The storeTokenInDb function effectively stores the token in the database and includes proper error handling. This implementation addresses the previous review comment suggestion.


35-108: ⚠️ Potential issue

Update company name and consider using APP_URL environment variable.

  1. The company name in the email template still references "Codú". Please update it to "CodeRabbit Inc." for consistency with your branding.

  2. Consider using NEXT_PUBLIC_APP_URL instead of NEXT_PUBLIC_BACKEND_URL for the verification link. This makes it clearer that it's the frontend URL where the verification page is hosted.

Apply these changes:

- <p>Best regards, <br>The Codú Team</p>
+ <p>Best regards, <br>The CodeRabbit Inc. Team</p>
...
- <p>&copy; ${new Date().getFullYear()} Codú. All rights reserved.</p>
+ <p>&copy; ${new Date().getFullYear()} CodeRabbit Inc. All rights reserved.</p>
...
- const verificationLink = `${process.env.NEXT_PUBLIC_BACKEND_URL}/verify-email?token=${token}`;
+ const verificationLink = `${process.env.NEXT_PUBLIC_APP_URL}/verify-email?token=${token}`;

Make sure to add NEXT_PUBLIC_APP_URL to your environment variables if it's not already present.


110-126: 🛠️ Refactor suggestion

Enhance getTokenFromDb for better security and consistency.

The current implementation can be improved in two ways:

  1. It doesn't check if the token has expired, which could be a security risk.
  2. It returns an array of tokens instead of a single token, which might be unexpected for the caller.

Consider applying these changes:

import { gt } from "drizzle-orm";

export const getTokenFromDb = async (token: string, userId: string) => {
  try {
    const tokenFromDb = await db
      .select()
      .from(emailVerificationToken)
      .where(
        and(
          eq(emailVerificationToken.token, token),
          eq(emailVerificationToken.userId, userId),
          gt(emailVerificationToken.expiresAt, new Date()) // Check if token is not expired
        ),
      )
      .limit(1); // Ensure we only get one result

    return tokenFromDb[0] || null; // Return null if no token found
  } catch (error) {
    console.error("Error fetching token from database:", error);
    throw new Error("Failed to fetch email verification token");
  }
};

These changes will ensure that only valid, non-expired tokens are returned, and the function consistently returns a single token object or null.


128-135: 🛠️ Refactor suggestion

Improve updateEmail with success confirmation.

The current implementation lacks confirmation of a successful update. This could make it difficult for the caller to know if the update was successful.

Consider applying these changes:

export const updateEmail = async (userId: string, newEmail: string): Promise<boolean> => {
  try {
    const result = await db
      .update(user)
      .set({ email: newEmail })
      .where(eq(user.id, userId))
      .returning({ updatedId: user.id });
    
    return result.length > 0;
  } catch (error) {
    console.error("Error updating email in database:", error);
    throw new Error("Failed to update email");
  }
};

These changes will:

  1. Return a boolean indicating whether the update was successful.
  2. Use the returning clause to confirm that a row was actually updated.

This will make the function more informative for the calling code.


137-146: 🛠️ Refactor suggestion

Enhance deleteTokenFromDb for improved security and confirmation.

The current implementation has two main areas for improvement:

  1. It doesn't include the user ID in the deletion criteria, which could be a security risk.
  2. It doesn't confirm if a deletion actually occurred.

Consider applying these changes:

export const deleteTokenFromDb = async (token: string, userId: string): Promise<boolean> => {
  try {
    const result = await db
      .delete(emailVerificationToken)
      .where(
        and(
          eq(emailVerificationToken.token, token),
          eq(emailVerificationToken.userId, userId)
        )
      )
      .returning({ deletedId: emailVerificationToken.id });
    
    return result.length > 0;
  } catch (error) {
    console.error("Error deleting token from database:", error);
    throw new Error("Failed to delete email verification token");
  }
};

These changes will:

  1. Include the user ID in the deletion criteria for improved security.
  2. Return a boolean indicating whether a token was actually deleted.
  3. Use the returning clause to confirm that a row was deleted.

This will make the function more secure and informative for the calling code.

Copy link
Contributor

@NiallJoeMaher NiallJoeMaher left a comment

Choose a reason for hiding this comment

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

A handful of more changes. This is a huge PR so hopefully the next one we can break into different steps to make it easier for you (and get you some Hacktoberfest points to make up for the pain of this one!)

Comment on lines +149 to +151
const userId = ctx.session.user.id;

const token = generateEmailToken();
Copy link
Contributor

Choose a reason for hiding this comment

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

This is definitely needed!

@Nil2000
Copy link
Contributor Author

Nil2000 commented Oct 8, 2024

Key changes

  • Introduced checkIfEmailExists to check email used by any user already. Will be checked everytime user tries to update with already used email.
  • Now email schema would be unique()
  • Error was not shown in toast previously (Updated client page of settings to support this)

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

🧹 Outside diff range and nitpick comments (1)
drizzle/0007_added_emailtokenverification_schema.sql (1)

1-10: LGTM! Consider adding a foreign key constraint for userId.

The EmailVerificationToken table structure is well-designed for its purpose. It includes all necessary columns with appropriate data types and constraints. The unique constraints on token and email columns ensure data integrity.

Consider adding a foreign key constraint for the userId column to enforce referential integrity with the user table. This can be done with an ALTER TABLE statement after the table creation:

ALTER TABLE "EmailVerificationToken" 
ADD CONSTRAINT "EmailVerificationToken_userId_fkey" 
FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE;

This ensures that every userId in the EmailVerificationToken table corresponds to a valid user and automatically deletes verification tokens when a user is deleted.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 36e63db and 93ea188.

⛔ Files ignored due to path filters (2)
  • drizzle/meta/0007_snapshot.json is excluded by !**/*.json
  • drizzle/meta/_journal.json is excluded by !**/*.json
📒 Files selected for processing (5)
  • app/(app)/settings/_client.tsx (7 hunks)
  • drizzle/0007_added_emailtokenverification_schema.sql (1 hunks)
  • server/api/router/profile.ts (2 hunks)
  • server/db/schema.ts (1 hunks)
  • utils/emailToken.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/db/schema.ts
🧰 Additional context used
🔇 Additional comments (9)
drizzle/0007_added_emailtokenverification_schema.sql (3)

27-33: LGTM! Good improvements to data integrity and tracking.

The changes to set CURRENT_TIMESTAMP as the default for updatedAt columns in various tables is a good practice. It ensures that the last modification time is automatically tracked without requiring explicit updates in application code.

Adding a primary key constraint to the account table using provider and providerAccountId columns is also a positive change for data integrity.

These changes will improve data consistency and make it easier to track when records were last modified.


12-26: Verify the impact of dropping existing indexes.

The migration drops several indexes from various tables. While this may be necessary for schema changes, it could potentially impact query performance.

Please ensure that:

  1. The removal of these indexes doesn't negatively impact query performance.
  2. Any necessary indexes are recreated after the schema changes.

Run the following script to check if any of these indexes are still being used:

#!/bin/bash
# Description: Check for queries that might be affected by the removed indexes

# Test: Search for queries using the tables with removed indexes
rg --type sql -i "FROM\s+(BannedUsers|Bookmark|Community|Event|Like|Post|PostTag|Tag|User|user)"

# Note: This is a basic check. A thorough analysis would require examining query plans 
# and performance metrics in the production environment.

Consider consulting with the database administrator or conducting thorough performance testing before applying these changes to the production environment.


40-54: LGTM! Verify if all necessary indexes are recreated.

The creation of these unique indexes is a good practice. They enforce data uniqueness and can significantly improve query performance for the covered columns.

Please ensure that:

  1. All necessary indexes that were dropped earlier have been recreated.
  2. The new index definitions match the requirements of your most common and performance-critical queries.

Run the following script to compare the dropped and recreated indexes:

Review the output to ensure all necessary indexes are present. If any critical indexes are missing, consider adding them to maintain query performance.

✅ Verification successful

All necessary indexes have been successfully recreated.

The creation of these unique indexes ensures data uniqueness and enhances query performance as intended.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Compare dropped and recreated indexes

# Test: Extract dropped and recreated index names
dropped_indexes=$(sed -n '/DROP INDEX/s/.*IF EXISTS "\([^"]*\)".*/\1/p' drizzle/0007_added_emailtokenverification_schema.sql)
recreated_indexes=$(sed -n '/CREATE.*INDEX/s/.*IF NOT EXISTS "\([^"]*\)".*/\1/p' drizzle/0007_added_emailtokenverification_schema.sql)

# Compare the lists
echo "Dropped indexes:"
echo "$dropped_indexes"
echo "Recreated indexes:"
echo "$recreated_indexes"

# Find indexes that were dropped but not recreated
echo "Indexes dropped but not recreated:"
comm -23 <(echo "$dropped_indexes" | sort) <(echo "$recreated_indexes" | sort)

# Note: This is a basic comparison. Ensure that the recreated indexes 
# serve the same purpose as the dropped ones, even if names differ.

Length of output: 484


Script:

#!/bin/bash
# Description: Compare dropped and recreated indexes

# Extract dropped and recreated index names
dropped_indexes=$(sed -n '/DROP INDEX/s/.*IF EXISTS "\([^"]*\)".*/\1/p' drizzle/0007_added_emailtokenverification_schema.sql)
recreated_indexes=$(sed -n '/CREATE.*INDEX/s/.*IF NOT EXISTS "\([^"]*\)".*/\1/p' drizzle/0007_added_emailtokenverification_schema.sql)

# Sort the lists and save to temporary files
echo "$dropped_indexes" | sort > dropped_sorted.txt
echo "$recreated_indexes" | sort > recreated_sorted.txt

# Find indexes that were dropped but not recreated using diff
echo "Indexes dropped but not recreated:"
diff dropped_sorted.txt recreated_sorted.txt | grep '^<' | sed 's/^< //'

Length of output: 1582

app/(app)/settings/_client.tsx (2)

49-52: LGTM: Improved form initialization.

The change to spread the entire profile object in defaultValues ensures that all profile fields are correctly initialized. This is a good practice for maintaining consistency between the form state and the actual profile data.


Line range hint 1-563: Overall assessment: Well-implemented email update feature.

The addition of the email update functionality to the Settings component is well-implemented and integrates smoothly with the existing code. The new feature provides a good user experience with appropriate feedback mechanisms.

Key points:

  1. The email update UI is clear and user-friendly.
  2. Error handling and success notifications are in place.
  3. The new functionality doesn't disrupt existing features.

Consider implementing the suggested improvements to further enhance the user experience and code quality. Great job on this feature implementation!

utils/emailToken.ts (4)

36-36: Previous comment still applies

The previous review comment about using an appropriate environment variable for the base URL (e.g., NEXT_PUBLIC_APP_URL instead of NEXT_PUBLIC_BACKEND_URL) still applies. Updating this will enhance flexibility across different environments.


110-126: Previous comment still applies

The previous review comment regarding enhancing getTokenFromDb for better security and consistency still applies. Implementing the suggested changes will ensure expired tokens are not used and that the function consistently returns a single token.


128-135: Previous comment still applies

The previous review comment about improving updateEmail with error handling and success confirmation still applies. Modifying the function to confirm that the update occurred will make it more robust.


137-146: Previous comment still applies

The previous review comment regarding enhancing deleteTokenFromDb for improved security and error handling still applies. Including the userId in the deletion criteria and confirming the deletion will strengthen the function's reliability.

Comment on lines 34 to 38
DO $$ BEGIN
ALTER TABLE "EmailVerificationToken" ADD CONSTRAINT "EmailVerificationToken_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
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

Consider changing ON DELETE action to CASCADE for EmailVerificationToken foreign key.

The addition of a foreign key constraint for the userId column in the EmailVerificationToken table is good for maintaining referential integrity. However, the current ON DELETE and ON UPDATE actions are set to "no action".

Consider changing the ON DELETE action to CASCADE. This would automatically delete the verification tokens when a user is deleted, preventing orphaned records. Here's the suggested modification:

ALTER TABLE "EmailVerificationToken" 
ADD CONSTRAINT "EmailVerificationToken_userId_user_id_fk" 
FOREIGN KEY ("userId") 
REFERENCES "public"."user"("id") 
ON DELETE CASCADE 
ON UPDATE NO ACTION;

This change ensures that the EmailVerificationToken table remains consistent with the user table without manual intervention.

Comment on lines +15 to +16
import { Button } from "@/components/ui-components/button";
import { CheckCheck, Loader2 } 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

Remove unused import 'CheckCheck' from 'lucide-react'.

The CheckCheck component is imported but not used in the code. Removing unused imports helps maintain a clean and efficient codebase.

Apply this diff to fix the issue:

-import { CheckCheck, Loader2 } from "lucide-react";
+import { Loader2 } from "lucide-react";
📝 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 { Button } from "@/components/ui-components/button";
import { CheckCheck, Loader2 } from "lucide-react";
import { Button } from "@/components/ui-components/button";
import { Loader2 } from "lucide-react";

Comment on lines 372 to 430
<div className="mt-6 text-neutral-600 dark:text-neutral-400">
<h2 className="text-xl font-bold tracking-tight text-neutral-800 dark:text-white">
Update email
</h2>
<p className="mt-1 text-sm">Change your email here.</p>
<div className="mt-2 flex flex-col gap-2">
<div className="flex flex-col">
<label htmlFor="currEmail">Current email</label>
<div>
<input
type="email"
id="currEmail"
value={profile.email!}
disabled
/>
</div>
</div>
<div className="flex flex-col">
<label htmlFor="newEmail">Update email</label>
<div>
<input
type="email"
id="newEmail"
onChange={(e) => setNewEmail(e.target.value)}
value={newEmail}
/>
</div>
</div>
{!sendForVerification ? (
<Button
className="w-[200px]"
disabled={!(newEmail || loading)}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Send verification link
</Button>
) : (
<div className="mt-2 flex flex-row gap-2">
<h2 className="flex items-center gap-2 text-sm italic text-green-400">
<CheckCheck />
Verification link sent
</h2>
<Button
className="w-[250px]"
disabled={!(newEmail || loading)}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Resend verification link
</Button>
</div>
)}
</div>
</div>
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

Improve button disabled state logic.

The current implementation of the disabled state for the "Send verification link" and "Resend verification link" buttons can be improved for better user experience and logic clarity.

Consider the following changes:

  1. The button should be disabled when there's no new email entered or when it's the same as the current email.
  2. The loading state should also be considered in the disabled logic.

Apply this diff to improve the logic:

-                        disabled={!(newEmail || loading)}
+                        disabled={!newEmail || newEmail === profile.email || loading}

This change ensures that the button is disabled when:

  • No new email is entered
  • The new email is the same as the current email
  • The form is in a loading state

This provides a more intuitive user experience and prevents unnecessary API calls.

📝 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
<div className="mt-6 text-neutral-600 dark:text-neutral-400">
<h2 className="text-xl font-bold tracking-tight text-neutral-800 dark:text-white">
Update email
</h2>
<p className="mt-1 text-sm">Change your email here.</p>
<div className="mt-2 flex flex-col gap-2">
<div className="flex flex-col">
<label htmlFor="currEmail">Current email</label>
<div>
<input
type="email"
id="currEmail"
value={profile.email!}
disabled
/>
</div>
</div>
<div className="flex flex-col">
<label htmlFor="newEmail">Update email</label>
<div>
<input
type="email"
id="newEmail"
onChange={(e) => setNewEmail(e.target.value)}
value={newEmail}
/>
</div>
</div>
{!sendForVerification ? (
<Button
className="w-[200px]"
disabled={!(newEmail || loading)}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Send verification link
</Button>
) : (
<div className="mt-2 flex flex-row gap-2">
<h2 className="flex items-center gap-2 text-sm italic text-green-400">
<CheckCheck />
Verification link sent
</h2>
<Button
className="w-[250px]"
disabled={!(newEmail || loading)}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Resend verification link
</Button>
</div>
)}
</div>
</div>
<div className="mt-6 text-neutral-600 dark:text-neutral-400">
<h2 className="text-xl font-bold tracking-tight text-neutral-800 dark:text-white">
Update email
</h2>
<p className="mt-1 text-sm">Change your email here.</p>
<div className="mt-2 flex flex-col gap-2">
<div className="flex flex-col">
<label htmlFor="currEmail">Current email</label>
<div>
<input
type="email"
id="currEmail"
value={profile.email!}
disabled
/>
</div>
</div>
<div className="flex flex-col">
<label htmlFor="newEmail">Update email</label>
<div>
<input
type="email"
id="newEmail"
onChange={(e) => setNewEmail(e.target.value)}
value={newEmail}
/>
</div>
</div>
{!sendForVerification ? (
<Button
className="w-[200px]"
disabled={!newEmail || newEmail === profile.email || loading}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Send verification link
</Button>
) : (
<div className="mt-2 flex flex-row gap-2">
<h2 className="flex items-center gap-2 text-sm italic text-green-400">
<CheckCheck />
Verification link sent
</h2>
<Button
className="w-[250px]"
disabled={!newEmail || newEmail === profile.email || loading}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Resend verification link
</Button>
</div>
)}
</div>
</div>

Comment on lines +169 to +173
throw new TRPCError({
code: error.code || "INTERNAL_SERVER_ERROR",
message: error.message || "Internal server 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

Avoid exposing internal error details to the client

Returning the original error code and message could potentially leak sensitive information. It's safer to log the error internally and return a generic error message to the client.

Apply this diff to fix the error handling:

       } catch (error: any) {
+        // Log the error internally for debugging purposes
+        console.error('Error updating email:', error);
-        throw new TRPCError({
-          code: error.code || "INTERNAL_SERVER_ERROR",
-          message: error.message || "Internal server error",
-        });
+        throw new TRPCError({
+          code: "INTERNAL_SERVER_ERROR",
+          message: "An unexpected error occurred",
+        });
       }
📝 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
throw new TRPCError({
code: error.code || "INTERNAL_SERVER_ERROR",
message: error.message || "Internal server error",
});
}
} catch (error: any) {
// Log the error internally for debugging purposes
console.error('Error updating email:', error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "An unexpected error occurred",
});
}

Comment on lines +143 to +166
if (!newEmail) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid request",
});
}

const ifEmailExists = await checkIfEmailExists(newEmail);

if (ifEmailExists) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Email already exists",
});
}

const userId = ctx.session.user.id;

const token = generateEmailToken();
const expiresAt = new Date(Date.now() + TOKEN_EXPIRATION_TIME);

await storeTokenInDb(userId, token, expiresAt, newEmail);
await sendVerificationEmail(newEmail, token);

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

Ensure email addresses are normalized and comparisons are case-insensitive

Email addresses are case-insensitive, so it's important to normalize them to lower case before processing. This avoids issues where [email protected] and [email protected] are treated differently.

Apply this diff to normalize the email address to lower case:

         const { newEmail } = input;

         if (!newEmail) {
           throw new TRPCError({
             code: "BAD_REQUEST",
             message: "Invalid request",
           });
         }

+        const normalizedEmail = newEmail.toLowerCase();

-        const ifEmailExists = await checkIfEmailExists(newEmail);
+        const ifEmailExists = await checkIfEmailExists(normalizedEmail);

         if (ifEmailExists) {
           throw new TRPCError({
             code: "BAD_REQUEST",
             message: "Email already exists",
           });
         }

         const userId = ctx.session.user.id;

         const token = generateEmailToken();

         const expiresAt = new Date(Date.now() + TOKEN_EXPIRATION_TIME);

-        await storeTokenInDb(userId, token, expiresAt, newEmail);
-        await sendVerificationEmail(newEmail, token);
+        await storeTokenInDb(userId, token, expiresAt, normalizedEmail);
+        await sendVerificationEmail(normalizedEmail, token);
📝 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
if (!newEmail) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid request",
});
}
const ifEmailExists = await checkIfEmailExists(newEmail);
if (ifEmailExists) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Email already exists",
});
}
const userId = ctx.session.user.id;
const token = generateEmailToken();
const expiresAt = new Date(Date.now() + TOKEN_EXPIRATION_TIME);
await storeTokenInDb(userId, token, expiresAt, newEmail);
await sendVerificationEmail(newEmail, token);
const { newEmail } = input;
if (!newEmail) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid request",
});
}
const normalizedEmail = newEmail.toLowerCase();
const ifEmailExists = await checkIfEmailExists(normalizedEmail);
if (ifEmailExists) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Email already exists",
});
}
const userId = ctx.session.user.id;
const token = generateEmailToken();
const expiresAt = new Date(Date.now() + TOKEN_EXPIRATION_TIME);
await storeTokenInDb(userId, token, expiresAt, normalizedEmail);
await sendVerificationEmail(normalizedEmail, token);

Comment on lines 35 to 108
export const sendVerificationEmail = async (email: string, token: string) => {
const verificationLink = `${process.env.NEXT_PUBLIC_BACKEND_URL}/verify-email?token=${token}`;
const subject = "Verify Your Email Address";

const htmlMessage = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Verification</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
p {
font-size: 16px;
line-height: 1.6;
margin-bottom: 20px;
}
.btn {
display: inline-block;
padding: 12px 24px;
font-size: 16px;
color: #fff !important;
font-weight: bold;
background-color: #007bff;
text-decoration: none;
border-radius: 4px;
margin-top: 20px;
}
.footer {
margin-top: 40px;
font-size: 12px;
color: #777;
}
</style>
</head>
<body>
<div class="container">
<h1>Confirm Your Email Address</h1>
<p>Hello,</p>
<p>Thank you for registering with us! To complete your registration, please verify your email address by clicking the button below:</p>
<a href="${verificationLink}" class="btn">Verify Email</a>
<p>Please note that this link is valid for 1 hour only. If it expires, you will need to request a new one.</p>
<p>If you did not create an account, please ignore this email.</p>
<p>Best regards, <br>The Codú Team</p>
<div class="footer">
<p>&copy; ${new Date().getFullYear()} Codú. All rights reserved.</p>
</div>
</div>
</body>
</html>
`;

return sendEmail({ recipient: email, htmlMessage, subject });
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling to sendVerificationEmail

Currently, the sendVerificationEmail function does not handle potential errors from the sendEmail function. Adding error handling will ensure that any issues during the email sending process are caught and managed appropriately.

Consider implementing error handling as follows:

export const sendVerificationEmail = async (email: string, token: string) => {
  const verificationLink = `${process.env.NEXT_PUBLIC_BACKEND_URL}/verify-email?token=${token}`;
  const subject = "Verify Your Email Address";
  const htmlMessage = `...`; // Existing HTML content

  try {
    await sendEmail({ recipient: email, htmlMessage, subject });
  } catch (error) {
    console.error("Error sending verification email:", error);
    throw new Error("Failed to send verification email");
  }
};

Comment on lines +39 to +105
const htmlMessage = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Verification</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
p {
font-size: 16px;
line-height: 1.6;
margin-bottom: 20px;
}
.btn {
display: inline-block;
padding: 12px 24px;
font-size: 16px;
color: #fff !important;
font-weight: bold;
background-color: #007bff;
text-decoration: none;
border-radius: 4px;
margin-top: 20px;
}
.footer {
margin-top: 40px;
font-size: 12px;
color: #777;
}
</style>
</head>
<body>
<div class="container">
<h1>Confirm Your Email Address</h1>
<p>Hello,</p>
<p>Thank you for registering with us! To complete your registration, please verify your email address by clicking the button below:</p>
<a href="${verificationLink}" class="btn">Verify Email</a>
<p>Please note that this link is valid for 1 hour only. If it expires, you will need to request a new one.</p>
<p>If you did not create an account, please ignore this email.</p>
<p>Best regards, <br>The Codú Team</p>
<div class="footer">
<p>&copy; ${new Date().getFullYear()} Codú. All rights reserved.</p>
</div>
</div>
</body>
</html>
`;
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

Consider using a template engine for email construction

The HTML content for the email is currently defined within a template string. For better maintainability and readability, consider using a templating engine like Handlebars or EJS. This approach will make it easier to manage and update the email content, especially as it grows in complexity.

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

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 93ea188 and ad9ef7f.

📒 Files selected for processing (2)
  • app/(app)/settings/_client.tsx (7 hunks)
  • utils/emailToken.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • utils/emailToken.ts
🧰 Additional context used
🔇 Additional comments (3)
app/(app)/settings/_client.tsx (3)

32-33: LGTM: User type update.

The addition of 'email' and 'id' fields to the User type is consistent with the new email-related features.


49-52: LGTM: New state variables and API mutation for email update.

The addition of new state variables (newEmail, sendForVerification, loading) and the updateEmail mutation are consistent with the new email update functionality. The defaultValues update in the useForm hook ensures proper initialization of the email field.

Also applies to: 62-64, 76-76


15-16: ⚠️ Potential issue

Remove unused import 'CheckCheck' from 'lucide-react'.

The CheckCheck component is imported but not used in the code. Removing unused imports helps maintain a clean and efficient codebase.

Apply this diff to fix the issue:

-import { CheckCheck, Loader2 } from "lucide-react";
+import { Loader2 } from "lucide-react";

Likely invalid or redundant comment.

Comment on lines 372 to 431
<div className="mt-6 text-neutral-600 dark:text-neutral-400">
<h2 className="text-xl font-bold tracking-tight text-neutral-800 dark:text-white">
Update email
</h2>
<p className="mt-1 text-sm">Change your email here.</p>
<div className="mt-2 flex flex-col gap-2">
<div className="flex flex-col">
<label htmlFor="currEmail">Current email</label>
<div>
<input
type="email"
id="currEmail"
value={profile.email!}
disabled
/>
</div>
</div>
<div className="flex flex-col">
<label htmlFor="newEmail">Update email</label>
<div>
<input
type="email"
id="newEmail"
onChange={(e) => setNewEmail(e.target.value)}
value={newEmail}
/>
</div>
</div>
{!sendForVerification ? (
<Button
className="w-[200px]"
disabled={!(newEmail || loading)}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Send verification link
</Button>
) : (
<div className="mt-2 flex flex-row gap-2">
<h2 className="flex items-center gap-2 text-sm italic text-green-400">
<CheckCheck />
Verification link sent
</h2>
<Button
className="w-[250px]"
disabled={
!newEmail || newEmail === profile.email || loading
}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Resend verification link
</Button>
</div>
)}
</div>
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

Improve button disabled state logic.

The current implementation of the disabled state for the "Send verification link" and "Resend verification link" buttons can be improved for better user experience and logic clarity.

Consider the following changes:

  1. The button should be disabled when there's no new email entered or when it's the same as the current email.
  2. The loading state should also be considered in the disabled logic.

Apply this diff to improve the logic:

-                        disabled={!(newEmail || loading)}
+                        disabled={!newEmail || newEmail === profile.email || loading}

This change ensures that the button is disabled when:

  • No new email is entered
  • The new email is the same as the current email
  • The form is in a loading state

This provides a more intuitive user experience and prevents unnecessary API calls.

📝 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
<div className="mt-6 text-neutral-600 dark:text-neutral-400">
<h2 className="text-xl font-bold tracking-tight text-neutral-800 dark:text-white">
Update email
</h2>
<p className="mt-1 text-sm">Change your email here.</p>
<div className="mt-2 flex flex-col gap-2">
<div className="flex flex-col">
<label htmlFor="currEmail">Current email</label>
<div>
<input
type="email"
id="currEmail"
value={profile.email!}
disabled
/>
</div>
</div>
<div className="flex flex-col">
<label htmlFor="newEmail">Update email</label>
<div>
<input
type="email"
id="newEmail"
onChange={(e) => setNewEmail(e.target.value)}
value={newEmail}
/>
</div>
</div>
{!sendForVerification ? (
<Button
className="w-[200px]"
disabled={!(newEmail || loading)}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Send verification link
</Button>
) : (
<div className="mt-2 flex flex-row gap-2">
<h2 className="flex items-center gap-2 text-sm italic text-green-400">
<CheckCheck />
Verification link sent
</h2>
<Button
className="w-[250px]"
disabled={
!newEmail || newEmail === profile.email || loading
}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Resend verification link
</Button>
</div>
)}
</div>
<div className="mt-6 text-neutral-600 dark:text-neutral-400">
<h2 className="text-xl font-bold tracking-tight text-neutral-800 dark:text-white">
Update email
</h2>
<p className="mt-1 text-sm">Change your email here.</p>
<div className="mt-2 flex flex-col gap-2">
<div className="flex flex-col">
<label htmlFor="currEmail">Current email</label>
<div>
<input
type="email"
id="currEmail"
value={profile.email!}
disabled
/>
</div>
</div>
<div className="flex flex-col">
<label htmlFor="newEmail">Update email</label>
<div>
<input
type="email"
id="newEmail"
onChange={(e) => setNewEmail(e.target.value)}
value={newEmail}
/>
</div>
</div>
{!sendForVerification ? (
<Button
className="w-[200px]"
disabled={!newEmail || newEmail === profile.email || loading}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Send verification link
</Button>
) : (
<div className="mt-2 flex flex-row gap-2">
<h2 className="flex items-center gap-2 text-sm italic text-green-400">
<CheckCheck />
Verification link sent
</h2>
<Button
className="w-[250px]"
disabled={
!newEmail || newEmail === profile.email || loading
}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Resend verification link
</Button>
</div>
)}
</div>

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

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between ad9ef7f and 56e3491.

📒 Files selected for processing (1)
  • app/(app)/settings/_client.tsx (7 hunks)
🧰 Additional context used
🔇 Additional comments (4)
app/(app)/settings/_client.tsx (4)

32-33: LGTM: User type update is consistent with new functionality.

The addition of email and id fields to the User type is appropriate for the new email verification feature.


49-52: LGTM: Proper initialization and setup for email verification feature.

The new state variables, updated defaultValues, and new API mutation are well-implemented and necessary for the email verification functionality.

Also applies to: 62-64, 76-76


Line range hint 1-563: Overall implementation of email verification feature is well-done.

The changes to implement the email verification feature are generally well-structured and integrated into the existing component. The new UI elements and functionality enhance the user experience by allowing email updates with proper verification.

A few minor improvements have been suggested:

  1. Removing an unused import.
  2. Enhancing error handling in the handleNewEmailUpdate function.
  3. Improving the disabled state logic for the verification link buttons.

With these small adjustments, the code will be more robust and provide an even better user experience.


15-16: ⚠️ Potential issue

Remove unused import 'CheckCheck' from 'lucide-react'.

The CheckCheck component is imported but not used in the code. Removing unused imports helps maintain a clean and efficient codebase.

Apply this diff to fix the issue:

-import { CheckCheck, Loader2 } from "lucide-react";
+import { Loader2 } from "lucide-react";

Likely invalid or redundant comment.

Comment on lines +372 to +433
<div className="mt-6 text-neutral-600 dark:text-neutral-400">
<h2 className="text-xl font-bold tracking-tight text-neutral-800 dark:text-white">
Update email
</h2>
<p className="mt-1 text-sm">Change your email here.</p>
<div className="mt-2 flex flex-col gap-2">
<div className="flex flex-col">
<label htmlFor="currEmail">Current email</label>
<div>
<input
type="email"
id="currEmail"
value={profile.email!}
disabled
/>
</div>
</div>
<div className="flex flex-col">
<label htmlFor="newEmail">Update email</label>
<div>
<input
type="email"
id="newEmail"
onChange={(e) => setNewEmail(e.target.value)}
value={newEmail}
/>
</div>
</div>
{!sendForVerification ? (
<Button
className="w-[200px]"
disabled={
!newEmail || newEmail === profile.email || loading
}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Send verification link
</Button>
) : (
<div className="mt-2 flex flex-row gap-2">
<h2 className="flex items-center gap-2 text-sm italic text-green-400">
<CheckCheck />
Verification link sent
</h2>
<Button
className="w-[250px]"
disabled={
!newEmail || newEmail === profile.email || loading
}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Resend verification link
</Button>
</div>
)}
</div>
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

Improve button disabled state logic.

The current implementation of the disabled state for the "Send verification link" and "Resend verification link" buttons can be improved for better user experience and logic clarity.

Consider the following changes:

  1. The button should be disabled when there's no new email entered or when it's the same as the current email.
  2. The loading state should also be considered in the disabled logic.

Apply this diff to improve the logic:

-                        disabled={!(newEmail || loading)}
+                        disabled={!newEmail || newEmail === profile.email || loading}

This change ensures that the button is disabled when:

  • No new email is entered
  • The new email is the same as the current email
  • The form is in a loading state

This provides a more intuitive user experience and prevents unnecessary API calls.

📝 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
<div className="mt-6 text-neutral-600 dark:text-neutral-400">
<h2 className="text-xl font-bold tracking-tight text-neutral-800 dark:text-white">
Update email
</h2>
<p className="mt-1 text-sm">Change your email here.</p>
<div className="mt-2 flex flex-col gap-2">
<div className="flex flex-col">
<label htmlFor="currEmail">Current email</label>
<div>
<input
type="email"
id="currEmail"
value={profile.email!}
disabled
/>
</div>
</div>
<div className="flex flex-col">
<label htmlFor="newEmail">Update email</label>
<div>
<input
type="email"
id="newEmail"
onChange={(e) => setNewEmail(e.target.value)}
value={newEmail}
/>
</div>
</div>
{!sendForVerification ? (
<Button
className="w-[200px]"
disabled={
!newEmail || newEmail === profile.email || loading
}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Send verification link
</Button>
) : (
<div className="mt-2 flex flex-row gap-2">
<h2 className="flex items-center gap-2 text-sm italic text-green-400">
<CheckCheck />
Verification link sent
</h2>
<Button
className="w-[250px]"
disabled={
!newEmail || newEmail === profile.email || loading
}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Resend verification link
</Button>
</div>
)}
</div>
<div className="mt-6 text-neutral-600 dark:text-neutral-400">
<h2 className="text-xl font-bold tracking-tight text-neutral-800 dark:text-white">
Update email
</h2>
<p className="mt-1 text-sm">Change your email here.</p>
<div className="mt-2 flex flex-col gap-2">
<div className="flex flex-col">
<label htmlFor="currEmail">Current email</label>
<div>
<input
type="email"
id="currEmail"
value={profile.email!}
disabled
/>
</div>
</div>
<div className="flex flex-col">
<label htmlFor="newEmail">Update email</label>
<div>
<input
type="email"
id="newEmail"
onChange={(e) => setNewEmail(e.target.value)}
value={newEmail}
/>
</div>
</div>
{!sendForVerification ? (
<Button
className="w-[200px]"
disabled={!newEmail || newEmail === profile.email || loading}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Send verification link
</Button>
) : (
<div className="mt-2 flex flex-row gap-2">
<h2 className="flex items-center gap-2 text-sm italic text-green-400">
<CheckCheck />
Verification link sent
</h2>
<Button
className="w-[250px]"
disabled={!newEmail || newEmail === profile.email || loading}
onClick={handleNewEmailUpdate}
>
{loading && (
<Loader2 className="text-primary h-6 w-6 animate-spin" />
)}
Resend verification link
</Button>
</div>
)}
</div>

@NiallJoeMaher
Copy link
Contributor

A few conflicts, if you could delete the generated migration file and the meta files that were generated, pull the changes and re-run with npx drizzle-kit generate --name=email-verification. It should fix any issues that are holding things up with the migration conflicts and the weirdness that was happening.

@Nil2000
Copy link
Contributor Author

Nil2000 commented Oct 9, 2024

A few conflicts, if you could delete the generated migration file and the meta files that were generated, pull the changes and re-run with npx drizzle-kit generate --name=email-verification. It should fix any issues that are holding things up with the migration conflicts and the weirdness that was happening.

Yeah sure. Let me do that once.

Edit : Done ✅

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (2)
drizzle/0009_email-verification.sql (1)

1-10: Table structure looks good, consider additional optimizations.

The EmailVerificationToken table structure is well-designed for its purpose. All necessary columns are present with appropriate constraints. However, consider the following optimizations:

  1. Add an index on the userId column to improve query performance when looking up tokens for a specific user.
  2. Consider using uuid instead of serial for the id column to prevent potential enumeration attacks.
  3. The token column could be of type varchar(n) with an appropriate length limit instead of text to optimize storage.

Would you like me to provide the SQL statements for these optimizations?

app/(app)/settings/_client.tsx (1)

62-64: Consider initializing 'newEmail' with the current email.

Initializing newEmail with the current email can improve user experience by providing a starting point for editing.

Consider applying this change:

-const [newEmail, setNewEmail] = useState("");
+const [newEmail, setNewEmail] = useState(profile.email || "");
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 56e3491 and 9373b83.

⛔ Files ignored due to path filters (3)
  • drizzle/meta/0009_snapshot.json is excluded by !**/*.json
  • drizzle/meta/_journal.json is excluded by !**/*.json
  • package-lock.json is excluded by !**/package-lock.json, !**/*.json
📒 Files selected for processing (3)
  • app/(app)/settings/_client.tsx (7 hunks)
  • drizzle/0009_email-verification.sql (1 hunks)
  • server/db/schema.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/db/schema.ts
🧰 Additional context used
🔇 Additional comments (6)
drizzle/0009_email-verification.sql (2)

8-9: Unique constraints are appropriately defined.

The unique constraints on both token and email columns are well-implemented:

  • EmailVerificationToken_token_unique ensures each token is unique, preventing potential conflicts or security issues.
  • EmailVerificationToken_email_unique prevents multiple verification tokens for the same email, which is logical for this system.

These constraints contribute to data integrity and system security.


12-16: 🛠️ Refactor suggestion

Foreign key constraint is good, but consider ON DELETE CASCADE.

The foreign key constraint EmailVerificationToken_userId_user_id_fk is well-implemented:

  • It ensures referential integrity between the EmailVerificationToken and user tables.
  • The DO block with exception handling is a good practice for idempotent migrations.

However, consider changing the ON DELETE behavior:

Replace:

ON DELETE no action ON UPDATE no action

With:

ON DELETE CASCADE ON UPDATE no action

This change will automatically remove associated verification tokens when a user is deleted, preventing orphaned records.

To check the current constraints on the EmailVerificationToken table, run:

This will display the table structure, including constraints, allowing you to verify the current setup and any changes made.

app/(app)/settings/_client.tsx (4)

32-33: LGTM: User type updated correctly.

The addition of email and id fields to the User type is appropriate for the new email update functionality.


372-433: LGTM: Email update UI implemented correctly.

The new UI elements for email update are well-implemented. The component correctly handles different states (initial, loading, and after sending verification) and provides appropriate feedback to the user.

However, there's a small improvement we can make to the button disabled state:

-disabled={!(newEmail || loading)}
+disabled={!newEmail || newEmail === profile.email || loading}

This change ensures that the button is disabled when:

  • No new email is entered
  • The new email is the same as the current email
  • The form is in a loading state

This provides a more intuitive user experience and prevents unnecessary API calls.


Line range hint 1-605: LGTM: Email update feature well-implemented.

The new email update feature has been successfully integrated into the Settings component. The implementation follows existing patterns and maintains code quality. Good job on adding this functionality while keeping the component structure clean and organized.


15-16: ⚠️ Potential issue

Remove unused import 'CheckCheck' from 'lucide-react'.

The CheckCheck icon is imported but not used in the component. To keep the codebase clean and optimize bundle size, it's best to remove unused imports.

Apply this diff to remove the unused import:

-import { CheckCheck, Loader2 } from "lucide-react";
+import { Loader2 } from "lucide-react";

Likely invalid or redundant comment.

Comment on lines +140 to +159
const handleNewEmailUpdate = async () => {
setLoading(true);
await updateEmail(
{ newEmail },
{
onError(error) {
setLoading(false);
if (error) return toast.error(error.message);
return toast.error(
"Something went wrong sending the verification link.",
);
},
onSuccess() {
setLoading(false);
toast.success("Verification link sent to your email.");
setSendForVerification(true);
},
},
);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance error handling in handleNewEmailUpdate.

The current error handling in handleNewEmailUpdate is generic. Consider providing more specific error messages based on the error type or code returned by the API.

Here's a suggested improvement:

const handleNewEmailUpdate = async () => {
  setLoading(true);
  await updateEmail(
    { newEmail },
    {
      onError(error: any) {
        setLoading(false);
        if (error.data?.code === "INVALID_EMAIL") {
          return toast.error("The provided email is invalid. Please check and try again.");
        } else if (error.data?.code === "EMAIL_IN_USE") {
          return toast.error("This email is already in use. Please use a different email.");
        }
        return toast.error(
          "Something went wrong sending the verification link.",
        );
      },
      onSuccess() {
        setLoading(false);
        toast.success("Verification link sent to your email.");
        setSendForVerification(true);
      },
    },
  );
};

This change provides more specific error messages based on potential API error codes, improving the user experience.

@NiallJoeMaher
Copy link
Contributor

Your package.lock is different than the main repo, so it should probably be undone, and make sure you are installing with the correct node.js version. And just in case it fails the check after that make sure you run npm run prettier:fix to ensure you don't get stopped at the next hurdle. Once the checks are passing I'll approve and merge .

}
}
}
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be a breaking change (I think it's one too many) would you unstage this file (I think Praveen on Discord left the command).

@vercel
Copy link

vercel bot commented Oct 10, 2024

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

Name Status Preview Comments Updated (UTC)
codu ✅ Ready (Inspect) Visit Preview 💬 Add feedback Oct 10, 2024 7:49am

Copy link
Contributor

@NiallJoeMaher NiallJoeMaher left a comment

Choose a reason for hiding this comment

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

LGTM! Great work! 🌮 🦖

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.

Allow user to update email

2 participants