add passkey support
ci/woodpecker/push/e2e Pipeline failed
ci/woodpecker/push/fmt Pipeline was successful
ci/woodpecker/push/test Pipeline was successful

This commit is contained in:
2026-08-02 16:46:52 -04:00
parent 04c9724c76
commit fdd3e8c5da
18 changed files with 1073 additions and 28 deletions
+4 -1
View File
@@ -42,7 +42,7 @@ async function startServer() {
.slice(2)}.db`,
);
const port = 20000 + Math.floor(Math.random() * 30000);
const baseURL = `http://127.0.0.1:${port}`;
const baseURL = `http://localhost:${port}`;
const child = spawn(
path.resolve(__dirname, "..", "target", "debug", "sustenance"),
@@ -53,6 +53,9 @@ async function startServer() {
DATABASE_PATH: dbPath,
REGISTRATION_MODE: "open",
BIND_ADDRESS: `127.0.0.1:${port}`,
PUBLIC_BASE_URL: baseURL,
// WebAuthn requires a valid domain for the RP ID; localhost is allowed.
RP_ID: "localhost",
// Point SEED_CONFIG at a nonexistent file so no default user is created.
SEED_CONFIG: path.join(os.tmpdir(), "sustenance-e2e-no-seed.json"),
},
+17 -1
View File
@@ -8,7 +8,8 @@
"name": "sustenance-e2e",
"version": "1.0.0",
"devDependencies": {
"@playwright/test": "^1.45.0"
"@playwright/test": "^1.45.0",
"@types/node": "^26.1.2"
}
},
"node_modules/@playwright/test": {
@@ -26,6 +27,15 @@
"node": ">=20"
}
},
"node_modules/@types/node": {
"version": "26.1.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
"integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
"dev": true,
"dependencies": {
"undici-types": "~8.3.0"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@@ -69,6 +79,12 @@
"engines": {
"node": ">=20"
}
},
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"dev": true
}
}
}
+2 -1
View File
@@ -7,6 +7,7 @@
"test:headed": "playwright test --headed"
},
"devDependencies": {
"@playwright/test": "^1.45.0"
"@playwright/test": "^1.45.0",
"@types/node": "^26.1.2"
}
}
+47
View File
@@ -0,0 +1,47 @@
import { expect } from "@playwright/test";
import { test } from "../fixtures";
import { registerAndLogin } from "../helpers";
/**
* Enables a virtual WebAuthn authenticator on the given context so the browser
* can complete passkey ceremonies without a real device.
*/
async function enableVirtualAuthenticator(context: any) {
const cdp = await context.newCDPSession(context.pages()[0]);
await cdp.send("WebAuthn.enable", { enableUI: false });
await cdp.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "internal",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
});
}
test("a user can register a passkey and sign in with it", async ({ page, browser, server }) => {
const context = await browser.newContext({ baseURL: server.baseURL });
const p = await context.newPage();
await enableVirtualAuthenticator(context);
// Register with a password first.
await registerAndLogin(p, "alice@example.com");
// Add a passkey from the account page.
await p.goto("/account");
await p.click("#add-passkey");
await expect(p.locator(".passkey-row")).toHaveCount(1);
// Log out.
await p.click('button:has-text("Sign out")');
await expect(p).toHaveURL(/\/login/);
// Sign in with the passkey.
await p.fill("#email", "alice@example.com");
await p.click("#passkey-login");
await expect(p).toHaveURL(/\/lists/);
await context.close();
});