From b31d686e8397b13cb03383b18ad2f103ee5917f5 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sat, 25 Jul 2026 23:38:36 +0000
Subject: [PATCH] fix: auth + wallet linking for Gmail/X sign-in users
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Create users table (migration) for Privy-authenticated accounts
- Fix /api/access to upsert into users table with correct id field
- Route /api/user/wallet GET/POST/DELETE to users table for Privy users,
access_codes for legacy invite-code users
- Add missing DELETE /api/user/wallet handler (Unlink button was broken)
- Remove Luca chat gate — anyone logged in can chat immediately,
wallet linking is optional for agent-scoped context
Co-Authored-By: Claude
---
src/app/api/access/route.ts | 9 ++++-
src/app/api/user/wallet/route.ts | 40 ++++++++++++++++---
src/app/dashboard/luca/page.tsx | 9 ++---
.../migrations/20260725000003_users_table.sql | 18 +++++++++
4 files changed, 64 insertions(+), 12 deletions(-)
create mode 100644 supabase/migrations/20260725000003_users_table.sql
diff --git a/src/app/api/access/route.ts b/src/app/api/access/route.ts
index 4fef23d9..535e123b 100644
--- a/src/app/api/access/route.ts
+++ b/src/app/api/access/route.ts
@@ -23,8 +23,13 @@ export async function POST(request: Request) {
try {
const supabase = getSupabaseAdminClient();
await supabase.from("users").upsert(
- { privy_id: userId, email: body.email || null, x_handle: body.xHandle || null, last_seen_at: new Date().toISOString() },
- { onConflict: "privy_id", ignoreDuplicates: false },
+ {
+ id: `privy:${userId}`,
+ email: body.email || null,
+ x_handle: body.xHandle || null,
+ last_seen_at: new Date().toISOString(),
+ },
+ { onConflict: "id", ignoreDuplicates: false },
);
} catch { /* non-fatal */ }
diff --git a/src/app/api/user/wallet/route.ts b/src/app/api/user/wallet/route.ts
index e7b122df..4cba59df 100644
--- a/src/app/api/user/wallet/route.ts
+++ b/src/app/api/user/wallet/route.ts
@@ -9,17 +9,30 @@ async function getCodeId(): Promise {
return verifyAccessToken(token);
}
+function isPrivyUser(codeId: string) {
+ return codeId.startsWith("privy:");
+}
+
export async function GET() {
const codeId = await getCodeId();
if (!codeId) return NextResponse.json({ wallet: null });
const supabase = getSupabaseAdminClient();
+
+ if (isPrivyUser(codeId)) {
+ const { data } = await supabase
+ .from("users")
+ .select("wallet")
+ .eq("id", codeId)
+ .maybeSingle();
+ return NextResponse.json({ wallet: data?.wallet ?? null });
+ }
+
const { data } = await supabase
.from("access_codes")
.select("wallet")
.eq("id", codeId)
.maybeSingle();
-
return NextResponse.json({ wallet: data?.wallet ?? null });
}
@@ -35,10 +48,27 @@ export async function POST(request: Request) {
}
const supabase = getSupabaseAdminClient();
- await supabase
- .from("access_codes")
- .update({ wallet })
- .eq("id", codeId);
+
+ if (isPrivyUser(codeId)) {
+ await supabase.from("users").update({ wallet }).eq("id", codeId);
+ } else {
+ await supabase.from("access_codes").update({ wallet }).eq("id", codeId);
+ }
+
+ return NextResponse.json({ ok: true });
+}
+
+export async function DELETE() {
+ const codeId = await getCodeId();
+ if (!codeId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+
+ const supabase = getSupabaseAdminClient();
+
+ if (isPrivyUser(codeId)) {
+ await supabase.from("users").update({ wallet: null }).eq("id", codeId);
+ } else {
+ await supabase.from("access_codes").update({ wallet: null }).eq("id", codeId);
+ }
return NextResponse.json({ ok: true });
}
diff --git a/src/app/dashboard/luca/page.tsx b/src/app/dashboard/luca/page.tsx
index ecc7a113..ffe5dd82 100644
--- a/src/app/dashboard/luca/page.tsx
+++ b/src/app/dashboard/luca/page.tsx
@@ -165,7 +165,7 @@ function LucaChat() {
}
const showEmpty = messages.length === 0;
- const isReady = Boolean(selectedSlug || wallet);
+ const isReady = true;
const greeting = (() => {
const h = new Date().getHours();
return h < 12 ? "Good morning" : h < 18 ? "Good afternoon" : "Good evening";
@@ -280,8 +280,7 @@ function LucaChat() {
What should I read for you today?
- Luca answers from your agent's attributed books only — every figure carries its period and confidence, and missing data is called missing.
- {!isReady && " Link your wallet or select an agent to get started."}
+ Luca answers from attributed books only — every figure carries its period and confidence, and missing data is called missing.
{/* Capability grid — 3-col, 6px radius (no pills) */}
@@ -434,8 +433,8 @@ function LucaChat() {