-
Notifications
You must be signed in to change notification settings - Fork 0
Add Server Action for User Creation and Frontend Wallet Connect Integration #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…a wallet connection
WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
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.
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 unit tests
Comment |
There was a problem hiding this 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
Schema tweak (requires a migration that enables citext and alters column type):
- email String? @unique + email String? @unique @db.Citextprisma/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
📒 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.
…mline user creation logic
There was a problem hiding this 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/secretsConsistently 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 errorsOnly 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 identitiesDecide 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
📒 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 updateThis path is clean and resilient for wallet-first sign-in.
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 |
There was a problem hiding this comment.
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.
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, | |
}); | |
} |
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; |
There was a problem hiding this comment.
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 omitspassword
. - 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.
There was a problem hiding this 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
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
Changes