326 lines
No EOL
8.3 KiB
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> |