mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat(notifications): add ntfy as a first-class notification channel
Adds ntfy.sh (and self-hosted instances) as a new push notification channel with full parity to the existing webhook channel. - Backend: NtfyConfig type, getUserNtfyConfig, getAdminNtfyConfig, resolveNtfyUrl, sendNtfy (header-based API with Title/Priority/Tags/ Click headers), testNtfy, NTFY_EVENT_META (priority + emoji tags per event), SSRF guard via existing checkSsrf + createPinnedDispatcher - notificationPreferencesService: ntfy added to NotifChannel union, IMPLEMENTED_COMBOS, getActiveChannels parser, getAvailableChannels, ADMIN_GLOBAL_CHANNELS, and AvailableChannels interface - notificationService: per-user ntfy dispatch after webhook block; admin-scoped ntfy via getAdminGlobalPref for version_available events - Routes: POST /api/notifications/test-ntfy with saved-token fallback - authService: admin_ntfy_server/topic/token in ADMIN_SETTINGS_KEYS, masked + encrypted on read/write - settingsService: ntfy_token added to ENCRYPTED_SETTING_KEYS - Frontend: ntfy topic/server/token inputs + Save/Test/Clear buttons in NotificationsTab; admin Ntfy panel in AdminPage; testNtfy API method - i18n: full English strings; English placeholders in 14 other locales - Tests: resolveNtfyUrl, sendNtfy, dispatch integration, UI tests, MSW handler for test-ntfy endpoint
This commit is contained in:
@@ -7,15 +7,20 @@ import {
|
||||
isSmtpConfigured,
|
||||
ADMIN_SCOPED_EVENTS,
|
||||
type NotifEventType,
|
||||
type NotifChannel,
|
||||
} from './notificationPreferencesService';
|
||||
import {
|
||||
getEventText,
|
||||
sendEmail,
|
||||
sendWebhook,
|
||||
sendNtfy,
|
||||
getUserEmail,
|
||||
getUserLanguage,
|
||||
getUserWebhookUrl,
|
||||
getAdminWebhookUrl,
|
||||
getUserNtfyConfig,
|
||||
getAdminNtfyConfig,
|
||||
resolveNtfyUrl,
|
||||
getAppUrl,
|
||||
} from './notifications';
|
||||
import {
|
||||
@@ -270,6 +275,19 @@ export async function send(payload: NotificationPayload): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ntfy (per-user) — skip for admin-scoped events (handled globally below) ──
|
||||
if (!ADMIN_SCOPED_EVENTS.has(event) && activeChannels.includes('ntfy') && isEnabledForEvent(recipientId, event, 'ntfy' as NotifChannel)) {
|
||||
const userNtfyCfg = getUserNtfyConfig(recipientId);
|
||||
const adminNtfyCfg = getAdminNtfyConfig();
|
||||
const ntfyUrl = resolveNtfyUrl(adminNtfyCfg, userNtfyCfg);
|
||||
if (ntfyUrl) {
|
||||
const lang = getUserLanguage(recipientId);
|
||||
const { title, body } = getEventText(lang, event, params);
|
||||
const token = userNtfyCfg?.token ?? adminNtfyCfg.token;
|
||||
promises.push(sendNtfy(ntfyUrl, token, { event, title, body, link: fullLink }));
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
for (const result of results) {
|
||||
if (result.status === 'rejected') {
|
||||
@@ -288,4 +306,16 @@ export async function send(payload: NotificationPayload): Promise<void> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Admin ntfy (scope: admin) — global, respects global pref ─────────
|
||||
if (scope === 'admin' && getAdminGlobalPref(event, 'ntfy')) {
|
||||
const adminNtfyCfg = getAdminNtfyConfig();
|
||||
const adminNtfyUrl = resolveNtfyUrl(adminNtfyCfg, null);
|
||||
if (adminNtfyUrl) {
|
||||
const { title, body } = getEventText('en', event, params);
|
||||
await sendNtfy(adminNtfyUrl, adminNtfyCfg.token, { event, title, body, link: fullLink }).catch((err: unknown) => {
|
||||
logError(`notificationService.send admin ntfy failed event=${event}: ${err instanceof Error ? err.message : err}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user