The Turnstile challenge failed reliably inside the Capacitor WebView, which blocked login and registration from the Android app. Three separate causes: 1. Android WebView blocks third-party cookies by default. Turnstile runs in a cross-origin iframe from challenges.cloudflare.com and needs its own storage, so the widget never emitted a token. MainActivity now calls setAcceptThirdPartyCookies on the app's own WebView. 2. The register handler read the Turnstile response with an unscoped document.querySelector, which matched the *login* widget's input (it comes first in the DOM). Registration therefore submitted the login widget's token — single-use with a 5 minute expiry, so any prior login attempt or slow signup made it fail server-side. 3. The register and forgot-password widgets auto-rendered inside forms that start at display:none, where Turnstile does not reliably complete a challenge, and nothing re-rendered them when the form was shown. Widgets are now rendered explicitly when their form first becomes visible, and tokens are captured from the render callback instead of being read back out of the injected input — which makes the unscoped lookup in (2) structurally impossible. Added error/expired/timeout callbacks so a widget failure surfaces the Cloudflare error code instead of failing silently behind a generic toast. Login is no longer gated at all. It is the path mobile users hit constantly, and it is already covered by a 10-per-15-min per-IP rate limit, a constant-time credential check, and TOTP 2FA. Registration and password reset — the endpoints that actually attract bots — stay gated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
3.9 KiB
JavaScript
87 lines
3.9 KiB
JavaScript
// ============================================================
|
|
// SESSION PERSISTENCE — full logout → login → still on the same
|
|
// tab + same sub-pill.
|
|
//
|
|
// The test does a programmatic logout (clear the ped_auth cookie,
|
|
// same effect server-side as clicking Logout) followed by a fresh
|
|
// programmatic login. This exercises the same localStorage
|
|
// persistence path a real logout/login would.
|
|
//
|
|
// (Historically this was a workaround for the Turnstile challenge on
|
|
// the login form, which could not be completed in the e2e container.
|
|
// Login is no longer gated, but driving it programmatically keeps
|
|
// the test focused on persistence rather than form mechanics.)
|
|
// ============================================================
|
|
|
|
const { test, expect, E2E_BASE, loginAs } = require('../fixtures');
|
|
|
|
async function openDesktopTab(page, name) {
|
|
const vp = page.viewportSize();
|
|
if (vp && vp.width <= 768) {
|
|
await page.click('#btn-menu-toggle').catch(() => {});
|
|
}
|
|
await page.click(`button.tab-btn[data-tab="${name}"]`);
|
|
await page.waitForFunction((t) => {
|
|
const el = document.getElementById(t + '-tab');
|
|
return el && el.classList.contains('active') && el.innerHTML.trim().length > 100;
|
|
}, name, { timeout: 15000 });
|
|
}
|
|
|
|
async function logoutClearsCookie(context) {
|
|
// Remove the ped_auth cookie — identical server-side to hitting /logout.
|
|
const cookies = await context.cookies();
|
|
const keep = cookies.filter(c => c.name !== 'ped_auth');
|
|
await context.clearCookies();
|
|
if (keep.length) await context.addCookies(keep);
|
|
}
|
|
|
|
test.describe('Logout → login restores last tab + sub-pill', () => {
|
|
|
|
test('tab choice + calc pill survive a full logout/login cycle', async ({ context, request, page }) => {
|
|
await loginAs(context, request);
|
|
await page.goto(E2E_BASE + '/');
|
|
await page.waitForSelector('button.tab-btn', { timeout: 15000 });
|
|
await openDesktopTab(page, 'calculators');
|
|
await page.click('button.calc-nav-pill[data-calc="bili"]');
|
|
await expect(page.locator('button.calc-nav-pill[data-calc="bili"].active')).toBeVisible();
|
|
|
|
// Simulate logout (clear session cookie)
|
|
await logoutClearsCookie(context);
|
|
// Visiting the app with no cookie lands on the auth screen
|
|
await page.goto(E2E_BASE + '/');
|
|
await expect(page.locator('#auth-screen')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Log back in (fresh cookie) and revisit the app
|
|
await loginAs(context, request);
|
|
await page.goto(E2E_BASE + '/');
|
|
await page.waitForSelector('button.tab-btn', { timeout: 15000 });
|
|
|
|
// Restore must put us back on Calculators / Bilirubin pill
|
|
await expect(page.locator('#calculators-tab.active')).toHaveCount(1, { timeout: 10000 });
|
|
await expect(page.locator('button.calc-nav-pill[data-calc="bili"].active')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('#calc-bili')).not.toHaveClass(/hidden/);
|
|
});
|
|
|
|
test('bedside sub-pill survives logout/login', async ({ context, request, page }) => {
|
|
await loginAs(context, request);
|
|
await page.goto(E2E_BASE + '/');
|
|
await page.waitForSelector('button.tab-btn', { timeout: 15000 });
|
|
await openDesktopTab(page, 'bedside');
|
|
await page.click('button.calc-pill[data-em="sepsis"]');
|
|
await expect(page.locator('button.calc-pill[data-em="sepsis"].active')).toBeVisible();
|
|
|
|
await logoutClearsCookie(context);
|
|
await page.goto(E2E_BASE + '/');
|
|
await expect(page.locator('#auth-screen')).toBeVisible({ timeout: 10000 });
|
|
|
|
await loginAs(context, request);
|
|
await page.goto(E2E_BASE + '/');
|
|
await page.waitForSelector('button.tab-btn', { timeout: 15000 });
|
|
|
|
await expect(page.locator('#bedside-tab.active')).toHaveCount(1, { timeout: 10000 });
|
|
await expect(page.locator('button.calc-pill[data-em="sepsis"].active')).toBeVisible({ timeout: 10000 });
|
|
await expect.poll(async () =>
|
|
await page.locator('#em-sepsis').evaluate(el => el.style.display),
|
|
{ timeout: 5000 }).not.toBe('none');
|
|
});
|
|
});
|