diff --git a/apps/backend/src/envelopPlugins.ts b/apps/backend/src/envelopPlugins.ts index eb88199d..0d7c3f60 100644 --- a/apps/backend/src/envelopPlugins.ts +++ b/apps/backend/src/envelopPlugins.ts @@ -17,8 +17,10 @@ import { } from '@envelop/generic-auth' import { makeExecutableSchema } from '@graphql-tools/schema' import { User } from '@prisma/client' +import { EnumValueNode } from 'graphql' import { TokenExpiredError } from 'jsonwebtoken' +import { Role } from './api/graphql/generated/graphql' import resolvers from './api/graphql/resolvers/resolvers' import { schema } from './api/graphql/typeDefs' import { createContext, GraphqlServerContext, prisma } from './context' @@ -48,11 +50,31 @@ const resolveUserFn: ResolveUserFn = async ( } } -const validateUserFn: ValidateUserFn = ({ user }) => { +const validateUserFn: ValidateUserFn = ({ + user, + fieldAuthDirectiveNode, +}) => { if (!user) { throw new EnvelopError('request not authenticated', { code: 'NOT_AUTHENTICATED', }) + } else { + if (!fieldAuthDirectiveNode?.arguments) { + return + } + + const valueNode = fieldAuthDirectiveNode.arguments.find( + (arg) => arg.name.value === 'role' + )?.value as EnumValueNode | undefined + + if (valueNode) { + const role = valueNode.value as Role + if (role !== user.role) { + throw new EnvelopError('request not authorized', { + code: 'NOT_AUTHORIZED', + }) + } + } } } diff --git a/apps/frontend/jest.setup.ts b/apps/frontend/jest.setup.ts index a4877e7f..013a65e4 100644 --- a/apps/frontend/jest.setup.ts +++ b/apps/frontend/jest.setup.ts @@ -4,6 +4,8 @@ import '@testing-library/jest-dom/extend-expect' import { resetFactoryIds } from '~/mocks/factories/factory' import { server } from '~/mocks/server' +globalThis.IS_REACT_ACT_ENVIRONMENT = true + process.env = { ...process.env, NEXT_PUBLIC_GRAPHQL_END_POINT: 'http://localhost:4000/dev/graphql', diff --git a/apps/frontend/package.json b/apps/frontend/package.json index d89d640f..3ba262a7 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -24,9 +24,10 @@ "@radix-ui/react-portal": "0.1.4", "graphql": "15.8.0", "graphql-anywhere": "4.2.7", - "next": "12.0.10", - "react": "17.0.2", - "react-dom": "17.0.2", + "next": "12.1.4", + "react": "18.0.0", + "react-dom": "18.0.0", + "react-error-boundary": "^3.1.4", "react-hook-form": "7.22.5", "spinners-react": "1.0.6", "styled-components": "5.3.3", @@ -53,7 +54,7 @@ "@graphql-typed-document-node/core": "3.1.1", "@next/bundle-analyzer": "12.0.10", "@testing-library/jest-dom": "5.16.2", - "@testing-library/react": "12.1.3", + "@testing-library/react": "^13.0.0", "@types/jest": "27.4.1", "@types/react": "17.0.39", "@types/react-dom": "17.0.13", diff --git a/apps/frontend/src/components/util/UrqlClientProvider.tsx b/apps/frontend/src/components/util/UrqlClientProvider.tsx index 47a1551f..b0d2ac4b 100644 --- a/apps/frontend/src/components/util/UrqlClientProvider.tsx +++ b/apps/frontend/src/components/util/UrqlClientProvider.tsx @@ -11,7 +11,12 @@ import { fetchExchange, Provider as UrqlProvider, } from 'urql' -import { pipe, tap } from 'wonka' +import { delay, pipe, tap } from 'wonka' + +const fakeSlowNetworkExchange: Exchange = + ({ forward }) => + (ops$) => + pipe(ops$, delay(1000), forward) const authCheckExchange: Exchange = ({ forward }) => @@ -57,6 +62,7 @@ const exchanges = [ cacheExchange, ...(process.env.NODE_ENV !== 'test' ? [debugExchange] : []), authCheckExchange, + fakeSlowNetworkExchange, fetchExchange, ] @@ -89,6 +95,7 @@ export function UrqlClientProvider({ authorization: token ? `Bearer ${token}` : '', }, }), + suspense: true, }) ) } diff --git a/apps/frontend/src/contexts/currentUser.tsx b/apps/frontend/src/contexts/currentUser.tsx index 9fd8415e..b25baf5a 100644 --- a/apps/frontend/src/contexts/currentUser.tsx +++ b/apps/frontend/src/contexts/currentUser.tsx @@ -8,7 +8,7 @@ import { gql } from '~/generated' type CurrentUserModel = Omit< ReturnType, - 'auth0Loading' + 'auth0Loading' | 'getProfileFetching' > const CurrentUserContext = createContext(undefined as any) @@ -53,6 +53,7 @@ function useCurrentUserInternal() { return { currentUser: res.data?.getProfile, + getProfileFetching: res.fetching, auth0Loading: isLoading, isOnboarded, isAuthenticated, @@ -68,9 +69,9 @@ export function CurrentUserProvider({ const value = useCurrentUserInternal() const router = useRouter() - const { auth0Loading, ...rest } = value + const { auth0Loading, getProfileFetching, ...rest } = value - if (auth0Loading) { + if (auth0Loading || getProfileFetching) { return } diff --git a/apps/frontend/src/pages/all-todos/AllTodoList.tsx b/apps/frontend/src/pages/all-todos/AllTodoList.tsx new file mode 100644 index 00000000..6aaf4fd6 --- /dev/null +++ b/apps/frontend/src/pages/all-todos/AllTodoList.tsx @@ -0,0 +1,41 @@ +import { useQuery } from 'urql' + +import { gql } from '~/generated' + +const GetAllTodos = gql(/* GraphQL */ ` + query GetAllTodos { + allTodos { + id + createdAt + updatedAt + title + content + completed + author { + name + } + } + } +`) + +export function AllTodoList() { + const [res] = useQuery({ query: GetAllTodos }) + + if (res.error) { + throw new Error( + res.error.graphQLErrors.map((error) => error.message).toString() + ) + } + + return ( +
+ {res.data && res.data.allTodos.length < 1 &&

No Items

} + {res.data?.allTodos.map((todo) => ( +
+
{todo.title}
+

{todo.author?.name}

+
+ ))} +
+ ) +} diff --git a/apps/frontend/src/pages/all-todos/index.page.tsx b/apps/frontend/src/pages/all-todos/index.page.tsx index e951a081..cfef75fb 100644 --- a/apps/frontend/src/pages/all-todos/index.page.tsx +++ b/apps/frontend/src/pages/all-todos/index.page.tsx @@ -1,28 +1,12 @@ -import { useQuery } from 'urql' +import { Suspense } from 'react' +import { ErrorBoundary } from 'react-error-boundary' import { Spinner } from '~/components' import { DevNote } from '~/components/general/DevNote' -import { gql } from '~/generated' -const GetAllTodos = gql(/* GraphQL */ ` - query GetAllTodos { - allTodos { - id - createdAt - updatedAt - title - content - completed - author { - name - } - } - } -`) +import { AllTodoList } from './AllTodoList' function AllTodos() { - const [res] = useQuery({ query: GetAllTodos }) - return (

Todos

@@ -35,17 +19,13 @@ function AllTodos() { admin role user to see - -
- {res.fetching && } - {res.data && res.data.allTodos.length < 1 &&

No Items

} - {res.data?.allTodos.map((todo) => ( -
-
{todo.title}
-

{todo.author?.name}

-
- ))} -
+
{JSON.stringify(props)}
} + > + }> + + +
) } diff --git a/apps/frontend/src/pages/signin/index.page.tsx b/apps/frontend/src/pages/signin/index.page.tsx index 3ff898d8..3aa7bca5 100644 --- a/apps/frontend/src/pages/signin/index.page.tsx +++ b/apps/frontend/src/pages/signin/index.page.tsx @@ -1,5 +1,4 @@ import { useAuth0 } from '@auth0/auth0-react' -import Link from 'next/link' import { Button } from '~/components' import { Role } from '~/generated/graphql' @@ -20,9 +19,6 @@ function SignIn() {
if you don't have an account? then - {/* - sign up - */} diff --git a/apps/frontend/src/pages/todos/CreateTodoModal.tsx b/apps/frontend/src/pages/todos/CreateTodoModal.tsx index f063ad44..cb3e3068 100644 --- a/apps/frontend/src/pages/todos/CreateTodoModal.tsx +++ b/apps/frontend/src/pages/todos/CreateTodoModal.tsx @@ -32,9 +32,15 @@ export function CreateTodoModal() { formState: { errors }, } = useForm() const onSubmit: SubmitHandler = async (data) => { - executeMutation({ - todo: removeEmptyFields(data), - }) + try { + await executeMutation({ + todo: removeEmptyFields(data), + }) + window.alert('success!') + } catch (error) { + console.log(error) + window.alert('Failed to create todo') + } } return ( @@ -50,11 +56,9 @@ export function CreateTodoModal() { {errors.title && title is required}