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(middleware): Add support for Middleware to SSR-Streaming server #9883

Merged
merged 40 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a560620
Add code for invoking middleware (WIP)
dac09 Jan 24, 2024
17ab5f7
Fix links in suspense apollo provider for backwards compatibility
dac09 Jan 24, 2024
84279f7
WIP: Start adding MW classes
dac09 Jan 24, 2024
f0e0f38
Use subclassing to create MiddlewareRequest
dac09 Jan 24, 2024
07207bc
WIP: Try extending WhatwgRequest instead
dac09 Jan 24, 2024
eec9e3e
Bump whatwg versions
dac09 Jan 25, 2024
8d64b1c
Add some more tests for MWReq, MWRes
dac09 Jan 25, 2024
7cac8aa
Merge branch 'main' of github.com:redwoodjs/redwood into feat/ssr-mid…
dac09 Jan 25, 2024
7e51740
Update packages to make tests pass
dac09 Jan 26, 2024
29cebfd
CookieJar unsetting a cookie
dac09 Jan 26, 2024
65f221a
Add test for multiple set cookies
dac09 Jan 26, 2024
4a82d78
Remove old encrypted session from ServerAuthState
dac09 Jan 26, 2024
85ba98c
Export middleware classes
dac09 Jan 26, 2024
e579c9d
Fix TS errors in apollo
dac09 Jan 26, 2024
93eaadd
Improve implementations of mwReq and mwRes
dac09 Jan 26, 2024
9c01b3e
Use classes while invoking middleware
dac09 Jan 26, 2024
57e7cb4
Update mRes implementation
dac09 Jan 26, 2024
8090968
Consistent version of whatwg fetch
dac09 Jan 26, 2024
cc16e07
Readd commented line
dac09 Jan 26, 2024
9b27895
Add test for constructor and perm redirects
dac09 Jan 26, 2024
bfb39ff
Send Response back from rwmw endpoint
dac09 Jan 26, 2024
4717aae
Default auth state in serverAuthContext
dac09 Jan 26, 2024
47705c2
Comment
dac09 Jan 26, 2024
30759dd
Move credentials include
dac09 Jan 26, 2024
9dff2d9
Merge branch 'main' into feat/ssr-middleware
dac09 Jan 26, 2024
9182742
Update yarn.lock
dac09 Jan 26, 2024
0029c2c
Merge branch 'main' into feat/ssr-middleware
dac09 Jan 27, 2024
20b18d8
Cleanup comments in AuthProvider
dac09 Jan 30, 2024
ac82e57
Apply suggestions from Tobbe's review
dac09 Jan 30, 2024
2a5b72d
Apply suggestions from Tobbe's review
dac09 Jan 30, 2024
6e89b52
Merge branch 'feat/ssr-middleware' of github.com:dac09/redwood into f…
dac09 Jan 30, 2024
5b46aec
Update packages/auth/src/AuthProvider/AuthProvider.tsx
dac09 Jan 30, 2024
78879a6
Implement middleware invoke helper
dac09 Jan 30, 2024
3d14b51
Merge branch 'feat/ssr-middleware' of github.com:dac09/redwood into f…
dac09 Jan 30, 2024
dcc50df
Rename cookieJar.delete to unset
dac09 Jan 30, 2024
a203a19
Also invoke middleware in prod fe server
dac09 Jan 30, 2024
beef260
Update packages/auth/src/AuthProvider/ServerAuthProvider.tsx
dac09 Jan 30, 2024
fda6e53
Merge branch 'main' into feat/ssr-middleware
Tobbe Jan 31, 2024
967a6c2
fix syntax error (extra })
Tobbe Jan 31, 2024
68250c2
Use relative import path
Tobbe Jan 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"dependencies": {
"@babel/runtime-corejs3": "7.23.6",
"@prisma/client": "5.7.0",
"@whatwg-node/fetch": "0.9.14",
"@whatwg-node/fetch": "0.9.16",
"core-js": "3.34.0",
"humanize-string": "2.1.0",
"jsonwebtoken": "9.0.2",
Expand Down
4 changes: 4 additions & 0 deletions packages/auth/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ declare global {
RWJS_API_URL: string

__REDWOOD__APP_TITLE: string

RWJS_EXP_STREAMING_SSR: boolean
}

var __REDWOOD__SERVER__AUTH_STATE__: AuthProviderState<any>

namespace NodeJS {
interface Global {
/** URL or absolute path to the GraphQL serverless function */
Expand Down
17 changes: 13 additions & 4 deletions packages/auth/src/AuthProvider/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { ReactNode } from 'react'
import React, { useEffect, useState } from 'react'
import React, { useContext, useEffect, useState } from 'react'

import type { AuthContextInterface, CurrentUser } from '../AuthContext'
import type { AuthImplementation } from '../AuthImplementation'

import type { AuthProviderState } from './AuthProviderState'
import { defaultAuthProviderState } from './AuthProviderState'
import { ServerAuthContext } from './ServerAuthProvider'
import { useCurrentUser } from './useCurrentUser'
import { useForgotPassword } from './useForgotPassword'
import { useHasRole } from './useHasRole'
Expand Down Expand Up @@ -82,9 +83,11 @@ export function createAuthProvider<
}: AuthProviderProps) => {
// const [hasRestoredState, setHasRestoredState] = useState(false)

const serverAuthState = useContext(ServerAuthContext)

const [authProviderState, setAuthProviderState] = useState<
AuthProviderState<TUser>
>(defaultAuthProviderState)
>(serverAuthState || defaultAuthProviderState)

const getToken = useToken(authImplementation)

Expand Down Expand Up @@ -131,11 +134,17 @@ export function createAuthProvider<
useEffect(() => {
async function doRestoreState() {
await authImplementation.restoreAuthState?.()
reauthenticate()

// @MARK(SSR-Auth): Conditionally call reauth, because initial state
// should come from server (on SSR).
// If the initial state didn't come from the server - or was restored already -
// reauthenticate will make a call to receive the current user from the server
reauthenticate()
}
}

doRestoreState()
}, [reauthenticate])
}, [reauthenticate, serverAuthState])

return (
<AuthContext.Provider
Expand Down
77 changes: 77 additions & 0 deletions packages/auth/src/AuthProvider/ServerAuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { ReactNode } from 'react'
import React from 'react'

import type { AuthProviderState } from './AuthProviderState'
import { defaultAuthProviderState } from './AuthProviderState'

export type ServerAuthState = AuthProviderState<never> & {
cookieHeader?: string
}

const getAuthInitialStateFromServer = () => {
if (globalThis?.__REDWOOD__SERVER__AUTH_STATE__) {
const initialState = {
...defaultAuthProviderState,
encryptedSession: null,
...(globalThis?.__REDWOOD__SERVER__AUTH_STATE__ || {}),
}
// Clear it so we don't accidentally use it again
globalThis.__REDWOOD__SERVER__AUTH_STATE__ = null
return initialState
}

// Already restored
return null
}

/**
* On the server, it resolves to the defaultAuthProviderState first.
*
* On the client it restores from the initial server state injected in the ServerAuthProvider
*/
export const ServerAuthContext = React.createContext<ServerAuthState>(
getAuthInitialStateFromServer()
)

/**
* Note: This only gets rendered on the server and serves two purposes:
* 1) On the server, it sets the auth state
* 2) On the client, it restores the auth state from the initial server render
*/
export const ServerAuthProvider = ({
value,
children,
}: {
value: ServerAuthState
children?: ReactNode[]
}) => {
// @NOTE: we "Sanitize" to remove encryptedSession and cookieHeader
dac09 marked this conversation as resolved.
Show resolved Hide resolved
// not totally necessary, but it's nice to not have them in the DOM
// @MARK: needs discussion!
Copy link
Member

Choose a reason for hiding this comment

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

What needs discussion? Can you and I discuss? I want to resolve this before merging

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I just want to call this out for awareness. It's harmless right now, but its possible a contribution or new code could leak information here.

const stringifiedAuthState = `__REDWOOD__SERVER__AUTH_STATE__ = ${JSON.stringify(
sanitizeServerAuthState(value)
)};`

return (
<>
<script
id="__REDWOOD__SERVER_AUTH_STATE__"
dangerouslySetInnerHTML={{
__html: stringifiedAuthState,
}}
/>

<ServerAuthContext.Provider value={value}>
{children}
</ServerAuthContext.Provider>
</>
)
}
function sanitizeServerAuthState(value: ServerAuthState) {
const sanitizedState = { ...value }
// Remove the cookie from being printed onto the DOM
// harmless, but still...
delete sanitizedState.cookieHeader

return sanitizedState
}
3 changes: 3 additions & 0 deletions packages/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ export { AuthContextInterface, CurrentUser } from './AuthContext'
export { useNoAuth, UseAuth } from './useAuth'
export { createAuthentication } from './authFactory'
export type { AuthImplementation } from './AuthImplementation'

export * from './AuthProvider/AuthProviderState'
export * from './AuthProvider/ServerAuthProvider'
2 changes: 1 addition & 1 deletion packages/codemods/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@svgr/core": "8.0.0",
"@svgr/plugin-jsx": "8.0.1",
"@vscode/ripgrep": "1.15.6",
"@whatwg-node/fetch": "0.9.14",
"@whatwg-node/fetch": "0.9.16",
"cheerio": "1.0.0-rc.12",
"core-js": "3.34.0",
"deepmerge": "4.3.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@types/jsonwebtoken": "9.0.5",
"@types/lodash": "4.14.201",
"@types/uuid": "9.0.7",
"@whatwg-node/fetch": "0.9.14",
"@whatwg-node/fetch": "0.9.16",
"aws-lambda": "1.0.7",
"jest": "29.7.0",
"jsonwebtoken": "9.0.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/prerender/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@redwoodjs/router": "6.0.7",
"@redwoodjs/structure": "6.0.7",
"@redwoodjs/web": "6.0.7",
"@whatwg-node/fetch": "0.9.14",
"@whatwg-node/fetch": "0.9.16",
"babel-plugin-ignore-html-and-css-imports": "0.1.0",
"cheerio": "1.0.0-rc.12",
"core-js": "3.34.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/telemetry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@babel/runtime-corejs3": "7.23.6",
"@redwoodjs/project-config": "6.0.7",
"@redwoodjs/structure": "6.0.7",
"@whatwg-node/fetch": "0.9.14",
"@whatwg-node/fetch": "0.9.16",
"ci-info": "4.0.0",
"core-js": "3.34.0",
"envinfo": "7.11.0",
Expand Down
11 changes: 9 additions & 2 deletions packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@
"types": "./dist/react-server-dom-webpack/node-loader.d.ts",
"default": "./dist/react-server-dom-webpack/node-loader.js"
},
"./bins/rw-vite-build.mjs": "./bins/rw-vite-build.mjs"
"./bins/rw-vite-build.mjs": "./bins/rw-vite-build.mjs",
"./middleware": {
"types": "./dist/middleware/index.d.ts",
"default": "./dist/middleware/index.js"
}
},
"bin": {
"rw-dev-fe": "./dist/devFeServer.js",
Expand Down Expand Up @@ -70,10 +74,12 @@
"@redwoodjs/web": "6.0.7",
"@swc/core": "1.3.60",
"@vitejs/plugin-react": "4.2.1",
"@whatwg-node/server": "0.9.18",
"@whatwg-node/fetch": "0.9.16",
"@whatwg-node/server": "0.9.24",
"acorn-loose": "8.3.0",
"buffer": "6.0.3",
"busboy": "^1.6.0",
"cookie": "0.6.0",
"core-js": "3.34.0",
"dotenv-defaults": "5.0.2",
"express": "4.18.2",
Expand All @@ -87,6 +93,7 @@
"devDependencies": {
"@babel/cli": "7.23.4",
"@types/busboy": "^1",
"@types/cookie": "^0",
"@types/express": "4",
"@types/react": "18.2.37",
"@types/yargs-parser": "21.0.3",
Expand Down
16 changes: 16 additions & 0 deletions packages/vite/src/devFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getProjectRoutes } from '@redwoodjs/internal/dist/routes'
import type { Paths } from '@redwoodjs/project-config'
import { getConfig, getPaths } from '@redwoodjs/project-config'

import { invoke } from './middleware/invokeMiddleware'
import { collectCssPaths, componentsModules } from './streaming/collectCss'
import { createReactStreamingHandler } from './streaming/createReactStreamingHandler'
import { registerFwGlobals } from './streaming/registerGlobals'
Expand Down Expand Up @@ -80,6 +81,21 @@ async function createServer() {
: route.pathDefinition

app.get(expressPathDef, createServerAdapter(routeHandler))

app.post(
'*',
createServerAdapter(async (req: Request) => {
const entryServerImport = await vite.ssrLoadModule(
rwPaths.web.entryServer as string // already validated in dev server
)

const middleware = entryServerImport.middleware

const [mwRes] = await invoke(req, middleware)

return mwRes.toResponse()
})
)
}

const port = getConfig().web.port
Expand Down
83 changes: 83 additions & 0 deletions packages/vite/src/middleware/CookieJar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* to keep the tests a little cleaner by using ! */

import { describe, expect, test } from 'vitest'

import { CookieJar } from './CookieJar'

describe('CookieJar', () => {
// Grabbed from github
const cookieJar = new CookieJar(
'color_mode=%7B%22color_mode%22%3A%22light%22%2C%22light_theme%22%3A%7B%22name%22%3A%22light%22%2C%22color_mode%22%3A%22light%22%7D%2C%22dark_theme%22%3A%7B%22name%22%3A%22dark_dimmed%22%2C%22color_mode%22%3A%22dark%22%7D%7D; preferred_color_mode=dark; tz=Asia%2FBangkok'
)

test('instatitates cookie jar from a cookie string', () => {
expect(cookieJar.get('color_mode')).toStrictEqual({
value: JSON.stringify({
color_mode: 'light',
light_theme: { name: 'light', color_mode: 'light' },
dark_theme: { name: 'dark_dimmed', color_mode: 'dark' },
}),
})

expect(cookieJar.get('preferred_color_mode')).toStrictEqual({
value: 'dark',
})

expect(cookieJar.get('tz')).toStrictEqual({
value: 'Asia/Bangkok',
})
})

describe('Helper methods like JS Map', () => {
test('has', () => {
expect(cookieJar.has('color_mode')).toBe(true)
expect(cookieJar.has('bazinga')).toBe(false)
})

test('size', () => {
expect(cookieJar.size).toBe(3)
})

test('entries', () => {
const iterator = cookieJar.entries()

expect(iterator.next().done).toBe(false)
expect(iterator.next().done).toBe(false)

const finalItem = iterator.next()
expect(finalItem.done).toBe(false)
expect(finalItem.value).toStrictEqual(['tz', { value: 'Asia/Bangkok' }])

expect(iterator.next().done).toBe(true)
})

// @MARK: API convention worth discussing!
// Unset is a little special, it doesn't actually delete the cookie
// but sets it to expire and sets an empty value
test('unset', () => {
const myJar = new CookieJar('auth_provider=kittens; session=woof-124556')

myJar.unset('auth_provider')

const { value: authProviderValue, options: authProviderOptions } =
myJar.get('auth_provider')!

expect(authProviderValue).toBeFalsy()
expect(authProviderOptions!.expires).toStrictEqual(new Date(0))
})

test('clear All', () => {
const myJar = new CookieJar('auth_provider=kittens; session=woof-124556')
myJar.clear()
expect(myJar.size).toBe(0)
})

test('clear by name', () => {
const myJar = new CookieJar('auth_provider=kittens; session=woof-124556')
myJar.clear('session')
expect(myJar.size).toBe(1)
expect(myJar.get('session')).toBeUndefined()
})
})
})
Loading