mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 15:45:13 +00:00
Persist visitor params values in public visitor session cookie (#3231)
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
type ResponseCookies,
|
||||
getPathScopedCookieName,
|
||||
getResponseCookiesForVisitorAuth,
|
||||
getVisitorPayload,
|
||||
getVisitorData,
|
||||
normalizeVisitorAuthURL,
|
||||
} from '@/lib/visitors';
|
||||
import { serveResizedImage } from '@/routes/image';
|
||||
@@ -95,7 +95,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
//
|
||||
// Detect and extract the visitor authentication token from the request
|
||||
//
|
||||
const { visitorToken, unsignedClaims } = getVisitorPayload({
|
||||
const { visitorToken, unsignedClaims, visitorParamsCookie } = getVisitorData({
|
||||
cookies: request.cookies.getAll(),
|
||||
url: siteRequestURL,
|
||||
});
|
||||
@@ -121,7 +121,13 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
apiToken,
|
||||
})
|
||||
);
|
||||
const cookies: ResponseCookies = [];
|
||||
|
||||
const cookies: ResponseCookies = visitorParamsCookie
|
||||
? [
|
||||
// If visitor.* params were passed to the site URL, include a session cookie to persist these params across navigation.
|
||||
visitorParamsCookie,
|
||||
]
|
||||
: [];
|
||||
|
||||
//
|
||||
// Handle redirects
|
||||
|
||||
@@ -178,7 +178,7 @@ describe('getVisitorUnsignedClaims', () => {
|
||||
const url = new URL('https://example.com/');
|
||||
const claims = getVisitorUnsignedClaims({ cookies, url });
|
||||
|
||||
expect(claims).toStrictEqual({
|
||||
expect(claims.all).toStrictEqual({
|
||||
bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } },
|
||||
launchdarkly: { flags: { ALPHA: true, API: true } },
|
||||
});
|
||||
@@ -191,7 +191,12 @@ describe('getVisitorUnsignedClaims', () => {
|
||||
|
||||
const claims = getVisitorUnsignedClaims({ cookies: [], url });
|
||||
|
||||
expect(claims).toStrictEqual({
|
||||
expect(claims.all).toStrictEqual({
|
||||
isEnterprise: true,
|
||||
language: 'fr',
|
||||
country: 'fr',
|
||||
});
|
||||
expect(claims.fromVisitorParams).toStrictEqual({
|
||||
isEnterprise: true,
|
||||
language: 'fr',
|
||||
country: 'fr',
|
||||
@@ -203,10 +208,13 @@ describe('getVisitorUnsignedClaims', () => {
|
||||
|
||||
const claims = getVisitorUnsignedClaims({ cookies: [], url });
|
||||
|
||||
expect(claims).toStrictEqual({
|
||||
expect(claims.all).toStrictEqual({
|
||||
isEnterprise: true,
|
||||
// otherParam is not present
|
||||
});
|
||||
expect(claims.fromVisitorParams).toStrictEqual({
|
||||
isEnterprise: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should support nested query param keys via dot notation', () => {
|
||||
@@ -216,7 +224,14 @@ describe('getVisitorUnsignedClaims', () => {
|
||||
|
||||
const claims = getVisitorUnsignedClaims({ cookies: [], url });
|
||||
|
||||
expect(claims).toStrictEqual({
|
||||
expect(claims.all).toStrictEqual({
|
||||
isEnterprise: true,
|
||||
flags: {
|
||||
ALPHA: true,
|
||||
API: false,
|
||||
},
|
||||
});
|
||||
expect(claims.fromVisitorParams).toStrictEqual({
|
||||
isEnterprise: true,
|
||||
flags: {
|
||||
ALPHA: true,
|
||||
@@ -235,7 +250,7 @@ describe('getVisitorUnsignedClaims', () => {
|
||||
const url = new URL('https://example.com/');
|
||||
const claims = getVisitorUnsignedClaims({ cookies, url });
|
||||
|
||||
expect(claims).toStrictEqual({});
|
||||
expect(claims.all).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('should merge claims from cookies and visitor.* query params', () => {
|
||||
@@ -250,16 +265,27 @@ describe('getVisitorUnsignedClaims', () => {
|
||||
},
|
||||
];
|
||||
const url = new URL(
|
||||
'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false'
|
||||
'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false&visitor.bucket.flags.HELLO=false'
|
||||
);
|
||||
|
||||
const claims = getVisitorUnsignedClaims({ cookies, url });
|
||||
|
||||
expect(claims).toStrictEqual({
|
||||
expect(claims.all).toStrictEqual({
|
||||
role: 'admin',
|
||||
language: 'fr',
|
||||
bucket: {
|
||||
flags: { SITE_AI: true, SITE_PREVIEW: true },
|
||||
flags: { HELLO: false, SITE_AI: true, SITE_PREVIEW: true },
|
||||
},
|
||||
isEnterprise: true,
|
||||
flags: {
|
||||
ALPHA: true,
|
||||
API: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(claims.fromVisitorParams).toStrictEqual({
|
||||
bucket: {
|
||||
flags: { HELLO: false },
|
||||
},
|
||||
isEnterprise: true,
|
||||
flags: {
|
||||
|
||||
@@ -41,13 +41,15 @@ type ClaimPrimitive =
|
||||
| ClaimPrimitive[];
|
||||
|
||||
/**
|
||||
* The result of a visitor info lookup that can include:
|
||||
* The result of a visitor data lookup that can include:
|
||||
* - a visitor token (JWT)
|
||||
* - a record of visitor public/unsigned claims (JSON object)
|
||||
* - a session cookie response to persist any visitor query params across navigations.
|
||||
*/
|
||||
export type VisitorPayloadLookup = {
|
||||
export type VisitorDataLookup = {
|
||||
visitorToken: VisitorTokenLookup;
|
||||
unsignedClaims: Record<string, ClaimPrimitive>;
|
||||
visitorParamsCookie: ResponseCookie | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -74,21 +76,26 @@ export type VisitorTokenLookup =
|
||||
| undefined;
|
||||
|
||||
/**
|
||||
* Get the visitor info for the request including its token and/or unsigned claims when present.
|
||||
* Get the visitor data for the request potentially including:
|
||||
* - a JWT token that may contain signed claims or can be used for VA authentication.
|
||||
* - a record of the unsigned claims passed via a cookie or visitor.* params.
|
||||
* - a session cookie response that is used to persist any visitor.* params that were passed via the site URL.
|
||||
*/
|
||||
export function getVisitorPayload({
|
||||
export function getVisitorData({
|
||||
cookies,
|
||||
url,
|
||||
}: {
|
||||
cookies: RequestCookies;
|
||||
url: URL | NextRequest['nextUrl'];
|
||||
}): VisitorPayloadLookup {
|
||||
}): VisitorDataLookup {
|
||||
const visitorToken = getVisitorToken({ cookies, url });
|
||||
const unsignedClaims = getVisitorUnsignedClaims({ cookies, url });
|
||||
const visitorParamsCookie = getResponseCookieForVisitorParams(unsignedClaims.fromVisitorParams);
|
||||
|
||||
return {
|
||||
visitorToken,
|
||||
unsignedClaims,
|
||||
unsignedClaims: unsignedClaims.all,
|
||||
visitorParamsCookie,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,9 +135,19 @@ export function getVisitorToken({
|
||||
export function getVisitorUnsignedClaims(args: {
|
||||
cookies: RequestCookies;
|
||||
url: URL | NextRequest['nextUrl'];
|
||||
}): Record<string, ClaimPrimitive> {
|
||||
}): {
|
||||
/**
|
||||
* The unsigned claims coming from both `gitbook-visitor-public` cookies and `visitor.*` query params.
|
||||
*/
|
||||
all: Record<string, ClaimPrimitive>;
|
||||
/**
|
||||
* The unsigned claims from the `visitor.*` query params.
|
||||
*/
|
||||
fromVisitorParams: Record<string, ClaimPrimitive>;
|
||||
} {
|
||||
const { cookies, url } = args;
|
||||
const claims: Record<string, ClaimPrimitive> = {};
|
||||
const searchParamsClaims: Record<string, ClaimPrimitive> = {};
|
||||
|
||||
for (const cookie of cookies) {
|
||||
if (cookie.name.startsWith(VISITOR_UNSIGNED_CLAIMS_PREFIX)) {
|
||||
@@ -149,11 +166,13 @@ export function getVisitorUnsignedClaims(args: {
|
||||
if (key.startsWith('visitor.')) {
|
||||
const claimPath = key.substring('visitor.'.length);
|
||||
const claimValue = parseVisitorQueryParamValue(value);
|
||||
|
||||
setVisitorClaimByPath(claims, claimPath, claimValue);
|
||||
setVisitorClaimByPath(searchParamsClaims, claimPath, claimValue);
|
||||
}
|
||||
}
|
||||
|
||||
return claims;
|
||||
return { all: claims, fromVisitorParams: searchParamsClaims };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,6 +240,26 @@ function parseVisitorQueryParamValue(value: string): ClaimPrimitive {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns to cookie response to use in order to persist visitor params that were passed to the URL.
|
||||
*/
|
||||
function getResponseCookieForVisitorParams(
|
||||
visitorParamsClaims: Record<string, ClaimPrimitive>
|
||||
): ResponseCookie | undefined {
|
||||
if (Object.keys(visitorParamsClaims).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
name: VISITOR_UNSIGNED_CLAIMS_PREFIX,
|
||||
value: JSON.stringify(visitorParamsClaims),
|
||||
options: {
|
||||
sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the lookup result for content served with visitor auth.
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
type ResponseCookies,
|
||||
type VisitorTokenLookup,
|
||||
getResponseCookiesForVisitorAuth,
|
||||
getVisitorPayload,
|
||||
getVisitorData,
|
||||
normalizeVisitorAuthURL,
|
||||
} from '@/lib/visitors';
|
||||
|
||||
@@ -392,7 +392,7 @@ async function lookupSiteInProxy(request: NextRequest, url: URL): Promise<Lookup
|
||||
* When serving multi spaces based on the current URL.
|
||||
*/
|
||||
async function lookupSiteInMultiMode(request: NextRequest, url: URL): Promise<LookupResult> {
|
||||
const { visitorToken } = getVisitorPayload({
|
||||
const { visitorToken } = getVisitorData({
|
||||
cookies: request.cookies.getAll(),
|
||||
url,
|
||||
});
|
||||
@@ -609,7 +609,7 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis
|
||||
const target = new URL(targetStr);
|
||||
target.search = url.search;
|
||||
|
||||
const { visitorToken } = getVisitorPayload({
|
||||
const { visitorToken } = getVisitorData({
|
||||
cookies: request.cookies.getAll(),
|
||||
url: target,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user