From 88f9500b9632f113fe1df018b84f1bccc4038f0c Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 15:59:47 +0530 Subject: [PATCH] Add support for re-using duplicated code in nextjs from customframework --- lib/build/customFramework.d.ts | 11 +++- lib/build/customFramework.js | 95 ++++++++++++++++++------------ lib/build/nextjs.d.ts | 2 + lib/build/nextjs.js | 66 +++------------------ lib/ts/customFramework.ts | 104 ++++++++++++++++++++------------- lib/ts/nextjs.ts | 77 ++++-------------------- 6 files changed, 153 insertions(+), 202 deletions(-) diff --git a/lib/build/customFramework.d.ts b/lib/build/customFramework.d.ts index 5d67e9952..169b3cc4d 100644 --- a/lib/build/customFramework.d.ts +++ b/lib/build/customFramework.d.ts @@ -4,7 +4,7 @@ * that can be used to easily integrate the SDK with most * frameworks if they are not directly supported. */ -import { PreParsedRequest } from "./framework/custom"; +import { CollectingResponse, PreParsedRequest } from "./framework/custom"; import { SessionContainer, VerifySessionOptions } from "./recipe/session"; import { JWTPayload } from "jose"; export declare type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; @@ -48,3 +48,12 @@ export declare function withSession( options?: VerifySessionOptions, userContext?: Record ): Promise; +export declare function addCookies( + baseResponse: CollectingResponse, + userResponse: UserResponseType +): UserResponseType; +export declare function handleError( + err: any, + baseRequest: PreParsedRequest, + baseResponse: CollectingResponse +): Promise; diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index 134c8c443..369c092ab 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -10,7 +10,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.withSession = exports.getSessionForSSR = exports.handleAuthAPIRequest = exports.getHandleCall = exports.getQueryFromRequest = exports.getCookieFromRequest = exports.createPreParsedRequest = void 0; +exports.handleError = exports.addCookies = exports.withSession = exports.getSessionForSSR = exports.handleAuthAPIRequest = exports.getHandleCall = exports.getQueryFromRequest = exports.getCookieFromRequest = exports.createPreParsedRequest = void 0; const cookie_1 = require("cookie"); const custom_1 = require("./framework/custom"); const session_1 = __importDefault(require("./recipe/session")); @@ -210,46 +210,65 @@ async function withSession(request, handler, options, userContext) { try { userResponse = await handler(undefined, session); } catch (err) { - await custom_1.errorHandler()(err, baseRequest, baseResponse, (errorHandlerError) => { - if (errorHandlerError) { - throw errorHandlerError; - } - }); - // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. - userResponse = new Response(baseResponse.body, { - status: baseResponse.statusCode, - headers: baseResponse.headers, - }); + userResponse = await handleError(err, baseRequest, baseResponse); } - let didAddCookies = false; - let didAddHeaders = false; - for (const respCookie of baseResponse.cookies) { - didAddCookies = true; - userResponse.headers.append( - "Set-Cookie", - cookie_1.serialize(respCookie.key, respCookie.value, { - domain: respCookie.domain, - expires: new Date(respCookie.expires), - httpOnly: respCookie.httpOnly, - path: respCookie.path, - sameSite: respCookie.sameSite, - secure: respCookie.secure, - }) - ); - } - baseResponse.headers.forEach((value, key) => { - didAddHeaders = true; - userResponse.headers.set(key, value); - }); - if (didAddCookies || didAddHeaders) { - if (!userResponse.headers.has("Cache-Control")) { - // This is needed for production deployments with Vercel - userResponse.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); - } - } - return userResponse; + return addCookies(baseResponse, userResponse); } catch (error) { return await handler(error, undefined); } } exports.withSession = withSession; +function addCookies(baseResponse, userResponse) { + /** + * Add cookies to the userResponse passed by copying it from the baseResponse. + */ + let didAddCookies = false; + let didAddHeaders = false; + for (const respCookie of baseResponse.cookies) { + didAddCookies = true; + userResponse.headers.append( + "Set-Cookie", + cookie_1.serialize(respCookie.key, respCookie.value, { + domain: respCookie.domain, + expires: new Date(respCookie.expires), + httpOnly: respCookie.httpOnly, + path: respCookie.path, + sameSite: respCookie.sameSite, + secure: respCookie.secure, + }) + ); + } + baseResponse.headers.forEach((value, key) => { + didAddHeaders = true; + userResponse.headers.set(key, value); + }); + /** + * For some deployment services (Vercel for example) production builds can return cached results for + * APIs with older header values. In this case if the session tokens have changed (because of refreshing + * for example) the cached result would still contain the older tokens and sessions would stop working. + * + * As a result, if we add cookies or headers from base response we also set the Cache-Control header + * to make sure that the final result is not a cached version. + */ + if (didAddCookies || didAddHeaders) { + if (!userResponse.headers.has("Cache-Control")) { + // This is needed for production deployments with Vercel + userResponse.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); + } + } + return userResponse; +} +exports.addCookies = addCookies; +async function handleError(err, baseRequest, baseResponse) { + await custom_1.errorHandler()(err, baseRequest, baseResponse, (errorHandlerError) => { + if (errorHandlerError) { + throw errorHandlerError; + } + }); + // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. + return new Response(baseResponse.body, { + status: baseResponse.statusCode, + headers: baseResponse.headers, + }); +} +exports.handleError = handleError; diff --git a/lib/build/nextjs.d.ts b/lib/build/nextjs.d.ts index 7b06b2936..038c70f52 100644 --- a/lib/build/nextjs.d.ts +++ b/lib/build/nextjs.d.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { CollectingResponse, PreParsedRequest } from "./framework/custom"; import { SessionContainer, VerifySessionOptions } from "./recipe/session"; +import { GetCookieFn } from "./customFramework"; declare type PartialNextRequest = { method: string; url: string; @@ -20,6 +21,7 @@ export default class NextJS { request: any, response: any ): Promise; + static getCookieExtractor(): GetCookieFn; static getAppDirRequestHandler( NextResponse: typeof Response ): (req: T) => Promise; diff --git a/lib/build/nextjs.js b/lib/build/nextjs.js index f65a3f79a..80f07d72b 100644 --- a/lib/build/nextjs.js +++ b/lib/build/nextjs.js @@ -76,9 +76,11 @@ class NextJS { } }); } + static getCookieExtractor() { + return (req) => Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); + } static getAppDirRequestHandler(NextResponse) { - const getCookieFromNextReq = (req) => - Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); + const getCookieFromNextReq = this.getCookieExtractor(); const stMiddleware = custom_1.middleware((req) => { return customFramework_1.createPreParsedRequest(req, getCookieFromNextReq); }); @@ -222,68 +224,16 @@ class NextJS { } } static async withPreParsedRequestResponse(req, handler) { - const query = Object.fromEntries(new URL(req.url).searchParams.entries()); - const cookies = Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); - let baseRequest = new custom_1.PreParsedRequest({ - method: req.method, - url: req.url, - query: query, - headers: req.headers, - cookies: cookies, - getFormBody: () => req.formData(), - getJSONBody: () => req.json(), - }); + const getCookieFromNextReq = this.getCookieExtractor(); + let baseRequest = customFramework_1.createPreParsedRequest(req, getCookieFromNextReq); let baseResponse = new custom_1.CollectingResponse(); let userResponse; try { userResponse = await handler(baseRequest, baseResponse); } catch (err) { - await custom_1.errorHandler()(err, baseRequest, baseResponse, (errorHandlerError) => { - if (errorHandlerError) { - throw errorHandlerError; - } - }); - // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. - userResponse = new Response(baseResponse.body, { - status: baseResponse.statusCode, - headers: baseResponse.headers, - }); - } - let didAddCookies = false; - let didAddHeaders = false; - for (const respCookie of baseResponse.cookies) { - didAddCookies = true; - userResponse.headers.append( - "Set-Cookie", - cookie_1.serialize(respCookie.key, respCookie.value, { - domain: respCookie.domain, - expires: new Date(respCookie.expires), - httpOnly: respCookie.httpOnly, - path: respCookie.path, - sameSite: respCookie.sameSite, - secure: respCookie.secure, - }) - ); - } - baseResponse.headers.forEach((value, key) => { - didAddHeaders = true; - userResponse.headers.set(key, value); - }); - /** - * For some deployment services (Vercel for example) production builds can return cached results for - * APIs with older header values. In this case if the session tokens have changed (because of refreshing - * for example) the cached result would still contain the older tokens and sessions would stop working. - * - * As a result, if we add cookies or headers from base response we also set the Cache-Control header - * to make sure that the final result is not a cached version. - */ - if (didAddCookies || didAddHeaders) { - if (!userResponse.headers.has("Cache-Control")) { - // This is needed for production deployments with Vercel - userResponse.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); - } + userResponse = await customFramework_1.handleError(err, baseRequest, baseResponse); } - return userResponse; + return customFramework_1.addCookies(baseResponse, userResponse); } } exports.default = NextJS; diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index de95c9762..661d16627 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -255,50 +255,76 @@ export async function withSession( try { userResponse = await handler(undefined, session); } catch (err) { - await errorHandler()(err, baseRequest, baseResponse, (errorHandlerError: Error) => { - if (errorHandlerError) { - throw errorHandlerError; - } - }); - - // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. - userResponse = new Response(baseResponse.body, { - status: baseResponse.statusCode, - headers: baseResponse.headers, - }); + userResponse = await handleError(err, baseRequest, baseResponse); } - let didAddCookies = false; - let didAddHeaders = false; + return addCookies(baseResponse, userResponse); + } catch (error) { + return await handler(error as Error, undefined); + } +} - for (const respCookie of baseResponse.cookies) { - didAddCookies = true; - userResponse.headers.append( - "Set-Cookie", - serialize(respCookie.key, respCookie.value, { - domain: respCookie.domain, - expires: new Date(respCookie.expires), - httpOnly: respCookie.httpOnly, - path: respCookie.path, - sameSite: respCookie.sameSite, - secure: respCookie.secure, - }) - ); +export function addCookies( + baseResponse: CollectingResponse, + userResponse: UserResponseType +): UserResponseType { + /** + * Add cookies to the userResponse passed by copying it from the baseResponse. + */ + let didAddCookies = false; + let didAddHeaders = false; + + for (const respCookie of baseResponse.cookies) { + didAddCookies = true; + userResponse.headers.append( + "Set-Cookie", + serialize(respCookie.key, respCookie.value, { + domain: respCookie.domain, + expires: new Date(respCookie.expires), + httpOnly: respCookie.httpOnly, + path: respCookie.path, + sameSite: respCookie.sameSite, + secure: respCookie.secure, + }) + ); + } + + baseResponse.headers.forEach((value: string, key: string) => { + didAddHeaders = true; + userResponse.headers.set(key, value); + }); + + /** + * For some deployment services (Vercel for example) production builds can return cached results for + * APIs with older header values. In this case if the session tokens have changed (because of refreshing + * for example) the cached result would still contain the older tokens and sessions would stop working. + * + * As a result, if we add cookies or headers from base response we also set the Cache-Control header + * to make sure that the final result is not a cached version. + */ + if (didAddCookies || didAddHeaders) { + if (!userResponse.headers.has("Cache-Control")) { + // This is needed for production deployments with Vercel + userResponse.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); } + } + return userResponse; +} - baseResponse.headers.forEach((value: string, key: string) => { - didAddHeaders = true; - userResponse.headers.set(key, value); - }); - if (didAddCookies || didAddHeaders) { - if (!userResponse.headers.has("Cache-Control")) { - // This is needed for production deployments with Vercel - userResponse.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); - } +export async function handleError( + err: any, + baseRequest: PreParsedRequest, + baseResponse: CollectingResponse +): Promise { + await errorHandler()(err, baseRequest, baseResponse, (errorHandlerError: Error) => { + if (errorHandlerError) { + throw errorHandlerError; } + }); - return userResponse; - } catch (error) { - return await handler(error as Error, undefined); - } + // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. + return new Response(baseResponse.body, { + status: baseResponse.statusCode, + headers: baseResponse.headers, + }) as UserResponseType; } diff --git a/lib/ts/nextjs.ts b/lib/ts/nextjs.ts index aff367f49..1c19383cf 100644 --- a/lib/ts/nextjs.ts +++ b/lib/ts/nextjs.ts @@ -28,7 +28,7 @@ import SessionRecipe from "./recipe/session/recipe"; import { getToken } from "./recipe/session/cookieAndHeaders"; import { availableTokenTransferMethods } from "./recipe/session/constants"; import { parseJWTWithoutSignatureVerification } from "./recipe/session/jwt"; -import { createPreParsedRequest, GetCookieFn, getHandleCall } from "./customFramework"; +import { addCookies, createPreParsedRequest, GetCookieFn, getHandleCall, handleError } from "./customFramework"; function next( request: any, @@ -91,9 +91,13 @@ export default class NextJS { }); } - static getAppDirRequestHandler(NextResponse: typeof Response) { - const getCookieFromNextReq: GetCookieFn = (req: T): Record => + static getCookieExtractor(): GetCookieFn { + return (req: T): Record => Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); + } + + static getAppDirRequestHandler(NextResponse: typeof Response) { + const getCookieFromNextReq = this.getCookieExtractor(); const stMiddleware = middleware((req) => { return createPreParsedRequest(req, getCookieFromNextReq); @@ -291,20 +295,8 @@ export default class NextJS { req: NextRequest, handler: (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => Promise ): Promise { - const query = Object.fromEntries(new URL(req.url).searchParams.entries()); - const cookies: Record = Object.fromEntries( - req.cookies.getAll().map((cookie) => [cookie.name, cookie.value]) - ); - - let baseRequest = new PreParsedRequest({ - method: req.method as HTTPMethod, - url: req.url, - query: query, - headers: req.headers, - cookies: cookies, - getFormBody: () => req!.formData(), - getJSONBody: () => req!.json(), - }); + const getCookieFromNextReq = this.getCookieExtractor(); + let baseRequest = createPreParsedRequest(req, getCookieFromNextReq); let baseResponse = new CollectingResponse(); let userResponse: NextResponse; @@ -312,57 +304,10 @@ export default class NextJS { try { userResponse = await handler(baseRequest, baseResponse); } catch (err) { - await customErrorHandler()(err, baseRequest, baseResponse, (errorHandlerError: Error) => { - if (errorHandlerError) { - throw errorHandlerError; - } - }); - - // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. - userResponse = new Response(baseResponse.body, { - status: baseResponse.statusCode, - headers: baseResponse.headers, - }) as NextResponse; - } - - let didAddCookies = false; - let didAddHeaders = false; - - for (const respCookie of baseResponse.cookies) { - didAddCookies = true; - userResponse.headers.append( - "Set-Cookie", - serialize(respCookie.key, respCookie.value, { - domain: respCookie.domain, - expires: new Date(respCookie.expires), - httpOnly: respCookie.httpOnly, - path: respCookie.path, - sameSite: respCookie.sameSite, - secure: respCookie.secure, - }) - ); + userResponse = await handleError(err, baseRequest, baseResponse); } - baseResponse.headers.forEach((value: string, key: string) => { - didAddHeaders = true; - userResponse.headers.set(key, value); - }); - - /** - * For some deployment services (Vercel for example) production builds can return cached results for - * APIs with older header values. In this case if the session tokens have changed (because of refreshing - * for example) the cached result would still contain the older tokens and sessions would stop working. - * - * As a result, if we add cookies or headers from base response we also set the Cache-Control header - * to make sure that the final result is not a cached version. - */ - if (didAddCookies || didAddHeaders) { - if (!userResponse.headers.has("Cache-Control")) { - // This is needed for production deployments with Vercel - userResponse.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); - } - } - return userResponse; + return addCookies(baseResponse, userResponse); } } export let superTokensNextWrapper = NextJS.superTokensNextWrapper;