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.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddPaymentMethodIdToPayments1776400000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddPaymentMethodIdToPayments1776400000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// 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<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "payments" DROP CONSTRAINT "FK_payments_payment_method_id"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "payments" DROP COLUMN "payment_method_id"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
@openAuth="handleOpenAuth"
|
||||
@selectContent="handleSearchSelect"
|
||||
@openMobileSearch="showMobileSearch = true"
|
||||
@openKeys="showKeysModal = true"
|
||||
/>
|
||||
|
||||
<!-- Route Content -->
|
||||
@@ -39,6 +40,12 @@
|
||||
@close="handleAuthClose"
|
||||
@success="handleAuthSuccess"
|
||||
/>
|
||||
|
||||
<!-- Keys Modal (view nsec/npub for generated accounts) -->
|
||||
<KeysModal
|
||||
:isOpen="showKeysModal"
|
||||
@close="showKeysModal = false"
|
||||
/>
|
||||
|
||||
<!-- Toast Notifications -->
|
||||
<ToastContainer />
|
||||
@@ -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<string | null>(null)
|
||||
|
||||
|
||||
@@ -218,6 +218,13 @@
|
||||
</svg>
|
||||
<span>My Library</span>
|
||||
</button>
|
||||
<!-- My Keys (generated/imported private key accounts only) -->
|
||||
<button v-if="hasPrivateKey" @click="handleShowKeys" class="profile-menu-item flex items-center gap-3 px-4 py-2.5 w-full text-left">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<span>My Keys</span>
|
||||
</button>
|
||||
<!-- Backstage (filmmaker only) -->
|
||||
<button v-if="isFilmmakerUser" @click="navigateTo('/backstage')" class="profile-menu-item flex items-center gap-3 px-4 py-2.5 w-full text-left">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -314,6 +321,7 @@ interface Emits {
|
||||
(e: 'openAuth', redirect?: string): void
|
||||
(e: 'selectContent', content: Content): void
|
||||
(e: 'openMobileSearch'): void
|
||||
(e: 'openKeys'): void
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
@@ -332,6 +340,7 @@ const {
|
||||
activePubkey: _nostrActivePubkey,
|
||||
activeName: nostrActiveName,
|
||||
activeProfilePicture: nostrProfilePicture,
|
||||
hasPrivateKey,
|
||||
// Hidden in production for now (see template comment)
|
||||
testPersonas: _testPersonas,
|
||||
tastemakerPersonas: _tastemakerPersonas,
|
||||
@@ -562,6 +571,11 @@ function navigateTo(path: string) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function handleShowKeys() {
|
||||
dropdownOpen.value = false
|
||||
emit('openKeys')
|
||||
}
|
||||
|
||||
// async function handlePersonaLogin(persona: Persona) {
|
||||
// personaMenuOpen.value = false
|
||||
// await _loginWithPersona(persona)
|
||||
|
||||
251
src/components/KeysModal.vue
Normal file
251
src/components/KeysModal.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<Transition name="modal-fade">
|
||||
<div v-if="isOpen" class="fixed inset-0 z-[10001] flex items-center justify-center p-4" @click.self="$emit('close')">
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/70 backdrop-blur-sm"></div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="relative w-full max-w-md rounded-2xl border border-white/10 bg-[#0a0a0a]/95 backdrop-blur-xl shadow-2xl overflow-hidden">
|
||||
<!-- Close button -->
|
||||
<button @click="$emit('close')" class="absolute top-4 right-4 z-10 w-8 h-8 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 transition-colors text-white/60 hover:text-white">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="p-6 space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="text-center pr-8">
|
||||
<div class="w-12 h-12 mx-auto mb-3 rounded-full bg-[#F7931A]/15 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-[#F7931A]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-xl font-bold text-white">Your Nostr Keys</h2>
|
||||
<p class="text-white/50 text-sm mt-1">These keys are your sovereign identity</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="flex justify-center py-8">
|
||||
<div class="w-8 h-8 border-2 border-white/20 border-t-[#F7931A] rounded-full animate-spin"></div>
|
||||
</div>
|
||||
|
||||
<!-- Keys -->
|
||||
<template v-else-if="keys">
|
||||
<!-- Public Key (npub) -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-white/60 text-xs font-medium uppercase tracking-wider">Public Key (safe to share)</label>
|
||||
<div class="relative group">
|
||||
<div class="key-display break-all text-sm font-mono text-white/80 leading-relaxed">
|
||||
{{ keys.npub }}
|
||||
</div>
|
||||
<button
|
||||
@click="copyToClipboard(keys!.npub, 'npub')"
|
||||
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-lg bg-white/10 hover:bg-white/20 text-white/60 hover:text-white"
|
||||
:title="copiedField === 'npub' ? 'Copied!' : 'Copy'"
|
||||
>
|
||||
<svg v-if="copiedField !== 'npub'" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<svg v-else class="w-4 h-4 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secret Key (nsec) -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-red-400/80 text-xs font-medium uppercase tracking-wider">Secret Key (NEVER share this!)</label>
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="key-display key-display-secret break-all text-sm font-mono leading-relaxed cursor-pointer transition-all"
|
||||
:class="nsecRevealed ? 'text-red-300/80' : 'text-transparent select-none'"
|
||||
@click="nsecRevealed = !nsecRevealed"
|
||||
>
|
||||
<span :class="{ 'blur-sm': !nsecRevealed }">{{ keys.nsec }}</span>
|
||||
</div>
|
||||
<div v-if="!nsecRevealed" class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<span class="text-white/40 text-sm font-medium">Click to reveal</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="nsecRevealed"
|
||||
@click="copyToClipboard(keys!.nsec, 'nsec')"
|
||||
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-lg bg-white/10 hover:bg-white/20 text-white/60 hover:text-white"
|
||||
:title="copiedField === 'nsec' ? 'Copied!' : 'Copy'"
|
||||
>
|
||||
<svg v-if="copiedField !== 'nsec'" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<svg v-else class="w-4 h-4 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warning -->
|
||||
<div class="rounded-xl bg-red-500/10 border border-red-500/20 p-4">
|
||||
<div class="flex gap-3">
|
||||
<svg class="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
<p class="text-red-300/80 text-sm leading-relaxed">
|
||||
Your secret key is the <strong>only way</strong> to access this identity. If you lose it, nobody can recover it for you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download button -->
|
||||
<button @click="downloadKeys" class="w-full flex items-center justify-center gap-2 py-3 px-4 rounded-xl bg-white/10 hover:bg-white/15 border border-white/10 text-white font-medium text-sm transition-all hover:translate-y-[-1px]">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Download Keys
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- No keys available -->
|
||||
<div v-else class="text-center py-6">
|
||||
<p class="text-white/50 text-sm">Unable to retrieve keys for this account type.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useAccounts } from '../composables/useAccounts'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'close'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
|
||||
const { getAccountKeys } = useAccounts()
|
||||
|
||||
const keys = ref<{ nsec: string; npub: string; hexPub: string } | null>(null)
|
||||
const loading = ref(false)
|
||||
const nsecRevealed = ref(false)
|
||||
const copiedField = ref<string | null>(null)
|
||||
|
||||
// Fetch keys when modal opens
|
||||
watch(() => props.isOpen, async (open) => {
|
||||
if (open) {
|
||||
loading.value = true
|
||||
nsecRevealed.value = false
|
||||
copiedField.value = null
|
||||
try {
|
||||
keys.value = await getAccountKeys()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} else {
|
||||
// Reset on close for security
|
||||
keys.value = null
|
||||
nsecRevealed.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function copyToClipboard(text: string, field: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
copiedField.value = field
|
||||
setTimeout(() => { copiedField.value = null }, 2000)
|
||||
} catch {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
copiedField.value = field
|
||||
setTimeout(() => { copiedField.value = null }, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
function downloadKeys() {
|
||||
if (!keys.value) return
|
||||
|
||||
const { nsec, npub } = keys.value
|
||||
const content = [
|
||||
'═══════════════════════════════════════════════════════',
|
||||
' YOUR SOVEREIGN NOSTR IDENTITY — KEEP THIS SAFE!',
|
||||
'═══════════════════════════════════════════════════════',
|
||||
'',
|
||||
'── Public Key (safe to share) ────────────────────────',
|
||||
'npub: ' + npub,
|
||||
'',
|
||||
'── Secret Key (NEVER share this!) ────────────────────',
|
||||
'nsec: ' + nsec,
|
||||
'',
|
||||
'═══════════════════════════════════════════════════════',
|
||||
' WARNING: If you lose this file, your identity is',
|
||||
' gone forever. Nobody can recover it for you.',
|
||||
'═══════════════════════════════════════════════════════',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
const blob = new Blob([content], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `indeedhub-keys-${npub.slice(0, 12)}.txt`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.key-display {
|
||||
padding: 12px 40px 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.key-display-secret {
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
border-color: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
/* Modal transition */
|
||||
.modal-fade-enter-active {
|
||||
transition: opacity 0.25s ease-out;
|
||||
}
|
||||
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
|
||||
.modal-fade-enter-from,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-fade-enter-active .relative {
|
||||
animation: modalSlideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalSlideUp {
|
||||
from {
|
||||
transform: translateY(16px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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,
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
<span>My Library</span>
|
||||
</button>
|
||||
|
||||
<button v-if="hasPrivateKey" @click="showKeysModal = true" class="bar-tab">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<span>My Keys</span>
|
||||
</button>
|
||||
|
||||
<button @click="handleSourceToggle" class="bar-tab">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
@@ -153,6 +160,16 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button v-if="hasPrivateKey" @click="showKeysModal = true" class="profile-action-row">
|
||||
<svg class="w-5 h-5 text-[#F7931A]/70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<span class="flex-1 text-white font-medium">My Keys</span>
|
||||
<svg class="w-4 h-4 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button @click="handleSourceToggle" class="profile-action-row">
|
||||
<svg class="w-5 h-5 text-white/70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
@@ -172,6 +189,12 @@
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Keys Modal -->
|
||||
<KeysModal
|
||||
:isOpen="showKeysModal"
|
||||
@close="showKeysModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -182,12 +205,15 @@ import { useAuth } from '../composables/useAuth'
|
||||
import { useAccounts } from '../composables/useAccounts'
|
||||
import { useContentSourceStore } from '../stores/contentSource'
|
||||
import { useContentStore } from '../stores/content'
|
||||
import KeysModal from '../components/KeysModal.vue'
|
||||
import { subscriptionService } from '../services/subscription.service'
|
||||
import type { ApiSubscription } from '../types/api'
|
||||
|
||||
const router = useRouter()
|
||||
const { user, linkNostr, unlinkNostr, logout: authLogout } = useAuth()
|
||||
const { logout: nostrLogout } = useAccounts()
|
||||
const { logout: nostrLogout, hasPrivateKey } = useAccounts()
|
||||
|
||||
const showKeysModal = ref(false)
|
||||
const contentSourceStore = useContentSourceStore()
|
||||
const contentStore = useContentStore()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user