From e54fdad6b07b3b64a3234fd26e0a7116d388d4d0 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Tue, 23 Jan 2024 14:15:51 -0700 Subject: [PATCH] feat(release): update 92ca053 --- .env.sample | 11 ++++---- README.md | 16 +++++------ docs/openapi.yaml | 9 ++++++ package.json | 2 +- src/middleware/verifySignature.ts | 2 +- src/routes/topUp.ts | 46 ++++++++++++++++++++++++------- src/server.ts | 12 +++++++- src/utils/validators.ts | 24 ++++++++++++++++ tests/helpers/stubs.ts | 18 ++++++++++-- tests/helpers/testHelpers.ts | 2 +- yarn.lock | 10 +++---- 11 files changed, 118 insertions(+), 34 deletions(-) diff --git a/.env.sample b/.env.sample index 0dd4431..c3e0a03 100644 --- a/.env.sample +++ b/.env.sample @@ -1,5 +1,6 @@ -NODE_ENV= -STRIPE_SECRET_KEY= -STRIPE_WEBHOOK_SECRET= -PRIVATE_ROUTE_SECRET= -MIGRATE_ON_STARTUP= +NODE_ENV=test +PORT=4000 +MIGRATE_ON_STARTUP=true +STRIPE_SECRET_KEY=test +STRIPE_WEBHOOK_SECRET=test +PRIVATE_ROUTE_SECRET=test diff --git a/README.md b/README.md index 43de152..4ec36a5 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ With a compatible system, follow these steps to start the upload service: - `yarn start` - alternatively use `yarn start:watch` to run the app in development mode with hot reloading provided by `nodemon` +Note: we store credentials for the service in AWS - to avoid these requests - set your NODE_ENV to `test` in your .env file. + ## Database The service relies on a postgres database. The following scripts can be used to create a local postgres database via docker: @@ -62,16 +64,14 @@ Additional `knex` documentation can be found [here](https://knexjs.org/guide/mig To run this service and a connected postgres database, fully migrated. -Run the container: - -```shell -yarn start:docker -``` +- `cp .env.sample .env` (and update values) +- `yarn start:docker` - run the local service and postgres database in docker containers -To build the image: +Alternatively, you can run the service in docker and connect to a local postgres database. You will need to standup `postgres` in a separate container. -```shell -docker build --build-arg NODE_VERSION=$(cat .nvmrc |cut -c2-8) --build-arg NODE_VERSION_SHORT=$(cat .nvmrc |cut -c2-3) . +```bash +docker run --name turbo-payment-service-postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres +docker run --env-file .env -p 4000:4000 ghcr.io/ardriveapp/turbo-payment-service:latest ``` ## Tests diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 041c35e..edb5829 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -401,6 +401,15 @@ paths: schema: "$ref": "#/components/schemas/PromoCode" + - name: uiMode + in: query + required: false + schema: + type: string + description: Which UI Mode to create the checkout session in + default: hosted + example: embedded + responses: "200": description: OK diff --git a/package.json b/package.json index d013fa1..5dce594 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "prom-client": "^14.1.0", "raw-body": "^2.5.2", "sinon-chai": "^3.7.0", - "stripe": "^11.13.0", + "stripe": "^14.11.0", "validator": "^13.11.0", "winston": "^3.8.2", "yaml": "^2.2.2" diff --git a/src/middleware/verifySignature.ts b/src/middleware/verifySignature.ts index 771c60c..4275f89 100644 --- a/src/middleware/verifySignature.ts +++ b/src/middleware/verifySignature.ts @@ -37,7 +37,7 @@ export async function verifySignature(ctx: Context, next: Next): Promise { try { if (!signature || !publicKey || !nonce) { - logger.info("Missing signature, public key or nonce"); + logger.debug("Missing signature, public key or nonce"); return next(); } logger.info("Verifying arweave signature"); diff --git a/src/routes/topUp.ts b/src/routes/topUp.ts index 71285d7..1a12f7b 100644 --- a/src/routes/topUp.ts +++ b/src/routes/topUp.ts @@ -38,6 +38,7 @@ import { parseQueryParams } from "../utils/parseQueryParams"; import { validateDestinationAddressType, validateGiftMessage, + validateUiMode, } from "../utils/validators"; export async function topUp(ctx: KoaContext, next: Next) { @@ -51,6 +52,8 @@ export async function topUp(ctx: KoaContext, next: Next) { address: rawDestinationAddress, } = ctx.params; + const referer = ctx.headers.referer; + const loggerObject = { amount, currency, method, rawDestinationAddress }; if (!topUpMethods.includes(method)) { @@ -63,6 +66,7 @@ export async function topUp(ctx: KoaContext, next: Next) { const { destinationAddressType: rawAddressType, giftMessage: rawGiftMessage, + uiMode: rawUiMode, } = ctx.query; const destinationAddressType = rawAddressType @@ -79,6 +83,11 @@ export async function topUp(ctx: KoaContext, next: Next) { return next(); } + const uiMode = rawUiMode ? validateUiMode(ctx, rawUiMode) : "hosted"; + if (uiMode === false) { + return next(); + } + let destinationAddress: string; if (destinationAddressType === "arweave") { if (!isValidArweaveBase64URL(rawDestinationAddress)) { @@ -207,6 +216,7 @@ export async function topUp(ctx: KoaContext, next: Next) { { ...stripeMetadataRaw, winstonCreditAmount: finalPrice.winc.toString(), + referer, } as Record ); @@ -220,6 +230,7 @@ export async function topUp(ctx: KoaContext, next: Next) { amount: actualPaymentAmount, currency: payment.type, metadata: stripeMetadata, + payment_method_types: ["card"], }); } else { const localGiftUrl = `http://localhost:5173`; @@ -232,17 +243,31 @@ export async function topUp(ctx: KoaContext, next: Next) { const urlEncodedGiftMessage = giftMessage ? encodeURIComponent(giftMessage) : undefined; + + const urls: + | { success_url: string; cancel_url: string } + | { redirect_on_completion: "never" } = + uiMode === "embedded" + ? { + redirect_on_completion: "never", + } + : { + // // TODO: Success and Cancel URLS (Do we need app origin? e.g: ArDrive Widget, Top Up Page, ario-turbo-cli) + success_url: "https://app.ardrive.io", + cancel_url: + destinationAddressType === "email" + ? `${giftUrl}?email=${destinationAddress}&amount=${ + payment.amount + }${ + urlEncodedGiftMessage + ? `&giftMessage=${urlEncodedGiftMessage}` + : "" + }` + : "https://app.ardrive.io", + }; + intentOrCheckout = await stripe.checkout.sessions.create({ - // TODO: Success and Cancel URLS (Do we need app origin? e.g: ArDrive Widget, Top Up Page, ario-turbo-cli) - success_url: "https://app.ardrive.io", - cancel_url: - destinationAddressType === "email" - ? `${giftUrl}?email=${destinationAddress}&amount=${payment.amount}${ - urlEncodedGiftMessage - ? `&giftMessage=${urlEncodedGiftMessage}` - : "" - }` - : "https://app.ardrive.io", + ...urls, currency: payment.type, automatic_tax: { enabled: !!process.env.ENABLE_AUTO_STRIPE_TAX || false, @@ -269,6 +294,7 @@ export async function topUp(ctx: KoaContext, next: Next) { metadata: stripeMetadata, }, mode: "payment", + ui_mode: uiMode, }); } } catch (error) { diff --git a/src/server.ts b/src/server.ts index be7abfa..cfe10c4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -46,6 +46,16 @@ process.on("uncaughtException", (error) => { logger.error("Uncaught exception:", error); }); +process.on("SIGTERM", () => { + logger.info("SIGTERM received, exiting..."); + process.exit(0); +}); + +process.on("SIGINT", () => { + logger.info("SIGINT received, exiting..."); + process.exit(0); +}); + export async function createServer( arch: Partial, port: number = defaultPort @@ -76,7 +86,7 @@ export async function createServer( const paymentDatabase = arch.paymentDatabase ?? new PostgresDatabase({ migrate: migrateOnStartup }); const stripe = - arch.stripe ?? new Stripe(STRIPE_SECRET_KEY, { apiVersion: "2022-11-15" }); + arch.stripe ?? new Stripe(STRIPE_SECRET_KEY, { apiVersion: "2023-10-16" }); const emailProvider = (() => { if (!isGiftingEnabled) { diff --git a/src/utils/validators.ts b/src/utils/validators.ts index 77715df..20b092a 100644 --- a/src/utils/validators.ts +++ b/src/utils/validators.ts @@ -163,3 +163,27 @@ export function validateGiftMessage( return validator.escape(message); } + +export const uiModes = ["hosted", "embedded"] as const; +export type UiMode = (typeof uiModes)[number]; +function isUiMode(uiMode: string): uiMode is UiMode { + return uiModes.includes(uiMode as UiMode); +} +export function validateUiMode( + ctx: KoaContext, + uiMode: string | string[] +): UiMode | false { + const mode = validateSingularQueryParameter(ctx, uiMode); + + if (!mode || !isUiMode(mode)) { + ctx.response.status = 400; + ctx.body = `Invalid ui mode! Allowed modes: "${uiModes.toString()}"`; + ctx.state.logger.error("Invalid ui mode!", { + query: ctx.query, + params: ctx.params, + }); + return false; + } + + return mode; +} diff --git a/tests/helpers/stubs.ts b/tests/helpers/stubs.ts index dc2acf6..bfd3b33 100644 --- a/tests/helpers/stubs.ts +++ b/tests/helpers/stubs.ts @@ -78,6 +78,11 @@ export const paymentIntentStub = ({ statement_descriptor_suffix: null, transfer_data: null, transfer_group: null, + latest_charge: null, + payment_method_configuration_details: { + id: "pm_1MnkNVC8apPOWkDLH9wJvENb", + parent: "card_1MnkNVC8apPOWkDLH9wJvENb", + }, }; }; @@ -104,6 +109,8 @@ export const checkoutSessionStub = ({ custom_text: { shipping_address: null, submit: null, + after_submit: null, + terms_of_service_acceptance: null, }, custom_fields: [], customer_creation: null, @@ -140,6 +147,13 @@ export const checkoutSessionStub = ({ success_url: "", total_details: null, url: null, + client_secret: null, + currency_conversion: null, + payment_method_configuration_details: { + id: "pm_1MnkNVC8apPOWkDLH9wJvENb", + parent: "card_1MnkNVC8apPOWkDLH9wJvENb", + }, + ui_mode: "hosted", }; }; @@ -233,7 +247,7 @@ export const stripeStubEvent = ({ }: { eventObject: Stripe.Dispute | Stripe.PaymentIntent; id?: string; - type: string; + type: "payment_intent.succeeded" | "charge.dispute.created"; }): Stripe.Event => { return { id, @@ -247,7 +261,7 @@ export const stripeStubEvent = ({ type: type, pending_webhooks: 0, request: null, - }; + } as Stripe.Event; }; export const expectedArPrices = { diff --git a/tests/helpers/testHelpers.ts b/tests/helpers/testHelpers.ts index 321f538..2e083df 100644 --- a/tests/helpers/testHelpers.ts +++ b/tests/helpers/testHelpers.ts @@ -110,7 +110,7 @@ export const coinGeckoOracle = new CoingeckoArweaveToFiatOracle(coinGeckoAxios); export const arweaveToFiatOracle = new ReadThroughArweaveToFiatOracle({ oracle: coinGeckoOracle, }); -export const stripe = new Stripe("test", { apiVersion: "2022-11-15" }); +export const stripe = new Stripe("test", { apiVersion: "2023-10-16" }); export const pricingService = new TurboPricingService({ arweaveToFiatOracle }); export const axios = axiosPackage.create({ baseURL: localTestUrl, diff --git a/yarn.lock b/yarn.lock index babf72b..0eaa50c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5697,7 +5697,7 @@ __metadata: rimraf: ^3.0.2 sinon: ^14.0.0 sinon-chai: ^3.7.0 - stripe: ^11.13.0 + stripe: ^14.11.0 ts-node: ^10.7.0 typescript: ^4.7.4 validator: ^13.11.0 @@ -6566,13 +6566,13 @@ __metadata: languageName: node linkType: hard -"stripe@npm:^11.13.0": - version: 11.18.0 - resolution: "stripe@npm:11.18.0" +"stripe@npm:^14.11.0": + version: 14.11.0 + resolution: "stripe@npm:14.11.0" dependencies: "@types/node": ">=8.1.0" qs: ^6.11.0 - checksum: 7da0195730df921c983560f22820797c1a9f8bc200e89012a87180d65628506a98fe400dca2c6e78ea98b9b931fac62dd9c223920566e0aa71468ec6cc671695 + checksum: 3d4c683675c0046de567d286aa5c79e3b8e2551e9cb64921790c835fe1fbe1855a31332a014e74507fad793f3976f683e94d2126a9e30b137c13d4992d0818e7 languageName: node linkType: hard