From 57863ec5eae382cb1d90b076f15a5c5dad5b3d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Sat, 24 Feb 2024 01:41:09 +0000 Subject: [PATCH] debug: add debug endpoints and IAP debug logs for easier testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the following debug features: - A debug endpoint to allow deletion of an account (useful for starting over a test on the staging server) - A debug endpoint to force a UUID on a pubkey. This is useful when there is a mismatch of UUIDs in the staging/test server and the IAP purchased via the sandbox, and it is a pain to reset those IAPs - Several debug logs on the IAP receipt verification module. This helps, among other things, to easily find what UUID is associated with an IAP These were made to make testing and debugging on staging easier. This commit also adds some documentation about it Testing: 1. Used the admin endpoint to delete a test account. PASS 2. Restarted Damus and tried observing the IAP debug logs 3. Found the UUID on my Sandbox transactions 4. Used the new debug endpoint via cURL to force that UUID on the local database, and when I restarted Damus the receipt verification passed without resetting my Sandbox. Signed-off-by: Daniel D’Aquino Reviewed-by: William Casarin Link: 20240224014100.54131-1-daniel@daquino.me Signed-off-by: William Casarin --- README.md | 16 +++++++ src/app_store_receipt_verifier.js | 16 ++++++- src/router_config.js | 74 ++++++++++++++++++++++++++++++- src/user_management.js | 15 ++++++- 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a144171..c6505d5 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ The Damus API backend for Damus Purple and other functionality. - `ALLOW_HTTP_AUTH`: Set to `"true"` to enable HTTP basic auth for all endpoints. (Useful for testing locally, otherwise it forces HTTPS) - `ADMIN_PASSWORD`: Password for admin API endpoints (optional, leaving this blank will disable admin endpoints) - `LN_INVOICE_CHECK_TIMEOUT_MS`: Timeout in milliseconds for checking the status of a Lightning Network invoice. Defaults to 60000 (60 seconds), and shorter for tests +- `ENABLE_DEBUG_ENDPOINTS`: Set to `"true"` to enable debug endpoints (for testing or staging only). This includes endpoints to delete users or force UUIDs. ## npm scripts @@ -46,3 +47,18 @@ The Damus API backend for Damus Purple and other functionality. - `npm test`: Run the unit tests - `npm run type-check`: Run a type check on all files in the project - `npm run type-check-path -- `: Run a type check on a specific file or directory + +## Testing and debugging tips + +### IAP (Apple In-app Purchase) receipt verification + +- Run the server with `DEBUG=iap` to see verbose debug logs for the IAP receipt verification process. You can also use those logs to find which UUID an IAP is associated with. +- If you need to force a specific UUID for a user (e.g. when you reset the db but can't reset your Sandbox IAP history), you can enable the `ENABLE_DEBUG_ENDPOINTS` environment variable and use this debug endpoint to force a UUID for a user: + ``` +```bash +curl -X PUT http:///admin/users//account-uuid \ + -H "Content-Type: application/json" \ + -d '{"admin_password": "", "account_uuid": ""}' +``` + + diff --git a/src/app_store_receipt_verifier.js b/src/app_store_receipt_verifier.js index 5318b1d..2613301 100644 --- a/src/app_store_receipt_verifier.js +++ b/src/app_store_receipt_verifier.js @@ -7,6 +7,7 @@ const { AppStoreServerAPIClient, Environment, ReceiptUtility, Order, ProductType, SignedDataVerifier } = require("@apple/app-store-server-library") const { current_time } = require("./utils") const fs = require('fs') +const debug = require('debug')('iap') /** * Verifies the receipt data and returns the expiry date if the receipt is valid. @@ -17,8 +18,10 @@ const fs = require('fs') * @returns {Promise} The expiry date of the receipt if valid, null otherwise. */ async function verify_receipt(receipt_data, authenticated_account_token) { + debug("Verifying receipt with authenticated account token: %s", authenticated_account_token); // Mocking logic for testing purposes if (process.env.MOCK_VERIFY_RECEIPT == "true") { + debug("Mocking verify_receipt with expiry date 30 days from now"); return current_time() + 60 * 60 * 24 * 30; } @@ -30,6 +33,7 @@ async function verify_receipt(receipt_data, authenticated_account_token) { // Get the transaction ID from the receipt const transactionId = extractTransactionIdFromAppReceipt(receipt_data); + debug("[Account token: %s] Transaction ID extracted from the receipt: %s", authenticated_account_token, transactionId); // If the transaction ID is present, fetch the transaction history, verify the transactions, and return the latest expiry date if (transactionId != null) { @@ -53,14 +57,19 @@ async function verify_receipt(receipt_data, authenticated_account_token) { */ async function fetchLastVerifiedExpiryDate(client, transactionId, rootCaDir, environment, bundleId, authenticatedAccountToken) { const transactions = await fetchTransactionHistory(client, transactionId); + debug("[Account token: %s] Fetched transaction history for transaction ID: %s; Found %d transactions", authenticatedAccountToken, transactionId, transactions.length); const rootCAs = readCertificateFiles(rootCaDir); const decodedTransactions = await verifyAndDecodeTransactions(transactions, rootCAs, environment, bundleId); + debug("[Account token: %s] Verified and decoded %d transactions", authenticatedAccountToken, decodedTransactions.length); const validDecodedTransactions = filterTransactionsThatBelongToAccount(decodedTransactions, authenticatedAccountToken); + debug("[Account token: %s] Filtered transactions that belong to the account UUID. Found %d matching transactions", authenticatedAccountToken, validDecodedTransactions.length); if (validDecodedTransactions.length === 0) { return null; } const expiryDates = decodedTransactions.map((decodedTransaction) => decodedTransaction.expiresDate); + debug("[Account token: %s] Found expiry dates: %o", authenticatedAccountToken, expiryDates); const latestExpiryDate = Math.max(...expiryDates); + debug("[Account token: %s] Latest expiry date: %d", authenticatedAccountToken, latestExpiryDate); return latestExpiryDate / 1000; // Return the latest expiry date in seconds } @@ -73,7 +82,12 @@ async function fetchLastVerifiedExpiryDate(client, transactionId, rootCaDir, env * @returns {Array} The transactions that belong to the authorized account token. */ function filterTransactionsThatBelongToAccount(transactions, authenticatedAccountToken) { - return transactions.filter((transaction) => transaction.appAccountToken.toUpperCase() === authenticatedAccountToken.toUpperCase()); + return transactions.filter((transaction) => { + const txToken = transaction.appAccountToken.toUpperCase(); + const authAccountToken = authenticatedAccountToken.toUpperCase(); + debug("Comparing transaction account token: %s with authenticated account token: %s", txToken, authAccountToken); + return txToken === authAccountToken; + }) } const certificateCache = new Map(); diff --git a/src/router_config.js b/src/router_config.js index b747e8e..078179f 100644 --- a/src/router_config.js +++ b/src/router_config.js @@ -1,5 +1,5 @@ const { json_response, simple_response, error_response, invalid_request, unauthorized_response } = require('./server_helpers') -const { create_account, get_account_info_payload, check_account, get_account, put_account, get_account_and_user_id, get_user_uuid, bumpy_set_expiry } = require('./user_management') +const { create_account, get_account_info_payload, check_account, get_account, put_account, get_account_and_user_id, get_user_uuid, bumpy_set_expiry, delete_account } = require('./user_management') const handle_translate = require('./translate') const verify_receipt = require('./app_store_receipt_verifier').verify_receipt const bodyParser = require('body-parser') @@ -293,7 +293,77 @@ function config_router(app) { } json_response(res, new_checkout_object) }) - + + if (process.env.ENABLE_DEBUG_ENDPOINTS == "true") { + /** + * This route is used to delete a user account. + * This is useful when testing the onboarding flow, and we need to reset the user's account. + */ + router.delete('/admin/users/:pubkey', async (req, res) => { + const pubkey = req.params.pubkey + const body = req.body + const admin_password = body.admin_password + if(!process.env.ADMIN_PASSWORD) { + unauthorized_response(res, 'Admin password not set in the environment variables') + return + } + if (!admin_password) { + unauthorized_response(res, 'Missing admin_password') + return + } + if (admin_password != process.env.ADMIN_PASSWORD) { + unauthorized_response(res, 'Invalid admin password') + return + } + if (!pubkey) { + invalid_request(res, 'Missing pubkey') + return + } + const { delete_error } = delete_account(app, pubkey) + if (delete_error) { + invalid_request(res, { error: delete_error }) + return + } + + json_response(res, { success: true }) + }) + + /** + * This route is used to force a specific UUID for a user account. + * + * This is useful when we accidentally nuke the db on staging, + * but we want to keep the user's account UUID the same as before because resetting Apple's Sandbox account is a pain. + */ + router.put('/admin/users/:pubkey/account-uuid', async (req, res) => { + const pubkey = req.params.pubkey + const body = req.body + const admin_password = body.admin_password + const account_uuid = body.account_uuid + if(!process.env.ADMIN_PASSWORD) { + unauthorized_response(res, 'Admin password not set in the environment variables') + return + } + if (!admin_password) { + unauthorized_response(res, 'Missing admin_password') + return + } + if (admin_password != process.env.ADMIN_PASSWORD) { + unauthorized_response(res, 'Invalid admin password') + return + } + if (!pubkey) { + invalid_request(res, 'Missing pubkey') + return + } + if (!account_uuid) { + invalid_request(res, 'Missing account_uuid') + return + } + app.dbs.pubkeys_to_user_uuids.put(pubkey, account_uuid.toUpperCase()) + + json_response(res, { success: true }) + }); + } } function get_allowed_cors_origins() { diff --git a/src/user_management.js b/src/user_management.js index df309e5..496ef1d 100644 --- a/src/user_management.js +++ b/src/user_management.js @@ -152,4 +152,17 @@ function get_user_uuid(api, pubkey) { return uuid } -module.exports = { check_account, create_account, get_account_info_payload, bump_expiry, get_account, put_account, get_account_and_user_id, get_user_id_from_pubkey, get_user_uuid, bumpy_set_expiry } +function delete_account(api, pubkey) { + const user_id = get_user_id_from_pubkey(api, pubkey); + if (!user_id) { + return { delete_error: 'User ID not found for the given pubkey' }; + } + + api.dbs.accounts.remove(user_id); + api.dbs.pubkeys_to_user_ids.remove(pubkey); + api.dbs.pubkeys_to_user_uuids.remove(pubkey); + + return { delete_error: null }; +} + +module.exports = { check_account, create_account, get_account_info_payload, bump_expiry, get_account, put_account, get_account_and_user_id, get_user_id_from_pubkey, get_user_uuid, bumpy_set_expiry, delete_account }