From fc549debf284cf06f31276aed746dfb6d5f5df3e Mon Sep 17 00:00:00 2001 From: Amadeo Pellicce Date: Tue, 25 Feb 2025 16:28:54 +0100 Subject: [PATCH] reverting old callback (#281) * reverting old callback * to more recent version --- .../app/connect/(oauth)/callback/newpage.tsx | 35 +++++ .../web/app/connect/(oauth)/callback/page.tsx | 125 +++++++++++++++--- 2 files changed, 139 insertions(+), 21 deletions(-) create mode 100644 apps/web/app/connect/(oauth)/callback/newpage.tsx diff --git a/apps/web/app/connect/(oauth)/callback/newpage.tsx b/apps/web/app/connect/(oauth)/callback/newpage.tsx new file mode 100644 index 00000000..a122399f --- /dev/null +++ b/apps/web/app/connect/(oauth)/callback/newpage.tsx @@ -0,0 +1,35 @@ +'use client' + +import {useSearchParams} from 'next/navigation' +import {useEffect} from 'react' +import {FullScreenCenter} from '@/components/FullScreenCenter' + +/** + * Workaround for searchParams being empty on production. Will ahve to check + * @see https://github.com/vercel/next.js/issues/43077#issuecomment-1383742153 + */ +export const dynamic = 'force-dynamic' + +/** https://beta.nextjs.org/docs/api-reference/file-conventions/page#searchparams-optional */ +export default function OAuthCallback() { + const searchParams = useSearchParams() + const code = searchParams.get('code') + const state = searchParams.get('state') + + useEffect(() => { + if ( + code && + state && + Buffer.from(state, 'base64').toString('utf8').startsWith('conn_') + ) { + // Just close the window - parent that opens this in a popup after redirect will read params directly + window.close() + } + }, [code, state]) + + return ( + + Processing authentication... + + ) +} diff --git a/apps/web/app/connect/(oauth)/callback/page.tsx b/apps/web/app/connect/(oauth)/callback/page.tsx index a122399f..3f260f13 100644 --- a/apps/web/app/connect/(oauth)/callback/page.tsx +++ b/apps/web/app/connect/(oauth)/callback/page.tsx @@ -1,35 +1,118 @@ -'use client' - -import {useSearchParams} from 'next/navigation' -import {useEffect} from 'react' +import '@openint/app-config/register.node' +import {cookies} from 'next/headers' +import {redirect} from 'next/navigation' +import {kAccessToken} from '@openint/app-config/constants' +import {envRequired} from '@openint/app-config/env' +import type {Id} from '@openint/cdk' +import {initNangoSDK, NangoConnect} from '@openint/cdk' +import type {FrameMessage} from '@openint/connect' import {FullScreenCenter} from '@/components/FullScreenCenter' +import {serverSideHelpersFromViewer} from '@/lib-server' +import {serverComponentGetViewer} from '@/lib-server/server-component-helpers' +import {kConnectSession, zConnectSession} from '../../shared' +import {CallbackEffect} from './CallbackEffect' + +export const metadata = { + title: 'OpenInt Oauth Callback', +} /** * Workaround for searchParams being empty on production. Will ahve to check - * @see https://github.com/vercel/next.js/issues/43077#issuecomment-1383742153 - */ +@@ -23,104 +11,25 @@ export const metadata = { export const dynamic = 'force-dynamic' /** https://beta.nextjs.org/docs/api-reference/file-conventions/page#searchparams-optional */ -export default function OAuthCallback() { - const searchParams = useSearchParams() - const code = searchParams.get('code') - const state = searchParams.get('state') - - useEffect(() => { - if ( - code && - state && - Buffer.from(state, 'base64').toString('utf8').startsWith('conn_') - ) { - // Just close the window - parent that opens this in a popup after redirect will read params directly - window.close() +export default async function ConnectCallback({ + searchParams, +}: { + // Only accessible in PageComponent rather than layout component + // @see https://github.com/vercel/next.js/issues/43704 + searchParams: Record +}) { + // TODO: Can we use cookies-next to read cookie in this environment? + const cookie = cookies().get(kConnectSession) + if (!cookie) { + console.warn('No cookie found, redirecting to openint') + // Temporary hack to redirect to the right place to accomodate for oauth url not fully changed yet + const url = new URL('https://app.venice.is/connect/callback') + for (const [key, value] of Object.entries(searchParams)) { + url.searchParams.append(key, value as string) + } + return redirect(url.toString()) + } + const msg = await (async (): Promise => { + try { + const res = await NangoConnect.doOauthCallback(searchParams) + if (!res) { + // This means that we are using the @nango/frontend websocket client... + return null + } + if (!cookie) { + return { + type: 'ERROR', + data: {code: 'BAD_REQUEST', message: 'No session found'}, + } + } + if (res.eventType !== 'AUTHORIZATION_SUCEEDED') { + return { + type: 'ERROR', + data: {code: res.data.authErrorType, message: res.data.authErrorDesc}, + } + } + const session = zConnectSession.parse(JSON.parse(cookie.value)) + const viewer = await serverComponentGetViewer({ + searchParams: {[kAccessToken]: session.token}, + }) + const connectionId = res.data.connectionId as Id['conn'] + if (session.connectionId !== connectionId) { + console.warn('Revoking due to unmatched connectionId') + const nango = initNangoSDK({ + headers: {authorization: `Bearer ${envRequired.NANGO_SECRET_KEY}`}, + }) + await nango.DELETE('/connection/{connectionId}', { + params: { + path: {connectionId: res.data.connectionId}, + query: {provider_config_key: res.data.providerConfigKey}, + }, + }) + return { + type: 'ERROR', + data: { + code: 'FORBIDDEN', + message: `Session connectionId (${session.connectionId}) not matching connected connectionId ${connectionId}`, + }, + } + } + const {caller} = serverSideHelpersFromViewer(viewer) + await caller.postConnect([res.data, res.data.providerConfigKey, {}]) + return { + type: 'SUCCESS', + data: {connectionId: res.data.connectionId as Id['conn']}, + } + } catch (err) { + console.error('[oauth] Error during connect', err) + return { + type: 'ERROR', + data: {code: 'INTERNAL_SERVER_ERROR', message: `${err}`}, + } } - }, [code, state]) + })() + console.log('[oauth] callback result', msg) + // How do we do redirect here? return ( - Processing authentication... + {msg && ( + <> + {msg.type} + + {msg.type === 'ERROR' + ? `[${msg.data.code}] ${msg.data.message}` + : msg.data.connectionId} + + + )} + ) }