feat: Add user profile menu with logout functionality
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 <noreply@anthropic.com>
This commit is contained in:
16
app/api/auth/logout/route.ts
Normal file
16
app/api/auth/logout/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
59
app/api/user/profile/route.ts
Normal file
59
app/api/user/profile/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
@@ -86,15 +87,18 @@ export default function ChatPage() {
|
||||
<Title order={2}>
|
||||
Ponderants Interview
|
||||
</Title>
|
||||
<Tooltip label="Start a new conversation">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={handleNewConversation}
|
||||
disabled={status === 'submitted' || status === 'streaming'}
|
||||
>
|
||||
New Conversation
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Group gap="md">
|
||||
<Tooltip label="Start a new conversation">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={handleNewConversation}
|
||||
disabled={status === 'submitted' || status === 'streaming'}
|
||||
>
|
||||
New Conversation
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<UserMenu />
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<ScrollArea
|
||||
|
||||
Reference in New Issue
Block a user