diff --git a/app/api/extensions/enable-banking/callback/__tests__/route.test.ts b/app/api/extensions/enable-banking/callback/__tests__/route.test.ts index bc8126b3..9b0a8a5b 100644 --- a/app/api/extensions/enable-banking/callback/__tests__/route.test.ts +++ b/app/api/extensions/enable-banking/callback/__tests__/route.test.ts @@ -91,12 +91,32 @@ describe('GET /api/extensions/enable-banking/callback', () => { expect(location).toContain('connection_id=conn-1') }) - it('redirects with error when bank returns error param', async () => { + it('redirects with error when bank returns error param (no state)', async () => { const response = await GET(makeRequest({ error: 'access_denied', error_description: 'User cancelled' })) expect(response.status).toBe(307) const location = response.headers.get('location') || '' expect(location).toContain('bank_error=User%20cancelled') + // No state → no DB cleanup attempted + expect(mockFrom).not.toHaveBeenCalled() + }) + + it('cleans up pending connection when bank returns error with state', async () => { + mockFrom.mockImplementation(() => + mockChain({ data: null, error: null }) + ) + + const response = await GET(makeRequest({ + error: 'access_denied', + error_description: 'Denied data sharing consent', + state: 'pending-state', + })) + + expect(response.status).toBe(307) + const location = response.headers.get('location') || '' + expect(location).toContain('bank_error=Denied%20data%20sharing%20consent') + // Should clean up the pending row + expect(mockFrom).toHaveBeenCalledWith('bank_connections') }) it('redirects with error when code or state is missing', async () => { diff --git a/app/api/extensions/enable-banking/callback/route.ts b/app/api/extensions/enable-banking/callback/route.ts index 3e6337d4..2dea897b 100644 --- a/app/api/extensions/enable-banking/callback/route.ts +++ b/app/api/extensions/enable-banking/callback/route.ts @@ -22,7 +22,44 @@ export async function GET(request: Request) { if (error) { const errorMessage = errorDescription || error - console.error('Bank authorization error:', errorMessage) + console.error('[enable-banking] Bank authorization denied', { + error, + error_description: errorDescription, + has_state: !!state, + }) + + // Clean up the pending bank_connections row so it doesn't accumulate + if (state) { + try { + const supabase = await createServiceClient() + + // Fetch connection details for logging before updating + const { data: pendingConn } = await supabase + .from('bank_connections') + .select('id, user_id, bank_name') + .eq('oauth_state', state) + .eq('status', 'pending') + .single() + + if (pendingConn) { + console.error('[enable-banking] Authorization denied details', { + connection_id: pendingConn.id, + user_id: pendingConn.user_id, + bank_name: pendingConn.bank_name, + error_code: error, + error_description: errorDescription, + }) + + await supabase + .from('bank_connections') + .update({ status: 'error', error_message: errorMessage, oauth_state: null }) + .eq('id', pendingConn.id) + } + } catch (cleanupError) { + console.error('[enable-banking] Failed to clean up pending bank connection:', cleanupError) + } + } + return NextResponse.redirect( `${baseUrl}/settings?bank_error=${encodeURIComponent(errorMessage)}` ) diff --git a/extensions/general/enable-banking/index.ts b/extensions/general/enable-banking/index.ts index a01c8254..c8f4470d 100644 --- a/extensions/general/enable-banking/index.ts +++ b/extensions/general/enable-banking/index.ts @@ -92,6 +92,35 @@ export const enableBankingExtension: Extension = { const psuType = companySettings?.entity_type === 'aktiebolag' ? 'business' : 'personal' + log.info('[enable-banking] Starting bank connection', { + user_id: user.id, + bank: aspsp_name, + country: aspsp_country, + entity_type: companySettings?.entity_type, + psu_type: psuType, + }) + + // Clean up any stale pending connections for this user+bank + // to avoid conflicts with the new authorization + const { data: staleConnections } = await supabase + .from('bank_connections') + .select('id') + .eq('user_id', user.id) + .eq('bank_name', aspsp_name) + .eq('status', 'pending') + + if (staleConnections && staleConnections.length > 0) { + log.info('[enable-banking] Cleaning up stale pending connections', { + count: staleConnections.length, + }) + await supabase + .from('bank_connections') + .update({ status: 'error', error_message: 'Superseded by new connection attempt', oauth_state: null }) + .eq('user_id', user.id) + .eq('bank_name', aspsp_name) + .eq('status', 'pending') + } + const redirectUrl = `${process.env.NEXT_PUBLIC_APP_URL}/api/extensions/enable-banking/callback` // Generate cryptographic state token for CSRF protection