Refactor login, pass bunker to alerts

This commit is contained in:
Jon Staab
2025-03-20 12:55:12 -07:00
parent 9eefd6600d
commit d2c537d275
6 changed files with 316 additions and 188 deletions

View File

@@ -466,27 +466,34 @@ export type AlertParams = {
email: string email: string
relay: string relay: string
filters: Filter[] filters: Filter[]
bunker: string
secret: string
} }
export const makeAlert = async ({cron, email, relay, filters}: AlertParams) => { export const makeAlert = async ({cron, email, relay, filters, bunker, secret}: AlertParams) => {
const handler = const tags = [
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050"
const handlerRelay = "wss://relay.nostr.band/"
return createEvent(ALERT, {
content: await signer
.get()
.nip44.encrypt(
NOTIFIER_PUBKEY,
JSON.stringify([
["cron", cron], ["cron", cron],
["email", email], ["email", email],
["relay", relay], ["relay", relay],
["channel", "email"], ["channel", "email"],
["handler", handler, handlerRelay, "web"], [
...unionFilters(filters).map(filter => ["filter", JSON.stringify(filter)]), "handler",
]), "31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
), "wss://relay.nostr.band/",
"web",
],
]
for (const filter of unionFilters(filters)) {
tags.push(["filter", JSON.stringify(filter)])
}
if (bunker) {
tags.push(["nip46", secret, bunker])
}
return createEvent(ALERT, {
content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
tags: [ tags: [
["d", randomId()], ["d", randomId()],
["p", NOTIFIER_PUBKEY], ["p", NOTIFIER_PUBKEY],

View File

@@ -3,6 +3,7 @@
import {randomInt} from "@welshman/lib" import {randomInt} from "@welshman/lib"
import {displayRelayUrl, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util" import {displayRelayUrl, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
import type {Filter} from "@welshman/util" import type {Filter} from "@welshman/util"
import type {Nip46ResponseWithResult} from "@welshman/signer"
import {pubkey} from "@welshman/app" import {pubkey} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -10,11 +11,13 @@
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import InfoBunker from "@app/components/InfoBunker.svelte"
import BunkerConnect, {BunkerConnectController} from "@app/components/BunkerConnect.svelte"
import {GENERAL, getMembershipUrls, getMembershipRoomsByUrl, userMembership} from "@app/state" import {GENERAL, getMembershipUrls, getMembershipRoomsByUrl, userMembership} from "@app/state"
import {loadAlertStatuses} from "@app/requests" import {loadAlertStatuses} from "@app/requests"
import {publishAlert} from "@app/commands" import {publishAlert} from "@app/commands"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {pushModal} from "@app/modal"
const timezone = new Date() const timezone = new Date()
.toString() .toString()
.match(/GMT[^\s]+/)![0] .match(/GMT[^\s]+/)![0]
@@ -29,12 +32,36 @@
let cron = WEEKLY let cron = WEEKLY
let email = "" let email = ""
let relay = "" let relay = ""
let bunker = ""
let secret = ""
let notifyThreads = true let notifyThreads = true
let notifyCalendar = true let notifyCalendar = true
let notifyChat = false let notifyChat = false
let showBunker = false
const back = () => history.back() const back = () => history.back()
const controller = new BunkerConnectController({
onNostrConnect: (response: Nip46ResponseWithResult) => {
bunker = controller.broker.getBunkerUrl()
secret = controller.broker.params.clientSecret
showBunker = false
},
})
const connectBunker = () => {
showBunker = true
}
const hideBunker = () => {
showBunker = false
}
const clearBunker = () => {
bunker = ""
secret = ""
}
const submit = async () => { const submit = async () => {
if (!email.includes("@")) { if (!email.includes("@")) {
return pushToast({ return pushToast({
@@ -79,7 +106,7 @@
loading = true loading = true
try { try {
const thunk = await publishAlert({cron, email, relay, filters}) const thunk = await publishAlert({cron, email, relay, filters, bunker, secret})
await thunk.result await thunk.result
await loadAlertStatuses($pubkey!) await loadAlertStatuses($pubkey!)
@@ -98,6 +125,13 @@
Add an Alert Add an Alert
{/snippet} {/snippet}
</ModalHeader> </ModalHeader>
{#if showBunker}
<div class="card2 flex flex-col items-center gap-4 bg-base-300">
<p>Scan using a nostr signer, or click to copy.</p>
<BunkerConnect {controller} />
<Button class="btn btn-neutral btn-sm" onclick={hideBunker}>Cancel</Button>
</div>
{:else}
<FieldInline> <FieldInline>
{#snippet label()} {#snippet label()}
<p>Email Address*</p> <p>Email Address*</p>
@@ -153,12 +187,38 @@
</div> </div>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
<div class="card2 flex flex-col gap-3 bg-base-300">
<div class="flex items-center justify-between">
<strong>Connect a Bunker</strong>
<span class="flex items-center gap-2 text-sm" class:text-primary={bunker}>
{#if bunker}
<Icon icon="check-circle" size={5} />
Connected
{:else}
<Icon icon="close-circle" size={5} />
Not Connected
{/if}
</span>
</div>
<p class="text-sm">
Required for receiving alerts about spaces with access controls. You can get one from your
<Button class="text-primary" onclick={() => pushModal(InfoBunker)}>remote signer app</Button
>.
</p>
{#if bunker}
<Button class="btn btn-neutral btn-sm flex-grow" onclick={clearBunker}>Disconnect</Button>
{:else}
<Button class="btn btn-primary btn-sm w-full flex-grow" onclick={connectBunker}
>Connect</Button>
{/if}
</div>
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon="alt-arrow-left" /> <Icon icon="alt-arrow-left" />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading}> <Button type="submit" class="btn btn-primary" disabled={loading || showBunker}>
<Spinner {loading}>Confirm</Spinner> <Spinner {loading}>Confirm</Spinner>
<Icon icon="alt-arrow-right" /> <Icon icon="alt-arrow-right" />
</Button> </Button>

View File

@@ -1,12 +1,20 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte"
import {pubkey} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import AlertAdd from "@app/components/AlertAdd.svelte" import AlertAdd from "@app/components/AlertAdd.svelte"
import AlertItem from "@app/components/AlertItem.svelte" import AlertItem from "@app/components/AlertItem.svelte"
import {loadAlertStatuses, loadAlerts} from "@app/requests"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {alerts} from "@app/state" import {alerts} from "@app/state"
const startAlert = () => pushModal(AlertAdd) const startAlert = () => pushModal(AlertAdd)
onMount(() => {
loadAlertStatuses($pubkey!)
loadAlerts($pubkey!)
})
</script> </script>
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl"> <div class="card2 bg-alt flex flex-col gap-6 shadow-xl">

View File

@@ -0,0 +1,78 @@
<script module lang="ts">
import type {Nip46ResponseWithResult} from "@welshman/signer"
import {Nip46Broker, makeSecret} from "@welshman/signer"
import {NIP46_PERMS, PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO, SIGNER_RELAYS} from "@app/state"
export class BunkerConnectController {
url = $state("")
bunker = $state("")
loading = $state(false)
clientSecret = makeSecret()
abortController = new AbortController()
broker = Nip46Broker.get({clientSecret: this.clientSecret, relays: SIGNER_RELAYS})
onNostrConnect: (response: Nip46ResponseWithResult) => void
constructor({onNostrConnect}: {onNostrConnect: (response: Nip46ResponseWithResult) => void}) {
this.onNostrConnect = onNostrConnect
}
async start() {
this.url = await this.broker.makeNostrconnectUrl({
perms: NIP46_PERMS,
url: PLATFORM_URL,
name: PLATFORM_NAME,
image: PLATFORM_LOGO,
})
let response
try {
response = await this.broker.waitForNostrconnect(this.url, this.abortController)
} catch (errorResponse: any) {
if (errorResponse?.error) {
pushToast({
theme: "error",
message: `Received error from signer: ${errorResponse.error}`,
})
} else if (errorResponse) {
console.error(errorResponse)
}
}
if (response) {
this.loading = true
this.onNostrConnect(response)
}
}
stop() {
this.abortController.abort()
}
}
</script>
<script lang="ts">
import {onMount, onDestroy} from "svelte"
import {slideAndFade} from "@lib/transition"
import QRCode from "@app/components/QRCode.svelte"
import {pushToast} from "@app/toast"
type Props = {
controller: BunkerConnectController
}
const {controller}: Props = $props()
onMount(() => {
controller.start()
})
onDestroy(() => {
controller.stop()
})
</script>
{#if controller.url}
<div class="flex justify-center" out:slideAndFade>
<QRCode code={controller.url} />
</div>
{/if}

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import {pushModal} from "@app/modal"
import InfoBunker from "@app/components/InfoBunker.svelte"
import Button from "@lib/components/Button.svelte"
import Field from "@lib/components/Field.svelte"
import Icon from "@lib/components/Icon.svelte"
type Props = {
bunker: string
loading: boolean
}
let {loading, bunker = $bindable("")}: Props = $props()
</script>
<Field>
{#snippet label()}
<p>Bunker Link*</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="cpu" />
<input disabled={loading} bind:value={bunker} class="grow" placeholder="bunker://" />
</label>
{/snippet}
{#snippet info()}
<p>
A login link provided by a nostr signing app.
<Button class="link" onclick={() => pushModal(InfoBunker)}>What is a bunker link?</Button>
</p>
{/snippet}
</Field>

View File

@@ -1,112 +1,34 @@
<script lang="ts"> <script lang="ts">
import {onMount, onDestroy} from "svelte" import type {Nip46ResponseWithResult} from "@welshman/signer"
import {Nip46Broker, getPubkey, makeSecret} from "@welshman/signer" import {Nip46Broker, getPubkey, makeSecret} from "@welshman/signer"
import {addSession} from "@welshman/app" import {addSession} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {slideAndFade} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Field from "@lib/components/Field.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import QRCode from "@app/components/QRCode.svelte" import BunkerConnect, {BunkerConnectController} from "@app/components/BunkerConnect.svelte"
import InfoBunker from "@app/components/InfoBunker.svelte" import BunkerUrl from "@app/components/BunkerUrl.svelte"
import {loginWithNip46} from "@app/commands" import {loginWithNip46} from "@app/commands"
import {loadUserData} from "@app/requests" import {loadUserData} from "@app/requests"
import {pushModal, clearModals} from "@app/modal" import {clearModals} from "@app/modal"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {NIP46_PERMS, PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO, SIGNER_RELAYS} from "@app/state" import {SIGNER_RELAYS} from "@app/state"
const clientSecret = makeSecret()
const abortController = new AbortController()
const broker = Nip46Broker.get({clientSecret, relays: SIGNER_RELAYS})
const back = () => history.back() const back = () => history.back()
const onSubmit = async () => { const controller = new BunkerConnectController({
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(bunker) onNostrConnect: async (response: Nip46ResponseWithResult) => {
const userPubkey = await controller.broker.getPublicKey()
if (loading) {
return
}
if (!signerPubkey || relays.length === 0) {
return pushToast({
theme: "error",
message: "Sorry, it looks like that's an invalid bunker link.",
})
}
loading = true
try {
const success = await loginWithNip46({connectSecret, clientSecret, signerPubkey, relays})
if (success) {
abortController.abort()
} else {
return pushToast({
theme: "error",
message: "Something went wrong, please try again!",
})
}
} finally {
loading = false
}
clearModals()
}
let url = $state("")
let bunker = $state("")
let loading = $state(false)
$effect(() => {
// For testing and for play store reviewers
if (bunker === "reviewkey") {
const secret = makeSecret()
addSession({method: "nip01", secret, pubkey: getPubkey(secret)})
}
})
onMount(async () => {
url = await broker.makeNostrconnectUrl({
perms: NIP46_PERMS,
url: PLATFORM_URL,
name: PLATFORM_NAME,
image: PLATFORM_LOGO,
})
let response
try {
response = await broker.waitForNostrconnect(url, abortController)
} catch (errorResponse: any) {
if (errorResponse?.error) {
pushToast({
theme: "error",
message: `Received error from signer: ${errorResponse.error}`,
})
} else if (errorResponse) {
console.error(errorResponse)
}
}
if (response) {
loading = true
const userPubkey = await broker.getPublicKey()
await loadUserData(userPubkey) await loadUserData(userPubkey)
addSession({ addSession({
method: "nip46", method: "nip46",
pubkey: userPubkey, pubkey: userPubkey,
secret: clientSecret, secret: controller.clientSecret,
handler: { handler: {
pubkey: response.event.pubkey, pubkey: response.event.pubkey,
relays: SIGNER_RELAYS, relays: SIGNER_RELAYS,
@@ -115,11 +37,49 @@
setChecked("*") setChecked("*")
clearModals() clearModals()
} },
}) })
onDestroy(() => { const onSubmit = async () => {
abortController.abort() if (controller.loading) return
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(controller.bunker)
if (!signerPubkey || relays.length === 0) {
return pushToast({
theme: "error",
message: "Sorry, it looks like that's an invalid bunker link.",
})
}
controller.loading = true
try {
const {clientSecret} = controller
const success = await loginWithNip46({connectSecret, clientSecret, signerPubkey, relays})
if (success) {
controller.stop()
} else {
return pushToast({
theme: "error",
message: "Something went wrong, please try again!",
})
}
} finally {
controller.loading = false
}
clearModals()
}
$effect(() => {
// For testing and for play store reviewers
if (controller.bunker === "reviewkey") {
const secret = makeSecret()
addSession({method: "nip01", secret, pubkey: getPubkey(secret)})
}
}) })
</script> </script>
@@ -132,35 +92,18 @@
<div>Connect your signer by scanning the QR code below or pasting a bunker link.</div> <div>Connect your signer by scanning the QR code below or pasting a bunker link.</div>
{/snippet} {/snippet}
</ModalHeader> </ModalHeader>
{#if !loading && url} <BunkerConnect {controller} />
<div class="flex justify-center" out:slideAndFade> <BunkerUrl loading={controller.loading} bind:bunker={controller.bunker} />
<QRCode code={url} />
</div>
{/if}
<Field>
{#snippet label()}
<p>Bunker Link*</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="cpu" />
<input disabled={loading} bind:value={bunker} class="grow" placeholder="bunker://" />
</label>
{/snippet}
{#snippet info()}
<p>
A login link provided by a nostr signing app.
<Button class="link" onclick={() => pushModal(InfoBunker)}>What is a bunker link?</Button>
</p>
{/snippet}
</Field>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back} disabled={loading}> <Button class="btn btn-link" onclick={back} disabled={controller.loading}>
<Icon icon="alt-arrow-left" /> <Icon icon="alt-arrow-left" />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading || !bunker}> <Button
<Spinner {loading}>Next</Spinner> type="submit"
class="btn btn-primary"
disabled={controller.loading || !controller.bunker}>
<Spinner loading={controller.loading}>Next</Spinner>
<Icon icon="alt-arrow-right" /> <Icon icon="alt-arrow-right" />
</Button> </Button>
</ModalFooter> </ModalFooter>