|
| 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 | +} |
0 commit comments