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() {