From f99aef295b73c82cef2b027a825976630861810b Mon Sep 17 00:00:00 2001 From: Alexander Buzin Date: Fri, 26 Jan 2024 20:00:00 +0200 Subject: [PATCH] feat: SessionAuth SSR support (initialSessionAuthContext) (#789) * feat: Add SSR support for SessionAuth (initialSessionAuthContext) * Add preloaded to sessionAuth test file * Rename preloaded to isContextFromSSR * changes to SSRSessionContextType * add missing initialSessionAuthContext test --- lib/build/index2.js | 21 +++++++--- lib/build/recipe/session/sessionAuth.d.ts | 5 +++ lib/build/recipe/session/types.d.ts | 2 + lib/ts/recipe/session/index.ts | 3 +- lib/ts/recipe/session/sessionAuth.tsx | 38 ++++++++++++++++--- lib/ts/recipe/session/types.ts | 3 ++ package-lock.json | 12 +++--- recipe/session/index.js | 1 + test/unit/componentOverrides.test.tsx | 1 + .../recipe/emailpassword/signInUp.test.tsx | 1 + .../recipe/passwordless/signInUp.test.tsx | 1 + test/unit/recipe/session/sessionAuth.test.tsx | 35 ++++++++++++++++- test/unit/recipe/thirdparty/signInUp.test.tsx | 1 + .../thirdpartyemailpassword/signInUp.test.tsx | 1 + .../thirdpartypasswordless/signInUp.test.tsx | 1 + 15 files changed, 107 insertions(+), 19 deletions(-) diff --git a/lib/build/index2.js b/lib/build/index2.js index 084c5ad42..98fa2fe3d 100644 --- a/lib/build/index2.js +++ b/lib/build/index2.js @@ -863,9 +863,15 @@ var SessionAuth = function (_a) { 'requireAuth prop should not change. If you are seeing this, it probably means that you are using SessionAuth in multiple routes with different values for requireAuth. To solve this, try adding the "key" prop to all uses of SessionAuth like ' ); } + var initialContext = props.initialSessionAuthContext + ? genericComponentOverrideContext.__assign( + genericComponentOverrideContext.__assign({}, props.initialSessionAuthContext), + { invalidClaims: [] } + ) + : { loading: true }; // Reusing the parent context was removed because it caused a redirect loop in an edge case // because it'd also reuse the invalid claims part until it loaded. - var _c = React.useState({ loading: true }), + var _c = React.useState(initialContext), context = _c[0], setContext = _c[1]; var session = React.useRef(); @@ -912,6 +918,7 @@ var SessionAuth = function (_a) { return [ 2 /*return*/, { + isContextFromSSR: false, loading: false, doesSessionExist: false, accessTokenPayload: {}, @@ -952,6 +959,7 @@ var SessionAuth = function (_a) { return [ 2 /*return*/, { + isContextFromSSR: false, loading: false, doesSessionExist: false, accessTokenPayload: {}, @@ -962,6 +970,7 @@ var SessionAuth = function (_a) { case 6: _b.trys.push([6, 9, , 11]); _a = { + isContextFromSSR: false, loading: false, doesSessionExist: true, invalidClaims: invalidClaims, @@ -999,6 +1008,7 @@ var SessionAuth = function (_a) { return [ 2 /*return*/, { + isContextFromSSR: false, loading: false, doesSessionExist: false, accessTokenPayload: {}, @@ -1138,7 +1148,7 @@ var SessionAuth = function (_a) { setContext( genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, event.sessionContext), - { loading: false, invalidClaims: invalidClaims } + { isContextFromSSR: false, loading: false, invalidClaims: invalidClaims } ) ); return [ @@ -1165,6 +1175,7 @@ var SessionAuth = function (_a) { genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, event.sessionContext), { + isContextFromSSR: false, loading: false, invalidClaims: invalidClaims, accessDeniedValidatorError: failureRedirectInfo.failedClaim, @@ -1178,7 +1189,7 @@ var SessionAuth = function (_a) { setContext( genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, event.sessionContext), - { loading: false, invalidClaims: invalidClaims } + { isContextFromSSR: false, loading: false, invalidClaims: invalidClaims } ) ); return [2 /*return*/]; @@ -1186,7 +1197,7 @@ var SessionAuth = function (_a) { setContext( genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, event.sessionContext), - { loading: false, invalidClaims: [] } + { isContextFromSSR: false, loading: false, invalidClaims: [] } ) ); return [2 /*return*/]; @@ -1194,7 +1205,7 @@ var SessionAuth = function (_a) { setContext( genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, event.sessionContext), - { loading: false, invalidClaims: [] } + { isContextFromSSR: false, loading: false, invalidClaims: [] } ) ); if (props.onSessionExpired !== undefined) { diff --git a/lib/build/recipe/session/sessionAuth.d.ts b/lib/build/recipe/session/sessionAuth.d.ts index 95cdd4607..c9e7f60da 100644 --- a/lib/build/recipe/session/sessionAuth.d.ts +++ b/lib/build/recipe/session/sessionAuth.d.ts @@ -1,8 +1,13 @@ import React from "react"; +import type { SSRSessionContextType } from "./types"; import type { Navigate, ReactComponentClass, SessionClaimValidator, UserContext } from "../../types"; import type { PropsWithChildren } from "react"; import type { ClaimValidationError } from "supertokens-web-js/recipe/session"; export declare type SessionAuthProps = { + /** + * Initial context that is rendered on a server side (SSR). + */ + initialSessionAuthContext?: SSRSessionContextType; /** * For a detailed explanation please see https://github.com/supertokens/supertokens-auth-react/issues/570 */ diff --git a/lib/build/recipe/session/types.d.ts b/lib/build/recipe/session/types.d.ts index 84028d572..55894626e 100644 --- a/lib/build/recipe/session/types.d.ts +++ b/lib/build/recipe/session/types.d.ts @@ -31,6 +31,7 @@ export declare type SessionContextUpdate = { accessTokenPayload: any; }; export declare type LoadedSessionContext = { + isContextFromSSR: boolean; loading: false; invalidClaims: ClaimValidationError[]; accessDeniedValidatorError?: ClaimValidationError; @@ -40,6 +41,7 @@ export declare type SessionContextType = | { loading: true; }; +export declare type SSRSessionContextType = Omit; export declare type AccessDeniedThemeProps = { recipe: Session; navigate: Navigate; diff --git a/lib/ts/recipe/session/index.ts b/lib/ts/recipe/session/index.ts index 4888ef6c3..c3f5a424a 100644 --- a/lib/ts/recipe/session/index.ts +++ b/lib/ts/recipe/session/index.ts @@ -25,7 +25,7 @@ import { RecipeComponentsOverrideContextProvider } from "./componentOverrideCont import Session from "./recipe"; import SessionAuthWrapper from "./sessionAuth"; import SessionContext from "./sessionContext"; -import { InputType, SessionContextType } from "./types"; +import { InputType, SessionContextType, SSRSessionContextType } from "./types"; import { useClaimValue as useClaimValueFunc } from "./useClaimValue"; import useSessionContextFunc from "./useSessionContext"; @@ -150,6 +150,7 @@ export { InputType, SessionContext, SessionContextType, + SSRSessionContextType, BooleanClaim, ClaimValidationError, ClaimValidationResult, diff --git a/lib/ts/recipe/session/sessionAuth.tsx b/lib/ts/recipe/session/sessionAuth.tsx index 18e8043ae..b6a27be3b 100644 --- a/lib/ts/recipe/session/sessionAuth.tsx +++ b/lib/ts/recipe/session/sessionAuth.tsx @@ -28,12 +28,21 @@ import Session from "./recipe"; import SessionContext from "./sessionContext"; import { getFailureRedirectionInfo } from "./utils"; -import type { LoadedSessionContext, RecipeEventWithSessionContext, SessionContextType } from "./types"; +import type { + LoadedSessionContext, + RecipeEventWithSessionContext, + SessionContextType, + SSRSessionContextType, +} from "./types"; import type { Navigate, ReactComponentClass, SessionClaimValidator, UserContext } from "../../types"; import type { PropsWithChildren } from "react"; import type { ClaimValidationError } from "supertokens-web-js/recipe/session"; export type SessionAuthProps = { + /** + * Initial context that is rendered on a server side (SSR). + */ + initialSessionAuthContext?: SSRSessionContextType; /** * For a detailed explanation please see https://github.com/supertokens/supertokens-auth-react/issues/570 */ @@ -65,9 +74,16 @@ const SessionAuth: React.FC> = ({ children, ); } + const initialContext: SessionContextType = props.initialSessionAuthContext + ? { + ...props.initialSessionAuthContext, + invalidClaims: [], // invalidClaims is currently unsupported on server (SSR) + } + : { loading: true }; + // Reusing the parent context was removed because it caused a redirect loop in an edge case // because it'd also reuse the invalid claims part until it loaded. - const [context, setContext] = useState({ loading: true }); + const [context, setContext] = useState(initialContext); const session = useRef(); @@ -101,6 +117,7 @@ const SessionAuth: React.FC> = ({ children, if (sessionExists === false) { return { + isContextFromSSR: false, loading: false, doesSessionExist: false, accessTokenPayload: {}, @@ -128,6 +145,7 @@ const SessionAuth: React.FC> = ({ children, throw err; } return { + isContextFromSSR: false, loading: false, doesSessionExist: false, accessTokenPayload: {}, @@ -138,6 +156,7 @@ const SessionAuth: React.FC> = ({ children, try { return { + isContextFromSSR: false, loading: false, doesSessionExist: true, invalidClaims, @@ -159,6 +178,7 @@ const SessionAuth: React.FC> = ({ children, // This means that loading the access token or the userId failed // This may happen if the server cleared the error since the validation was done which should be extremely rare return { + isContextFromSSR: false, loading: false, doesSessionExist: false, accessTokenPayload: {}, @@ -246,7 +266,12 @@ const SessionAuth: React.FC> = ({ children, userContext, }); if (failureRedirectInfo.redirectPath) { - setContext({ ...event.sessionContext, loading: false, invalidClaims }); + setContext({ + ...event.sessionContext, + isContextFromSSR: false, + loading: false, + invalidClaims, + }); return await SuperTokens.getInstanceOrThrow().redirectToUrl( failureRedirectInfo.redirectPath, navigate @@ -259,21 +284,22 @@ const SessionAuth: React.FC> = ({ children, }); return setContext({ ...event.sessionContext, + isContextFromSSR: false, loading: false, invalidClaims, accessDeniedValidatorError: failureRedirectInfo.failedClaim, }); } } - setContext({ ...event.sessionContext, loading: false, invalidClaims }); + setContext({ ...event.sessionContext, isContextFromSSR: false, loading: false, invalidClaims }); return; } case "SIGN_OUT": - setContext({ ...event.sessionContext, loading: false, invalidClaims: [] }); + setContext({ ...event.sessionContext, isContextFromSSR: false, loading: false, invalidClaims: [] }); return; case "UNAUTHORISED": - setContext({ ...event.sessionContext, loading: false, invalidClaims: [] }); + setContext({ ...event.sessionContext, isContextFromSSR: false, loading: false, invalidClaims: [] }); if (props.onSessionExpired !== undefined) { props.onSessionExpired(); } else if (props.requireAuth !== false && props.doRedirection !== false) { diff --git a/lib/ts/recipe/session/types.ts b/lib/ts/recipe/session/types.ts index 4050d4bd9..9214117a8 100644 --- a/lib/ts/recipe/session/types.ts +++ b/lib/ts/recipe/session/types.ts @@ -50,6 +50,7 @@ export type SessionContextUpdate = { }; export type LoadedSessionContext = { + isContextFromSSR: boolean; loading: false; invalidClaims: ClaimValidationError[]; accessDeniedValidatorError?: ClaimValidationError; @@ -61,6 +62,8 @@ export type SessionContextType = loading: true; }; +export type SSRSessionContextType = Omit; + export type AccessDeniedThemeProps = { recipe: Session; navigate: Navigate; diff --git a/package-lock.json b/package-lock.json index 9e01540a7..bae114388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15917,9 +15917,9 @@ } }, "node_modules/supertokens-website": { - "version": "17.0.4", - "resolved": "https://registry.npmjs.org/supertokens-website/-/supertokens-website-17.0.4.tgz", - "integrity": "sha512-ayWhEFvspUe26YhM1bq11ssEpnFCZIsoHZtJwJHgHsoflfMUKdgrzOix/bboI0PWJeNTUphHyZebw0ApctaS1Q==", + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/supertokens-website/-/supertokens-website-17.0.5.tgz", + "integrity": "sha512-NBOiKO3NV2VBAFgO+ZEmpOPVde2BwOjB6T0qjj2XaZX4jh+6yDGhrckJMwF5R0ucpTgOQXmBrpDnUJ5kFZlgiQ==", "peer": true, "dependencies": { "browser-tabs-lock": "^1.3.0", @@ -28941,9 +28941,9 @@ } }, "supertokens-website": { - "version": "17.0.4", - "resolved": "https://registry.npmjs.org/supertokens-website/-/supertokens-website-17.0.4.tgz", - "integrity": "sha512-ayWhEFvspUe26YhM1bq11ssEpnFCZIsoHZtJwJHgHsoflfMUKdgrzOix/bboI0PWJeNTUphHyZebw0ApctaS1Q==", + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/supertokens-website/-/supertokens-website-17.0.5.tgz", + "integrity": "sha512-NBOiKO3NV2VBAFgO+ZEmpOPVde2BwOjB6T0qjj2XaZX4jh+6yDGhrckJMwF5R0ucpTgOQXmBrpDnUJ5kFZlgiQ==", "peer": true, "requires": { "browser-tabs-lock": "^1.3.0", diff --git a/recipe/session/index.js b/recipe/session/index.js index cdb7217bd..23cbbbd3b 100644 --- a/recipe/session/index.js +++ b/recipe/session/index.js @@ -13,6 +13,7 @@ * under the License. */ "use strict"; +"use client"; // Important for NextJS support (SessionAuth is a client component) function __export(m) { for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; } diff --git a/test/unit/componentOverrides.test.tsx b/test/unit/componentOverrides.test.tsx index 11adfbfc0..75ac05cd6 100644 --- a/test/unit/componentOverrides.test.tsx +++ b/test/unit/componentOverrides.test.tsx @@ -214,6 +214,7 @@ describe("Components override per recipe provider", () => { ], }); setMockResolvesSession({ + isContextFromSSR: false, userId: "mock-user-id", accessTokenPayload: {}, invalidClaims: [], diff --git a/test/unit/recipe/emailpassword/signInUp.test.tsx b/test/unit/recipe/emailpassword/signInUp.test.tsx index f6bc5b8ae..9bf27eb83 100644 --- a/test/unit/recipe/emailpassword/signInUp.test.tsx +++ b/test/unit/recipe/emailpassword/signInUp.test.tsx @@ -56,6 +56,7 @@ describe("EmailPassword.SignInAndUp", () => { }); setMockResolvesSession({ + isContextFromSSR: false, userId: "mock-user-id", accessTokenPayload: {}, invalidClaims: [], diff --git a/test/unit/recipe/passwordless/signInUp.test.tsx b/test/unit/recipe/passwordless/signInUp.test.tsx index 53be0c7c8..104412f2b 100644 --- a/test/unit/recipe/passwordless/signInUp.test.tsx +++ b/test/unit/recipe/passwordless/signInUp.test.tsx @@ -57,6 +57,7 @@ describe("Passwordless.SingInUp", () => { }); setMockResolvesSession({ + isContextFromSSR: false, userId: "mock-user-id", accessTokenPayload: {}, invalidClaims: [], diff --git a/test/unit/recipe/session/sessionAuth.test.tsx b/test/unit/recipe/session/sessionAuth.test.tsx index f76daa20f..495d9af90 100644 --- a/test/unit/recipe/session/sessionAuth.test.tsx +++ b/test/unit/recipe/session/sessionAuth.test.tsx @@ -5,7 +5,7 @@ import SuperTokens from "../../../../lib/ts/superTokens"; import Session from "../../../../lib/ts/recipe/session/recipe"; import SessionAuth from "../../../../lib/ts/recipe/session/sessionAuth"; import SessionContext from "../../../../lib/ts/recipe/session/sessionContext"; -import { SessionContextType } from "../../../../lib/ts/recipe/session"; +import { SessionContextType, SSRSessionContextType } from "../../../../lib/ts/recipe/session"; import { PrimitiveClaim, SessionClaim, useClaimValue } from "../../../../lib/ts/recipe/session"; import * as utils from "supertokens-web-js/utils"; @@ -90,6 +90,7 @@ describe("SessionAuth", () => { doesSessionExist: true, invalidClaims: [], loading: false, + isContextFromSSR: false, }); }); @@ -155,6 +156,34 @@ describe("SessionAuth", () => { result.unmount(); }); + + test("set initial SSR context (initialSessionAuthContext)", async () => { + const initialSessionAuthContext: SSRSessionContextType = { + isContextFromSSR: true, + loading: false, + doesSessionExist: true, + accessTokenPayload: { + foo: "bar", + }, + userId: "mock-ssr-user-id", + }; + + // when + const result = render( + + + + ); + + // then + expect(await result.findByText(/^userId:/)).toHaveTextContent(`userId: mock-ssr-user-id`); + expect(await result.findByText(/^accessTokenPayload:/)).toHaveTextContent( + `accessTokenPayload: ${JSON.stringify({ + foo: "bar", + })}` + ); + }); + test("set initial context", async () => { // when const result = render( @@ -216,6 +245,7 @@ describe("SessionAuth", () => { userId: "mock-id", invalidClaims: [], loading: false, + isContextFromSSR: false, }); // when @@ -508,6 +538,7 @@ describe("SessionAuth", () => { userId: "mock-id", invalidClaims: [{ validatorId: "st-test-claim", reason: "test-reason" }], loading: false, + isContextFromSSR: false, }); await act(() => @@ -664,6 +695,7 @@ describe("SessionAuth", () => { userId: "", invalidClaims: [], loading: false, + isContextFromSSR: false, }); // when @@ -692,6 +724,7 @@ describe("SessionAuth", () => { userId: "", invalidClaims: [], loading: false, + isContextFromSSR: false, }); // when diff --git a/test/unit/recipe/thirdparty/signInUp.test.tsx b/test/unit/recipe/thirdparty/signInUp.test.tsx index 3814af60b..a88ddd625 100644 --- a/test/unit/recipe/thirdparty/signInUp.test.tsx +++ b/test/unit/recipe/thirdparty/signInUp.test.tsx @@ -68,6 +68,7 @@ describe("ThirdParty.SignInAndUp", () => { }); setMockResolvesSession({ + isContextFromSSR: false, userId: "mock-user-id", accessTokenPayload: {}, invalidClaims: [], diff --git a/test/unit/recipe/thirdpartyemailpassword/signInUp.test.tsx b/test/unit/recipe/thirdpartyemailpassword/signInUp.test.tsx index b249b8562..de22f745e 100644 --- a/test/unit/recipe/thirdpartyemailpassword/signInUp.test.tsx +++ b/test/unit/recipe/thirdpartyemailpassword/signInUp.test.tsx @@ -56,6 +56,7 @@ describe("ThirdPartyEmailPassword.SignInAndUp", () => { }); setMockResolvesSession({ + isContextFromSSR: false, userId: "mock-user-id", accessTokenPayload: {}, invalidClaims: [], diff --git a/test/unit/recipe/thirdpartypasswordless/signInUp.test.tsx b/test/unit/recipe/thirdpartypasswordless/signInUp.test.tsx index 6fd489232..a84ffb557 100644 --- a/test/unit/recipe/thirdpartypasswordless/signInUp.test.tsx +++ b/test/unit/recipe/thirdpartypasswordless/signInUp.test.tsx @@ -57,6 +57,7 @@ describe("ThirdPartyPasswordless.SignInAndUp", () => { }); setMockResolvesSession({ + isContextFromSSR: false, userId: "mock-user-id", accessTokenPayload: {}, invalidClaims: [],