From 0924d8f3924cc20bb32a1ff40d2a4cf5499e4d3a Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Fri, 12 Apr 2024 20:10:02 +0200 Subject: [PATCH] Use invite page for handling redirects Signed-off-by: Dinika Saxena --- Dockerfile | 2 +- src/app/api/invite/route.ts | 162 ------------------------------- src/app/invite/page.tsx | 12 +++ src/components/Invites/api.ts | 31 ++++++ src/components/Invites/index.tsx | 58 +++++++++++ src/components/Invites/utils.ts | 60 ++++++++++++ src/middleware.ts | 2 +- 7 files changed, 163 insertions(+), 164 deletions(-) delete mode 100644 src/app/api/invite/route.ts create mode 100644 src/app/invite/page.tsx create mode 100644 src/components/Invites/api.ts create mode 100644 src/components/Invites/index.tsx create mode 100644 src/components/Invites/utils.ts diff --git a/Dockerfile b/Dockerfile index ef856ec8f..7c341f6ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,7 +57,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 8000 + ENV PORT 8000 -ENV HOSTNAME "0.0.0.0" CMD ["node", "server.js"] diff --git a/src/app/api/invite/route.ts b/src/app/api/invite/route.ts deleted file mode 100644 index 5991337bc..000000000 --- a/src/app/api/invite/route.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* eslint-disable no-param-reassign */ -import { getServerSession } from 'next-auth/next'; -import { NextRequest, NextResponse } from 'next/server'; -import { captureException } from '@sentry/nextjs'; -import { Session } from 'next-auth'; -import { NextURL } from 'next/dist/server/web/next-url'; -import { authOptions } from '@/auth'; -import { - InviteData, - InviteErrorCodes, - InviteResponse, - isVlmInviteResponse, -} from '@/types/virtual-lab/invites'; -import { VlmError, isVlmError } from '@/types/virtual-lab/common'; - -const errorPath = '/'; -const projectPath = (labId: string, projectId: string) => - `/virtual-lab/lab/${labId}/project/${projectId!}/home`; -const labPath = (labId: string) => `/virtual-lab/lab/${labId}/lab`; - -export async function GET(req: NextRequest): Promise { - const inviteToken = req.nextUrl.searchParams.get('token'); - const url = req.nextUrl.clone(); - - // eslint-disable-next-line no-console - console.log('URL basepath when request is received', url); - - url.searchParams.delete('token'); - - const session = await getServerSession(authOptions); - if (!session?.accessToken) { - return NextResponse.redirect(getErrorUrl(url, null, session, null)); - } - - if (!inviteToken) { - return NextResponse.redirect(getErrorUrl(url, null, session, inviteToken ?? null)); - } - - const response = await processInvite(session?.accessToken, inviteToken); - if (!isVlmInviteResponse(response)) { - return NextResponse.redirect(getErrorUrl(url, response, session, inviteToken)); - } - - switch (response.data.origin) { - case 'Lab': - return NextResponse.redirect(getLabUrl(url, response.data)); - case 'Project': - return NextResponse.redirect(getProjectUrl(url, response.data)); - default: - captureException( - new Error( - `User ${session.user.username} could not accept invite ${inviteToken} because unknown origin returned by server` - ), - { extra: response.data.origin } - ); - return NextResponse.redirect(getErrorUrl(url, response, session, inviteToken)); - } -} - -const getProjectUrl = (url: NextURL, vlmData: InviteData): NextURL => { - const { status, virtual_lab_id: labId, project_id: projectId, origin } = vlmData; - if (status === 'already_accepted') { - url.pathname = errorPath; - url.searchParams.set('errorcode', `${InviteErrorCodes.INVITE_ALREADY_ACCEPTED}`); - url.searchParams.set('origin', origin); - url.searchParams.set('lab_id', labId); - url.searchParams.set('project_id', projectId!); - return url; - } - - url.pathname = projectPath(labId, projectId!); - url.searchParams.set('invite_accepted', 'true'); - return url; -}; - -const getLabUrl = (url: NextURL, vlmData: InviteData): NextURL => { - const { status, virtual_lab_id: labId, origin } = vlmData; - if (status === 'already_accepted') { - url.pathname = errorPath; - url.searchParams.set('errorcode', `${InviteErrorCodes.INVITE_ALREADY_ACCEPTED}`); - url.searchParams.set('origin', origin); - url.searchParams.set('lab_id', labId); - return url; - } - - url.pathname = labPath(labId); - url.searchParams.set('invite_accepted', 'true'); - return url; -}; - -const getErrorUrl = ( - url: NextURL, - response: VlmError | any, - session: Session | null, - inviteToken: string | null -): NextURL => { - url.pathname = errorPath; - - if (!session?.accessToken) { - url.searchParams.set('errorcode', `${InviteErrorCodes.UNAUTHORIZED}`); - return url; - } - - if (!inviteToken) { - url.searchParams.set('errorcode', `${InviteErrorCodes.INVALID_LINK}`); - return url; - } - - if (isVlmError(response)) { - captureException(new Error(`User invite could not be accepted because of VLM Error`), { - extra: { vliError: response, username: session?.user?.name, invite: inviteToken }, - }); - if (response.error_code === 'AUTHORIZATION_ERROR') { - url.searchParams.set('errorcode', `${InviteErrorCodes.UNAUTHORIZED}`); - return url; - } - - if (response.error_code === 'TOKEN_EXPIRED') { - url.searchParams.set('errorcode', `${InviteErrorCodes.TOKEN_EXPIRED}`); - return url; - } - - // eslint-disable-next-line no-console - console.log('URL basepath at time of redirection', url); - url.searchParams.set('errorcode', `${InviteErrorCodes.UNKNOWN}`); - return url; - } - - captureException(new Error(`User invite could not be accepted because of Unknown error`), { - extra: { error: response, username: session?.user?.name, invite: inviteToken }, - }); - - url.searchParams.set('errorcode', `${InviteErrorCodes.UNKNOWN}`); - return url; -}; - -const processInvite = async ( - sessionToken: string, - inviteToken: string -): Promise => { - return fetch(`http://localhost:8000/invites?token=${inviteToken}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - Authorization: `Bearer ${sessionToken}`, - }, - }) - .then((response) => { - // Valid response or client errors (40X) - return response.json(); - }) - .catch((err) => { - // Server errors (50X) - // eslint-disable-next-line no-console - console.error(err); - captureException( - new Error(`User could not accept invite ${inviteToken} because of an unknown error`) - ); - return { error_code: 'INTERNAL_SERVER_ERROR', message: 'Vlm server is down' } as VlmError; - }); -}; diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx new file mode 100644 index 000000000..d70af2ec4 --- /dev/null +++ b/src/app/invite/page.tsx @@ -0,0 +1,12 @@ +import { ErrorBoundary } from 'react-error-boundary'; + +import SimpleErrorComponent from '@/components/GenericErrorFallback'; +import InviteLoader from '@/components/Invites'; + +export default function InvitePage() { + return ( + + + + ); +} diff --git a/src/components/Invites/api.ts b/src/components/Invites/api.ts new file mode 100644 index 000000000..6e32d7aa1 --- /dev/null +++ b/src/components/Invites/api.ts @@ -0,0 +1,31 @@ +import { captureException } from '@sentry/nextjs'; + +import { InviteResponse } from '@/types/virtual-lab/invites'; +import { VlmError } from '@/types/virtual-lab/common'; +import { virtualLabApi } from '@/config'; + +export const processInvite = async ( + sessionToken: string, + inviteToken: string +): Promise => { + return fetch(`${virtualLabApi.url}/invites?token=${inviteToken}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Bearer ${sessionToken}`, + }, + }) + .then((response) => { + // Valid response or client errors (40X) + return response.json(); + }) + .catch((err) => { + // Server errors (50X) + captureException( + new Error(`User could not accept invite ${inviteToken} because of an unknown error`), + { extra: err } + ); + return { error_code: 'INTERNAL_SERVER_ERROR', message: 'Vlm server is down' } as VlmError; + }); +}; diff --git a/src/components/Invites/index.tsx b/src/components/Invites/index.tsx new file mode 100644 index 000000000..0a3c5746a --- /dev/null +++ b/src/components/Invites/index.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { LoadingOutlined } from '@ant-design/icons'; +import { Spin } from 'antd'; +import { useAtomValue } from 'jotai'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect } from 'react'; +import { captureException } from '@sentry/nextjs'; + +import { processInvite } from './api'; +import { getErrorUrl, getLabUrl, getProjectUrl } from './utils'; +import sessionAtom from '@/state/session'; +import { isVlmInviteResponse } from '@/types/virtual-lab/invites'; + +export default function InviteLoader() { + const session = useAtomValue(sessionAtom); + const inviteToken = useSearchParams().get('token'); + const router = useRouter(); + + useEffect(() => { + if (!session?.accessToken) { + return router.push(getErrorUrl(null, session?.accessToken, inviteToken)); + } + if (!inviteToken) { + return router.push(getErrorUrl(null, session?.accessToken, inviteToken)); + } + + processInvite(session.accessToken, inviteToken).then((response) => { + if (!isVlmInviteResponse(response)) { + router.push(getErrorUrl(response, session?.accessToken, inviteToken)); + return; + } + + switch (response.data.origin) { + case 'Lab': + router.push(getLabUrl(response.data)); + return; + case 'Project': + router.push(getProjectUrl(response.data)); + return; + default: + captureException( + new Error( + `User could not accept invite ${inviteToken} because unknown origin returned by server` + ), + { extra: response.data.origin } + ); + router.push(getErrorUrl(response, session?.accessToken, inviteToken)); + } + }); + }, [session, inviteToken, router]); + + return ( +
+ } /> +
+ ); +} diff --git a/src/components/Invites/utils.ts b/src/components/Invites/utils.ts new file mode 100644 index 000000000..392a22a25 --- /dev/null +++ b/src/components/Invites/utils.ts @@ -0,0 +1,60 @@ +import { captureException } from '@sentry/nextjs'; + +import { InviteData, InviteErrorCodes } from '@/types/virtual-lab/invites'; +import { VlmError, isVlmError } from '@/types/virtual-lab/common'; + +const errorPath = '/'; +const projectPath = (labId: string, projectId: string) => + `/virtual-lab/lab/${labId}/project/${projectId!}/home`; +const labPath = (labId: string) => `/virtual-lab/lab/${labId}/lab`; + +export const getLabUrl = (vlmData: InviteData): string => { + const { status, virtual_lab_id: labId, origin } = vlmData; + if (status === 'already_accepted') { + return `${errorPath}?errorcode=${InviteErrorCodes.INVITE_ALREADY_ACCEPTED}&origin=${origin}&lab_id=${labId}`; + } + + return `${labPath(labId)}?invite_accepted=true`; +}; + +export const getProjectUrl = (vlmData: InviteData): string => { + const { status, virtual_lab_id: labId, project_id: projectId, origin } = vlmData; + if (status === 'already_accepted') { + return `${errorPath}?errorcode=${InviteErrorCodes.INVITE_ALREADY_ACCEPTED}&origin=${origin}&lab_id=${labId}&project_id=${projectId}`; + } + + return `${projectPath(labId, projectId!)}?invite_accepted=true`; +}; + +export const getErrorUrl = ( + response: VlmError | any, + accessToken?: string, + inviteToken?: string | null +): string => { + if (!accessToken) { + return `${errorPath}?errorcode=${InviteErrorCodes.UNAUTHORIZED}`; + } + if (!inviteToken) { + return `${errorPath}?errorcode=${InviteErrorCodes.INVALID_LINK}`; + } + if (isVlmError(response)) { + captureException(new Error(`User invite could not be accepted because of VLM Error`), { + extra: { vliError: response, invite: inviteToken }, + }); + + if (response.error_code === 'AUTHORIZATION_ERROR') { + return `${errorPath}?errorcode=${InviteErrorCodes.UNAUTHORIZED}`; + } + + if (response.error_code === 'TOKEN_EXPIRED') { + return `${errorPath}?errorcode=${InviteErrorCodes.TOKEN_EXPIRED}`; + } + return `${errorPath}?errorcode=${InviteErrorCodes.UNKNOWN}`; + } + + captureException(new Error(`User invite could not be accepted because of Unknown error`), { + extra: { error: response, invite: inviteToken }, + }); + + return `${errorPath}?errorcode=${InviteErrorCodes.UNKNOWN}`; +}; diff --git a/src/middleware.ts b/src/middleware.ts index dddaad376..85a5d06e7 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -32,7 +32,7 @@ export const config = { matcher: [ '/', '/main', - '/api/invite', + '/invite', '/(build|simulate|simulations|main|explore|experiment-designer|svc|virtual-lab)/(.*)', ], };