server/stripeSetupIntentsHandler.ts (117 lines of code) (raw):
import * as Sentry from '@sentry/node';
import type express from 'express';
import fetch from 'node-fetch';
import type { StripeSetupIntent } from '../shared/stripeSetupIntent';
import { STRIPE_PUBLIC_KEY_HEADER } from '../shared/stripeSetupIntent';
import { log, putMetric } from './log';
import { recaptchaConfigPromise } from './recaptchaConfig';
import { stripeSetupIntentConfigPromise } from './stripeSetupIntentConfig';
export const stripeSetupIntentHandler = async (
request: express.Request,
response: express.Response,
) => {
const recaptchaOneTimeToken = request.body.toString();
const recaptchaSecret = (await recaptchaConfigPromise)?.secretKey;
const recaptchaResult = await (
await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `secret=${recaptchaSecret}&response=${recaptchaOneTimeToken}`,
})
).json();
if (!recaptchaResult?.success) {
const loggingDetail = {
loggingCode: 'RECAPTURE_FAILURE',
recaptchaResult,
};
log.error('failed server-side reCaptcha verification', loggingDetail);
putMetric({
...loggingDetail,
isOK: false,
});
response.status(400).send('reCaptcha missing/failed');
return;
}
stripeSetupIntentConfigPromise
.then((stripePublicToSecretKeyMapping) => {
if (!stripePublicToSecretKeyMapping) {
throw new Error('missing Stripe SetupIntent config');
}
const stripePublicKey = request.header(STRIPE_PUBLIC_KEY_HEADER);
if (!stripePublicKey) {
response
.status(400)
.send(`missing header '${STRIPE_PUBLIC_KEY_HEADER}'`);
return;
}
const stripeSecretKey =
stripePublicToSecretKeyMapping[stripePublicKey];
if (!stripeSecretKey) {
throw new Error(
`no secret key mapping for Stripe public key '${stripePublicKey}'`,
);
}
const httpMethod = request.method;
const outgoingURL = 'https://api.stripe.com/v1/setup_intents'; // using URL rather than stripe library due to missing type defs
const requestBody = 'usage=off_session';
// tslint:disable-next-line:no-object-mutation
response.locals.loggingDetail = {
loggingCode: 'STRIPE_SETUP_INTENT',
stripePublicKey, // this will indicate 'test mode' vs 'live'
httpMethod,
identityID:
response.locals.identity && response.locals.identity.userId,
incomingURL: request.originalUrl,
requestBody,
outgoingURL,
};
fetch(outgoingURL, {
method: httpMethod,
headers: {
Authorization: `Bearer ${stripeSecretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: requestBody,
})
.then((stripeResponse) => {
// tslint:disable-next-line:no-object-mutation
response.locals.loggingDetail.status =
stripeResponse.status;
// tslint:disable-next-line:no-object-mutation
response.locals.loggingDetail.isOK = stripeResponse.ok;
if (stripeResponse.ok) {
return stripeResponse.json();
} else {
throw new Error(
`Failed to load SetupIntent : ${
stripeResponse.status
} ${
stripeResponse.statusText
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- we believe this function will not evaluate to '[object Object'
} : ${stripeResponse.text()}`,
);
}
})
.then((setupIntent: StripeSetupIntent) => {
const suitableLog = response.locals.loggingDetail.isOK
? log.info
: log.warning;
suitableLog('fetching', response.locals.loggingDetail);
putMetric(response.locals.loggingDetail);
response.json({
id: setupIntent.id,
client_secret: setupIntent.client_secret,
});
})
.catch(handleTerminalError(response));
})
.catch(handleTerminalError(response));
};
const handleTerminalError = (response: express.Response) => (error: Error) => {
Sentry.captureException(error);
log.error('Failed to create SetupIntent', {
...response.locals.loggingDetail,
exception: error || 'undefined',
});
putMetric(response.locals.loggingDetail);
response.status(500).send();
};