// ============================================================ // NOTIFY — Push notifications via ntfy (self-hosted) // Sends notifications to user-specific ntfy topics // Topic format: pedscribe-{userId} (user subscribes in ntfy app) // ============================================================ var NTFY_URL = process.env.NTFY_URL; var NTFY_TOKEN = process.env.NTFY_TOKEN; // Send a push notification to a user async function notifyUser(userId, title, message, opts) { if (!NTFY_URL) return; opts = opts || {}; var topic = 'pedscribe-user-' + userId; var headers = { 'Title': title, 'Priority': opts.priority || '3', // 1=min, 3=default, 5=urgent 'Tags': opts.tags || 'stethoscope' }; if (opts.click) headers['Click'] = opts.click; if (opts.actions) headers['Actions'] = opts.actions; if (NTFY_TOKEN) headers['Authorization'] = 'Bearer ' + NTFY_TOKEN; try { await fetch(NTFY_URL + '/' + topic, { method: 'POST', headers: headers, body: message }); } catch (e) { console.warn('[Notify] ntfy send failed:', e.message); } } // Send notification to admin topic async function notifyAdmin(title, message, opts) { if (!NTFY_URL) return; opts = opts || {}; var headers = { 'Title': title, 'Priority': opts.priority || '3', 'Tags': opts.tags || 'shield' }; if (NTFY_TOKEN) headers['Authorization'] = 'Bearer ' + NTFY_TOKEN; try { await fetch(NTFY_URL + '/pedscribe-admin', { method: 'POST', headers: headers, body: message }); } catch (e) { console.warn('[Notify] ntfy admin send failed:', e.message); } } // ── Notification helpers for specific events ── // New login from unknown device function notifyNewLogin(userId, deviceLabel, ip) { notifyUser(userId, 'New Login Detected', 'New sign-in from ' + deviceLabel + ' (' + ip + '). If this was not you, change your password immediately.', { priority: '4', tags: 'warning', click: '/settings' } ); } // Password changed function notifyPasswordChanged(userId) { notifyUser(userId, 'Password Changed', 'Your password was changed. All other sessions have been logged out.', { priority: '4', tags: 'lock' } ); } // Encounter expiring soon (called from cleanup job) function notifyEncounterExpiring(userId, label, hoursLeft) { notifyUser(userId, 'Encounter Expiring', 'Your encounter "' + (label || 'Unlabeled') + '" will be deleted in ' + hoursLeft + ' hours. Save or export it now.', { priority: '3', tags: 'clock' } ); } // New user registered (admin notification) function notifyNewRegistration(email, name) { notifyAdmin('New User Registered', name + ' (' + email + ') has registered.', { priority: '3', tags: 'new' } ); } // Failed login attempts (admin notification) function notifyFailedLogins(email, count, ip) { notifyAdmin('Failed Login Attempts', count + ' failed login attempts for ' + email + ' from ' + ip, { priority: '4', tags: 'warning' } ); } module.exports = { notifyUser, notifyAdmin, notifyNewLogin, notifyPasswordChanged, notifyEncounterExpiring, notifyNewRegistration, notifyFailedLogins };