From ecb5faf9bb22ff328a417f465e1d9f6f82066512 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Wed, 15 May 2024 09:36:27 -0400 Subject: [PATCH 01/14] Builds server entries from routes rather than pages --- packages/vite/src/lib/entries.ts | 39 +++++++++++--------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/packages/vite/src/lib/entries.ts b/packages/vite/src/lib/entries.ts index cfffa1e41dce..0f984bc76c7c 100644 --- a/packages/vite/src/lib/entries.ts +++ b/packages/vite/src/lib/entries.ts @@ -1,37 +1,24 @@ -import path from 'node:path' - -import type { PagesDependency } from '@redwoodjs/project-config' -import { - ensurePosixPath, - getPaths, - processPagesDir, -} from '@redwoodjs/project-config' +import { ensurePosixPath, getPaths } from '@redwoodjs/project-config' +import { getProject } from '@redwoodjs/structure/dist/index' +import type { RWPage } from '@redwoodjs/structure/dist/model/RWPage' +import type { RWRoute } from '@redwoodjs/structure/dist/model/RWRoute' import { makeFilePath } from '../utils' -const getPathRelativeToSrc = (maybeAbsolutePath: string) => { - // If the path is already relative - if (!path.isAbsolute(maybeAbsolutePath)) { - return maybeAbsolutePath - } - - return `./${path.relative(getPaths().web.src, maybeAbsolutePath)}` -} - -const withRelativeImports = (page: PagesDependency) => { - return { - ...page, - relativeImport: ensurePosixPath(getPathRelativeToSrc(page.importPath)), - } -} - export function getEntries() { const entries: Record = {} + // Build the entries object based on routes and pages + // Given the page's route, we can determine whether or not + // the entry requires authentication checks + const rwProject = getProject(getPaths().base) + const routes = rwProject.getRouter().routes + // Add the various pages - const pages = processPagesDir().map(withRelativeImports) + const pages = routes.map((route: RWRoute) => route.page) as RWPage[] + for (const page of pages) { - entries[page.importName] = page.path + entries[page.const_] = ensurePosixPath(page.path) } // Add the ServerEntry entry, noting we use the "__rwjs__" prefix to avoid From 3e42a4c4f314951388b8603356ab76f68e616628 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Wed, 15 May 2024 09:51:51 -0400 Subject: [PATCH 02/14] A route is private if it has a parent PrivateSet --- packages/structure/src/model/RWRoute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/structure/src/model/RWRoute.ts b/packages/structure/src/model/RWRoute.ts index 7a7da004561a..36275c454fa8 100644 --- a/packages/structure/src/model/RWRoute.ts +++ b/packages/structure/src/model/RWRoute.ts @@ -49,7 +49,7 @@ export class RWRoute extends BaseNode { ?.getOpeningElement() ?.getTagNameNode() ?.getText() - return tagText === 'Private' + return tagText === 'Private' || tagText === 'PrivateSet' } @lazy() get hasParameters(): boolean { From 2b6ebf16e0c09b902395bc4352500a915b2e6fc1 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Wed, 15 May 2024 09:52:08 -0400 Subject: [PATCH 03/14] Adds isPrivate attribute to Route manifest --- packages/internal/src/routes.ts | 3 +++ packages/vite/src/buildRouteManifest.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/packages/internal/src/routes.ts b/packages/internal/src/routes.ts index 48505f8b9230..be3a3960ff5d 100644 --- a/packages/internal/src/routes.ts +++ b/packages/internal/src/routes.ts @@ -73,6 +73,7 @@ export interface RWRouteManifestItem { hasParams: boolean relativeFilePath: string redirect: { to: string; permanent: boolean } | null + isPrivate: boolean // Probably want isNotFound here, so we can attach a separate 404 handler } @@ -80,6 +81,7 @@ export interface RouteSpec extends RWRouteManifestItem { id: string isNotFound: boolean filePath: string | undefined + isPrivate: boolean relativeFilePath: string } @@ -111,6 +113,7 @@ export const getProjectRoutes = (): RouteSpec[] => { redirect: route.redirect ? { to: route.redirect, permanent: false } : null, + isPrivate: route.isPrivate, } }) } diff --git a/packages/vite/src/buildRouteManifest.ts b/packages/vite/src/buildRouteManifest.ts index 2c518595d444..814c776612c4 100644 --- a/packages/vite/src/buildRouteManifest.ts +++ b/packages/vite/src/buildRouteManifest.ts @@ -47,6 +47,7 @@ export async function buildRouteManifest() { } : null, relativeFilePath: route.relativeFilePath, + isPrivate: route.isPrivate, } return acc From 674a7253141470dd6fab916584d9038c82c96eb8 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Wed, 15 May 2024 10:09:09 -0400 Subject: [PATCH 04/14] Add changeset --- .changesets/10572.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changesets/10572.md diff --git a/.changesets/10572.md b/.changesets/10572.md new file mode 100644 index 000000000000..28ece5ff8374 --- /dev/null +++ b/.changesets/10572.md @@ -0,0 +1,9 @@ +- feat: Reworks RSC server entries and route manifest building to derive from routes and include if route is Private (#10572) by @dthyresson + +This PR is in furtherance of authentication support in RSC. + +It refactors: + +- How server entries are built -- not from "processing the pages dir" (which is a deprecated function) but rather the routes ... and the page info for that route. Note here that a page can be used in multiple routes, so the auth info cannot really be determined here. + +- The route manifest building to include an isPrivate attribute. Now if some page, route request is being handler we might be able to check if it "isPrivate" and enforce auth. From 6416eba9b284f1750228ea7fc82ac287d723aa76 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Wed, 15 May 2024 10:45:23 -0400 Subject: [PATCH 05/14] Favor PrivateSet over Private in fixtures to authenticate routes. Test that can determine private routes. --- __fixtures__/example-todo-main/web/src/Routes.js | 6 +++--- .../fragment-test-project/web/src/Routes.tsx | 6 +++--- __fixtures__/test-project/web/src/Routes.tsx | 6 +++--- packages/structure/src/model/RWRouter.ts | 6 +++++- .../structure/src/model/__tests__/model.test.ts | 13 +++++++++++++ 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/__fixtures__/example-todo-main/web/src/Routes.js b/__fixtures__/example-todo-main/web/src/Routes.js index 760b6fe958b4..a7dec5ee5f6d 100644 --- a/__fixtures__/example-todo-main/web/src/Routes.js +++ b/__fixtures__/example-todo-main/web/src/Routes.js @@ -7,7 +7,7 @@ // 'src/pages/HomePage/HomePage.js' -> HomePage // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage -import { Private, Router, Route } from '@redwoodjs/router' +import { PrivateSet, Router, Route } from '@redwoodjs/router' import SetLayout from 'src/layouts/SetLayout' import FooPage from 'src/pages/FooPage' @@ -22,9 +22,9 @@ const Routes = () => { - + - + ) diff --git a/__fixtures__/fragment-test-project/web/src/Routes.tsx b/__fixtures__/fragment-test-project/web/src/Routes.tsx index a8b80e306513..3982fc7790f5 100644 --- a/__fixtures__/fragment-test-project/web/src/Routes.tsx +++ b/__fixtures__/fragment-test-project/web/src/Routes.tsx @@ -7,7 +7,7 @@ // 'src/pages/HomePage/HomePage.js' -> HomePage // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage -import { Router, Route, Private, Set } from '@redwoodjs/router' +import { Router, Route, PrivateSet, Set } from '@redwoodjs/router' import BlogLayout from 'src/layouts/BlogLayout' import ScaffoldLayout from 'src/layouts/ScaffoldLayout' @@ -38,9 +38,9 @@ const Routes = () => { - + - + diff --git a/__fixtures__/test-project/web/src/Routes.tsx b/__fixtures__/test-project/web/src/Routes.tsx index e6cb92cc822e..56d1979fcb4a 100644 --- a/__fixtures__/test-project/web/src/Routes.tsx +++ b/__fixtures__/test-project/web/src/Routes.tsx @@ -7,7 +7,7 @@ // 'src/pages/HomePage/HomePage.js' -> HomePage // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage -import { Router, Route, Private, Set } from '@redwoodjs/router' +import { Router, Route, PrivateSet, Set } from '@redwoodjs/router' import BlogLayout from 'src/layouts/BlogLayout' import ScaffoldLayout from 'src/layouts/ScaffoldLayout' @@ -37,9 +37,9 @@ const Routes = () => { - + - + diff --git a/packages/structure/src/model/RWRouter.ts b/packages/structure/src/model/RWRouter.ts index 96a29a7d615c..1a757338f1dd 100644 --- a/packages/structure/src/model/RWRouter.ts +++ b/packages/structure/src/model/RWRouter.ts @@ -75,7 +75,11 @@ export class RWRouter extends FileNode { .getDescendantsOfKind(tsm.SyntaxKind.JsxElement) .filter((x) => { const tagName = x.getOpeningElement().getTagNameNode().getText() - return tagName === 'Set' || tagName === 'Private' + return ( + tagName === 'Set' || + tagName === 'Private' || + tagName === 'PrivateSet' + ) }) const prerenderSets = sets.filter((set) => diff --git a/packages/structure/src/model/__tests__/model.test.ts b/packages/structure/src/model/__tests__/model.test.ts index cf9ba1cfb0d6..e3e5bc724d4b 100644 --- a/packages/structure/src/model/__tests__/model.test.ts +++ b/packages/structure/src/model/__tests__/model.test.ts @@ -1,3 +1,4 @@ +import exp from 'constants' import { basename, resolve } from 'path' import { describe, it, expect } from 'vitest' @@ -140,6 +141,18 @@ describe('Redwood Route detection', () => { path: '/private-page', }) }) + it('detects authenticated routes', async () => { + const projectRoot = getFixtureDir('example-todo-main') + const project = new RWProject({ projectRoot, host: new DefaultHost() }) + const routes = project.getRouter().routes + + const authenticatedRoutes = routes + .filter((r) => r.isPrivate) + .map(({ name, path }) => ({ name, path })) + expect(authenticatedRoutes.length).toBe(1) + expect(authenticatedRoutes[0].name).toBe('privatePage') + expect(authenticatedRoutes[0].path).toBe('/private-page') + }) }) function getFixtureDir( From 569b5103f6a5e24cbb7feb1b79ba37cb4b879199 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Thu, 16 May 2024 08:49:35 -0400 Subject: [PATCH 06/14] Adds unauthenticated path to RWRoute model --- packages/structure/src/model/RWRoute.ts | 22 +++++++++++++++++++ .../src/model/__tests__/model.test.ts | 7 +++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/structure/src/model/RWRoute.ts b/packages/structure/src/model/RWRoute.ts index 36275c454fa8..7e7396423fac 100644 --- a/packages/structure/src/model/RWRoute.ts +++ b/packages/structure/src/model/RWRoute.ts @@ -52,6 +52,28 @@ export class RWRoute extends BaseNode { return tagText === 'Private' || tagText === 'PrivateSet' } + @lazy() get unauthenticated() { + if (!this.isPrivate) { + return undefined + } + + const a = this.jsxNode + .getParentIfKind(tsm.SyntaxKind.JsxElement) + ?.getOpeningElement() + .getAttribute('unauthenticated') + + if (!a) { + return undefined + } + if (tsm.Node.isJsxAttribute(a)) { + const init = a.getInitializer() + if (tsm.Node.isStringLiteral(init!)) { + return init.getLiteralValue() + } + } + return undefined + } + @lazy() get hasParameters(): boolean { if (!this.path) { return false diff --git a/packages/structure/src/model/__tests__/model.test.ts b/packages/structure/src/model/__tests__/model.test.ts index e3e5bc724d4b..18a2cb41b2b5 100644 --- a/packages/structure/src/model/__tests__/model.test.ts +++ b/packages/structure/src/model/__tests__/model.test.ts @@ -148,10 +148,15 @@ describe('Redwood Route detection', () => { const authenticatedRoutes = routes .filter((r) => r.isPrivate) - .map(({ name, path }) => ({ name, path })) + .map(({ name, path, unauthenticated }) => ({ + name, + path, + unauthenticated, + })) expect(authenticatedRoutes.length).toBe(1) expect(authenticatedRoutes[0].name).toBe('privatePage') expect(authenticatedRoutes[0].path).toBe('/private-page') + expect(authenticatedRoutes[0].unauthenticated).toBe('/home') }) }) From 74a96944ca357451e9434212d40b17b2f9d16340 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Thu, 16 May 2024 08:53:14 -0400 Subject: [PATCH 07/14] private redirect fix --- packages/structure/src/model/__tests__/model.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/structure/src/model/__tests__/model.test.ts b/packages/structure/src/model/__tests__/model.test.ts index 18a2cb41b2b5..d9837b76d8db 100644 --- a/packages/structure/src/model/__tests__/model.test.ts +++ b/packages/structure/src/model/__tests__/model.test.ts @@ -156,7 +156,7 @@ describe('Redwood Route detection', () => { expect(authenticatedRoutes.length).toBe(1) expect(authenticatedRoutes[0].name).toBe('privatePage') expect(authenticatedRoutes[0].path).toBe('/private-page') - expect(authenticatedRoutes[0].unauthenticated).toBe('/home') + expect(authenticatedRoutes[0].unauthenticated).toBe('home') }) }) From f7e35e8da4909f0beb9267f610c3a77343943b4e Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Thu, 16 May 2024 09:16:11 -0400 Subject: [PATCH 08/14] update route manifest --- __fixtures__/test-project/web/src/Routes.tsx | 6 +++--- packages/internal/src/routes.ts | 3 +++ packages/vite/src/buildRouteManifest.ts | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/__fixtures__/test-project/web/src/Routes.tsx b/__fixtures__/test-project/web/src/Routes.tsx index 56d1979fcb4a..e6cb92cc822e 100644 --- a/__fixtures__/test-project/web/src/Routes.tsx +++ b/__fixtures__/test-project/web/src/Routes.tsx @@ -7,7 +7,7 @@ // 'src/pages/HomePage/HomePage.js' -> HomePage // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage -import { Router, Route, PrivateSet, Set } from '@redwoodjs/router' +import { Router, Route, Private, Set } from '@redwoodjs/router' import BlogLayout from 'src/layouts/BlogLayout' import ScaffoldLayout from 'src/layouts/ScaffoldLayout' @@ -37,9 +37,9 @@ const Routes = () => { - + - + diff --git a/packages/internal/src/routes.ts b/packages/internal/src/routes.ts index be3a3960ff5d..4ad3c89065d3 100644 --- a/packages/internal/src/routes.ts +++ b/packages/internal/src/routes.ts @@ -74,6 +74,7 @@ export interface RWRouteManifestItem { relativeFilePath: string redirect: { to: string; permanent: boolean } | null isPrivate: boolean + unauthenticated: string | null // Probably want isNotFound here, so we can attach a separate 404 handler } @@ -82,6 +83,7 @@ export interface RouteSpec extends RWRouteManifestItem { isNotFound: boolean filePath: string | undefined isPrivate: boolean + unauthenticated: string | null relativeFilePath: string } @@ -114,6 +116,7 @@ export const getProjectRoutes = (): RouteSpec[] => { ? { to: route.redirect, permanent: false } : null, isPrivate: route.isPrivate, + unauthenticated: route.unauthenticated, } }) } diff --git a/packages/vite/src/buildRouteManifest.ts b/packages/vite/src/buildRouteManifest.ts index 814c776612c4..9b8637ed77c5 100644 --- a/packages/vite/src/buildRouteManifest.ts +++ b/packages/vite/src/buildRouteManifest.ts @@ -48,6 +48,7 @@ export async function buildRouteManifest() { : null, relativeFilePath: route.relativeFilePath, isPrivate: route.isPrivate, + unauthenticated: route.unauthenticated, } return acc From 0f5b4f22c192e6e8f4aad47b132cd1387e83afd7 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Thu, 16 May 2024 09:33:09 -0400 Subject: [PATCH 09/14] Had extraneous exp typo import --- packages/structure/src/model/__tests__/model.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/structure/src/model/__tests__/model.test.ts b/packages/structure/src/model/__tests__/model.test.ts index d9837b76d8db..d057d4959a08 100644 --- a/packages/structure/src/model/__tests__/model.test.ts +++ b/packages/structure/src/model/__tests__/model.test.ts @@ -1,4 +1,3 @@ -import exp from 'constants' import { basename, resolve } from 'path' import { describe, it, expect } from 'vitest' From 9e9f062aedd4dda7bce82e3c9be388aa599bcc85 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Thu, 16 May 2024 12:38:58 -0400 Subject: [PATCH 10/14] remove console --- packages/vite/src/rsc/rscRequestHandler.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/vite/src/rsc/rscRequestHandler.ts b/packages/vite/src/rsc/rscRequestHandler.ts index ae6eeaf60723..f82b1d7f1df1 100644 --- a/packages/vite/src/rsc/rscRequestHandler.ts +++ b/packages/vite/src/rsc/rscRequestHandler.ts @@ -31,8 +31,6 @@ export function createRscRequestHandler() { let rsfId: string | undefined let args: unknown[] = [] - console.log('url.pathname', url.pathname) - if (url.pathname.startsWith(basePath)) { rscId = url.pathname.split('/').pop() rsfId = url.searchParams.get('action_id') || undefined From 400580f9d786e64d0c8026d826792e4d46c2f0d6 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Thu, 16 May 2024 12:39:13 -0400 Subject: [PATCH 11/14] Add roles to building route manifest --- .../example-todo-main/web/src/Routes.js | 8 +- .../typeDefinitions.test.ts.snap | 2 +- packages/internal/src/routes.ts | 2 + packages/structure/src/model/RWRoute.ts | 61 +++++++++++++++ .../src/model/__tests__/model.test.ts | 76 +++++++++++++++++-- packages/vite/src/buildRouteManifest.ts | 1 + 6 files changed, 142 insertions(+), 8 deletions(-) diff --git a/__fixtures__/example-todo-main/web/src/Routes.js b/__fixtures__/example-todo-main/web/src/Routes.js index a7dec5ee5f6d..ca898eabcf67 100644 --- a/__fixtures__/example-todo-main/web/src/Routes.js +++ b/__fixtures__/example-todo-main/web/src/Routes.js @@ -22,9 +22,15 @@ const Routes = () => { - + + + + + + + ) diff --git a/packages/internal/src/__tests__/__snapshots__/typeDefinitions.test.ts.snap b/packages/internal/src/__tests__/__snapshots__/typeDefinitions.test.ts.snap index 7dfc8178c065..5e448ff75f3c 100644 --- a/packages/internal/src/__tests__/__snapshots__/typeDefinitions.test.ts.snap +++ b/packages/internal/src/__tests__/__snapshots__/typeDefinitions.test.ts.snap @@ -22,7 +22,7 @@ exports[`generates global page imports source maps 1`] = ` exports[`generates source maps for the router routes 1`] = ` { "file": "web-routerRoutes.d.ts", - "mappings": ";;;;;;IAiBM;IACA;IACA;IAEE;IACA;IAGA", + "mappings": ";;;;;;IAiBM;IACA;IACA;IAEE;IACA;IAGA;IAGA;IAGA", "names": [], "sources": [ "../../../web/src/Routes.js", diff --git a/packages/internal/src/routes.ts b/packages/internal/src/routes.ts index 4ad3c89065d3..5225e7324ec1 100644 --- a/packages/internal/src/routes.ts +++ b/packages/internal/src/routes.ts @@ -75,6 +75,7 @@ export interface RWRouteManifestItem { redirect: { to: string; permanent: boolean } | null isPrivate: boolean unauthenticated: string | null + roles: string | string[] | null // Probably want isNotFound here, so we can attach a separate 404 handler } @@ -117,6 +118,7 @@ export const getProjectRoutes = (): RouteSpec[] => { : null, isPrivate: route.isPrivate, unauthenticated: route.unauthenticated, + roles: route.roles, } }) } diff --git a/packages/structure/src/model/RWRoute.ts b/packages/structure/src/model/RWRoute.ts index 7e7396423fac..37adbb5aaad3 100644 --- a/packages/structure/src/model/RWRoute.ts +++ b/packages/structure/src/model/RWRoute.ts @@ -74,6 +74,67 @@ export class RWRoute extends BaseNode { return undefined } + @lazy() + get roles() { + if (!this.isPrivate) { + return undefined + } + + const a = this.jsxNode + .getParentIfKind(tsm.SyntaxKind.JsxElement) + ?.getOpeningElement() + .getAttribute('roles') + + if (!a) { + return undefined + } + + if (tsm.Node.isJsxAttribute(a)) { + const init = a.getInitializer() + + // Handle string literals + if (tsm.Node.isStringLiteral(init)) { + let literalValue = init.getLiteralValue() + + // Check if the string looks like an array with single quotes + if (literalValue.startsWith('[') && literalValue.endsWith(']')) { + try { + // Unescape the string by replacing single quotes with double quotes + const correctedLiteralValue = literalValue.replace(/'/g, '"') + // Attempt to parse as JSON array + const parsedValue = JSON.parse(correctedLiteralValue) + if (Array.isArray(parsedValue)) { + return parsedValue + } + } catch (e) { + // If parsing fails, return undefined + return undefined + } + } + + // If not an array, return the string value + return literalValue + } + + // Handle JSX expressions with array literals + if (tsm.Node.isJsxExpression(init)) { + const expr = init.getExpression() + if (tsm.Node.isArrayLiteralExpression(expr)) { + return expr + .getElements() + .map((element) => { + if (tsm.Node.isStringLiteral(element)) { + return element.getLiteralValue() + } + return undefined + }) + .filter((val) => val !== undefined) + } + } + } + return undefined + } + @lazy() get hasParameters(): boolean { if (!this.path) { return false diff --git a/packages/structure/src/model/__tests__/model.test.ts b/packages/structure/src/model/__tests__/model.test.ts index d057d4959a08..7558f552a66c 100644 --- a/packages/structure/src/model/__tests__/model.test.ts +++ b/packages/structure/src/model/__tests__/model.test.ts @@ -123,7 +123,7 @@ describe('Redwood Route detection', () => { // interested in .map(({ name, path }) => ({ name, path })) - expect(prerenderRoutes.length).toBe(6) + expect(prerenderRoutes.length).toBe(8) expect(prerenderRoutes).toContainEqual({ name: 'home', path: '/' }) expect(prerenderRoutes).toContainEqual({ name: 'typescriptPage', @@ -147,15 +147,79 @@ describe('Redwood Route detection', () => { const authenticatedRoutes = routes .filter((r) => r.isPrivate) - .map(({ name, path, unauthenticated }) => ({ + .map(({ name, path, unauthenticated, roles }) => ({ name, path, unauthenticated, + roles, })) - expect(authenticatedRoutes.length).toBe(1) - expect(authenticatedRoutes[0].name).toBe('privatePage') - expect(authenticatedRoutes[0].path).toBe('/private-page') - expect(authenticatedRoutes[0].unauthenticated).toBe('home') + + expect(authenticatedRoutes.length).toBe(3) + }) + + it('detects name and path for an authenticated route', async () => { + const projectRoot = getFixtureDir('example-todo-main') + const project = new RWProject({ projectRoot, host: new DefaultHost() }) + const routes = project.getRouter().routes + + const authenticatedRoutes = routes + .filter((r) => r.isPrivate) + .map(({ name, path, unauthenticated, roles }) => ({ + name, + path, + unauthenticated, + roles, + })) + + expect(authenticatedRoutes[1].name).toBe('privatePageAdmin') + expect(authenticatedRoutes[1].path).toBe('/private-page-admin') + expect(authenticatedRoutes[1].unauthenticated).toBe('home') + expect(authenticatedRoutes[1].roles).toBeTypeOf('string') + expect(authenticatedRoutes[1].roles).toContain('admin') + }) + + it('detects roles for an authenticated route when roles is a string of a single role', async () => { + const projectRoot = getFixtureDir('example-todo-main') + const project = new RWProject({ projectRoot, host: new DefaultHost() }) + const routes = project.getRouter().routes + + const authenticatedRoutes = routes + .filter((r) => r.isPrivate) + .map(({ name, path, unauthenticated, roles }) => ({ + name, + path, + unauthenticated, + roles, + })) + + expect(authenticatedRoutes[1].name).toBe('privatePageAdmin') + expect(authenticatedRoutes[1].path).toBe('/private-page-admin') + expect(authenticatedRoutes[1].unauthenticated).toBe('home') + expect(authenticatedRoutes[1].roles).toBeTypeOf('string') + expect(authenticatedRoutes[1].roles).toContain('admin') + }) + + it('detects roles for an authenticated route when roles is an array of a roles', async () => { + const projectRoot = getFixtureDir('example-todo-main') + const project = new RWProject({ projectRoot, host: new DefaultHost() }) + const routes = project.getRouter().routes + + const authenticatedRoutes = routes + .filter((r) => r.isPrivate) + .map(({ name, path, unauthenticated, roles }) => ({ + name, + path, + unauthenticated, + roles, + })) + + expect(authenticatedRoutes[2].name).toBe('privatePageAdminSuper') + expect(authenticatedRoutes[2].path).toBe('/private-page-admin-super') + expect(authenticatedRoutes[2].unauthenticated).toBe('home') + expect(authenticatedRoutes[2].roles).toBeInstanceOf(Array) + expect(authenticatedRoutes[2].roles).toContain('owner') + expect(authenticatedRoutes[2].roles).toContain('superuser') + expect(authenticatedRoutes[2].roles).not.toContain('member') }) }) diff --git a/packages/vite/src/buildRouteManifest.ts b/packages/vite/src/buildRouteManifest.ts index 9b8637ed77c5..f0d1efc4cdd0 100644 --- a/packages/vite/src/buildRouteManifest.ts +++ b/packages/vite/src/buildRouteManifest.ts @@ -49,6 +49,7 @@ export async function buildRouteManifest() { relativeFilePath: route.relativeFilePath, isPrivate: route.isPrivate, unauthenticated: route.unauthenticated, + roles: route.roles, } return acc From 778929aed6604ad7d9e76d5c2742e7866dcac5a2 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Thu, 16 May 2024 12:46:25 -0400 Subject: [PATCH 12/14] improve changeset --- .changesets/10572.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.changesets/10572.md b/.changesets/10572.md index 28ece5ff8374..6fbc3b7f2b84 100644 --- a/.changesets/10572.md +++ b/.changesets/10572.md @@ -1,4 +1,4 @@ -- feat: Reworks RSC server entries and route manifest building to derive from routes and include if route is Private (#10572) by @dthyresson +- feat: feat: Reworks RSC server entries and route manifest building to derive from routes and include if route info related to authentication (#10572) by @dthyresson This PR is in furtherance of authentication support in RSC. @@ -6,4 +6,10 @@ It refactors: - How server entries are built -- not from "processing the pages dir" (which is a deprecated function) but rather the routes ... and the page info for that route. Note here that a page can be used in multiple routes, so the auth info cannot really be determined here. -- The route manifest building to include an isPrivate attribute. Now if some page, route request is being handler we might be able to check if it "isPrivate" and enforce auth. +- The route manifest building to include per route: + +* isPrivate - is the route private, i.e, is it wrapped in a PrivateSet +* unauthenticated - what route to navigate to if the user in not authenticated +* roles - the roles to check to see if user has the require RBAC permission to navigate to the route + +Now if some page, route request is being handled by RSC we might be able to check if it "isPrivate" and enforce auth with the roles and even where tp redirect to if not authenticated. From 458416453982b16b73a194097f475cb7319ca853 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Fri, 17 May 2024 19:50:25 -0400 Subject: [PATCH 13/14] fix windows ci? --- packages/vite/src/lib/entries.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/lib/entries.ts b/packages/vite/src/lib/entries.ts index 0f984bc76c7c..0938fd4e0bdb 100644 --- a/packages/vite/src/lib/entries.ts +++ b/packages/vite/src/lib/entries.ts @@ -1,4 +1,8 @@ -import { ensurePosixPath, getPaths } from '@redwoodjs/project-config' +import { + ensurePosixPath, + getPaths, + importStatementPath, +} from '@redwoodjs/project-config' import { getProject } from '@redwoodjs/structure/dist/index' import type { RWPage } from '@redwoodjs/structure/dist/model/RWPage' import type { RWRoute } from '@redwoodjs/structure/dist/model/RWRoute' @@ -18,7 +22,7 @@ export function getEntries() { const pages = routes.map((route: RWRoute) => route.page) as RWPage[] for (const page of pages) { - entries[page.const_] = ensurePosixPath(page.path) + entries[page.const_] = ensurePosixPath(importStatementPath(page.path)) } // Add the ServerEntry entry, noting we use the "__rwjs__" prefix to avoid From 6418794ded8e3511560a15bfe4a9a55cc8a74f1f Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Tue, 21 May 2024 08:24:17 -0400 Subject: [PATCH 14/14] Rename RWPage's const to constName --- packages/project-config/src/paths.ts | 4 ++-- packages/structure/src/model/RWPage.ts | 4 ++-- packages/structure/src/model/RWProject.ts | 4 +++- packages/structure/src/model/RWRoute.ts | 2 +- .../src/model/__tests__/model.test.ts | 22 +++++++++++++++++++ packages/vite/src/lib/entries.ts | 2 +- ...vite-plugin-rsc-route-auto-loader.test.mts | 16 +++++++------- .../vite-plugin-rsc-routes-auto-loader.ts | 4 ++-- 8 files changed, 41 insertions(+), 17 deletions(-) diff --git a/packages/project-config/src/paths.ts b/packages/project-config/src/paths.ts index 0bcdcda157a9..49287370e17e 100644 --- a/packages/project-config/src/paths.ts +++ b/packages/project-config/src/paths.ts @@ -80,7 +80,7 @@ export interface PagesDependency { /** the variable to which the import is assigned */ importName: string /** @alias importName */ - const: string + constName: string /** absolute path without extension */ importPath: string /** absolute path with extension */ @@ -346,7 +346,7 @@ export const processPagesDir = ( const importStatement = `const ${importName} = { name: '${importName}', loader: import('${importPath}') }` return { importName, - const: importName, + constName: importName, importPath, path: path.join(webPagesDir, pagePath), importStatement, diff --git a/packages/structure/src/model/RWPage.ts b/packages/structure/src/model/RWPage.ts index d82486408f6f..1edd2e0a9304 100644 --- a/packages/structure/src/model/RWPage.ts +++ b/packages/structure/src/model/RWPage.ts @@ -9,7 +9,7 @@ import type { RWProject } from './RWProject' export class RWPage extends FileNode { constructor( - public const_: string, + public constName: string, public path: string, public parent: RWProject, ) { @@ -20,7 +20,7 @@ export class RWPage extends FileNode { } @lazy() get route() { return this.parent.router.routes.find( - (r) => r.page_identifier_str === this.const_, + (r) => r.page_identifier_str === this.constName, ) } @lazy() get layoutName(): string | undefined { diff --git a/packages/structure/src/model/RWProject.ts b/packages/structure/src/model/RWProject.ts index f82b169e1be1..fae2229b25e6 100644 --- a/packages/structure/src/model/RWProject.ts +++ b/packages/structure/src/model/RWProject.ts @@ -112,7 +112,9 @@ export class RWProject extends BaseNode { } } @lazy() get pages(): RWPage[] { - return this.processPagesDir.map((p) => new RWPage(p.const, p.path, this)) + return this.processPagesDir.map( + (p) => new RWPage(p.constName, p.path, this), + ) } @lazy() get router() { return this.getRouter() diff --git a/packages/structure/src/model/RWRoute.ts b/packages/structure/src/model/RWRoute.ts index 37adbb5aaad3..bb701f602373 100644 --- a/packages/structure/src/model/RWRoute.ts +++ b/packages/structure/src/model/RWRoute.ts @@ -181,7 +181,7 @@ export class RWRoute extends BaseNode { return undefined } return this.parent.parent.pages.find( - (p) => p.const_ === this.page_identifier_str, + (p) => p.constName === this.page_identifier_str, ) } /** diff --git a/packages/structure/src/model/__tests__/model.test.ts b/packages/structure/src/model/__tests__/model.test.ts index 7558f552a66c..9cc7a6be83ac 100644 --- a/packages/structure/src/model/__tests__/model.test.ts +++ b/packages/structure/src/model/__tests__/model.test.ts @@ -111,6 +111,28 @@ describe.skip('env vars', () => { }) }) +describe('Redwood Page detection', () => { + it('detects pages', async () => { + const projectRoot = getFixtureDir('example-todo-main') + const project = new RWProject({ projectRoot, host: new DefaultHost() }) + const routes = project.getRouter().routes + const pages = routes.map((r) => r.page).sort() + const pageConstants = pages.map((p) => p?.constName) + // Note: Pages can be duplicated if used by multiple routes, we use a Set + expect(pageConstants).toEqual([ + 'HomePage', + 'TypeScriptPage', + 'FooPage', + 'BarPage', + 'PrivatePage', + 'PrivatePage', + 'PrivatePage', + 'NotFoundPage', + undefined, + ]) + }) +}) + describe('Redwood Route detection', () => { it('detects routes with the prerender prop', async () => { const projectRoot = getFixtureDir('example-todo-main') diff --git a/packages/vite/src/lib/entries.ts b/packages/vite/src/lib/entries.ts index 0938fd4e0bdb..b1f1711414da 100644 --- a/packages/vite/src/lib/entries.ts +++ b/packages/vite/src/lib/entries.ts @@ -22,7 +22,7 @@ export function getEntries() { const pages = routes.map((route: RWRoute) => route.page) as RWPage[] for (const page of pages) { - entries[page.const_] = ensurePosixPath(importStatementPath(page.path)) + entries[page.constName] = ensurePosixPath(importStatementPath(page.path)) } // Add the ServerEntry entry, noting we use the "__rwjs__" prefix to avoid diff --git a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-route-auto-loader.test.mts b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-route-auto-loader.test.mts index 71ce27b0aefb..1003a85d3da7 100644 --- a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-route-auto-loader.test.mts +++ b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-route-auto-loader.test.mts @@ -385,56 +385,56 @@ describe('rscRoutesAutoLoader', () => { const pages = [ { importName: 'AboutPage', - const: 'AboutPage', + constName: 'AboutPage', importPath: '/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage', path: '/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage.tsx', importStatement: "const AboutPage = { name: 'AboutPage', loader: import('/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage') }" }, { importName: 'FatalErrorPage', - const: 'FatalErrorPage', + constName: 'FatalErrorPage', importPath: '/Users/mojombo/rw-app/web/src/pages/FatalErrorPage/FatalErrorPage', path: '/Users/mojombo/rw-app/web/src/pages/FatalErrorPage/FatalErrorPage.tsx', importStatement: "const FatalErrorPage = { name: 'FatalErrorPage', loader: import('/Users/mojombo/rw-app/web/src/pages/FatalErrorPage/FatalErrorPage') }" }, { importName: 'HomePage', - const: 'HomePage', + constName: 'HomePage', importPath: '/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage', path: '/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.tsx', importStatement: "const HomePage = { name: 'HomePage', loader: import('/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage') }" }, { importName: 'NotFoundPage', - const: 'NotFoundPage', + constName: 'NotFoundPage', importPath: '/Users/mojombo/rw-app/web/src/pages/NotFoundPage/NotFoundPage', path: '/Users/mojombo/rw-app/web/src/pages/NotFoundPage/NotFoundPage.tsx', importStatement: "const NotFoundPage = { name: 'NotFoundPage', loader: import('/Users/mojombo/rw-app/web/src/pages/NotFoundPage/NotFoundPage') }" }, { importName: 'EmptyUserEditEmptyUserPage', - const: 'EmptyUserEditEmptyUserPage', + constName: 'EmptyUserEditEmptyUserPage', importPath: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EditEmptyUserPage/EditEmptyUserPage', path: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EditEmptyUserPage/EditEmptyUserPage.tsx', importStatement: "const EmptyUserEditEmptyUserPage = { name: 'EmptyUserEditEmptyUserPage', loader: import('/Users/mojombo/rw-app/web/src/pages/EmptyUser/EditEmptyUserPage/EditEmptyUserPage') }" }, { importName: 'EmptyUserEmptyUserPage', - const: 'EmptyUserEmptyUserPage', + constName: 'EmptyUserEmptyUserPage', importPath: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUserPage/EmptyUserPage', path: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUserPage/EmptyUserPage.tsx', importStatement: "const EmptyUserEmptyUserPage = { name: 'EmptyUserEmptyUserPage', loader: import('/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUserPage/EmptyUserPage') }" }, { importName: 'EmptyUserEmptyUsersPage', - const: 'EmptyUserEmptyUsersPage', + constName: 'EmptyUserEmptyUsersPage', importPath: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUsersPage/EmptyUsersPage', path: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUsersPage/EmptyUsersPage.tsx', importStatement: "const EmptyUserEmptyUsersPage = { name: 'EmptyUserEmptyUsersPage', loader: import('/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUsersPage/EmptyUsersPage') }" }, { importName: 'EmptyUserNewEmptyUserPage', - const: 'EmptyUserNewEmptyUserPage', + constName: 'EmptyUserNewEmptyUserPage', importPath: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage', path: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage.tsx', importStatement: "const EmptyUserNewEmptyUserPage = { name: 'EmptyUserNewEmptyUserPage', loader: import('/Users/mojombo/rw-app/web/src/pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage') }" diff --git a/packages/vite/src/plugins/vite-plugin-rsc-routes-auto-loader.ts b/packages/vite/src/plugins/vite-plugin-rsc-routes-auto-loader.ts index 1aca4ee7a5ca..4ba4fb5711dc 100644 --- a/packages/vite/src/plugins/vite-plugin-rsc-routes-auto-loader.ts +++ b/packages/vite/src/plugins/vite-plugin-rsc-routes-auto-loader.ts @@ -127,9 +127,9 @@ export function rscRoutesAutoLoader(): Plugin { ast.program.body.unshift( t.variableDeclaration('const', [ t.variableDeclarator( - t.identifier(page.const), + t.identifier(page.constName), t.callExpression(t.identifier(loadFunctionName), [ - t.stringLiteral(page.const), + t.stringLiteral(page.constName), ]), ), ]),