mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-21 08:10:48 +08:00
* chore: upgrade dependencies - dompurify 3.3.1 → 3.3.2 - naive-ui 2.43.2 → 2.44.0 - vue-i18n 11.2.8 → 11.3.0 - @cloudflare/workers-types 4.20260305.1 → 4.20260307.1 - @types/node 25.3.3 → 25.3.5 - wrangler 4.70.0 → 4.71.0 (all subprojects) * feat: upgrade @simplewebauthn packages from v10 to v13 Breaking changes addressed: - [v11] startRegistration/startAuthentication now take object param - [v11] registrationInfo.credential replaces flat destructuring - [v11] authenticator param renamed to credential in verifyAuthenticationResponse - [v13] @simplewebauthn/types removed, types imported from @simplewebauthn/server Packages: - @simplewebauthn/server: 10.0.1 → 13.2.3 - @simplewebauthn/browser: 10.0.0 → 13.2.2 - @simplewebauthn/types: removed (deprecated) * test: add passkey API E2E tests - User registration and login flow - register_request/authenticate_request return valid WebAuthn options - authenticate_response with invalid credential returns 404 - register_response with invalid credential returns error - Passkey list empty for new user - Rename/delete operations with validation * fix: use UI login instead of localStorage injection in browser passkey test The localStorage approach doesn't work with VueUse's useStorage because it doesn't detect external changes during page navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: hash password before registration to match frontend login behavior The frontend hashes passwords with SHA-256 before sending to the API. Registration via API must use the same hashed password so that UI login matches the stored value. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: allow crypto.subtle in Docker browser tests The frontend uses crypto.subtle for password hashing, which requires a secure context (HTTPS or localhost). In Docker, the frontend runs at http://frontend:5173 which is not a secure context. Add Chromium flag to treat this origin as secure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: serve frontend over HTTPS in Docker for WebAuthn secure context WebAuthn (navigator.credentials) and crypto.subtle both require a secure context (HTTPS or localhost). The Docker frontend was serving over HTTP, making passkey operations impossible. Changes: - Generate self-signed cert in Dockerfile.frontend - Configure Vite to serve over HTTPS - Update FRONTEND_URL to https:// - Add ignoreHTTPSErrors to Playwright browser config - Use localStorage injection for passkey test login Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add Vite proxy to avoid mixed-content blocking in HTTPS Docker frontend HTTPS pages cannot make HTTP API requests (mixed content). Add a Vite proxy for all API paths so the browser makes same-origin HTTPS requests, which Vite proxies to the HTTP worker server-to-server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: store userJwt without JSON.stringify in localStorage VueUse's useStorage with a string default uses raw string serialization (no JSON wrapping). Using JSON.stringify added double quotes around the JWT token, causing 401 Unauthorized from the worker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: clean up passkey API test per review feedback Remove unused variables and rename test to match actual behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
115 lines
4.5 KiB
TypeScript
115 lines
4.5 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { request as apiRequest } from '@playwright/test';
|
|
import { createHash } from 'crypto';
|
|
import { WORKER_URL, FRONTEND_URL } from '../../fixtures/test-helpers';
|
|
|
|
const TEST_USER_EMAIL = `passkey-browser-${Date.now()}@test.example.com`;
|
|
const TEST_USER_PASSWORD = 'browser-test-pwd-123';
|
|
|
|
// Frontend hashes passwords with SHA-256 before sending to the API.
|
|
// Register with the hashed password so UI login matches.
|
|
const HASHED_PASSWORD = createHash('sha256').update(TEST_USER_PASSWORD).digest('hex');
|
|
|
|
test.describe('Passkey Browser Flow', () => {
|
|
let userJwt: string;
|
|
|
|
test.beforeAll(async () => {
|
|
const api = await apiRequest.newContext();
|
|
try {
|
|
// Enable user registration
|
|
await api.post(`${WORKER_URL}/admin/user_settings`, {
|
|
data: { enable: true, enableMailVerify: false },
|
|
});
|
|
// Register user with hashed password (matching frontend behavior)
|
|
await api.post(`${WORKER_URL}/user_api/register`, {
|
|
data: { email: TEST_USER_EMAIL, password: HASHED_PASSWORD },
|
|
});
|
|
// Login to get JWT for localStorage injection
|
|
const loginRes = await api.post(`${WORKER_URL}/user_api/login`, {
|
|
data: { email: TEST_USER_EMAIL, password: HASHED_PASSWORD },
|
|
});
|
|
const body = await loginRes.json();
|
|
userJwt = body.jwt;
|
|
} finally {
|
|
await api.dispose();
|
|
}
|
|
});
|
|
|
|
test('register passkey, then login with passkey', async ({ page, context }) => {
|
|
// Set up virtual authenticator via CDP
|
|
const cdp = await context.newCDPSession(page);
|
|
await cdp.send('WebAuthn.enable');
|
|
const { authenticatorId } = await cdp.send('WebAuthn.addVirtualAuthenticator', {
|
|
options: {
|
|
protocol: 'ctap2',
|
|
transport: 'internal',
|
|
hasResidentKey: true,
|
|
hasUserVerification: true,
|
|
isUserVerified: true,
|
|
},
|
|
});
|
|
|
|
try {
|
|
// === Step 1: Login via localStorage injection ===
|
|
// Inject JWT into localStorage to skip UI login flow.
|
|
await page.goto(`${FRONTEND_URL}/en/`);
|
|
// VueUse's useStorage with string default stores raw strings (no JSON)
|
|
await page.evaluate((jwt) => {
|
|
localStorage.setItem('userJwt', jwt);
|
|
}, userJwt);
|
|
await page.goto(`${FRONTEND_URL}/en/user`);
|
|
|
|
// Wait for user settings to load (shows user email)
|
|
await expect(page.getByText(TEST_USER_EMAIL)).toBeVisible({ timeout: 15_000 });
|
|
|
|
// === Step 2: Click "User Settings" tab ===
|
|
await page.getByText('User Settings').click();
|
|
|
|
// === Step 3: Create a passkey ===
|
|
await page.getByRole('button', { name: 'Create Passkey' }).click();
|
|
|
|
// Fill passkey name in the modal
|
|
const createModal = page.locator('.n-dialog');
|
|
await expect(createModal).toBeVisible({ timeout: 5_000 });
|
|
await createModal.getByRole('textbox').fill('E2E Test Passkey');
|
|
|
|
// Click the Create Passkey button inside the modal
|
|
await createModal.getByRole('button', { name: 'Create Passkey' }).click();
|
|
|
|
// Wait for success — modal should close
|
|
await expect(createModal).not.toBeVisible({ timeout: 10_000 });
|
|
|
|
// === Step 4: Verify passkey appears in the list ===
|
|
await page.getByRole('button', { name: 'Show Passkey List' }).click();
|
|
|
|
const listModal = page.locator('.n-card-header:has-text("Show Passkey List")').locator('..');
|
|
await expect(page.getByText('E2E Test Passkey')).toBeVisible({ timeout: 5_000 });
|
|
|
|
// Close the list modal
|
|
await page.keyboard.press('Escape');
|
|
|
|
// === Step 5: Logout ===
|
|
await page.getByRole('button', { name: 'Logout' }).click();
|
|
const logoutModal = page.locator('.n-dialog');
|
|
await expect(logoutModal).toBeVisible({ timeout: 5_000 });
|
|
await logoutModal.getByRole('button', { name: 'Logout' }).click();
|
|
|
|
// Wait for logout to complete and navigate to user page
|
|
await page.waitForTimeout(2000);
|
|
await page.goto(`${FRONTEND_URL}/en/user`);
|
|
|
|
// === Step 6: Login with passkey ===
|
|
const passkeyBtn = page.getByRole('button', { name: 'Login with Passkey' });
|
|
await expect(passkeyBtn).toBeVisible({ timeout: 10_000 });
|
|
await passkeyBtn.click();
|
|
|
|
// Virtual authenticator handles the WebAuthn ceremony automatically
|
|
// Wait for login to complete — user email should appear
|
|
await expect(page.getByText(TEST_USER_EMAIL)).toBeVisible({ timeout: 15_000 });
|
|
} finally {
|
|
await cdp.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
|
|
await cdp.detach();
|
|
}
|
|
});
|
|
});
|