Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:

- A unique constraint covering the columns `[walletAddress]` on the table `User` will be added. If there are existing duplicate values, this will fail.

*/
-- AlterTable
ALTER TABLE "public"."User" ADD COLUMN "walletAddress" TEXT;

-- CreateIndex
CREATE UNIQUE INDEX "User_walletAddress_key" ON "public"."User"("walletAddress");
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "public"."User" ALTER COLUMN "email" DROP NOT NULL;
3 changes: 2 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ datasource db {

model User {
id String @id @default(uuid())
email String @unique
walletAddress String? @unique
email String? @unique
name String?
password String?
role Role @default(USER)
Expand Down
3 changes: 2 additions & 1 deletion src/components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import GoogleSignInButton from "@/components/GoogleSignInButton";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import WalletConnectButton from "./walletconnectbutton";

export default function LoginForm() {
const [email, setEmail] = useState("");
Expand Down Expand Up @@ -42,7 +43,7 @@ export default function LoginForm() {
<div className="flex flex-col justify-center space-y-3">
<GoogleSignInButton />
<div className="flex justify-center">
<ConnectButton />
<WalletConnectButton/>
</div>
</div>
<div className="relative">
Expand Down
24 changes: 24 additions & 0 deletions src/components/walletconnectbutton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";
import { addUser } from "@/lib/action";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { useEffect } from "react";
import { useAccount } from "wagmi";

export default function WalletConnectButton() {

const {address, isConnected} = useAccount();

useEffect(()=>{
if(isConnected && address){
addUser(address)
.then((user)=>
console.log("User added/login",user))
.catch((err)=>{
console.error("Error adding user", err);
})
}
},
[address, isConnected]);

return <ConnectButton />;
}
66 changes: 66 additions & 0 deletions src/lib/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use server";
import { prisma } from "./prisma";


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;
Comment on lines +12 to +24
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.


// 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,
});
}
Comment on lines +5 to +66
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,
});
}