Merge branch 'master' into fix-issue-1272

This commit is contained in:
Frank Elsinga 2024-10-20 15:20:51 +02:00 committed by GitHub
commit 6008da0d56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1148 additions and 67 deletions

988
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -154,6 +154,7 @@
"@fortawesome/vue-fontawesome": "~3.0.0-5", "@fortawesome/vue-fontawesome": "~3.0.0-5",
"@playwright/test": "~1.39.0", "@playwright/test": "~1.39.0",
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"@testcontainers/hivemq": "^10.13.1",
"@types/bootstrap": "~5.1.9", "@types/bootstrap": "~5.1.9",
"@types/node": "^20.8.6", "@types/node": "^20.8.6",
"@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/eslint-plugin": "^6.7.5",
@ -189,6 +190,7 @@
"stylelint-config-standard": "~25.0.0", "stylelint-config-standard": "~25.0.0",
"terser": "~5.15.0", "terser": "~5.15.0",
"test": "~3.3.0", "test": "~3.3.0",
"testcontainers": "^10.13.1",
"typescript": "~4.4.4", "typescript": "~4.4.4",
"v-pagination-3": "~0.1.7", "v-pagination-3": "~0.1.7",
"vite": "~5.2.8", "vite": "~5.2.8",

View file

@ -0,0 +1,65 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SendGrid extends NotificationProvider {
name = "SendGrid";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${notification.sendgridApiKey}`,
},
};
let personalizations = {
to: [{ email: notification.sendgridToEmail }],
};
// Add CC recipients if provided
if (notification.sendgridCcEmail) {
personalizations.cc = notification.sendgridCcEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}
// Add BCC recipients if provided
if (notification.sendgridBccEmail) {
personalizations.bcc = notification.sendgridBccEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}
let data = {
personalizations: [ personalizations ],
from: { email: notification.sendgridFromEmail.trim() },
subject:
notification.sendgridSubject ||
"Notification from Your Uptime Kuma",
content: [
{
type: "text/plain",
value: msg,
},
],
};
await axios.post(
"https://api.sendgrid.com/v3/mail/send",
data,
config
);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SendGrid;

View file

@ -68,6 +68,7 @@ const GtxMessaging = require("./notification-providers/gtx-messaging");
const Cellsynt = require("./notification-providers/cellsynt"); const Cellsynt = require("./notification-providers/cellsynt");
const Onesender = require("./notification-providers/onesender"); const Onesender = require("./notification-providers/onesender");
const Wpush = require("./notification-providers/wpush"); const Wpush = require("./notification-providers/wpush");
const SendGrid = require("./notification-providers/send-grid");
class Notification { class Notification {
@ -153,6 +154,7 @@ class Notification {
new GtxMessaging(), new GtxMessaging(),
new Cellsynt(), new Cellsynt(),
new Wpush(), new Wpush(),
new SendGrid()
]; ];
for (let item of list) { for (let item of list) {
if (! item.name) { if (! item.name) {

View file

@ -165,6 +165,7 @@ export default {
"whapi": "WhatsApp (Whapi)", "whapi": "WhatsApp (Whapi)",
"gtxmessaging": "GtxMessaging", "gtxmessaging": "GtxMessaging",
"Cellsynt": "Cellsynt", "Cellsynt": "Cellsynt",
"SendGrid": "SendGrid"
}; };
// 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

View file

@ -0,0 +1,47 @@
<template>
<div class="mb-3">
<label for="sendgrid-api-key" class="form-label">{{ $t("SendGrid API Key") }}</label>
<HiddenInput id="push-api-key" v-model="$parent.notification.sendgridApiKey" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="sendgrid-from-email" class="form-label">{{ $t("From Email") }}</label>
<input id="sendgrid-from-email" v-model="$parent.notification.sendgridFromEmail" type="email" class="form-control" required>
</div>
<div class="mb-3">
<label for="sendgrid-to-email" class="form-label">{{ $t("To Email") }}</label>
<input id="sendgrid-to-email" v-model="$parent.notification.sendgridToEmail" type="email" class="form-control" required>
</div>
<div class="mb-3">
<label for="sendgrid-cc-email" class="form-label">{{ $t("smtpCC") }}</label>
<input id="sendgrid-cc-email" v-model="$parent.notification.sendgridCcEmail" type="email" class="form-control">
<div class="form-text">{{ $t("Separate multiple email addresses with commas") }}</div>
</div>
<div class="mb-3">
<label for="sendgrid-bcc-email" class="form-label">{{ $t("smtpBCC") }}</label>
<input id="sendgrid-bcc-email" v-model="$parent.notification.sendgridBccEmail" type="email" class="form-control">
<small class="form-text text-muted">{{ $t("Separate multiple email addresses with commas") }}</small>
</div>
<div class="mb-3">
<label for="sendgrid-subject" class="form-label">{{ $t("Subject:") }}</label>
<input id="sendgrid-subject" v-model="$parent.notification.sendgridSubject" type="text" class="form-control">
<small class="form-text text-muted">{{ $t("leave blank for default subject") }}</small>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://docs.sendgrid.com/api-reference/mail-send/mail-send" target="_blank">https://docs.sendgrid.com/api-reference/mail-send/mail-send</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
mounted() {
if (typeof this.$parent.notification.sendgridSubject === "undefined") {
this.$parent.notification.sendgridSubject = "Notification from Your Uptime Kuma";
}
},
};
</script>

View file

@ -66,6 +66,7 @@ import Whapi from "./Whapi.vue";
import Cellsynt from "./Cellsynt.vue"; import Cellsynt from "./Cellsynt.vue";
import WPush from "./WPush.vue"; import WPush from "./WPush.vue";
import SIGNL4 from "./SIGNL4.vue"; import SIGNL4 from "./SIGNL4.vue";
import SendGrid from "./SendGrid.vue";
/** /**
* Manage all notification form. * Manage all notification form.
@ -139,7 +140,8 @@ const NotificationFormList = {
"whapi": Whapi, "whapi": Whapi,
"gtxmessaging": GtxMessaging, "gtxmessaging": GtxMessaging,
"Cellsynt": Cellsynt, "Cellsynt": Cellsynt,
"WPush": WPush "WPush": WPush,
"SendGrid": SendGrid,
}; };
export default NotificationFormList; export default NotificationFormList;

View file

@ -1051,5 +1051,7 @@
"From":"From", "From":"From",
"Can be found on:": "Can be found on: {0}", "Can be found on:": "Can be found on: {0}",
"The phone number of the recipient in E.164 format.": "The phone number of the recipient in E.164 format.", "The phone number of the recipient in E.164 format.": "The phone number of the recipient in E.164 format.",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.":"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies." "Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.":"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.",
"SendGrid API Key": "SendGrid API Key",
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas"
} }

View file

@ -0,0 +1,102 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { HiveMQContainer } = require("@testcontainers/hivemq");
const mqtt = require("mqtt");
const { MqttMonitorType } = require("../../server/monitor-types/mqtt");
const { UP, PENDING } = require("../../src/util");
/**
* Runs an MQTT test with the
* @param {string} mqttSuccessMessage the message that the monitor expects
* @param {null|"keyword"|"json-query"} mqttCheckType the type of check we perform
* @param {string} receivedMessage what message is recieved from the mqtt channel
* @returns {Promise<Heartbeat>} the heartbeat produced by the check
*/
async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) {
const hiveMQContainer = await new HiveMQContainer().start();
const connectionString = hiveMQContainer.getConnectionString();
const mqttMonitorType = new MqttMonitorType();
const monitor = {
jsonPath: "firstProp", // always return firstProp for the json-query monitor
hostname: connectionString.split(":", 2).join(":"),
mqttTopic: "test",
port: connectionString.split(":")[2],
mqttUsername: null,
mqttPassword: null,
interval: 20, // controls the timeout
mqttSuccessMessage: mqttSuccessMessage, // for keywords
expectedValue: mqttSuccessMessage, // for json-query
mqttCheckType: mqttCheckType,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const testMqttClient = mqtt.connect(hiveMQContainer.getConnectionString());
testMqttClient.on("connect", () => {
testMqttClient.subscribe("test", (error) => {
if (!error) {
testMqttClient.publish("test", receivedMessage);
}
});
});
try {
await mqttMonitorType.check(monitor, heartbeat, {});
} finally {
testMqttClient.end();
hiveMQContainer.stop();
}
return heartbeat;
}
describe("MqttMonitorType", {
concurrency: true,
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64")
}, () => {
test("valid keywords (type=default)", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
test("valid keywords (type=keyword)", async () => {
const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
test("invalid keywords (type=default)", async () => {
await assert.rejects(
testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
);
});
test("invalid keyword (type=keyword)", async () => {
await assert.rejects(
testMqtt("NOT_PRESENT", "keyword", "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
);
});
test("valid json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Message received, expected value is found");
});
test("invalid (because query fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects(
testMqtt("[not_relevant]", "json-query", "{}"),
new Error("Message received but value is not equal to expected value, value was: [undefined]"),
);
});
test("invalid (because successMessage fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects(
testMqtt("[wrong_success_messsage]", "json-query", "{\"firstProp\":\"present\"}"),
new Error("Message received but value is not equal to expected value, value was: [present]")
);
});
});