server/routes/api.ts (346 lines of code) (raw):
import * as Sentry from '@sentry/node';
import { Router } from 'express';
import type { MembersDataApiResponse } from '@/shared/productResponse';
import { isProduct, MDA_TEST_USER_HEADER } from '@/shared/productResponse';
import {
cancellationSfCasesAPI,
deliveryRecordsAPI,
discountAPI,
holidayStopAPI,
invoicingAPI,
productMoveAPI,
productSwitchAPI,
updateSupporterPlusAmountAPI,
} from '../apiGatewayDiscovery';
import {
customMembersDataApiHandler,
membersDataApiHandler,
proxyApiHandler,
straightThroughBodyHandler,
userBenefitsApiHandler,
} from '../apiProxy';
import { s3FilePromise } from '../awsIntegration';
import { conf } from '../config';
import { contactUsFormHandler } from '../contactUsApi';
import { augmentProductDetailWithDeliveryAddressChangeEffectiveDateForToday } from '../fulfilmentDateCalculatorReader';
import { getArticleHandler, getTopicHandler } from '../helpCentreApi';
import { log } from '../log';
import { withIdentity } from '../middleware/identityMiddleware';
import { withOktaServerSideValidation } from '../middleware/oktaServerSideAuthMiddleware';
import {
cancelReminderHandler,
createOneOffReminderHandler,
publicCreateReminderHandler,
reactivateReminderHandler,
} from '../reminders/reminderApi';
import { stripeCreateCheckoutSessionHandler } from '../stripeCreateCheckoutSessionHandler';
import { stripeGetCheckoutSessionHandler } from '../stripeGetCheckoutSessionHandler';
import { stripeSetupIntentHandler } from '../stripeSetupIntentsHandler';
const router = Router();
router.use(withIdentity(401));
router.get(
'/existing-payment-options',
membersDataApiHandler(
'user-attributes/me/existing-payment-options',
'MDA_EXISTING_PAYMENT_OPTIONS',
),
);
router.get(
'/me/mma/:subscriptionName?',
customMembersDataApiHandler((response, body) => {
const isTestUser = response.getHeader(MDA_TEST_USER_HEADER) === 'true';
const mdapiResponse = JSON.parse(
body.toString(),
) as MembersDataApiResponse;
const augmentedWithTestUser = mdapiResponse.products.map(
(mdapiObject) => ({
...mdapiObject,
isTestUser,
}),
);
Promise.all(
augmentedWithTestUser
.filter(isProduct)
// TODO move this to members-data-api, so we can eliminate this customMembersDataApiHandler
.map(
augmentProductDetailWithDeliveryAddressChangeEffectiveDateForToday,
),
)
.then((productDetails) => {
mdapiResponse.products = productDetails;
response.json(mdapiResponse);
})
.catch((error) => {
const errorMessage = `Unexpected error when augmenting members-data-api response with 'deliveryAddressChangeEffectiveDate' error message was ${error}`;
log.error(errorMessage, error);
Sentry.captureMessage(errorMessage);
mdapiResponse.products = augmentedWithTestUser;
response.json(mdapiResponse); // fallback to sending the response augmented with just isTestUser
});
})(
'user-attributes/me/mma/:subscriptionName',
'MDA_DETAIL',
['subscriptionName'],
true,
),
);
router.get(
'/me/one-off-contributions',
membersDataApiHandler(
'user-attributes/me/one-off-contributions',
'MDA_DETAIL',
[],
),
);
router.get(
'/me/user-attributes',
membersDataApiHandler('user-attributes/me', 'MDA_DETAIL', []),
);
router.get(
'/cancellation-date/:subscriptionName',
membersDataApiHandler(
'user-attributes/me/cancellation-date/:subscriptionName',
'MDA_CANCEL',
['subscriptionName'],
),
);
router.post(
'/cancel/:subscriptionName?',
membersDataApiHandler(
'/user-attributes/me/cancel/:subscriptionName',
'MDA_CANCEL',
['subscriptionName'],
),
);
router.post(
'/update-cancellation-reason/:subscriptionName?',
membersDataApiHandler(
'/user-attributes/me/update-cancellation-reason/:subscriptionName',
'MDA_UPDATE_CANCELLATION_REASON',
['subscriptionName'],
),
);
router.post(
'/supporter-plus-cancel/:subscriptionName',
withOktaServerSideValidation,
productMoveAPI(
'supporter-plus-cancel/:subscriptionName',
'CANCEL_SUPPORTER_PLUS',
['subscriptionName'],
),
);
router.post(
'/payment/card',
withOktaServerSideValidation,
stripeSetupIntentHandler,
);
router.post(
'/payment/card/:subscriptionName',
membersDataApiHandler(
'/user-attributes/me/update-card/:subscriptionName',
'MDA_UPDATE_PAYMENT_CARD',
['subscriptionName'],
),
);
router.post(
'/payment/dd/:subscriptionName',
membersDataApiHandler(
'/user-attributes/me/update-direct-debit/:subscriptionName',
'MDA_UPDATE_PAYMENT_DIRECT_DEBIT',
['subscriptionName'],
),
);
router.post(
'/payment/checkout-session',
withOktaServerSideValidation,
stripeCreateCheckoutSessionHandler,
);
router.get(
'/payment/checkout-session/:id',
withOktaServerSideValidation,
stripeGetCheckoutSessionHandler,
);
router.post(
'/validate/payment/dd',
proxyApiHandler('payment.' + conf.API_DOMAIN)(straightThroughBodyHandler)(
'direct-debit/check-account',
'PAPI_VALIDATE_DIRECT_DEBIT',
[],
true,
),
);
router.post(
'/case/:caseId?',
withOktaServerSideValidation,
cancellationSfCasesAPI('case', 'CREATE_CANCELLATION_CASE'),
);
router.patch(
'/case/:caseId?',
withOktaServerSideValidation,
cancellationSfCasesAPI('case/:caseId', 'UPDATE_CANCELLATION_CASE', [
'caseId',
]),
);
router.post(
'/discounts/preview-discount',
discountAPI('preview-discount', 'PREVIEW_DISCOUNT'),
);
router.post(
'/discounts/apply-discount',
discountAPI('apply-discount', 'APPLY_DISCOUNT'),
);
// The two switch types are using different apis for now, membership to recurring contribution
// is using the old api
router.post(
'/product-move/to-recurring-contribution/:subscriptionName',
withOktaServerSideValidation,
productMoveAPI(
'product-move/to-recurring-contribution/:subscriptionName',
'MOVE_PRODUCT',
['subscriptionName'],
),
);
// recurring contribution to supporter plus is using the new api
router.post(
'/product-move/recurring-contribution-to-supporter-plus/:subscriptionName',
withOktaServerSideValidation,
productSwitchAPI(
'product-move/recurring-contribution-to-supporter-plus/:subscriptionName',
'MOVE_PRODUCT',
['subscriptionName'],
),
);
router.post(
'/update-supporter-plus-amount/:subscriptionName',
withOktaServerSideValidation,
updateSupporterPlusAmountAPI(
'update-supporter-plus-amount/:subscriptionName',
'UPDATE_SUPPORTER_PLUS_AMOUNT',
['subscriptionName'],
),
);
router.get(
'/holidays/:subscriptionName/potential',
holidayStopAPI('potential/:subscriptionName', 'HOLIDAY_STOP_POTENTIALS', [
'subscriptionName',
]),
);
router.get(
'/holidays/:subscriptionName/cancel',
holidayStopAPI(
'hsr/:subscriptionName/cancel',
'HOLIDAY_STOP_CANCELLATION_PREVIEW',
['subscriptionName'],
),
);
router.get(
'/holidays/:subscriptionName',
holidayStopAPI('hsr/:subscriptionName', 'HOLIDAY_STOP_EXISTING', [
'subscriptionName',
]),
);
router.post(
'/holidays',
withOktaServerSideValidation,
holidayStopAPI('/hsr', 'HOLIDAY_STOP_CREATE'),
);
router.patch(
'/holidays/:subscriptionName/:sfId',
withOktaServerSideValidation,
holidayStopAPI('hsr/:subscriptionName/:sfId', 'HOLIDAY_STOP_AMEND', [
'subscriptionName',
'sfId',
]),
);
router.delete(
'/holidays/:subscriptionName/:sfId',
withOktaServerSideValidation,
holidayStopAPI('hsr/:subscriptionName/:sfId', 'HOLIDAY_STOP_WITHDRAW', [
'subscriptionName',
'sfId',
]),
);
router.get(
'/delivery-records/:subscriptionName',
deliveryRecordsAPI(
'delivery-records/:subscriptionName',
'DELIVERY_RECORDS_GET',
['subscriptionName'],
),
);
router.get(
'/delivery-records/:subscriptionName/cancel',
deliveryRecordsAPI(
'delivery-records/:subscriptionName/cancel',
'DELIVERY_RECORDS_CANCELLATION_PREVIEW',
['subscriptionName'],
),
);
router.post(
'/delivery-records/:subscriptionName',
withOktaServerSideValidation,
deliveryRecordsAPI(
'delivery-records/:subscriptionName',
'DELIVERY_PROBLEM_CREATE',
['subscriptionName'],
),
);
router.put(
'/delivery/address/update/:contactId',
membersDataApiHandler(
'/user-attributes/me/delivery-address/:contactId',
'MDA_DELIVERY_ADDRESS_UPDATE',
['contactId'],
),
);
router.get('/invoices', invoicingAPI('invoices', 'LIST_INVOICES'));
router.get(
'/invoices/:invoiceId',
invoicingAPI(
'invoices/:invoiceId',
'GET_INVOICE_PDF',
['invoiceId'],
{ Accept: 'application/pdf' },
true, // should not log body for pdf download
),
);
router.get(
'/cancelled',
membersDataApiHandler(
'user-attributes/me/cancelled-subscriptions',
'MDA_CANCELLED_SUBSCRIPTIONS',
),
);
router.get('/known-issues', async (_, response) => {
const bucketName = 'manage-help-content';
const filePath = `${conf.STAGE}/known-issues/knownIssuesConfig.json`;
const data = await s3FilePromise(bucketName, filePath);
response.json(data || []);
});
router.get('/help-centre/article/:article', getArticleHandler);
router.get('/help-centre/topic/:topic', getTopicHandler);
router.post('/contact-us', contactUsFormHandler);
router.post('/reminders/create', createOneOffReminderHandler); // requires sign-in
router.post(
'/public/reminders/create/one-off',
publicCreateReminderHandler('ONE_OFF'),
); // does not require sign-in, uses verification token
router.post(
'/public/reminders/create/recurring',
publicCreateReminderHandler('RECURRING'),
); // does not require sign-in, uses verification token
router.get(
'/reminders/status',
membersDataApiHandler(
'user-attributes/me/reminders',
'MDA_REMINDERS_STATUS',
),
);
router.post('/reminders/cancel', cancelReminderHandler);
router.post('/reminders/reactivate', reactivateReminderHandler);
router.post('/csp-audit-report-endpoint', (req, res) => {
const parsedBody = JSON.parse(req.body.toString());
log.warn(JSON.stringify(parsedBody));
res.status(204).end();
});
router.get(
'/benefits/me',
userBenefitsApiHandler('benefits/me', 'USER_BENEFITS'),
);
export { router };