diff --git a/pass/app/Order.js b/pass/app/Order.js index 70ff3223e546cc5207e752f1de4783ca3d926a33..2a3a26d6d4f2480a4f335280afaa98a2d8b7f5a9 100644 --- a/pass/app/Order.js +++ b/pass/app/Order.js @@ -4,6 +4,10 @@ const dayjs = require("dayjs"); const path = require("path"); class Order { + static get STORAGE_MUTEX_KEY_PREFIX() { + return "order_mutex"; + } + static get STORAGE_KEY_PREFIX() { return "order_"; } @@ -100,47 +104,62 @@ class Order { } static async LOAD_ORDER_FROM_ID(order_id) { + let Redis = require("ioredis"); + let redis_client = new Redis({ + host: config.get("redis.host"), + }); return new Promise((resolve, reject) => { - let Redis = require("ioredis"); - let redis_client = new Redis({ - host: config.get("redis.host"), - }); - - let redis_key = Order.STORAGE_KEY_PREFIX + order_id; - - redis_client.hgetall(redis_key).then((order_data) => { - console.log(Object.keys(order_data).length); - if (Object.keys(order_data).length === 0) { - // Checking FS for order - let order_date = dayjs.unix(order_id.substr(0, 10)); - let order_file = path.join( - Order.GET_ORDER_FILE_BASE_PATH(order_date), - order_id.toString() + ".json" - ); - let fs = require("fs"); - console.log("Loading from fs: " + order_file); - if (fs.existsSync(order_file)) { - order_data = JSON.parse(fs.readFileSync(order_file)); - console.log(order_data); + redis_client + .setnx(Order.STORAGE_MUTEX_KEY_PREFIX + order_id, 1) + .then((mutex) => { + console.log(mutex); + if (mutex !== 1) { + // Could not acquire lock. Try again + throw "LOCK_NOT_ACQUIRED"; } else { - return reject("Could not find Order in our database! Checking FS"); + return redis_client.expire(Order.STORAGE_MUTEX_KEY_PREFIX, 15); } - } - let loaded_order = new Order( - order_data.order_id, - order_data.amount, - order_data.price - ); - if (order_data.payment_method_link) { - loaded_order.setPaymentMethodLink( - JSON.parse(order_data.payment_method_link) + }) + .then(() => redis_client.hgetall(Order.STORAGE_KEY_PREFIX + order_id)) + .then((order_data) => { + if (Object.keys(order_data).length === 0) { + // Checking FS for order + let order_date = dayjs.unix(order_id.substr(0, 10)); + let order_file = path.join( + Order.GET_ORDER_FILE_BASE_PATH(order_date), + order_id.toString() + ".json" + ); + let fs = require("fs"); + if (fs.existsSync(order_file)) { + order_data = JSON.parse(fs.readFileSync(order_file)); + } else { + throw "Could not find Order in our database! Checking FS"; + } + } + console.log(order_data); + let loaded_order = new Order( + order_data.order_id, + order_data.amount, + order_data.price ); - } - if (order_data.payment_completed) { - loaded_order.setPaymentCompleted(true); - } - resolve(loaded_order); - }); + if (order_data.payment_method_link) { + loaded_order.setPaymentMethodLink( + JSON.parse(order_data.payment_method_link) + ); + } + if (order_data.payment_completed) { + loaded_order.setPaymentCompleted(true); + } + resolve(loaded_order); + }) + .catch((reason) => { + if (reason === "LOCK_NOT_ACQUIRED") { + console.log("lock not acquired"); + setTimeout(Order.LOAD_ORDER_FROM_ID(order_id), 5000); + } else { + reject(reason); + } + }); }); } @@ -168,12 +187,14 @@ class Order { if (!fs.existsSync(path.dirname(order_file))) { fs.mkdirSync(path.dirname(order_file), { recursive: true }); } - this.#redis_client.del(redis_key).then(() => { - return fs.writeFileSync( - order_file, - JSON.stringify(stored_data, null, 4) - ); - }); + let redis_client = this.#redis_client; + let mutex_key = Order.STORAGE_MUTEX_KEY_PREFIX + this.#order_id; + return this.#redis_client + .del(redis_key) + .then(() => + fs.writeFileSync(order_file, JSON.stringify(stored_data, null, 4)) + ) + .then(() => redis_client.del(mutex_key)); } else { // Store Order in Redis let expiration = new dayjs(); @@ -185,6 +206,7 @@ class Order { .pipeline() .hmset(redis_key, stored_data) .expireat(redis_key, expiration.unix()) + .del(Order.STORAGE_MUTEX_KEY_PREFIX + this.#order_id) .exec(); } } @@ -216,13 +238,10 @@ class Order { let order_mutex = Math.floor(Math.random() * 10000); do { // make sure this order_id is not already registered - let order_lock = await redis_connection.setnx( - "" + order_base + order_mutex, - true - ); + order_id = "" + order_base + order_mutex; + let order_lock = await redis_connection.setnx(order_id, true); if (order_lock === 1) { await redis_connection.expire(order_id, 5); - order_id = "" + order_base + order_mutex; } else { console.log("Couldn't acquire lock"); order_mutex++; diff --git a/pass/config/default.json b/pass/config/default.json index ecaad1b319b13fb18de96ca3e99e71fe53682f0d..35f9aa080dad97a53592d9687f132b2ac525589a 100644 --- a/pass/config/default.json +++ b/pass/config/default.json @@ -1,4 +1,7 @@ { + "app": { + "url": "http://localhost:8080" + }, "price": { "per_300": 5 }, diff --git a/pass/routes/checkout/paypal.js b/pass/routes/checkout/paypal.js index ada00709727b3cb7b9546fbe914be0d858a5f576..7ec7c3e0c130f2c9401d00084989d1673b96acc9 100644 --- a/pass/routes/checkout/paypal.js +++ b/pass/routes/checkout/paypal.js @@ -121,47 +121,31 @@ router.post("/:funding_source/order/cancel", async (req, res) => { // capture payment & store order information or fullfill order router.post("/:funding_source/order/capture", async (req, res) => { - Order.LOAD_ORDER_FROM_ID(req.body.order_id).then((loaded_order) => { - loaded_order.setPaymentCompleted(true); - let paypal_order = loaded_order.getPaymentMethodLink(); - if (paypal_order.name !== "paypal") { - res - .status(400) - .json({ errors: [{ msg: "This order is not a PayPal Payment" }] }); - return; - } - let key_filled = false; - loaded_order - .save() - .then(() => { - return Key.CHARGE_KEY(req.data.key.key, req.data.checkout.amount); // ToDo verify amount - }) - .then((key_charge) => { - key_filled = true; - return key_charge; - }) - .then((key_charge) => { - return capturePayment(paypal_order.order_id); - }) - .then((captureData) => { - if (captureData.status === "COMPLETED") { - captureData.redirect_url = "/key/" + req.data.key.key; - res.json(captureData); - } else { - res - .status(400) - .json({ errors: [{ msg: "Couldn't capture the payment" }] }); - } - }) - .catch((error) => { - // We captured a payment but did not successfully update the order - if (key_filled) { - // Capture failed... Remove searches from key again - console.log("Removing searches from key again"); - } - res.status(400).json({ errors: [{ msg: error.toString() }] }); - }); - }); + verifyWebhook() + .then(() => { + return Order.LOAD_ORDER_FROM_ID(req.body.order_id); + }) + .then((loaded_order) => { + let paypal_order = loaded_order.getPaymentMethodLink(); + capturePayment(paypal_order.order_id) + .then((captureData) => { + if (captureData.status === "COMPLETED") { + captureData.redirect_url = "/key/" + req.data.key.key; + res.json(captureData); + } else { + throw "Couldn't capture the payment"; + } + }) + .then(() => { + loaded_order.setPaymentCompleted(true); + return loaded_order.save(); + }) + .then(() => Key.CHARGE_KEY(req.data.key.key, loaded_order.getAmount())) + .catch((reason) => { + console.error(reason); + res.status(400).json({ errors: [{ msg: reason.toString() }] }); + }); + }); }); module.exports = router; @@ -176,7 +160,6 @@ module.exports = router; async function createOrder(loaded_order) { const accessToken = await generateAccessToken(); - console.log(accessToken); const url = `${base}/v2/checkout/orders`; @@ -280,6 +263,71 @@ async function capturePayment(orderId) { return data; } +async function verifyWebhook() { + let domain = config.get("app.url").replace(/\/+$/, ""); + + if (!domain.startsWith("https://")) { + // Do not attempt to register a webhook for localhost etc. + return; + } + + let Redis = require("ioredis"); + let redis_client = new Redis({ + host: config.get("redis.host"), + }); + let webhook_already_registered_key = "paypal_webhook_registered"; + if (await redis_client.get(webhook_already_registered_key)) { + return; + } + const accessToken = await generateAccessToken(); + return fetch(`${base}/v1/notifications/webhooks`, { + method: "get", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + }) + .then((response) => response.json()) + .then((webhooks) => { + webhooks.webhooks.forEach((webhook) => { + if (webhook.url.startsWith(domain)) { + throw "WEBHOOK_ALREADY_REGISTERED"; // Webhook already registered + } + }); + }) + .then(async () => { + // Webhook does not exist yet + return fetch(`${base}/v1/notifications/webhooks`, { + method: "post", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + event_types: [ + { name: "PAYMENT.CAPTURE.COMPLETED" }, + { name: "PAYMENT.CAPTURE.REFUNDED" }, + { name: "PAYMENT.CAPTURE.REVERSED" }, + ], + url: domain + "/webhooks/paypal", + }), + }).then((response) => { + if (response.status !== 201) { + return Promise.reject(response); + } + redis_client.set(webhook_already_registered_key, true); + }); + }) + .catch((reason) => { + if (reason !== "WEBHOOK_ALREADY_REGISTERED") { + console.warning("Could not register Webhook for PayPal."); + console.warning(reason); + } else { + redis_client.set(webhook_already_registered_key, true); + } + }); +} + async function refundPayment(capture_ids) { const accessToken = await generateAccessToken(); let promises = [];