diff --git a/.github/workflows/e2e-third-party.yml b/.github/workflows/e2e-third-party.yml index 2b3b5198df5..b8f56c30826 100644 --- a/.github/workflows/e2e-third-party.yml +++ b/.github/workflows/e2e-third-party.yml @@ -57,7 +57,6 @@ jobs: echo 'STRIPE_PUBLIC_KEY=${{ secrets.STRIPE_PUBLIC_KEY }}' >> .env echo 'STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }}' >> .env echo 'PAYPAL_CLIENT_ID=${{ secrets.PAYPAL_CLIENT_ID }}' >> .env - echo 'PAYPAL_SECRET=${{ secrets.PAYPAL_SECRET }}' >> .env - name: Install and Build run: | diff --git a/api-server/config/secrets.js b/api-server/config/secrets.js index 62c8b03ab20..2f7c1df1bf6 100644 --- a/api-server/config/secrets.js +++ b/api-server/config/secrets.js @@ -30,13 +30,7 @@ const { SENTRY_DSN, STRIPE_PUBLIC_KEY, - STRIPE_SECRET_KEY, - - PAYPAL_CLIENT_ID, - PAYPAL_SECRET, - PAYPAL_VERIFY_WEBHOOK_URL, - PAYPAL_API_TOKEN_URL, - PAYPAL_WEBHOOK_ID + STRIPE_SECRET_KEY } = process.env; module.exports = { @@ -98,13 +92,5 @@ module.exports = { stripe: { public: STRIPE_PUBLIC_KEY, secret: STRIPE_SECRET_KEY - }, - - paypal: { - client: PAYPAL_CLIENT_ID, - secret: PAYPAL_SECRET, - verifyWebhookURL: PAYPAL_VERIFY_WEBHOOK_URL, - tokenUrl: PAYPAL_API_TOKEN_URL, - webhookId: PAYPAL_WEBHOOK_ID } }; diff --git a/api-server/package.json b/api-server/package.json index ed97701eaf3..fb735cca061 100644 --- a/api-server/package.json +++ b/api-server/package.json @@ -32,7 +32,6 @@ "@sentry/node": "7.37.1", "@sentry/tracing": "7.37.1", "accepts": "1.3.8", - "axios": "0.23.0", "body-parser": "1.20.0", "compression": "1.7.4", "connect-mongo": "3.2.0", @@ -88,7 +87,6 @@ "@babel/preset-env": "7.18.0", "@babel/register": "7.17.7", "loopback-component-explorer": "6.4.0", - "nodemon": "2.0.16", - "smee-client": "1.2.3" + "nodemon": "2.0.16" } } diff --git a/api-server/src/development-start.js b/api-server/src/development-start.js index 4b11c906447..f1317cbbfe9 100644 --- a/api-server/src/development-start.js +++ b/api-server/src/development-start.js @@ -3,30 +3,8 @@ require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); const createDebugger = require('debug'); const nodemon = require('nodemon'); -const SmeeClient = require('smee-client'); - const log = createDebugger('fcc:start:development'); -if (process.env.WEBHOOK_PROXY_URL) { - const paypalPayloadHandler = new SmeeClient({ - source: process.env.WEBHOOK_PROXY_URL, - target: process.env.API_LOCATION + '/hooks/update-paypal', - logger: { info: log, error: log } - }); - - const paypalevents = paypalPayloadHandler.start(); - - process.on('exit', () => { - log('Stopping webhook proxy client.'); - paypalevents.close(() => { - log('Webhook proxy client is stopped.'); - }); - }); -} else { - log('Webhook client is not configured.'); - log('This can be ignored when not working with webhooks locally.'); -} - nodemon({ ext: 'js json', // --silent squashes an ELIFECYCLE error when the server exits diff --git a/api-server/src/server/boot/donate.js b/api-server/src/server/boot/donate.js index c01f56bbadd..13507296b67 100644 --- a/api-server/src/server/boot/donate.js +++ b/api-server/src/server/boot/donate.js @@ -4,10 +4,6 @@ import Stripe from 'stripe'; import { donationSubscriptionConfig } from '../../../../shared/config/donation-settings'; import keys from '../../../config/secrets'; import { - getAsyncPaypalToken, - verifyWebHook, - updateUser, - verifyWebHookType, createStripeCardDonation, handleStripeCardUpdateSession } from '../utils/donation'; @@ -195,37 +191,15 @@ export default function donateBoot(app, done) { } } - function updatePaypal(req, res) { - const { headers, body } = req; - return Promise.resolve(req) - .then(verifyWebHookType) - .then(getAsyncPaypalToken) - .then(token => verifyWebHook(headers, body, token, keys.paypal.webhookId)) - .then(hookBody => updateUser(hookBody, app)) - .catch(err => { - // Todo: This probably need to be thrown and caught in error handler - log(err.message); - }) - .finally(() => res.status(200).json({ message: 'received paypal hook' })); - } - const stripeKey = keys.stripe.public; const secKey = keys.stripe.secret; - const paypalKey = keys.paypal.client; - const paypalSec = keys.paypal.secret; - const stripeSecretInvalid = !secKey || secKey === 'sk_from_stripe_dashboard'; const stripPublicInvalid = !stripeKey || stripeKey === 'pk_from_stripe_dashboard'; - const paypalSecretInvalid = - !paypalKey || paypalKey === 'id_from_paypal_dashboard'; - const paypalPublicInvalid = - !paypalSec || paypalSec === 'secret_from_paypal_dashboard'; const stripeInvalid = stripeSecretInvalid || stripPublicInvalid; - const paypalInvalid = paypalPublicInvalid || paypalSecretInvalid; - if (stripeInvalid || paypalInvalid) { + if (stripeInvalid) { if (process.env.FREECODECAMP_NODE_ENV === 'production') { throw new Error('Donation API keys are required to boot the server!'); } @@ -236,7 +210,6 @@ export default function donateBoot(app, done) { api.post('/charge-stripe-card', handleStripeCardDonation); api.put('/update-stripe-card', handleStripeCardUpdate); api.post('/add-donation', addDonation); - hooks.post('/update-paypal', updatePaypal); donateRouter.use('/donate', api); donateRouter.use('/hooks', hooks); app.use(donateRouter); diff --git a/api-server/src/server/middlewares/csurf.js b/api-server/src/server/middlewares/csurf.js index 7831537b50d..46b1c0fafa7 100644 --- a/api-server/src/server/middlewares/csurf.js +++ b/api-server/src/server/middlewares/csurf.js @@ -14,9 +14,7 @@ export default function getCsurf() { const { path } = req; if ( // eslint-disable-next-line max-len - /^\/hooks\/update-paypal$|^\/donate\/charge-stripe$|^\/coderoad-challenge-completed$/.test( - path - ) + /^\/donate\/charge-stripe$|^\/coderoad-challenge-completed$/.test(path) ) { next(); } else { diff --git a/api-server/src/server/middlewares/request-authorization.js b/api-server/src/server/middlewares/request-authorization.js index 95379be9402..8895e1670a9 100644 --- a/api-server/src/server/middlewares/request-authorization.js +++ b/api-server/src/server/middlewares/request-authorization.js @@ -22,7 +22,6 @@ const signinRE = /^\/signin/; const statusRE = /^\/status\/ping$/; const unsubscribedRE = /^\/unsubscribed\//; const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//; -const updateHooksRE = /^\/hooks\/update-paypal$/; // note: this would be replaced by webhooks later const donateRE = /^\/donate\/charge-stripe$/; const submitCoderoadChallengeRE = /^\/coderoad-challenge-completed$/; @@ -40,7 +39,6 @@ const _pathsAllowedREs = [ statusRE, unsubscribedRE, unsubscribeRE, - updateHooksRE, donateRE, submitCoderoadChallengeRE, mobileLoginRE diff --git a/api-server/src/server/middlewares/request-authorization.test.js b/api-server/src/server/middlewares/request-authorization.test.js index 7b61b4f8212..2cb878081bb 100644 --- a/api-server/src/server/middlewares/request-authorization.test.js +++ b/api-server/src/server/middlewares/request-authorization.test.js @@ -48,7 +48,6 @@ describe('request-authorization', () => { const statusRE = /^\/status\/ping$/; const unsubscribedRE = /^\/unsubscribed\//; const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//; - const updateHooksRE = /^\/hooks\/update-paypal$/; const allowedPathsList = [ authRE, @@ -61,8 +60,7 @@ describe('request-authorization', () => { signinRE, statusRE, unsubscribedRE, - unsubscribeRE, - updateHooksRE + unsubscribeRE ]; it('returns a boolean', () => { @@ -79,10 +77,8 @@ describe('request-authorization', () => { '/ue/WmjInLerysPrcon6fMb/', allowedPathsList ); - const resultC = isAllowedPath('/hooks/update-paypal', allowedPathsList); expect(resultA).toBe(true); expect(resultB).toBe(true); - expect(resultC).toBe(true); }); it('returns false for a non-white-listed path', () => { diff --git a/api-server/src/server/utils/__mocks__/donation.js b/api-server/src/server/utils/__mocks__/donation.js deleted file mode 100644 index df1fb13339c..00000000000 --- a/api-server/src/server/utils/__mocks__/donation.js +++ /dev/null @@ -1,205 +0,0 @@ -/* eslint-disable camelcase */ -export const mockCancellationHook = { - headers: { - host: 'a47fb0f4.ngrok.io', - accept: '*/*', - 'paypal-transmission-id': '2e24bc40-61d1-11ea-8ac4-7d4e2605c70c', - 'paypal-transmission-time': '2020-03-09T06:42:43Z', - 'paypal-transmission-sig': 'ODCa4gXmfnxkNga1t9p2HTIWFjlTj68P7MhueQd', - 'paypal-auth-version': 'v2', - 'paypal-cert-url': 'https://api.sandbox.paypal.com/v1/notifications/certs', - 'paypal-auth-algo': 'SHA256withRSA', - 'content-type': 'application/json', - 'user-agent': 'PayPal/AUHD-214.0-54280748', - 'correlation-id': 'c3823d4c07ce5', - cal_poolstack: 'amqunphttpdeliveryd:UNPHTTPDELIVERY', - client_pid: '23853', - 'content-length': '1706', - 'x-forwarded-proto': 'https', - 'x-forwarded-for': '173.0.82.126' - }, - body: { - id: 'WH-1VF24938EU372274X-83540367M0110254R', - event_version: '1.0', - create_time: '2020-03-06T15:34:50.000Z', - resource_type: 'subscription', - resource_version: '2.0', - event_type: 'BILLING.SUBSCRIPTION.CANCELLED', - summary: 'Subscription cancelled', - resource: { - shipping_amount: { currency_code: 'USD', value: '0.0' }, - start_time: '2020-03-05T08:00:00Z', - update_time: '2020-03-09T06:42:09Z', - quantity: '1', - subscriber: { - name: [Object], - email_address: 'sb-zdry81054163@personal.example.com', - payer_id: '82PVXVLDAU3E8', - shipping_address: [Object] - }, - billing_info: { - outstanding_balance: [Object], - cycle_executions: [Array], - last_payment: [Object], - next_billing_time: '2020-04-05T10:00:00Z', - failed_payments_count: 0 - }, - create_time: '2020-03-06T07:34:50Z', - links: [[Object]], - id: 'I-BA1ATBNF8T3P', - plan_id: 'P-6VP46874PR423771HLZDKFBA', - status: 'CANCELLED', - status_update_time: '2020-03-09T06:42:09Z' - }, - links: [ - { - href: 'https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1VF24938EU372274X-83540367M0110254R', - rel: 'self', - method: 'GET' - }, - { - href: 'https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1VF24938EU372274X-83540367M0110254R/resend', - rel: 'resend', - method: 'POST' - } - ] - } -}; -export const mockActivationHook = { - headers: { - host: 'a47fb0f4.ngrok.io', - accept: '*/*', - 'paypal-transmission-id': '22103660-5f7d-11ea-8ac4-7d4e2605c70c', - 'paypal-transmission-time': '2020-03-06T07:36:03Z', - 'paypal-transmission-sig': - 'a;sldfn;lqwjhepjtn12l3n5123mnpu1i-sc-_+++dsflqenwpk1n234uthmsqwr123', - 'paypal-auth-version': 'v2', - 'paypal-cert-url': - 'https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-1d93a270', - 'paypal-auth-algo': 'SHASHASHA', - 'content-type': 'application/json', - 'user-agent': 'PayPal/AUHD-214.0-54280748', - 'correlation-id': 'e0b25772e11af', - client_pid: '14973', - 'content-length': '2201', - 'x-forwarded-proto': 'https', - 'x-forwarded-for': '173.0.82.126' - }, - body: { - id: 'WH-77687562XN25889J8-8Y6T55435R66168T6', - create_time: '2018-19-12T22:20:32.000Z', - resource_type: 'subscription', - event_type: 'BILLING.SUBSCRIPTION.ACTIVATED', - summary: 'A billing agreement was activated.', - resource: { - quantity: '20', - subscriber: { - name: { - given_name: 'John', - surname: 'Doe' - }, - email_address: 'donor@freecodecamp.com', - shipping_address: { - name: { - full_name: 'John Doe' - }, - address: { - address_line_1: '2211 N First Street', - address_line_2: 'Building 17', - admin_area_2: 'San Jose', - admin_area_1: 'CA', - postal_code: '95131', - country_code: 'US' - } - } - }, - create_time: '2018-12-10T21:20:49Z', - shipping_amount: { - currency_code: 'USD', - value: '10.00' - }, - start_time: '2018-11-01T00:00:00Z', - update_time: '2018-12-10T21:20:49Z', - billing_info: { - outstanding_balance: { - currency_code: 'USD', - value: '10.00' - }, - cycle_executions: [ - { - tenure_type: 'TRIAL', - sequence: 1, - cycles_completed: 1, - cycles_remaining: 0, - current_pricing_scheme_version: 1 - }, - { - tenure_type: 'REGULAR', - sequence: 2, - cycles_completed: 1, - cycles_remaining: 0, - current_pricing_scheme_version: 2 - } - ], - last_payment: { - amount: { - currency_code: 'USD', - value: '500.00' - }, - time: '2018-12-01T01:20:49Z' - }, - next_billing_time: '2019-01-01T00:20:49Z', - final_payment_time: '2020-01-01T00:20:49Z', - failed_payments_count: 2 - }, - links: [ - { - href: 'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G', - rel: 'self', - method: 'GET' - }, - { - href: 'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G', - rel: 'edit', - method: 'PATCH' - }, - { - href: 'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/suspend', - rel: 'suspend', - method: 'POST' - }, - { - href: 'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/cancel', - rel: 'cancel', - method: 'POST' - }, - { - href: 'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/capture', - rel: 'capture', - method: 'POST' - } - ], - id: 'I-BW452GLLEP1G', - plan_id: 'P-5ML4271244454362WXNWU5NQ', - auto_renewal: true, - status: 'ACTIVE', - status_update_time: '2018-12-10T21:20:49Z' - }, - links: [ - { - href: 'https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J8-8Y6T55435R66168T6', - rel: 'self', - method: 'GET', - encType: 'application/json' - }, - { - href: 'https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J8-8Y6T55435R66168T6/resend', - rel: 'resend', - method: 'POST', - encType: 'application/json' - } - ], - event_version: '1.0', - resource_version: '2.0' - } -}; diff --git a/api-server/src/server/utils/donation.js b/api-server/src/server/utils/donation.js index abb2fe063fa..5500d445a50 100644 --- a/api-server/src/server/utils/donation.js +++ b/api-server/src/server/utils/donation.js @@ -1,91 +1,15 @@ /* eslint-disable camelcase */ -import axios from 'axios'; import debug from 'debug'; -import isEmail from 'validator/lib/isEmail'; import { donationSubscriptionConfig } from '../../../../shared/config/donation-settings'; -import keys from '../../../config/secrets'; const log = debug('fcc:boot:donate'); -const paypalVerifyWebhookURL = - keys.paypal.verifyWebhookURL || - `https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature`; -const paypalTokenURL = - keys.paypal.tokenUrl || `https://api.sandbox.paypal.com/v1/oauth2/token`; - -export async function getAsyncPaypalToken() { - const res = await axios.post(paypalTokenURL, null, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - auth: { - username: keys.paypal.client, - password: keys.paypal.secret - }, - params: { - grant_type: 'client_credentials' - } - }); - return res.data.access_token; -} - export function capitalizeKeys(object) { Object.keys(object).forEach(function (key) { object[key.toUpperCase()] = object[key]; }); } -export async function verifyWebHook(headers, body, token, webhookId) { - var webhookEventBody = typeof body === 'string' ? JSON.parse(body) : body; - - capitalizeKeys(headers); - - const payload = { - auth_algo: headers['PAYPAL-AUTH-ALGO'], - cert_url: headers['PAYPAL-CERT-URL'], - transmission_id: headers['PAYPAL-TRANSMISSION-ID'], - transmission_sig: headers['PAYPAL-TRANSMISSION-SIG'], - transmission_time: headers['PAYPAL-TRANSMISSION-TIME'], - webhook_id: webhookId, - webhook_event: webhookEventBody - }; - - const response = await axios.post(paypalVerifyWebhookURL, payload, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - } - }); - - if (response.data.verification_status === 'SUCCESS') { - return body; - } else { - throw { - // if verification fails, throw token verification error - message: `Failed token verification.`, - type: 'FailedPaypalTokenVerificationError' - }; - } -} - -export function verifyWebHookType(req) { - // check if webhook type for creation - const { - body: { event_type } - } = req; - - if ( - event_type === 'BILLING.SUBSCRIPTION.ACTIVATED' || - event_type === 'BILLING.SUBSCRIPTION.CANCELLED' - ) - return req; - else - throw { - message: 'Webhook type is not supported', - type: 'UnsupportedWebhookType' - }; -} - export const createAsyncUserDonation = (user, donation) => { log(`Creating donation:${donation.subscriptionId}`); // log user donation @@ -97,108 +21,6 @@ export const createAsyncUserDonation = (user, donation) => { }); }; -export function createDonationObj(body) { - // creates donation object - const { - resource: { - id, - status_update_time, - subscriber: { email_address } = { - email_address: null - } - } - } = body; - - let donation = { - email: email_address, - amount: 500, - duration: 'month', - provider: 'paypal', - subscriptionId: id, - customerId: email_address, - startDate: new Date(status_update_time).toISOString() - }; - return donation; -} - -export function createDonation(body, app) { - const { User } = app.models; - const { - resource: { - subscriber: { email_address } = { - email_address: null - } - } - } = body; - - let donation = createDonationObj(body); - - let email = email_address; - if (!email || !isEmail(email)) { - throw { - message: 'Paypal webhook email is not valid', - type: 'InvalidPaypalWebhookEmail' - }; - } - - return User.findOne({ where: { email } }, (err, user) => { - if (err) throw new Error(err); - if (!user) { - log(`Creating new user:${email}`); - return User.create({ email }) - .then(user => { - createAsyncUserDonation(user, donation); - }) - .catch(err => { - throw { - message: - err.message || 'findOne Donation records with email failed', - type: err.name || 'FailedFindingOneDonationEmail' - }; - }); - } - return createAsyncUserDonation(user, donation); - }); -} - -export async function cancelDonation(body, app) { - const { - resource: { id, status_update_time = new Date(Date.now()).toISOString() } - } = body; - const { Donation } = app.models; - Donation.findOne({ where: { subscriptionId: id } }, (err, donation) => { - if (err) - throw { - message: - err.message || 'findOne Donation records with subscriptionId failed', - type: err.name || 'FailedFindingOneSubscriptionId' - }; - if (!donation) - throw { - message: 'Donation record with provided subscription id is not found', - type: 'SubscriptionIdNotFound' - }; - log(`Updating donation record: ${donation.subscriptionId}`); - donation.updateAttributes({ - endDate: new Date(status_update_time).toISOString() - }); - }); -} - -export async function updateUser(body, app) { - const { event_type } = body; - if (event_type === 'BILLING.SUBSCRIPTION.ACTIVATED') { - // update user status based on new billing subscription events - createDonation(body, app); - } else if (event_type === 'BILLING.SUBSCRIPTION.CANCELLED') { - cancelDonation(body, app); - } else - throw { - message: 'Webhook type is not supported', - type: 'UnsupportedWebhookType' - }; -} - export async function createStripeCardDonation(req, res, stripe) { const { body: { paymentMethodId, amount, duration }, diff --git a/api-server/src/server/utils/donation.test.js b/api-server/src/server/utils/donation.test.js index 33ae2edef55..174299aa166 100644 --- a/api-server/src/server/utils/donation.test.js +++ b/api-server/src/server/utils/donation.test.js @@ -1,26 +1,8 @@ /* eslint-disable camelcase */ -import axios from 'axios'; import stripe from 'stripe'; import { ObjectId } from 'mongodb'; -import keys from '../../../config/secrets'; -import { - mockApp, - createDonationMockFn, - createUserMockFn, - updateDonationAttr, - updateUserAttr -} from '../boot_tests/fixtures'; -import { mockActivationHook, mockCancellationHook } from './__mocks__/donation'; -import { - getAsyncPaypalToken, - verifyWebHook, - updateUser, - capitalizeKeys, - createDonationObj, - handleStripeCardUpdateSession -} from './donation'; +import { handleStripeCardUpdateSession } from './donation'; -jest.mock('axios'); jest.mock('stripe', () => ({ checkout: { sessions: { @@ -29,143 +11,7 @@ jest.mock('stripe', () => ({ } })); -const verificationUrl = `https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature`; -const tokenUrl = `https://api.sandbox.paypal.com/v1/oauth2/token`; -const { body: activationHookBody, headers: activationHookHeaders } = - mockActivationHook; - describe('donation', () => { - describe('getAsyncPaypalToken', () => { - it('call paypal api for token ', async () => { - const res = { - data: { - access_token: 'token' - } - }; - - axios.post.mockImplementationOnce(() => Promise.resolve(res)); - - await expect(getAsyncPaypalToken()).resolves.toEqual( - res.data.access_token - ); - - expect(axios.post).toHaveBeenCalledTimes(1); - expect(axios.post).toHaveBeenCalledWith(tokenUrl, null, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - auth: { - username: keys.paypal.client, - password: keys.paypal.secret - }, - params: { - grant_type: 'client_credentials' - } - }); - }); - }); - - describe('verifyWebHook', () => { - // normalize headers - capitalizeKeys(activationHookHeaders); - const mockWebhookId = 'qwdfq;3w12341dfa4'; - const mockAccessToken = '241231223$!@$#1243'; - const mockPayLoad = { - auth_algo: activationHookHeaders['PAYPAL-AUTH-ALGO'], - cert_url: activationHookHeaders['PAYPAL-CERT-URL'], - transmission_id: activationHookHeaders['PAYPAL-TRANSMISSION-ID'], - transmission_sig: activationHookHeaders['PAYPAL-TRANSMISSION-SIG'], - transmission_time: activationHookHeaders['PAYPAL-TRANSMISSION-TIME'], - webhook_id: mockWebhookId, - webhook_event: activationHookBody - }; - const failedVerificationErr = { - message: `Failed token verification.`, - type: 'FailedPaypalTokenVerificationError' - }; - const axiosOptions = { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${mockAccessToken}` - } - }; - const successVerificationResponse = { - data: { - verification_status: 'SUCCESS' - } - }; - const failedVerificationResponse = { - data: { - verification_status: 'FAILED' - } - }; - - it('calls paypal for Webhook verification', async () => { - axios.post.mockImplementationOnce(() => - Promise.resolve(successVerificationResponse) - ); - - await expect( - verifyWebHook( - activationHookHeaders, - activationHookBody, - mockAccessToken, - mockWebhookId - ) - ).resolves.toEqual(activationHookBody); - - expect(axios.post).toHaveBeenCalledWith( - verificationUrl, - mockPayLoad, - axiosOptions - ); - }); - it('throws error if verification not successful', async () => { - axios.post.mockImplementationOnce(() => - Promise.resolve(failedVerificationResponse) - ); - - await expect( - verifyWebHook( - activationHookHeaders, - activationHookBody, - mockAccessToken, - mockWebhookId - ) - ).rejects.toEqual(failedVerificationErr); - }); - }); - - describe('updateUser', () => { - it('created a donation when a machting user found', () => { - updateUser(activationHookBody, mockApp); - expect(createDonationMockFn).toHaveBeenCalledTimes(1); - expect(createDonationMockFn).toHaveBeenCalledWith( - createDonationObj(activationHookBody) - ); - }); - it('create a user and donation when no machting user found', () => { - let newActivationHookBody = activationHookBody; - newActivationHookBody.resource.subscriber.email_address = - 'new@freecodecamp.org'; - updateUser(newActivationHookBody, mockApp); - expect(createUserMockFn).toHaveBeenCalledTimes(1); - }); - - it('modify user and donation records on cancellation', () => { - const { body: cancellationHookBody } = mockCancellationHook; - const { - resource: { status_update_time = new Date(Date.now()).toISOString() } - } = cancellationHookBody; - - updateUser(cancellationHookBody, mockApp); - expect(updateDonationAttr).toHaveBeenCalledWith({ - endDate: new Date(status_update_time).toISOString() - }); - expect(updateUserAttr).not.toHaveBeenCalled(); - }); - }); - describe('handleStripeCardUpdateSession', () => { const mockUserId = ObjectId('507f1f77bcf86cd799439011'); const mockDonation = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 330e03b2610..6385fe691e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -310,9 +310,6 @@ importers: accepts: specifier: 1.3.8 version: 1.3.8 - axios: - specifier: 0.23.0 - version: 0.23.0(debug@2.2.0) body-parser: specifier: 1.20.0 version: 1.20.0 @@ -476,9 +473,6 @@ importers: nodemon: specifier: 2.0.16 version: 2.0.16 - smee-client: - specifier: 1.2.3 - version: 1.2.3 client: dependencies: @@ -2196,7 +2190,7 @@ packages: '@babel/traverse': 7.23.0 '@babel/types': 7.23.0 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2430,7 +2424,7 @@ packages: '@babel/core': 7.18.0 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 lodash.debounce: 4.0.8 resolve: 1.22.8 semver: 6.3.1 @@ -5675,7 +5669,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.3 '@babel/types': 7.23.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5692,7 +5686,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.9 '@babel/types': 7.23.9 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -9424,7 +9418,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -9984,14 +9978,6 @@ packages: - debug dev: false - /axios@0.23.0(debug@2.2.0): - resolution: {integrity: sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==} - dependencies: - follow-redirects: 1.15.3(debug@2.2.0) - transitivePeerDependencies: - - debug - dev: false - /axios@1.6.7(debug@4.3.4): resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} dependencies: @@ -11886,6 +11872,7 @@ packages: /chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + requiresBuild: true dependencies: anymatch: 3.1.3 braces: 3.0.2 @@ -13015,6 +13002,17 @@ packages: ms: 2.1.2 dev: true + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -14703,11 +14701,6 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - /eventsource@1.1.2: - resolution: {integrity: sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==} - engines: {node: '>=0.12.0'} - dev: true - /evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} dependencies: @@ -17100,7 +17093,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -19277,7 +19270,7 @@ packages: dependencies: async: 3.2.4 bluebird: 3.7.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 msgpack5: 4.5.1 strong-globalize: 5.1.0 uuid: 7.0.3 @@ -24347,19 +24340,6 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - /smee-client@1.2.3: - resolution: {integrity: sha512-uDrU8u9/Ln7aRXyzGHgVaNUS8onHZZeSwQjCdkMoSL7U85xI+l+Y2NgjibkMJAyXkW7IAbb8rw9RMHIjS6lAwA==} - hasBin: true - dependencies: - commander: 2.20.3 - eventsource: 1.1.2 - morgan: 1.10.0 - superagent: 7.1.5 - validator: 13.11.0 - transitivePeerDependencies: - - supports-color - dev: true - /smtp-connection@2.12.0: resolution: {integrity: sha512-UP5jK4s5SGcUcqPN4U9ingqKt9mXYSKa52YhqxPuMecAnUOsVJpOmtgGaOm1urUBJZlzDt1M9WhZZkgbhxQlvg==} dependencies: @@ -24968,7 +24948,7 @@ packages: dependencies: '@types/express': 4.17.18 accepts: 1.3.8 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 ejs: 3.1.9 fast-safe-stringify: 2.1.1 http-status: 1.7.0 @@ -24983,7 +24963,7 @@ packages: engines: {node: '>=6'} dependencies: accept-language: 3.0.18 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globalize: 1.7.0 lodash: 4.17.21 md5: 2.3.0 @@ -24998,7 +24978,7 @@ packages: engines: {node: '>=8.9'} dependencies: accept-language: 3.0.18 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globalize: 1.7.0 lodash: 4.17.21 md5: 2.3.0 @@ -25014,7 +24994,7 @@ packages: engines: {node: '>=10'} dependencies: accept-language: 3.0.18 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globalize: 1.7.0 lodash: 4.17.21 md5: 2.3.0 @@ -25031,7 +25011,7 @@ packages: dependencies: async: 3.2.4 body-parser: 1.20.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 depd: 2.0.0 escape-string-regexp: 2.0.0 eventemitter2: 5.0.1 @@ -25119,25 +25099,6 @@ packages: /sudo-prompt@8.2.5: resolution: {integrity: sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==} - /superagent@7.1.5: - resolution: {integrity: sha512-HQYyGuDRFGmZ6GNC4hq2f37KnsY9Lr0/R1marNZTgMweVDQLTLJJ6DGQ9Tj/xVVs5HEnop9EMmTbywb5P30aqw==} - engines: {node: '>=6.4.0 <13 || >=14'} - dependencies: - component-emitter: 1.3.0 - cookiejar: 2.1.4 - debug: 4.3.4(supports-color@8.1.1) - fast-safe-stringify: 2.1.1 - form-data: 4.0.0 - formidable: 2.1.2 - methods: 1.1.2 - mime: 2.6.0 - qs: 6.11.2 - readable-stream: 3.6.2 - semver: 7.6.0 - transitivePeerDependencies: - - supports-color - dev: true - /superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} @@ -26408,6 +26369,7 @@ packages: /validator@13.11.0: resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} engines: {node: '>= 0.10'} + dev: false /validator@13.7.0: resolution: {integrity: sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==} diff --git a/sample.env b/sample.env index 2ec638c9d14..092811e8cd9 100644 --- a/sample.env +++ b/sample.env @@ -27,13 +27,6 @@ STRIPE_SECRET_KEY=sk_from_stripe_dashboard # PayPal PAYPAL_CLIENT_ID=id_from_paypal_dashboard -PAYPAL_SECRET=secret_from_paypal_dashboard -PAYPAL_VERIFY_WEBHOOK_URL=https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature -PAYPAL_API_TOKEN_URL=https://api.sandbox.paypal.com/v1/oauth2/token -PAYPAL_WEBHOOK_ID=webhook_id_from_paypal_dashboard - -# Webhook proxy url from smee.io for PayPal -WEBHOOK_PROXY_URL= # Patreon PATREON_CLIENT_ID=id_from_patreon_dashboard