This commit is contained in:
Shreyaschorge 2025-07-10 21:25:55 +05:30
parent 96bb45ede0
commit 797c5b7154
No known key found for this signature in database
5 changed files with 495 additions and 103 deletions

View File

@ -0,0 +1,99 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '~/auth';
import { getNeynarClient } from '~/lib/neynar';
export async function GET(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.fid) {
return NextResponse.json(
{ error: 'No authenticated session found' },
{ status: 401 }
);
}
const { searchParams } = new URL(request.url);
const message = searchParams.get('message');
const signature = searchParams.get('signature');
if (!message || !signature) {
return NextResponse.json(
{ error: 'Message and signature are required' },
{ status: 400 }
);
}
const client = getNeynarClient();
const data = await client.fetchSigners({ message, signature });
const signers = data.signers;
// Fetch user data if signers exist
let user = null;
if (signers && signers.length > 0) {
try {
const userResponse = await fetch(
`${process.env.NEXTAUTH_URL}/api/users?fids=${signers[0].fid}`
);
if (userResponse.ok) {
const userDataResponse = await userResponse.json();
user = userDataResponse.users?.[0] || null;
}
} catch (error) {
console.error('Error fetching user data:', error);
}
}
return NextResponse.json({
signers,
user,
});
} catch (error) {
console.error('Error in session-signers API:', error);
return NextResponse.json(
{ error: 'Failed to fetch signers' },
{ status: 500 }
);
}
}
export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.fid) {
return NextResponse.json(
{ error: 'No authenticated session found' },
{ status: 401 }
);
}
const body = await request.json();
const { message, signature, signers, user } = body;
if (!message || !signature || !signers) {
return NextResponse.json(
{ error: 'Message, signature, and signers are required' },
{ status: 400 }
);
}
// Since we can't directly modify the session token here,
// we'll return the data and let the client trigger a session update
// The client will need to call getSession() to refresh the session
return NextResponse.json({
success: true,
message: 'Session data prepared for update',
signers,
user,
});
} catch (error) {
console.error('Error updating session signers:', error);
return NextResponse.json(
{ error: 'Failed to update session' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,46 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '~/auth';
export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.fid) {
return NextResponse.json(
{ error: 'No authenticated session found' },
{ status: 401 }
);
}
const body = await request.json();
const { signers, user } = body;
if (!signers || !user) {
return NextResponse.json(
{ error: 'Signers and user are required' },
{ status: 400 }
);
}
// For NextAuth to update the session, we need to trigger the JWT callback
// This is typically done by calling the session endpoint with updated data
// However, we can't directly modify the session token from here
// Instead, we'll store the data temporarily and let the client refresh the session
// The session will be updated when the JWT callback is triggered
return NextResponse.json({
success: true,
message: 'Session update prepared',
signers,
user,
});
} catch (error) {
console.error('Error preparing session update:', error);
return NextResponse.json(
{ error: 'Failed to prepare session update' },
{ status: 500 }
);
}
}

View File

@ -4,16 +4,198 @@ import { createAppClient, viemConnector } from '@farcaster/auth-client';
declare module 'next-auth' { declare module 'next-auth' {
interface Session { interface Session {
user: { provider?: string;
user?: {
fid: number; fid: number;
provider?: string; object?: 'user';
username?: string; username?: string;
display_name?: string;
pfp_url?: string;
custody_address?: string;
profile?: {
bio: {
text: string;
mentioned_profiles?: Array<{
object: 'user_dehydrated';
fid: number;
username: string;
display_name: string;
pfp_url: string;
custody_address: string;
}>;
mentioned_profiles_ranges?: Array<{
start: number;
end: number;
}>;
};
location?: {
latitude: number;
longitude: number;
address: {
city: string;
state: string;
country: string;
country_code: string;
};
};
};
follower_count?: number;
following_count?: number;
verifications?: string[];
verified_addresses?: {
eth_addresses: string[];
sol_addresses: string[];
primary: {
eth_address: string;
sol_address: string;
};
};
verified_accounts?: Array<Record<string, unknown>>;
power_badge?: boolean;
url?: string;
experimental?: {
neynar_user_score: number;
deprecation_notice: string;
};
score?: number;
}; };
signers?: {
object: 'signer';
signer_uuid: string;
public_key: string;
status: 'approved';
fid: number;
}[];
} }
interface User { interface User {
provider?: string; provider?: string;
username?: string; signers?: Array<{
object: 'signer';
signer_uuid: string;
public_key: string;
status: 'approved';
fid: number;
}>;
user?: {
object: 'user';
fid: number;
username: string;
display_name: string;
pfp_url: string;
custody_address: string;
profile: {
bio: {
text: string;
mentioned_profiles?: Array<{
object: 'user_dehydrated';
fid: number;
username: string;
display_name: string;
pfp_url: string;
custody_address: string;
}>;
mentioned_profiles_ranges?: Array<{
start: number;
end: number;
}>;
};
location?: {
latitude: number;
longitude: number;
address: {
city: string;
state: string;
country: string;
country_code: string;
};
};
};
follower_count: number;
following_count: number;
verifications: string[];
verified_addresses: {
eth_addresses: string[];
sol_addresses: string[];
primary: {
eth_address: string;
sol_address: string;
};
};
verified_accounts: Array<Record<string, unknown>>;
power_badge: boolean;
url?: string;
experimental?: {
neynar_user_score: number;
deprecation_notice: string;
};
score: number;
};
}
interface JWT {
provider?: string;
signers?: Array<{
object: 'signer';
signer_uuid: string;
public_key: string;
status: 'approved';
fid: number;
}>;
user?: {
object: 'user';
fid: number;
username: string;
display_name: string;
pfp_url: string;
custody_address: string;
profile: {
bio: {
text: string;
mentioned_profiles?: Array<{
object: 'user_dehydrated';
fid: number;
username: string;
display_name: string;
pfp_url: string;
custody_address: string;
}>;
mentioned_profiles_ranges?: Array<{
start: number;
end: number;
}>;
};
location?: {
latitude: number;
longitude: number;
address: {
city: string;
state: string;
country: string;
country_code: string;
};
};
};
follower_count: number;
following_count: number;
verifications: string[];
verified_addresses: {
eth_addresses: string[];
sol_addresses: string[];
primary: {
eth_address: string;
sol_address: string;
};
};
verified_accounts?: Array<Record<string, unknown>>;
power_badge?: boolean;
url?: string;
experimental?: {
neynar_user_score: number;
deprecation_notice: string;
};
score?: number;
};
} }
} }
@ -127,20 +309,15 @@ export const authOptions: AuthOptions = {
type: 'text', type: 'text',
placeholder: '0', placeholder: '0',
}, },
username: { signers: {
label: 'Username', label: 'Signers',
type: 'text', type: 'text',
placeholder: 'username', placeholder: 'JSON string of signers',
}, },
displayName: { user: {
label: 'Display Name', label: 'User Data',
type: 'text', type: 'text',
placeholder: 'Display Name', placeholder: 'JSON string of user data',
},
pfpUrl: {
label: 'Profile Picture URL',
type: 'text',
placeholder: 'https://...',
}, },
}, },
async authorize(credentials) { async authorize(credentials) {
@ -182,13 +359,11 @@ export const authOptions: AuthOptions = {
return { return {
id: fid.toString(), id: fid.toString(),
name:
credentials?.displayName ||
credentials?.username ||
`User ${fid}`,
image: credentials?.pfpUrl || null,
provider: 'neynar', provider: 'neynar',
username: credentials?.username || undefined, signers: credentials?.signers
? JSON.parse(credentials.signers)
: undefined,
user: credentials?.user ? JSON.parse(credentials.user) : undefined,
}; };
} catch (error) { } catch (error) {
console.error('Error in Neynar auth:', error); console.error('Error in Neynar auth:', error);
@ -199,18 +374,27 @@ export const authOptions: AuthOptions = {
], ],
callbacks: { callbacks: {
session: async ({ session, token }) => { session: async ({ session, token }) => {
if (session?.user) { // Set provider at the root level
session.user.fid = parseInt(token.sub ?? ''); session.provider = token.provider as string;
// Add provider information to session
session.user.provider = token.provider as string; if (token.provider === 'farcaster') {
session.user.username = token.username as string; // For Farcaster, simple structure
session.user = {
fid: parseInt(token.sub ?? ''),
};
} else if (token.provider === 'neynar') {
// For Neynar, use full user data structure from user
session.user = token.user as typeof session.user;
session.signers = token.signers as typeof session.signers;
} }
return session; return session;
}, },
jwt: async ({ token, user }) => { jwt: async ({ token, user }) => {
if (user) { if (user) {
token.provider = user.provider; token.provider = user.provider;
token.username = user.username; token.signers = user.signers;
token.user = user.user;
} }
return token; return token;
}, },

View File

@ -137,6 +137,36 @@ export function NeynarAuthButton() {
} }
}, []); }, []);
// Helper function to update session with signers (backend flow only)
const updateSessionWithSigners = useCallback(
async (
signers: StoredAuthState['signers'],
user: StoredAuthState['user']
) => {
if (!useBackendFlow) return;
try {
// For backend flow, we need to sign in again with the additional data
if (message && signature) {
const signInData = {
message,
signature,
redirect: false,
nonce: nonce || '',
fid: user?.fid?.toString() || '',
signers: JSON.stringify(signers),
user: JSON.stringify(user),
};
await backendSignIn('neynar', signInData);
}
} catch (error) {
console.error('❌ Error updating session with signers:', error);
}
},
[useBackendFlow, message, signature, nonce]
);
// Helper function to fetch user data from Neynar API // Helper function to fetch user data from Neynar API
const fetchUserData = useCallback( const fetchUserData = useCallback(
async (fid: number): Promise<User | null> => { async (fid: number): Promise<User | null> => {
@ -201,33 +231,51 @@ export function NeynarAuthButton() {
try { try {
setSignersLoading(true); setSignersLoading(true);
const response = await fetch( const endpoint = useBackendFlow
`/api/auth/signers?message=${encodeURIComponent( ? `/api/auth/session-signers?message=${encodeURIComponent(
message message
)}&signature=${signature}` )}&signature=${signature}`
); : `/api/auth/signers?message=${encodeURIComponent(
message
)}&signature=${signature}`;
const response = await fetch(endpoint);
const signerData = await response.json(); const signerData = await response.json();
if (response.ok) { if (response.ok) {
let user: StoredAuthState['user'] | null = null; if (useBackendFlow) {
// For backend flow, update session with signers
if (signerData.signers && signerData.signers.length > 0) {
const user =
signerData.user ||
(await fetchUserData(signerData.signers[0].fid));
await updateSessionWithSigners(signerData.signers, user);
}
return signerData.signers;
} else {
// For frontend flow, store in localStorage
let user: StoredAuthState['user'] | null = null;
if (signerData.signers && signerData.signers.length > 0) { if (signerData.signers && signerData.signers.length > 0) {
user = await fetchUserData(signerData.signers[0].fid); const fetchedUser = (await fetchUserData(
signerData.signers[0].fid
)) as StoredAuthState['user'];
user = fetchedUser;
}
// Store signers in localStorage, preserving existing auth data
const existingAuth = getItem<StoredAuthState>(STORAGE_KEY);
const updatedState: StoredAuthState = {
...existingAuth,
isAuthenticated: !!user,
signers: signerData.signers || [],
user,
};
setItem<StoredAuthState>(STORAGE_KEY, updatedState);
setStoredAuth(updatedState);
return signerData.signers;
} }
// Store signers in localStorage, preserving existing auth data
const existingAuth = getItem<StoredAuthState>(STORAGE_KEY);
const updatedState: StoredAuthState = {
...existingAuth,
isAuthenticated: !!user,
signers: signerData.signers || [],
user,
};
setItem<StoredAuthState>(STORAGE_KEY, updatedState);
setStoredAuth(updatedState);
return signerData.signers;
} else { } else {
console.error('❌ Failed to fetch signers'); console.error('❌ Failed to fetch signers');
// throw new Error('Failed to fetch signers'); // throw new Error('Failed to fetch signers');
@ -239,7 +287,7 @@ export function NeynarAuthButton() {
setSignersLoading(false); setSignersLoading(false);
} }
}, },
[] [useBackendFlow, fetchUserData, updateSessionWithSigners]
); );
// Helper function to poll signer status // Helper function to poll signer status
@ -305,26 +353,34 @@ export function NeynarAuthButton() {
generateNonce(); generateNonce();
}, []); }, []);
// Load stored auth state on mount // Load stored auth state on mount (only for frontend flow)
useEffect(() => { useEffect(() => {
const stored = getItem<StoredAuthState>(STORAGE_KEY); if (!useBackendFlow) {
if (stored && stored.isAuthenticated) { const stored = getItem<StoredAuthState>(STORAGE_KEY);
setStoredAuth(stored); if (stored && stored.isAuthenticated) {
setStoredAuth(stored);
}
} }
}, []); }, [useBackendFlow]);
// Success callback - this is critical! // Success callback - this is critical!
const onSuccessCallback = useCallback((res: unknown) => { const onSuccessCallback = useCallback(
const existingAuth = getItem<StoredAuthState>(STORAGE_KEY); (res: unknown) => {
const authState: StoredAuthState = { if (!useBackendFlow) {
isAuthenticated: true, // Only handle localStorage for frontend flow
user: res as StoredAuthState['user'], const existingAuth = getItem<StoredAuthState>(STORAGE_KEY);
signers: existingAuth?.signers || [], // Preserve existing signers const authState: StoredAuthState = {
}; isAuthenticated: true,
setItem<StoredAuthState>(STORAGE_KEY, authState); user: res as StoredAuthState['user'],
setStoredAuth(authState); signers: existingAuth?.signers || [], // Preserve existing signers
// setShowDialog(false); };
}, []); setItem<StoredAuthState>(STORAGE_KEY, authState);
setStoredAuth(authState);
}
// For backend flow, the session will be handled by NextAuth
},
[useBackendFlow]
);
// Error callback // Error callback
const onErrorCallback = useCallback((error?: Error | null) => { const onErrorCallback = useCallback((error?: Error | null) => {
@ -368,12 +424,6 @@ export function NeynarAuthButton() {
if (message && signature) { if (message && signature) {
const handleSignerFlow = async () => { const handleSignerFlow = async () => {
try { try {
// // Ensure we have message and signature
// if (!message || !signature) {
// console.error('❌ Missing message or signature');
// return;
// }
// Step 1: Change to loading state // Step 1: Change to loading state
setDialogStep('loading'); setDialogStep('loading');
setSignersLoading(true); setSignersLoading(true);
@ -403,10 +453,13 @@ export function NeynarAuthButton() {
setDebugState('Setting signer approval URL...'); setDebugState('Setting signer approval URL...');
setSignerApprovalUrl(signedKeyData.signer_approval_url); setSignerApprovalUrl(signedKeyData.signer_approval_url);
setSignersLoading(false); // Stop loading, show QR code setSignersLoading(false); // Stop loading, show QR code
if ( // Check if we're in a mobile context
context?.client?.platformType === 'mobile' && const clientContext = context?.client as Record<string, unknown>;
context?.client?.clientFid === FARCASTER_FID const isMobileContext =
) { clientContext?.platformType === 'mobile' &&
clientContext?.clientFid === FARCASTER_FID;
if (isMobileContext) {
setDebugState('Opening mobile app...'); setDebugState('Opening mobile app...');
setShowDialog(false); setShowDialog(false);
await sdk.actions.openUrl( await sdk.actions.openUrl(
@ -418,16 +471,11 @@ export function NeynarAuthButton() {
} else { } else {
setDebugState( setDebugState(
'Opening access dialog...' + 'Opening access dialog...' +
` ${context?.client?.platformType}` + ` ${clientContext?.platformType}` +
` ${context?.client?.clientFid}` ` ${clientContext?.clientFid}`
); );
setDialogStep('access'); setDialogStep('access');
setShowDialog(true); setShowDialog(true);
setDebugState(
'Opening access dialog...2' +
` ${dialogStep}` +
` ${showDialog}`
);
} }
// Step 4: Start polling for signer approval // Step 4: Start polling for signer approval
@ -514,15 +562,17 @@ export function NeynarAuthButton() {
if (useBackendFlow) { if (useBackendFlow) {
// Only sign out from NextAuth if the current session is from Neynar provider // Only sign out from NextAuth if the current session is from Neynar provider
if (session?.user?.provider === 'neynar') { if (session?.provider === 'neynar') {
await backendSignOut({ redirect: false }); await backendSignOut({ redirect: false });
} }
} else { } else {
// Frontend flow sign out
frontendSignOut(); frontendSignOut();
removeItem(STORAGE_KEY);
setStoredAuth(null);
} }
removeItem(STORAGE_KEY); // Common cleanup for both flows
setStoredAuth(null);
setShowDialog(false); setShowDialog(false);
setDialogStep('signin'); setDialogStep('signin');
setSignerApprovalUrl(null); setSignerApprovalUrl(null);
@ -543,15 +593,27 @@ export function NeynarAuthButton() {
} }
}, [useBackendFlow, frontendSignOut, pollingInterval, session]); }, [useBackendFlow, frontendSignOut, pollingInterval, session]);
// The key fix: match the original library's authentication logic exactly const authenticated = useBackendFlow
const authenticated = ? !!(
((isSuccess && validSignature) || storedAuth?.isAuthenticated) && session?.provider === 'neynar' &&
!!(storedAuth?.signers && storedAuth.signers.length > 0); session?.user?.fid &&
const userData = { session?.signers &&
fid: storedAuth?.user?.fid, session.signers.length > 0
username: storedAuth?.user?.username || '', )
pfpUrl: storedAuth?.user?.pfp_url || '', : ((isSuccess && validSignature) || storedAuth?.isAuthenticated) &&
}; !!(storedAuth?.signers && storedAuth.signers.length > 0);
const userData = useBackendFlow
? {
fid: session?.user?.fid,
username: session?.user?.username || '',
pfpUrl: session?.user?.pfp_url || '',
}
: {
fid: storedAuth?.user?.fid,
username: storedAuth?.user?.username || '',
pfpUrl: storedAuth?.user?.pfp_url || '',
};
// Show loading state while nonce is being fetched or signers are loading // Show loading state while nonce is being fetched or signers are loading
if (!nonce || signersLoading) { if (!nonce || signersLoading) {
@ -589,12 +651,15 @@ export function NeynarAuthButton() {
</> </>
) : ( ) : (
<> <>
<span>{debugState || 'Sign in with Neynar'}</span> <span>Sign in with Neynar</span>
</> </>
)} )}
</Button> </Button>
)} )}
<p>LocalStorage state</p>
{window && JSON.stringify(window.localStorage.getItem(STORAGE_KEY))}
{/* Unified Auth Dialog */} {/* Unified Auth Dialog */}
{ {
<AuthDialog <AuthDialog

View File

@ -105,7 +105,7 @@ export function SignIn() {
try { try {
setAuthState((prev) => ({ ...prev, signingOut: true })); setAuthState((prev) => ({ ...prev, signingOut: true }));
// Only sign out if the current session is from Farcaster provider // Only sign out if the current session is from Farcaster provider
if (session?.user?.provider === 'farcaster') { if (session?.provider === 'farcaster') {
await signOut({ redirect: false }); await signOut({ redirect: false });
} }
setSignInResult(undefined); setSignInResult(undefined);
@ -118,18 +118,16 @@ export function SignIn() {
return ( return (
<> <>
{/* Authentication Buttons */} {/* Authentication Buttons */}
{(status !== 'authenticated' || {(status !== 'authenticated' || session?.provider !== 'farcaster') && (
session?.user?.provider !== 'farcaster') && (
<Button onClick={handleSignIn} disabled={authState.signingIn}> <Button onClick={handleSignIn} disabled={authState.signingIn}>
Sign In with Farcaster Sign In with Farcaster
</Button> </Button>
)} )}
{status === 'authenticated' && {status === 'authenticated' && session?.provider === 'farcaster' && (
session?.user?.provider === 'farcaster' && ( <Button onClick={handleSignOut} disabled={authState.signingOut}>
<Button onClick={handleSignOut} disabled={authState.signingOut}> Sign out
Sign out </Button>
</Button> )}
)}
{/* Session Information */} {/* Session Information */}
{session && ( {session && (