aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-27 15:24:13 -0800
committerAdam Soltys <[email protected]>2026-02-27 15:24:13 -0800
commit16da2cb00d16f87d02535bfdae815f83f19f251f (patch)
tree2e6d003975d5a7a67cc81243da4a59bc476fff48
parent11fbf552ebbb9d1d5ae6823893ee80ae23f450a0 (diff)
downloadcoinos-ui-16da2cb00d16f87d02535bfdae815f83f19f251f.tar.gz
coinos-ui-16da2cb00d16f87d02535bfdae815f83f19f251f.zip
fix race condition
-rw-r--r--src/components/Account.svelte24
-rw-r--r--src/routes/(static)/login/+page.server.ts51
-rw-r--r--src/routes/(static)/register/auth/+page.svelte4
-rw-r--r--test/e2e/register-ark-receive.spec.ts53
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/");
+});