mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-12-10 00:30:23 -06:00
Webpush notifications (#6421)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
This commit is contained in:
parent
892bd42dae
commit
23c4916c74
@ -2,6 +2,7 @@ import vue from "@vitejs/plugin-vue";
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import visualizer from "rollup-plugin-visualizer";
|
import visualizer from "rollup-plugin-visualizer";
|
||||||
import viteCompression from "vite-plugin-compression";
|
import viteCompression from "vite-plugin-compression";
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
const postCssScss = require("postcss-scss");
|
const postCssScss = require("postcss-scss");
|
||||||
const postcssRTLCSS = require("postcss-rtlcss");
|
const postcssRTLCSS = require("postcss-rtlcss");
|
||||||
@ -30,6 +31,12 @@ export default defineConfig({
|
|||||||
algorithm: "brotliCompress",
|
algorithm: "brotliCompress",
|
||||||
filter: viteCompressionFilter,
|
filter: viteCompressionFilter,
|
||||||
}),
|
}),
|
||||||
|
VitePWA({
|
||||||
|
registerType: null,
|
||||||
|
srcDir: "src",
|
||||||
|
filename: "serviceWorker.ts",
|
||||||
|
strategies: "injectManifest",
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
css: {
|
css: {
|
||||||
postcss: {
|
postcss: {
|
||||||
|
|||||||
3170
package-lock.json
generated
3170
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -147,6 +147,7 @@
|
|||||||
"tcp-ping": "~0.1.1",
|
"tcp-ping": "~0.1.1",
|
||||||
"thirty-two": "~1.0.2",
|
"thirty-two": "~1.0.2",
|
||||||
"tough-cookie": "~4.1.3",
|
"tough-cookie": "~4.1.3",
|
||||||
|
"web-push": "^3.6.7",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -161,6 +162,7 @@
|
|||||||
"@testcontainers/rabbitmq": "^10.13.2",
|
"@testcontainers/rabbitmq": "^10.13.2",
|
||||||
"@types/bootstrap": "~5.1.9",
|
"@types/bootstrap": "~5.1.9",
|
||||||
"@types/node": "^20.8.6",
|
"@types/node": "^20.8.6",
|
||||||
|
"@types/web-push": "^3.6.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
||||||
"@typescript-eslint/parser": "^6.7.5",
|
"@typescript-eslint/parser": "^6.7.5",
|
||||||
"@vitejs/plugin-vue": "~5.0.1",
|
"@vitejs/plugin-vue": "~5.0.1",
|
||||||
@ -199,6 +201,7 @@
|
|||||||
"v-pagination-3": "~0.1.7",
|
"v-pagination-3": "~0.1.7",
|
||||||
"vite": "~5.4.15",
|
"vite": "~5.4.15",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vite-plugin-pwa": "^1.1.0",
|
||||||
"vue": "~3.4.2",
|
"vue": "~3.4.2",
|
||||||
"vue-chartjs": "~5.2.0",
|
"vue-chartjs": "~5.2.0",
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
|
|||||||
47
server/notification-providers/Webpush.js
Normal file
47
server/notification-providers/Webpush.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const { UP } = require("../../src/util");
|
||||||
|
const webpush = require("web-push");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
|
||||||
|
class Webpush extends NotificationProvider {
|
||||||
|
name = "Webpush";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
const okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const publicVapidKey = await setting("webpushPublicVapidKey");
|
||||||
|
const privateVapidKey = await setting("webpushPrivateVapidKey");
|
||||||
|
|
||||||
|
webpush.setVapidDetails("https://github.com/louislam/uptime-kuma", publicVapidKey, privateVapidKey);
|
||||||
|
|
||||||
|
if (heartbeatJSON === null && monitorJSON === null) {
|
||||||
|
// Test message
|
||||||
|
const data = JSON.stringify({
|
||||||
|
title: "TEST",
|
||||||
|
body: `Test Alert - ${msg}`
|
||||||
|
});
|
||||||
|
|
||||||
|
await webpush.sendNotification(notification.subscription, data);
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.stringify({
|
||||||
|
title: heartbeatJSON["status"] === UP ? "Monitor Up" : "Monitor DOWN",
|
||||||
|
body: heartbeatJSON["status"] === UP ? `❌ ${heartbeatJSON["name"]} is DOWN` : `✅ ${heartbeatJSON["name"]} is UP`
|
||||||
|
});
|
||||||
|
|
||||||
|
await webpush.sendNotification(notification.subscription, data);
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Webpush;
|
||||||
@ -83,6 +83,7 @@ const SMSPlanet = require("./notification-providers/sms-planet");
|
|||||||
const SpugPush = require("./notification-providers/spugpush");
|
const SpugPush = require("./notification-providers/spugpush");
|
||||||
const SMSIR = require("./notification-providers/smsir");
|
const SMSIR = require("./notification-providers/smsir");
|
||||||
const { commandExists } = require("./util-server");
|
const { commandExists } = require("./util-server");
|
||||||
|
const Webpush = require("./notification-providers/Webpush");
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
providerList = {};
|
providerList = {};
|
||||||
@ -174,13 +175,14 @@ class Notification {
|
|||||||
new GtxMessaging(),
|
new GtxMessaging(),
|
||||||
new Cellsynt(),
|
new Cellsynt(),
|
||||||
new Wpush(),
|
new Wpush(),
|
||||||
new SendGrid(),
|
|
||||||
new Brevo(),
|
new Brevo(),
|
||||||
new YZJ(),
|
new YZJ(),
|
||||||
new SMSPlanet(),
|
new SMSPlanet(),
|
||||||
new SpugPush(),
|
new SpugPush(),
|
||||||
new Notifery(),
|
new Notifery(),
|
||||||
new SMSIR(),
|
new SMSIR(),
|
||||||
|
new SendGrid(),
|
||||||
|
new Webpush(),
|
||||||
];
|
];
|
||||||
for (let item of list) {
|
for (let item of list) {
|
||||||
if (!item.name) {
|
if (!item.name) {
|
||||||
|
|||||||
@ -96,6 +96,8 @@ const { getSettings, setSettings, setting, initJWTSecret, checkLogin, doubleChec
|
|||||||
log.debug("server", "Importing Notification");
|
log.debug("server", "Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
Notification.init();
|
Notification.init();
|
||||||
|
log.debug("server", "Importing Web-Push");
|
||||||
|
const webpush = require("web-push");
|
||||||
|
|
||||||
log.debug("server", "Importing Database");
|
log.debug("server", "Importing Database");
|
||||||
const Database = require("./database");
|
const Database = require("./database");
|
||||||
@ -1563,6 +1565,32 @@ let needSetup = false;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("getWebpushVapidPublicKey", async (callback) => {
|
||||||
|
try {
|
||||||
|
let publicVapidKey = await Settings.get("webpushPublicVapidKey");
|
||||||
|
|
||||||
|
if (!publicVapidKey) {
|
||||||
|
log.debug("webpush", "Generating new VAPID keys");
|
||||||
|
const vapidKeys = webpush.generateVAPIDKeys();
|
||||||
|
|
||||||
|
await Settings.set("webpushPublicVapidKey", vapidKeys.publicKey);
|
||||||
|
await Settings.set("webpushPrivateVapidKey", vapidKeys.privateKey);
|
||||||
|
|
||||||
|
publicVapidKey = vapidKeys.publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: publicVapidKey,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("clearEvents", async (monitorID, callback) => {
|
socket.on("clearEvents", async (monitorID, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|||||||
@ -173,7 +173,8 @@ export default {
|
|||||||
"Cellsynt": "Cellsynt",
|
"Cellsynt": "Cellsynt",
|
||||||
"SendGrid": "SendGrid",
|
"SendGrid": "SendGrid",
|
||||||
"Brevo": "Brevo",
|
"Brevo": "Brevo",
|
||||||
"notifery": "Notifery"
|
"notifery": "Notifery",
|
||||||
|
"Webpush": "Webpush",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Put notifications here if it's not supported in most regions or its documentation is not in English
|
// Put notifications here if it's not supported in most regions or its documentation is not in English
|
||||||
|
|||||||
97
src/components/notifications/Webpush.vue
Normal file
97
src/components/notifications/Webpush.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="mb-3"
|
||||||
|
type="button" :class="[
|
||||||
|
'btn',
|
||||||
|
browserSupportsServiceWorkers ? 'btn-primary' : 'btn-danger'
|
||||||
|
]"
|
||||||
|
:disabled="!btnEnabled"
|
||||||
|
@click="registerWebpush"
|
||||||
|
>
|
||||||
|
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||||
|
<span v-else-if="$parent.notification.subscription" class="me-1">✓</span>
|
||||||
|
{{ btnText }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("Webpush Helptext") }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
btnEnabled: false,
|
||||||
|
btnText: "",
|
||||||
|
processing: false,
|
||||||
|
browserSupportsServiceWorkers: false,
|
||||||
|
publicVapidKey: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.$parent.notification.subscription) {
|
||||||
|
this.btnEnabled = false;
|
||||||
|
this.browserSupportsServiceWorkers = true;
|
||||||
|
this.btnText = this.$t("Notifications Enabled");
|
||||||
|
} else {
|
||||||
|
if (("serviceWorker" in navigator)) {
|
||||||
|
this.btnText = this.$t("Allow Notifications");
|
||||||
|
this.browserSupportsServiceWorkers = true;
|
||||||
|
this.btnEnabled = true;
|
||||||
|
} else {
|
||||||
|
this.btnText = this.$t("Browser not supported");
|
||||||
|
this.browserSupportsServiceWorkers = false;
|
||||||
|
this.btnEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async registerWebpush() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const publicKey = await new Promise((resolve, reject) => {
|
||||||
|
this.$root.getSocket().emit("getWebpushVapidPublicKey", (resp) => {
|
||||||
|
if (!resp.ok) {
|
||||||
|
reject(new Error(resp.msg));
|
||||||
|
}
|
||||||
|
console.log(resp.msg);
|
||||||
|
resolve(resp.msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const permission = await Notification.requestPermission();
|
||||||
|
if (permission !== "granted") {
|
||||||
|
this.$root.toastRes({
|
||||||
|
ok: false,
|
||||||
|
msg: this.$t("Unable to get permission to notify"),
|
||||||
|
});
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
|
||||||
|
const subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$parent.notification.subscription = subscription;
|
||||||
|
this.btnEnabled = false;
|
||||||
|
this.browserSupportsServiceWorkers = true;
|
||||||
|
this.btnText = this.$t("Notifications Enabled");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Subscription failed:", error);
|
||||||
|
this.$root.toastRes({
|
||||||
|
ok: false,
|
||||||
|
msg: error
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.processing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -80,6 +80,7 @@ import Brevo from "./Brevo.vue";
|
|||||||
import YZJ from "./YZJ.vue";
|
import YZJ from "./YZJ.vue";
|
||||||
import SMSPlanet from "./SMSPlanet.vue";
|
import SMSPlanet from "./SMSPlanet.vue";
|
||||||
import SMSIR from "./SMSIR.vue";
|
import SMSIR from "./SMSIR.vue";
|
||||||
|
import Webpush from "./Webpush.vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage all notification form.
|
* Manage all notification form.
|
||||||
@ -168,6 +169,7 @@ const NotificationFormList = {
|
|||||||
"Brevo": Brevo,
|
"Brevo": Brevo,
|
||||||
"YZJ": YZJ,
|
"YZJ": YZJ,
|
||||||
"SMSPlanet": SMSPlanet,
|
"SMSPlanet": SMSPlanet,
|
||||||
|
"Webpush": Webpush,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotificationFormList;
|
export default NotificationFormList;
|
||||||
|
|||||||
@ -1191,5 +1191,10 @@
|
|||||||
"Maximum Retries": "Maximum Retries",
|
"Maximum Retries": "Maximum Retries",
|
||||||
"Template ID": "Template ID",
|
"Template ID": "Template ID",
|
||||||
"wayToGetClickSMSIRTemplateID": "Your template must contain an {uptkumaalert} field. You can create a new template {here}.",
|
"wayToGetClickSMSIRTemplateID": "Your template must contain an {uptkumaalert} field. You can create a new template {here}.",
|
||||||
"Recipient Numbers": "Recipient Numbers"
|
"Recipient Numbers": "Recipient Numbers",
|
||||||
|
"Notifications Enabled": "Notifications Enabled",
|
||||||
|
"Allow Notifications": "Allow Notifications",
|
||||||
|
"Browser not supported": "Browser not supported",
|
||||||
|
"Unable to get permission to notify": "Unable to get permission to notify (request either denied or ignored).",
|
||||||
|
"Webpush Helptext": "Web push only works with SSL (HTTPS) connections. For iOS devices, webpage must be added to homescreen beforehand."
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/serviceWorker.ts
Normal file
23
src/serviceWorker.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Needed per Vite PWA docs
|
||||||
|
import { precacheAndRoute } from 'workbox-precaching'
|
||||||
|
declare let self: ServiceWorkerGlobalScope
|
||||||
|
precacheAndRoute(self.__WB_MANIFEST)
|
||||||
|
|
||||||
|
// Receive push notifications
|
||||||
|
self.addEventListener('push', function (event) {
|
||||||
|
if (self.Notification?.permission !== 'granted') {
|
||||||
|
console.error("Notifications aren't supported or permission not granted!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data) {
|
||||||
|
let message = event.data.json();
|
||||||
|
try {
|
||||||
|
self.registration.showNotification(message.title, {
|
||||||
|
body: message.body,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to show notification:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user