diff options
| author | Adam Soltys <[email protected]> | 2025-01-09 23:44:40 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2025-01-09 23:44:40 -0800 |
| commit | 8588de25f16486b56ef0ad533013bd8cb48ff120 (patch) | |
| tree | 4c1e39704e2edb332c5fe8c58fb0d81c8154af2b /src/routes | |
| parent | 2358d1bf39c34a508db6c50f0f6312ee411a8a17 (diff) | |
| download | coinos-ui-8588de25f16486b56ef0ad533013bd8cb48ff120.tar.gz coinos-ui-8588de25f16486b56ef0ad533013bd8cb48ff120.zip | |
nostr signing
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/(app)/+layout.svelte | 6 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/(profile)/+layout.svelte | 22 | ||||
| -rw-r--r-- | src/routes/(app)/messages/[recipient]/+page.svelte | 2 | ||||
| -rw-r--r-- | src/routes/(app)/settings/+layout.svelte | 28 | ||||
| -rw-r--r-- | src/routes/(app)/settings/nostr/+page.server.ts | 6 | ||||
| -rw-r--r-- | src/routes/(app)/settings/nostr/+page.svelte | 114 | ||||
| -rw-r--r-- | src/routes/(static)/+layout.svelte | 2 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.server.ts | 8 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.svelte | 76 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.server.ts | 4 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.svelte | 46 |
11 files changed, 195 insertions, 119 deletions
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} /> |
