Skip to content

Conversation

regenify
Copy link
Collaborator

@regenify regenify commented Sep 12, 2025

Description:
This PR adds a new server action to handle user creation via wallet address or email, updating the backend logic in action.ts. It also introduces frontend integration for wallet connection using RainbowKit, including a dedicated WalletConnectButton component. These changes enable users to register or log in with their wallet, and ensure seamless communication between the frontend and backend for user onboarding and authentication.

Summary by CodeRabbit

  • New Features

    • Sign in with your crypto wallet using a new wallet connect button.
    • First-time wallet sign-in automatically creates an account.
  • Changes

    • Email is now optional for accounts.
    • Wallet addresses are unique across accounts to prevent duplicates.
    • Login form updated to prioritize wallet-based sign-in while preserving existing methods.

Copy link

coderabbitai bot commented Sep 12, 2025

Walkthrough

Adds an optional walletAddress field and makes User.email optional in the Prisma schema and DB via two migrations. Adds a WalletConnectButton component that calls a new server action addUser(address, email?) to find-or-create users by walletAddress or email. Updates LoginForm to use the new component.

Changes

Cohort / File(s) Summary
Prisma migrations
prisma/migrations/20250912152754_add_walletaddress_to_user_model/migration.sql, prisma/migrations/20250912153358_update_user_model_email/migration.sql
Adds walletAddress TEXT column and a unique index User_walletAddress_key on public.User; makes email column nullable (DROP NOT NULL).
Prisma schema
prisma/schema.prisma
Updates User model: email changed from String @unique to String? @unique; adds walletAddress String? @unique.
Auth UI integration
src/components/LoginForm.tsx
Replaces direct RainbowKit ConnectButton usage with the new local WalletConnectButton component.
Wallet connect component
src/components/walletconnectbutton.tsx
New client component that renders RainbowKit ConnectButton; uses wagmi useAccount and a useEffect to call server action addUser(address) on connect; logs success/errors.
Server action
src/lib/action.ts
New server action export async function addUser(address: string, email?: string) (use server): normalizes address, validates inputs, upserts/finds user by walletAddress or email, updates lastLoginAt, handles race conditions, and returns selected user fields via Prisma.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant LF as LoginForm
  participant WCB as WalletConnectButton
  participant RK as RainbowKit ConnectButton
  participant WA as wagmi/useAccount
  participant SA as addUser (server)
  participant DB as Prisma/DB

  U->>LF: open login
  LF->>WCB: render WalletConnectButton
  WCB->>RK: render ConnectButton
  U->>RK: connect wallet
  RK-->>WA: wallet connected (address, isConnected)
  WA-->>WCB: address, isConnected
  Note over WCB: useEffect triggers on connect
  WCB->>SA: call addUser(address)
  SA->>DB: query by walletAddress
  alt found
    DB-->>SA: return existing user
  else not found
    opt email provided
      SA->>DB: query by email
      DB-->>SA: return user or null
    end
    alt none found
      SA->>DB: create user with walletAddress (and email?)
      DB-->>SA: new user
    end
  end
  SA-->>WCB: user
  WCB-->>LF: log success / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • tasin2610

A hop, I sniff the schema's seam,
Wallets sparkle in my dream.
ConnectButton hums, I bound and write,
Server burrows—finds me right.
Thump-thump, a user—new delight. 🐇✨

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title accurately and concisely summarizes the primary work—adding a server action for user creation and integrating frontend wallet connect—which matches the changes in src/lib/action.ts, the new WalletConnectButton, and LoginForm updates. It is specific, clear, and avoids vague or noisy phrasing. The title therefore reflects the main changes and is appropriate for the changeset.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/wallet-integration

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

Copy link

@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

🧹 Nitpick comments (4)
src/components/LoginForm.tsx (1)

16-16: Swap-in looks good; remove unused RainbowKit import.

You replaced the button correctly. The ConnectButton import on Line 15 is now unused; drop it to avoid lint errors.

prisma/schema.prisma (1)

18-19: Make identifier handling robust (address normalization, email case-insensitivity).

  • Store wallet addresses normalized (e.g., lowercase) at write-time to avoid duplicates by case and simplify matching.
  • Consider email as case-insensitive using Postgres CITEXT to prevent duplicates differing only by case.

Schema tweak (requires a migration that enables citext and alters column type):

-  email            String?    @unique
+  email            String?    @unique @db.Citext
prisma/migrations/20250912152754_add_walletaddress_to_user_model/migration.sql (1)

8-11: Consider enforcing on-database format for wallet addresses.

Add a CHECK constraint to prevent invalid values slipping in via other code paths. Example migration (follow-up):

ALTER TABLE "public"."User"
ADD CONSTRAINT "walletAddress_format_chk"
CHECK ("walletAddress" IS NULL OR "walletAddress" ~ '^0x[0-9a-fA-F]{40}$');
src/components/walletconnectbutton.tsx (1)

11-21: Avoid duplicate calls and verbose PII logging.

  • Debounce re-connect spam; only call once per address.
  • Log minimal fields to avoid printing user profiles in dev tools.

Apply this diff:

 export default function WalletConnectButton() {
 
-    const {address, isConnected} = useAccount();
+    const { address, isConnected } = useAccount();
+    const lastSyncedRef = useRef<string | null>(null);
 
-    useEffect(()=>{
-        if(isConnected && address){
-            addUser(address)
-            .then((user)=> 
-                console.log("User added/login",user))
-            .catch((err)=>{
-                console.error("Error adding user", err);
-            })
-        }
-    },
-    [address, isConnected]);
+    useEffect(() => {
+      if (!isConnected || !address) return;
+      if (lastSyncedRef.current === address) return;
+      lastSyncedRef.current = address;
+      void addUser(address)
+        .then((user) =>
+          console.log("User added/login", {
+            id: user.id,
+            email: user.email,
+            walletAddress: user.walletAddress,
+          }),
+        )
+        .catch((err) => {
+          console.error("Error adding user", err);
+          lastSyncedRef.current = null; // allow retry on failure
+        });
+    }, [address, isConnected]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 404c742 and e8e3ecc.

📒 Files selected for processing (6)
  • prisma/migrations/20250912152754_add_walletaddress_to_user_model/migration.sql (1 hunks)
  • prisma/migrations/20250912153358_update_user_model_email/migration.sql (1 hunks)
  • prisma/schema.prisma (1 hunks)
  • src/components/LoginForm.tsx (2 hunks)
  • src/components/walletconnectbutton.tsx (1 hunks)
  • src/lib/action.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/lib/action.ts (2)
src/lib/prisma.ts (1)
  • prisma (13-14)
src/lib/auth.ts (3)
  • createUser (16-30)
  • authenticateUser (40-60)
  • getUserByEmail (32-38)
src/components/LoginForm.tsx (1)
src/components/walletconnectbutton.tsx (1)
  • WalletConnectButton (7-24)
src/components/walletconnectbutton.tsx (1)
src/lib/action.ts (1)
  • addUser (5-37)
prisma/migrations/20250912153358_update_user_model_email/migration.sql (1)
src/lib/auth.ts (1)
  • getUserByEmail (32-38)
🔇 Additional comments (4)
src/components/LoginForm.tsx (1)

46-46: LGTM on WalletConnectButton usage.

Placement and usage are correct.

prisma/migrations/20250912153358_update_user_model_email/migration.sql (1)

1-2: LGTM.

Email nullability change aligns with the model update.

src/lib/action.ts (1)

11-26: Link wallet to existing email account and normalize addresses.

Current addUser finds by wallet then email but does not set walletAddress on an email-matched user; addresses are not normalized and walletAddress is unique in the schema (migration exists) — updating can conflict.

  • Fix: normalize address (e.g., address = address?.trim().toLowerCase()) before DB lookups; if a user is found by email and user.walletAddress is null and address is provided, check for an existing user with that walletAddress and, if none, update the email user's walletAddress via prisma.user.update.
  • Policy: confirm behavior when the provided walletAddress is already linked to a different user — reject (recommended), overwrite/merge, or other. Current suggestion rejects (do not overwrite existing links).

Location: src/lib/action.ts (addUser).

src/components/walletconnectbutton.tsx (1)

2-2: Verify Server Actions usage from a Client Component.

Directly importing a "use server" action into a "use client" component requires Server Actions to be enabled/configured in your Next.js version. Please confirm your Next config/Next version supports this pattern in your setup.

Copy link

@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: 4

♻️ Duplicate comments (1)
src/lib/action.ts (1)

12-24: Good: safe field selection; avoids leaking password/secrets

Consistently returning a whitelisted shape is the right approach for Server Actions. Thanks for addressing this.

🧹 Nitpick comments (2)
src/lib/action.ts (2)

48-56: Narrow error handling to unique-constraint race; don’t mask unrelated DB errors

Only recover on P2002, otherwise rethrow.

-        } catch (e) {
-            // Handle race: if unique constraint hit, re-fetch
-            const existing = await prisma.user.findUnique({
-                where: { walletAddress: normalizedAddress },
-                select: selectSafe,
-            });
-            if (existing) return existing;
-            throw e;
-        }
+        } catch (e) {
+            if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") {
+                const existing = await prisma.user.findUnique({
+                    where: { walletAddress: normalizedAddress },
+                    select: selectSafe,
+                });
+                if (existing) return existing;
+            }
+            throw e;
+        }

5-66: Plan for account linking/merges to avoid split identities

Decide the source of truth when both an email-only and a wallet-only account exist. Prefer a transactional merge (move walletAddress to the email user, migrate FKs, delete duplicate) or block linking and prompt the user.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8e3ecc and acaff5a.

📒 Files selected for processing (1)
  • src/lib/action.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/lib/action.ts (2)
src/lib/prisma.ts (1)
  • prisma (13-14)
src/lib/auth.ts (3)
  • createUser (16-30)
  • authenticateUser (40-60)
  • getUserByEmail (32-38)
🔇 Additional comments (1)
src/lib/action.ts (1)

27-38: LGTM: idempotent upsert with lastLoginAt update

This path is clean and resilient for wallet-first sign-in.

Comment on lines +5 to +66
export async function addUser(address: string, email?: string) {

const normalizedAddress = address ? address.trim().toLowerCase() : undefined;
if (!normalizedAddress && !email) {
throw new Error("Must provide wallet address or email");
}

const selectSafe = {
id: true,
email: true,
walletAddress: true,
role: true,
name: true,
image: true,
subscriptionType: true,
createdAt: true,
updatedAt: true,
lastLoginAt: true,
isActive: true,
} as const;

// Prefer wallet path when available (idempotent via upsert)
if (normalizedAddress) {
try {
const user = await prisma.user.upsert({
where: { walletAddress: normalizedAddress },
update: { lastLoginAt: new Date() },
create: {
walletAddress: normalizedAddress,
email: email ?? null,
lastLoginAt: new Date(),
},
select: selectSafe,
});
// Optionally link email if provided and not set yet
if (email && !user.email) {
return await prisma.user.update({
where: { id: user.id },
data: { email },
select: selectSafe,
});
}
return user;
} catch (e) {
// Handle race: if unique constraint hit, re-fetch
const existing = await prisma.user.findUnique({
where: { walletAddress: normalizedAddress },
select: selectSafe,
});
if (existing) return existing;
throw e;
}
}

// Email-only path (idempotent)
return prisma.user.upsert({
where: { email: email as string },
update: { lastLoginAt: new Date() },
create: { email: email as string },
select: selectSafe,
});
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enforce wallet ownership via signature (SIWE) — current flow lets clients spoof any address

Anyone can call this Server Action with an arbitrary address and gain/create an account. Require a signed message and verify it server-side (ideally with a nonce/expiry to prevent replay) before upserting by walletAddress.

Apply this minimal hardening (illustrative; wire a nonce challenge for real SIWE):

@@
-"use server";
+ "use server";
  import { prisma } from "./prisma";
+ import { verifyMessage } from "viem";
@@
-export async function addUser(address: string, email?: string) {
+export async function addUser(
+  address?: string,
+  email?: string,
+  proof?: { message: string; signature: `0x${string}` }
+) {
@@
-    // Prefer wallet path when available (idempotent via upsert)
-    if (normalizedAddress) {
+    // Prefer wallet path when available (idempotent via upsert)
+    if (normalizedAddress) {
+        // Verify wallet ownership (recommend SIWE w/ nonce to prevent replay)
+        if (!proof?.message || !proof?.signature) {
+            throw new Error("Signature required to link or sign in with a wallet");
+        }
+        const validSig = await verifyMessage({
+            address: normalizedAddress as `0x${string}`,
+            message: proof.message,
+            signature: proof.signature,
+        });
+        if (!validSig) throw new Error("Invalid signature");
📝 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 async function addUser(address: string, email?: string) {
const normalizedAddress = address ? address.trim().toLowerCase() : undefined;
if (!normalizedAddress && !email) {
throw new Error("Must provide wallet address or email");
}
const selectSafe = {
id: true,
email: true,
walletAddress: true,
role: true,
name: true,
image: true,
subscriptionType: true,
createdAt: true,
updatedAt: true,
lastLoginAt: true,
isActive: true,
} as const;
// Prefer wallet path when available (idempotent via upsert)
if (normalizedAddress) {
try {
const user = await prisma.user.upsert({
where: { walletAddress: normalizedAddress },
update: { lastLoginAt: new Date() },
create: {
walletAddress: normalizedAddress,
email: email ?? null,
lastLoginAt: new Date(),
},
select: selectSafe,
});
// Optionally link email if provided and not set yet
if (email && !user.email) {
return await prisma.user.update({
where: { id: user.id },
data: { email },
select: selectSafe,
});
}
return user;
} catch (e) {
// Handle race: if unique constraint hit, re-fetch
const existing = await prisma.user.findUnique({
where: { walletAddress: normalizedAddress },
select: selectSafe,
});
if (existing) return existing;
throw e;
}
}
// Email-only path (idempotent)
return prisma.user.upsert({
where: { email: email as string },
update: { lastLoginAt: new Date() },
create: { email: email as string },
select: selectSafe,
});
}
"use server";
import { prisma } from "./prisma";
import { verifyMessage } from "viem";
export async function addUser(
address?: string,
email?: string,
proof?: { message: string; signature: `0x${string}` }
) {
const normalizedAddress = address ? address.trim().toLowerCase() : undefined;
if (!normalizedAddress && !email) {
throw new Error("Must provide wallet address or email");
}
const selectSafe = {
id: true,
email: true,
walletAddress: true,
role: true,
name: true,
image: true,
subscriptionType: true,
createdAt: true,
updatedAt: true,
lastLoginAt: true,
isActive: true,
} as const;
// Prefer wallet path when available (idempotent via upsert)
if (normalizedAddress) {
// Verify wallet ownership (recommend SIWE w/ nonce to prevent replay)
if (!proof?.message || !proof?.signature) {
throw new Error("Signature required to link or sign in with a wallet");
}
const validSig = await verifyMessage({
address: normalizedAddress as `0x${string}`,
message: proof.message,
signature: proof.signature,
});
if (!validSig) throw new Error("Invalid signature");
try {
const user = await prisma.user.upsert({
where: { walletAddress: normalizedAddress },
update: { lastLoginAt: new Date() },
create: {
walletAddress: normalizedAddress,
email: email ?? null,
lastLoginAt: new Date(),
},
select: selectSafe,
});
// Optionally link email if provided and not set yet
if (email && !user.email) {
return await prisma.user.update({
where: { id: user.id },
data: { email },
select: selectSafe,
});
}
return user;
} catch (e) {
// Handle race: if unique constraint hit, re-fetch
const existing = await prisma.user.findUnique({
where: { walletAddress: normalizedAddress },
select: selectSafe,
});
if (existing) return existing;
throw e;
}
}
// Email-only path (idempotent)
return prisma.user.upsert({
where: { email: email as string },
update: { lastLoginAt: new Date() },
create: { email: email as string },
select: selectSafe,
});
}

Comment on lines +12 to +24
const selectSafe = {
id: true,
email: true,
walletAddress: true,
role: true,
name: true,
image: true,
subscriptionType: true,
createdAt: true,
updatedAt: true,
lastLoginAt: true,
isActive: true,
} as const;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify other Prisma user reads don’t return password to clients

Run this to find other calls and ensure they also use a safe select (e.g., src/lib/auth.ts returns full user).


🏁 Script executed:

#!/bin/bash
# List Prisma user operations for manual inspection (ensure select is used where data crosses trust boundaries)
rg -nP -C3 'prisma\.user\.(findUnique|findFirst|findMany|create|update|upsert)\s*\(' src

Length of output: 2492


Do not return user passwords — exclude password from all Prisma user responses

  • src/lib/auth.ts — prisma.user.create (around line 23): currently returns the created user (includes password). Add select: selectSafe or an explicit select that omits password.
  • src/lib/auth.ts — getUserByEmail (prisma.user.findUnique, around line 33): return only safe fields (use selectSafe or explicit field list).
  • src/lib/action.ts already uses selectSafe and needs no change.
🤖 Prompt for AI Agents
In src/lib/auth.ts around lines 23 and 33, the Prisma calls currently return
full user objects including the password; update prisma.user.create (around line
23) to pass select: selectSafe (or an explicit select that omits password) so
the created user response excludes the password, and update
prisma.user.findUnique / getUserByEmail (around line 33) to also use select:
selectSafe (or explicit safe fields) so fetched users never include the
password; src/lib/action.ts (lines 12–24) already defines selectSafe correctly
and requires no change.

Copy link
Contributor

@tasin2610 tasin2610 left a comment

Choose a reason for hiding this comment

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

There are a lot of changes we need to make but ill merge it for now and have a chat with you about our codebase structure tmrw @regenify

@tasin2610 tasin2610 merged commit b9abda0 into main Sep 14, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants