forked from coracle/flotilla
Add pomade signing
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
||||
VITE_BURROW_URL=
|
||||
VITE_POMADE_SIGNERS=92f258a6e1baf021408e4fe9f8ec526a93397a3616b52bd64d88ef51433a4e53,c00b809f52cdeca7e8fb7e91d5d9526a3fa44ff8e513d92e5abac3a26d88f89f,98d9454a5ef68d6b9973831a969cd564447263b4e6d27c7efe7909c40c3b4825
|
||||
VITE_PLATFORM_URL=https://app.flotilla.social
|
||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||
|
||||
+16
-2
@@ -85,7 +85,8 @@
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"tippy.js": "^6.3.7"
|
||||
"tippy.js": "^6.3.7",
|
||||
"@pomade/core": "^0.0.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"ignoredBuiltDependencies": [
|
||||
@@ -94,6 +95,19 @@
|
||||
],
|
||||
"onlyBuiltDependencies": [
|
||||
"sharp"
|
||||
]
|
||||
],
|
||||
"overrides": {
|
||||
"@welshman/app": "link:../welshman/packages/app",
|
||||
"@welshman/content": "link:../welshman/packages/content",
|
||||
"@welshman/editor": "link:../welshman/packages/editor",
|
||||
"@welshman/feeds": "link:../welshman/packages/feeds",
|
||||
"@welshman/lib": "link:../welshman/packages/lib",
|
||||
"@welshman/net": "link:../welshman/packages/net",
|
||||
"@welshman/router": "link:../welshman/packages/router",
|
||||
"@welshman/signer": "link:../welshman/packages/signer",
|
||||
"@welshman/store": "link:../welshman/packages/store",
|
||||
"@welshman/util": "link:../welshman/packages/util",
|
||||
"@pomade/core": "link:../pomade/packages/core"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,32 +5,13 @@
|
||||
import Landing from "@app/components/Landing.svelte"
|
||||
import Toast from "@app/components/Toast.svelte"
|
||||
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
||||
import EmailConfirm from "@app/components/EmailConfirm.svelte"
|
||||
import PasswordReset from "@app/components/PasswordReset.svelte"
|
||||
import {BURROW_URL} from "@app/core/state"
|
||||
import {modals, pushModal} from "@app/util/modal"
|
||||
import {modals} from "@app/util/modal"
|
||||
|
||||
interface Props {
|
||||
children: Snippet
|
||||
}
|
||||
|
||||
const {children}: Props = $props()
|
||||
|
||||
if (BURROW_URL && !$pubkey) {
|
||||
if ($page.url.pathname === "/confirm-email") {
|
||||
pushModal(EmailConfirm, {
|
||||
email: $page.url.searchParams.get("email"),
|
||||
confirm_token: $page.url.searchParams.get("confirm_token"),
|
||||
})
|
||||
}
|
||||
|
||||
if ($page.url.pathname === "/reset-password") {
|
||||
pushModal(PasswordReset, {
|
||||
email: $page.url.searchParams.get("email"),
|
||||
reset_token: $page.url.searchParams.get("reset_token"),
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {postJson, sleep} from "@welshman/lib"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {BURROW_URL} from "@app/core/state"
|
||||
|
||||
const {email, confirm_token} = $props()
|
||||
|
||||
const login = () => {
|
||||
pushModal(LogInPassword, {email}, {path: "/"})
|
||||
}
|
||||
|
||||
let error = $state("")
|
||||
let loading = $state(true)
|
||||
|
||||
onMount(async () => {
|
||||
const [res] = await Promise.all([
|
||||
postJson(BURROW_URL + "/user/confirm-email", {email, confirm_token}),
|
||||
sleep(2000),
|
||||
])
|
||||
|
||||
error = res.error
|
||||
loading = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
<h1 class="heading">
|
||||
{#if loading}
|
||||
Just a second...
|
||||
{:else if error}
|
||||
Oops!
|
||||
{:else}
|
||||
Success!
|
||||
{/if}
|
||||
</h1>
|
||||
<p class="m-auto max-w-sm text-center">
|
||||
<Spinner {loading}>
|
||||
{#if loading}
|
||||
Hang tight, we're checking your confirmation link.
|
||||
{:else if error}
|
||||
Looks like something went wrong. {error}
|
||||
{:else}
|
||||
You're all set - click below to log in.
|
||||
{/if}
|
||||
</Spinner>
|
||||
</p>
|
||||
<Button class="btn btn-primary" onclick={login} disabled={loading}>Continue to Login</Button>
|
||||
</div>
|
||||
@@ -4,7 +4,7 @@
|
||||
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
|
||||
import {addSession, type Session, makeNip07Session, makeNip55Session} from "@welshman/app"
|
||||
import Widget from "@assets/icons/widget-2.svg?dataurl"
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
|
||||
import Compass from "@assets/icons/compass-big.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
@@ -13,15 +13,17 @@
|
||||
import SignUp from "@app/components/SignUp.svelte"
|
||||
import InfoNostr from "@app/components/InfoNostr.svelte"
|
||||
import LogInBunker from "@app/components/LogInBunker.svelte"
|
||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||
import LogInEmail from "@app/components/LogInEmail.svelte"
|
||||
import {pushModal, clearModals} from "@app/util/modal"
|
||||
import {PLATFORM_NAME, BURROW_URL} from "@app/core/state"
|
||||
import {PLATFORM_NAME, POMADE_SIGNERS} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
|
||||
let signers: any[] = $state([])
|
||||
let loading: string | undefined = $state()
|
||||
|
||||
const hasPomade = POMADE_SIGNERS.length >= 3
|
||||
|
||||
const disabled = $derived(loading ? true : undefined)
|
||||
|
||||
const signUp = () => pushModal(SignUp)
|
||||
@@ -72,7 +74,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const loginWithPassword = () => pushModal(LogInPassword)
|
||||
const loginWithEmail = () => pushModal(LogInEmail)
|
||||
|
||||
const loginWithBunker = () => pushModal(LogInBunker)
|
||||
|
||||
@@ -112,39 +114,31 @@
|
||||
Log in with {app.name}
|
||||
</Button>
|
||||
{/each}
|
||||
{#if BURROW_URL && !hasSigner}
|
||||
<Button {disabled} onclick={loginWithPassword} class="btn btn-primary">
|
||||
{#if loading === "password"}
|
||||
<span class="loading loading-spinner mr-3"></span>
|
||||
{:else}
|
||||
<Icon icon={Key} />
|
||||
{/if}
|
||||
Log in with Password
|
||||
{#if hasPomade && !hasSigner}
|
||||
<Button {disabled} onclick={loginWithEmail} class="btn btn-primary">
|
||||
<Icon icon={Letter} />
|
||||
Log in with Email
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
onclick={loginWithBunker}
|
||||
{disabled}
|
||||
class="btn {hasSigner || BURROW_URL ? 'btn-neutral' : 'btn-primary'}">
|
||||
class="btn {hasSigner || hasPomade ? 'btn-neutral' : 'btn-primary'}">
|
||||
<Icon icon={Cpu} />
|
||||
Log in with Remote Signer
|
||||
</Button>
|
||||
{#if BURROW_URL && hasSigner}
|
||||
<Button {disabled} onclick={loginWithPassword} class="btn">
|
||||
{#if loading === "password"}
|
||||
<span class="loading loading-spinner mr-3"></span>
|
||||
{:else}
|
||||
<Icon icon={Key} />
|
||||
{/if}
|
||||
Log in with Password
|
||||
{#if hasPomade && hasSigner}
|
||||
<Button {disabled} onclick={loginWithEmail} class="btn">
|
||||
<Icon icon={Letter} />
|
||||
Log in with Email
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !hasSigner || !BURROW_URL}
|
||||
{#if !hasSigner || !hasPomade}
|
||||
<Link
|
||||
external
|
||||
{disabled}
|
||||
href="https://nostrapps.com#signers"
|
||||
class="btn {hasSigner || BURROW_URL ? '' : 'btn-neutral'}">
|
||||
class="btn {hasSigner || hasPomade ? '' : 'btn-neutral'}">
|
||||
<Icon icon={Compass} />
|
||||
Browse Signer Apps
|
||||
</Link>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import {Client} from "@pomade/core"
|
||||
import {loginWithPomade} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Key from "@assets/icons/key.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import LogInOTP from "@app/components/LogInOTP.svelte"
|
||||
import {pushModal, clearModals} from "@app/util/modal"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
interface Props {
|
||||
email?: string
|
||||
}
|
||||
|
||||
let {email = $bindable("")}: Props = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const loginWithOTP = () => pushModal(LogInOTP, {email})
|
||||
|
||||
const onSubmit = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const {ok, options, messages, clientSecret} = await Client.loginWithPassword(email, password)
|
||||
|
||||
if (!ok) {
|
||||
console.error(messages)
|
||||
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we were unable to log you in.",
|
||||
})
|
||||
}
|
||||
|
||||
const [client, peers] = options[0]!
|
||||
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
|
||||
|
||||
if (res.ok && clientOptions) {
|
||||
loginWithPomade(clientOptions.group.group_pk.slice(2), clientOptions)
|
||||
pushToast({message: "Successfully logged in!"})
|
||||
setChecked("*")
|
||||
clearModals()
|
||||
} else {
|
||||
console.error(res.messages)
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we were unable to log you in.",
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
let password = $state("")
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Log In</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Log in using your email and password</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Email*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Letter} />
|
||||
<input bind:value={email} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Password*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Key} />
|
||||
<input type="password" bind:value={password} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<p class="text-sm">
|
||||
Forgot your password? <Button class="link" onclick={loginWithOTP}>Log in with a one-time access code</Button
|
||||
>.
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading || !email || !password}>
|
||||
<Spinner {loading}>Log in</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
+28
-28
@@ -1,24 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {postJson, sleep} from "@welshman/lib"
|
||||
import {Client} from "@pomade/core"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||
import LogInOTPConfirm from "@app/components/LogInOTPConfirm.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {BURROW_URL} from "@app/core/state"
|
||||
|
||||
interface Props {
|
||||
email: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
let {email = $bindable()}: Props = $props()
|
||||
let {email = $bindable("")}: Props = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
@@ -26,16 +26,15 @@
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const [res] = await Promise.all([
|
||||
postJson(BURROW_URL + "/user/request-reset", {email}),
|
||||
sleep(1000),
|
||||
])
|
||||
const {ok} = await Client.requestChallenge(email)
|
||||
|
||||
if (res.error) {
|
||||
pushToast({message: res.error, theme: "error"})
|
||||
if (ok) {
|
||||
pushModal(LogInOTPConfirm, {email})
|
||||
} else {
|
||||
pushToast({message: `Password reset email has been sent!`})
|
||||
pushModal(LogInPassword, {email}, {path: "/"})
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we were unable to request a login code.",
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
@@ -48,30 +47,31 @@
|
||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Reset your password</div>
|
||||
<div>Log In</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Log in using a one-time login code</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<FieldInline disabled={loading}>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Email Address</p>
|
||||
<p>Email*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={UserRounded} />
|
||||
<input bind:value={email} class="grow" />
|
||||
<Icon icon={Letter} />
|
||||
<input bind:value={email} />
|
||||
</label>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>You'll be sent an email with a password reset link.</p>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Request password reset link</Spinner>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading || !email}>
|
||||
<Spinner {loading}>Log in</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import {Client} from "@pomade/core"
|
||||
import {identity} from "@welshman/lib"
|
||||
import {loginWithPomade} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {clearModals} from "@app/util/modal"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type Props = {
|
||||
email: string
|
||||
}
|
||||
|
||||
const {email}: Props = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const onSubmit = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const {ok, options, messages, clientSecret} = await Client.loginWithChallenge(
|
||||
email,
|
||||
challenges,
|
||||
)
|
||||
|
||||
if (!ok) {
|
||||
console.error(messages)
|
||||
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we were unable to log you in.",
|
||||
})
|
||||
}
|
||||
|
||||
const [client, peers] = options[0]!
|
||||
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
|
||||
|
||||
if (res.ok && clientOptions) {
|
||||
loginWithPomade(clientOptions.group.group_pk.slice(2), clientOptions)
|
||||
pushToast({message: "Successfully logged in!"})
|
||||
setChecked("*")
|
||||
clearModals()
|
||||
} else {
|
||||
console.error(res.messages)
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we were unable to log you in.",
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const challenges = $state(["", "", ""])
|
||||
|
||||
let loading = $state(false)
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Log In</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Enter the one-time login code sent to your email</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Login Code #1*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Key} />
|
||||
<input bind:value={challenges[0]} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Login Code #2*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Key} />
|
||||
<input bind:value={challenges[1]} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Login Code #3*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Key} />
|
||||
<input bind:value={challenges[2]} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<p class="text-sm">
|
||||
To keep your key as safe a possible, you will receive <strong>three separate emails</strong>.
|
||||
Be sure to enter all three codes!
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading || !challenges.every(identity)}>
|
||||
<Spinner {loading}>Log In</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -1,156 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
import {postJson, stripProtocol} from "@welshman/lib"
|
||||
import {Nip46Broker} from "@welshman/signer"
|
||||
import {normalizeRelayUrl, makeSecret} from "@welshman/util"
|
||||
import {addSession, makeNip46Session} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
||||
import {clearModals, pushModal} from "@app/util/modal"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {
|
||||
NIP46_PERMS,
|
||||
BURROW_URL,
|
||||
PLATFORM_URL,
|
||||
PLATFORM_NAME,
|
||||
PLATFORM_LOGO,
|
||||
} from "@app/core/state"
|
||||
|
||||
interface Props {
|
||||
email?: string
|
||||
}
|
||||
|
||||
let {email = $bindable("")}: Props = $props()
|
||||
|
||||
const clientSecret = makeSecret()
|
||||
|
||||
const startReset = () => pushModal(PasswordResetRequest, {email})
|
||||
|
||||
const abortController = new AbortController()
|
||||
|
||||
const relays = BURROW_URL.startsWith("http://")
|
||||
? [normalizeRelayUrl("ws://" + stripProtocol(BURROW_URL))]
|
||||
: [normalizeRelayUrl(BURROW_URL)]
|
||||
|
||||
const broker = new Nip46Broker({clientSecret, relays})
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const onSubmit = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const res = await postJson(BURROW_URL + "/session", {email, password, nostrconnect: url})
|
||||
|
||||
if (res.error) {
|
||||
pushToast({message: res.error, theme: "error"})
|
||||
loading = false
|
||||
}
|
||||
} catch (e) {
|
||||
pushToast({message: "Something went wrong, please try again!", theme: "error"})
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let url = ""
|
||||
let password = $state("")
|
||||
let loading = $state(false)
|
||||
|
||||
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.signal)
|
||||
} 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 pubkey = await broker.getPublicKey()
|
||||
const session = makeNip46Session(pubkey, clientSecret, response.event.pubkey, relays)
|
||||
|
||||
addSession({...session, email})
|
||||
broker.cleanup()
|
||||
setChecked("*")
|
||||
clearModals()
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
abortController.abort()
|
||||
})
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Log In</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Log in using your email and password</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Email</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={UserRounded} />
|
||||
<input bind:value={email} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Password</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Key} />
|
||||
<input bind:value={password} type="password" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<p class="text-sm">
|
||||
Your email and password only work to log in to {PLATFORM_NAME}. To use your key on other nostr
|
||||
applications, visit your settings page. <Button class="link" onclick={startReset}
|
||||
>Forgot your password?</Button>
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading || !email || !password}>
|
||||
<Spinner {loading}>Next</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -1,73 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {postJson, sleep} from "@welshman/lib"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {BURROW_URL} from "@app/core/state"
|
||||
|
||||
const {email, reset_token} = $props()
|
||||
|
||||
const onSubmit = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const [res] = await Promise.all([
|
||||
postJson(BURROW_URL + "/user/confirm-reset", {email, password, reset_token}),
|
||||
sleep(1000),
|
||||
])
|
||||
|
||||
if (res.error) {
|
||||
pushToast({message: res.error, theme: "error"})
|
||||
} else {
|
||||
pushToast({message: "Password reset successfully!"})
|
||||
pushModal(LogInPassword, {email}, {path: "/"})
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
let password = $state("")
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Reset your password</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<FieldInline disabled={loading}>
|
||||
{#snippet label()}
|
||||
<p>Email Address</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={UserRounded} />
|
||||
<input readonly value={email} class="grow" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline disabled={loading}>
|
||||
{#snippet label()}
|
||||
<p>New Password</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Key} />
|
||||
<input bind:value={password} class="grow" type="password" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Reset password</Spinner>
|
||||
</Button>
|
||||
</form>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {getProfile} from "@welshman/app"
|
||||
import {loadProfile} from "@welshman/app"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
|
||||
type Props = {
|
||||
@@ -8,14 +8,21 @@
|
||||
}
|
||||
|
||||
const {pubkeys, size = 7}: Props = $props()
|
||||
|
||||
for (const pubkey of pubkeys) {
|
||||
loadProfile(pubkey)
|
||||
}
|
||||
|
||||
const visiblePubkeys = $derived.by(() => {
|
||||
const filtered = pubkeys.filter(pubkey => getProfile(pubkey)?.picture)
|
||||
|
||||
return filtered.length > 0 ? filtered : pubkeys.slice(0, 1)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex pr-3">
|
||||
{#each pubkeys
|
||||
.filter(p => getProfile(p)?.picture)
|
||||
.toSorted()
|
||||
.slice(0, 15) as pubkey (pubkey)}
|
||||
<div class="z-feature -mr-3 inline-block">
|
||||
{#each visiblePubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
|
||||
<div class="z-feature -mr-3 inline-block flex items-center justify-center bg-base-100 rounded-full h-8 w-8">
|
||||
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {size} />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import {postJson} from "@welshman/lib"
|
||||
import {session} from "@welshman/app"
|
||||
import {slideAndFade} from "@lib/transition"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
@@ -12,9 +11,7 @@
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {PLATFORM_NAME, BURROW_URL} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {logout} from "@app/core/commands"
|
||||
import {PLATFORM_NAME} from "@app/core/state"
|
||||
|
||||
const email = $session?.email
|
||||
|
||||
@@ -24,29 +21,13 @@
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const payload = {email, password, eject: true}
|
||||
const res = await postJson(BURROW_URL + "/user", payload, {method: "delete"})
|
||||
|
||||
if (res.error) {
|
||||
return pushToast({message: res.error, theme: "error"})
|
||||
}
|
||||
|
||||
success = true
|
||||
pushToast({message: "Success! Please check your messages and continue when you're ready."})
|
||||
|
||||
await logout()
|
||||
// TODO: Implement export functionality
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
loading = true
|
||||
window.location.href = "/"
|
||||
}
|
||||
|
||||
let password = $state("")
|
||||
let success = $state(false)
|
||||
const success = $state(false)
|
||||
let loading = $state(false)
|
||||
</script>
|
||||
|
||||
@@ -85,7 +66,7 @@
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Key} />
|
||||
<input type="password" disabled={loading} bind:value={password} class="grow" />
|
||||
<input type="password" disabled={loading} class="grow" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</Field>
|
||||
@@ -97,7 +78,7 @@
|
||||
Go back
|
||||
</Button>
|
||||
{#if success}
|
||||
<Button class="btn btn-primary" disabled={loading} onclick={reload}>
|
||||
<Button class="btn btn-primary" disabled={loading}>
|
||||
<Icon icon={CheckCircle} />
|
||||
<Spinner {loading}>Refresh the page</Spinner>
|
||||
</Button>
|
||||
|
||||
@@ -1,95 +1,37 @@
|
||||
<script lang="ts">
|
||||
import {postJson} from "@welshman/lib"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import LogIn from "@app/components/LogIn.svelte"
|
||||
import InfoNostr from "@app/components/InfoNostr.svelte"
|
||||
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
||||
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {BURROW_URL, PLATFORM_NAME} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {POMADE_SIGNERS, PLATFORM_NAME} from "@app/core/state"
|
||||
|
||||
const hasPomade = POMADE_SIGNERS.length >= 3
|
||||
|
||||
const login = () => pushModal(LogIn)
|
||||
|
||||
const signupPassword = async () => {
|
||||
loading = true
|
||||
const useEmail = () => pushModal(SignUpProfile, {flow: "email"})
|
||||
|
||||
try {
|
||||
const res = await postJson(BURROW_URL + "/user", {email, password})
|
||||
|
||||
if (res.error) {
|
||||
pushToast({message: res.error, theme: "error"})
|
||||
} else {
|
||||
pushModal(SignUpSuccess, {email}, {replaceState: true})
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const usePassword = () => {
|
||||
if (BURROW_URL) {
|
||||
signupPassword()
|
||||
}
|
||||
}
|
||||
|
||||
const next = () => pushModal(SignUpProfile)
|
||||
|
||||
let email = $state("")
|
||||
let password = $state("")
|
||||
let loading = $state(false)
|
||||
const useNostr = () => pushModal(SignUpProfile, {flow: "nostr"})
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(usePassword)}>
|
||||
<div class="column gap-4">
|
||||
<h1 class="heading">Sign up with Nostr</h1>
|
||||
<p class="m-auto max-w-sm text-center">
|
||||
{PLATFORM_NAME} is built using the
|
||||
<Button class="link" onclick={() => pushModal(InfoNostr)}>nostr protocol</Button>, which gives
|
||||
users control over their digital identity using <strong>cryptographic key pairs</strong>.
|
||||
</p>
|
||||
{#if BURROW_URL}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Email</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={UserRounded} />
|
||||
<input bind:value={email} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Password</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Key} />
|
||||
<input bind:value={password} type="password" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading || !email || !password}>
|
||||
<Spinner {loading}>Sign Up</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
{#if hasPomade}
|
||||
<Button onclick={useEmail} class="btn btn-primary">
|
||||
<Icon icon={Letter} />
|
||||
Sign up with email
|
||||
</Button>
|
||||
<p class="text-sm opacity-75">
|
||||
Note that your email and password will only work to log in to {PLATFORM_NAME}. To use your key
|
||||
on other nostr applications, you can create a nostr key yourself, or export your key from {PLATFORM_NAME}
|
||||
later.
|
||||
</p>
|
||||
<Divider>Or</Divider>
|
||||
{/if}
|
||||
<Button onclick={next} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
||||
<Button onclick={useNostr} class="btn {hasPomade ? 'btn-neutral' : 'btn-primary'}">
|
||||
<Icon icon={Key} />
|
||||
Generate a key
|
||||
</Button>
|
||||
@@ -97,4 +39,4 @@
|
||||
Already have an account?
|
||||
<Button class="link" onclick={login}>Log in instead</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
import {Client} from "@pomade/core"
|
||||
import {choice} from "@welshman/lib"
|
||||
import {makeSecret} from "@welshman/util"
|
||||
import type {Profile} from "@welshman/util"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Key from "@assets/icons/key.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
type Props = {
|
||||
profile: Profile
|
||||
}
|
||||
|
||||
const {profile}: Props = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (password.trim().length < 12) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Password must be at least 12 characters long.",
|
||||
})
|
||||
}
|
||||
|
||||
loading = true
|
||||
|
||||
let client: Client | undefined = undefined
|
||||
|
||||
try {
|
||||
const {ok, clientOptions} = await Client.register(2, 3, makeSecret())
|
||||
|
||||
if (!ok) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Failed to register! Please try again.",
|
||||
})
|
||||
}
|
||||
|
||||
client = new Client(clientOptions)
|
||||
|
||||
const setupRes = await client.setupRecovery(email, password)
|
||||
|
||||
if (!setupRes.ok) {
|
||||
const message = setupRes.messages[0]?.payload.message || "Please try again."
|
||||
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: `Failed to register! ${message}.`,
|
||||
})
|
||||
}
|
||||
|
||||
const challengeRes = await Client.requestChallenge(email, [choice(clientOptions.peers)])
|
||||
|
||||
if (!challengeRes.ok) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: `Failed to request confirmation code! Please try again..`,
|
||||
})
|
||||
}
|
||||
|
||||
pushModal(SignUpEmailConfirm, {email, profile, clientOptions})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Failed to register! Please try again.",
|
||||
})
|
||||
} finally {
|
||||
client?.stop()
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let email = $state("")
|
||||
let password = $state("")
|
||||
let loading = $state(false)
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Sign up with Email</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Keep your keys safe using multi-signer key sharing</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<p>
|
||||
Under the hood, nostr uses "cryptographic keypairs" to help you prove that your identity is
|
||||
actually you.
|
||||
</p>
|
||||
<p>
|
||||
If you you're not ready to take control of your keys though, that's ok! We'll keep them safe
|
||||
until you are.
|
||||
</p>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Email*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Letter} />
|
||||
<input bind:value={email} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Password*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Key} />
|
||||
<input type="password" bind:value={password} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button class="btn btn-primary" type="submit" disabled={loading || !email || !password}>
|
||||
<Spinner {loading}>Continue</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import type {ClientOptions} from "@pomade/core"
|
||||
import type {Profile} from "@welshman/util"
|
||||
import {sleep} from "@welshman/lib"
|
||||
import {loginWithPomade} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {clearModals} from "@app/util/modal"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type Props = {
|
||||
email: string
|
||||
profile: Profile
|
||||
clientOptions: ClientOptions
|
||||
}
|
||||
|
||||
const {email, clientOptions}: Props = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const onSubmit = async () => {
|
||||
loading = true
|
||||
|
||||
// Just pretend we're validating, they clearly got a code from somewhere
|
||||
await sleep(800)
|
||||
|
||||
try {
|
||||
loginWithPomade(clientOptions.group.group_pk.slice(2), clientOptions)
|
||||
pushToast({message: "Successfully logged in!"})
|
||||
setChecked("*")
|
||||
clearModals()
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let challenge = $state("")
|
||||
let loading = $state(false)
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Verify your Email Address</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Enter the one-time confirmation code sent to your email</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Confirmation Code*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Key} />
|
||||
<input bind:value={challenge} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<p class="text-sm">
|
||||
We just sent a one-time confirmation code to {email}. Once you receive it, you can enter it above.
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading || !challenge}>
|
||||
<Spinner {loading}>Log In</Spinner>
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -8,8 +8,15 @@
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||
import SignUpKey from "@app/components/SignUpKey.svelte"
|
||||
import SignUpEmail from "@app/components/SignUpEmail.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
type Props = {
|
||||
flow: "nostr" | "email"
|
||||
}
|
||||
|
||||
const {flow}: Props = $props()
|
||||
|
||||
const initialValues = {
|
||||
profile: makeProfile(),
|
||||
shouldBroadcast: false,
|
||||
@@ -17,7 +24,8 @@
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const onsubmit = (values: {profile: Profile}) => pushModal(SignUpKey, values)
|
||||
const onsubmit = (values: {profile: Profile}) =>
|
||||
pushModal(flow === "nostr" ? SignUpKey : SignUpEmail, values)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
const {email} = $props()
|
||||
|
||||
const login = () => pushModal(LogInPassword)
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
<h1 class="heading">Success!</h1>
|
||||
<p class="m-auto max-w-sm text-center">
|
||||
A confirmation email has been sent to {email}.
|
||||
</p>
|
||||
<p>Once you've confirmed your account you'll be redirected to the login page.</p>
|
||||
<Button class="btn btn-primary" onclick={login}>Back to Login</Button>
|
||||
</div>
|
||||
@@ -24,7 +24,7 @@
|
||||
initialValues: RelayProfile
|
||||
}
|
||||
|
||||
const {url, initialValues}: Props = $props()
|
||||
const {url, initialValues = {}}: Props = $props()
|
||||
|
||||
const values = $state(initialValues)
|
||||
|
||||
|
||||
@@ -15,18 +15,14 @@
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const tryJoin = async () => {
|
||||
await addSpaceMembership(url)
|
||||
|
||||
broadcastUserData([url])
|
||||
clearModals()
|
||||
}
|
||||
|
||||
const join = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await tryJoin()
|
||||
await addSpaceMembership(url)
|
||||
|
||||
broadcastUserData([url])
|
||||
clearModals()
|
||||
} catch (e) {
|
||||
loading = false
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import twColors from "tailwindcss/colors"
|
||||
import {context as pomadeContext} from "@pomade/core"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {get, derived, readable, writable} from "svelte/store"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
@@ -163,7 +164,7 @@ export const PLATFORM_DESCRIPTION = import.meta.env.VITE_PLATFORM_DESCRIPTION
|
||||
|
||||
export const DEFAULT_BLOSSOM_SERVERS = fromCsv(import.meta.env.VITE_DEFAULT_BLOSSOM_SERVERS)
|
||||
|
||||
export const BURROW_URL = import.meta.env.VITE_BURROW_URL
|
||||
export const POMADE_SIGNERS = fromCsv(import.meta.env.VITE_POMADE_SIGNERS)
|
||||
|
||||
export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
|
||||
|
||||
@@ -233,6 +234,10 @@ export const deriveRelaySignedEvents = (url: string, filters: Filter[]) =>
|
||||
|
||||
// Context
|
||||
|
||||
pomadeContext.setSignerPubkeys(POMADE_SIGNERS)
|
||||
|
||||
pomadeContext.setArgonWorker(import('@pomade/core/argon-worker.js?worker'))
|
||||
|
||||
appContext.dufflepudUrl = DUFFLEPUD_URL
|
||||
|
||||
routerContext.getIndexerRelays = always(INDEXER_RELAYS)
|
||||
@@ -516,9 +521,9 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
|
||||
}
|
||||
|
||||
for (const event of metaEvents) {
|
||||
const meta = readRoomMeta(event)
|
||||
const meta = tryCatch(() => readRoomMeta(event))
|
||||
|
||||
if (gt(deletedByH.get(meta.h), meta.event.created_at)) {
|
||||
if (!meta || gt(deletedByH.get(meta.h), meta.event.created_at)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user