Skip to content

Commit

Permalink
debug: add debug endpoints and IAP debug logs for easier testing
Browse files Browse the repository at this point in the history
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 <[email protected]>
Reviewed-by: William Casarin <[email protected]>
Link: [email protected]
Signed-off-by: William Casarin <[email protected]>
  • Loading branch information
danieldaquino authored and jb55 committed Feb 26, 2024
1 parent 47475fc commit 57863ec
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 4 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 -- <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://<HOST_AND_PORT>/admin/users/<PUBKEY_HEX_FORMAT>/account-uuid \
-H "Content-Type: application/json" \
-d '{"admin_password": "<ADMIN_PASSWORD_SET_ON_THE_RESPECTIVE_ENV_VARIABLE>", "account_uuid": "<UUID_FOUND_ON_IAP_TRANSACTION>"}'
```


16 changes: 15 additions & 1 deletion src/app_store_receipt_verifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -17,8 +18,10 @@ const fs = require('fs')
* @returns {Promise<number|null>} 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;
}

Expand All @@ -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) {
Expand All @@ -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
}

Expand All @@ -73,7 +82,12 @@ async function fetchLastVerifiedExpiryDate(client, transactionId, rootCaDir, env
* @returns {Array<JWSTransactionDecodedPayload>} 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();
Expand Down
74 changes: 72 additions & 2 deletions src/router_config.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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() {
Expand Down
15 changes: 14 additions & 1 deletion src/user_management.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

0 comments on commit 57863ec

Please sign in to comment.