From 2b3cd0c99bb3e2e0248eeb779d11419c9dabbcfd Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 9 Nov 2025 05:11:54 +0000 Subject: [PATCH] feat: Add user profile menu with logout functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented user profile display in the chat interface: - Created UserMenu component with avatar, handle display, and dropdown - Added /api/user/profile endpoint to fetch user data from Bluesky - Added /api/auth/logout endpoint to clear auth cookie - Integrated UserMenu into chat page header - Shows user's ATproto avatar with initials fallback - Dropdown menu displays user info and logout option - Fixed JWT import to use verifySurrealJwt 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/api/auth/logout/route.ts | 16 ++++++ app/api/user/profile/route.ts | 59 +++++++++++++++++++++ app/chat/page.tsx | 22 ++++---- components/UserMenu.tsx | 99 +++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 app/api/auth/logout/route.ts create mode 100644 app/api/user/profile/route.ts create mode 100644 components/UserMenu.tsx diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..e309078 --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +/** + * POST /api/auth/logout + * + * Logs out the current user by clearing the auth cookie + */ +export async function POST() { + const cookieStore = await cookies(); + + // Clear the auth cookie + cookieStore.delete('ponderants-auth'); + + return NextResponse.json({ success: true }); +} diff --git a/app/api/user/profile/route.ts b/app/api/user/profile/route.ts new file mode 100644 index 0000000..5560ae6 --- /dev/null +++ b/app/api/user/profile/route.ts @@ -0,0 +1,59 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { verifySurrealJwt } from '@/lib/auth/jwt'; + +/** + * GET /api/user/profile + * + * Returns the current user's profile information from ATproto + */ +export async function GET() { + try { + // Check authentication + const cookieStore = await cookies(); + const authCookie = cookieStore.get('ponderants-auth'); + + if (!authCookie) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // Verify JWT and get DID + const payload = verifySurrealJwt(authCookie.value); + if (!payload) { + return NextResponse.json({ error: 'Invalid token' }, { status: 401 }); + } + const did = payload.did; + + // Fetch the user's profile using the public Bluesky API + const response = await fetch( + `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${did}` + ); + + if (!response.ok) { + console.error('[Profile API] Failed to fetch profile:', response.statusText); + // Return basic info if full profile fetch fails + return NextResponse.json({ + did, + handle: did, // fallback to DID + displayName: null, + avatar: null, + }); + } + + const profile = await response.json(); + + return NextResponse.json({ + did: profile.did, + handle: profile.handle, + displayName: profile.displayName || null, + avatar: profile.avatar || null, + description: profile.description || null, + }); + } catch (error) { + console.error('[Profile API] Error:', error); + return NextResponse.json( + { error: 'Failed to fetch profile' }, + { status: 500 } + ); + } +} diff --git a/app/chat/page.tsx b/app/chat/page.tsx index a661f3e..7757f29 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -17,6 +17,7 @@ import { } from '@mantine/core'; import { useRef, useState, useEffect } from 'react'; import { MicrophoneRecorder } from '@/components/MicrophoneRecorder'; +import { UserMenu } from '@/components/UserMenu'; export default function ChatPage() { const viewport = useRef(null); @@ -86,15 +87,18 @@ export default function ChatPage() { Ponderants Interview - - - + + + + + + (null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Fetch user profile on mount + fetch('/api/user/profile') + .then((res) => res.json()) + .then((data) => { + if (!data.error) { + setProfile(data); + } + }) + .catch((error) => { + console.error('Failed to fetch profile:', error); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + const handleLogout = async () => { + try { + await fetch('/api/auth/logout', { method: 'POST' }); + router.push('/login'); + } catch (error) { + console.error('Logout failed:', error); + } + }; + + if (loading || !profile) { + return ( + + ? + + ); + } + + // Get display name or handle + const displayText = profile.displayName || profile.handle; + // Get initials for fallback + const initials = profile.displayName + ? profile.displayName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2) + : profile.handle.slice(0, 2).toUpperCase(); + + return ( + + + + + + {initials} + + + + + + + + + {displayText} + + + @{profile.handle} + + + + + Log out + + + + ); +}