Tie push subscriptions to current session

If a user logs out from a device the expectation should be that device
no longer having any association with the user's account.  Any existing
push notifications should thefore be removed on server.  For this reason
tie push notifications to a session, and remove them when the session is
deleted.
This commit is contained in:
Hornwitser 2025-03-07 14:11:07 +01:00
parent 150cb82f5c
commit 52dfde95d1
7 changed files with 39 additions and 15 deletions

View file

@ -8,9 +8,9 @@
</nav> </nav>
<div class="account"> <div class="account">
<template v-if="session?.account"> <template v-if="session?.account">
{{ session?.account.name || "anonymous" }} {{ session.account.name || "anonymous" }}
(s:{{ session?.id }} a:{{ session?.account.id }}) (s:{{ session.id }} a:{{ session.account.id }}{{ session.push ? " push" : null }})
{{ session?.account.type }} {{ session.account.type }}
<button type="button" @click="logOut">Log out</button> <button type="button" @click="logOut">Log out</button>
</template> </template>
<template v-else> <template v-else>

View file

@ -99,12 +99,14 @@ async function getSubscription(
const unsupported = ref<boolean | undefined>(undefined); const unsupported = ref<boolean | undefined>(undefined);
const subscription = ref<PushSubscription | null>(null); const subscription = ref<PushSubscription | null>(null);
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const { refresh: refreshSession } = useAccountSession();
function onClick() { async function onClick() {
if (!subscription.value) if (!subscription.value)
registerAndSubscribe(runtimeConfig.public.vapidPublicKey, (subs) => { subscription.value = subs }) await registerAndSubscribe(runtimeConfig.public.vapidPublicKey, (subs) => { subscription.value = subs })
else else
unsubscribe(subscription.value, () => { subscription.value = null }) await unsubscribe(subscription.value, () => { subscription.value = null })
refreshSession();
} }
onMounted(() => { onMounted(() => {

View file

@ -1,14 +1,19 @@
import { readAccounts } from "~/server/database"; import { readAccounts, readSubscriptions } from "~/server/database";
import { AccountSession } from "~/shared/types/account"; import { AccountSession } from "~/shared/types/account";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event): Promise<AccountSession | undefined> => {
const session = await getAccountSession(event); const session = await getAccountSession(event);
if (!session) if (!session)
return; return;
const accounts = await readAccounts(); const accounts = await readAccounts();
const subscriptions = await readSubscriptions();
const push = Boolean(
subscriptions.find(sub => sub.type === "push" && sub.sessionId === session.id)
);
return { return {
id: session.id, id: session.id,
account: accounts.find(account => account.id === session.accountId)!, account: accounts.find(account => account.id === session.accountId)!,
} satisfies AccountSession; push,
};
}) })

View file

@ -2,12 +2,17 @@ import { readSubscriptions, writeSubscriptions } from "~/server/database";
import { Subscription } from "~/shared/types/account"; import { Subscription } from "~/shared/types/account";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const session = await requireAccountSession(event);
const body: { subscription: PushSubscriptionJSON } = await readBody(event); const body: { subscription: PushSubscriptionJSON } = await readBody(event);
const subscriptions = await readSubscriptions(); const subscriptions = await readSubscriptions();
const existingIndex = subscriptions.findIndex( const existingIndex = subscriptions.findIndex(
sub => sub.type === "push" && sub.push.endpoint === body.subscription.endpoint sub => sub.type === "push" && sub.sessionId === session.id
); );
const subscription: Subscription = { type: "push", push: body.subscription }; const subscription: Subscription = {
type: "push",
sessionId: session.id,
push: body.subscription
};
if (existingIndex !== -1) { if (existingIndex !== -1) {
subscriptions[existingIndex] = subscription; subscriptions[existingIndex] = subscription;
} else { } else {

View file

@ -1,10 +1,10 @@
import { readSubscriptions, writeSubscriptions } from "~/server/database"; import { readSubscriptions, writeSubscriptions } from "~/server/database";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const body: { subscription: PushSubscriptionJSON } = await readBody(event); const session = await requireAccountSession(event);
const subscriptions = await readSubscriptions(); const subscriptions = await readSubscriptions();
const existingIndex = subscriptions.findIndex( const existingIndex = subscriptions.findIndex(
sub => sub.type === "push" && sub.push.endpoint === body.subscription.endpoint sub => sub.type === "push" && sub.sessionId === session.id
); );
if (existingIndex !== -1) { if (existingIndex !== -1) {
subscriptions.splice(existingIndex, 1); subscriptions.splice(existingIndex, 1);
@ -13,4 +13,4 @@ export default defineEventHandler(async (event) => {
} }
await writeSubscriptions(subscriptions); await writeSubscriptions(subscriptions);
return { message: "Existing subscription removed."}; return { message: "Existing subscription removed."};
}) });

View file

@ -1,7 +1,16 @@
import type { H3Event } from "h3"; import type { H3Event } from "h3";
import { nextSessionId, readSessions, writeSessions } from "~/server/database"; import { nextSessionId, readSessions, readSubscriptions, writeSessions, writeSubscriptions } from "~/server/database";
import { Session } from "~/shared/types/account"; import { Session } from "~/shared/types/account";
async function removeSessionSubscription(sessionId: number) {
const subscriptions = await readSubscriptions();
const index = subscriptions.findIndex(subscription => subscription.sessionId === sessionId);
if (index !== -1) {
subscriptions.splice(index, 1);
await writeSubscriptions(subscriptions);
}
}
async function clearAccountSessionInternal(event: H3Event, sessions: Session[]) { async function clearAccountSessionInternal(event: H3Event, sessions: Session[]) {
const existingSessionCookie = await getSignedCookie(event, "session"); const existingSessionCookie = await getSignedCookie(event, "session");
if (existingSessionCookie) { if (existingSessionCookie) {
@ -9,6 +18,7 @@ async function clearAccountSessionInternal(event: H3Event, sessions: Session[])
const sessionIndex = sessions.findIndex(session => session.id === sessionId); const sessionIndex = sessions.findIndex(session => session.id === sessionId);
if (sessionIndex !== -1) { if (sessionIndex !== -1) {
sessions.splice(sessionIndex, 1); sessions.splice(sessionIndex, 1);
await removeSessionSubscription(sessionId);
return true; return true;
} }
} }

View file

@ -7,6 +7,7 @@ export interface Account {
export interface Subscription { export interface Subscription {
type: "push", type: "push",
sessionId: number,
push: PushSubscriptionJSON, push: PushSubscriptionJSON,
} }
@ -18,4 +19,5 @@ export interface Session {
export interface AccountSession { export interface AccountSession {
id: number, id: number,
account: Account, account: Account,
push: boolean,
} }