From 7cf621b62d6a430d779f67549cf88f4097cb58f3 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 4 Jul 2024 14:53:03 +0200 Subject: [PATCH 1/5] RSC: Add auth support to ClientRouter --- packages/vite/src/ClientRouter.tsx | 55 +++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/ClientRouter.tsx b/packages/vite/src/ClientRouter.tsx index 8bd0920a24c2..e84f32a0cdba 100644 --- a/packages/vite/src/ClientRouter.tsx +++ b/packages/vite/src/ClientRouter.tsx @@ -3,9 +3,11 @@ import React, { useMemo } from 'react' +import type { GeneratedRoutesMap } from '@redwoodjs/router/dist/analyzeRoutes' import { analyzeRoutes } from '@redwoodjs/router/dist/analyzeRoutes' import { LocationProvider, useLocation } from '@redwoodjs/router/dist/location' import { namedRoutes } from '@redwoodjs/router/dist/namedRoutes' +import { Redirect } from '@redwoodjs/router/dist/redirect' import type { RouterProps } from '@redwoodjs/router/dist/router' import { rscFetch } from './rsc/rscFetchForClientRouter' @@ -24,7 +26,7 @@ export const Router = ({ paramTypes, children }: RouterProps) => { const LocationAwareRouter = ({ paramTypes, children }: RouterProps) => { const { pathname, search } = useLocation() - const { namedRoutesMap } = useMemo(() => { + const { namedRoutesMap, pathRouteMap } = useMemo(() => { return analyzeRoutes(children, { currentPathName: pathname, userParamTypes: paramTypes, @@ -35,5 +37,56 @@ const LocationAwareRouter = ({ paramTypes, children }: RouterProps) => { // Note that the value changes at runtime Object.assign(namedRoutes, namedRoutesMap) + // Use results from `analyzeRoutes` to determine if the user has access to + // the requested route + const requestedRoute = pathRouteMap[pathname] + + const privateSet = requestedRoute.sets.find((set) => set.isPrivate) + + if (privateSet) { + const redirectTarget = privateSet.props.unauthenticated + + if (!redirectTarget || typeof redirectTarget !== 'string') { + throw new Error( + `Route ${pathname} is private and no unauthenticated redirect target was provided`, + ) + } + + // We type cast like this, because AvailableRoutes is generated in the + // user's project + const generatedRoutesMap = namedRoutes as GeneratedRoutesMap + + if (!generatedRoutesMap[redirectTarget]) { + throw new Error(`We could not find a route named ${redirectTarget}`) + } + + const currentLocation = + globalThis.location.pathname + + encodeURIComponent(globalThis.location.search) + + let unauthenticatedPath + + try { + unauthenticatedPath = generatedRoutesMap[redirectTarget]() + } catch (e) { + if ( + e instanceof Error && + /Missing parameter .* for route/.test(e.message) + ) { + throw new Error( + `Redirecting to route "${redirectTarget}" would require route ` + + 'parameters, which currently is not supported. Please choose ' + + 'a different route', + ) + } + + throw new Error(`Could not redirect to the route named ${redirectTarget}`) + } + + return ( + + ) + } + return rscFetch('__rwjs__Routes', { location: { pathname, search } }) } From 15b81666fda71ea788ad535a37ad2fb2126b8d3a Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 4 Jul 2024 15:15:51 +0200 Subject: [PATCH 2/5] render AuthenticatedRoute instead --- packages/vite/src/ClientRouter.tsx | 46 +++++------------------------- 1 file changed, 7 insertions(+), 39 deletions(-) diff --git a/packages/vite/src/ClientRouter.tsx b/packages/vite/src/ClientRouter.tsx index e84f32a0cdba..5f0f3305c3ed 100644 --- a/packages/vite/src/ClientRouter.tsx +++ b/packages/vite/src/ClientRouter.tsx @@ -3,11 +3,10 @@ import React, { useMemo } from 'react' -import type { GeneratedRoutesMap } from '@redwoodjs/router/dist/analyzeRoutes' import { analyzeRoutes } from '@redwoodjs/router/dist/analyzeRoutes' +import { AuthenticatedRoute } from '@redwoodjs/router/dist/AuthenticatedRoute' import { LocationProvider, useLocation } from '@redwoodjs/router/dist/location' import { namedRoutes } from '@redwoodjs/router/dist/namedRoutes' -import { Redirect } from '@redwoodjs/router/dist/redirect' import type { RouterProps } from '@redwoodjs/router/dist/router' import { rscFetch } from './rsc/rscFetchForClientRouter' @@ -44,47 +43,16 @@ const LocationAwareRouter = ({ paramTypes, children }: RouterProps) => { const privateSet = requestedRoute.sets.find((set) => set.isPrivate) if (privateSet) { - const redirectTarget = privateSet.props.unauthenticated - - if (!redirectTarget || typeof redirectTarget !== 'string') { + const unauthenticated = privateSet.props.unauthenticated + if (!unauthenticated || typeof unauthenticated !== 'string') { throw new Error( - `Route ${pathname} is private and no unauthenticated redirect target was provided`, + 'You must specify an `unauthenticated` route when using PrivateSet', ) } - - // We type cast like this, because AvailableRoutes is generated in the - // user's project - const generatedRoutesMap = namedRoutes as GeneratedRoutesMap - - if (!generatedRoutesMap[redirectTarget]) { - throw new Error(`We could not find a route named ${redirectTarget}`) - } - - const currentLocation = - globalThis.location.pathname + - encodeURIComponent(globalThis.location.search) - - let unauthenticatedPath - - try { - unauthenticatedPath = generatedRoutesMap[redirectTarget]() - } catch (e) { - if ( - e instanceof Error && - /Missing parameter .* for route/.test(e.message) - ) { - throw new Error( - `Redirecting to route "${redirectTarget}" would require route ` + - 'parameters, which currently is not supported. Please choose ' + - 'a different route', - ) - } - - throw new Error(`Could not redirect to the route named ${redirectTarget}`) - } - return ( - + + {rscFetch('__rwjs__Routes', { location: { pathname, search } })} + ) } From 25aca323ae0ac9edcb2bb70a81f64354f1d3b07b Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 4 Jul 2024 15:54:24 +0200 Subject: [PATCH 3/5] reverse sets array --- packages/vite/src/ClientRouter.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/ClientRouter.tsx b/packages/vite/src/ClientRouter.tsx index 5f0f3305c3ed..3c481a710945 100644 --- a/packages/vite/src/ClientRouter.tsx +++ b/packages/vite/src/ClientRouter.tsx @@ -36,11 +36,14 @@ const LocationAwareRouter = ({ paramTypes, children }: RouterProps) => { // Note that the value changes at runtime Object.assign(namedRoutes, namedRoutesMap) - // Use results from `analyzeRoutes` to determine if the user has access to - // the requested route const requestedRoute = pathRouteMap[pathname] - const privateSet = requestedRoute.sets.find((set) => set.isPrivate) + // Need to reverse the sets array when finding the private set so that we + // find the inner-most private set first. Otherwise we could end up + // redirecting to the wrong route. + const reversedSets = requestedRoute.sets.toReversed() + + const privateSet = reversedSets.find((set) => set.isPrivate) if (privateSet) { const unauthenticated = privateSet.props.unauthenticated From 6eb2d1810b85aa50194637de74657e2faf8211e0 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 4 Jul 2024 23:08:46 +0200 Subject: [PATCH 4/5] Add todo and formatting --- packages/vite/src/ClientRouter.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vite/src/ClientRouter.tsx b/packages/vite/src/ClientRouter.tsx index 3c481a710945..e6558ef60679 100644 --- a/packages/vite/src/ClientRouter.tsx +++ b/packages/vite/src/ClientRouter.tsx @@ -41,6 +41,7 @@ const LocationAwareRouter = ({ paramTypes, children }: RouterProps) => { // Need to reverse the sets array when finding the private set so that we // find the inner-most private set first. Otherwise we could end up // redirecting to the wrong route. + // TODO (RSC): Add tests for finding the correct unauthenticated prop const reversedSets = requestedRoute.sets.toReversed() const privateSet = reversedSets.find((set) => set.isPrivate) @@ -52,6 +53,7 @@ const LocationAwareRouter = ({ paramTypes, children }: RouterProps) => { 'You must specify an `unauthenticated` route when using PrivateSet', ) } + return ( {rscFetch('__rwjs__Routes', { location: { pathname, search } })} From fb0caaee609b5380ca6d0fd9a7dd7248318ac30d Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 6 Jul 2024 07:53:49 +0200 Subject: [PATCH 5/5] set up router context --- packages/vite/src/ClientRouter.tsx | 41 +++++++++++++++---- .../tests/rsc-kitchen-sink.spec.ts | 12 +++++- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/packages/vite/src/ClientRouter.tsx b/packages/vite/src/ClientRouter.tsx index e6558ef60679..9ba3f696ff66 100644 --- a/packages/vite/src/ClientRouter.tsx +++ b/packages/vite/src/ClientRouter.tsx @@ -8,35 +8,53 @@ import { AuthenticatedRoute } from '@redwoodjs/router/dist/AuthenticatedRoute' import { LocationProvider, useLocation } from '@redwoodjs/router/dist/location' import { namedRoutes } from '@redwoodjs/router/dist/namedRoutes' import type { RouterProps } from '@redwoodjs/router/dist/router' +import { RouterContextProvider } from '@redwoodjs/router/dist/router-context' import { rscFetch } from './rsc/rscFetchForClientRouter' -export const Router = ({ paramTypes, children }: RouterProps) => { +export const Router = ({ useAuth, paramTypes, children }: RouterProps) => { return ( // Wrap it in the provider so that useLocation can be used - + {children} ) } -const LocationAwareRouter = ({ paramTypes, children }: RouterProps) => { +const LocationAwareRouter = ({ + useAuth, + paramTypes, + children, +}: RouterProps) => { const { pathname, search } = useLocation() - const { namedRoutesMap, pathRouteMap } = useMemo(() => { + const analyzeRoutesResult = useMemo(() => { return analyzeRoutes(children, { currentPathName: pathname, userParamTypes: paramTypes, }) }, [pathname, children, paramTypes]) + const { namedRoutesMap, pathRouteMap, activeRoutePath } = analyzeRoutesResult + // Assign namedRoutes so it can be imported like import {routes} from 'rwjs/router' // Note that the value changes at runtime Object.assign(namedRoutes, namedRoutesMap) - const requestedRoute = pathRouteMap[pathname] + // No activeRoutePath basically means 404. + // TODO (RSC): Add tests for this + // TODO (RSC): Figure out how to handle this case better + if (!activeRoutePath) { + // throw new Error( + // 'No route found for the current URL. Make sure you have a route ' + + // 'defined for the root of your React app.', + // ) + return rscFetch('__rwjs__Routes', { location: { pathname, search } }) + } + + const requestedRoute = pathRouteMap[activeRoutePath] // Need to reverse the sets array when finding the private set so that we // find the inner-most private set first. Otherwise we could end up @@ -55,9 +73,16 @@ const LocationAwareRouter = ({ paramTypes, children }: RouterProps) => { } return ( - - {rscFetch('__rwjs__Routes', { location: { pathname, search } })} - + + + {rscFetch('__rwjs__Routes', { location: { pathname, search } })} + + ) } diff --git a/tasks/smoke-tests/rsc-kitchen-sink/tests/rsc-kitchen-sink.spec.ts b/tasks/smoke-tests/rsc-kitchen-sink/tests/rsc-kitchen-sink.spec.ts index b25a6bbc06e4..eaa10b5041c0 100644 --- a/tasks/smoke-tests/rsc-kitchen-sink/tests/rsc-kitchen-sink.spec.ts +++ b/tasks/smoke-tests/rsc-kitchen-sink/tests/rsc-kitchen-sink.spec.ts @@ -26,8 +26,18 @@ test.beforeAll(async ({ browser }) => { // TODO (RSC): When we get toasts working we should check for a toast // message instead of network stuff, like in signUpTestUser() page.waitForResponse(async (response) => { + // Status >= 300 and < 400 is a redirect + // We get that sometimes for things like + // http://localhost:8910/assets/jsx-runtime-CGe0JNFD.mjs + if (response.status() >= 300 && response.status() < 400) { + return false + } + const body = await response.body() - return body.includes(`Username \`${testUser.email}\` already in use`) + return ( + response.url().includes('middleware') && + body.includes(`Username \`${testUser.email}\` already in use`) + ) }), ])