Add keypair rotation detection and migration feature
When a user rotates their Nostr keypair in account settings, the
merchant still holds the old key. This adds:
- key_mismatch flag on MerchantConfig (runtime, not persisted) -
detected on each GET /api/v1/merchant by comparing account vs
merchant pubkey
- POST /api/v1/merchant/{id}/migrate-keys endpoint that updates
the merchant keys, republishes all stalls/products under the new
identity, and resubscribes
- Warning banner in the UI with a "Migrate Keys" button and
confirmation dialog
- update_merchant_keys() crud function
Orders and DM history are preserved since they reference customer
pubkeys. Old stall/product events on relays become orphaned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5c38947fc6
commit
25023df8bd
5 changed files with 142 additions and 0 deletions
76
views_api.py
76
views_api.py
|
|
@ -39,6 +39,7 @@ from .crud import (
|
|||
get_last_direct_messages_time,
|
||||
get_merchant_by_pubkey,
|
||||
get_merchant_for_user,
|
||||
update_merchant_keys,
|
||||
get_order,
|
||||
get_order_by_event_id,
|
||||
get_orders,
|
||||
|
|
@ -184,6 +185,11 @@ async def api_get_merchant(
|
|||
assert merchant.time
|
||||
merchant.config.restore_in_progress = (merchant.time - last_dm_time) < 30
|
||||
|
||||
# Detect keypair rotation: account key no longer matches merchant key
|
||||
account = await get_account(wallet.wallet.user)
|
||||
if account and account.pubkey and account.pubkey != merchant.public_key:
|
||||
merchant.config.key_mismatch = True
|
||||
|
||||
return merchant
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
|
@ -229,6 +235,76 @@ async def api_delete_merchant(
|
|||
await subscribe_to_all_merchants()
|
||||
|
||||
|
||||
@nostrmarket_ext.post("/api/v1/merchant/{merchant_id}/migrate-keys")
|
||||
async def api_migrate_merchant_keys(
|
||||
merchant_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Merchant:
|
||||
"""
|
||||
Migrate a merchant to the current account keypair.
|
||||
|
||||
When a user rotates their Nostr keypair, the merchant still holds the old
|
||||
key. This endpoint updates the merchant's keys to match the account,
|
||||
then republishes all stalls and products under the new identity.
|
||||
|
||||
Orders and DM history are preserved (they reference customer pubkeys,
|
||||
not the merchant key). Old stall/product events on relays become
|
||||
orphaned — clients following the new pubkey will see the fresh events.
|
||||
"""
|
||||
try:
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Merchant cannot be found"
|
||||
assert merchant.id == merchant_id, "Wrong merchant ID"
|
||||
|
||||
account = await get_account(wallet.wallet.user)
|
||||
assert account and account.pubkey and account.prvkey, (
|
||||
"Account has no Nostr keypair"
|
||||
)
|
||||
|
||||
if account.pubkey == merchant.public_key:
|
||||
return merchant # already in sync
|
||||
|
||||
# Check no other merchant is using the new pubkey
|
||||
existing = await get_merchant_by_pubkey(account.pubkey)
|
||||
assert existing is None, (
|
||||
"Another merchant already uses this public key"
|
||||
)
|
||||
|
||||
old_pubkey = merchant.public_key
|
||||
|
||||
# Update merchant keys in DB
|
||||
merchant = await update_merchant_keys(
|
||||
wallet.wallet.user, merchant.id,
|
||||
account.prvkey, account.pubkey,
|
||||
)
|
||||
assert merchant
|
||||
|
||||
# Republish all stalls and products under the new key
|
||||
merchant = await update_merchant_to_nostr(merchant)
|
||||
|
||||
logger.info(
|
||||
f"[NOSTRMARKET] Migrated merchant {merchant.id} "
|
||||
f"from {old_pubkey[:16]}... to {account.pubkey[:16]}..."
|
||||
)
|
||||
|
||||
# Resubscribe with new pubkey
|
||||
await resubscribe_to_all_merchants()
|
||||
|
||||
return merchant
|
||||
|
||||
except AssertionError as ex:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot migrate merchant keys",
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrmarket_ext.patch("/api/v1/merchant/{merchant_id}")
|
||||
async def api_update_merchant(
|
||||
merchant_id: str,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue