Nya notifikations alternativ
This commit is contained in:
@@ -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",
|
||||
|
||||
Generated
+10
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user