server/middleware/requestMiddleware.ts (88 lines of code) (raw):
import type { UrlWithParsedQuery } from 'url';
import url from 'url';
export interface MockableExpressRequest {
baseUrl: string;
path: string;
get: (name: string) => string | undefined;
header: (name: string) => string | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- assume we don't know the range of possible types for the query attribute?
query: any;
}
type QueryParameters = Record<string, string>;
// Names of query parameters to that facilitate sign-in on profile.
export const signInTokenQueryParameterNames = [
'encryptedEmail',
'autoSignInToken',
];
// Filter query parameters to include only those whose name satisfies the predicate p.
const filterQueryParametersByName = (
params: QueryParameters,
p: (name: string) => boolean,
): QueryParameters => {
return Object.entries(params)
.filter(([name, _]) => p(name))
.reduce(
(params2, [name, value]) => ({ ...params2, [name]: value }),
{},
);
};
// Adds the redirect url (if defined) as query parameter profileReferer,
// and removes the sign-in token query parameters since they are not required by manage
// (only used by profile if the user is redirected their to sign-in).
export const updateManageUrl = (
req: MockableExpressRequest,
useRefererHeader: boolean,
redirectUrl?: UrlWithParsedQuery,
): string => {
// It is vital that the sign-in query parameters are removed.
// See the implementation of withIdentity() for more context.
const queryParameters = filterQueryParametersByName(
req.query,
(name) => !signInTokenQueryParameterNames.includes(name),
);
const profileReferrer =
redirectUrl && redirectUrl.path
? redirectUrl.path.substring(1)
: undefined;
const refererHeader = req.header('referer');
return useRefererHeader && refererHeader
? refererHeader
: url.format({
protocol: 'https',
host: req.get('host'),
pathname: req.baseUrl + req.path,
query: {
...queryParameters,
profileReferrer,
},
});
};
export const augmentRedirectURL = (
req: MockableExpressRequest,
simpleRedirectURL: string,
currentDomain: string,
useRefererHeaderForReturnURL: boolean,
) => {
const parsedSimpleURL = url.parse(
// the replace below essentially allows DEV to use CODE IDAPI but still redirect to profile.thegulocal.com
simpleRedirectURL.replace('code.dev-theguardian.com', currentDomain),
true,
);
const returnUrl = updateManageUrl(
req,
useRefererHeaderForReturnURL,
parsedSimpleURL,
);
// To avoid potential clashes with query parameters that have a special meaning on profile (e.g. error),
// only forward specific query parameters to profile.
const profileQueryParameterNames = [
'INTCMP',
// By passing these to profile, can measure the sign in rates across test segments.
'abName',
'abVariant',
'journey',
...signInTokenQueryParameterNames,
];
const profileQueryParameters = filterQueryParametersByName(
req.query,
(name) => profileQueryParameterNames.includes(name),
);
return url.format({
protocol: parsedSimpleURL.protocol,
host: parsedSimpleURL.host,
pathname: parsedSimpleURL.pathname,
query: {
...parsedSimpleURL.query,
...profileQueryParameters,
returnUrl, // this is automatically URL encoded
},
});
};