mirror of
https://github.com/graphql-hive/console
synced 2026-05-23 17:18:23 +00:00
Split server-only and client-only code in authenticated-container.tsx (#609)
This commit is contained in:
parent
51cb4bb412
commit
6a0c77ae59
24 changed files with 108 additions and 92 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import { ReactElement } from 'react';
|
||||
import { gql, useQuery } from 'urql';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { TargetLayout } from '@/components/layouts';
|
||||
import { SchemaExplorerFilter } from '@/components/target/explorer/filter';
|
||||
import { GraphQLObjectTypeComponent } from '@/components/target/explorer/object-type';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { ReactElement } from 'react';
|
||||
import { DocumentType, gql, useQuery } from 'urql';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { TargetLayout } from '@/components/layouts';
|
||||
import {
|
||||
GraphQLEnumTypeComponent,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { VscBug, VscDiff, VscListFlat } from 'react-icons/vsc';
|
|||
import reactStringReplace from 'react-string-replace';
|
||||
import { useQuery } from 'urql';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { Label } from '@/components/common';
|
||||
import { TargetLayout } from '@/components/layouts';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { withSessionProtection } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import HistoryPage from '../history';
|
||||
|
||||
export const getServerSideProps = withSessionProtection();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ import { VscClose } from 'react-icons/vsc';
|
|||
import { gql, useMutation, useQuery } from 'urql';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { TargetLayout } from '@/components/layouts';
|
||||
import { MarkAsValid } from '@/components/target/history/MarkAsValid';
|
||||
import { Button, DataWrapper, GraphQLBlock, noSchema, Title } from '@/components/v2';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { ReactElement, useCallback, useState } from 'react';
|
|||
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||
import { GraphiQL } from 'graphiql';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { TargetLayout } from '@/components/layouts';
|
||||
import { Button, Title } from '@/components/v2';
|
||||
import { HiveLogo, Link2Icon } from '@/components/v2/icon';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { formatISO, subDays, subHours, subMinutes } from 'date-fns';
|
|||
import { VscChevronDown } from 'react-icons/vsc';
|
||||
import { useQuery } from 'urql';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { TargetLayout } from '@/components/layouts';
|
||||
import { OperationsFilterTrigger } from '@/components/target/operations/Filters';
|
||||
import { OperationsList } from '@/components/target/operations/List';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { useFormik } from 'formik';
|
|||
import { gql, useMutation, useQuery } from 'urql';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { TargetLayout } from '@/components/layouts';
|
||||
import { SchemaEditor } from '@/components/schema-editor';
|
||||
import { Button, Card, Checkbox, Heading, Input, Switch, Table, Tag, TimeAgo, Title } from '@/components/v2';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { ReactElement, useCallback, useState } from 'react';
|
||||
import { useMutation, useQuery } from 'urql';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { ProjectLayout } from '@/components/layouts';
|
||||
import { Button, Card, Checkbox, Heading, Table, Tag, Title } from '@/components/v2';
|
||||
import { CreateAlertModal, CreateChannelModal } from '@/components/v2/modals';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import NextLink from 'next/link';
|
|||
import clsx from 'clsx';
|
||||
import { useQuery } from 'urql';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { ProjectLayout } from '@/components/layouts';
|
||||
import { Activities, Badge, Button, Card, DropdownMenu, EmptyList, Heading, TimeAgo, Title } from '@/components/v2';
|
||||
import { LinkIcon, MoreIcon, SettingsIcon } from '@/components/v2/icon';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { useFormik } from 'formik';
|
|||
import { gql, useMutation, useQuery } from 'urql';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { ProjectLayout } from '@/components/layouts';
|
||||
import { ExternalCompositionSettings } from '@/components/project/settings/external-composition';
|
||||
import { Button, Card, Heading, Input, Link, Select, Spinner, Tag, Title } from '@/components/v2';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import NextLink from 'next/link';
|
|||
import { onlyText } from 'react-children-utilities';
|
||||
import { useQuery } from 'urql';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { OrganizationLayout } from '@/components/layouts';
|
||||
import { Activities, Button, Card, DropdownMenu, EmptyList, Heading, Skeleton, TimeAgo, Title } from '@/components/v2';
|
||||
import { getActivity } from '@/components/v2/activities';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { useFormik } from 'formik';
|
|||
import { DocumentType, gql, useMutation, useQuery } from 'urql';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { OrganizationLayout } from '@/components/layouts';
|
||||
import { Avatar, Button, Card, Checkbox, DropdownMenu, Input, Title } from '@/components/v2';
|
||||
import { CopyIcon, KeyIcon, MoreIcon, SettingsIcon, TrashIcon } from '@/components/v2/icon';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { useFormik } from 'formik';
|
|||
import { gql, useMutation, useQuery } from 'urql';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { OrganizationLayout } from '@/components/layouts';
|
||||
import { OIDCIntegrationSection } from '@/components/organization/settings/oidc-integration-section';
|
||||
import { Button, Card, Heading, Input, Spinner, Tag, Title } from '@/components/v2';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import dynamic from 'next/dynamic';
|
|||
import { Stat, StatHelpText, StatLabel, StatNumber } from '@chakra-ui/react';
|
||||
import { endOfMonth, startOfMonth } from 'date-fns';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { OrganizationLayout } from '@/components/layouts';
|
||||
import { BillingView } from '@/components/organization/billing/Billing';
|
||||
import { CurrencyFormatter } from '@/components/organization/billing/helpers';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { Stat, StatHelpText, StatLabel, StatNumber } from '@chakra-ui/react';
|
|||
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||
import { useMutation, useQuery } from 'urql';
|
||||
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { Section } from '@/components/common';
|
||||
import { DataWrapper, QueryError } from '@/components/common/DataWrapper';
|
||||
import { OrganizationLayout } from '@/components/layouts';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { DataWrapper } from '@/components/common/DataWrapper';
|
|||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
||||
import Cookies from 'cookies';
|
||||
import { LAST_VISITED_ORG_KEY } from '@/constants';
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
import { createTRPCClient } from '@trpc/client';
|
||||
import type { InternalApi } from '@hive/server';
|
||||
import { env } from '@/env/backend';
|
||||
|
|
@ -14,10 +15,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
|||
import Session from 'supertokens-node/recipe/session';
|
||||
import { writeLastVisitedOrganization } from '@/lib/cookies';
|
||||
|
||||
export async function getSuperTokensUserIdFromRequest(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
): Promise<string | null> {
|
||||
async function getSuperTokensUserIdFromRequest(req: NextApiRequest, res: NextApiResponse): Promise<string | null> {
|
||||
const session = await Session.getSession(req, res, { sessionRequired: false });
|
||||
return session?.getUserId() ?? null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import { Button } from '@chakra-ui/react';
|
|||
import { Title } from '@/components/common';
|
||||
import { DataWrapper } from '@/components/common/DataWrapper';
|
||||
import { useNotifications } from '@/lib/hooks/use-notifications';
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
const Center = tw.div`w-full h-full flex flex-row items-center justify-center`;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { VscChevronDown } from 'react-icons/vsc';
|
|||
import { AdminStats, Filters } from '@/components/admin/AdminStats';
|
||||
import { Page } from '@/components/common';
|
||||
import { DATE_RANGE_OPTIONS } from '@/components/common/TimeFilter';
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
function Manage() {
|
||||
const [last, setLast] = React.useState(30);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import * as Yup from 'yup';
|
|||
|
||||
import { Avatar, Button, SubHeader, Heading, Input, Tabs, Title } from '@/components/v2';
|
||||
import { MeDocument } from '@/graphql';
|
||||
import { authenticated, withSessionProtection } from '@/components/authenticated-container';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
||||
const UpdateMeMutation = gql(/* GraphQL */ `
|
||||
mutation updateMe($input: UpdateMeInput!) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import React from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { captureException } from '@sentry/nextjs';
|
||||
import { Header } from './v2';
|
||||
import { HiveStripeWrapper } from '@/lib/billing/stripe';
|
||||
import type { GetServerSideProps } from 'next';
|
||||
import { SessionContainerInterface } from 'supertokens-node/lib/build/recipe/session/types';
|
||||
import Session, { SessionAuth } from 'supertokens-auth-react/recipe/session';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
|
|
@ -57,66 +54,3 @@ export const authenticated =
|
|||
</AuthenticatedContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const serverSidePropsSessionHandling = async (context: Parameters<GetServerSideProps>[0]) => {
|
||||
const { backendConfig } = await import('@/config/supertokens/backend');
|
||||
const SupertokensNode = await import('supertokens-node');
|
||||
const Session = await import('supertokens-node/recipe/session');
|
||||
SupertokensNode.init(backendConfig());
|
||||
let session: SessionContainerInterface | undefined;
|
||||
|
||||
try {
|
||||
session = await Session.getSession(context.req, context.res, { sessionRequired: false });
|
||||
// TODO: better error decoding :)
|
||||
} catch (err: any) {
|
||||
// Check whether the email is already verified.
|
||||
// If it is not then we need to redirect to the email verification page - which will trigger the email sending.
|
||||
if (err.type === Session.Error.INVALID_CLAIMS) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/auth/verify-email',
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (err.type === Session.Error.TRY_REFRESH_TOKEN) {
|
||||
return { props: { fromSupertokens: 'needs-refresh' } };
|
||||
}
|
||||
captureException(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (session === undefined) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth?redirectToPath=${encodeURIComponent(context.resolvedUrl)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function defaultHandler() {
|
||||
return Promise.resolve({ props: {} });
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for protecting a server side props function with session handling.
|
||||
* Redirects user to the login page in case there is no session.
|
||||
*/
|
||||
export function withSessionProtection(handlerFn: GetServerSideProps = defaultHandler) {
|
||||
const getServerSideProps: GetServerSideProps = async context => {
|
||||
const result = await serverSidePropsSessionHandling(context);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return handlerFn(context);
|
||||
};
|
||||
|
||||
return getServerSideProps;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { superTokensNextWrapper } from 'supertokens-node/nextjs';
|
|||
import { verifySession } from 'supertokens-node/recipe/session/framework/express';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { backendConfig } from '@/config/supertokens/backend';
|
||||
import { SessionContainerInterface } from 'supertokens-node/lib/build/recipe/session/types';
|
||||
import type { SessionContainerInterface } from 'supertokens-node/lib/build/recipe/session/types';
|
||||
|
||||
supertokens.init(backendConfig());
|
||||
|
||||
|
|
|
|||
66
packages/web/app/src/lib/supertokens/guard.ts
Normal file
66
packages/web/app/src/lib/supertokens/guard.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { captureException } from '@sentry/nextjs';
|
||||
import type { GetServerSideProps } from 'next';
|
||||
import type { SessionContainerInterface } from 'supertokens-node/lib/build/recipe/session/types';
|
||||
|
||||
export const serverSidePropsSessionHandling = async (context: Parameters<GetServerSideProps>[0]) => {
|
||||
const { backendConfig } = await import('@/config/supertokens/backend');
|
||||
const SupertokensNode = await import('supertokens-node');
|
||||
const Session = await import('supertokens-node/recipe/session');
|
||||
SupertokensNode.init(backendConfig());
|
||||
let session: SessionContainerInterface | undefined;
|
||||
|
||||
try {
|
||||
session = await Session.getSession(context.req, context.res, { sessionRequired: false });
|
||||
// TODO: better error decoding :)
|
||||
} catch (err: any) {
|
||||
// Check whether the email is already verified.
|
||||
// If it is not then we need to redirect to the email verification page - which will trigger the email sending.
|
||||
if (err.type === Session.Error.INVALID_CLAIMS) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/auth/verify-email',
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (err.type === Session.Error.TRY_REFRESH_TOKEN) {
|
||||
return { props: { fromSupertokens: 'needs-refresh' } };
|
||||
}
|
||||
captureException(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (session === undefined) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth?redirectToPath=${encodeURIComponent(context.resolvedUrl)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function defaultHandler() {
|
||||
return Promise.resolve({ props: {} });
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for protecting a server side props function with session handling.
|
||||
* Redirects user to the login page in case there is no session.
|
||||
*/
|
||||
export function withSessionProtection(handlerFn: GetServerSideProps = defaultHandler) {
|
||||
const getServerSideProps: GetServerSideProps = async context => {
|
||||
const result = await serverSidePropsSessionHandling(context);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return handlerFn(context);
|
||||
};
|
||||
|
||||
return getServerSideProps;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { env } from '@/env/backend';
|
||||
import { TypeProvider } from 'supertokens-node/recipe/thirdpartyemailpassword';
|
||||
import type { TypeProvider } from 'supertokens-node/recipe/thirdpartyemailpassword';
|
||||
import zod from 'zod';
|
||||
|
||||
type OktaConfig = Exclude<typeof env['auth']['okta'], null>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue