mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-16 00:17:56 +00:00
fix: token extension for PWAs
This commit is contained in:
@@ -255,6 +255,7 @@ Reference: <https://stackoverflow.com/questions/34160509/options-for-testing-ser
|
|||||||
go run main.go \
|
go run main.go \
|
||||||
--log-level debug \
|
--log-level debug \
|
||||||
serve \
|
serve \
|
||||||
|
--base-url http://localhost \
|
||||||
--web-push-public-key KEY \
|
--web-push-public-key KEY \
|
||||||
--web-push-private-key KEY \
|
--web-push-private-key KEY \
|
||||||
--web-push-email-address <email> \
|
--web-push-email-address <email> \
|
||||||
|
|||||||
+80
-9
@@ -4,6 +4,7 @@ import { NavigationRoute, registerRoute } from "workbox-routing";
|
|||||||
import { NetworkFirst } from "workbox-strategies";
|
import { NetworkFirst } from "workbox-strategies";
|
||||||
import { clientsClaim } from "workbox-core";
|
import { clientsClaim } from "workbox-core";
|
||||||
import { dbAsync } from "../src/app/db";
|
import { dbAsync } from "../src/app/db";
|
||||||
|
import session from "../src/app/Session";
|
||||||
import { ACTION_HTTP, ACTION_VIEW } from "../src/app/actions";
|
import { ACTION_HTTP, ACTION_VIEW } from "../src/app/actions";
|
||||||
import { badge, icon, messageWithSequenceId, notificationTag, toNotificationParams } from "../src/app/notificationUtils";
|
import { badge, icon, messageWithSequenceId, notificationTag, toNotificationParams } from "../src/app/notificationUtils";
|
||||||
import initI18n from "../src/app/i18n";
|
import initI18n from "../src/app/i18n";
|
||||||
@@ -35,6 +36,7 @@ const broadcastChannel = new BroadcastChannel("web-push-broadcast");
|
|||||||
*/
|
*/
|
||||||
const handlePushMessage = async (data) => {
|
const handlePushMessage = async (data) => {
|
||||||
const { subscription_id: subscriptionId, message } = data;
|
const { subscription_id: subscriptionId, message } = data;
|
||||||
|
|
||||||
const db = await dbAsync();
|
const db = await dbAsync();
|
||||||
|
|
||||||
console.log("[ServiceWorker] Message received", data);
|
console.log("[ServiceWorker] Message received", data);
|
||||||
@@ -43,9 +45,24 @@ const handlePushMessage = async (data) => {
|
|||||||
const subscription = await db.subscriptions.get(subscriptionId);
|
const subscription = await db.subscriptions.get(subscriptionId);
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
console.log("[ServiceWorker] Subscription not found", subscriptionId);
|
console.log("[ServiceWorker] Subscription not found", subscriptionId);
|
||||||
|
handlePushUnknown(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: As soon as possible, to avoid this Safari error:
|
||||||
|
// > Push event handling completed without showing any notification via
|
||||||
|
// > ServiceWorkerRegistration.showNotification(). This may trigger removal of
|
||||||
|
// > the push subscription.
|
||||||
|
await self.registration.showNotification(
|
||||||
|
...toNotificationParams({
|
||||||
|
message,
|
||||||
|
defaultTitle: message.topic,
|
||||||
|
topicRoute: new URL(message.topic, self.location.origin).toString(),
|
||||||
|
baseUrl: subscription.baseUrl,
|
||||||
|
topic: subscription.topic,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Delete existing notification with same sequence ID (if any)
|
// Delete existing notification with same sequence ID (if any)
|
||||||
const sequenceId = message.sequence_id || message.id;
|
const sequenceId = message.sequence_id || message.id;
|
||||||
if (sequenceId) {
|
if (sequenceId) {
|
||||||
@@ -71,17 +88,71 @@ const handlePushMessage = async (data) => {
|
|||||||
// Broadcast the message to potentially play a sound
|
// Broadcast the message to potentially play a sound
|
||||||
broadcastChannel.postMessage(message);
|
broadcastChannel.postMessage(message);
|
||||||
|
|
||||||
await self.registration.showNotification(
|
await extendToken();
|
||||||
...toNotificationParams({
|
|
||||||
message,
|
|
||||||
defaultTitle: message.topic,
|
|
||||||
topicRoute: new URL(message.topic, self.location.origin).toString(),
|
|
||||||
baseUrl: subscription.baseUrl,
|
|
||||||
topic: subscription.topic,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshThreshold = 1000 * 60 * 60; // 1 hour
|
||||||
|
const extendToken = async () => {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.warn("[ServiceWorker] Skipping token extension in development since no config.base_url exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await session.tokenAsync();
|
||||||
|
if (!token) {
|
||||||
|
console.debug("[ServiceWorker] No session token, skipping token extension");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastExtendedAt = await session.lastExtendedAtAsync();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (lastExtendedAt && now - lastExtendedAt < refreshThreshold) {
|
||||||
|
console.debug(`[ServiceWorker] Token extended ${Math.floor((now - lastExtendedAt) / 1000 / 60)} minutes ago, skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[ServiceWorker] Extending user access token");
|
||||||
|
|
||||||
|
// duplicated from utils.js#accountTokenUrl since we can't import that here
|
||||||
|
// as long as there's mp3 and other incompatible imports there
|
||||||
|
const tokenUrl = `${config.base_url}/v1/account/token`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(tokenUrl, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await session.setLastExtendedAtAsync();
|
||||||
|
console.log(`[ServiceWorker] Token extended successfully`);
|
||||||
|
} else {
|
||||||
|
console.error(`[ServiceWorker] Failed to extend token: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ServiceWorker] Failed to extend token", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a periodic-sync listener for `extend-token` (see hooks.js).
|
||||||
|
* This extends the token regardless of whether the browser is open.
|
||||||
|
*
|
||||||
|
* CAVEATS:
|
||||||
|
* - Chromium-only
|
||||||
|
* - Only when the PWA is _installed_ (not just running in a browser tab)
|
||||||
|
* - Only when notifications are granted
|
||||||
|
*/
|
||||||
|
self.addEventListener("periodicsync", (event) => {
|
||||||
|
if (event.tag === "extend-token") {
|
||||||
|
console.log('[ServiceWorker] Received periodicsync event "extend-token"');
|
||||||
|
event.waitUntil(extendToken());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a message_delete event: delete the notification from the database.
|
* Handle a message_delete event: delete the notification from the database.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ class AccountApi {
|
|||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: withBearerAuth({}, session.token()),
|
headers: withBearerAuth({}, session.token()),
|
||||||
});
|
});
|
||||||
|
await session.setLastExtendedAtAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteToken(token) {
|
async deleteToken(token) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class Session {
|
|||||||
await this.db.kv.bulkPut([
|
await this.db.kv.bulkPut([
|
||||||
{ key: "user", value: username },
|
{ key: "user", value: username },
|
||||||
{ key: "token", value: token },
|
{ key: "token", value: token },
|
||||||
|
{ key: "lastExtendedAt", value: Date.now() },
|
||||||
]);
|
]);
|
||||||
localStorage.setItem("user", username);
|
localStorage.setItem("user", username);
|
||||||
localStorage.setItem("token", token);
|
localStorage.setItem("token", token);
|
||||||
@@ -52,6 +53,18 @@ class Session {
|
|||||||
return (await this.db.kv.get({ key: "user" }))?.value;
|
return (await this.db.kv.get({ key: "user" }))?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async tokenAsync() {
|
||||||
|
return (await this.db.kv.get({ key: "token" }))?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async lastExtendedAtAsync() {
|
||||||
|
return (await this.db.kv.get({ key: "lastExtendedAt" }))?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setLastExtendedAtAsync() {
|
||||||
|
await this.db.kv.put({ key: "lastExtendedAt", value: Date.now() });
|
||||||
|
}
|
||||||
|
|
||||||
exists() {
|
exists() {
|
||||||
return this.username() && this.token();
|
return this.username() && this.token();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,6 +283,49 @@ export const useStandaloneWebPushAutoSubscribe = () => {
|
|||||||
}, [isLaunchedPWA]);
|
}, [isLaunchedPWA]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a periodicsync listener for `extend-token` (see sw.js).
|
||||||
|
* This extends the token regardless of whether the browser is open.
|
||||||
|
*
|
||||||
|
* CAVEATS:
|
||||||
|
* - Chromium-only
|
||||||
|
* - Only when the PWA is _installed_ (not just running in a browser tab)
|
||||||
|
* - Only when notifications are granted
|
||||||
|
*/
|
||||||
|
const usePeriodicTokenExtend = () => {
|
||||||
|
const isLaunchedPWA = useIsLaunchedPWA();
|
||||||
|
const pushPossible = useNotificationPermissionListener(() => notifier.pushPossible());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (!isLaunchedPWA) {
|
||||||
|
console.debug("[usePeriodicTokenExtend] Skipping: Not running as PWA");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pushPossible) {
|
||||||
|
console.debug("[usePeriodicTokenExtend] Skipping: Web push not possible or granted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
if (!registration.periodicSync) {
|
||||||
|
console.debug("[usePeriodicTokenExtend] Skipping: Periodic Sync not supported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[usePeriodicTokenExtend] Turning on periodicsync "extend-token"`);
|
||||||
|
await registration.periodicSync.register("extend-token", {
|
||||||
|
minInterval: 24 * 60 * 60 * 1000, // 24 hours
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[usePeriodicTokenExtend] Periodic Sync could not be registered", error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [isLaunchedPWA, pushPossible]);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the poller and the pruner. This is done in a side effect as opposed to just in Pruner.js
|
* Start the poller and the pruner. This is done in a side effect as opposed to just in Pruner.js
|
||||||
* and Poller.js, because side effect imports are not a thing in JS, and "Optimize imports" cleans
|
* and Poller.js, because side effect imports are not a thing in JS, and "Optimize imports" cleans
|
||||||
@@ -305,6 +348,7 @@ const stopWorkers = () => {
|
|||||||
|
|
||||||
export const useBackgroundProcesses = () => {
|
export const useBackgroundProcesses = () => {
|
||||||
useStandaloneWebPushAutoSubscribe();
|
useStandaloneWebPushAutoSubscribe();
|
||||||
|
usePeriodicTokenExtend();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[useBackgroundProcesses] mounting");
|
console.log("[useBackgroundProcesses] mounting");
|
||||||
|
|||||||
Reference in New Issue
Block a user