feat: implement nsec login functionality in AuthModal
- Added a new nsec login option that allows users to sign in using their private key. - Introduced a toggle to reveal the nsec input field, enhancing user experience. - Implemented validation and error handling for nsec submissions, ensuring robust login flow. - Updated styles and layout for better visual consistency and usability. These changes enhance the authentication process by providing an additional secure login method for users.
This commit is contained in:
@@ -49,26 +49,16 @@
|
|||||||
<div v-if="mode === 'login'" class="text-right">
|
<div v-if="mode === 'login'" class="text-right">
|
||||||
<span class="text-sm text-white/60">Forgot password?</span>
|
<span class="text-sm text-white/60">Forgot password?</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="hero-play-button w-full flex items-center justify-center">
|
<button type="submit" class="hero-play-button w-full flex items-center justify-center mb-4">
|
||||||
<span>{{ mode === 'register' ? 'Create Account' : 'Sign In' }}</span>
|
<span>{{ mode === 'register' ? 'Create Account' : 'Sign In' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<div class="relative my-6">
|
|
||||||
<div class="absolute inset-0 flex items-center">
|
|
||||||
<div class="w-full border-t border-white/10"></div>
|
|
||||||
</div>
|
|
||||||
<div class="relative flex justify-center text-sm">
|
|
||||||
<span class="px-4 bg-transparent text-white/40">or</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nostr Login Button (NIP-07 Browser Extension) -->
|
<!-- Nostr Login Button (NIP-07 Browser Extension) -->
|
||||||
<button
|
<button
|
||||||
@click="handleNostrLogin"
|
@click="handleNostrLogin"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
class="nostr-login-button w-full flex items-center justify-center gap-2"
|
class="nostr-login-button w-full flex items-center justify-center gap-2 mt-4"
|
||||||
>
|
>
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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" />
|
||||||
@@ -76,17 +66,50 @@
|
|||||||
Sign in with Nostr Extension
|
Sign in with Nostr Extension
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Divider between Nostr methods -->
|
<!-- nsec login (hidden by default; tap to reveal field) -->
|
||||||
<div class="relative my-4">
|
<template v-if="!showNsecField">
|
||||||
<div class="absolute inset-0 flex items-center">
|
<button
|
||||||
<div class="w-full border-t border-white/10"></div>
|
type="button"
|
||||||
</div>
|
@click="showNsecField = true"
|
||||||
<div class="relative flex justify-center text-sm">
|
:disabled="isLoading"
|
||||||
<span class="px-4 bg-transparent text-white/30 text-xs">or</span>
|
class="nostr-login-button w-full flex items-center justify-center gap-2 mt-6"
|
||||||
|
>
|
||||||
|
<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="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||||
|
</svg>
|
||||||
|
Sign in with nsec (private key)
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<div v-else class="nsec-field-block mt-6">
|
||||||
|
<input
|
||||||
|
v-model="nsecInput"
|
||||||
|
type="password"
|
||||||
|
placeholder="nsec1..."
|
||||||
|
class="auth-input w-full"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<div class="nsec-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleNsecSubmit"
|
||||||
|
:disabled="!nsecInput.trim() || isLoading"
|
||||||
|
class="nostr-login-button flex-1 flex items-center justify-center gap-2"
|
||||||
|
:class="{ 'opacity-40 cursor-not-allowed': !nsecInput.trim() || isLoading }"
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="cancelNsecField"
|
||||||
|
class="nsec-cancel-btn"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Amber Login (NIP-55 Android Signer) -->
|
<!-- Amber Login (hidden for now — re-enable by changing v-if to amberPhase === 'idle') -->
|
||||||
|
<template v-if="false">
|
||||||
<button
|
<button
|
||||||
v-if="amberPhase === 'idle'"
|
v-if="amberPhase === 'idle'"
|
||||||
@click="handleAmberOpen"
|
@click="handleAmberOpen"
|
||||||
@@ -99,8 +122,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Sign in with Amber
|
Sign in with Amber
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Waiting for pubkey from Amber (auto-detects on return) -->
|
|
||||||
<div v-else-if="amberPhase === 'waiting-pubkey'" class="mt-3 space-y-3">
|
<div v-else-if="amberPhase === 'waiting-pubkey'" class="mt-3 space-y-3">
|
||||||
<div class="flex items-center justify-center gap-2 text-sm text-[#F7931A]">
|
<div class="flex items-center justify-center gap-2 text-sm text-[#F7931A]">
|
||||||
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
@@ -180,7 +201,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Completing sign-in -->
|
|
||||||
<div v-else-if="amberPhase === 'completing'" class="mt-3">
|
<div v-else-if="amberPhase === 'completing'" class="mt-3">
|
||||||
<div class="flex items-center justify-center gap-2 text-sm text-[#F7931A]">
|
<div class="flex items-center justify-center gap-2 text-sm text-[#F7931A]">
|
||||||
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
@@ -190,6 +210,7 @@
|
|||||||
<span>Completing sign-in...</span>
|
<span>Completing sign-in...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Toggle Mode -->
|
<!-- Toggle Mode -->
|
||||||
<div class="mt-6 text-center text-sm text-white/60">
|
<div class="mt-6 text-center text-sm text-white/60">
|
||||||
@@ -291,13 +312,17 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
const { loginWithNostr, isLoading: authLoading } = useAuth()
|
const { loginWithNostr, isLoading: authLoading } = useAuth()
|
||||||
const { loginWithExtension } = useAccounts()
|
const { loginWithExtension, loginWithPrivateKey } = useAccounts()
|
||||||
|
|
||||||
const mode = ref<'login' | 'register' | 'forgot'>(props.defaultMode)
|
const mode = ref<'login' | 'register' | 'forgot'>(props.defaultMode)
|
||||||
const errorMessage = ref<string | null>(null)
|
const errorMessage = ref<string | null>(null)
|
||||||
const isLoading = computed(() => authLoading.value || sovereignGenerating.value)
|
const isLoading = computed(() => authLoading.value || sovereignGenerating.value)
|
||||||
|
|
||||||
// Amber login state
|
// nsec login (tap to reveal field)
|
||||||
|
const showNsecField = ref(false)
|
||||||
|
const nsecInput = ref('')
|
||||||
|
|
||||||
|
// Amber login state (hidden for now)
|
||||||
const amberPhase = ref<'idle' | 'waiting-pubkey' | 'waiting-signature' | 'completing'>('idle')
|
const amberPhase = ref<'idle' | 'waiting-pubkey' | 'waiting-signature' | 'completing'>('idle')
|
||||||
const amberPubkey = ref<string | null>(null)
|
const amberPubkey = ref<string | null>(null)
|
||||||
const amberUnsignedEvent = ref<any>(null)
|
const amberUnsignedEvent = ref<any>(null)
|
||||||
@@ -322,6 +347,8 @@ watch(() => props.isOpen, (open) => {
|
|||||||
sovereignDismissed.value = false
|
sovereignDismissed.value = false
|
||||||
generatedKeys.value = null
|
generatedKeys.value = null
|
||||||
errorMessage.value = null
|
errorMessage.value = null
|
||||||
|
showNsecField.value = false
|
||||||
|
nsecInput.value = ''
|
||||||
cancelAmber()
|
cancelAmber()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -471,6 +498,33 @@ async function handleNostrLogin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancelNsecField() {
|
||||||
|
showNsecField.value = false
|
||||||
|
nsecInput.value = ''
|
||||||
|
errorMessage.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign in with pasted nsec. Adds account, sets active, then creates backend session.
|
||||||
|
*/
|
||||||
|
async function handleNsecSubmit() {
|
||||||
|
const nsec = nsecInput.value.trim()
|
||||||
|
if (!nsec) return
|
||||||
|
errorMessage.value = null
|
||||||
|
try {
|
||||||
|
await loginWithPrivateKey(nsec)
|
||||||
|
const account = accountManager.active
|
||||||
|
if (!account) throw new Error('Account not set')
|
||||||
|
await loginWithNostr(account.pubkey, 'nsec', {}, undefined)
|
||||||
|
nsecInput.value = ''
|
||||||
|
showNsecField.value = false
|
||||||
|
emit('success')
|
||||||
|
closeModal()
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage.value = err.message || 'Invalid nsec or sign-in failed.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up Amber flow state, timers, and event listeners.
|
* Clean up Amber flow state, timers, and event listeners.
|
||||||
*/
|
*/
|
||||||
@@ -809,6 +863,42 @@ declare global {
|
|||||||
color: rgba(255, 255, 255, 0.3);
|
color: rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* nsec login field block */
|
||||||
|
.nsec-field-block {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.nsec-hint {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.nsec-field-block .auth-input {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.nsec-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.nsec-cancel-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
.nsec-cancel-btn:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
/* Modal Transitions */
|
/* Modal Transitions */
|
||||||
.modal-fade-enter-active,
|
.modal-fade-enter-active,
|
||||||
.modal-fade-leave-active {
|
.modal-fade-leave-active {
|
||||||
|
|||||||
Reference in New Issue
Block a user