pediatric-ai-scribe-v3/src/utils/notify.js
Daniel d0d65446f6 Add biometric auth, ntfy push notifications, mobile improvements
Mobile:
- Add biometric authentication (Face ID/Touch ID/fingerprint) on app launch
  with PIN/password fallback, auto-prompts on launch, skip option
- Add @aparajita/capacitor-biometric-auth plugin

Backend:
- Add ntfy push notification support (src/utils/notify.js)
  Self-hosted, no Firebase dependency, uses user's existing ntfy instance
- Notifications for: new login, password changed, new registration (admin)
- Topic format: pedscribe-user-{id} for users, pedscribe-admin for admins
- Env: NTFY_URL, NTFY_TOKEN (optional)
2026-04-11 02:35:07 +02:00

110 lines
3.1 KiB
JavaScript

// ============================================================
// 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
};