Skip to content
Open
7 changes: 7 additions & 0 deletions config/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { defineConfig } from "vite";
import visualizer from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression";
import VueDevTools from "vite-plugin-vue-devtools";
import { VitePWA } from "vite-plugin-pwa";

const postCssScss = require("postcss-scss");
const postcssRTLCSS = require("postcss-rtlcss");
Expand Down Expand Up @@ -32,6 +33,12 @@ export default defineConfig({
filter: viteCompressionFilter,
}),
VueDevTools(),
VitePWA({
registerType: null,
srcDir: "src",
filename: "serviceWorker.ts",
strategies: "injectManifest",
}),
],
css: {
postcss: {
Expand Down
18,035 changes: 0 additions & 18,035 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2",
"tough-cookie": "~4.1.3",
"web-push": "^3.6.7",
"ws": "^8.13.0"
},
"devDependencies": {
Expand All @@ -155,6 +156,7 @@
"@testcontainers/rabbitmq": "^10.13.2",
"@types/bootstrap": "~5.1.9",
"@types/node": "^20.8.6",
"@types/web-push": "^3.6.4",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@vitejs/plugin-vue": "~5.0.1",
Expand Down Expand Up @@ -193,6 +195,7 @@
"v-pagination-3": "~0.1.7",
"vite": "~5.4.15",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-vue-devtools": "^7.0.15",
"vue": "~3.4.2",
"vue-chartjs": "~5.2.0",
Expand Down
51 changes: 51 additions & 0 deletions server/notification-providers/Webpush.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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 title = `Monitor ${heartbeatJSON["status"] === UP ? "UP" : "DOWN"}`;
const down = "❌ " + monitorJSON["name"] + " is DOWN ❌";
const up = "✅ " + monitorJSON["name"] + " is UP ✅";

const data = JSON.stringify({
title: title,
body: `${heartbeatJSON["status"] === UP ? up : down}`
});

await webpush.sendNotification(notification.subscription, data);

return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}

module.exports = Webpush;
2 changes: 2 additions & 0 deletions server/notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const Cellsynt = require("./notification-providers/cellsynt");
const Onesender = require("./notification-providers/onesender");
const Wpush = require("./notification-providers/wpush");
const SendGrid = require("./notification-providers/send-grid");
const Webpush = require("./notification-providers/Webpush");
const YZJ = require("./notification-providers/yzj");
const SMSPlanet = require("./notification-providers/sms-planet");
const SpugPush = require("./notification-providers/spugpush");
Expand Down Expand Up @@ -167,6 +168,7 @@ class Notification {
new Cellsynt(),
new Wpush(),
new SendGrid(),
new Webpush(),
new YZJ(),
new SMSPlanet(),
new SpugPush(),
Expand Down
28 changes: 28 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ const { getSettings, setSettings, setting, initJWTSecret, checkLogin, doubleChec
log.debug("server", "Importing Notification");
const { Notification } = require("./notification");
Notification.init();
log.debug("server", "Importing Web-Push");
const webpush = require("web-push");

log.debug("server", "Importing Database");
const Database = require("./database");
Expand Down Expand Up @@ -1488,6 +1490,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) => {
try {
checkLogin(socket);
Expand Down
1 change: 1 addition & 0 deletions src/components/NotificationDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export default {
"gtxmessaging": "GtxMessaging",
"Cellsynt": "Cellsynt",
"SendGrid": "SendGrid",
"Webpush": "Webpush",
"notifery": "Notifery"
};

Expand Down
98 changes: 98 additions & 0 deletions src/components/notifications/Webpush.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<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>
2 changes: 2 additions & 0 deletions src/components/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import Cellsynt from "./Cellsynt.vue";
import WPush from "./WPush.vue";
import SIGNL4 from "./SIGNL4.vue";
import SendGrid from "./SendGrid.vue";
import Webpush from "./Webpush.vue";
import YZJ from "./YZJ.vue";
import SMSPlanet from "./SMSPlanet.vue";

Expand Down Expand Up @@ -156,6 +157,7 @@ const NotificationFormList = {
"Cellsynt": Cellsynt,
"WPush": WPush,
"SendGrid": SendGrid,
"Webpush": Webpush,
"YZJ": YZJ,
"SMSPlanet": SMSPlanet,
};
Expand Down
5 changes: 5 additions & 0 deletions src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,11 @@
"rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.",
"SendGrid API Key": "SendGrid API Key",
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas",
"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.",
"Custom URL": "Custom URL",
"customUrlDescription": "Will be used as the clickable URL instead of the monitor's one.",
"OneChatAccessToken": "OneChat Access Token",
Expand Down
23 changes: 23 additions & 0 deletions src/serviceWorker.ts
Original file line number Diff line number Diff line change
@@ -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