Skip to content

Commit 7a6209b

Browse files
committed
Para to Supabase JWT auth
1 parent d580244 commit 7a6209b

4 files changed

Lines changed: 571 additions & 1 deletion

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { createClient } from '@supabase/supabase-js';
2+
import { SignJWT, jwtVerify, createRemoteJWKSet } from 'jose';
3+
import { NextRequest, NextResponse } from 'next/server';
4+
5+
// Define Para JWKS URLs based on environment
6+
const PARA_JWKS_URLS = {
7+
sandbox: 'https://api.sandbox.getpara.com/.well-known/jwks.json',
8+
beta: 'https://api.beta.getpara.com/.well-known/jwks.json',
9+
prod: 'https://api.getpara.com/.well-known/jwks.json',
10+
};
11+
12+
// Type for request body
13+
interface ExchangeRequestBody {
14+
paraJwt: string;
15+
}
16+
17+
// Type for Para JWT payload (based on your provided Para doc)
18+
interface ParaJwtPayload {
19+
data: {
20+
userId: string;
21+
wallets?: Array<{
22+
id: string;
23+
type: string;
24+
address: string;
25+
publicKey: string;
26+
}>;
27+
email?: string;
28+
phone?: string;
29+
telegramUserId?: string;
30+
farcasterUsername?: string;
31+
externalWalletAddress?: string;
32+
authType: string;
33+
identifier: string;
34+
oAuthMethod?: string;
35+
externalWallet?: {
36+
address: string;
37+
type: string;
38+
provider: string;
39+
};
40+
};
41+
iat: number;
42+
exp: number;
43+
sub: string;
44+
}
45+
46+
export async function POST(req: NextRequest) {
47+
try {
48+
const body: ExchangeRequestBody = await req.json();
49+
const { paraJwt } = body;
50+
51+
if (!paraJwt) {
52+
return NextResponse.json({ error: 'Missing Para JWT' }, { status: 400 });
53+
}
54+
55+
// Get JWKS URL based on environment
56+
const env = (process.env.PARA_ENVIRONMENT || 'prod') as keyof typeof PARA_JWKS_URLS;
57+
const jwksUrl = PARA_JWKS_URLS[env];
58+
const JWKS = createRemoteJWKSet(new URL(jwksUrl));
59+
60+
// Verify Para JWT
61+
const { payload } = await jwtVerify<ParaJwtPayload>(paraJwt, JWKS, {
62+
algorithms: ['RS256'], // Para likely uses RS256; confirm in docs if needed
63+
});
64+
65+
// Extract key user details
66+
const externalId = payload.data.userId;
67+
const email = payload.data.email || `${externalId}@para-fallback.com`; // Fallback if no email
68+
console.log('email', email);
69+
const walletData = payload.data.wallets?.[0] || null; // Use first wallet if available
70+
71+
// Initialize Supabase admin client
72+
const supabaseAdmin = createClient(
73+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
74+
process.env.SUPABASE_SERVICE_ROLE_KEY!,
75+
{ auth: { autoRefreshToken: false, persistSession: false } }
76+
);
77+
78+
let supabaseUser = null;
79+
80+
// Try to find user by email first (more reliable)
81+
try {
82+
const { data: users, error: listError } = await supabaseAdmin.auth.admin.listUsers();
83+
if (!listError && users.users) {
84+
supabaseUser = users.users.find((u) => u.email === email);
85+
if (supabaseUser) {
86+
console.log('Found existing user by email:', email);
87+
}
88+
}
89+
} catch (emailLookupError) {
90+
console.log('No user found by email:', email);
91+
}
92+
93+
// If not found by email, try to find by external_id in metadata
94+
if (!supabaseUser) {
95+
try {
96+
const { data: users, error: listError } = await supabaseAdmin.auth.admin.listUsers();
97+
if (!listError && users.users) {
98+
supabaseUser = users.users.find(
99+
(u) => u.user_metadata?.external_id === externalId
100+
);
101+
if (supabaseUser) {
102+
console.log('Found existing user by external_id:', externalId);
103+
}
104+
}
105+
} catch (listError) {
106+
console.log('Error listing users:', listError);
107+
}
108+
}
109+
110+
// If found by email, update the user's metadata to include external_id
111+
if (supabaseUser && !supabaseUser.user_metadata?.external_id) {
112+
try {
113+
const { error: updateError } = await supabaseAdmin.auth.admin.updateUserById(
114+
supabaseUser.id,
115+
{
116+
user_metadata: {
117+
...supabaseUser.user_metadata,
118+
external_id: externalId,
119+
para_auth_type: payload.data.authType,
120+
wallet_address: walletData?.address,
121+
wallet_type: walletData?.type,
122+
},
123+
}
124+
);
125+
if (updateError) {
126+
console.error('Error updating user metadata:', updateError);
127+
} else {
128+
console.log('Updated user metadata with Para info');
129+
}
130+
} catch (updateError) {
131+
console.error('Error updating user metadata:', updateError);
132+
}
133+
}
134+
135+
// Create user if not found by either method
136+
if (!supabaseUser) {
137+
try {
138+
const { data: newUser, error: createError } = await supabaseAdmin.auth.admin.createUser({
139+
email,
140+
email_confirm: true,
141+
user_metadata: {
142+
external_id: externalId,
143+
para_auth_type: payload.data.authType,
144+
wallet_address: walletData?.address,
145+
wallet_type: walletData?.type,
146+
},
147+
});
148+
if (createError) throw createError;
149+
supabaseUser = newUser.user;
150+
console.log('Created new user for Para:', externalId);
151+
} catch (createError: any) {
152+
// If user creation fails due to email already existing, try to get the user again
153+
if (createError.message?.includes('already been registered') || createError.code === 'email_exists') {
154+
console.log('User creation failed, trying to get existing user again');
155+
try {
156+
const { data: users, error: listError } = await supabaseAdmin.auth.admin.listUsers();
157+
if (!listError && users.users) {
158+
const existingUser = users.users.find((u) => u.email === email);
159+
if (existingUser) {
160+
supabaseUser = existingUser;
161+
console.log('Retrieved existing user after creation failure');
162+
} else {
163+
throw new Error('Failed to retrieve existing user');
164+
}
165+
} else {
166+
throw new Error('Failed to list users');
167+
}
168+
} catch (getError: any) {
169+
throw new Error(`User exists but cannot be retrieved: ${getError.message || 'Unknown error'}`);
170+
}
171+
} else {
172+
throw createError;
173+
}
174+
}
175+
}
176+
177+
if (!supabaseUser) {
178+
throw new Error('Failed to find or create user');
179+
}
180+
181+
const supabaseUserId = supabaseUser.id;
182+
183+
console.log('supabaseUser', supabaseUser);
184+
185+
// Sign Supabase JWT (HS256)
186+
const jwtSecret = new TextEncoder().encode(process.env.SUPABASE_JWT_SECRET!);
187+
const now = Math.floor(Date.now() / 1000);
188+
const supabaseJwt = await new SignJWT({
189+
sub: supabaseUserId,
190+
aud: 'authenticated',
191+
role: 'authenticated',
192+
email: supabaseUser.email,
193+
// Add custom claims if needed (e.g., from Para)
194+
para_user_id: externalId,
195+
iat: now,
196+
exp: now + 60 * 60, // 1 hour expiry; sync with Para's default 30min if preferred
197+
})
198+
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
199+
.sign(jwtSecret);
200+
201+
return NextResponse.json({ supabaseJwt }, { status: 200 });
202+
} catch (error: any) {
203+
console.error('Token exchange error:', error);
204+
return NextResponse.json({ error: error.message || 'Token exchange failed' }, { status: 500 });
205+
}
206+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use client';
2+
3+
import { useEffect, useRef } from 'react';
4+
import Onboarding from '@/components/Onboarding';
5+
import ConnectedWallet from '@/components/ConnectedWallet';
6+
import ParaIntegration from '@/components/ParaIntegration';
7+
import { useUnifiedConnection } from '@/hooks/useUnifiedConnection';
8+
9+
export default function HomePage() {
10+
// Unified connection status - trust the unified hook completely
11+
const { isConnected, address, isPara } = useUnifiedConnection();
12+
13+
// Only log significant connection changes to avoid spam
14+
const lastLoggedState = useRef<string | null>(null);
15+
useEffect(() => {
16+
const stateKey = `${isConnected}-${address}-${isPara}`;
17+
if (stateKey !== lastLoggedState.current) {
18+
console.log('🔗 [PAGE] Connection state:', {
19+
isConnected,
20+
hasAddress: !!address,
21+
isPara,
22+
address: address ? `${address.slice(0, 6)}...${address.slice(-4)}` : undefined
23+
});
24+
lastLoggedState.current = stateKey;
25+
}
26+
}, [isConnected, address, isPara]);
27+
28+
return (
29+
<>
30+
<ParaIntegration
31+
address={address}
32+
isPara={isPara}
33+
onConnect={() => {
34+
console.log('Para integration connected');
35+
}}
36+
/>
37+
</>
38+
);
39+
}

devconnect-app/src/components/Auth.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ export default function Auth({ children }: { children: React.ReactNode }) {
1818
}, []);
1919

2020
// Skip authentication
21-
const authSkipPaths = ['/map', '/quests', '/programme', '/scan', '/pos'];
21+
const authSkipPaths = [
22+
'/map',
23+
'/quests',
24+
'/programme',
25+
'/scan',
26+
'/pos',
27+
'/para',
28+
];
2229
if (authSkipPaths.includes(pathname)) {
2330
return children;
2431
}

0 commit comments

Comments
 (0)