Nya notifikations alternativ

This commit is contained in:
2026-06-16 21:37:11 +02:00
parent 8491fe1386
commit 125fddfb1b
8 changed files with 395 additions and 78 deletions
+24
View File
@@ -1,4 +1,28 @@
[
{
"id": "1780770465729-72o2",
"timestamp": "2026-06-06T18:27:45.730Z",
"provider": "cpanel",
"operation": "GET /execute/DomainInfo/list_domains",
"method": "GET",
"url": "https://cpsrv32.misshosting.com/execute/DomainInfo/list_domains",
"status": 200,
"latency": 18,
"ok": true,
"error": null
},
{
"id": "1780770465709-j66b",
"timestamp": "2026-06-06T18:27:45.709Z",
"provider": "cpanel",
"operation": "GET /execute/DNS/list_zones",
"method": "GET",
"url": "https://cpsrv32.misshosting.com/execute/DNS/list_zones",
"status": 200,
"latency": 82,
"ok": true,
"error": null
},
{
"id": "1780397326074-s7tw",
"timestamp": "2026-06-02T10:48:46.074Z",
+10
View File
@@ -14,6 +14,7 @@
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"node-schedule": "^2.1.1",
"nodemailer": "^6.9.14",
"xml2js": "^0.6.2",
"xmlbuilder2": "^3.1.1"
},
@@ -1008,6 +1009,15 @@
"node": ">=6"
}
},
"node_modules/nodemailer": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": {
"version": "3.1.14",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+1
View File
@@ -11,6 +11,7 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"bcryptjs": "^2.4.3",
"nodemailer": "^6.9.14",
"node-schedule": "^2.1.1",
"jsonwebtoken": "^9.0.2",
"xml2js": "^0.6.2",
+130 -37
View File
@@ -1,69 +1,162 @@
const settings = require('./settings');
const db = require('./db');
const settings = require('./settings');
const db = require('./db');
const nodemailer = require('nodemailer');
/**
* Send a Gotify notification if enabled and configured.
* Errors are logged but never thrown — notifications are best-effort.
*/
async function notify(title, message) {
// ─── Channel senders ──────────────────────────────────────────────────────────
async function sendGotify(title, message) {
const { gotify } = settings.get();
if (!gotify.enabled || !gotify.url || !gotify.token) return;
const base = gotify.url.replace(/\/$/, '');
try {
const res = await fetch(`${base}/message?token=${encodeURIComponent(gotify.token)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title,
message,
priority: gotify.priority ?? 5,
}),
body: JSON.stringify({ title, message, priority: gotify.priority ?? 5 }),
});
if (!res.ok) {
const text = await res.text().catch(() => '');
console.error(`[notify] Gotify error ${res.status}: ${text}`);
}
if (!res.ok) console.error(`[notify] Gotify error ${res.status}: ${await res.text().catch(() => '')}`);
} catch (err) {
console.error('[notify] Failed to send Gotify notification:', err.message);
console.error('[notify] Gotify failed:', err.message);
}
}
// ─── Convenience helpers ─────────────────────────────────────────────────────
async function sendNtfy(title, message) {
const { ntfy } = settings.get();
if (!ntfy.enabled || !ntfy.url || !ntfy.topic) return;
const base = ntfy.url.replace(/\/$/, '');
const headers = {
'Content-Type': 'text/plain',
'Title': title,
'Priority': String(ntfy.priority ?? 3),
};
if (ntfy.token) headers['Authorization'] = `Bearer ${ntfy.token}`;
try {
const res = await fetch(`${base}/${encodeURIComponent(ntfy.topic)}`, {
method: 'POST',
headers,
body: message,
});
if (!res.ok) console.error(`[notify] ntfy error ${res.status}: ${await res.text().catch(() => '')}`);
} catch (err) {
console.error('[notify] ntfy failed:', err.message);
}
}
async function sendSmtp(title, message) {
const { smtp } = settings.get();
if (!smtp.enabled || !smtp.host || !smtp.to || !smtp.from) return;
try {
const transporter = nodemailer.createTransport({
host: smtp.host,
port: Number(smtp.port) || 587,
secure: smtp.secure === true || smtp.secure === 'true',
auth: smtp.username ? { user: smtp.username, pass: smtp.password } : undefined,
});
await transporter.sendMail({
from: smtp.from,
to: smtp.to,
subject: title,
text: message,
});
} catch (err) {
console.error('[notify] SMTP failed:', err.message);
}
}
async function sendWebhook(title, message) {
const { webhook } = settings.get();
if (!webhook.enabled || !webhook.url) return;
const body = JSON.stringify({ content: `**${title}**\n${message}` });
const headers = { 'Content-Type': 'application/json' };
if (webhook.secret) headers['X-Webhook-Secret'] = webhook.secret;
try {
const res = await fetch(webhook.url, { method: 'POST', headers, body });
if (!res.ok) console.error(`[notify] Webhook error ${res.status}: ${await res.text().catch(() => '')}`);
} catch (err) {
console.error('[notify] Webhook failed:', err.message);
}
}
// ─── Broadcast to all enabled channels ───────────────────────────────────────
async function notify(title, message) {
await Promise.all([
sendGotify(title, message),
sendNtfy(title, message),
sendSmtp(title, message),
sendWebhook(title, message),
]);
}
// ─── Exported senders for test routes ────────────────────────────────────────
async function testGotify(cfg) {
const base = cfg.url.replace(/\/$/, '');
const res = await fetch(`${base}/message?token=${encodeURIComponent(cfg.token)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: '🦥 Sloth Manager — Test', message: 'Gotify notifications are working correctly.', priority: cfg.priority ?? 5 }),
});
if (!res.ok) throw new Error(`Gotify returned ${res.status}: ${await res.text().catch(() => '')}`);
}
async function testNtfy(cfg) {
const base = cfg.url.replace(/\/$/, '');
const headers = { 'Content-Type': 'text/plain', Title: '🦥 Sloth Manager — Test', Priority: String(cfg.priority ?? 3) };
if (cfg.token) headers['Authorization'] = `Bearer ${cfg.token}`;
const res = await fetch(`${base}/${encodeURIComponent(cfg.topic)}`, { method: 'POST', headers, body: 'ntfy notifications are working correctly.' });
if (!res.ok) throw new Error(`ntfy returned ${res.status}: ${await res.text().catch(() => '')}`);
}
async function testSmtp(cfg) {
const transporter = nodemailer.createTransport({
host: cfg.host,
port: Number(cfg.port) || 587,
secure: cfg.secure === true || cfg.secure === 'true',
auth: cfg.username ? { user: cfg.username, pass: cfg.password } : undefined,
});
await transporter.verify();
await transporter.sendMail({
from: cfg.from,
to: cfg.to,
subject: '🦥 Sloth Manager — Test',
text: 'SMTP notifications are working correctly.',
});
}
async function testWebhook(cfg) {
const headers = { 'Content-Type': 'application/json' };
if (cfg.secret) headers['X-Webhook-Secret'] = cfg.secret;
const res = await fetch(cfg.url, {
method: 'POST',
headers,
body: JSON.stringify({ content: '**🦥 Sloth Manager — Test**\nWebhook notifications are working correctly.' }),
});
if (!res.ok) throw new Error(`Webhook returned ${res.status}: ${await res.text().catch(() => '')}`);
}
// ─── Event helpers ────────────────────────────────────────────────────────────
function eventEnabled(key) {
const { notifications } = settings.get();
return notifications?.[key] !== false;
return settings.get().notifications?.[key] !== false;
}
function recordAdded(provider, zoneId, record) {
if (!eventEnabled('dns_add')) return;
const zoneName = db.getZoneName(provider, zoneId);
return notify(
`DNS Record Added`,
`[${provider}] ${zoneName}\n+ ${record.type} ${record.name}${record.content}`,
);
return notify(`DNS Record Added`, `[${provider}] ${zoneName}\n+ ${record.type} ${record.name}${record.content}`);
}
function recordUpdated(provider, zoneId, oldRecord, newRecord) {
if (!eventEnabled('dns_update')) return;
const zoneName = db.getZoneName(provider, zoneId);
return notify(
`DNS Record Updated`,
`[${provider}] ${zoneName}\n${oldRecord.type} ${oldRecord.name}\n ${oldRecord.content}${newRecord.content}`,
);
return notify(`DNS Record Updated`, `[${provider}] ${zoneName}\n${oldRecord.type} ${oldRecord.name}\n ${oldRecord.content}${newRecord.content}`);
}
function recordDeleted(provider, zoneId, record) {
if (!eventEnabled('dns_delete')) return;
const zoneName = db.getZoneName(provider, zoneId);
return notify(
`DNS Record Deleted`,
`[${provider}] ${zoneName}\n ${record.type} ${record.name} ${record.content}`,
);
return notify(`DNS Record Deleted`, `[${provider}] ${zoneName}\n ${record.type} ${record.name} ${record.content}`);
}
module.exports = { notify, recordAdded, recordUpdated, recordDeleted };
module.exports = { notify, testGotify, testNtfy, testSmtp, testWebhook, recordAdded, recordUpdated, recordDeleted };
+43 -29
View File
@@ -1,7 +1,7 @@
const express = require('express');
const router = express.Router();
const settings = require('../settings');
const { notify } = require('../notify');
const { testGotify, testNtfy, testSmtp, testWebhook } = require('../notify');
// GET /api/settings
router.get('/', (req, res) => {
@@ -12,8 +12,7 @@ router.get('/', (req, res) => {
router.put('/', (req, res) => {
try {
const updated = settings.update(req.body);
// Reschedule secret check if the notification time changed
if (req.body.notifications?.secret_check_time) {
if (req.body.notifications?.secret_check_time || req.body.notifications?.timezone) {
try { require('../index').scheduleSecretCheck(); } catch { /* safe to ignore */ }
}
res.json(updated);
@@ -22,36 +21,51 @@ router.put('/', (req, res) => {
}
});
// POST /api/settings/test-notification
router.post('/test-notification', async (req, res) => {
// Use the payload from the request body so the user can test
// before saving (the frontend sends the current form values)
// POST /api/settings/test-gotify
router.post('/test-gotify', async (req, res) => {
const { url, token, priority } = req.body;
if (!url || !token) {
return res.status(400).json({ error: 'URL and token are required' });
}
const base = url.replace(/\/$/, '');
if (!url || !token) return res.status(400).json({ error: 'URL and token are required' });
try {
const response = await fetch(`${base}/message?token=${encodeURIComponent(token)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: '🦥 Sloth Manager — Test',
message: 'Gotify notifications are working correctly.',
priority: priority ?? 5,
}),
});
if (!response.ok) {
const text = await response.text().catch(() => '');
return res.status(502).json({ error: `Gotify returned ${response.status}: ${text}` });
}
await testGotify({ url, token, priority });
res.json({ success: true });
} catch (err) {
res.status(502).json({ error: `Could not reach Gotify: ${err.message}` });
res.status(502).json({ error: err.message });
}
});
// POST /api/settings/test-ntfy
router.post('/test-ntfy', async (req, res) => {
const { url, topic, token, priority } = req.body;
if (!url || !topic) return res.status(400).json({ error: 'URL and topic are required' });
try {
await testNtfy({ url, topic, token, priority });
res.json({ success: true });
} catch (err) {
res.status(502).json({ error: err.message });
}
});
// POST /api/settings/test-smtp
router.post('/test-smtp', async (req, res) => {
const { host, port, secure, username, password, from, to } = req.body;
if (!host || !from || !to) return res.status(400).json({ error: 'Host, from, and to are required' });
try {
await testSmtp({ host, port, secure, username, password, from, to });
res.json({ success: true });
} catch (err) {
res.status(502).json({ error: err.message });
}
});
// POST /api/settings/test-webhook
router.post('/test-webhook', async (req, res) => {
const { url, secret } = req.body;
if (!url) return res.status(400).json({ error: 'Webhook URL is required' });
try {
await testWebhook({ url, secret });
res.json({ success: true });
} catch (err) {
res.status(502).json({ error: err.message });
}
});
+28
View File
@@ -18,6 +18,28 @@ const DEFAULTS = {
secret_check_time: '08:00',
timezone: 'UTC',
},
ntfy: {
enabled: false,
url: 'https://ntfy.sh',
topic: '',
token: '',
priority: 3,
},
smtp: {
enabled: false,
host: '',
port: 587,
secure: false,
username: '',
password: '',
from: '',
to: '',
},
webhook: {
enabled: false,
url: '',
secret: '',
},
providerColors: {
cloudflare: '#f6821f',
loopia: '#2ecc71',
@@ -36,6 +58,9 @@ function load() {
...raw,
gotify: { ...DEFAULTS.gotify, ...(raw.gotify ?? {}) },
notifications: { ...DEFAULTS.notifications, ...(raw.notifications ?? {}) },
ntfy: { ...DEFAULTS.ntfy, ...(raw.ntfy ?? {}) },
smtp: { ...DEFAULTS.smtp, ...(raw.smtp ?? {}) },
webhook: { ...DEFAULTS.webhook, ...(raw.webhook ?? {}) },
providerColors: { ...DEFAULTS.providerColors, ...(raw.providerColors ?? {}) },
};
} catch {
@@ -58,6 +83,9 @@ function update(partial) {
...partial,
gotify: { ...current.gotify, ...(partial.gotify ?? {}) },
notifications: { ...current.notifications, ...(partial.notifications ?? {}) },
ntfy: { ...current.ntfy, ...(partial.ntfy ?? {}) },
smtp: { ...current.smtp, ...(partial.smtp ?? {}) },
webhook: { ...current.webhook, ...(partial.webhook ?? {}) },
providerColors: { ...current.providerColors, ...(partial.providerColors ?? {}) },
};
save(merged);