npub.email/src/components/LoginWithNostr.svelte

326 lines
No EOL
8.3 KiB
Svelte

<script lang="ts">
import { activeUser, ndk } from '$lib/stores.svelte';
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { NDKNip07Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { decode as decodeKey } from 'nostr-tools/nip19';
import { decrypt as decryptNsec, encrypt as encryptNsec } from 'nostr-tools/nip49';
import { goto } from '$app/navigation';
let ncryptsec = $state(browser ? localStorage.getItem('ncryptsec') || '' : '');
let nsec = $state('');
let nsecField = $state('');
let password = $state('');
let isLoggingIn = $state(false);
async function login() {
localStorage.setItem('useNip07', 'true');
$ndk.signer = new NDKNip07Signer();
await $ndk.connect(6000);
await $ndk.signer!.user();
activeUser.set($ndk.activeUser);
}
async function loginWithExistingNcryptSec() {
let decrypted;
try {
decrypted = decryptNsec(ncryptsec, password);
if (!decrypted) return alert('Invalid password');
} catch (e) {
return alert('Invalid password');
}
$ndk.signer = new NDKPrivateKeySigner(decrypted);
await $ndk.connect(6000);
activeUser.set(await $ndk.signer!.user());
activeUser.set($ndk.activeUser);
goto('/');
}
function loginWithNsec() {
if (password.length < 32) return alert('Password must be at least 32 characters long');
let nsecBytes = decodeKey(nsec);
if (nsecBytes.type !== 'nsec') return alert('Invalid nsec');
let encrypted = encryptNsec(nsecBytes.data, password);
localStorage.setItem('ncryptsec', encrypted);
$ndk.signer = new NDKPrivateKeySigner(nsec);
$ndk.connect(6000);
$ndk.signer!.user();
activeUser.set($ndk.activeUser);
goto('/');
}
function cancelLoginWithNsec() {
if (!ncryptsec)
nsec = '';
nsecField = '';
isLoggingIn = false;
}
function nsecOk() {
try {
let decoded = decodeKey(nsecField);
if (decoded.type !== 'nsec')
return alert('Invalid nsec');
nsec = nsecField;
} catch (e) {
return alert('Invalid nsec');
}
}
onMount(() => {
if ($ndk.activeUser)
activeUser.set($ndk.activeUser);
});
</script>
{#if isLoggingIn}
<div class="login-warning">
<h3>🚨 Critical Security Warning 🚨</h3>
<p>Your nsec is the master key to your ENTIRE Nostr identity. If someone gets it:</p>
<ul class="warning-list">
<li>They can read ALL your letters - past and future</li>
<li>They can post ANYTHING pretending to be you</li>
<li>They can take over your account PERMANENTLY</li>
<li>There is NO WAY to undo this - not even we can help you</li>
<li>Your account would be LOST FOREVER</li>
</ul>
<p class="warning-action">Only enter your nsec on devices you completely trust!</p>
<p class="warning-final">If you're not 100% sure about this, STOP and use a signing extension instead (or use
bunkers, coming soon).</p>
</div>
{#if ncryptsec || nsec}
<h3>Password Required</h3>
<p>
First time here? Create a strong password that is AT LEAST 32 characters long.<br>
A good approach is to use 4-5 random words with numbers and symbols between them.<br>
Example: correct-horse9battery!staple$running<br /><br />
<a
href="https://xkcd.com/936/"
target="_blank"
rel="noopener"
class="button"
>
(See this XKCD comic for why this works)
</a>
</p>
<p class="warning-action">
Your password is used to encrypt your nsec. Write it down somewhere safe, it cannot be recovered.
</p>
<input
bind:value={password}
type="password"
placeholder="Minimum 32 characters"
class="password-input"
/>
<div class="password-strength">
Length: {password?.length || 0}/32 characters
</div>
<div class="button-group">
<button onclick={cancelLoginWithNsec}>Go Back</button>
<button
onclick={ncryptsec ? loginWithExistingNcryptSec : loginWithNsec}
disabled={password?.length < 32}
>
Sign In
</button>
</div>
{:else}
<p>
Already have a Nostr account? Enter your nsec below.<br>
Need an account? Create one using any of these popular clients:
</p>
<ul class="client-list">
<li>
<a href="https://nosta.me" target="_blank" rel="noopener">
Nosta
<span class="client-desc">Simple nostr client focused on profile creation and viewing</span>
</a>
</li>
<li>
<a href="https://primal.net" target="_blank" rel="noopener">
Primal
<span class="client-desc">Feature-rich web and mobile nostr client with great UI</span>
</a>
</li>
<li>
<a href="https://damus.io" target="_blank" rel="noopener">
Damus
<span class="client-desc">Popular iOS nostr client</span>
</a>
</li>
<li>
<a href="https://amethyst.social" target="_blank" rel="noopener">
Amethyst
<span class="client-desc">Feature-packed Android nostr client</span>
</a>
</li>
<li>
<a href="https://iris.to" target="_blank" rel="noopener">
Iris
<span class="client-desc">Minimalist web nostr client</span>
</a>
</li>
</ul>
<input
bind:value={nsecField}
type="password"
placeholder="nsec1..."
class="nsec-input"
/>
<button onclick={nsecOk}>Continue</button>
{/if}
{:else}
<button onclick={login}>Login with Nostr Extension</button>
<button onclick={() => isLoggingIn = true}>Login with Nsec</button>
{/if}
<style>
button {
display: block;
align-self: center;
}
.login-warning {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
margin-block: var(--spacing-md);
background: var(--glass-bg);
padding: var(--spacing-md);
}
.client-list {
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-sm);
margin: var(--spacing-md) 0;
li {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: var(--spacing-sm);
transition: all 0.3s ease;
position: relative;
isolation: isolate;
&::before {
content: '';
position: absolute;
inset: -2px;
background: linear-gradient(45deg, var(--accent), transparent);
border-radius: 10px;
z-index: -1;
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 0 15px var(--glass-glow);
&::before {
opacity: 1;
}
}
a {
color: var(--text-primary);
text-decoration: none;
display: block;
font-weight: 500;
.client-desc {
display: block;
color: var(--text-secondary);
font-size: 0.8em;
margin-top: var(--spacing-xs);
opacity: 0.7;
}
&:hover {
text-decoration: underline;
}
}
}
}
.warning-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
margin: var(--spacing-md) 0;
li {
backdrop-filter: blur(var(--blur-strength));
background: var(--glass-bg-dark);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: var(--spacing-sm);
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
box-shadow: var(--depth-shadow);
transition: transform 0.3s ease;
&::before {
content: "⚠️";
font-size: 1.2em;
}
&:hover {
transform: translateX(var(--spacing-xs));
background: var(--glass-bg-darker);
}
}
}
.warning-action {
backdrop-filter: blur(var(--blur-strength));
background: var(--chip-error);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: var(--spacing-md);
margin: var(--spacing-md) 0;
text-align: center;
font-weight: 600;
color: var(--text-primary);
box-shadow: var(--depth-shadow),
0 0 15px var(--glass-glow);
&:hover {
background: color-mix(in srgb, var(--chip-error) 80%, black);
}
}
.warning-final {
position: relative;
text-align: center;
padding: var(--spacing-md) 0;
margin-top: var(--spacing-md);
font-weight: 600;
color: var(--text-primary);
&::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 2px;
background: linear-gradient(
90deg,
transparent,
var(--chip-error),
transparent
);
}
}
</style>