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)
110 lines
3.1 KiB
JavaScript
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
|
|
};
|