|
1 |
| -'use client' |
2 |
| - |
3 |
| -import {useSearchParams} from 'next/navigation' |
4 |
| -import {useEffect} from 'react' |
| 1 | +import '@openint/app-config/register.node' |
| 2 | +import {cookies} from 'next/headers' |
| 3 | +import {redirect} from 'next/navigation' |
| 4 | +import {kAccessToken} from '@openint/app-config/constants' |
| 5 | +import {envRequired} from '@openint/app-config/env' |
| 6 | +import type {Id} from '@openint/cdk' |
| 7 | +import {initNangoSDK, NangoConnect} from '@openint/cdk' |
| 8 | +import type {FrameMessage} from '@openint/connect' |
5 | 9 | import {FullScreenCenter} from '@/components/FullScreenCenter'
|
| 10 | +import {serverSideHelpersFromViewer} from '@/lib-server' |
| 11 | +import {serverComponentGetViewer} from '@/lib-server/server-component-helpers' |
| 12 | +import {kConnectSession, zConnectSession} from '../../shared' |
| 13 | +import {CallbackEffect} from './CallbackEffect' |
| 14 | + |
| 15 | +export const metadata = { |
| 16 | + title: 'OpenInt Oauth Callback', |
| 17 | +} |
6 | 18 |
|
7 | 19 | /**
|
8 | 20 | * Workaround for searchParams being empty on production. Will ahve to check
|
9 |
| - * @see https://github.com/vercel/next.js/issues/43077#issuecomment-1383742153 |
10 |
| - */ |
| 21 | +@@ -23,104 +11,25 @@ export const metadata = { |
11 | 22 | export const dynamic = 'force-dynamic'
|
12 | 23 |
|
13 | 24 | /** https://beta.nextjs.org/docs/api-reference/file-conventions/page#searchparams-optional */
|
14 |
| -export default function OAuthCallback() { |
15 |
| - const searchParams = useSearchParams() |
16 |
| - const code = searchParams.get('code') |
17 |
| - const state = searchParams.get('state') |
18 |
| - |
19 |
| - useEffect(() => { |
20 |
| - if ( |
21 |
| - code && |
22 |
| - state && |
23 |
| - Buffer.from(state, 'base64').toString('utf8').startsWith('conn_') |
24 |
| - ) { |
25 |
| - // Just close the window - parent that opens this in a popup after redirect will read params directly |
26 |
| - window.close() |
| 25 | +export default async function ConnectCallback({ |
| 26 | + searchParams, |
| 27 | +}: { |
| 28 | + // Only accessible in PageComponent rather than layout component |
| 29 | + // @see https://github.com/vercel/next.js/issues/43704 |
| 30 | + searchParams: Record<string, string | string[] | undefined> |
| 31 | +}) { |
| 32 | + // TODO: Can we use cookies-next to read cookie in this environment? |
| 33 | + const cookie = cookies().get(kConnectSession) |
| 34 | + if (!cookie) { |
| 35 | + console.warn('No cookie found, redirecting to openint') |
| 36 | + // Temporary hack to redirect to the right place to accomodate for oauth url not fully changed yet |
| 37 | + const url = new URL('https://app.venice.is/connect/callback') |
| 38 | + for (const [key, value] of Object.entries(searchParams)) { |
| 39 | + url.searchParams.append(key, value as string) |
| 40 | + } |
| 41 | + return redirect(url.toString()) |
| 42 | + } |
| 43 | + const msg = await (async (): Promise<FrameMessage | null> => { |
| 44 | + try { |
| 45 | + const res = await NangoConnect.doOauthCallback(searchParams) |
| 46 | + if (!res) { |
| 47 | + // This means that we are using the @nango/frontend websocket client... |
| 48 | + return null |
| 49 | + } |
| 50 | + if (!cookie) { |
| 51 | + return { |
| 52 | + type: 'ERROR', |
| 53 | + data: {code: 'BAD_REQUEST', message: 'No session found'}, |
| 54 | + } |
| 55 | + } |
| 56 | + if (res.eventType !== 'AUTHORIZATION_SUCEEDED') { |
| 57 | + return { |
| 58 | + type: 'ERROR', |
| 59 | + data: {code: res.data.authErrorType, message: res.data.authErrorDesc}, |
| 60 | + } |
| 61 | + } |
| 62 | + const session = zConnectSession.parse(JSON.parse(cookie.value)) |
| 63 | + const viewer = await serverComponentGetViewer({ |
| 64 | + searchParams: {[kAccessToken]: session.token}, |
| 65 | + }) |
| 66 | + const connectionId = res.data.connectionId as Id['conn'] |
| 67 | + if (session.connectionId !== connectionId) { |
| 68 | + console.warn('Revoking due to unmatched connectionId') |
| 69 | + const nango = initNangoSDK({ |
| 70 | + headers: {authorization: `Bearer ${envRequired.NANGO_SECRET_KEY}`}, |
| 71 | + }) |
| 72 | + await nango.DELETE('/connection/{connectionId}', { |
| 73 | + params: { |
| 74 | + path: {connectionId: res.data.connectionId}, |
| 75 | + query: {provider_config_key: res.data.providerConfigKey}, |
| 76 | + }, |
| 77 | + }) |
| 78 | + return { |
| 79 | + type: 'ERROR', |
| 80 | + data: { |
| 81 | + code: 'FORBIDDEN', |
| 82 | + message: `Session connectionId (${session.connectionId}) not matching connected connectionId ${connectionId}`, |
| 83 | + }, |
| 84 | + } |
| 85 | + } |
| 86 | + const {caller} = serverSideHelpersFromViewer(viewer) |
| 87 | + await caller.postConnect([res.data, res.data.providerConfigKey, {}]) |
| 88 | + return { |
| 89 | + type: 'SUCCESS', |
| 90 | + data: {connectionId: res.data.connectionId as Id['conn']}, |
| 91 | + } |
| 92 | + } catch (err) { |
| 93 | + console.error('[oauth] Error during connect', err) |
| 94 | + return { |
| 95 | + type: 'ERROR', |
| 96 | + data: {code: 'INTERNAL_SERVER_ERROR', message: `${err}`}, |
| 97 | + } |
27 | 98 | }
|
28 |
| - }, [code, state]) |
| 99 | + })() |
| 100 | + console.log('[oauth] callback result', msg) |
29 | 101 |
|
| 102 | + // How do we do redirect here? |
30 | 103 | return (
|
31 | 104 | <FullScreenCenter>
|
32 |
| - <span className="mb-2">Processing authentication...</span> |
| 105 | + {msg && ( |
| 106 | + <> |
| 107 | + <span className="mb-2">{msg.type} </span> |
| 108 | + <span className="mb-2"> |
| 109 | + {msg.type === 'ERROR' |
| 110 | + ? `[${msg.data.code}] ${msg.data.message}` |
| 111 | + : msg.data.connectionId} |
| 112 | + </span> |
| 113 | + </> |
| 114 | + )} |
| 115 | + <CallbackEffect msg={msg} autoClose={!msg} /> |
33 | 116 | </FullScreenCenter>
|
34 | 117 | )
|
35 | 118 | }
|
0 commit comments