diff options
| author | Adam Soltys <[email protected]> | 2026-02-27 15:24:13 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-02-27 15:24:13 -0800 |
| commit | 16da2cb00d16f87d02535bfdae815f83f19f251f (patch) | |
| tree | 2e6d003975d5a7a67cc81243da4a59bc476fff48 | |
| parent | 11fbf552ebbb9d1d5ae6823893ee80ae23f450a0 (diff) | |
| download | coinos-ui-16da2cb00d16f87d02535bfdae815f83f19f251f.tar.gz coinos-ui-16da2cb00d16f87d02535bfdae815f83f19f251f.zip | |
fix race condition
| -rw-r--r-- | src/components/Account.svelte | 24 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.server.ts | 51 | ||||
| -rw-r--r-- | src/routes/(static)/register/auth/+page.svelte | 4 | ||||
| -rw-r--r-- | test/e2e/register-ark-receive.spec.ts | 53 |
4 files changed, 68 insertions, 64 deletions
diff --git a/src/components/Account.svelte b/src/components/Account.svelte index 45ffdd8b..ff9218fe 100644 --- a/src/components/Account.svelte +++ b/src/components/Account.svelte @@ -18,8 +18,8 @@ let arkBalance = $state(0); let arkLoading = $state(false); - let tryUnlockArk = async () => { - const deriveArkKey = async (key: ArrayBuffer) => { + let tryUnlockArk = async (): Promise<string | false> => { + const deriveArkKey = async (key: ArrayBuffer): Promise<string> => { const [{ HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = await Promise.all([ import("@scure/bip32"), import("@scure/bip39"), @@ -36,8 +36,7 @@ const s = await mnemonicToSeed(mnemonicStr); const master = HDKey.fromMasterSeed(s, versions); const arkChild = master.derive("m/86'/0'/0'/0/0"); - $arkkey = bytesToHex(arkChild.privateKey!); - $arkaid = id; + return bytesToHex(arkChild.privateKey!); }; // Try cached PRF key or silent nsec @@ -45,8 +44,7 @@ const entropy = await getWalletEntropy(); if (entropy) { try { - await deriveArkKey(entropy); - return true; + return await deriveArkKey(entropy); } catch (e) { console.log("Wallet entropy unlock failed", e); } @@ -55,8 +53,7 @@ // Try interactive nostr derivation try { const interactiveEntropy = await deriveNostrEntropy(); - await deriveArkKey(interactiveEntropy); - return true; + return await deriveArkKey(interactiveEntropy); } catch (e) { console.log("Interactive nostr unlock failed", e); return false; @@ -69,9 +66,14 @@ document.cookie = `aid=${id}; path=/; max-age=86400`; if (isArk) { if ($arkkey && $arkaid === id) { - goto(url); - } else if (await tryUnlockArk()) { - goto(url); + await goto(url); + } else { + const key = await tryUnlockArk(); + if (key) { + await goto(url); + $arkkey = key; + $arkaid = id; + } } } else { $arkkey = undefined; diff --git a/src/routes/(static)/login/+page.server.ts b/src/routes/(static)/login/+page.server.ts index d402fdad..68143d83 100644 --- a/src/routes/(static)/login/+page.server.ts +++ b/src/routes/(static)/login/+page.server.ts @@ -97,57 +97,6 @@ export const actions = { }; }, - passwordAuth: async ({ cookies, fetch, request }) => { - const form = await fd(request); - let { event, challenge, username, loginRedirect, recaptcha, token: twofa } = form; - if (loginRedirect === "null" || loginRedirect === "undefined") loginRedirect = undefined; - event = JSON.parse(event); - - const maxAge = 380 * 24 * 60 * 60; - - const res = await fetch(`${PUBLIC_COINOS_URL}/authKeyLogin`, { - method: "POST", - body: JSON.stringify({ event, challenge, username, recaptcha, twofa }), - headers: { - "content-type": "application/json", - accept: "application/json", - "cf-connecting-ip": request.headers.get("cf-connecting-ip") ?? "", - "x-forwarded-host": request.headers.get("host") ?? "", - }, - }); - - const text = await res.text(); - - if (res.status === 401) { - if (text.startsWith("2fa")) throw new Error("2fa"); - throw new Error(text); - } - - if (!res.ok) { - let message; - try { - ({ message } = JSON.parse(text)); - } catch { - message = text; - } - return fail(400, { error: message || "Login failed" }); - } - - const { user, token } = JSON.parse(text); - if (!token) return fail(400, { error: "Login failed" }); - const { username: uname, language } = user; - - const expires = new Date(); - expires.setSeconds(expires.getSeconds() + maxAge); - - const opts = { path: "/", expires }; - if (language) cookies.set("lang", language, opts); - cookies.set("username", uname, opts); - cookies.set("token", token, opts); - - redirect(307, loginRedirect || `/${uname}`); - }, - passkey: async ({ cookies, fetch, request }) => { const form = await fd(request); let { credential, challengeId, loginRedirect } = form; diff --git a/src/routes/(static)/register/auth/+page.svelte b/src/routes/(static)/register/auth/+page.svelte index d285e0af..ad5d6e54 100644 --- a/src/routes/(static)/register/auth/+page.svelte +++ b/src/routes/(static)/register/auth/+page.svelte @@ -71,7 +71,7 @@ const getRecaptchaToken = () => new Promise((resolve, reject) => { - if (isTor) return resolve(""); + if (isTor || !recaptchaSiteKey) return resolve(""); if (!browser || !grecaptcha) return reject(new Error("captcha unavailable")); grecaptcha.ready(() => { grecaptcha.execute(recaptchaSiteKey, { action: "register" }).then(resolve).catch(reject); @@ -144,7 +144,7 @@ const result = deserialize(await response.text()); - if (result.type === "success") { + if (result.type === "success" || result.type === "redirect") { try { const { deriveAuthKeypair } = await import("$lib/deriveAuthKey"); const { sk } = await deriveAuthKeypair(user.username, user.password); diff --git a/test/e2e/register-ark-receive.spec.ts b/test/e2e/register-ark-receive.spec.ts new file mode 100644 index 00000000..abfc64c0 --- /dev/null +++ b/test/e2e/register-ark-receive.spec.ts @@ -0,0 +1,53 @@ +import { expect, test } from "@playwright/test"; + +/** + * Registers a brand-new user via the two-step register flow, + * creates an Ark vault, then clicks Receive on the Ark card + * and verifies navigation to /invoice. + */ +test("register new user, create ark vault, click Receive", async ({ page }) => { + test.setTimeout(60_000); + + const username = `test${Date.now()}`; + const password = "testpass123"; + + // ── Step 1: /register — pick username ── + await page.goto("/register"); + await page.waitForLoadState("networkidle"); + + const usernameInput = page.locator('input[name="username"]'); + await expect(usernameInput).toBeVisible({ timeout: 5_000 }); + await usernameInput.fill(username); + + const continueBtn = page.locator("button.btn-accent", { hasText: "Continue" }); + await continueBtn.click(); + + await page.waitForURL(/\/register\/auth\?/, { timeout: 10_000 }); + + // ── Step 2: /register/auth — register with password ── + const passwordInput = page.locator('input[name="password"]'); + await expect(passwordInput).toBeVisible({ timeout: 5_000 }); + await passwordInput.fill(password); + + const registerBtn = page.locator("button.btn-accent[type='submit']"); + await registerBtn.click(); + + await page.waitForURL(new RegExp(`/${username}(?:[/?#]|$)`), { timeout: 30_000 }); + + // ── Step 3: Create Ark vault ── + await page.goto("/account/ark"); + await page.waitForURL((u) => !u.pathname.includes("/account/ark"), { timeout: 30_000 }); + + // ── Step 4: Click Receive on Ark card ── + await page.goto(`/${username}`); + await page.waitForLoadState("networkidle"); + + const arkCard = page.locator('[data-testid="account-card"][data-account-type="ark"]'); + await expect(arkCard.first()).toBeVisible({ timeout: 10_000 }); + + const receiveBtn = arkCard.first().getByTestId("account-receive"); + await receiveBtn.click(); + + await page.waitForURL(/\/invoice\/[^/?#]+/, { timeout: 15_000 }); + expect(page.url()).toContain("/invoice/"); +}); |
