Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Reworks RSC server entries and route manifest building to derive from routes and include if route info related to authentication #10572

Merged
merged 22 commits into from
May 21, 2024

Conversation

dthyresson
Copy link
Contributor

@dthyresson dthyresson commented May 15, 2024

In furtherance if https://github.com/orgs/redwoodjs/projects/18/views/3?pane=issue&itemId=59049687

Currently @dac09 and I are thinking that to enforce authenticate during RSC rendering, we need to know if the entry request that renders the RSC component requires auth. This is not online how a graphQL operation is marked as requiring auth so that then the headers/cookie/credentials can be verified.

This PR refactors

  1. 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.

  2. 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.

  3. Add unauthenticated property to the route manifest to show where the user would expect to be redirected to if not permitted.

  4. Add roles to the route manifest

Both of these efforts are just a little speculation, but need the ability to check to see if our approach is reasonable.

FYI. The change in the RWRoute appears to have been an oversight from when the set was renamed from Private to PrivateSet. Now a route properly knows if it's private based on its parents.

Tests

Added to test to confirm that router can determine which routes are private. Note that fixtures are updated to favor PrivateSet over Private ... but the detection code still allows Private or PrivateSet.

Tests added to check the name, path, unauthenticated and roles attributers of a RWRoute.

Examples

Entries

The web/dist/rsc/entries.mjs builds correctly after refactor and app runs.

// client component mapping (dist/rsc -> dist/client)
export const clientEntries = {
  "assets/rsc-AboutCounter.tsx-3.mjs": "assets/rsc-AboutCounter.tsx-3-CQh4fLws.mjs",
  "assets/rsc-UserExamplesCell.tsx-7.mjs": "assets/rsc-UserExamplesCell.tsx-7-BLKpK17-.mjs",
  "assets/rsc-Counter.tsx-1.mjs": "assets/rsc-Counter.tsx-1-BAFuKeO7.mjs",
  "assets/rsc-rsc-test.es.js-0.mjs": "assets/rsc-rsc-test.es.js-0-Bkk2qwSq.mjs",
  "assets/rsc-NewUserExample.tsx-4.mjs": "assets/rsc-NewUserExample.tsx-4-j-hB-OZ1.mjs",
  "assets/rsc-UserExample.tsx-8.mjs": "assets/rsc-UserExample.tsx-8-Dzll3QYc.mjs",
  "assets/rsc-UpdateRandomButton.tsx-2.mjs": "assets/rsc-UpdateRandomButton.tsx-2-BicEZIWn.mjs",
  "assets/rsc-EmptyUsersCell.tsx-5.mjs": "assets/rsc-EmptyUsersCell.tsx-5-BItXvbaj.mjs",
  "assets/rsc-NewEmptyUser.tsx-6.mjs": "assets/rsc-NewEmptyUser.tsx-6-DFmv_Yef.mjs",
  "assets/rsc-UserExamples.tsx-11.mjs": "assets/rsc-UserExamples.tsx-11-BrzStkU7.mjs",
  "assets/rsc-link.js-10.mjs": "assets/rsc-link.js-10-D1gHLB5z.mjs",
  "assets/rsc-CellErrorBoundary.js-13.mjs": "assets/rsc-CellErrorBoundary.js-13-C9r2jdob.mjs",
  "assets/rsc-index.js-12.mjs": "assets/rsc-index.js-12-BlDxo_As.mjs",
  "assets/rsc-navLink.js-9.mjs": "assets/rsc-navLink.js-9-BsKFwPvg.mjs"
};

// server component mapping (src -> dist/rsc)
export const serverEntries = {
  "HomePage": "assets/HomePage-BkyFywXn.mjs",
  "AboutPage": "assets/AboutPage-qKLtmepV.mjs",
  "MultiCellPage": "assets/MultiCellPage-Bb1xy2-X.mjs",
  "EmptyUserNewEmptyUserPage": "assets/EmptyUserNewEmptyUserPage-CRWGWYlQ.mjs",
  "EmptyUserEditEmptyUserPage": "assets/EmptyUserEditEmptyUserPage-DK14w5wt.mjs",
  "EmptyUserEmptyUserPage": "assets/EmptyUserEmptyUserPage-BDEfXSJo.mjs",
  "EmptyUserEmptyUsersPage": "assets/EmptyUserEmptyUsersPage-BZjgUE33.mjs",
  "UserExampleNewUserExamplePage": "assets/UserExampleNewUserExamplePage-CPG6dVwh.mjs",
  "UserExampleEditUserExamplePage": "assets/UserExampleEditUserExamplePage-BfvIJHxv.mjs",
  "UserExampleUserExamplePage": "assets/UserExampleUserExamplePage-Bwqi8NaS.mjs",
  "UserExampleUserExamplesPage": "assets/UserExampleUserExamplesPage-MQMjbAP0.mjs",
  "NotFoundPage": "assets/NotFoundPage-CstA1diQ.mjs",
  "__rwjs__ServerEntry": "entry.server.mjs"
};

Route Manifest

...
    <Router>
      <Set wrap={NavigationLayout}>
        <Route path="/" page={HomePage} name="home" />
        <PrivateSet unauthenticated="home">
          <Route path="/about" page={AboutPage} name="about" />
        </PrivateSet>
        <PrivateSet unauthenticated="home" roles="owner">
          <Route path="/about-secret" page={AboutPage} name="aboutSecret" />
        </PrivateSet>
        <PrivateSet unauthenticated="home" roles={['publisher', 'admin']}>
          <Route path="/about-secret-admin" page={AboutPage} name="aboutSecretAdmin" />
        </PrivateSet>
        <Route path="/multi-cell" page={MultiCellPage} name="multiCell" />
...

builds .. notice the about routes are private.

{
  "/": {
    "name": "home",
    "bundle": null,
    "matchRegexString": "^/$",
    "pathDefinition": "/",
    "hasParams": false,
    "routeHooks": null,
    "redirect": null,
    "relativeFilePath": "pages/HomePage/HomePage.tsx",
    "isPrivate": false
  },
  "/about": {
    "name": "about",
    "bundle": null,
    "matchRegexString": "^/about$",
    "pathDefinition": "/about",
    "hasParams": false,
    "routeHooks": null,
    "redirect": null,
    "relativeFilePath": "pages/AboutPage/AboutPage.tsx",
    "isPrivate": true,
    "unauthenticated": "home"
  },
  "/about-secret": {
    "name": "aboutSecret",
    "bundle": null,
    "matchRegexString": "^/about-secret$",
    "pathDefinition": "/about-secret",
    "hasParams": false,
    "routeHooks": null,
    "redirect": null,
    "relativeFilePath": "pages/AboutPage/AboutPage.tsx",
    "isPrivate": true,
    "unauthenticated": "home",
    "roles": "owner"
  },
  "/about-secret-admin": {
    "name": "aboutSecretAdmin",
    "bundle": null,
    "matchRegexString": "^/about-secret-admin$",
    "pathDefinition": "/about-secret-admin",
    "hasParams": false,
    "routeHooks": null,
    "redirect": null,
    "relativeFilePath": "pages/AboutPage/AboutPage.tsx",
    "isPrivate": true,
    "unauthenticated": "home",
    "roles": [
      "publisher",
      "admin"
    ]
  },
  "/multi-cell": {
    "name": "multiCell",
    "bundle": null,
    "matchRegexString": "^/multi-cell$",
    "pathDefinition": "/multi-cell",
    "hasParams": false,
    "routeHooks": null,
    "redirect": null,
    "relativeFilePath": "pages/MultiCellPage/MultiCellPage.tsx",
    "isPrivate": false
  },

@@ -49,7 +49,7 @@ export class RWRoute extends BaseNode {
?.getOpeningElement()
?.getTagNameNode()
?.getText()
return tagText === 'Private'
return tagText === 'Private' || tagText === 'PrivateSet'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to keep "Private"? I think it has to be "PrivateSet" now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say lets remove Private in a separate PR, and remove it from exports too!

for (const page of pages) {
entries[page.importName] = page.path
entries[page.const_] = ensurePosixPath(page.path)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure of the use of const_ is appropriate, but it generates the correct key.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do again? Sorry can't remember 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use on const_ here provides the expected key for the entries file that is compatible with the prior way of serving the key:

  "UserExampleNewUserExamplePage": "assets/UserExampleNewUserExamplePage-CPG6dVwh.mjs",
  "UserExampleEditUserExamplePage": "assets/UserExampleEditUserExamplePage-BfvIJHxv.mjs",

UserExampleNewUserExamplePage is the key.

Before, the processPagesDir created a new type that enriches the page with and importName based on the glob file name:

export const processPagesDir = (
  webPagesDir: string = getPaths().web.pages,
): Array<PagesDependency> => {
  const pagePaths = fg.sync('**/*Page.{js,jsx,ts,tsx}', {
    cwd: webPagesDir,
    ignore: ['node_modules'],
  })
  return pagePaths.map((pagePath) => {
    const p = path.parse(pagePath)

    const importName = p.dir.replace(/\//g, '') <<<<<----
    const importPath = importStatementPath(
      path.join(webPagesDir, p.dir, p.name),

Whatever const_ is it appears to have the same info we need without having to process the paths.

I think it comes from structure model File or BaseNode.

@dthyresson dthyresson added the release:feature This PR introduces a new feature label May 15, 2024
@dthyresson dthyresson added this to the RSC milestone May 15, 2024
@dthyresson dthyresson requested a review from dac09 May 15, 2024 14:45
@dac09
Copy link
Collaborator

dac09 commented May 16, 2024

Nice one @dthyresson!

I think we'll need to go one step further. Right now in this PR it just tells us whether its private or not:

  "/about": {
    "name": "about",
    "bundle": null,
    "matchRegexString": "^/about$",
    "pathDefinition": "/about",
    "hasParams": false,
    "routeHooks": null,
    "redirect": null,
    "relativeFilePath": "pages/AboutPage/AboutPage.tsx",
+    "isPrivate": true
  },

But I think the missing steps are:

  1. Adding the unauthenticated path to the route manifest too (I think.... we need to know where to redirect the user, no?) I can't remember if we were settling for just "error out" or if we wanted to send back a redirect.

  2. You're probably still working on this.... but we'll need to change the code to actually use the new info available to us right? Like the entries has to build from or reference the routes manifest?

@dthyresson
Copy link
Contributor Author

@dac I added a unauthenticated property to the route manifest item:

        <Route path="/" page={HomePage} name="home" />
        <PrivateSet unauthenticated="home">
          <Route path="/about" page={AboutPage} name="about" />
          <Route path="/about-secret" page={AboutPage} name="aboutSecret" />
        </PrivateSet>
        <Route path="/multi-cell" page={MultiCellPage} name="multiCell" />

now results in:

{
  "/": {
    "name": "home",
    "bundle": null,
    "matchRegexString": "^/$",
    "pathDefinition": "/",
    "hasParams": false,
    "routeHooks": null,
    "redirect": null,
    "relativeFilePath": "pages/HomePage/HomePage.tsx",
    "isPrivate": false
  },
  "/about": {
    "name": "about",
    "bundle": null,
    "matchRegexString": "^/about$",
    "pathDefinition": "/about",
    "hasParams": false,
    "routeHooks": null,
    "redirect": null,
    "relativeFilePath": "pages/AboutPage/AboutPage.tsx",
    "isPrivate": true,
    "unauthenticated": "home"
  },
  "/about-secret": {
    "name": "aboutSecret",
    "bundle": null,
    "matchRegexString": "^/about-secret$",
    "pathDefinition": "/about-secret",
    "hasParams": false,
    "routeHooks": null,
    "redirect": null,
    "relativeFilePath": "pages/AboutPage/AboutPage.tsx",
    "isPrivate": true,
    "unauthenticated": "home"
  },
  "/multi-cell": {
    "name": "multiCell",
    "bundle": null,
    "matchRegexString": "^/multi-cell$",
    "pathDefinition": "/multi-cell",
    "hasParams": false,
    "routeHooks": null,
    "redirect": null,
    "relativeFilePath": "pages/MultiCellPage/MultiCellPage.tsx",
    "isPrivate": false
  },
...

@dthyresson dthyresson changed the title DRAFT: Reworks RSC server entries and route manifest building to derive from routes and include if route is Private DRAFT: Reworks RSC server entries and route manifest building to derive from routes and include if private route info May 16, 2024
@dthyresson dthyresson marked this pull request as ready for review May 16, 2024 16:41
@dthyresson dthyresson changed the title DRAFT: Reworks RSC server entries and route manifest building to derive from routes and include if private route info feat: Reworks RSC server entries and route manifest building to derive from routes and include if private route info May 16, 2024
@dthyresson dthyresson changed the title feat: Reworks RSC server entries and route manifest building to derive from routes and include if private route info feat: Reworks RSC server entries and route manifest building to derive from routes and include if route info related to authnetication May 16, 2024
@dthyresson dthyresson changed the title feat: Reworks RSC server entries and route manifest building to derive from routes and include if route info related to authnetication feat: Reworks RSC server entries and route manifest building to derive from routes and include if route info related to authentication May 16, 2024
@dthyresson
Copy link
Contributor Author

dthyresson commented May 16, 2024

Hm am seeing a Windows fail:

1. rscBuildAnalyze
==================

file:///D:/a/redwood/redwood/node_modules/rollup/dist/es/shared/parseAst.js:394
    const errorInstance = Object.assign(new Error(base.message), base);
                                        ^

Error [RollupError]: Could not resolve entry module "../../../../../d/a/redwood/rsc-project/web/src/pages/HomePage/HomePage.tsx".
    at getRollupError (file:///D:/a/redwood/redwood/node_modules/rollup/dist/es/shared/parseAst.js:394:41)
    at error (file:///D:/a/redwood/redwood/node_modules/rollup/dist/es/shared/parseAst.js:390:42)
    at ModuleLoader.loadEntryModule (file:///D:/a/redwood/redwood/node_modules/rollup/dist/es/shared/node-entry.js:19127:20)
    at async Promise.all (index 0) {
  code: 'UNRESOLVED_ENTRY',
  watchFiles: [
    'D:/a/redwood/rsc-project/web/src/entry.server.tsx',
    'D:/a/redwood/rsc-project/web/src/Document.tsx'
  ]
}

Node.js v20.13.1
[FAILED] Command failed with exit code 1: node D:\a\redwood\redwood\packages\vite\bins\rw-vite-build.mjs --webDir="D:\a\redwood\rsc-project\web" --verbose=true
Error: Command failed with exit code 1: node D:\a\redwood\redwood\packages\vite\bins\rw-vite-build.mjs --webDir="D:\a\redwood\rsc-project\web" --verbose=true

I wonder if the path I build now isn't compatible.

But - the path in server entries would have assets, not the project/web/src/pages/... path. so have to know where this is coming from.

@dthyresson
Copy link
Contributor Author

Hm am seeing a Windows fail:

1. rscBuildAnalyze
==================

file:///D:/a/redwood/redwood/node_modules/rollup/dist/es/shared/parseAst.js:394
    const errorInstance = Object.assign(new Error(base.message), base);
                                        ^

Error [RollupError]: Could not resolve entry module "../../../../../d/a/redwood/rsc-project/web/src/pages/HomePage/HomePage.tsx".
    at getRollupError (file:///D:/a/redwood/redwood/node_modules/rollup/dist/es/shared/parseAst.js:394:41)
    at error (file:///D:/a/redwood/redwood/node_modules/rollup/dist/es/shared/parseAst.js:390:42)
    at ModuleLoader.loadEntryModule (file:///D:/a/redwood/redwood/node_modules/rollup/dist/es/shared/node-entry.js:19127:20)
    at async Promise.all (index 0) {
  code: 'UNRESOLVED_ENTRY',
  watchFiles: [
    'D:/a/redwood/rsc-project/web/src/entry.server.tsx',
    'D:/a/redwood/rsc-project/web/src/Document.tsx'
  ]
}

Node.js v20.13.1
[FAILED] Command failed with exit code 1: node D:\a\redwood\redwood\packages\vite\bins\rw-vite-build.mjs --webDir="D:\a\redwood\rsc-project\web" --verbose=true
Error: Command failed with exit code 1: node D:\a\redwood\redwood\packages\vite\bins\rw-vite-build.mjs --webDir="D:\a\redwood\rsc-project\web" --verbose=true

I wonder if the path I build now isn't compatible.

But - the path in server entries would have assets, not the project/web/src/pages/... path. so have to know where this is coming from.

Fixed it :)

All checks have passed

@dthyresson
Copy link
Contributor Author

@dac09 To be used used in conjunction with #10647 to look up routes to enforce auth

Copy link
Collaborator

@dac09 dac09 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Leaving a couple of minor comments DT!

@dthyresson dthyresson requested a review from dac09 May 21, 2024 12:28
@dthyresson
Copy link
Contributor Author

LGTM! Leaving a couple of minor comments DT!

Thanks!

Updated the RWPage const to constName and added tests.

@dthyresson dthyresson enabled auto-merge (squash) May 21, 2024 13:16
@dac09
Copy link
Collaborator

dac09 commented May 21, 2024

DT forgot to mention, totally OK in another PR! We will need to update the equivalent of "route manifest" on the dev side too, but should be straightforward!

https://github.com/redwoodjs/redwood/blob/main/packages/internal/src/routes.ts#L86

@dthyresson
Copy link
Contributor Author

DT forgot to mention, totally OK in another PR! We will need to update the equivalent of "route manifest" on the dev side too, but should be straightforward!

https://github.com/redwoodjs/redwood/blob/main/packages/internal/src/routes.ts#L86

Oh, I didn't know that existed in dev. Ok will do in a PR soon.

I have been running and testing RSC with rw serve which is why didn't see that.

@dthyresson dthyresson merged commit d80d010 into main May 21, 2024
46 checks passed
@dthyresson dthyresson deleted the dt-refactor-server-entries-from-routes branch May 21, 2024 15:29
dac09 added a commit to dac09/redwood that referenced this pull request May 22, 2024
…-role-authmw

* 'main' of github.com:redwoodjs/redwood:
  fix(dbAuthMw): Update and fix logic related to dbAuth "verbs" and decryptionErrors (redwoodjs#10668)
  RSC: routes-auto-loader is not used for SSR anymore (redwoodjs#10672)
  chore(crwa): Remove unused jest dev dependency (redwoodjs#10673)
  RSC: rscBuildEntriesFile: Only ServerEntry and Routes needed for serverEntries (redwoodjs#10671)
  RSC: clientSsr: getServerEntryComponent() (redwoodjs#10670)
  RSC: worker: getFunctionComponent -> getRoutesComponent (redwoodjs#10669)
  RSC: kitchen-sink: Make the ReadFileServerCell output take up less space (redwoodjs#10667)
  RSC: Remove commented code related to prefixToRemove transform() (redwoodjs#10666)
  RSC Client Router (redwoodjs#10557)
  RSC: Add 'use client' to remaining client cells in kitchen-sink (redwoodjs#10665)
  RSC: vite auto-loader: Spell out 'path' and other chores (redwoodjs#10662)
  fix(cli): Handle case for no arguments for verbose baremetal deploy  (redwoodjs#10663)
  RSC: kitchen-sink: Make it more clear where layout ends and main content starts (redwoodjs#10661)
  RSC: Make the kitchen-sink smoke-test more robust/resilient (redwoodjs#10660)
  RSC: Source format of EmptyUsersCell in kitchen-sink (redwoodjs#10658)
  RSC: Add 'use client' to all client cells in kitchen-sink (redwoodjs#10659)
  chore(__fixtures__): Follow-up: Make test projects match newer CRWA template (redwoodjs#10657)
  feat: Reworks RSC server entries and route manifest building to derive from routes and include if route info related to authentication (redwoodjs#10572)
  chore(__fixtures__): Make test projects match newer CRWA template (redwoodjs#10655)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release:feature This PR introduces a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants