fix: harden protected login and browser accessibility
This commit is contained in:
parent
63bf7fad3b
commit
2372947430
|
|
@ -30,7 +30,7 @@
|
||||||
--color-base-100: oklch(30.33% 0.016 252.42);
|
--color-base-100: oklch(30.33% 0.016 252.42);
|
||||||
--color-base-200: oklch(25.26% 0.014 253.1);
|
--color-base-200: oklch(25.26% 0.014 253.1);
|
||||||
--color-base-300: oklch(20.15% 0.012 254.09);
|
--color-base-300: oklch(20.15% 0.012 254.09);
|
||||||
--color-base-content: #f3f7ff;
|
--color-base-content: #ffffff;
|
||||||
--color-primary: #a5b4fc;
|
--color-primary: #a5b4fc;
|
||||||
--color-primary-content: #18162a;
|
--color-primary-content: #18162a;
|
||||||
--color-secondary: #a5b4fc;
|
--color-secondary: #a5b4fc;
|
||||||
|
|
@ -128,6 +128,21 @@
|
||||||
color: var(--color-base-content) !important;
|
color: var(--color-base-content) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .btn-ghost,
|
||||||
|
[data-theme="dark"] .btn-outline {
|
||||||
|
background-color: var(--color-base-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .btn-soft.btn-primary {
|
||||||
|
color: var(--color-primary-content);
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-active {
|
||||||
|
color: var(--color-neutral-content) !important;
|
||||||
|
background-color: var(--color-neutral) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.aid-map {
|
.aid-map {
|
||||||
min-height: 22rem;
|
min-height: 22rem;
|
||||||
border-radius: 1.25rem;
|
border-radius: 1.25rem;
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ const activateProtectedTokenFragment = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
activateProtectedTokenFragment()
|
activateProtectedTokenFragment()
|
||||||
|
window.addEventListener("hashchange", activateProtectedTokenFragment)
|
||||||
|
|
||||||
const liveSocket = new LiveSocket("/live", Socket, {
|
const liveSocket = new LiveSocket("/live", Socket, {
|
||||||
longPollFallbackMs: 2500,
|
longPollFallbackMs: 2500,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ test("browser and Android share chat and live tracking state", async ({ browser
|
||||||
await expect(requester.page.getByText(androidMessage, { exact: true })).toBeVisible();
|
await expect(requester.page.getByText(androidMessage, { exact: true })).toBeVisible();
|
||||||
|
|
||||||
if (phase === "observe_android") {
|
if (phase === "observe_android") {
|
||||||
await expect.poll(() => markerKinds(requester.page)).toContain("Shared live location");
|
await expect.poll(() => markerKinds(requester.page)).toContain("Shared location");
|
||||||
await requester.page
|
await requester.page
|
||||||
.getByPlaceholder("Write a safe coordination message…")
|
.getByPlaceholder("Write a safe coordination message…")
|
||||||
.fill(browserReply);
|
.fill(browserReply);
|
||||||
|
|
@ -32,7 +32,7 @@ test("browser and Android share chat and live tracking state", async ({ browser
|
||||||
await expect(requester.page.getByText(browserReply, { exact: true })).toBeVisible();
|
await expect(requester.page.getByText(browserReply, { exact: true })).toBeVisible();
|
||||||
} else if (phase === "observe_stopped") {
|
} else if (phase === "observe_stopped") {
|
||||||
await expect(requester.page.getByText(browserReply, { exact: true })).toBeVisible();
|
await expect(requester.page.getByText(browserReply, { exact: true })).toBeVisible();
|
||||||
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared live location");
|
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared location");
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported CROSS_CLIENT_PHASE: ${phase}`);
|
throw new Error(`Unsupported CROSS_CLIENT_PHASE: ${phase}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
const CACHE = "who-need-help-static-v3";
|
const CACHE_PREFIX = "who-need-help-static-";
|
||||||
const STALE_CACHE = "who-need-help-static-v0";
|
const STALE_CACHE = "who-need-help-static-v0";
|
||||||
const SHELL_PATHS = [
|
const SHELL_PATHS = [
|
||||||
"/offline.html",
|
"/offline.html",
|
||||||
|
|
@ -45,6 +45,21 @@ test("public HTTPS PWA installs, updates its cache, and falls back safely offlin
|
||||||
expect(manifestResponse.status()).toBe(200);
|
expect(manifestResponse.status()).toBe(200);
|
||||||
expect(manifestResponse.headers()["content-type"]).toContain("application/manifest+json");
|
expect(manifestResponse.headers()["content-type"]).toContain("application/manifest+json");
|
||||||
|
|
||||||
|
const serviceWorkerResponse = await request.get("/sw.js");
|
||||||
|
expect(serviceWorkerResponse.status()).toBe(200);
|
||||||
|
expect(serviceWorkerResponse.headers()["content-type"]).toContain("javascript");
|
||||||
|
const serviceWorker = await serviceWorkerResponse.text();
|
||||||
|
const cacheVersionMatch = serviceWorker.match(
|
||||||
|
/const CACHE = `\$\{CACHE_PREFIX\}(v[0-9]+)`/,
|
||||||
|
);
|
||||||
|
expect(cacheVersionMatch).not.toBeNull();
|
||||||
|
|
||||||
|
if (!cacheVersionMatch) {
|
||||||
|
throw new Error("The service worker does not declare a versioned static cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheName = `${CACHE_PREFIX}${cacheVersionMatch[1]}`;
|
||||||
|
|
||||||
const manifest = (await manifestResponse.json()) as {
|
const manifest = (await manifestResponse.json()) as {
|
||||||
name: string;
|
name: string;
|
||||||
short_name: string;
|
short_name: string;
|
||||||
|
|
@ -102,12 +117,12 @@ test("public HTTPS PWA installs, updates its cache, and falls back safely offlin
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
.poll(() => page.evaluate(() => caches.keys()))
|
.poll(() => page.evaluate(() => caches.keys()))
|
||||||
.toContain(CACHE);
|
.toContain(cacheName);
|
||||||
|
|
||||||
const cachedShell = await page.evaluate(async (cacheName) => {
|
const cachedShell = await page.evaluate(async (activeCacheName) => {
|
||||||
const cache = await caches.open(cacheName);
|
const cache = await caches.open(activeCacheName);
|
||||||
return (await cache.keys()).map((entry) => new URL(entry.url).pathname).sort();
|
return (await cache.keys()).map((entry) => new URL(entry.url).pathname).sort();
|
||||||
}, CACHE);
|
}, cacheName);
|
||||||
expect(cachedShell).toEqual(expect.arrayContaining(SHELL_PATHS));
|
expect(cachedShell).toEqual(expect.arrayContaining(SHELL_PATHS));
|
||||||
|
|
||||||
await page.goto("/safety");
|
await page.goto("/safety");
|
||||||
|
|
@ -146,7 +161,7 @@ test("public HTTPS PWA installs, updates its cache, and falls back safely offlin
|
||||||
await expect
|
await expect
|
||||||
.poll(() => updatedPage.evaluate(() => caches.keys()))
|
.poll(() => updatedPage.evaluate(() => caches.keys()))
|
||||||
.not.toContain(STALE_CACHE);
|
.not.toContain(STALE_CACHE);
|
||||||
expect(await updatedPage.evaluate(() => caches.keys())).toContain(CACHE);
|
expect(await updatedPage.evaluate(() => caches.keys())).toContain(cacheName);
|
||||||
|
|
||||||
await context.setOffline(true);
|
await context.setOffline(true);
|
||||||
const offlineResponse = await updatedPage.goto("/requests");
|
const offlineResponse = await updatedPage.goto("/requests");
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ test("active chat and browser tracking recover after the serving BEAM node resta
|
||||||
await expect(helper.page.getByRole("button", { name: "Stop and delete position" })).toBeVisible();
|
await expect(helper.page.getByRole("button", { name: "Stop and delete position" })).toBeVisible();
|
||||||
await expect.poll(() => positionCount(helper.page)).toBe(1);
|
await expect.poll(() => positionCount(helper.page)).toBe(1);
|
||||||
await expect.poll(() => positionCount(requester.page)).toBe(1);
|
await expect.poll(() => positionCount(requester.page)).toBe(1);
|
||||||
await expect.poll(() => markerKinds(requester.page)).toContain("Shared live location");
|
await expect.poll(() => markerKinds(requester.page)).toContain("Shared location");
|
||||||
|
|
||||||
await helper.page
|
await helper.page
|
||||||
.getByPlaceholder("Write a safe coordination message…")
|
.getByPlaceholder("Write a safe coordination message…")
|
||||||
|
|
@ -173,7 +173,7 @@ test("active chat and browser tracking recover after the serving BEAM node resta
|
||||||
await expect.poll(() => positionCount(helper.page), { timeout: 30_000 }).toBe(1);
|
await expect.poll(() => positionCount(helper.page), { timeout: 30_000 }).toBe(1);
|
||||||
await expect.poll(() => positionCount(requester.page), { timeout: 30_000 }).toBe(1);
|
await expect.poll(() => positionCount(requester.page), { timeout: 30_000 }).toBe(1);
|
||||||
await expect.poll(() => markerKinds(requester.page), { timeout: 30_000 }).toContain(
|
await expect.poll(() => markerKinds(requester.page), { timeout: 30_000 }).toContain(
|
||||||
"Shared live location",
|
"Shared location",
|
||||||
);
|
);
|
||||||
|
|
||||||
await helper.page
|
await helper.page
|
||||||
|
|
@ -188,7 +188,7 @@ test("active chat and browser tracking recover after the serving BEAM node resta
|
||||||
await expect(helper.page.getByRole("button", { name: "Share location" })).toBeVisible();
|
await expect(helper.page.getByRole("button", { name: "Share location" })).toBeVisible();
|
||||||
await expect.poll(() => positionCount(helper.page)).toBe(0);
|
await expect.poll(() => positionCount(helper.page)).toBe(0);
|
||||||
await expect.poll(() => positionCount(requester.page)).toBe(0);
|
await expect.poll(() => positionCount(requester.page)).toBe(0);
|
||||||
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared live location");
|
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared location");
|
||||||
|
|
||||||
await testInfo.attach("active-failover-evidence", {
|
await testInfo.attach("active-failover-evidence", {
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,13 @@ export async function registerAndConfirm(
|
||||||
.check();
|
.check();
|
||||||
await page.getByRole("button", { name: "Create an account" }).click();
|
await page.getByRole("button", { name: "Create an account" }).click();
|
||||||
await expect(page).toHaveURL(/\/users\/log-in$/);
|
await expect(page).toHaveURL(/\/users\/log-in$/);
|
||||||
await expect(page.getByText(`An email was sent to ${email}`)).toBeVisible();
|
await expect(
|
||||||
|
page
|
||||||
|
.getByRole("alert")
|
||||||
|
.getByText(
|
||||||
|
"If this address can be registered or signed in, login instructions will arrive shortly.",
|
||||||
|
),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await gotoWithTransientRetry(page, await waitForMagicLink(request, email));
|
await gotoWithTransientRetry(page, await waitForMagicLink(request, email));
|
||||||
await expect(page.locator("#magic-link-fragment-form")).toBeVisible();
|
await expect(page.locator("#magic-link-fragment-form")).toBeVisible();
|
||||||
|
|
@ -357,8 +363,10 @@ export async function loginWithMagicLink(
|
||||||
page,
|
page,
|
||||||
await waitForMagicLink(request, email, previousMessageID),
|
await waitForMagicLink(request, email, previousMessageID),
|
||||||
);
|
);
|
||||||
await expect(page.getByRole("heading", { name: `Welcome ${email}` })).toBeVisible();
|
await expect(page.locator("#magic-link-fragment-form")).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Log me in only this time" }).click();
|
await page
|
||||||
|
.getByRole("button", { name: "Confirm and log in only this time" })
|
||||||
|
.click();
|
||||||
await expect(page.getByText(email, { exact: true })).toBeVisible();
|
await expect(page.getByText(email, { exact: true })).toBeVisible();
|
||||||
|
|
||||||
return { context, page, email };
|
return { context, page, email };
|
||||||
|
|
|
||||||
|
|
@ -83,11 +83,11 @@ test("two users complete medicine tracking, handover, realtime chat, and blind r
|
||||||
});
|
});
|
||||||
await helper.page.getByRole("button", { name: "Share location" }).click();
|
await helper.page.getByRole("button", { name: "Share location" }).click();
|
||||||
await expect(helper.page.getByRole("button", { name: "Stop and delete position" })).toBeVisible();
|
await expect(helper.page.getByRole("button", { name: "Stop and delete position" })).toBeVisible();
|
||||||
await expect.poll(() => markerKinds(requester.page)).toContain("Shared live location");
|
await expect.poll(() => markerKinds(requester.page)).toContain("Shared location");
|
||||||
|
|
||||||
await helper.page.getByRole("button", { name: "Stop and delete position" }).click();
|
await helper.page.getByRole("button", { name: "Stop and delete position" }).click();
|
||||||
await expect(helper.page.getByRole("button", { name: "Share location" })).toBeVisible();
|
await expect(helper.page.getByRole("button", { name: "Share location" })).toBeVisible();
|
||||||
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared live location");
|
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared location");
|
||||||
|
|
||||||
await helper.page.getByRole("button", { name: "Start helping" }).click();
|
await helper.page.getByRole("button", { name: "Start helping" }).click();
|
||||||
const handoverCode = (
|
const handoverCode = (
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ defmodule WhoNeedHelpWeb.Layouts do
|
||||||
|
|
||||||
def app(assigns) do
|
def app(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<header class="sticky top-0 z-30 border-b border-base-300 bg-base-100/90 px-4 py-2 backdrop-blur sm:px-6 lg:px-8">
|
<header class="sticky top-0 z-30 border-b border-base-300 bg-base-100 px-4 py-2 sm:px-6 lg:px-8">
|
||||||
<div class="mx-auto flex min-h-12 w-full max-w-7xl items-center justify-between gap-3">
|
<div class="mx-auto flex min-h-12 w-full max-w-7xl items-center justify-between gap-3">
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
|
|
@ -195,7 +195,7 @@ defmodule WhoNeedHelpWeb.Layouts do
|
||||||
href="?locale=en"
|
href="?locale=en"
|
||||||
class={[
|
class={[
|
||||||
"btn btn-ghost btn-xs join-item",
|
"btn btn-ghost btn-xs join-item",
|
||||||
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "btn-active"
|
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "btn-active locale-active"
|
||||||
]}
|
]}
|
||||||
aria-label="English"
|
aria-label="English"
|
||||||
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "page"}
|
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "page"}
|
||||||
|
|
@ -206,7 +206,7 @@ defmodule WhoNeedHelpWeb.Layouts do
|
||||||
href="?locale=uk"
|
href="?locale=uk"
|
||||||
class={[
|
class={[
|
||||||
"btn btn-ghost btn-xs join-item",
|
"btn btn-ghost btn-xs join-item",
|
||||||
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "btn-active"
|
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "btn-active locale-active"
|
||||||
]}
|
]}
|
||||||
aria-label="Українська"
|
aria-label="Українська"
|
||||||
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "page"}
|
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "page"}
|
||||||
|
|
@ -217,7 +217,7 @@ defmodule WhoNeedHelpWeb.Layouts do
|
||||||
href="?locale=ru"
|
href="?locale=ru"
|
||||||
class={[
|
class={[
|
||||||
"btn btn-ghost btn-xs join-item",
|
"btn btn-ghost btn-xs join-item",
|
||||||
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "btn-active"
|
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "btn-active locale-active"
|
||||||
]}
|
]}
|
||||||
aria-label="Русский"
|
aria-label="Русский"
|
||||||
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "page"}
|
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "page"}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user