mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
fix(auth): take the push subscription off on every sign-out button
Only the menu's sign-out removed this browser's push subscription. The sign-out buttons on the registration page and the profile take-over page, which exist so someone else can use the browser next, left it behind, and the next person kept seeing the last account's reminders. All three now go through one signOutRequest. The browser's own subscription is now removed only once the server has confirmed the sign-out; the server row still goes first, while the session can name it. A refused sign-out stores the row again, so the person who is still signed in keeps push. The logout test pins the new order and the refusal. The helper file is renamed web-push-subscription.ts, since it now also saves a subscription for the settings card.
This commit is contained in:
1 parent
6914f844be
commit
367ba65bf6
10 files changed
+231
-69
No files matched your search
@@ -117,6 +117,42 @@ describe("ClaimProfile", () => {
|
||||
});
|
||||
|
||||
describe("ClaimProfile, already signed in", () => {
|
||||
it("takes this browser's push subscription with it on sign-out", async () => {
|
||||
// The button exists so the profile's person can take over on this
|
||||
// browser; a subscription left behind would show them the last
|
||||
// account's reminders.
|
||||
const unsubscribe = vi.fn(async () => true);
|
||||
const registration = vi
|
||||
.spyOn(navigator.serviceWorker, "getRegistration")
|
||||
.mockResolvedValue({
|
||||
pushManager: {
|
||||
getSubscription: async () => ({
|
||||
endpoint: "https://push.example.test/send/abc",
|
||||
unsubscribe,
|
||||
}),
|
||||
},
|
||||
} as unknown as ServiceWorkerRegistration);
|
||||
api
|
||||
.on("DELETE", "/api/notifications/web-push", { data: {} })
|
||||
.on("POST", "/api/auth/logout", { data: { loggedOut: true } });
|
||||
await renderWithApp(<ClaimProfile token={TOKEN} />, {
|
||||
user: { username: "signed-in" },
|
||||
});
|
||||
|
||||
await click("claim-sign-out");
|
||||
|
||||
await expect.poll(() => unsubscribe.mock.calls.length).toBe(1);
|
||||
const order = api.requests
|
||||
.map((r) => `${r.method} ${r.path.split("?")[0]}`)
|
||||
.filter((call) => !call.startsWith("GET "));
|
||||
// The signed-out page then loads the link's preview.
|
||||
expect(order.slice(0, 2)).toEqual([
|
||||
"DELETE /api/notifications/web-push",
|
||||
"POST /api/auth/logout",
|
||||
]);
|
||||
registration.mockRestore();
|
||||
});
|
||||
|
||||
it("keeps the session's page and says so when the server refuses the sign-out", async () => {
|
||||
api.on("POST", "/api/auth/logout", {
|
||||
status: 403,
|
||||
|
||||
@@ -13,8 +13,12 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Logo } from "@/components/ui/logo";
|
||||
import { PasswordStrength } from "@/components/ui/password-strength";
|
||||
import { clearCachesForSessionEnd, useAuth } from "@/hooks/use-auth";
|
||||
import { ApiError, apiFetch } from "@/lib/api/api-fetch";
|
||||
import {
|
||||
clearCachesForSessionEnd,
|
||||
signOutRequest,
|
||||
useAuth,
|
||||
} from "@/hooks/use-auth";
|
||||
import { ApiError } from "@/lib/api/api-fetch";
|
||||
import { showErrorToast } from "@/lib/errors/error-toast";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { loadDocument } from "@/lib/navigation/load-document";
|
||||
@@ -79,7 +83,7 @@ export function ClaimProfile({
|
||||
async function signOut() {
|
||||
setSigningOut(true);
|
||||
try {
|
||||
await apiFetch("/api/auth/logout", { method: "POST" });
|
||||
await signOutRequest();
|
||||
clearCachesForSessionEnd(queryClient);
|
||||
// Back to this page with the link intact: the token only rides the URL.
|
||||
router.replace(
|
||||
|
||||
@@ -10,7 +10,10 @@ import { SettingsCardActions } from "@/components/settings/_card-actions";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { apiFetchRaw } from "@/lib/api/api-fetch";
|
||||
import { unsubscribeWebPush } from "@/lib/notifications/web-push-unsubscribe";
|
||||
import {
|
||||
saveWebPushSubscription,
|
||||
unsubscribeWebPush,
|
||||
} from "@/lib/notifications/web-push-subscription";
|
||||
|
||||
/** Convert a URL-safe base64 string to an ArrayBuffer (for VAPID key). */
|
||||
function urlBase64ToArrayBuffer(base64String: string): ArrayBuffer {
|
||||
@@ -38,27 +41,6 @@ export function needsHomeScreen(): boolean {
|
||||
return appleTouch && !window.matchMedia("(display-mode: standalone)").matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store this browser's subscription for the signed-in account. Also run for a
|
||||
* subscription the browser already holds, so a row the server lost or gave
|
||||
* to another account on this browser comes back, and a channel switched off
|
||||
* by failed sends is turned on again.
|
||||
*/
|
||||
function saveSubscription(subscription: PushSubscription): Promise<Response> {
|
||||
const subJson = subscription.toJSON();
|
||||
return apiFetchRaw("/api/notifications/web-push", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
endpoint: subJson.endpoint,
|
||||
keys: {
|
||||
p256dh: subJson.keys?.p256dh ?? "",
|
||||
auth: subJson.keys?.auth ?? "",
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
type Unsupported = "browser" | "install" | null;
|
||||
|
||||
export function WebPushCard() {
|
||||
@@ -94,7 +76,11 @@ export function WebPushCard() {
|
||||
if (registration) {
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
setIsSubscribed(!!subscription);
|
||||
if (subscription) void saveSubscription(subscription).catch(() => {});
|
||||
// Sent again on every open: the server may have lost the row or
|
||||
// switched the channel off, and the browser cannot tell.
|
||||
if (subscription) {
|
||||
void saveWebPushSubscription(subscription).catch(() => null);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during check
|
||||
@@ -133,7 +119,7 @@ export function WebPushCard() {
|
||||
applicationServerKey: urlBase64ToArrayBuffer(vapidPublicKey),
|
||||
});
|
||||
|
||||
const res = await saveSubscription(subscription);
|
||||
const res = await saveWebPushSubscription(subscription);
|
||||
if (res.ok) {
|
||||
setIsSubscribed(true);
|
||||
setMsg(t("settings.webPushSubscribed"));
|
||||
|
||||
Reference in new issue
Block a user