From a560620e36db68cd10ab6bbb0ab7200e452b05fa Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Wed, 24 Jan 2024 17:14:45 +0700 Subject: [PATCH 01/34] Add code for invoking middleware (WIP) Add serverAuthState --- packages/auth/ambient.d.ts | 4 + .../auth/src/AuthProvider/AuthProvider.tsx | 23 +++++- .../src/AuthProvider/ServerAuthProvider.tsx | 79 +++++++++++++++++++ packages/auth/src/index.ts | 3 + packages/vite/src/devFeServer.ts | 24 ++++++ .../streaming/createReactStreamingHandler.ts | 19 +++++ packages/vite/src/streaming/streamHelpers.ts | 33 +++++--- 7 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 packages/auth/src/AuthProvider/ServerAuthProvider.tsx diff --git a/packages/auth/ambient.d.ts b/packages/auth/ambient.d.ts index b4e7a73a07c2..c53e66b14846 100644 --- a/packages/auth/ambient.d.ts +++ b/packages/auth/ambient.d.ts @@ -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 + namespace NodeJS { interface Global { /** URL or absolute path to the GraphQL serverless function */ diff --git a/packages/auth/src/AuthProvider/AuthProvider.tsx b/packages/auth/src/AuthProvider/AuthProvider.tsx index b4a5d9a20d29..97afe439b235 100644 --- a/packages/auth/src/AuthProvider/AuthProvider.tsx +++ b/packages/auth/src/AuthProvider/AuthProvider.tsx @@ -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' @@ -82,9 +83,11 @@ export function createAuthProvider< }: AuthProviderProps) => { // const [hasRestoredState, setHasRestoredState] = useState(false) + const serverAuthState = useContext(ServerAuthContext) + const [authProviderState, setAuthProviderState] = useState< AuthProviderState - >(defaultAuthProviderState) + >(serverAuthState || defaultAuthProviderState) const getToken = useToken(authImplementation) @@ -130,12 +133,24 @@ export function createAuthProvider< // Whenever the authImplementation is ready to go, restore auth and reauthenticate useEffect(() => { async function doRestoreState() { + // @MARK: this is where we fetch currentUser from graphql again + // because without SSR, initial state doesn't exist + // what we want to do here is to conditionally call reauthenticate + // so that the restoreAuthState comes from the injected state + + // the problem is that reauthenticate does both getCurrentUser and udpate the auth state await authImplementation.restoreAuthState?.() - reauthenticate() + + // If the inital state didn't come from the server (or was restored before) + // reauthenticate will make an API call to the middleware to receive the current user + // (instead of called the graphql endpoint with currentUser) + if (!serverAuthState) { + reauthenticate() + } } doRestoreState() - }, [reauthenticate]) + }, [reauthenticate, serverAuthState]) return ( & { + // Used by AuthProvider in getToken. We can probably remove this + encryptedSession?: string | null + 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( + 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 + // not totally necessary, but it's nice to not have them in the DOM + // @MARK: needs discussion! + const stringifiedAuthState = `__REDWOOD__SERVER__AUTH_STATE__ = ${JSON.stringify( + sanitizeServerAuthState(value) + )};` + + return ( + <> +