aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2025-01-09 23:44:40 -0800
committerAdam Soltys <[email protected]>2025-01-09 23:44:40 -0800
commit8588de25f16486b56ef0ad533013bd8cb48ff120 (patch)
tree4c1e39704e2edb332c5fe8c58fb0d81c8154af2b
parent2358d1bf39c34a508db6c50f0f6312ee411a8a17 (diff)
downloadcoinos-ui-8588de25f16486b56ef0ad533013bd8cb48ff120.tar.gz
coinos-ui-8588de25f16486b56ef0ad533013bd8cb48ff120.zip
nostr signing
-rw-r--r--src/components/AppHeader.svelte2
-rw-r--r--src/components/Nostr.svelte310
-rw-r--r--src/lib/nostr.ts95
-rw-r--r--src/lib/settings.ts14
-rw-r--r--src/lib/store.ts13
-rw-r--r--src/lib/utils.ts20
-rw-r--r--src/locales/en.json1
-rw-r--r--src/routes/(app)/+layout.svelte6
-rw-r--r--src/routes/(app)/[username]/(profile)/+layout.svelte22
-rw-r--r--src/routes/(app)/messages/[recipient]/+page.svelte2
-rw-r--r--src/routes/(app)/settings/+layout.svelte28
-rw-r--r--src/routes/(app)/settings/nostr/+page.server.ts6
-rw-r--r--src/routes/(app)/settings/nostr/+page.svelte114
-rw-r--r--src/routes/(static)/+layout.svelte2
-rw-r--r--src/routes/(static)/login/+page.server.ts8
-rw-r--r--src/routes/(static)/login/+page.svelte76
-rw-r--r--src/routes/(static)/register/+page.server.ts4
-rw-r--r--src/routes/(static)/register/+page.svelte46
18 files changed, 422 insertions, 347 deletions
diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte
index a667085b..7c131ec4 100644
--- a/src/components/AppHeader.svelte
+++ b/src/components/AppHeader.svelte
@@ -61,7 +61,7 @@
{#if !$loading}
<header
- class="bg-gradient-to-r bg-primary bg-accent h-[175px] w-full relative bg-cover mb-20"
+ class="bg-gradient-to-r bg-primary bg-accent h-[175px] w-full relative bg-cover mb-20 !z-30"
style:background-image={bg}
>
<nav class="flex justify-end items-center space-x-4 p-5">
diff --git a/src/components/Nostr.svelte b/src/components/Nostr.svelte
index 0765f440..a4f20d23 100644
--- a/src/components/Nostr.svelte
+++ b/src/components/Nostr.svelte
@@ -1,264 +1,212 @@
<script lang="ts">
- import { applyAction, deserialize } from "$app/forms";
import { onMount } from "svelte";
import { browser } from "$app/environment";
import { t } from "$lib/translations";
import PasswordInput from "$comp/PasswordInput.svelte";
import { Relay } from "nostr-tools/relay";
- import { encrypt, decrypt, getConversationKey } from "nostr-tools/nip44";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import {
nip04,
nip19,
getEventHash,
- finalizeEvent,
generateSecretKey,
getPublicKey,
} from "nostr-tools";
import { AmberClipboardSigner } from "applesauce-signer";
import { copy, focus, fail, post } from "$lib/utils";
- import { password as pw, passwordPrompt } from "$lib/store";
- // import { PUBLIC_COINOS_RELAY as relayUrl } from "$env/static/public";
- const relayUrl = "wss://relay.nsec.app";
+ import { eventToSign, signer } from "$lib/store";
+ import { nostrConnectRelay } from "$lib/nostr";
+ import Success from "$comp/Success.svelte";
- let { id, redirect, token, nostrSignin = $bindable() } = $props();
- let extension = $state();
-
- onMount(() => {
- if (browser && window.nostr) extension = true;
- });
+ let { id } = $props();
+ let extensionAvailable = $derived(browser && window.nostr);
let cancel = () => {
- nostrSignin = false;
+ eventToSign.set();
showNsec = false;
pubkey = undefined;
connectUrl = undefined;
};
- let sk = $state();
- let ck = $state();
- let pk = $state();
- let pubkey = $state();
-
- let signer = $derived(
+ let signerAvailable = $derived(
browser &&
navigator.userAgent.includes("Android") &&
navigator.clipboard &&
navigator.clipboard.readText,
);
+ let hadSigner = $state(!!$signer);
+
let connectUrl = $state();
let nostrConnect = async () => {
- let secret = crypto.randomUUID();
-
- sk = generateSecretKey();
- pubkey = getPublicKey(sk);
+ let connectionSecret = crypto.randomUUID();
- connectUrl = `nostrconnect://${pubkey}?relay=${encodeURIComponent(relayUrl)}&perms=sign_event%3A1&name=Coinos&secret=${secret}`;
+ let sk = generateSecretKey();
+ let pubkey = getPublicKey(sk);
- let relay = await Relay.connect(relayUrl);
+ connectUrl = `nostrconnect://${pubkey}?relay=${encodeURIComponent(nostrConnectRelay)}&perms=sign_event%3A1&name=Coinos&secret=${connectionSecret}`;
- let signatureRequested;
- const sub = relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], {
+ let relay = await Relay.connect(nostrConnectRelay);
+ relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], {
async onevent(event) {
- pk = event.pubkey;
- ck = getConversationKey(sk, pk);
- let decrypted;
- console.log("EVENT", event);
+ let pk = event.pubkey;
try {
- decrypted = JSON.parse(decrypt(event.content, ck));
- } catch (e) {
- try {
- decrypted = JSON.parse(await nip04.decrypt(sk, pk, event.content));
- } catch (e) {
- console.log("ERROR", e);
- }
- }
-
- if (decrypted?.result?.includes("sig")) {
- await nostrLogin(JSON.parse(decrypted.result));
- } else if (!signatureRequested) {
- signatureRequested = true;
- let signEvent = {
- id: crypto.randomUUID(),
- method: "sign_event",
- params: [JSON.stringify({ ...ev, pubkey })],
- };
-
- // let content = encrypt(JSON.stringify(signEvent), ck);
- let content = await nip04.encrypt(sk, pk, JSON.stringify(signEvent));
- console.log("CONTENT", content);
- let signedSignEvent = finalizeEvent(
- {
- created_at: Math.round(Date.now() / 1000),
- kind: 24133,
- pubkey,
- content,
- tags: [["p", pk]],
- },
- sk,
+ let { result } = JSON.parse(
+ await nip04.decrypt(sk, pk, event.content),
);
-
- await new Promise((r) => setTimeout(r, 1000));
- console.log("PUBLISHING", signedSignEvent);
- relay.publish(signedSignEvent);
+ if (result === connectionSecret) {
+ $signer = {
+ method: "connect",
+ params: {
+ sk: bytesToHex(sk),
+ pk,
+ pubkey,
+ },
+ ready: true,
+ };
+ }
+ } catch (e) {
+ console.log("failed to parse nostr connect event", e);
}
},
});
};
- let ev = $derived({
- kind: 1,
- created_at: Date.now(),
- content: id,
- tags: [],
- });
-
let nsec = $state();
let showNsec = $state();
- let toggleNsec = () => (showNsec = !showNsec);
+ let toggleNsec = () => {
+ $signer = {
+ method: "nsec",
+ };
+ };
+
let nsecSign = async () => {
let sk;
if (nsec.startsWith("nsec")) sk = nip19.decode(nsec).data as Uint8Array;
else sk = hexToBytes(nsec);
- nostrLogin(finalizeEvent(ev, sk));
+ $signer = {
+ method: "nsec",
+ params: { sk: bytesToHex(sk), pk: getPublicKey(sk) },
+ ready: true,
+ };
};
let extensionSign = async () => {
- nostrLogin(await window.nostr.signEvent(ev));
+ $signer = { method: "extension", ready: true };
};
let signerGetPubkey = async () => {
- const signer = new AmberClipboardSigner();
- pubkey = await signer.getPublicKey();
- ev.pubkey = pubkey;
- console.log("PUBKEY", ev.pubkey);
+ const pubkey = await new AmberClipboardSigner().getPublicKey();
+ $signer = {
+ method: "signer",
+ params: { pubkey },
+ };
};
- let signerSigning;
+
let signerSign = async () => {
- signerSigning = true;
document.addEventListener("focus", signerSigned, true);
};
const signerSigned = async () => {
- document.removeEventListener("focus", signerSigned, true);
- await new Promise((resolve) => setTimeout(resolve, 500));
- ev.sig = await navigator.clipboard.readText();
- ev.id = getEventHash(ev);
- nostrLogin(ev);
+ await new Promise((r) => setTimeout(r, 100));
+ let sig = await navigator.clipboard.readText();
+ $signer.params.sig = sig;
+ $signer.ready = true;
+ $signer = $signer;
};
- let nostrLogin = async (signedEvent) => {
- // let pubkey = await window.nostr.getPublicKey();
- const formData = new FormData();
-
- try {
- formData.append("loginRedirect", redirect);
- formData.append("token", token);
- formData.append("event", JSON.stringify(signedEvent));
- formData.append("id", id);
-
- let response = await fetch("/login?/nostr", {
- method: "POST",
- body: formData,
- });
-
- const result = deserialize(await response.text());
-
- if (result.type === "success") {
- await invalidateAll();
- }
-
- applyAction(result);
- } catch (e) {
- fail(e.message);
- }
- };
+ let signUrl = $derived(
+ `nostrsigner:${encodeURIComponent(JSON.stringify($eventToSign))}?compressionType=none&returnType=signature&type=sign_event&appName=Coinos`,
+ );
</script>
-hi
-{nostrSignin}
-{#if nostrSignin}
+{#if $eventToSign && !$signer?.ready}
<div
class="fixed inset-0 bg-base-100 bg-opacity-90 overflow-y-auto h-full w-full z-50"
>
<div
class="relative mx-auto mt-10 p-5 border w-96 shadow-lg rounded-md bg-base-100 space-y-5"
>
- <h1 class="text-center text-2xl font-semibold">
- {$t("login.nostr")}
- </h1>
+ <h1 class="text-center text-2xl font-semibold">Nostr sign</h1>
- {#if showNsec}
- <input bind:value={nsec} placeholder="nsec..." />
- <button type="button" class="btn" onclick={nsecSign}>
- <iconify-icon icon="ph:key-bold" width="32"></iconify-icon>
- <div class="my-auto">Login</div>
- </button>
- {:else if connectUrl}
- <a href={connectUrl}>
- <img
- src={`/qr/${encodeURIComponent(connectUrl)}/raw`}
- class="z-10 border-4 border-white"
- alt="QR"
- />
- <div class="break-all">
- {connectUrl}
- </div>
- </a>
- <button class="btn" onclick={() => copy(connectUrl)}>Copy</button>
- {:else if signerSigning && pubkey}
- <a
- href={`nostrsigner:${encodeURIComponent(JSON.stringify(ev))}?compressionType=none&returnType=signature&type=sign_event&appName=Coinos`}
- type="button"
- class="btn"
- onclick={signerSign}
- >
- <iconify-icon
- icon="material-symbols:diamond-rounded"
- width="32"
- class="text-orange-400"
- ></iconify-icon>
- <div class="my-auto">NIP-55 Signer Step 2</div>
- </a>
+ {#if $eventToSign.sig && !hadSigner}
+ <Success />
{:else}
- {#if extension}
- <button type="button" class="btn" onclick={extensionSign}>
- <iconify-icon
- icon="lucide-lab:bee"
- width="32"
- class="text-yellow-400"
- ></iconify-icon>
- <div class="my-auto">NIP-10 Extension</div>
- </button>
- {/if}
- <button type="button" class="btn" onclick={nostrConnect}>
- <img src="/images/nostr.png" class="w-8" />
- <div class="my-auto">NIP-46 Connect</div>
- </button>
- {#if signer}
- <a type="button" class="btn" onclick={signerGetPubkey}>
- <iconify-icon
- icon="material-symbols:diamond-rounded"
- width="32"
- class="text-orange-400"
- ></iconify-icon>
- <div class="my-auto">NIP-55 Signer</div>
+ {#if connectUrl}
+ <a href={connectUrl}>
+ <img
+ src={`/qr/${encodeURIComponent(connectUrl)}/raw`}
+ class="z-10 border-4 border-white"
+ alt="QR"
+ />
+ <div class="break-all">
+ {connectUrl}
+ </div>
</a>
+ <button class="btn" onclick={() => copy(connectUrl)}>Copy</button>
+ {:else}
+ {#if $signer?.method === "nsec"}
+ <input bind:value={nsec} placeholder="nsec..." />
+ <button type="button" class="btn btn-accent" onclick={nsecSign}>
+ <iconify-icon icon="ph:paper-plane-right-bold" width="32"
+ ></iconify-icon>
+ <div class="my-auto">Submit</div>
+ </button>
+ {/if}
+
+ {#if $signer?.method === "signer"}
+ <a href={signUrl} type="button" class="btn" onclick={signerSign}>
+ <iconify-icon
+ icon="material-symbols:diamond-rounded"
+ width="32"
+ class="text-orange-400"
+ ></iconify-icon>
+ <div class="my-auto">NIP-55 Signer</div>
+ </a>
+ {/if}
+
+ {#if !$signer}
+ {#if extensionAvailable}
+ <button type="button" class="btn" onclick={extensionSign}>
+ <iconify-icon
+ icon="lucide-lab:bee"
+ width="32"
+ class="text-yellow-400"
+ ></iconify-icon>
+ <div class="my-auto">Browser extension</div>
+ </button>
+ {/if}
+
+ <button type="button" class="btn" onclick={nostrConnect}>
+ <img src="/images/nostr.png" class="w-8" alt="Nostr" />
+ <div class="my-auto">Nostr connect</div>
+ </button>
+
+ {#if signerAvailable}
+ <button type="button" class="btn" onclick={signerGetPubkey}>
+ <iconify-icon
+ icon="material-symbols:diamond-rounded"
+ width="32"
+ class="text-orange-400"
+ ></iconify-icon>
+ <div class="my-auto">NIP-55 Signer</div>
+ </button>
+ {/if}
+
+ <button type="button" class="btn" onclick={toggleNsec}>
+ <iconify-icon icon="ph:key-bold" width="32"></iconify-icon>
+ <div class="my-auto">Secret key</div>
+ </button>
+ {/if}
{/if}
- <button type="button" class="btn" onclick={toggleNsec}>
- <iconify-icon icon="ph:key-bold" width="32"></iconify-icon>
- <div class="my-auto">Nsec Login</div>
+ <button type="button" class="btn" onclick={cancel}>
+ <iconify-icon icon="ph:x-bold" width="32"></iconify-icon>
+ <div class="my-auto">Cancel</div>
</button>
{/if}
-
- <button type="button" class="btn" onclick={cancel}>
- <iconify-icon icon="ph:x-bold" width="32"></iconify-icon>
- <div class="my-auto">Cancel</div>
- </button>
</div>
</div>
{/if}
diff --git a/src/lib/nostr.ts b/src/lib/nostr.ts
index 77c1d60a..85f1bdaa 100644
--- a/src/lib/nostr.ts
+++ b/src/lib/nostr.ts
@@ -2,18 +2,21 @@ import { Buffer } from "buffer";
import { browser } from "$app/environment";
import {
decrypted,
+ eventToSign,
password as pw,
passwordPrompt,
- signaturePrompt,
+ signer as $signer,
} from "$lib/store";
import { post, wait } from "$lib/utils";
import {
type EventTemplate,
finalizeEvent,
+ getEventHash,
getPublicKey,
nip04,
nip19,
} from "nostr-tools";
+import { Relay } from "nostr-tools/relay";
import { get } from "svelte/store";
import {
@@ -21,8 +24,6 @@ import {
encrypt as nip49encrypt,
} from "nostr-tools/nip49";
-import { bech32m } from "@scure/base";
-
type User = {
[key: string]: any;
};
@@ -101,19 +102,17 @@ export const encryptNsec = async (nsec: string) => {
return nip49encrypt(d, await getPassword());
};
-type SignParams = {
- event: EventTemplate;
- user: User;
-};
-
-export const sign = async ({ event, user }: SignParams) => {
- let sk;
- if (user.nsec) {
- sk = await getPrivateKey(user);
- event = finalizeEvent(event, sk);
- } else {
- signaturePrompt.set(event);
- }
+export const sign = async (event) => {
+ eventToSign.set(event);
+ let unsubscribe;
+ const { method, params } = await new Promise((r) => {
+ unsubscribe = $signer.subscribe((v) => v?.ready && r(v));
+ });
+ await unsubscribe();
+ const signedEvent = await signingMethods[method](event, params);
+ eventToSign.set(signedEvent);
+ setTimeout(() => eventToSign.set(null), 1000);
+ return signedEvent;
};
export const send = (event: EventTemplate) => {
@@ -125,3 +124,67 @@ const getPassword = async (): Promise<string> => {
await wait(() => !!get(pw));
return get(pw) || "";
};
+
+export const nostrConnectRelay = "wss://relay.nsec.app";
+
+const signingMethods = {
+ async connect(event, { sk, pk, pubkey }) {
+ const id = crypto.randomUUID();
+ const signEvent = {
+ id,
+ method: "sign_event",
+ params: [JSON.stringify(event)],
+ };
+
+ const content = await nip04.encrypt(sk, pk, JSON.stringify(signEvent));
+
+ const signedSignEvent = finalizeEvent(
+ {
+ created_at: Math.round(Date.now() / 1000),
+ kind: 24133,
+ pubkey,
+ content,
+ tags: [["p", pk]],
+ },
+ sk,
+ );
+
+ const relay = await Relay.connect(nostrConnectRelay);
+ relay.publish(signedSignEvent);
+
+ return new Promise((resolve) => {
+ relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], {
+ async onevent(event) {
+ pk = event.pubkey;
+ try {
+ const response = JSON.parse(
+ await nip04.decrypt(sk, pk, event.content),
+ );
+
+ if (response.id === id) resolve(JSON.parse(response.result));
+ } catch (e) {
+ console.log("failed to parse nostr connect event", e);
+ }
+ },
+ });
+ });
+ },
+
+ async nsec(event, { sk }) {
+ return finalizeEvent(event, sk);
+ },
+
+ async extension(event) {
+ return window.nostr.signEvent(event);
+ },
+
+ async signer(event, { pubkey, sig }) {
+ event.pubkey = pubkey;
+ const signedEvent = {
+ ...event,
+ id: getEventHash(event),
+ sig,
+ };
+ return signedEvent;
+ },
+};
diff --git a/src/lib/settings.ts b/src/lib/settings.ts
index 3ce1edd5..53a25e18 100644
--- a/src/lib/settings.ts
+++ b/src/lib/settings.ts
@@ -1,4 +1,4 @@
-import { auth, fd, get, post } from "$lib/utils";
+import { auth, fd, post } from "$lib/utils";
import { fail } from "@sveltejs/kit";
type Error = {
@@ -16,16 +16,14 @@ export default async ({ cookies, request }) => {
form.autowithdraw = form.autowithdraw === "on";
}
- let user = { ...(await get("/me", auth(cookies))), ...form };
-
try {
- ({ user } = await post("/user", user, auth(cookies)));
+ const { user } = await post("/user", form, auth(cookies));
+
+ if (user.language) cookies.set("lang", user.language, { path: "/" });
+
+ return { user, success: true };
} catch (e) {
const { message } = e as Error;
return fail(400, { message });
}
-
- if (user.language) cookies.set("lang", user.language, { path: "/" });
-
- return { user, success: true };
};
diff --git a/src/lib/store.ts b/src/lib/store.ts
index 0aba3d46..23b38ace 100644
--- a/src/lib/store.ts
+++ b/src/lib/store.ts
@@ -28,7 +28,13 @@ const persistLocal = (key, defaultValue: any = undefined) => {
s.subscribe((v) => {
try {
- browser && localStorage.setItem(key, JSON.stringify(v));
+ if (browser) {
+ if (v) {
+ localStorage.setItem(key, JSON.stringify(v));
+ } else {
+ localStorage.removeItem(key);
+ }
+ }
} catch (e) {
console.log("problem setting key", v);
console.log(e);
@@ -54,7 +60,10 @@ export const mnemonic = persistSession("mnemonic");
export const newPayment = persistLocal("newPayment");
export const password = writable<string | undefined>();
export const passwordPrompt = writable();
-export const signaturePrompt = writable();
+export const eventToSign = writable();
+export const pubkey = persistLocal("pubkey");
+export const signer = persistLocal("signer");
+export const selectSigner = writable();
export const pin = persistLocal("pin");
export const rate = writable();
export const request = writable();
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index b21ce176..e39c2adc 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -338,26 +338,6 @@ export const wait = async (
return f();
};
-export const stretch = async (password: string, salt: Uint8Array) =>
- crypto.subtle.deriveKey(
- {
- name: "PBKDF2",
- salt,
- iterations: 100000,
- hash: "SHA-256",
- },
- await crypto.subtle.importKey(
- "raw",
- new TextEncoder().encode(password),
- { name: "PBKDF2" },
- false,
- ["deriveBits", "deriveKey"],
- ),
- { name: "AES-GCM", length: 256 },
- true,
- ["encrypt", "decrypt"],
- );
-
export const types = {
bitcoin: "bitcoin",
liquid: "liquid",
diff --git a/src/locales/en.json b/src/locales/en.json
index 848b467d..b07b47a4 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -253,6 +253,7 @@
},
"settings": {
"integrations": "Integrations",
+ "syncWithExtension": "Sync with extension",
"connectNostr": "Nostr Wallet Connect",
"pleaseEnterYourPassword": "Please enter your password",
"memoPrompt": "Checkout instructions",
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte
index e179005f..d037e5be 100644
--- a/src/routes/(app)/+layout.svelte
+++ b/src/routes/(app)/+layout.svelte
@@ -8,7 +8,7 @@
request,
ndef,
passwordPrompt,
- signaturePrompt,
+ selectSigner,
password,
pin,
theme as themeStore,
@@ -91,8 +91,8 @@
<Password {user} />
{/if}
-{#if browser && $signaturePrompt}
- <Nostr event={$signaturePrompt} />
+{#if browser}
+ <Nostr />
{/if}
<svelte:head>
diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte
index a83ed8d0..a5f336e7 100644
--- a/src/routes/(app)/[username]/(profile)/+layout.svelte
+++ b/src/routes/(app)/[username]/(profile)/+layout.svelte
@@ -77,7 +77,7 @@
};
try {
- await sign({ event, user });
+ await sign(event);
await send(event);
} catch (e) {
console.log(e);
@@ -205,7 +205,8 @@
)}`}
class="my-auto"
>
- <iconify-icon noobserver icon="ph:qr-code-bold" width="32"></iconify-icon>
+ <iconify-icon noobserver icon="ph:qr-code-bold" width="32"
+ ></iconify-icon>
</a>
</div>
</div>
@@ -222,7 +223,8 @@
></iconify-icon></button
>
<a href={`/qr/${encodeURIComponent(npub)}`} class="my-auto">
- <iconify-icon noobserver icon="ph:qr-code-bold" width="32"></iconify-icon>
+ <iconify-icon noobserver icon="ph:qr-code-bold" width="32"
+ ></iconify-icon>
</a>
</div>
</div>
@@ -234,12 +236,14 @@
{#if user && user.username !== subject.username && subject.pubkey}
{#if following}
<button class="btn" onclick={unfollow}>
- <iconify-icon noobserver icon="ph:user-bold" width="32"></iconify-icon>
+ <iconify-icon noobserver icon="ph:user-bold" width="32"
+ ></iconify-icon>
<div class="my-auto">{$t("user.unfollow")}</div>
</button>
{:else}
<button class="btn" onclick={follow}>
- <iconify-icon noobserver icon="ph:user-bold" width="32"></iconify-icon>
+ <iconify-icon noobserver icon="ph:user-bold" width="32"
+ ></iconify-icon>
<div class="my-auto">{$t("user.follow")}</div>
</button>
{/if}
@@ -266,7 +270,8 @@
class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex w-60"
>
<div class="mx-auto flex">
- <iconify-icon noobserver icon="ph:clock" width="32"></iconify-icon>
+ <iconify-icon noobserver icon="ph:clock" width="32"
+ ></iconify-icon>
</div>
</button>
</form>
@@ -296,7 +301,10 @@
</div>
<div class="text-secondary flex ml-auto">
<div class="flex items-center">
- <iconify-icon noobserver icon="ph:lightning-fill" class="text-yellow-300"
+ <iconify-icon
+ noobserver
+ icon="ph:lightning-fill"
+ class="text-yellow-300"
></iconify-icon>
{s((1 * sats) / rate)} =
{f(1, currency, locale, 0, 0)}
diff --git a/src/routes/(app)/messages/[recipient]/+page.svelte b/src/routes/(app)/messages/[recipient]/+page.svelte
index bc1a6383..d1cf6f94 100644
--- a/src/routes/(app)/messages/[recipient]/+page.svelte
+++ b/src/routes/(app)/messages/[recipient]/+page.svelte
@@ -91,7 +91,7 @@
user,
});
event.id = getEventHash(event);
- await sign({ event, user });
+ await sign(event);
await send(event);
sent = true;
diff --git a/src/routes/(app)/settings/+layout.svelte b/src/routes/(app)/settings/+layout.svelte
index 85e5b0e6..c6cc81c4 100644
--- a/src/routes/(app)/settings/+layout.svelte
+++ b/src/routes/(app)/settings/+layout.svelte
@@ -9,7 +9,7 @@
import Pin from "$comp/Pin.svelte";
import { loading, t } from "$lib/translations";
import { fd, fail, auth, post, sleep, warning, success } from "$lib/utils";
- import { avatar, banner, password, pin, save } from "$lib/store";
+ import { avatar, banner, signer, password, pin, save } from "$lib/store";
import { upload } from "$lib/upload";
import { page } from "$app/stores";
import { sign, send, setNsec } from "$lib/nostr";
@@ -53,10 +53,21 @@
},
});
- if (data.get("newNsec")) {
- await setNsec(user, data.get("newNsec"));
- data.set("nsec", user.nsec);
- data.set("pubkey", user.pubkey);
+ if (!user.pubkey || user.pubkey === prev.pubkey) {
+ data.delete("pubkey");
+ } else {
+ signer.set(
+ user.extension ? { method: "extension", ready: true } : null,
+ );
+ let event = {
+ kind: 1,
+ created_at: Date.now(),
+ content: user.challenge,
+ tags: [],
+ };
+
+ let signedEvent = await sign(event);
+ data.set("event", JSON.stringify(signedEvent));
}
if ($avatar) {
@@ -89,7 +100,7 @@
}
if (
- ["username", "about", "picture"].some(
+ ["username", "about", "picture", "display", "banner"].some(
(a) => user[a] && user[a] !== prev[a],
)
) {
@@ -101,14 +112,17 @@
name: user.username,
about: user.about,
picture: user.picture,
+ banner: user.banner,
+ displayName: user.display,
}),
tags: [],
};
try {
- await sign({ event, user });
+ await sign(event);
await send(event);
} catch (e) {
+ console.log(e);
warning("Nostr profile could not be updated");
}
}
diff --git a/src/routes/(app)/settings/nostr/+page.server.ts b/src/routes/(app)/settings/nostr/+page.server.ts
index 4145935e..b4bb124b 100644
--- a/src/routes/(app)/settings/nostr/+page.server.ts
+++ b/src/routes/(app)/settings/nostr/+page.server.ts
@@ -1,5 +1,11 @@
+import { get } from "$lib/utils";
import updateUser from "$lib/settings";
+export const load = async () => {
+ const { challenge } = await get("/challenge");
+ return { challenge };
+};
+
export const actions = {
default: updateUser,
};
diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte
index 3757f96f..cfcfaf39 100644
--- a/src/routes/(app)/settings/nostr/+page.svelte
+++ b/src/routes/(app)/settings/nostr/+page.svelte
@@ -3,35 +3,29 @@
import { page } from "$app/stores";
import { tick } from "svelte";
import { t } from "$lib/translations";
- import { copy } from "$lib/utils";
+ import { copy, fail } from "$lib/utils";
import {
PUBLIC_COINOS_PUBKEY as pk,
PUBLIC_COINOS_RELAY as relay,
} from "$env/static/public";
+ import { signer, save } from "$lib/store";
let { data } = $props();
- let { user } = $derived(data);
- let { id, npub } = $derived(user);
+ let { challenge, user } = $derived(data);
+ let { npub } = $state(user);
- let importing = $state(),
- newNsec = $state(),
+ let newNsec = $state(),
nsec = $state(),
pin = $state(""),
revealNsec = $state(),
revealNwc = $state();
- let toggleImporting = () => {
- revealNsec = false;
- importing = !importing;
- };
-
let toggleNsec = async () => {
try {
nsec = await getNsec(user);
revealNsec = !revealNsec;
- importing = false;
} catch (e) {
- console.log(e);
+ fail("Failed to decrypt nsec");
}
};
@@ -42,9 +36,19 @@
)}&secret=${user.nwc}&lud16=${lud16}`,
);
let toggleNwc = () => (revealNwc = !revealNwc);
+
+ let extension = $state();
+ let getPubkey = async () => {
+ extension = true;
+ npub = await window.nostr.getPublicKey();
+ await tick();
+ $save.click();
+ };
</script>
<div>
+ <input type="hidden" name="challenge" value={challenge} />
+ <input type="hidden" name="extension" value={extension} />
<label for="nwc" class="font-bold">{$t("user.settings.nwc")}</label>
<p class="text-secondary mb-1">
@@ -83,12 +87,10 @@
</div>
{/if}
-<div>
+<div class="space-y-2">
<div class="font-bold">{$t("user.nostrPubkey")}</div>
<div class="flex gap-4">
- <div class="break-all grow">
- {npub}
- </div>
+ <textarea name="pubkey" bind:value={npub}> </textarea>
<div class="flex my-auto gap-1">
<button type="button" class="my-auto" onclick={() => copy(npub)}
><iconify-icon noobserver icon="ph:copy-bold" width="32"
@@ -100,54 +102,48 @@
</a>
</div>
</div>
-</div>
-<div class="space-y-2">
- <label for="seedphrase" class="font-bold"
- >{$t("user.settings.nostrKeys")}</label
+ <button class="btn" type="button" onclick={getPubkey}>
+ <iconify-icon icon="lucide-lab:bee" width="32" class="text-yellow-400"
+ ></iconify-icon>
+ {$t("user.settings.syncWithExtension")}</button
>
+</div>
- <p class="text-secondary mb-1">
- {$t("user.settings.nostrDescription")}
- </p>
-
- <div class="flex flex-wrap sm:flex-nowrap gap-2">
- <button
- type="button"
- class="btn !w-auto flex-grow"
- onclick={toggleImporting}
+{#if user.nsec}
+ <div class="space-y-2">
+ <label for="seedphrase" class="font-bold"
+ >{$t("user.settings.nostrKeys")}</label
>
- <iconify-icon noobserver icon="ph:arrow-down-left-bold" width="32"
- ></iconify-icon>
- {$t("user.settings.import")}
- </button>
- <button type="button" class="btn !w-auto flex-grow" onclick={toggleNsec}>
- {#if revealNsec}
- <iconify-icon noobserver icon="ph:eye-slash-bold" width="32"
- ></iconify-icon>
- {$t("user.settings.hideNsec")}
- {:else}
- <iconify-icon noobserver icon="ph:warning-bold" width="32"
- ></iconify-icon>
- {$t("user.settings.revealNsec")}
- {/if}
- </button>
- </div>
+ <p class="text-secondary mb-1">
+ {$t("user.settings.nostrDescription")}
+ </p>
- {#if importing}
- <input name="newNsec" bind:value={newNsec} placeholder="nsec..." />
- {/if}
+ <div class="flex flex-wrap sm:flex-nowrap gap-2">
+ <button type="button" class="btn !w-auto flex-grow" onclick={toggleNsec}>
+ {#if revealNsec}
+ <iconify-icon noobserver icon="ph:eye-slash-bold" width="32"
+ ></iconify-icon>
+ {$t("user.settings.hideNsec")}
+ {:else}
+ <iconify-icon noobserver icon="ph:warning-bold" width="32"
+ ></iconify-icon>
+ {$t("user.settings.revealNsec")}
+ {/if}
+ </button>
+ </div>
- {#if revealNsec}
- <button
- type="button"
- class="btn break-all !h-auto font-normal leading-normal flex-nowrap"
- onclick={() => copy(nsec)}
- >
- <div>{nsec}</div>
+ {#if revealNsec}
+ <button
+ type="button"
+ class="btn break-all !h-auto font-normal leading-normal flex-nowrap"
+ onclick={() => copy(nsec)}
+ >
+ <div>{nsec}</div>
- <iconify-icon noobserver icon="ph:copy-bold" width="32"></iconify-icon>
- </button>
- {/if}
-</div>
+ <iconify-icon noobserver icon="ph:copy-bold" width="32"></iconify-icon>
+ </button>
+ {/if}
+ </div>
+{/if}
diff --git a/src/routes/(static)/+layout.svelte b/src/routes/(static)/+layout.svelte
index 44310c97..a0061a19 100644
--- a/src/routes/(static)/+layout.svelte
+++ b/src/routes/(static)/+layout.svelte
@@ -1,6 +1,7 @@
<script>
import { run } from 'svelte/legacy';
+ import Nostr from "$comp/Nostr.svelte";
import { SvelteToast } from "@zerodevx/svelte-toast";
import Footer from "$comp/Footer.svelte";
import LandingHeader from "$comp/LandingHeader.svelte";
@@ -16,6 +17,7 @@
<SvelteToast options={{ reversed: true, intro: { y: 192 } }} />
<LandingHeader {user} />
+<Nostr />
<main
class="container flex px-4 md:px-40 mx-auto min-h-[600px]"
data-theme={theme}
diff --git a/src/routes/(static)/login/+page.server.ts b/src/routes/(static)/login/+page.server.ts
index 6bf06ad2..f86f62e9 100644
--- a/src/routes/(static)/login/+page.server.ts
+++ b/src/routes/(static)/login/+page.server.ts
@@ -5,8 +5,8 @@ import { fail, redirect } from "@sveltejs/kit";
export const load = async ({ parent }) => {
const { user } = await parent();
if (user?.pubkey) redirect(307, `/${user.username}`);
- const { id } = await get("/challenge");
- return { id };
+ const { challenge } = await get("/challenge");
+ return { challenge };
};
export const actions = {
@@ -29,7 +29,7 @@ export const actions = {
nostr: async ({ cookies, fetch, request }) => {
const form = await fd(request);
- let { id, event, loginRedirect } = form;
+ let { challenge, event, loginRedirect } = form;
if (loginRedirect === "null") loginRedirect = undefined;
event = JSON.parse(event);
@@ -37,7 +37,7 @@ export const actions = {
const res = await fetch(`${PUBLIC_COINOS_URL}/nostrLogin`, {
method: "POST",
- body: JSON.stringify({ id, event }),
+ body: JSON.stringify({ challenge, event }),
headers: {
"content-type": "application/json",
accept: "application/json",
diff --git a/src/routes/(static)/login/+page.svelte b/src/routes/(static)/login/+page.svelte
index 7e8234ee..e4dcbf15 100644
--- a/src/routes/(static)/login/+page.svelte
+++ b/src/routes/(static)/login/+page.svelte
@@ -1,39 +1,42 @@
<script>
import { browser } from "$app/environment";
import handler from "$lib/handler";
-
- import { onMount } from "svelte";
import { applyAction, deserialize } from "$app/forms";
- import { tick } from "svelte";
+ import { onMount, tick } from "svelte";
import { fly } from "svelte/transition";
import { enhance } from "$app/forms";
- import Nostr from "$comp/Nostr.svelte";
import Pinpad from "$comp/Pinpad.svelte";
import PasswordInput from "$comp/PasswordInput.svelte";
+ import Spinner from "$comp/Spinner.svelte";
import { focus, fail } from "$lib/utils";
- import { password, pin, loginRedirect } from "$lib/store";
+ import { password, signer, pin, loginRedirect } from "$lib/store";
import { t } from "$lib/translations";
import { page } from "$app/stores";
+ import { sign } from "$lib/nostr";
import { invalidateAll } from "$app/navigation";
let { data, form } = $props();
- onMount(() => {
- $pin = undefined;
- localStorage.clear();
- sessionStorage.clear();
+ page.subscribe(async () => {
+ if (browser) {
+ setTimeout(() => {
+ password.set(undefined);
+ pin?.set(undefined);
+ signer.set(undefined);
+ localStorage.clear();
+ sessionStorage.clear();
+ }, 500);
+ }
+ });
+ onMount(() => {
let lang = $page.url.searchParams.get("lang");
if (lang) document.cookie = `lang=${lang} ;`;
});
- let { id } = $derived(data);
- let nostrSignin = $state();
-
+ let { challenge } = $derived(data);
let token = $state("");
- $password = undefined;
-
let cancel = () => (need2fa = false);
let username = $state(),
@@ -81,13 +84,43 @@
let redirect = $derived(
$loginRedirect || $page.url.searchParams.get("redirect"),
);
+
+ let nostrLogin = async () => {
+ let event = {
+ kind: 1,
+ created_at: Date.now(),
+ content: challenge,
+ tags: [],
+ };
+
+ let signedEvent = await sign(event);
+
+ const formData = new FormData();
+
+ try {
+ formData.append("loginRedirect", redirect);
+ formData.append("token", token);
+ formData.append("event", JSON.stringify(signedEvent));
+ formData.append("challenge", challenge);
+
+ let response = await fetch("/login?/nostr", {
+ method: "POST",
+ body: formData,
+ });
+
+ const result = deserialize(await response.text());
+
+ applyAction(result);
+ } catch (e) {
+ fail(e.message);
+ }
+ };
</script>
<div
class="mx-auto md:shadow-xl rounded-3xl max-w-xl w-full md:w-[480px] md:p-8 mb-20"
>
<h1 class="text-2xl font-bold text-center">{$t("login.signIn")}</h1>
-
{#if form?.error && !form?.message.includes("2fa")}
<div class="text-red-600 text-center" in:fly>
{form.error}
@@ -126,10 +159,17 @@
{$t("login.signIn")}
</button>
- <button type="button" class="btn" onclick={() => (nostrSignin = true)}>
- <img src="/images/nostr.png" class="w-8" />
+ <button type="button" class="btn" onclick={nostrLogin}>
+ {#if $signer?.ready}
+ <div class="shrink">
+ <Spinner />
+ </div>
+ {:else}
+ <img src="/images/nostr.png" class="w-8" />
+ {/if}
<div class="my-auto">{$t("login.nostr")}</div>
</button>
+
<p class="text-secondary text-center font-medium">
{$t("login.noAccount")}
<a
@@ -163,5 +203,3 @@
</div>
</div>
{/if}
-
-<Nostr {id} bind:nostrSignin {redirect} {token} />
diff --git a/src/routes/(static)/register/+page.server.ts b/src/routes/(static)/register/+page.server.ts
index bf908c54..2e5c5f19 100644
--- a/src/routes/(static)/register/+page.server.ts
+++ b/src/routes/(static)/register/+page.server.ts
@@ -6,9 +6,9 @@ export const load = async ({ parent }) => {
if (user) redirect(307, `/${user.username}`);
const index = Math.floor(Math.random() * 64) + 1;
- const { id } = await get("/challenge");
+ const { challenge } = await get("/challenge");
- return { index, id };
+ return { index, challenge };
};
export const actions = {
diff --git a/src/routes/(static)/register/+page.svelte b/src/routes/(static)/register/+page.svelte
index 77394e1a..e7819f4f 100644
--- a/src/routes/(static)/register/+page.svelte
+++ b/src/routes/(static)/register/+page.svelte
@@ -8,13 +8,12 @@
import { fly } from "svelte/transition";
import { enhance } from "$app/forms";
- import Nostr from "$comp/Nostr.svelte";
import Pin from "$comp/Pin.svelte";
import Spinner from "$comp/Spinner.svelte";
import PasswordInput from "$comp/PasswordInput.svelte";
import { focus, fail } from "$lib/utils";
- import { avatar, password, loginRedirect } from "$lib/store";
+ import { avatar, signer, password, loginRedirect } from "$lib/store";
import { t } from "$lib/translations";
import { page } from "$app/stores";
import {
@@ -24,11 +23,22 @@
adjectives,
animals,
} from "unique-names-generator";
+ import { sign } from "$lib/nostr";
let { form, data } = $props();
- let { id } = $derived(data);
+ let { challenge } = $derived(data);
- let nostrSignin = $state();
+ page.subscribe(async () => {
+ if (browser) {
+ setTimeout(() => {
+ password.set(undefined);
+ pin.set(undefined);
+ signer.set(undefined);
+ localStorage.clear();
+ sessionStorage.clear();
+ }, 500);
+ }
+ });
let username = $state();
let { index } = $state(data);
@@ -161,24 +171,22 @@
});
let nostrLogin = async () => {
- let pubkey = await window.nostr.getPublicKey();
- const formData = new FormData();
-
- let ev = {
+ let event = {
kind: 1,
- pubkey,
created_at: Date.now(),
- content: id,
+ content: challenge,
tags: [],
};
- try {
- let signedEvent = await window.nostr.signEvent(ev);
+ let signedEvent = await sign(event);
+ const formData = new FormData();
+
+ try {
formData.append("loginRedirect", redirect);
formData.append("token", token);
formData.append("event", JSON.stringify(signedEvent));
- formData.append("id", id);
+ formData.append("challenge", challenge);
let response = await fetch("/login?/nostr", {
method: "POST",
@@ -309,8 +317,14 @@
{/if}
</button>
- <button type="button" class="btn" onclick={() => (nostrSignin = true)}>
- <img src="/images/nostr.png" class="w-8" />
+ <button type="button" class="btn" onclick={nostrLogin}>
+ {#if $signer?.ready}
+ <div class="shrink">
+ <Spinner />
+ </div>
+ {:else}
+ <img src="/images/nostr.png" class="w-8" />
+ {/if}
<div class="my-auto">{$t("login.nostr")}</div>
</button>
@@ -325,5 +339,3 @@
</p>
</form>
</div>
-
-<Nostr {id} {nostrSignin} {redirect} {token} />