From 2a168024046d0d26c221823611cfe1be4c8e6404 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sat, 14 Feb 2026 12:18:48 +0000 Subject: [PATCH] feat: add KeysModal for managing private key accounts - Introduced a new KeysModal component to display and manage nsec/npub for accounts with local private keys. - Updated AppHeader and Profile views to include a "My Keys" button, conditionally rendered based on the presence of a private key. - Enhanced the useAccounts composable to determine if the active account holds a local private key, enabling key management functionality. These changes improve user access to their private key information and enhance the overall account management experience. --- ...00000-add-payment-method-id-to-payments.ts | 28 ++ src/App.vue | 9 + src/components/AppHeader.vue | 14 + src/components/KeysModal.vue | 251 ++++++++++++++++++ src/composables/useAccounts.ts | 46 ++++ src/views/Profile.vue | 28 +- 6 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 backend/src/database/migrations/1776400000000-add-payment-method-id-to-payments.ts create mode 100644 src/components/KeysModal.vue diff --git a/backend/src/database/migrations/1776400000000-add-payment-method-id-to-payments.ts b/backend/src/database/migrations/1776400000000-add-payment-method-id-to-payments.ts new file mode 100644 index 0000000..1f67ea7 --- /dev/null +++ b/backend/src/database/migrations/1776400000000-add-payment-method-id-to-payments.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPaymentMethodIdToPayments1776400000000 + implements MigrationInterface +{ + name = 'AddPaymentMethodIdToPayments1776400000000'; + + public async up(queryRunner: QueryRunner): Promise { + // Add the missing payment_method_id column + await queryRunner.query( + `ALTER TABLE "payments" ADD "payment_method_id" character varying`, + ); + + // Add foreign key constraint to payment_methods + await queryRunner.query( + `ALTER TABLE "payments" ADD CONSTRAINT "FK_payments_payment_method_id" FOREIGN KEY ("payment_method_id") REFERENCES "payment_methods"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "payments" DROP CONSTRAINT "FK_payments_payment_method_id"`, + ); + await queryRunner.query( + `ALTER TABLE "payments" DROP COLUMN "payment_method_id"`, + ); + } +} diff --git a/src/App.vue b/src/App.vue index 28ad4be..6f67ad6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,6 +10,7 @@ @openAuth="handleOpenAuth" @selectContent="handleSearchSelect" @openMobileSearch="showMobileSearch = true" + @openKeys="showKeysModal = true" /> @@ -39,6 +40,12 @@ @close="handleAuthClose" @success="handleAuthSuccess" /> + + + @@ -54,6 +61,7 @@ import { useSearchSelectionStore } from './stores/searchSelection' import AppHeader from './components/AppHeader.vue' import BackstageHeader from './components/BackstageHeader.vue' import AuthModal from './components/AuthModal.vue' +import KeysModal from './components/KeysModal.vue' import BackstageMobileNav from './components/BackstageMobileNav.vue' import MobileNav from './components/MobileNav.vue' import MobileSearch from './components/MobileSearch.vue' @@ -76,6 +84,7 @@ const isBackstageRoute = computed(() => ) const showAuthModal = ref(false) +const showKeysModal = ref(false) const showMobileSearch = ref(false) const pendingRedirect = ref(null) diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index b6214df..87c7621 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -218,6 +218,13 @@ My Library + + + +
+ +
+
+ + + +
+

Your Nostr Keys

+

These keys are your sovereign identity

+
+ + +
+
+
+ + + + + +
+

Unable to retrieve keys for this account type.

+
+
+ + + + + + + + diff --git a/src/composables/useAccounts.ts b/src/composables/useAccounts.ts index a19fab6..542aacd 100644 --- a/src/composables/useAccounts.ts +++ b/src/composables/useAccounts.ts @@ -273,6 +273,48 @@ export function useAccounts() { subscriptions.forEach((sub) => sub.unsubscribe()) }) + /** + * Whether the active account holds a local private key + * (generated sovereign identity or imported nsec). + * When true, the user can view/export their keys. + */ + const hasPrivateKey = computed(() => { + const acct = activeAccount.value + if (!acct) return false + // PrivateKeyAccount has type 'local' and stores the secret key + // in its signer. Check the account type name set by applesauce. + const typeName = acct.constructor?.name ?? acct.type ?? '' + return typeName === 'PrivateKeyAccount' || typeName === 'local' + }) + + /** + * Retrieve the active account's nsec and npub. + * Only works for private-key-based accounts (generated or imported nsec). + * Returns null if the active account doesn't hold a local secret key. + */ + async function getAccountKeys(): Promise<{ nsec: string; npub: string; hexPub: string } | null> { + const acct = activeAccount.value + if (!acct?.pubkey) return null + + try { + const nip19 = await import('nostr-tools/nip19') + const npub = nip19.npubEncode(acct.pubkey) + + // The PrivateKeyAccount stores the secret key in its signer. + // Access it through the account's serialisation (toJSON includes secrets). + const signer = acct.signer ?? acct._signer + const secretKey: Uint8Array | undefined = signer?.key ?? signer?.secretKey ?? signer?._key + + if (!secretKey) return null + + const nsec = nip19.nsecEncode(secretKey) + return { nsec, npub, hexPub: acct.pubkey } + } catch (err) { + console.warn('[useAccounts] Failed to extract keys:', err) + return null + } + } + return { // State activeAccount, @@ -285,6 +327,10 @@ export function useAccounts() { activeProfile, activeProfilePicture, + // Key management + hasPrivateKey, + getAccountKeys, + // Login methods loginWithExtension, loginWithPersona, diff --git a/src/views/Profile.vue b/src/views/Profile.vue index 092bc07..c145b84 100644 --- a/src/views/Profile.vue +++ b/src/views/Profile.vue @@ -14,6 +14,13 @@ My Library + + + +