Use simple OTPs

This commit is contained in:
Jon Staab
2026-01-14 09:23:58 -08:00
parent 5b43c62f2d
commit f3647e9bc1
5 changed files with 50 additions and 64 deletions
+14 -10
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Client, decodeChallenge} from "@pomade/core" import {Client} from "@pomade/core"
import {tryCatch} from "@welshman/lib"
import {getPubkey} from "@welshman/util" import {getPubkey} from "@welshman/util"
import type {SessionPomade} from "@welshman/app" import type {SessionPomade} from "@welshman/app"
import {session} from "@welshman/app" import {session} from "@welshman/app"
@@ -17,25 +16,31 @@
import {pushModal, clearModals} from "@app/util/modal" import {pushModal, clearModals} from "@app/util/modal"
import {POMADE_SIGNERS} from "@app/core/state" import {POMADE_SIGNERS} from "@app/core/state"
type Props = {
peersByPrefix: Map<string, string>
}
const {peersByPrefix}: Props = $props()
const { const {
email, email,
clientOptions: {secret, peers}, clientOptions: {secret, peers},
} = $session as SessionPomade } = $session as SessionPomade
const confirmRecovery = async () => { const confirmRecovery = async () => {
const challenges = input const otps = input
.split(/\n/) .split(/\n/)
.map(x => x.trim()) .map(x => x.trim())
.filter(x => tryCatch(() => decodeChallenge(x))) .filter(x => x.match(/^[0-9]{8}$/))
if (challenges.length < 2) { if (otps.length < 2) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Failed to recover, not enough valid challenges were provided.", message: "Failed to recover, not enough valid recovery codes were provided.",
}) })
} }
const request = await Client.recoverWithChallenge(email, challenges) const request = await Client.recoverWithChallenge(email, peersByPrefix, otps)
if (!request.ok) { if (!request.ok) {
console.log(request.messages) console.log(request.messages)
@@ -88,12 +93,11 @@
<p>Your recovery codes have been sent!</p> <p>Your recovery codes have been sent!</p>
<p> <p>
For security reasons, you may receive three or more emails with recovery codes in them. Please For security reasons, you may receive three or more emails with recovery codes in them. Please
paste <i>all</i> paste <strong>all</strong> recovery codes into the text box below, on separate lines.
recovery codes into the text box below, on separate lines.
</p> </p>
<textarea <textarea
rows={POMADE_SIGNERS.length + 1} rows={POMADE_SIGNERS.length + 1}
class="textarea textarea-bordered text-xs leading-4" class="textarea textarea-bordered leading-4"
bind:value={input}></textarea> bind:value={input}></textarea>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
+3 -2
View File
@@ -19,8 +19,9 @@
} = $session as SessionPomade } = $session as SessionPomade
const requestRecovery = async () => { const requestRecovery = async () => {
await Client.requestChallenge(email, peers) const {peersByPrefix} = await Client.requestChallenge(email, peers)
pushModal(KeyRecoveryConfirm)
pushModal(KeyRecoveryConfirm, {peersByPrefix})
} }
const submit = async () => { const submit = async () => {
+2 -2
View File
@@ -26,10 +26,10 @@
loading = true loading = true
try { try {
const {ok} = await Client.requestChallenge(email) const {ok, peersByPrefix} = await Client.requestChallenge(email)
if (ok) { if (ok) {
pushModal(LogInOTPConfirm, {email}) pushModal(LogInOTPConfirm, {email, peersByPrefix})
} else { } else {
pushToast({ pushToast({
theme: "error", theme: "error",
+28 -45
View File
@@ -1,12 +1,9 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {identity} from "@welshman/lib"
import {loginWithPomade} from "@welshman/app" import {loginWithPomade} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
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 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 AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -15,22 +12,37 @@
import {clearModals} from "@app/util/modal" import {clearModals} from "@app/util/modal"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {POMADE_SIGNERS} from "@app/core/state"
type Props = { type Props = {
email: string email: string
peersByPrefix: Map<string, string>
} }
const {email}: Props = $props() const {email, peersByPrefix}: Props = $props()
const back = () => history.back() const back = () => history.back()
const onSubmit = async () => { const onSubmit = async () => {
const otps = input
.split(/\n/)
.map(x => x.trim())
.filter(x => x.match(/^[0-9]{8}$/))
if (otps.length < 2) {
return pushToast({
theme: "error",
message: "Failed to recover, not enough valid recovery codes were provided.",
})
}
loading = true loading = true
try { try {
const {ok, options, messages, clientSecret} = await Client.loginWithChallenge( const {ok, options, messages, clientSecret} = await Client.loginWithChallenge(
email, email,
challenges, peersByPrefix,
otps,
) )
if (!ok) { if (!ok) {
@@ -63,8 +75,7 @@
} }
} }
const challenges = $state(["", "", ""]) let input = $state("")
let loading = $state(false) let loading = $state(false)
</script> </script>
@@ -74,52 +85,24 @@
<div>Log In</div> <div>Log In</div>
{/snippet} {/snippet}
{#snippet info()} {#snippet info()}
<div>Enter the one-time login code sent to your email</div> <div>Enter the login codes sent to your email</div>
{/snippet} {/snippet}
</ModalHeader> </ModalHeader>
<FieldInline> <p>Your login codes have been sent!</p>
{#snippet label()} <p>
<p>Login Code #1*</p> For security reasons, you may receive three or more emails with login codes in them. Please
{/snippet} paste <strong>all</strong> login codes into the text box below, on separate lines.
{#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> </p>
<textarea
rows={POMADE_SIGNERS.length + 1}
class="textarea textarea-bordered leading-4"
bind:value={input}></textarea>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back} disabled={loading}> <Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading || !challenges.every(identity)}> <Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Log In</Spinner> <Spinner {loading}>Log In</Spinner>
<Icon icon={AltArrowRight} /> <Icon icon={AltArrowRight} />
</Button> </Button>
+3 -5
View File
@@ -39,11 +39,9 @@
let client: Client | undefined = undefined let client: Client | undefined = undefined
try { try {
const userSecret = makeSecret() const {clientOptions, ...registerRes} = await Client.register(2, 3, makeSecret())
console.log(userSecret)
const {ok, clientOptions} = await Client.register(2, 3, userSecret)
if (!ok) { if (!registerRes.ok) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Failed to register! Please try again.", message: "Failed to register! Please try again.",
@@ -68,7 +66,7 @@
if (!challengeRes.ok) { if (!challengeRes.ok) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: `Failed to request confirmation code! Please try again..`, message: `Failed to request confirmation code! Please try again.`,
}) })
} }